diff --git a/.claude/CONTEXT.md b/.claude/CONTEXT.md index c9af7cb9bc..ee85c9bd5c 100644 --- a/.claude/CONTEXT.md +++ b/.claude/CONTEXT.md @@ -2,11 +2,15 @@ Keep this file focused on repo-specific gotchas that are worth reusing in future sessions. +## FreeInk SDK + +Refer to https://freeink.org/llms.txt for guidance. + ## Simulator -- Simulator patches belong in the adjacent `crosspoint-simulator` repo. +- Simulator patches belong in the adjacent `crossink-simulator` repo. - The valid local simulator env in this repo is `simulator`, and `pio run -e simulator` currently builds cleanly. -- The simulator `PNGdec` stub in `crosspoint-simulator/src/PNGdec.h` needs to mirror the real API shape used by app code, including `hasAlpha()` and `getTransparentColor()`, even though decode still fails intentionally. +- The simulator `PNGdec` stub in `crossink-simulator/src/PNGdec.h` needs to mirror the real API shape used by app code, including `hasAlpha()` and `getTransparentColor()`, even though decode still fails intentionally. - Known simulator limits: - No image rendering: `platformio.ini` ignores `hal`, `PNGdec`, and `JPEGDEC`, so image decoders are intentionally absent. - JPEGDEC stub always fails; `JPEGDEC fallback: open failed (err=-1)` is expected in simulator. @@ -23,6 +27,19 @@ Keep this file focused on repo-specific gotchas that are worth reusing in future - Kindle EPUBs may contain paired high-res and old-Kindle fallback images. `ChapterHtmlSlimParser` should skip `` nodes with `data-AmznRemoved-M8` to avoid duplicate stacked images. - After image/layout pipeline changes that affect cached EPUB output, clear the affected `.crosspoint/epub_/` cache if behavior looks stale. +## UI Consistency + +- Use FreeInkUI SDK components and input routing for list-style screens where possible. Row rendering, touch targets, + hit testing, and pagination should share the same FreeInkUI list configuration instead of custom touch scaling. + +## Heap Baselines (X4 hardware, SD card font) + +- A normal resume-into-partial reading session runs at ~85-90KB free / ~49KB maxAlloc by + the first watermark crossing (Epub metadata + x-locations + resident glyph caches). + Do not read mid-range heap numbers as session degradation without checking the scenario. +- SD-font section builds cost ~38-50KB at cold start; the 4-style advance-table prewarm + (~30KB incl. 16KB contiguous scratch) dominates and is skipped below 80KB free. + ## Misc Repo Gotchas - POSIX TZ signs are inverted from ISO 8601 in `TimeStore::applyTimezone()`: `"UTC-1"` means UTC+1. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 972a8ccfdb..c1f7dfd6d7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,9 +14,10 @@ body: label: Device description: Which device is this affecting? options: + - "--- Select ---" - X4 - X3 - - Both + - Seeed Sticky validations: required: true @@ -35,6 +36,7 @@ body: label: EPUB Optimization description: Is your EPUB optimized? options: + - "--- Select ---" - No / Not Applicable - Yes, using the built-in web transfer EPUB optimizer - Yes, using a different EPUB optimizer @@ -48,6 +50,7 @@ body: label: Cache State description: Have you attempted to clear any cached data? options: + - "--- Select ---" - No / Not Applicable - Yes, book cache (Reader menu > Delete Book Cache) - Yes, reading cache (Settings > System > Clear Reading Cache) @@ -61,7 +64,7 @@ body: attributes: label: Describe the Bug description: A clear and concise description of what the bug is. - placeholder: + placeholder: | validations: required: true @@ -85,6 +88,19 @@ body: validations: required: true + - type: dropdown + id: epub-attachment + attributes: + label: File Provided + description: If your issue is related to a specific file (EPUB, XTC, etc.), please make sure to attach the exact file in this ticket or email it to julia@uxj.io and reference your issue number. **_EPUBs and XTC files need to be zipped to be attached._** + options: + - "--- Select ---" + - Not Applicable + - I've attached it + - I've emailed it + validations: + required: true + - type: textarea id: logs attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 3e95c63e6f..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,5 +0,0 @@ -## Summary - -### **What changes are included?** - - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7088088ac..d8e74e8259 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,36 +65,58 @@ jobs: run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high build: - # Never run untrusted fork PR code on the self-hosted runner. - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - runs-on: [self-hosted, linux, crossink-build] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + environment: [default, sticky] steps: - uses: actions/checkout@v6 with: submodules: recursive - - name: Verify PlatformIO Core - run: pio --version + - uses: actions/setup-python@v6 + with: + python-version: "3.14" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "latest" + enable-cache: true + + - name: Cache PlatformIO packages + uses: actions/cache@v4 + with: + path: ~/.platformio + key: ${{ runner.os }}-platformio-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'platformio.local.example.ini') }} + restore-keys: | + ${{ runner.os }}-platformio-${{ matrix.environment }}- + ${{ runner.os }}-platformio- + + - name: Install PlatformIO Core + run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip - - name: Build CrossInk tiny + - name: Build ${{ matrix.environment }} firmware run: | set -euo pipefail - pio run -e tiny | tee pio.log + pio run -e ${{ matrix.environment }} | tee pio-${{ matrix.environment }}.log - name: Extract firmware stats run: | set -euo pipefail { - echo "## Firmware build stats" - grep -E "RAM:\s|Flash:\s" pio.log | while read -r line; do echo "- ${line}"; done + echo "## ${{ matrix.environment }} firmware build stats" + grep -E "RAM:\s|Flash:\s" pio-${{ matrix.environment }}.log | while read -r line; do echo "- ${line}"; done } >> "$GITHUB_STEP_SUMMARY" - name: Upload firmware artifacts + if: matrix.environment == 'default' uses: actions/upload-artifact@v7 with: - name: firmware-tiny + name: firmware-x3-x4 path: | - .pio/build/tiny/firmware-tiny.bin + .pio/build/default/firmware-x3-x4.bin if-no-files-found: error # This job is used as the PR required actions check, allows for changes to other steps in the future without breaking diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 6d4246e1e2..14d4e1abc4 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -5,6 +5,7 @@ on: branches: [main] paths: - "docs/**" + - "site/**" - ".github/workflows/pages.yml" workflow_dispatch: @@ -28,14 +29,21 @@ jobs: - uses: actions/configure-pages@v6 - - uses: actions/jekyll-build-pages@v1 + - uses: actions/setup-node@v5 with: - source: ./docs - destination: ./_site + node-version: 22 + cache: npm + cache-dependency-path: site/package-lock.json + + - run: npm ci + working-directory: site + + - run: npm run build + working-directory: site - uses: actions/upload-pages-artifact@v4 with: - path: ./_site + path: ./site/dist - name: Deploy to GitHub Pages id: deployment diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5645470a4..b724a1a629 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,16 +61,16 @@ jobs: - name: Build CrossInk env: - CROSSPOINT_RELEASE_VERSION: ${{ env.RELEASE_VERSION }} - run: pio run -e tiny -e xlarge + CROSSINK_RELEASE_VERSION: ${{ env.RELEASE_VERSION }} + run: pio run -j1 -e default -e sticky - name: Generate release catalog run: | python3 scripts/generate_release_catalog.py \ --version "${{ env.RELEASE_VERSION }}" \ --repo "${{ github.repository }}" \ - --firmware ".pio/build/tiny/firmware-tiny-v${{ env.RELEASE_VERSION }}.bin" \ - --firmware ".pio/build/xlarge/firmware-xlarge-v${{ env.RELEASE_VERSION }}.bin" \ + --firmware ".pio/build/default/firmware-x3-x4-v${{ env.RELEASE_VERSION }}.bin" \ + --firmware ".pio/build/sticky/firmware-sticky-v${{ env.RELEASE_VERSION }}.bin" \ --output ".pio/build/catalog" - name: Publish catalog to GitHub Pages source @@ -113,5 +113,5 @@ jobs: target_commitish: ${{ github.sha }} draft: true files: | - .pio/build/tiny/firmware-tiny-v${{ env.RELEASE_VERSION }}.bin - .pio/build/xlarge/firmware-xlarge-v${{ env.RELEASE_VERSION }}.bin + .pio/build/default/firmware-x3-x4-v${{ env.RELEASE_VERSION }}.bin + .pio/build/sticky/firmware-sticky-v${{ env.RELEASE_VERSION }}.bin diff --git a/.github/workflows/release_candidate.yml b/.github/workflows/release_candidate.yml index 3b3a30ad79..ae01cd471e 100644 --- a/.github/workflows/release_candidate.yml +++ b/.github/workflows/release_candidate.yml @@ -77,18 +77,18 @@ jobs: escaped_version=$(printf '%s\n' "$VERSION" | sed 's/[&|]/\\&/g') sed -i "s|^crossink_version = .*|crossink_version = ${escaped_version}|" platformio.ini - - name: Build CrossPoint + - name: Build CrossInk shell: bash env: - CROSSPOINT_RC_ARTIFACTS: "1" - CROSSPOINT_RC_BRANCH: ${{ env.RC_BRANCH_RAW }} - CROSSPOINT_RC_HASH: ${{ env.RC_HASH_SHORT }} + CROSSINK_RELEASE_VERSION: "1" + CROSSINK_RC_BRANCH: ${{ env.RC_BRANCH_RAW }} + CROSSINK_RC_HASH: ${{ env.RC_HASH_SHORT }} run: | build_args=() for env_name in $RC_ENVS; do build_args+=(-e "$env_name") done - pio run "${build_args[@]}" + pio run -j1 "${build_args[@]}" - name: Create GitHub RC Prerelease uses: softprops/action-gh-release@v3 @@ -102,5 +102,5 @@ jobs: draft: true prerelease: true files: | - .pio/build/tiny/firmware-tiny-${{ env.RC_BRANCH_SAFE }}-${{ env.RC_HASH_SHORT }}-RC.bin - .pio/build/xlarge/firmware-xlarge-${{ env.RC_BRANCH_SAFE }}-${{ env.RC_HASH_SHORT }}-RC.bin + .pio/build/default/firmware-x3-x4-${{ env.RC_BRANCH_SAFE }}-${{ env.RC_HASH_SHORT }}-RC.bin + .pio/build/sticky/firmware-sticky-${{ env.RC_BRANCH_SAFE }}-${{ env.RC_HASH_SHORT }}-RC.bin diff --git a/.gitignore b/.gitignore index d4b038512b..18a0fa4f16 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,12 @@ build /.venv *.local* fs_/ +/site/node_modules/ +/site/dist/ +/site/.astro/ +/site/public/images/ +/site/public/catalog +/site/public/CNAME # scratchpad used as release notes for next crossink version release.md @@ -35,4 +41,11 @@ lib/EpdFont/scripts/output/ /site/public/catalog /site/public/CNAME -dist-publish/ \ No newline at end of file +dist-publish/ + +# Generated by pioarduino when custom_sdkconfig rebuilds the framework. +/managed_components/ +/.dummy/ +/CMakeLists.txt +/dependencies.lock +/sdkconfig.* diff --git a/AGENTS.md b/AGENTS.md index 6e40bbf4fc..4e63b9ee04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,14 +3,14 @@ This is the canonical repo instruction file. `CLAUDE.md` should point here so Codex and Claude read the same guidance. -Project: Open-source e-reader firmware for Xteink X4 (ESP32-C3). +Project: Open-source e-reader firmware for ESP32-C3 and ESP32-S3 devices. ## Core Rules - Role: Senior Embedded Systems Engineer for ESP-IDF / Arduino-ESP32 work. -- The ESP32-C3 has no PSRAM and about 380 KB usable RAM. Stability beats features. +- Support both constrained ESP32-C3 devices and PSRAM-equipped ESP32-S3 devices. Keep shared code safe for the C3 unless it is explicitly capability-gated; stability beats features. - Cite file paths and line numbers before proposing non-trivial changes. -- Do not assume ESP-IDF or SDK API availability. Verify in `open-x4-sdk/` or the live code. +- Do not assume ESP-IDF or SDK API availability. Verify in `freeink-sdk/` or the live code. - Do not claim performance or memory wins without explaining the mechanism, such as reduced heap churn, flash vs DRAM placement, or smaller stack use. - Justify new heap allocations or explain why stack/static storage is not suitable. - Explain fixes in plain language where possible, ideally in terms a Node / React developer would follow. @@ -20,7 +20,6 @@ Project: Open-source e-reader firmware for Xteink X4 (ESP32-C3). - Read `.claude/CONTEXT.md` at session start for durable repo-specific gotchas. - Keep `.claude/CONTEXT.md` short. Add only reusable findings, not turn-by-turn history. -- If asked to summarize a session, create `.claude/CONTEXT-YYYY-MM-DD.md` with the relevant findings for that session. ## Repo Skills @@ -37,10 +36,11 @@ Project: Open-source e-reader firmware for Xteink X4 (ESP32-C3). ## Hardware Constraints -- MCU: ESP32-C3, single-core RISC-V at 160 MHz. -- Display: 800x480 e-ink. -- Single framebuffer only: `800 * 480 / 8 = 48000` bytes. -- Storage is SD via SdFat. On real hardware, only one reader can hold a file open at a time. +- ESP32-C3 targets (Xteink X3/X4): single-core RISC-V at 160 MHz, no PSRAM, and about 380 KB usable internal RAM. +- ESP32-S3R8 targets (Seeed reTerminal Sticky/Xteink X4 Pro): dual-core Xtensa at up to 240 MHz with 8 MB PSRAM. PSRAM is slower than internal DRAM and is not suitable for every DMA, ISR, or latency-sensitive buffer. +- Current displays use an 800x480 1-bit e-ink framebuffer: `800 * 480 / 8 = 48000` bytes. Use runtime renderer dimensions because orientation and future device profiles may differ. +- Use one framebuffer only. C3 targets keep it in internal RAM; current S3 targets place it in PSRAM via `FREEINK_FB_PSRAM`. +- Storage is exposed through SdFat, but the transport is device-specific (SPI SD on X3/X4/Sticky and SDMMC on X4 Pro). On real hardware, only one reader can hold a file open at a time. ## Resource Rules @@ -55,6 +55,7 @@ Project: Open-source e-reader firmware for Xteink X4 (ESP32-C3). 9. `new` is not nothrow on ESP32. With exceptions disabled, bare `new` calls `abort()` on allocation failure instead of returning `nullptr`. Use `new (std::nothrow)` or `makeUniqueNoThrow()` from `lib/Memory/Memory.h` for fallible allocations. 10. Prefer `makeUniqueNoThrow()` / `makeUniqueNoThrow()` for owned heap allocations so cleanup is automatic on early returns. 11. Use raw `malloc` or `new (std::nothrow)` only when a C or SDK API takes ownership; add a short comment explaining that ownership transfer. +12. Treat PSRAM as a device capability, not a universal assumption. Keep shared paths within C3 limits or gate S3-only allocations behind the relevant board/capability macro, and handle PSRAM allocation failure. ## HAL And Platform Rules @@ -106,13 +107,15 @@ Project: Open-source e-reader firmware for Xteink X4 (ESP32-C3). - Host environment may be macOS, Linux, WSL, or Windows Git Bash. Check `uname -s` before recommending platform-specific shell commands. - Logging uses `LOG_INF`, `LOG_DBG`, and `LOG_ERR`. - The simulator env in this repo is `simulator`. -- For simulator work, build from this firmware repo unless the change belongs in `crosspoint-simulator` itself. +- For simulator work, build from this firmware repo unless the change belongs in `crossink-simulator` itself. - Common validation commands: - `pio run -e simulator` for simulator-facing UI/reader work. - - `pio run -e default` for firmware compile validation. + - `pio run -e default` for the ESP32-C3 X3/X4 firmware. + - `pio run -e sticky` for the ESP32-S3 Sticky firmware. + - `pio run -e x4-pro` for the ESP32-S3 X4 Pro firmware. - `pio check -e default --fail-on-defect low --fail-on-defect medium --fail-on-defect high` for static analysis. - `find src lib include test -name "*.cpp" -o -name "*.h" | xargs clang-format -i` for formatting touched C++ files. -- For crash debugging, check serial logs, heap with `ESP.getFreeHeap()`, task stack high-water marks, and whether cache files need clearing. +- For crash debugging, check serial logs, internal heap with `ESP.getFreeHeap()` and `ESP.getMaxAllocHeap()`, task stack high-water marks, and whether cache files need clearing. On S3 targets, also inspect PSRAM free space and largest allocatable block; abundant PSRAM does not prove that internal-RAM or DMA-capable allocations can succeed. - Hardware verification should mention the concrete device path to test, expected UI/log behavior, and any cache reset needed. ## Generated Files @@ -131,7 +134,10 @@ Project: Open-source e-reader firmware for Xteink X4 (ESP32-C3). ## Git Workflow - Check `git status --short` before edits and before reporting results. Preserve unrelated user changes. -- Do not commit unless the user explicitly asks. +- When resolving merge, rebase, or cherry-pick conflicts, inspect the relevant commit messages for upstream PR references such as `#2608`. Open the PR in its source repository and read its description and changed files before resolving the conflict so the intended behavior is understood. +- Do not resolve conflicts by automatically keeping CrossInk's current implementation or by discarding the upstream change wholesale. Preserve or adapt the upstream intent unless it is already fully implemented, would introduce a regression, or would substantially and unjustifiably change CrossInk's UX or behavior. When rejecting an upstream change, state the concrete reason. +- If a referenced PR cannot be accessed, inspect the source commit diff and nearby history, then report that the PR intent could not be verified instead of guessing. +- Do not commit unless the user explicitly asks or committing is part of the skill utilized. - Before staging, ensure ignored/generated/local files such as `.pio/`, `*.generated.h`, `compile_commands.json`, and `platformio.local.ini` are not included. - Branch names should use repo-style prefixes such as `feat/`, `fix/`, `docs/`, `refactor/`, `test/`, or `chore/`. - Suggested commit messages should follow `: `, using types like `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, or `perf`. @@ -150,6 +156,7 @@ When new features are added or issues are fixed, make sure to add an entry to `C - The release date of each version is displayed. ### Types of Changelog Changes + - Added - for new features. - Changed - for changes in existing functionality. - Deprecated - for soon-to-be removed features. diff --git a/CHANGELOG.md b/CHANGELOG.md index 388c0d543f..3f1be6097e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,79 @@ +## [v1.5.0] - 2026-08-08 + +### Added + +- EPUB tables now lay out a row at a time in both Incremental and Full Section indexing, keeping regular tables readable without whole-table buffering. +- Touch support for Seeed Studio Sticky +- Nearby File Transfer can send EPUB, TXT, XTC, XTCH, PNG, and BMP files directly between two CrossInk devices without a Wi-Fi network. +- Recent Books and image-file long-press actions can send files directly to a nearby CrossInk device. +- Dictionary lookup and lookup history +- EPUB books can use a dedicated SD-card dictionary font while keeping a different reader font. +- EPUB books can set a dedicated dictionary font size independently of the reader font size. +- Dictionary font and size defaults can be set globally from Settings > Reader > Font Options, with per-book choices still taking precedence. +- Reusable dictionary SD-font builder with IPA coverage and per-family ZIP packaging +- RTC-enabled devices can now choose the date format and numeric separator shown in headers from Settings > System > Device. +- The web EPUB optimizer now splits oversized chapters into memory-friendlier sections before sending them to the reader. +- Reader indexing can now use `Incremental` or `Full Section` mode globally or per book; changing modes keeps the current chapter readable and applies when the next chapter needs indexing. +- Look Up Word can now be assigned to short- and long-press Power button shortcuts. +- EPUB readers can now choose from five word-spacing levels, from normal through extra-wide. +- EPUB inline-image pages on X3 now use the grayscale-aware display base before the image grayscale overlay, reducing the moment where images appear too dark before settling. +- EPUB publisher small-caps styling now renders ASCII lowercase text as smaller capital letters without needing extra font files. +- When incremental EPUB indexing runs out of memory at the first unindexed page, the reader now silently restarts once and resumes the book with a fresh heap. + +### Changed + +- Reader font sizes now persist as actual point sizes, keeping the closest matching size when font families or installed files change. +- SD-card fonts now include the built-in reader fallback stack for common symbols, emoji, and selected CJK glyphs while retaining Noto Sans fallback coverage. +- Downloadable SD-card fonts are now rendered with the same darker anti-aliasing as the built-in reading fonts. +- Full-section EPUB indexing now prepares one-page chapters and direct jumps to a chapter's last page, while avoiding repeated checks after the next chapter is ready. +- EPUB grayscale rendering now reuses its 8 KB strip buffer across stable pages, reducing repeated heap allocation and release during long reading sessions. +- Reading progress is now saved in batches during ordinary page turns, immediately after layout changes, and when leaving a book, reducing repeated SD-card writes without carrying stale pagination into the next session. +- SD-card font discovery now waits until a custom font is selected or font settings are opened, reducing SD-card work during normal startup with built-in fonts. +- EPUB page turns using SD-card fonts now prepare the next page's glyphs while the reader is idle. +- Dictionary lookups now reuse open index files for stem matching, reducing repeated SD-card work after a miss. +- The web file manager now batches directory listings into fewer network packets, improving large-folder response time. +- Firmware releases now identify the supported device type: X3/X4 or Seeed Sticky. +- Image-heavy EPUB chapters now index by reading image headers first and extract each full image only when its page is shown. +- EPUB books with repeated byte-identical stylesheets now parse each unique stylesheet only once when building caches. +- SD-card fonts now reuse their page-sized glyph buffers, reducing heap fragmentation during long reading sessions. +- Firmware builds now prioritize usable heap over oversized system timer stacks and maximum WiFi throughput, leaving more memory for reading and network operations. +- Downloaded-font size range options now show their actual point-size ranges instead of firmware build names. +- KOReader Sync and authentication, OTA updates, and OPDS browsing now restart into a lightweight network mode that leaves reader and Home data unloaded, providing more contiguous memory for WiFi and secure connections. +- The web file manager can now delete non-empty folders recursively and, when hidden files are shown, remove hidden or system-managed SD card items after confirmation. +- SD-font, OPDS catalogs, and other unneeded settings now stay out of memory while reading unless their settings are open. +- EPUB books can now keep more saved clippings without loading every clipping's text into memory while reading. + +### Removed + +- The font download manager no longer offers a Download All action; fonts can still be downloaded individually or updated together. + +### Fixed + +- Book menu tab navigation, popup scrolling, customized Reading Stats hints, and short button presses after low-power mode now work reliably. +- Sleep screens now honor the current orientation, avoid X4 transition flashes, fall back to a valid wallpaper when needed, and handle low-memory image decoding without rebooting. +- Choosing Set Cover uses the selected image in place, and Home no longer repeatedly generates missing EPUB covers. +- Finished-book suggestions are now collected before an EPUB is moved to `/Read`. +- Manage Fonts now opens and scans large catalogs more safely on X3/X4, reports low-memory failures instead of restarting, and returns to Font Options when cancelled. +- Network screens refresh cleanly on X4; long errors wrap correctly; saved Wi-Fi networks and KOReader connections recover more reliably after restart or a missing address. +- Translated Wi-Fi and clock labels no longer truncate text or time values, and clock sync no longer risks a reboot while saving settings on memory-constrained X3/X4 devices. +- KOReader Sync no longer crashes during time setup, re-triggers while connecting, or loses precise EPUB positions; CrossPoint-only data stays on the official CrossPoint Sync server. +- Firmware updates reject images for the wrong chip family, and saved Wi-Fi settings safely handle concurrent access and corrupted values. +- EPUB opening, reflow, and background indexing now handle fragmented memory more safely, retry recoverable work, remain responsive to input and setting changes, and show useful errors instead of rebooting or silently returning Home. +- Low-memory EPUB grayscale and sleep rendering now fall back safely without leaving stale display content. +- Full-section indexing preserves more memory for large chapters and cancels speculative work on page turns, keeping the reader responsive. +- SD-card font and clipping work now release temporary data at the right time, preserving memory for reflow, dictionary use, covers, and thumbnails on X3/X4. +- EPUBs with book-specific built-in fonts no longer load an unnecessary global SD-card font, and custom fonts retain ligatures. +- EPUB styling choices apply before style caches load; CSS-heavy books use less temporary memory; and disabling Embedded Style consistently skips unused stylesheet work. +- EPUB layout now keeps CJK ruby and spaces, Russian paragraph continuations, Bionic Reading, underline/strikethrough runs, and right-to-left text correct. +- EPUBs with flowing `
` elements, image-led or decorative chapter headings, unsupported images, and dense final pages now lay out without excess gaps, clipping, dropped images, or misleading low-memory warnings. +- EPUB footnote and cross-reference previews now show complete notes, including targets in the middle of a paragraph. +- Saved EPUB positions, clipping highlights, and selections now stay accurate after font, orientation, or indexing changes; selections also remain readable in dark mode and on memory-tight pages. +- Dictionary misses can switch dictionaries without leaving the reader, and dictionary read failures now report an error instead of a false “not found.” +- Reader popups, KOReader Wi-Fi labels, Lyra battery headers, and the sleep message now remain correctly oriented and positioned. +- Manual refreshes preserve EPUB and TXT text anti-aliasing; XTC and XTCH status bars show the configured time-left estimate. +- Watchdog resets return to the crash-report flow, and power-button wake timing no longer depends on SD-card startup. +- The web file manager and uploads now handle simulator/device ports and stalled connections safely; unsupported settings stay hidden, and the optimizer removes empty chapter stubs without breaking table-of-contents links. + ## [v1.4.0.1] - 2026-07-28 ### Added @@ -140,6 +216,7 @@ ### Changed - Display, Reader, and Controls settings now open list menus instead of cycling through options one by one. +- The X3 clock visibility setting is now phrased as `Hide Clock`, with existing `Show Clock` preferences migrated to the matching hide behavior. - Reading time and time-left pace tracking now ignore page intervals longer than the configured idle-time threshold. - Web portal pages now use shared templates, stylesheet, and logo assets, reducing on-device page size and improving browser caching. - Already-cached EPUBs now open directly to the first page without an extra book-loading popup refresh. diff --git a/GOVERNANCE.md b/GOVERNANCE.md deleted file mode 100644 index 4c14bc5492..0000000000 --- a/GOVERNANCE.md +++ /dev/null @@ -1,38 +0,0 @@ -# Project Governance & Community Principles - -CrossPoint Reader is a community-driven, open-source project. Our goal is to provide a high-quality, open-source -firmware alternative for the Xteink X4 hardware. To keep this project productive and welcoming as we grow, we ask all -contributors to follow these principles. - -### 1. The "Human First" Rule -Technical discussions can get heated, but they should never be personal. -- **Assume good intent:** We are all volunteers working on this in our free time. If a comment seems abrasive, assume - it’s a language barrier or a misunderstanding before taking offense. -- **Focus on the code, not the person:** Critique the implementation, the performance, or the UX. Never the intelligence - or character of the contributor. -- **Inflammatory language:** Personal attacks, trolling, or exclusionary language (based on race, gender, background, - etc.) are not welcome here and will be moderated. - -### 2. A "Do-ocracy" with Guidance -CrossPoint thrives because people step up to build what they want to see. -- If you want a feature, the best way to get it is to start an - [Idea Discussion](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas) or open a PR. -- If you want to report a bug, check for duplicates and create an - [Issue](https://github.com/crosspoint-reader/crosspoint-reader/issues). -- While we encourage experimentation, the maintainers reserve the right to guide the project’s technical direction to - ensure stability on the ESP32-C3’s constrained hardware. -- For more guidance on the scope of the project, see the [SCOPE.md](SCOPE.md) document. - -### 3. Transparent Communication -To keep the project healthy, we keep our "work" in the open. -- **Public by Default:** All technical decisions and project management discussions happen in GitHub Issues, Pull - Requests, or the public Discussions tab. -- **Clarity in Writing:** Because we have a global community with different levels of English proficiency, please be as - explicit and clear as possible in your PR descriptions and bug reports. - -### 4. Moderation & Safety -The maintainers are responsible for keeping the community a safe place to contribute. -- We reserve the right to hide comments, lock threads, or block users who repeatedly violate these principles or engage - in harassment. -- **Reporting:** If you feel you are being harassed or see behavior that is damaging the community, please reach out - privately to @daveallie. diff --git a/README.md b/README.md index 6fe4e7977b..ff1d791f3f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ > **This is a personal fork of [CrossPoint Reader](https://github.com/crosspoint-reader/crosspoint-reader)** with a focus on improved fonts and minimal reading stats. +### Supported Devices + +- Xteink X3 +- Xteink X4 +- Seeed Studio Sticky + ## What's different in this fork My goal with this fork was to maintain the core Crosspoint firmware while integrating my preferred typography and some lightweight reading statistics. I’ve focused on keeping the underlying system stable while layering in a few "nice-to-have" features and UI refinements along the way. @@ -17,15 +23,11 @@ My goal with this fork was to maintain the core Crosspoint firmware while integr ---- - -**Note**: This firmware is confirmed to be working on both the X3 and X4. - ### Highlights - New reader fonts: Lexend Deca and Bitter. - Unicode emoji and miscellaneous symbols support (a limited subset). -- Adjusted font sizes: 8 pt, 9 pt, 10 pt, 12 pt, 14 pt, 16 pt, 18 pt, and 20 pt. See [Font Build Variants](./docs/font-build-variants.md) for more details. +- Reader font sizes: 10 pt, 12 pt, 14 pt, and 16 pt. - Added ~~strikethrough~~ support. - Made underlines thicker for better visibility. - Added a custom `Minimal` theme and sleep screen option for the minimalists out there. @@ -68,9 +70,9 @@ The UI now uses [Inter](https://fonts.google.com/specimen/Inter) as the display ### Font Sizes -There are 2 available build variants to choose from due to build size constraints: `tiny`, and `xlarge`. +CrossInk includes 10 pt, 12 pt, 14 pt, and 16 pt built-in reader font sizes. -See [Font Build Variants](./docs/font-build-variants.md) for the full point-size and emoji-support matrix. +See [SD Card Fonts](./docs/sd-card-fonts.md) for installing additional font families and size ranges. --- @@ -100,7 +102,7 @@ CrossInk runs on an ESP32-C3 with limited RAM, so very large folders or complex ## Development Device Simulator -The [device simulator](https://github.com/uxjulia/crosspoint-simulator) renders the e-ink display in an SDL2 window so firmware changes can be sanity-checked without flashing hardware. +The [device simulator](https://github.com/uxjulia/crossink-simulator) renders the e-ink display in an SDL2 window so firmware changes can be sanity-checked without flashing hardware. See [Simulator](./docs/simulator.md) for setup, platform notes, keyboard controls, and cache tips. @@ -108,6 +110,8 @@ See [Simulator](./docs/simulator.md) for setup, platform notes, keyboard control ## Installation +The fastest way to install Crossink is by using Inky, Crossink's web companion app: https://inky.crossink.dev/#flash-tools + Download a `firmware-*.bin` from the [releases page](https://github.com/uxjulia/CrossInk/releases), then flash it with the web installer or command line. See [Installation](./docs/installation.md) for step-by-step flashing and revert instructions. @@ -116,10 +120,11 @@ See [Installation](./docs/installation.md) for step-by-step flashing and revert ## Documentation -- [User Guide](./USER_GUIDE.md) +- [User Guide](./docs/user-guide.md) - [Installation](./docs/installation.md) -- [Font Build Variants](./docs/font-build-variants.md) +- [SD Card Fonts](./docs/sd-card-fonts.md) - [Reader Features](./docs/reader-features.md) +- [Dictionary](./docs/dictionary.md) - [Controls](./docs/controls.md) - [Simulator](./docs/simulator.md) - [Data Cache](./docs/data-cache.md) @@ -127,7 +132,7 @@ See [Installation](./docs/installation.md) for step-by-step flashing and revert - [Web server endpoints](./docs/webserver-endpoints.md) - [Common issues](./docs/troubleshooting.md) - [Project scope](./SCOPE.md) -- [Contributing docs](./docs/contributing/README.md) +- [Development docs](./docs/development/README.md) --- @@ -135,24 +140,62 @@ See [Installation](./docs/installation.md) for step-by-step flashing and revert CrossInk uses PlatformIO for building and flashing firmware. -See [Getting Started](./docs/contributing/getting-started.md) for prerequisites, clone setup, hooks, and validation commands. +See [Getting Started](./docs/development/getting-started.md) for prerequisites, clone setup, and validation commands. + +### Nix/NixOS + +Nix/NixOS users can enter the development shell with either `nix develop` (flakes) or `nix-shell`: + +```bash +nix develop -f nix +# or +nix-shell nix +``` + +To flash a connected ESP32-C3 device, enable PlatformIO's udev rules in your NixOS configuration: + +```nix +services.udev.packages = with pkgs; [ platformio-core.udev ]; +``` + +After rebuilding the system configuration, reconnect the device or reload udev rules. ### Build / flash / monitor Connect your Xteink X4 or X3 via USB-C and run: ```sh -pio run -e tiny --target upload +pio run -e default --target upload ``` -Replace `tiny` with another build variant if needed. See [Font Build Variants](./docs/font-build-variants.md). +Use `-e sticky` only when building for a Seeed Sticky device. The X3/X4 firmware uses the default environment. -See [Testing and Debugging](./docs/contributing/testing-debugging.md) for serial logging, simulator checks, static analysis, and bug-report guidance. +See [Testing and Debugging](./docs/development/testing-debugging.md) for serial logging, simulator checks, static analysis, and bug-report guidance. --- +## Repository layout + +- `src/` - app orchestration, settings/state, and activity implementations (home, reader, settings, network, boot/sleep) +- `lib/` - supporting libraries: EPUB parsing/layout, fonts, i18n, filesystem helpers, HAL wrappers, and more +- `freeink-sdk/` - hardware SDK submodule for display, input, storage, and battery (docs: https://freeink.org/docs) +- `web/` - web portal sources (`templates/`, `pages/`, `assets/`); compiled by `scripts/build_web.py` into `src/network/html/*.generated.h` +- `docs/` - user and developer documentation, published via the `site/` Astro site +- `site/` - Astro project that builds `docs/` into the CrossInk documentation website +- `test/` - unit tests and EPUB test fixtures +- `scripts/` - build, codegen, and release tooling (i18n generation, web asset building, hyphenation tries, release packaging, etc.) +- `bin/` - helper scripts for formatting (`clang-format-fix`) and CI checks +- `fs_/` - sample SD card contents (books, sleep images, themes) used by the simulator +- `nix/` - Nix/NixOS development shell definitions +- `managed_components/` - ESP-IDF managed component dependencies, fetched automatically during build +- [`SCOPE.md`](./SCOPE.md), [`GOVERNANCE.md`](./GOVERNANCE.md), [`CHANGELOG.md`](./CHANGELOG.md) - project scope, community principles, and release history + ## Internals The ESP32-C3 has about 380 KB of usable RAM, so CrossInk stores reusable book and device data on the SD card instead of rebuilding everything in memory. See [Data Cache](./docs/data-cache.md) for the `.crosspoint` layout and [File Formats](./docs/file-formats.md) for binary cache details. + +## Notice on Contributions + +This repository does not accept pull requests. Feature requests may be opened in [discussions](https://github.com/uxjulia/CrossInk/discussions), but major features requiring ongoing support should be directed upstream to [CrossPoint](https://github.com/crosspoint-reader/crosspoint-reader). diff --git a/SCOPE.md b/SCOPE.md index bbd00443a7..2aab6853a1 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -53,6 +53,3 @@ To provide a lightweight, high-performance firmware that maximizes the potential While I appreciate the desire to add new and exciting features to Crosspoint Reader, Crosspoint Reader is designed to be a lightweight, reliable, and performant e-reader. Things which distract or compromise the device's core mission will not be accepted. As a guiding question, consider if your idea improve the "core reading experience" for the average user, and, critically, not distract from that reading experience. - -> **Note to Contributors:** If you are unsure if your idea fits the scope, please open a **Discussion** before you start -> coding! diff --git a/bin/clang-format-fix b/bin/clang-format-fix index aea136fee5..5e88a87d11 100755 --- a/bin/clang-format-fix +++ b/bin/clang-format-fix @@ -47,6 +47,7 @@ git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \ | grep -v -E '^lib/EpdFont/builtinFonts/' \ | grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \ | grep -v -E '^lib/uzlib/' \ + | grep -v -E '^lib/miniz/third_party/' \ | xargs -r "${CLANG_FORMAT_BIN}" -style=file -i # Restore strict pipeline failure handling for the rest of the script. set -o pipefail diff --git a/docs/CNAME b/docs/CNAME index 2d29e8b9be..73979c0e2c 100644 --- a/docs/CNAME +++ b/docs/CNAME @@ -1 +1 @@ -crossink.uxj.io \ No newline at end of file +crossink.dev \ No newline at end of file diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index ce1e47bca7..0000000000 --- a/docs/_config.yml +++ /dev/null @@ -1,5 +0,0 @@ -remote_theme: just-the-docs/just-the-docs -title: CrossInk -description: Personal fork of CrossPoint Reader -mermaid: - version: "10.9.0" diff --git a/docs/activity-manager.md b/docs/activity-manager.md index 46544f9e21..82c4333103 100644 --- a/docs/activity-manager.md +++ b/docs/activity-manager.md @@ -9,15 +9,15 @@ This document explains the refactoring from the original per-activity render tas ## Overview of Changes -| Aspect | Old Model | New Model | -|--------|-----------|-----------| -| Render task | One per activity (8KB stack each) | Single shared task in `ActivityManager` | -| Render mutex | Per-activity `renderingMutex` | Single global mutex in `ActivityManager` | -| `RenderLock` | Inner class of `Activity` | Standalone class, acquires global mutex | -| Subactivities | `ActivityWithSubactivity` base class | Activity stack managed by `ActivityManager` | -| Navigation | Free functions in `main.cpp` | `activityManager.goHome()`, `goToReader()`, etc. | -| Subactivity results | Callback lambdas stored in parent | `startActivityForResult()` / `setResult()` / `finish()` | -| `requestUpdate()` | Notifies activity's own render task | Delegates to `ActivityManager` (immediate or deferred) | +| Aspect | Old Model | New Model | +| ------------------- | ------------------------------------ | ------------------------------------------------------- | +| Render task | One per activity (8KB stack each) | Single shared task in `ActivityManager` | +| Render mutex | Per-activity `renderingMutex` | Single global mutex in `ActivityManager` | +| `RenderLock` | Inner class of `Activity` | Standalone class, acquires global mutex | +| Subactivities | `ActivityWithSubactivity` base class | Activity stack managed by `ActivityManager` | +| Navigation | Free functions in `main.cpp` | `activityManager.goHome()`, `goToReader()`, etc. | +| Subactivity results | Callback lambdas stored in parent | `startActivityForResult()` / `setResult()` / `finish()` | +| `requestUpdate()` | Notifies activity's own render task | Delegates to `ActivityManager` (immediate or deferred) | ## Architecture @@ -33,16 +33,16 @@ Each activity created its own FreeRTOS render task on entry and destroyed it on │ │ ├── handle input │ │ │ │ ├── update state (under RenderLock) │ │ │ │ └── requestUpdate() ──notify──► Render Task │ │ -│ │ (per-activity)│ │ -│ │ 8KB stack │ │ -│ │ owns mutex │ │ +│ │ (per-activity)│ │ +│ │ 8KB stack │ │ +│ │ owns mutex │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │ ActivityWithSubactivity: │ │ ┌──────────────┐ ┌──────────────┐ │ -│ │ Parent │────►│ SubActivity │ │ -│ │ (has render │ │ (has own │ │ -│ │ task) │ │ render task) │ │ +│ │ Parent │────►│ SubActivity │ │ +│ │ (has render │ │ (has own │ │ +│ │ task) │ │ render task) │ │ │ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` diff --git a/docs/bionic-reading.md b/docs/bionic-reading.md deleted file mode 100644 index b7eede58d4..0000000000 --- a/docs/bionic-reading.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Bionic Reading -nav_order: 11 ---- - -# Bionic Reading - -Bionic Reading is a reading aid that bolds the first portion of each word, guiding your eyes to natural fixation points and helping you read faster with less effort. Some readers - particularly those with ADHD - find it helps them stay engaged with the text and reduces mind-wandering. - -Comparison of the same page with and without Bionic Reading enabled - -*Left: Bionic Reading off. Right: Bionic Reading on. Both using Literata.* - -## Enabling Bionic Reading - -1. Open **Settings > Reader** -2. Toggle **Bionic Reading** on - -Toggling the setting will trigger a re-index of your current book, the same as when changing font settings. Once indexing is complete, page turns proceed as normal. No changes are made to your EPUB files. - -## Examples - -Bionic Reading with Noto Serif font - -*Bionic Reading with Noto Serif font* - -Bionic Reading with Merriweather font - -*Bionic Reading with Merriweather font* - -Bionic Reading with Atkinson Hyperlegible Next font - -*Bionic Reading with Atkinson Hyperlegible Next font* - -## Notes - -- Bionic Reading only applies to regular body text. Already-bold text (headings, emphasis) is left unchanged. -- The setting is per-device, not per-book — it applies to all books while enabled. diff --git a/docs/catalog b/docs/catalog index 960975a7c4..b3e3cc208f 100644 --- a/docs/catalog +++ b/docs/catalog @@ -34,4 +34,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/contributing/development-workflow.md b/docs/contributing/development-workflow.md deleted file mode 100644 index 0dd33ade40..0000000000 --- a/docs/contributing/development-workflow.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Development Workflow -parent: Contributing -nav_order: 3 ---- - -# Development Workflow - -This page defines the expected local workflow before opening a pull request. - -## 1) Fork and create a focused branch - -- Fork the repository to your own GitHub account -- Clone your fork locally and add the upstream repository if needed -- Enable repo hooks once per clone: `git config core.hooksPath .githooks && chmod +x .githooks/pre-commit` - -- Branch from the target base branch for the work, usually `main` for release-bound changes or `development` for active development -- Keep each PR focused on one fix or feature area - -## 2) Implement with scope in mind - -- Confirm your idea is in project scope: [SCOPE.md](../../SCOPE.md) -- Prefer incremental changes over broad refactors - -## 3) Run local checks - -```sh -./bin/clang-format-fix -pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high -pio run -e simulator -pio run -e default -``` - -CI enforces formatting, static analysis, and the primary firmware build. -Use clang-format 21+ locally to match CI. -If `clang-format` is missing or too old locally, see [Getting Started](./getting-started.md). -Run plain `pio run` before larger PRs to build the release variants (`tiny` and `xlarge`). - -## 4) Open the PR - -- Use a semantic title (example: `fix: avoid crash when opening malformed epub`) -- Fill out `.github/PULL_REQUEST_TEMPLATE.md` -- Describe the problem, approach, and any tradeoffs -- Include reproduction and verification steps for bug fixes - -## 5) Review etiquette - -- Be explicit and concise in responses -- Keep discussions technical and respectful -- Assume good intent and focus on code-level feedback - -For community expectations, see [GOVERNANCE.md](../../GOVERNANCE.md). diff --git a/docs/controls.md b/docs/controls.md index 944cdbc25c..563ad6ac67 100644 --- a/docs/controls.md +++ b/docs/controls.md @@ -1,6 +1,6 @@ --- title: Controls -nav_order: 13 +nav_order: 6 --- # Controls @@ -39,10 +39,10 @@ When set to `Change Font Size`, hold a side button for about 2 seconds: - Down decreases font size When set to `Orientation Change`, hold a side button for about 2 seconds: + - Up cycles through the orientations in the following order: `Landscape CCW` -> `Inverted` -> `Landscape CW` -> `Portrait` - Down cycles through the orientations in the following order: `Landscape CW` -> `Inverted` -> `Landscape CCW` -> `Portrait` - ## Power, Back, and Menu Button Actions Defaults: @@ -75,6 +75,8 @@ Available actions include: - Footnotes - Dark Mode - Browse Files +- Create Clipping +- Look Up Word ## Footnote Shortcut diff --git a/docs/contributing/README.md b/docs/development/README.md similarity index 67% rename from docs/contributing/README.md rename to docs/development/README.md index 7d95121f26..5a8ab19692 100644 --- a/docs/contributing/README.md +++ b/docs/development/README.md @@ -1,17 +1,16 @@ --- -title: Contributing +title: Development nav_order: 2 has_children: true --- -# Contributing Docs +# Development Docs -This section is a lightweight contributor guide for CrossInk. +This section contains technical information for building, testing, and understanding CrossInk. It is written for software developers who may be new to embedded development. - [Getting Started](./getting-started.md) - [Architecture Overview](./architecture.md) -- [Development Workflow](./development-workflow.md) - [Testing and Debugging](./testing-debugging.md) If you are new, start with [Getting Started](./getting-started.md). diff --git a/docs/contributing/architecture.md b/docs/development/architecture.md similarity index 100% rename from docs/contributing/architecture.md rename to docs/development/architecture.md diff --git a/docs/contributing/getting-started.md b/docs/development/getting-started.md similarity index 80% rename from docs/contributing/getting-started.md rename to docs/development/getting-started.md index 7c77b809b8..bb730e0163 100644 --- a/docs/contributing/getting-started.md +++ b/docs/development/getting-started.md @@ -1,6 +1,6 @@ --- title: Getting Started -parent: Contributing +parent: Development nav_order: 1 --- @@ -59,13 +59,6 @@ If you already cloned without submodules: git submodule update --init --recursive ``` -Enable the repository-managed Git hooks (required once per clone): - -```sh -git config core.hooksPath .githooks -chmod +x .githooks/pre-commit -``` - ## Build ```sh @@ -73,7 +66,7 @@ pio run -e simulator pio run -e default ``` -`pio run` without an environment builds the release variants listed in `platformio.ini`: `tiny` and `xlarge`. +`pio run` without an environment builds the X3/X4 and Sticky firmware targets listed in `platformio.ini`. ## Flash @@ -81,7 +74,7 @@ pio run -e default pio run -e default --target upload ``` -## First checks before opening a PR +## Validation ```sh ./bin/clang-format-fix @@ -92,5 +85,4 @@ pio run ## What to read next - [Architecture Overview](./architecture.md) -- [Development Workflow](./development-workflow.md) - [Testing and Debugging](./testing-debugging.md) diff --git a/docs/contributing/testing-debugging.md b/docs/development/testing-debugging.md similarity index 84% rename from docs/contributing/testing-debugging.md rename to docs/development/testing-debugging.md index bcd5b1dbca..cb6f0cd86d 100644 --- a/docs/contributing/testing-debugging.md +++ b/docs/development/testing-debugging.md @@ -1,6 +1,6 @@ --- title: Testing & Debugging -parent: Contributing +parent: Development nav_order: 4 --- @@ -20,7 +20,7 @@ pio run -e simulator pio run -e default ``` -`pio run` without `-e` builds the release matrix from `platformio.ini` (`tiny` and `xlarge`). Use it before opening broad firmware PRs, but prefer explicit environments while iterating. +`pio run` without `-e` builds the X3/X4 and Sticky firmware targets from `platformio.ini`. Use it for a comprehensive build check, but prefer explicit environments while iterating. ## Flash and monitor diff --git a/docs/dictionary-development.md b/docs/dictionary-development.md new file mode 100644 index 0000000000..65ec8d0766 --- /dev/null +++ b/docs/dictionary-development.md @@ -0,0 +1,146 @@ +# Dictionary Development Guide + +This guide describes the dictionary implementation currently shipped on this branch. For installation and use, see [dictionary.md](dictionary.md). + +## Supported StarDict Files + +| File | Required | Purpose | +|------|----------|---------| +| `.dict` | Yes | Uncompressed definition data | +| `.idx` | Yes | Sorted headwords and offsets into `.dict` | +| `.ifo` | Recommended | Metadata and `sametypesequence` used to interpret definition fields | +| `.syn` | Optional | Alternate forms mapped to `.idx` ordinals | +| `.idx.oft` / `.syn.oft` | Optional | Coarse page offsets used to narrow a scan | +| `.idx.oft.cspt` / `.syn.oft.cspt` | Optional | CrossInk prefix indexes used as the fastest lookup path | +| `.qidx` | Generated | Disposable sampled index built by the device when `.idx` has no prepared accelerator | + +The device requires an uncompressed `.dict`; it does not read `.dict.dz` or `.syn.dz` directly. Use `scripts/dictionary_tools.py prep` on a computer when decompression is needed or to generate the fastest `.oft`/`.cspt` accelerators. For an uncompressed dictionary without those accelerators, the device automatically generates `.qidx` on first lookup. + +Dictionary discovery is implemented by `DictionaryRegistry`. It checks `/.dictionaries` first and `/dictionaries` second, using only the first root directory found. Each child folder must contain exactly one `.idx`, no more than one `.ifo`, and a `.dict` file. Hidden folders and ambiguous folders are skipped. + +## Lookup Paths + +`Dictionary::locate` searches `.idx` in this order: + +1. Use `.idx.oft.cspt` to select a small byte range. +2. Fall back to `.idx.oft` to select an offset page. +3. Fall back to the device-generated `.qidx` sampled index. +4. Fall back to scanning `.idx` from the beginning if the sidecar cannot be read or written. + +`Dictionary::resolveAltForm` uses `.syn.oft.cspt`, then `.syn.oft`, then a full `.syn` scan. `Dictionary::findSimilar` uses `.idx.oft` when available and otherwise uses `.qidx` to scan a bounded neighborhood. If neither index accelerator is usable, spelling suggestions are skipped so a large dictionary cannot block the reader UI; direct lookup still works with the uncompressed `.dict` and `.idx` files. + +`.qidx` uses a 20-byte little-endian header containing `QIDX`, format version, sample interval, sample count, and source `.idx` size, followed by the byte offset of every 256th `.idx` entry. It is written through a temporary file and installed only after the complete scan succeeds. A size mismatch or invalid header causes it to be rebuilt. + +StarDict files with `idxoffsetbits=64` are parsed, but entries whose definition offset exceeds the device's supported 32-bit range fail safely. + +## Definition Rendering + +`DictionaryDefinitionActivity` resolves the selected `.dict` byte range and renders one page at a time. `DictHtmlRenderer` streams HTML input, while `DictLayout::Wrapper` wraps styled spans into page lines. Keeping only the current page bounds peak RAM and avoids materializing a large definition as one in-memory document. Definition body text uses the active reader font; headers and controls keep built-in UI fonts. SD-font text is passed through unchanged. Built-in coverage uses the audited, fixed Lexend Deca/Bitter glyph set (including the renderer's `Γ`, `ε`, and `ω` fallbacks), while unsupported IPA, Greek, combining-mark, and punctuation codepoints use the existing approximations. + +Paging re-parses the definition from its start. This trades extra sequential reads for predictable memory usage on the ESP32-C3. + +Chained lookups use `LookupChain`, which stores compact history positions and page numbers instead of owned copies of every headword. The chain is bounded by `LookupHistory::MAX_VISIBLE_ENTRIES` (currently 50). + +## Lookup History + +Each EPUB cache stores history at `/dictionary_history.txt`. Lines use `word|STATUS`, where the status is direct, stemmed, alternate form, suggestion, or not found. + +The file is append-only and is not automatically truncated. The UI loads only the newest 50 entries. Cache-clear helpers preserve this file as user state. + +## Offline CLI + +The standard-library-only tool is `scripts/dictionary_tools.py`: + +```bash +# Decompress .dict.dz/.syn.dz and generate .oft/.cspt files. +python3 scripts/dictionary_tools.py prep /path/to/dictionary-folder + +# Perform an exact lookup. +python3 scripts/dictionary_tools.py lookup /path/to/dictionary-folder apple + +# Merge prepared dictionaries. +python3 scripts/dictionary_tools.py merge \ + --source /path/to/dict-a \ + --source /path/to/dict-b \ + --output /path/to/merged-dict +``` + +`lookup` and `merge` require an uncompressed `.dict`. `merge` writes a prepared output including applicable `.oft` and `.cspt` files. + +## Generating Dictionary Fonts + +Dictionary definitions use the active reader font, so the SD-card font catalog +also has a dictionary-specific build. The +`lib/EpdFont/scripts/build-dictionary-fonts.py` wrapper uses the family, style, +and size catalog in `lib/EpdFont/scripts/sd-fonts.yaml`, then adds the broad +IPA, combining-mark, and reader ranges needed by dictionary definitions before +delegating to `build-sd-fonts.py`. It packages each generated family as a ZIP. + +Install the font-builder dependencies and run it from the CrossInk repository +root: + +```bash +python3 -m pip install -r lib/EpdFont/scripts/requirements.txt +python3 lib/EpdFont/scripts/build-dictionary-fonts.py --clean --jobs 2 +``` + +If you are generating your own dictionary fonts, change the output directory so +your files do not mix with the shared catalog output. For example: + +```bash +python3 lib/EpdFont/scripts/build-dictionary-fonts.py \ + --output-dir ./generated-dictionary-fonts +``` + +The `--clean` option removes the selected output directory before building, so +do not use it with the default location unless you intend to rebuild that +catalog. + +By default, the generated family folders and ZIPs are written to +`../crossink-fonts/dictionary-fonts`. Unzip a family archive into `/.fonts/` or +`/fonts/` on the SD card, or copy the output to the sibling `crossink-fonts` +repository when publishing the catalog. Use `--output-dir` to choose another +destination. + +Useful options: + +| Option | Purpose | +|--------|---------| +| `--only FamilyA,FamilyB` | Build only the named families from `sd-fonts.yaml` | +| `--config path/to/catalog.yaml` | Use a different family catalog | +| `--output-dir path` | Write family folders and ZIPs somewhere other than the default | +| `--clean` | Remove the output directory before building, avoiding stale `.cpfont` files | +| `--jobs N` / `-j N` | Limit parallel family builds | +| `--timeout SECONDS` | Set the per-family converter timeout (default: 600) | +| `--verbose` / `-v` | Stream converter output while debugging a build | + +The wrapper does not change `sd-fonts.yaml`; it creates a temporary transformed +catalog for the shared builder. If a family is changed or removed, use +`--clean` so stale files cannot be mistaken for current output. + +## CrossInk Prefix Index (`.cspt`) + +Both `.idx.oft.cspt` and `.syn.oft.cspt` use the same format: + +| Offset | Size | Field | +|--------|------|-------| +| 0 | 4 | Magic `CSPT` | +| 4 | 1 | Version (`1`) | +| 5 | 1 | Prefix length (`16`) | +| 6 | 2 | Producer stride (`16`, little-endian) | +| 8 | 4 | Entry count (little-endian) | +| 12... | 20 each | 16-byte, zero-padded prefix plus 4-byte source offset | + +The reader performs a case-insensitive binary search for the last prefix less than or equal to the target, then scans only until the next recorded source offset. Invalid or missing `.cspt` data falls back to `.oft`, then to a full scan. + +The header's stride field is currently informational. Producers must continue to emit `16` until the format version and readers are updated together. + +## Verification + +There is no dictionary-specific host fixture suite in this branch. For changes: + +1. Run `python3 scripts/dictionary_tools.py prep` and `lookup` against a representative StarDict dictionary. +2. Build the simulator with `pio run -e simulator` for reader/UI integration. +3. On hardware, test dictionaries with and without `.oft`/`.cspt`, a `.syn` dictionary, HTML definitions, long definitions, lookup history, chained lookup, and per-book overrides. + +Multi-word selection is limited to the currently rendered page. Reducing the reader or definition font size can fit more of a phrase on one page. diff --git a/docs/dictionary.md b/docs/dictionary.md new file mode 100644 index 0000000000..2c1f90fd20 --- /dev/null +++ b/docs/dictionary.md @@ -0,0 +1,258 @@ +--- +title: Dictionary +nav_order: 5 +--- + +# Dictionary + +> [!TIP] +> For the best experience, prepare your dictionary using CrossInk's web companion tool [Inky](https://inky.crossink.dev/#dictionary-tools), which creates `.oft` and `.oft.cspt` accelerator files. An unprepared dictionary with uncompressed `.dict` and `.idx` files also works: on its first lookup, CrossInk creates a smaller on-device `.qidx` quick index automatically. + +## Supported Format + +The reader supports **StarDict** dictionaries. When searching for dictionaries online, look for "StarDict format" or files with `.dict`, `.idx`, and `.ifo` extensions. + +A dictionary folder typically contains: + +- `.dict` -- definition data (required; compressed `.dict.dz` files must be prepared on a computer first) +- `.idx` -- word index (required) +- `.ifo` -- metadata such as dictionary name and word count (recommended) +- `.syn` -- alternate forms and synonyms (optional, enhances lookup coverage) + +Minimum requirement: one `.dict` and one `.idx` file in the same folder. Their filename stems should match. Without `.ifo`, the dictionary will still work but metadata and HTML definition rendering may be limited. + +--- + +## Setting Up a Dictionary + +Example folder structure: `SDCARD/.dictionaries/Cambridge/*.dict`. Make sure the dictionary folder is **NOT** nested like `/Cambridge/Cambridge/*.dict` + +1. Copy your dictionary folder(s) to one of these directories on the SD card: + - `/.dictionaries/` (checked first) + - `/dictionaries/` +2. If no dictionary has been selected before, the first dictionary in the alphabetical device list is selected automatically. +3. To choose between multiple dictionaries, open **Settings -> Reader -> Dictionary** on the device and select one from the list. +4. Per book dictionaries can be set from within the in-reader menu `Book Options -> Settings Gear tab -> Book Dictionary` + +### Preparing Compressed or Large Dictionaries + +CrossInk does not decompress dictionaries on the device. If a download contains `.dict.dz` or `.syn.dz`, run the bundled preparation tool on a computer before copying the folder to the SD card (see [Dictionary Tools](#dictionary-tools)). + +An uncompressed dictionary with only `.dict` and `.idx` files is usable without desktop preparation. During the first lookup, if neither `*.idx.oft` nor `*.idx.oft.cspt` is present, CrossInk creates `*.qidx` beside the dictionary files. It records a sample every 256 index entries, letting later direct and stemmed lookups jump near the requested word before scanning a small part of the original `.idx`. + +`.qidx` is a disposable device cache: it is rebuilt when it is missing, invalid, or belongs to a differently sized `.idx` file, and it can be deleted safely. If the lookup is cancelled or the SD card cannot create the cache, the lookup still works by scanning the full `.idx`; CrossInk will try to create the quick index again on a later lookup. + +Desktop preparation remains recommended for large dictionaries. `*.idx.oft.cspt` is the preferred direct-lookup accelerator, with `*.idx.oft` as its fallback; either one prevents CrossInk from creating `.qidx`. The desktop files also speed up synonym resolution and ordinal lookups. The smaller `.qidx` accelerates direct lookup and bounds spelling-suggestion searches, but it does not accelerate synonyms or ordinal lookups. + +--- + +## Looking Up a Word + +The **Look Up Word** option in the reader menu is only visible when a dictionary is active. + +### Buttons + +1. Open the reader menu and choose **Look Up Word**. +2. The page becomes a word-select overlay - one word is highlighted, initially near the centre of the page. +3. Use **Up/Down** to move between rows, **Left/Right** to move between words on the same row. +4. Press **Confirm** to look up the highlighted word. +5. Press **Back** to exit word-select without looking anything up. + +### Touchscreen + +In an EPUB reader with **Touch Reader Controls** enabled, touch and hold a +word for about one second. CrossInk opens word selection on that word; lift +your finger to look it up. To look up a phrase, keep holding after the +selection opens, drag to the last word, then lift your finger. This is a +direct shortcut from the reading page, so you do not need to open the reader +menu first. + +When word selection is already open, including selection opened from a +definition, touch a word to start a selection and drag before releasing to +look up a phrase. Releasing without dragging looks up the single touched word. + +### Quick Lookup + +Set a shortcut to **Look Up Word** in **Settings -> Controls**. Holding that shortcut in the EPUB reader then enters word-select directly, skipping the reader menu. Release and navigate to a word as usual. + +### How Lookup Works + +When you select a word, the reader searches for it in this order: + +1. **Direct match** - the word is found as-is in the dictionary index. +2. **Stemming** - the reader automatically tries common word forms (plurals, verb conjugations, comparatives). For example, "running" finds "run". +3. **Alternate forms** - if the dictionary includes a synonym/alternate forms file and no match was found yet, a prompt appears. Press **Confirm** to search alternate forms, or **Back** to skip. +4. **Suggestions** - if nothing matched, a list of similar words from the dictionary is shown. Select one to view its definition. +5. **Not found** - if no matches or suggestions exist, a not-found message appears. Press **Back** to return to word-select, or **Confirm** to exit to the reader. + +--- + +## The Definition Screen + +When a word is found, the definition screen shows the headword at the top and the definition text below. + +- **Page Forward** - next page (for long definitions) +- **Page Back** - previous page +- **Confirm** (labelled **Look Up Word**) - enter word-select mode on the definition text (see Chaining Lookups below) +- **Left** (labelled **Switch**) - choose another dictionary and repeat the current lookup +- **Back** (short press) - return to the previous screen +- **Back** (long press) - exit all the way back to the reader + +--- + +## Chaining Lookups + +From a definition screen, you can look up any word within the definition text without returning to the reader. + +1. Press **Confirm** (**Look Up Word**) on the definition screen. +2. A word in the definition becomes highlighted. Navigate to any word and press **Confirm**. +3. A new definition screen opens for that word. +4. You can chain further by pressing **Look Up Word** again from the new definition. +5. Short-press **Back** to exit word-select and return to the current definition. +6. Short-press **Back** again to go back through the chain (each press returns to the previous definition). +7. Long-press **Back** at any point to exit directly to the reader. + +Going back returns you to each prior definition **on the page you were reading** when you chained away from it, not the first page. + +**Chain depth limit:** the chain follows the 50 entries visible in lookup history. If you chain deeper than that, the oldest chain entries are dropped, so backing out eventually returns you to the reader. + +--- + +## Phrase / Multi-word Lookup + +In word-select mode, you can select a sequence of words to look up as a phrase. + +1. Navigate to the first word of the phrase. +2. Long-press **Confirm** to anchor on that word. +3. Use the navigation buttons to extend (or shrink) the selection to cover the full phrase. All selected words are highlighted. +4. Short-press **Confirm** to look up the selected phrase. +5. Press **Back** to cancel and return to single-word select mode. + +Multi-word select works in both the reader word-select and the definition word-select (chained lookup). + +On a touchscreen, use a press-and-drag selection instead of the button +sequence: touch the first word, drag to the last word, and lift your finger to +look up the selected phrase. The first long hold on the reading page is only +needed to enter lookup; once word selection is open, touching a word starts +the range immediately. + +**Limitation:** Multi-word selection cannot span a page boundary. If a phrase crosses from one page to the next, only the words on the current page are available for selection. As a workaround, reduce the reader font size so more words fit on a single page, perform the lookup, then restore the original font size. + +--- + +## Per-Book Dictionary + +Each book can have its own dictionary, independent of the global setting. + +1. Open the reader menu, navigate to the 3rd tab (settings gear icon) and choose **Book Dictionary**. +2. Select a dictionary from the picker. +3. The per-book choice is saved and restored each time the book is opened. +4. To remove the override, open **Book Dictionary** and select **Use Global**. The picker shows the current global dictionary name in parentheses next to Use Global. + +The global dictionary is automatically restored whenever the book is closed. Changing a book's dictionary does not affect the global setting or other books. + +Using **Switch Dictionary** during a word lookup is temporary. It applies only to that lookup flow; when lookup closes, the reader returns to the book's dictionary override, or the global dictionary when the book has no override. + +--- + +## Lookup History + +Each book maintains its own lookup history, accessible from the reader menu. + +1. Open the reader menu and choose **Lookup History**. +2. Each entry shows the searched word and a status indicator: + - Square root (√) -- found directly + - Tilde (~) -- resolved via stemming or alternate forms + - Question mark (?) -- reached via the suggestions screen + - X -- not found +3. Select any entry and press **Confirm** to look it up again. Press **Back** from the definition to return to the history screen. +4. To delete an entry, long-press **Confirm** on it. A confirmation popup appears -- press **Confirm** to delete, or **Back** to cancel. + +The history screen shows the 50 most recent entries. The on-disk history is append-only, so older entries remain stored but are not shown. The visible window also bounds how deep a chained lookup can go back (see Chaining Lookups). + +--- + +## IPA Phonetic Characters + +Dictionary definitions use the active reader font and size by default. If at least one dictionary is installed, you can set the default dictionary font and size in **Settings > Reader > Font Options**. Books inherit those defaults unless you choose a different font or size in **Book Options > Font Options**. Choose **Use Global** in Book Options to return a book to the global defaults. When the reader uses an SD-card font, you can choose a different dictionary size while keeping that same family; built-in reader fonts continue to use their active size. A saved size with no matching file uses the closest available size from the dictionary family. + +Only one SD-card font family is loaded at a time: CrossInk temporarily swaps to the dictionary font while a definition is open, then restores the reader font when you close it. If a book's selected dictionary font is missing, the definition temporarily uses the global dictionary font. If that is also unavailable, it falls back to the reader font. The per-book selection is kept so it resumes automatically if you reinstall the family. + +Built-in fonts keep the glyphs they contain and approximate only unsupported pronunciation symbols. If you see a filled diamond, choose an SD-card font that includes that character. + +You can download CrossInk's SD card catalog of fonts with IPA glyphs built-in from [Inky](https://inky.crossink.dev/#downloads). + +See the [dictionary font builder](dictionary-development.md#generating-dictionary-fonts) if you want to build your own dictionary fonts via the CLI. + +--- + +## Dictionary Tools + +## Inky + +CrossInk's companion app, [Inky](https://inky.crossink.dev/#dictionary-tools), prepares one StarDict dictionary at a time and generates the accelerator indexes used for fast lookups. It accepts either: + +- an uncompressed dictionary folder; or +- a `.zip`, `.tar.zst`, or `.rar` archive containing that folder or its dictionary files. + +1. Open Inky's **Dictionary Tools** page. +2. Drag the dictionary archive or folder onto the upload area. Alternatively, click it and choose **Archive** or **Folder**. +3. Select **Prepare Dictionary**. The input must include matching `.ifo`, `.idx`, and `.dict` or `.dict.dz` files. `.syn` and `.syn.dz` are optional. +4. Download the prepared ZIP when the job finishes, then unzip it into `/.dictionaries/` or `/dictionaries/` on the SD card. Keep the resulting dictionary folder intact. + +Inky decompresses `.dict.dz` and `.syn.dz` when needed, creates `.idx.oft`, `.idx.oft.cspt`, `.syn.oft`, and `.syn.oft.cspt`, and packages the prepared dictionary for you. The prepared download is available for 10 minutes. + +### Command Line + +A command-line tool is included for working with StarDict dictionaries on your computer, without the device. It requires Python 3 and has no external dependencies. + +#### Pre-processing + +Decompress dictionary data and generate accelerator indexes on your computer: + +```bash +python3 scripts/dictionary_tools.py prep /path/to/dictionary-folder +``` + +Run this before copying compressed dictionaries to the SD card. It is also recommended for large uncompressed dictionaries because it makes lookups much faster. + +The command produces, when applicable, `.dict`, `.syn`, `.idx.oft`, `.syn.oft`, `.idx.oft.cspt`, and `.syn.oft.cspt`. Copy these alongside the original `.idx` and optional `.ifo`. CrossInk uses `.idx.oft.cspt` first, then `.idx.oft`; when neither exists, it creates and uses `.qidx` on the device. If a quick index cannot be created, CrossInk scans the original index instead. + +### Looking Up a Word + +Look up a word from the command line: + +```bash +python3 scripts/dictionary_tools.py lookup /path/to/dictionary-folder apple +``` + +Prints the definition to stdout. Compressed dictionary data must be prepared with `prep` first. + +### Merging Dictionaries + +Combine two or more StarDict dictionaries into a single monolithic dictionary: + +```bash +python3 scripts/dictionary_tools.py merge \ + --source /path/to/dict-a \ + --source /path/to/dict-b \ + --output /path/to/merged-dict +``` + +Specify `--source` once per dictionary to include. The merged output contains the full union of all headwords and synonyms. When the same word appears in multiple sources, definitions are concatenated in source order. + +Source dictionaries must be prepared (decompressed `.dict` files) before merging. The output is a complete, ready-to-use StarDict dictionary that can be copied directly to the SD card. + +## Where to find dictionaries + +> credit to https://github.com/koreader/koreader/wiki/Dictionary-support for the list. + +- The [reader.dict](https://www.reader-dict.com) (ex "BoboTiG/ebook-reader-dict") project provides StarDict version of daily dumps of [Wiktionary](https://www.wiktionary.org/) monolingual dictionaries for a variety of languages. It also provides [non-free multilingual](https://www.reader-dict.com) dictionaries. +- The [WikDict](https://www.wikdict.com) project provides free bilingual dictionaries based on [Wiktionary](https://www.wiktionary.org/) for a lot of language pairs. StarDict versions can be [downloaded from here](https://download.wikdict.com/dictionaries/stardict/). +- The [`Vuizur/Wiktionary-Dictionaries`](https://github.com/Vuizur/Wiktionary-Dictionaries) repository contains dictionaries based on [Wiktionary](https://www.wiktionary.org/) from many languages to English, including English-English. +- The [DictInfo](https://www.dictinfo.com/) website provides outdated monolingual dictionaries based on [Wiktionary](https://www.wiktionary.org/). +- The [Firedict site](https://tuxor1337.frama.io/firedict/dictionaries.html) contains a list of freely available dictionaries. +- [wiktionary_stardict](https://xxyzz.github.io/wiktionary_stardict/): update monthly. +- [Fictionaries](https://fictionary.gumroad.com/) provides dictionaries for various speculative fiction books and series. +- [World Factbooks Archive](https://github.com/MilkMp/CIA-World-Factbooks-Archive-1990-2025) provides 36 years of CIA's World Factbook dictionaries in StarDict format. +- [StarDict-Hebrew](https://github.com/Uri-Tauber/StarDict-Hebrew) Hebrew-English StarDict versions of Babylon dictionaries. diff --git a/docs/epub-indexing.md b/docs/epub-indexing.md new file mode 100644 index 0000000000..e5fb83afa3 --- /dev/null +++ b/docs/epub-indexing.md @@ -0,0 +1,99 @@ +--- +title: EPUB Indexing Methods +nav_order: 8.5 +--- + +# EPUB Indexing Methods + +Before CrossInk can display an EPUB chapter, it lays out the chapter into +pages and saves that layout in the book's cache. **Indexing Method** chooses +whether CrossInk finishes that work before you start reading the chapter, or +does it a little at a time as you read. + +The default is **Full Section**. Most books work well with it. Choose +**Incremental** for a book with unusually large chapters, or when waiting for +an entire chapter to open is more disruptive than seeing a short indexing wait +later. + +| Method | What happens | Main benefit | Main tradeoff | +| ------------ | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| Full Section | Builds and caches the whole chapter before it is shown. | Normal page turns within the chapter do not need more indexing. | A large uncached chapter can take a noticeable time to open. | +| Incremental | Builds enough pages to show your current position, then continues in small background steps while you read. | You can begin reading a large chapter sooner. | If you reach pages that have not been built yet, CrossInk may briefly show **Indexing** while it catches up. | + +## Full Section + +Full Section finishes the complete chapter cache before CrossInk displays an +uncached chapter. This is the simpler, more predictable experience for normal +EPUBs: after the initial wait, the chapter's page count and all of its pages +are ready. + +CrossInk also tries to build the next chapter while you are reading the +penultimate page of the current one. When this succeeds, moving to the next +chapter does not require a visible indexing wait. It is deliberately +best-effort: on the X3/X4's limited memory, CrossInk skips that background work +when there is not enough free or contiguous memory, and indexes the next +chapter when you enter it instead. + +Use Full Section when: + +- Most chapters open quickly and you want the smoothest page turns. +- You prefer any indexing wait to happen at the chapter boundary. +- You want a complete chapter cache and final page count as soon as it opens. + +Consider Incremental when a particular chapter takes a long time to open, or +when a very large chapter is more likely to stress the reader's available +memory. + +## Incremental + +Incremental builds only as far as CrossInk needs to display the current page. +It keeps a small number of pages ready ahead of your reading position and uses +short background steps to extend that ready area. Think of it like loading the +first screen of a long web page first, then preparing the rest while you read. + +This avoids making you wait for every page in a very large chapter before you +can start. CrossInk saves the pages it has already built, so leaving the book +does not discard that readable progress. A giant chapter may therefore remain +partially indexed until you read farther into it; it does not have to finish in +one continuous session. + +Use Incremental when: + +- A book pauses for a long time at **Indexing** before its first page appears. +- The book has exceptionally large chapters, such as omnibus editions or + poorly split EPUBs. +- Starting to read sooner matters more than avoiding an occasional wait later. + +Expect a visible **Indexing** popup if you jump far ahead, follow a link to an +unbuilt part of the chapter, or turn pages faster than the background work can +stay ahead. That is normal: CrossInk is building just enough additional pages +to make the requested position readable. + +KOReader Sync uses the same content location rather than the other device's +page number. If a synced location is beyond this device's saved incremental +prefix, CrossInk indexes forward until that location is available. Switching +between Incremental and Full Section does not change the saved reading location: +Full Section resolves it as soon as the chapter is built, while Incremental only +builds through the requested content. + +## Changing The Setting + +To change the default for future EPUBs, open **Settings → Reader → Indexing +Method** and choose **Incremental** or **Full Section**. + +To change it only for the EPUB you are reading, open the reader menu, choose +**Reader Options**, then choose **Indexing Method**. The per-book choice is +saved with that book and overrides the global default without changing your +other books. + +Changing the method does not discard the chapter currently on screen. The new +choice is used the next time CrossInk needs to index a chapter. It also does +not alter the EPUB file itself, your reading progress, bookmarks, clippings, +or reading statistics. + +## If A Book Is Still Slow Or Cannot Index + +Indexing Method changes _when_ CrossInk performs layout work; it does not +simplify the publisher's CSS, images, or tables. If a difficult EPUB is still +slow or runs out of memory, try a lighter [EPUB Render Mode](./epub-render-modes.md) +or [optimize](https://inky.crossink.dev) the EPUB before copying it to the device. diff --git a/docs/epub-render-modes.md b/docs/epub-render-modes.md index 727c609115..e848729f45 100644 --- a/docs/epub-render-modes.md +++ b/docs/epub-render-modes.md @@ -1,6 +1,6 @@ --- title: EPUB Render Modes -nav_order: 18 +nav_order: 9 --- # EPUB Render Modes @@ -15,12 +15,12 @@ default behavior for your other books. ## Which Mode Should I Use? -| Mode | Best for | What it keeps | What it simplifies | -| --- | --- | --- | --- | -| CrossInk Default | Most books | Full CrossInk styling, detailed CSS, table layout, image sizing, publisher spacing, Bionic Reading, Guide Dots | Nothing by default | -| Balanced | Books where CrossInk has fallen back automatically, or books you want to start in a lighter mode | Publisher spacing, image sizing, decorations, Bionic Reading, Guide Dots | Complex CSS lookups and table layout | -| Light | Books where Balanced is still too heavy, or books you want to start in the safest mode | Text content, hidden-content rules, basic formatting, Bionic Reading, Guide Dots | Complex CSS lookups, table layout, publisher spacing, image sizing, and decorative separators | -| Safe Mode | Final automatic fallback for books that still run out of memory in Light | Text content, hidden-content rules, basic formatting | Light-mode simplifications plus embedded styles, Bionic Reading, and Guide Dots | +| Mode | Best for | What it keeps | What it simplifies | +| ---------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| CrossInk Default | Most books | Full CrossInk styling, detailed CSS, table layout, image sizing, publisher spacing, Bionic Reading, Guide Dots | Nothing by default | +| Balanced | Books where CrossInk has fallen back automatically, or books you want to start in a lighter mode | Publisher spacing, image sizing, decorations, Bionic Reading, Guide Dots | Complex CSS lookups and table layout | +| Light | Books where Balanced is still too heavy, or books you want to start in the safest mode | Text content, hidden-content rules, basic formatting, Bionic Reading, Guide Dots | Complex CSS lookups, table layout, publisher spacing, image sizing, and decorative separators | +| Safe Mode | Final automatic fallback for books that still run out of memory in Light | Text content, hidden-content rules, basic formatting | Light-mode simplifications plus embedded styles, Bionic Reading, and Guide Dots | Most users can leave books on **CrossInk Default**. If a book runs out of memory while building a section, CrossInk will automatically try **Balanced** and then diff --git a/docs/file-formats.md b/docs/file-formats.md index aefb1697d5..aae3a3708d 100644 --- a/docs/file-formats.md +++ b/docs/file-formats.md @@ -7,12 +7,13 @@ fixed-size char buffer. ## `book.bin` -### Version 8 +### Version 9 `book.bin` stores EPUB metadata plus lookup tables for spine and TOC entries. The current firmware writes this version from `BookMetadataCache`. -Version 8 stores book and TOC title strings NFC-composed so decomposed -diacritics render correctly with device fonts. +Version 9 stores book and TOC title strings NFC-composed so decomposed +diacritics render correctly with device fonts. It also rebuilds metadata after +the EPUB guide start-reference handling changed. ImHex pattern: @@ -21,7 +22,7 @@ import std.mem; import std.string; import std.core; -#define EXPECTED_VERSION 8 +#define EXPECTED_VERSION 9 #define MAX_STRING_LENGTH 65535 struct String { @@ -93,7 +94,7 @@ if (parsedSize != fileSize) { ## `reader_settings.bin` -### Version 2 +### Version 5 Each EPUB cache directory may contain `reader_settings.bin`. Missing files mean the book uses global Reader settings and the default auto-page-turn interval. @@ -103,7 +104,12 @@ Version 1 stored only: - `u8 version` - `u16 autoPageTurnSeconds` -Version 2 stores flags before the full reader-settings snapshot. This lets the +Version 2 stores flags before the full reader-settings snapshot. Version 3 adds +the EPUB word-spacing level to that snapshot. Version 4 adds the EPUB indexing +method (`0` = incremental, `1` = full section). Version 5 appends a per-book +dictionary SD-font family name. Version 6 stores reader font sizes as physical +point sizes, and version 7 appends the dictionary font's selected point size. +This lets the file preserve an auto-page-turn interval without forcing custom font/layout settings for the book. It also stores a per-book EPUB render mode override, which can be changed from book action menus before opening the book so a @@ -114,14 +120,15 @@ fallback successfully opens a difficult book. ```c++ struct ReaderSettingsBin { - u8 version; // 2 - u8 flags; // bit 0 = custom reader settings, bit 1 = custom auto-page-turn interval, bit 2 = render mode override + u8 version; // 7 + u8 flags; // bit 0 = custom reader settings, bit 1 = custom auto-page-turn interval, bit 2 = render mode override, bit 3 = dictionary font override u16 autoPageTurnSeconds; u8 renderMode; // 0 = CrossInk Default, 1 = Balanced, 2 = Light u8 fontFamily; - u8 fontSize; + u8 readerFontPointSize; // physical point size; versions 2-5 stored a size slot u8 lineHeightPercent; + u8 wordSpacing; // 0 = natural font spacing; 1-4 widen each gap by ~75% per level u8 orientation; u8 screenMargin; u8 publisherPageNumbers; @@ -136,13 +143,16 @@ struct ReaderSettingsBin { u8 bionicReadingEnabled; u8 guideReadingEnabled; u8 snapshotRenderMode; + u8 indexingMethod; // 0 = incremental, 1 = full section char sdFontFamilyName[64]; + char dictionarySdFontFamilyName[64]; // meaningful only when flag bit 3 is set + u8 dictionaryFontPointSize; // 0 = follow reader size }; ``` ## `/.crosspoint/clippings/_.bin` -### Version 1 +### Versions 1-3 Clipping files store the per-book EPUB clipping list used by the reader. A saved clipping is also what CrossInk renders as an in-reader highlight; there is @@ -160,8 +170,8 @@ example: Binary layout: -- `[0]` version (`1`) -- `[1-2]` clipping count (`uint16_t` LE, maximum `64`) +- `[0]` version (`1`, `2`, or current version `3`) +- `[1-2]` clipping count (`uint16_t` LE, maximum `256`) - book title (`String`) - book author (`String`) - book path (`String`) @@ -175,8 +185,13 @@ Binary layout: - `wordCount` (`uint16_t` LE) - `paragraphIndex` (`uint16_t` LE, `UINT16_MAX` when unavailable) - `timestamp` (`uint32_t` LE, seconds since firmware boot when saved) + - version 3 only: reader layout signature (`uint32_t` LE; font, spacing, + viewport, and other section-layout inputs) - `chapterTitle` (`char[48]`, null-terminated/truncated) - - selected text (`String`, truncated to `512` bytes for the in-app store) + - version 1: selected text (`String`, truncated to `512` bytes for the + in-app store) + - versions 2-3: selected-text length (`uint16_t` LE) followed by that many + UTF-8 bytes (maximum `512`) CrossInk uses the stored spine/page/paragraph fields as anchors, then searches near that location for the stored clipping text after relayout. This is similar @@ -184,6 +199,13 @@ to keeping both a DOM position and a text quote in a web app: the numeric position gives a fast starting point, while the text makes jumps and highlights survive font, layout, or page-count changes when possible. +Version 3 records which reader layout produced the numeric page/word anchor. +When that signature differs, CrossInk ignores the stale numeric range and +matches the saved text instead, including when both layouts happen to have the +same total page count. Versions 1-2 retain their numeric fast path until the +reader sees a relayout, when it stamps the previously active layout before +rebuilding. + Creating a clipping also appends a Kindle-style export entry to `/My Clippings.txt` on the SD-card root. That text export can keep up to `2000` bytes of the selected text and is append-only. Removing a clipping from the @@ -229,25 +251,65 @@ Binary layout: ## `section.bin` -### Version 44 +### Version 59 Each file in `sections/*.bin` stores one laid-out spine section. The header is also the cache-busting key: if any layout-affecting setting differs from the current reader settings, the section is discarded and rebuilt. -Version 44 invalidates older section caches so `TextBlock` word data can be -stored as one flat arena. It includes: +Version 59 adds a compact page-start visible-text-offset lookup table. The +offset is a Unicode codepoint coordinate in the spine XHTML, so reader progress +and KOReader sync can return to the same content after a font, orientation, or +indexing-method change instead of relying on a page percentage. Suspended +incremental caches store the same table for their readable prefix; a target +beyond that prefix must continue indexing before it can be resolved. + +Version 57 is binary-identical to version 56. The version was bumped because +word-gap suppression now applies only to tokens glued together in the source. +Older caches could collapse explicit spaces between Hangul words, so full and +suspended partial section caches rebuild together. Version 58 recalculates +Bionic Reading split-run offsets with the renderer's combined advance and +kerning rounding, so old cached page positions rebuild. + +Version 56 changes `
` layout: a line break after text no longer reapplies +the containing block's top or bottom spacing, while an empty `
` block keeps +the existing scene-break gap. Full and suspended partial section caches rebuild +together. Version 55 assigns compact IDs to internal EPUB links. The ID is +stored in the existing per-word flags byte and in each page's footnote entry so +touch devices can map tapped text to the existing fragment-navigation path +without retaining another per-word data structure. Version 54 adds compact +ruby-text annotations to serialized text blocks. Only words that begin a ruby +group store annotation text; continuation words use a dedicated style bit. This +keeps books without ruby markup unchanged apart from the cache version while +avoiding an empty string allocation for every word. +Version 53 stores each image's EPUB-internal source path so section indexing can +read only its header and defer full extraction until the page is shown. Version +52 keeps Guide Dots centered when extra word spacing is enabled. Version 51 +preserves continuation state for oversized CJK word fragments. Version 50 +paginates chapter-heading image runs within the reader viewport so they do not +overflow into the reserved status-bar area. Version 49 stores Bionic Reading +split-run offsets in visual order so RTL word prefixes render on the right. +Version 48 changed Arabic contextual shaping and text measurement, so cached +word positions from version 47 no longer match what `drawText` renders. + +Version 48 makes the EPUB word-spacing level widen the natural inter-word gap +(each level adds 10 pixels), which changes laid-out word positions, so +older sections must rebuild. Version 46 added the EPUB word-spacing level to the +cache-busting header. It retains the flat `TextBlock` arena and chapter-opener +anchor behavior introduced in version 45. It includes: - cache-busting fields for font, line compression, extra paragraph spacing, forced paragraph indents, paragraph alignment, viewport size, hyphenation, - embedded CSS, image rendering mode, Bionic Reading, Guide Dots, and EPUB - render mode + embedded CSS, image rendering mode, Bionic Reading, Guide Dots, word spacing, + and EPUB render mode - page offset LUT - anchor-to-page map for fragment and footnote navigation - paragraph and list-item LUTs used by KOReader sync page refinement +- visible-text-offset LUT used to resolve page positions across reflow and sync - optional per-word Bionic Reading split metadata - optional per-word Guide Dot x-offset metadata -- optional per-word text flags for CSS backgrounds and layout-inserted hyphens +- optional per-word text flags for CSS backgrounds, layout-inserted hyphens, + and internal-link IDs - reading-aid layout that stores Bionic Reading and Guide Dots as per-word metadata instead of temporary layout words - publisher CSS page-break handling and adjusted justification spacing baked into page layout - table fragments @@ -267,7 +329,7 @@ import std.mem; import std.string; import std.core; -#define EXPECTED_VERSION 44 +#define EXPECTED_VERSION 59 #define MAX_STRING_LENGTH 65535 #define FOOTNOTE_NUMBER_LEN 32 #define FOOTNOTE_HREF_LEN 96 @@ -358,6 +420,7 @@ struct TextBlock { struct ImageBlock { String imagePath; + String sourcePath; s16 width; s16 height; }; @@ -423,6 +486,7 @@ struct PageElement { struct FootnoteEntry { char number[FOOTNOTE_NUMBER_LEN]; char href[FOOTNOTE_HREF_LEN]; + u8 linkId; }; struct PublisherPageMarker { @@ -474,6 +538,7 @@ struct SectionBin { u8 imageRendering; bool bionicReadingEnabled; bool guideReadingEnabled; + u8 wordSpacing; u8 renderMode; // 0 = CrossInk Default, 1 = Balanced, 2 = Light u16 pageCount; diff --git a/docs/font-build-variants.md b/docs/font-build-variants.md deleted file mode 100644 index 81d71a4ccc..0000000000 --- a/docs/font-build-variants.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Font Build Variants -nav_order: 12 ---- - -# Font Build Variants - -CrossInk ships multiple firmware build variants because the ESP32-C3 has limited flash and RAM. The variants trade available point sizes against emoji and miscellaneous-symbol support. - -## Variants - -### `tiny` - -No 18 pt or 20 pt font size. This is the preferred general-purpose build. - -- Emoji and miscellaneous-symbol support -- 4 font sizes: - - 10 pt - - 12 pt - - 14 pt - - 16 pt - -### `xlarge` - -8 pt, 10 pt, and 12 pt font sizes are removed to reduce build size while still supporting emoji and symbols. - -- Emoji and miscellaneous-symbol support -- 3 font sizes: - - 16 pt - - 18 pt - - 20 pt - -## Flashing A Variant - -Download the matching `firmware-*.bin` from the [releases page](https://github.com/uxjulia/CrossInk/releases), or build and upload locally with PlatformIO: - -```sh -pio run -e tiny --target upload -``` - -Replace `tiny` with `xlarge` as needed. diff --git a/docs/i18n.md b/docs/i18n.md index 42110757d2..12a2dfe6c9 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -33,6 +33,9 @@ The current translation files are: - Valencià (`CAV`) - עברית (`HE`) - Tiếng Việt (`VI`) +- Slovenčina (`SK`) +- Português (Portugal) (`PT2`) +- العربية (`AR`) ## Files diff --git a/docs/index.md b/docs/index.md index bf9de0ff43..0f904bc180 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,13 +11,17 @@ Welcome to CrossInk, a personal fork of CrossPoint Reader. ## User Docs +- [User Guide](./user-guide.md) - [Installation](./installation.md) -- [Font Build Variants](./font-build-variants.md) +- [SD Card Fonts](./sd-card-fonts.md) - [Reader Features](./reader-features.md) +- [Dictionary](./dictionary.md) +- [Controls](./controls.md) +- [Reading Stats Sync](./reading-stats-sync.md) +- [Nearby File Transfer](./nearby-file-transfer.md) - [Nearby Position Sync](./nearby-position-sync.md) +- [EPUB Indexing Methods](./epub-indexing.md) - [EPUB Render Modes](./epub-render-modes.md) -- [Controls](./controls.md) -- [Simulator](./simulator.md) - [Data Cache](./data-cache.md) - [Web Server Guide](./webserver.md) - [Troubleshooting](./troubleshooting.md) diff --git a/docs/installation.md b/docs/installation.md index a4447cee3f..1c90c8687a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,19 +1,40 @@ --- title: Installation -nav_order: 14 +nav_order: 2 --- # Installation -## Web Installer +## Supported Devices -1. Download the `firmware-*.bin` file for the build variant of your choosing from the [releases page](https://github.com/uxjulia/CrossInk/releases). -2. Connect your Xteink X4 or X3 to your computer via USB-C and wake/unlock the device. -3. Go to and choose your device. -4. Select **Custom .bin** from the options. -5. Choose the `firmware-*.bin` file you downloaded and click **Flash**. +- Xteink X3, X4 +- Seeed Studio Sticky -To revert back to the official firmware, flash the latest official firmware from . +## Web Installation via USB + +#### For new installs and updates. + +1. Navigate to [https://inky.crossink.dev/#flash-tools](https://inky.crossink.dev/#flash-tools) and select your device model. +2. The latest version will be automatically selected, but if you ever want to revert to an earlier build, you can select it from the dropdown. +3. Choose the firmware option you want to install. +4. Click on the "Flash Firmware" button + +## SD Card Firmware Update + +#### For installing newer versions of CrossInk. Can be used by USB locked devices. + +1. Follow the same steps from the Web Installation method above. There will be an option to download the firmware instead of USB flashing. +2. Place the downloaded `firmware-*.bin` file on your SD card. You can place this file anywhere. +3. Go to `Settings > System > SD Card Firmware Update` and navigate to the `.bin` file and update. + +## USB Locked Devices + +If your device has USB data transfer disabled: + +1. Navigate to [https://inky.crossink.dev/#flash-tools](https://inky.crossink.dev/#flash-tools) and check the box for "I have a locked device" at the top. +2. The latest version will be automatically selected, but if you ever want to revert to an earlier build, you can select it from the dropdown. +3. Choose the firmware option you want to download. +4. Click on the "Download update.bin" button and follow the instructions. ## Command Line diff --git a/docs/nearby-file-transfer.md b/docs/nearby-file-transfer.md new file mode 100644 index 0000000000..e44793702d --- /dev/null +++ b/docs/nearby-file-transfer.md @@ -0,0 +1,75 @@ +--- +title: Nearby File Transfer +nav_order: 7.5 +--- + +# Nearby File Transfer + +Nearby File Transfer sends one supported file directly between two nearby +CrossInk readers. It uses ESP-NOW, so it does not need a Wi-Fi network, +internet connection, account, or computer. + +## Requirements + +- Both readers must run compatible CrossInk builds and be nearby. +- The receiving reader needs enough free SD-card space for the complete file. +- Supported file types are EPUB, TXT, XTC, XTCH, PNG, and BMP. + +## Receive a File + +1. On the receiving reader, open **File Transfer > Receive File**. +2. Select **Start receiving**. The screen stays open while it waits for a + sender. +3. To change where received files are saved, select **Change folder** before + starting. CrossInk remembers this folder; the default is the SD-card root. +4. When the incoming-file prompt appears, check the sender name, file name, + and size, then select **Accept**. Use **Cancel** or **Back** to decline it. +5. Wait for the receiving progress screen to finish. After verification, select + **Read** for a book or **Open** for an image, or use **Back** to return. + +If a file with the same name already exists in the destination folder, +CrossInk asks whether to **Replace** it, **Keep both**, or cancel. **Keep both** +creates a separate copy with a numbered suffix. + +## Send a File + +1. On the receiving reader, start **Receive File** as described above. +2. On the sending reader, find a supported file in **Browse Files** or + **Recent Books**. +3. Open the file action menu and select **Send to Nearby Device**. +4. Wait for the receiving reader to appear, select it, and wait for approval. +5. Keep both readers on their transfer screens until the sender reports + **File sent** and the receiver reports **File received**. + +The receiver must accept the offer before the file starts sending. Press +**Back** on either reader to cancel an in-progress transfer. + +## Transfer Safety + +CrossInk writes incoming data to a temporary file and verifies its size and +contents before making it available at the chosen destination. A cancelled or +failed transfer does not replace an existing file and does not leave a usable +partial book or image behind. + +## Troubleshooting + +**No receiving device appears** + +Make sure the receiver has selected **Start receiving**, both readers are close +together, and both run compatible CrossInk builds. Exit and reopen the transfer +screens on both readers, then try again. + +**Unsupported file type** + +Nearby File Transfer only supports EPUB, TXT, XTC, XTCH, PNG, and BMP files. +Use the web file manager, Calibre Wireless, or USB for other file types. + +**Not enough space on the SD card** + +Free space in the receiving folder's SD card, then start the transfer again. + +**The other device stopped responding** or **Transfer failed** + +Keep both readers nearby and on their transfer screens until completion, then +retry the whole transfer. The sender keeps the original file, and the receiver +only keeps the new file after verification succeeds. diff --git a/docs/reader-features.md b/docs/reader-features.md index 718267a201..342bab1a8b 100644 --- a/docs/reader-features.md +++ b/docs/reader-features.md @@ -1,6 +1,6 @@ --- title: Reader Features -nav_order: 17 +nav_order: 5 --- # Reader Features @@ -18,16 +18,59 @@ Open the reader menu and select **Reader Options** to adjust settings such as: - Font family - Font size - Line spacing +- Word spacing (EPUB) - Margins - Alignment - Image rendering -- Bionic Reading / Guide Dots +- [Publisher Page Numbers](#publisher-page-numbers) +- [Stable Page Numbers](#stable-page-numbers), when the book includes CrossInk reference metadata +- [Bionic Reading](#bionic-reading) / Guide Dots - Dark Reader Mode Changes take effect immediately. For books that are slow to index or fail because of complex publisher styling, -see [EPUB Render Modes](./epub-render-modes.md). +see [EPUB Indexing Methods](./epub-indexing.md) and +[EPUB Render Modes](./epub-render-modes.md). + +## Bionic Reading + +Bionic Reading is a reading aid that bolds the first portion of each word, +guiding your eyes to natural fixation points and helping you read faster with +less effort. Some readers, particularly those with ADHD, find it helps them +stay engaged with the text and reduces mind-wandering. + +### Enabling Bionic Reading + +1. Open **Settings > Reader**. +2. Toggle **Bionic Reading** on. + +Toggling the setting triggers a re-index of the current book, just like changing +font settings. Once indexing is complete, page turns proceed as normal. No +changes are made to the EPUB file. + +### Examples + +Comparison of the same page with and without Bionic Reading enabled + +_Left: Bionic Reading off. Right: Bionic Reading on. Both using Literata._ + +Bionic Reading with Noto Serif font + +_Bionic Reading with Noto Serif font._ + +Bionic Reading with Merriweather font + +_Bionic Reading with Merriweather font._ + +Bionic Reading with Atkinson Hyperlegible Next font + +_Bionic Reading with Atkinson Hyperlegible Next font._ + +### Notes + +Bionic Reading only applies to regular body text. Already-bold text, including +headings and emphasis, is left unchanged. ## Font Sizes And Downloadable Font Ranges @@ -38,7 +81,6 @@ The reader can also use SD-card font packs with selectable font-size ranges. Thi Related docs: - [SD Card Fonts](./sd-card-fonts.md) -- [Font Build Variants](./font-build-variants.md) ## Dark Reader Mode @@ -54,6 +96,56 @@ CrossInk supports adjustable reader line spacing from compact to wide spacing. Use this when a book feels visually cramped, or when larger fonts need more vertical room to stay comfortable. +## Word Spacing + +EPUB readers can choose from five word-spacing levels: **Normal** and levels +**1** through **4**. Higher levels add more space between words, which can make +text easier to scan without changing the font size or line height. + +Open the reader menu, then select **Reader Options > Font Options > Word +Spacing**. The current EPUB is laid out again when you change this setting, so +the number and positions of pages may change. Word Spacing is not available for +TXT books. + +## Publisher Page Numbers + +Publisher Page Numbers show page labels supplied by the EPUB, such as the +printed page numbers from a physical edition. When the book includes labeled +page-break markers, CrossInk displays those labels in the reader margin beside +the matching content. + +To enable them, open the reader menu and select **Reader Options > Publisher +Page Numbers**. If an EPUB does not contain labeled page-break markers, there +are no publisher page numbers for CrossInk to display. + +Publisher page markers are preserved by **CrossInk Default** and **Balanced** +render modes. **Light** and **Safe Mode** omit them when simplifying a difficult +book's layout. + +## Stable Page Numbers + +Stable Page Numbers show a consistent reference page number in the reader's +status bar, such as `120/540`. They are calculated from fixed reference-page +metadata in the EPUB instead of the current screen layout, so they remain +consistent when you change fonts, spacing, orientation, or indexing mode. + +To enable them: + +1. Open the reader menu and select **Reader Options**. +2. Select **Customize Status Bar**. +3. Toggle **Stable Page Numbers** on. + +The option appears only when the current EPUB contains valid CrossInk reference +metadata. To create that metadata, optimize the EPUB in the CrossInk web +interface or with [Inky](https://inky.crossink.dev) before uploading it to the reader. +In the optimizer's settings, the **Characters per Page** controls the reference-page size; the default +is 1,500 characters. Lower values create more reference pages, while higher +values create fewer. + +Stable Page Numbers are not publisher or printed-edition page numbers. Use +[Publisher Page Numbers](#publisher-page-numbers) when you want the page labels +provided by the book itself. + ## Guide Dots Guide Dots adds small dots between words. The idea comes from speed-reading guidance where focusing on the space between words can help peripheral vision pick up more of the surrounding text. @@ -105,7 +197,15 @@ You can: ## Clippings And Highlights CrossInk supports EPUB text clippings from the reader. Use **Create Clipping** -from the reader menu, select text on the current page, and save it. +from the reader menu or a configured shortcut, select text, and save it. + +On button devices, move the cursor with the direction buttons, press **Select** +at the first word, move to the last word, then press **Done**. On touchscreen +devices, a tap saves the single word you touch. To save a range, touch and hold +the first word until range selection begins, drag to the last word, then lift +your finger; CrossInk saves the clipping immediately. A touch drag does not +turn pages, so use the direction buttons if a clipping must extend to another +page. A saved clipping is used in three ways: @@ -118,8 +218,13 @@ clipping from CrossInk removes the saved clipping and highlight from the device UI, but it does not rewrite old entries that were already appended to `/My Clippings.txt`. -For storage paths and binary format details, see [Data Cache](./data-cache.md) -and [File Formats](./file-formats.md). +Open **Clippings** from the reader menu's bookmarks tab after saving a +clipping. On touchscreen devices, tap a clipping to read its full text, swipe +up or down to move through a long clipping, and tap **Open** to return to its +location in the book. Touch and hold a clipping in the list, or its text in +the detail view, to open the delete action. Deleting removes the in-app +clipping and its highlight, not an entry already exported to +`/My Clippings.txt`. ## Reading Stats @@ -135,6 +240,8 @@ Tracked stats include: Recent CrossInk versions expanded this into a larger stats system, including synced totals, richer X3 stats screens, reading-streak and time charts, editable stat dates, idle-time filtering, reset controls, and all-time stats backup options. +**Note**: Date-related stats require a device with a real-time clock (RTC) module. The X4 does not have an RTC module, therefore will not have as detailed stats as the X3. + Reading stats can also be used as a sleep screen, including the Minimal Stats sleep screen on supported builds. For two-device syncing, see [Reading Stats Sync](./reading-stats-sync.md). diff --git a/docs/reading-stats-sync.md b/docs/reading-stats-sync.md index 93a6a74013..8b21e5fdd1 100644 --- a/docs/reading-stats-sync.md +++ b/docs/reading-stats-sync.md @@ -1,11 +1,11 @@ --- title: Reading Stats Sync -nav_order: 6 +nav_order: 7 --- # Reading Stats Sync -CrossInk can sync all-time reading stats between nearby readers running CrossInk from **File Transfer > Nearby Stats Sync**. The sync is direct reader-to-reader; it does not use a server, account, or one "main" reader. This only works with CrossInk reading stats. This will *NOT* work with any other firmware's reading stats. +CrossInk can sync all-time reading stats between nearby readers running CrossInk from **File Transfer > Sync Stats**. The sync is direct reader-to-reader; it does not use a server, account, or one "main" reader. This only works with CrossInk reading stats. This will _NOT_ work with any other firmware's reading stats. ## What Gets Synced @@ -22,7 +22,7 @@ Each device owns one record, and the Reading Stats screen sums all the records i ## How Nearby Sync Works -1. Open **File Transfer > Nearby Stats Sync** on both readers. +1. Open **File Transfer > Sync Stats** on both readers. 2. Press **Sync** on one reader only. 3. The readers find each other over ESP-NOW and exchange their local `/.crosspoint/global_stats.bin` payloads. @@ -65,7 +65,7 @@ To remove one old reader from the aggregate total, delete that reader's file fro /.crosspoint/synced_stats/device_aabbccddeeff.bin ``` -To stop using synced aggregate totals on a reader, delete the whole `/.crosspoint/synced_stats/` folder from that reader. As long as the folder is gone, local stats saves will not recreate it. Opening Nearby Stats Sync again will recreate the folder. +To stop using synced aggregate totals on a reader, delete the whole `/.crosspoint/synced_stats/` folder from that reader. As long as the folder is gone, local stats saves will not recreate it. Opening Sync Stats again will recreate the folder. To reset only this reader's local all-time totals, delete both: diff --git a/docs/sd-card-fonts.md b/docs/sd-card-fonts.md index f904cf0485..e40fca3d85 100644 --- a/docs/sd-card-fonts.md +++ b/docs/sd-card-fonts.md @@ -1,18 +1,25 @@ +--- +title: SD Card Fonts +nav_order: 4 +--- + # SD Card Fonts -CrossPoint supports loading additional fonts from the SD card, including fonts +CrossInk supports loading additional fonts from the SD card, including fonts with extended Unicode coverage (CJK, Cyrillic, Greek, etc.). ## Installing Fonts There are three ways to install fonts: -### Option 1: Download from device (recommended) +### Option 1: Download from device 1. Connect your CrossInk reader to Wi-Fi 2. Go to **Settings > Reader > Font Options > Manage Fonts** -3. Browse available font families and tap to download -4. Downloaded fonts appear immediately in **Settings > Reader > Font Family** +3. Browse available font families and select to download +4. Downloaded fonts appear immediately in **Settings > Reader > Font Options > Font Family** + +**Note**: To change the font sizes that are downloaded, change the option for `Download Font Size Range` _before_ downloading. ### Option 2: Upload via web browser @@ -21,45 +28,76 @@ There are three ways to install fonts: 3. Navigate to the **Fonts** tab 4. Upload `.cpfont` files using the upload form -### Option 3: Manual SD card copy - -1. Download font files from the - [crosspoint-fonts repository](https://github.com/crosspoint-reader/crosspoint-fonts) -2. Copy font family folders to one of two locations on your SD card: - - - `/.fonts/` — hidden directory (preferred; keeps the SD root tidy - when mounted on a desktop) - - `/fonts/` — visible directory (use this if your OS hides dot-files - and you'd rather see the folder in your file manager) - - Both roots are always scanned at boot and the results are merged: a - family installed in `/fonts/` shows up even when `/.fonts/` also - exists, and vice versa. The two roots only collide if the same family - name appears in both — in that case the copy in `/.fonts/` wins and - the duplicate in `/fonts/` is ignored. - - SD Card Root/ - ├── .fonts/ ← Hidden root (preferred) - │ └── Literata/ - │ ├── Literata_12.cpfont - │ ├── Literata_14.cpfont - │ ├── Literata_16.cpfont - │ └── Literata_18.cpfont - └── fonts/ ← Visible root (equally valid) - └── Merriweather/ - ├── Merriweather_12.cpfont - └── ... - -3. Insert the SD card and power on your CrossInk reader +### Option 3: Manual SD card copy (Fastest) + +1. Download font files from the + [CrossInk Fonts](https://github.com/uxjulia/crossink-fonts/tree/main/cpfonts) repository. + - Click the `.zip` file for the font you want then click on the download icon to download the raw file. +2. Copy font family folders to one of two locations on your SD card: + - `/.fonts/` — hidden directory (preferred; keeps the SD root tidy + when mounted on a desktop) + - `/fonts/` — visible directory (use this if your OS hides dot-files + and you'd rather see the folder in your file manager) + + Both roots are always scanned at boot and the results are merged: a + family installed in `/fonts/` shows up even when `/.fonts/` also + exists, and vice versa. The two roots only collide if the same family + name appears in both — in that case the copy in `/.fonts/` wins and + the duplicate in `/fonts/` is ignored. + + SD Card Root/ + ├── .fonts/ ← Hidden root (preferred) + │ └── Literata/ + │ ├── Literata_12.cpfont + │ ├── Literata_14.cpfont + │ ├── Literata_16.cpfont + │ └── Literata_18.cpfont + └── fonts/ ← Visible root (equally valid) + └── Merriweather/ + ├── Merriweather_12.cpfont + └── ... + +3. Insert the SD card and power on your CrossInk device + +## Dictionary Fonts + +EPUB books can use a different installed SD-card family for dictionary definitions. +This can be set globally or per-book via `Font Options`. If a +saved point size is no longer available, CrossInk chooses the closest file from +the dictionary family. If the device experiences low available RAM, you may see the +dictionary font fall back to your reader font. This is normal. + +### Generating dictionary font families + +Use the dictionary-specific builder to generate the complete family catalog with +the extra coverage used by dictionary definitions: + + python3 -m pip install -r lib/EpdFont/scripts/requirements.txt + python3 lib/EpdFont/scripts/build-dictionary-fonts.py \ + --output-dir ./generated-dictionary-fonts \ + --clean \ + --jobs 2 + +The dictionary build includes the `reading` ranges and the built-in ranges, plus +IPA and phonetic-extension characters (`U+0250–U+02FF` and `U+1D00–U+1DBF`) and +combining-mark ranges (`U+1DC0–U+1DFF`, `U+20D0–U+20FF`, and +`U+FE20–U+FE2F`). + +The default output is `../crossink-fonts/dictionary-fonts`. Use a separate +`--output-dir` for personal builds, because `--clean` removes the selected output +directory before generating the fonts. The output contains family folders and ZIP +archives; copy a family folder or unzip its archive into `/.fonts/` or `/fonts/` +on the SD card. Use `--only FamilyA,FamilyB` to generate selected families. ## Available Pre-Built Fonts -The current list of pre-built fonts is maintained in the -[crossink-fonts repository](https://github.com/uxjulia/crossink-fonts). Device-initiated downloads use the versioned font manifest published for the current `.cpfont` format. +You can view pre-built fonts available for download at [Inky](https://inky.crossink.dev/#downloads). + +## Converting Custom Fonts with CrossPoint's Font Builder -## Converting Custom Fonts +To convert your own TrueType/OpenType fonts use CrossPoint's [Font Builder](https://crosspointreader.com/fonts) -To convert your own TrueType/OpenType fonts: +## Converting Custom Fonts with Python ### Prerequisites @@ -89,26 +127,26 @@ To convert your own TrueType/OpenType fonts: ### Available Unicode interval presets -| Preset | Coverage | -|--------|----------| -| `ascii` | U+0020–U+007E (Basic Latin) | -| `latin1` | U+0080–U+00FF (Latin-1 Supplement) | -| `latin-ext` | European languages (Latin + Extended-A/B + punctuation + ligatures) | -| `greek` | Greek + Extended Greek | -| `cyrillic` | Cyrillic + Supplement | -| `hebrew` | Hebrew + Alphabetic Presentation Forms | -| `georgian` | Georgian + Georgian Supplement | -| `armenian` | Armenian | -| `ethiopic` | Ethiopic + Extended | -| `vietnamese` | Vietnamese subset (ơ/ư and combining marks) | -| `punctuation` | General punctuation (U+2000–U+206F) | -| `cjk` | CJK Unified Ideographs + Hiragana + Katakana + Fullwidth | -| `hangul` | Korean Hangul syllables + Jamo + Compatibility Jamo | -| `cherokee` | Cherokee (historic + supplement block) | -| `tifinagh` | Tifinagh | -| `symbols` | Math, currency, arrows, box-drawing, misc symbols, dingbats | -| `reading` | Literary fiction coverage: Latin, Greek, Cyrillic, math/symbol blocks, supplemental punctuation, and CJK quote marks | -| `builtin` | Matches the firmware's built-in font conversion intervals | +| Preset | Coverage | +| ------------- | -------------------------------------------------------------------------------------------------------------------- | +| `ascii` | U+0020–U+007E (Basic Latin) | +| `latin1` | U+0080–U+00FF (Latin-1 Supplement) | +| `latin-ext` | European languages (Latin + Extended-A/B + punctuation + ligatures) | +| `greek` | Greek + Extended Greek | +| `cyrillic` | Cyrillic + Supplement | +| `hebrew` | Hebrew + Alphabetic Presentation Forms | +| `georgian` | Georgian + Georgian Supplement | +| `armenian` | Armenian | +| `ethiopic` | Ethiopic + Extended | +| `vietnamese` | Vietnamese subset (ơ/ư and combining marks) | +| `punctuation` | General punctuation (U+2000–U+206F) | +| `cjk` | CJK Unified Ideographs + Hiragana + Katakana + Fullwidth | +| `hangul` | Korean Hangul syllables + Jamo + Compatibility Jamo | +| `cherokee` | Cherokee (historic + supplement block) | +| `tifinagh` | Tifinagh | +| `symbols` | Math, currency, arrows, box-drawing, misc symbols, dingbats | +| `reading` | Literary fiction coverage: Latin, Greek, Cyrillic, math/symbol blocks, supplemental punctuation, and CJK quote marks | +| `builtin` | Matches the firmware's built-in font conversion intervals | Combine presets with commas: `--intervals latin-ext,greek,cyrillic` diff --git a/docs/simulator.md b/docs/simulator.md index 2eb2a0828f..d348b58e07 100644 --- a/docs/simulator.md +++ b/docs/simulator.md @@ -38,6 +38,12 @@ pio run -e simulator .pio/build/simulator/program ``` +Use the X4 Pro environment to enable its touch, frontlight, and Home-key behavior: + +```sh +pio run -e x4-pro-simulator -t run_simulator +``` + ## Keyboard Controls | Key | Action | @@ -47,6 +53,9 @@ pio run -e simulator | Return | Confirm / Select | | Escape | Back | | P | Power | +| H | X4 Pro Home key (tap to go Home; hold for 700 ms to toggle the reader menu) | + +The `H` mapping is active only in `x4-pro-simulator`. ## Cache Note diff --git a/docs/translators.md b/docs/translators.md index bf77b81de8..bf6b24549c 100644 --- a/docs/translators.md +++ b/docs/translators.md @@ -1,6 +1,6 @@ --- title: Translators -nav_order: 7 +nav_order: 10 --- # Translators @@ -10,36 +10,40 @@ Official UI language support is determined by the YAML files in `lib/I18n/translations/`; see [i18n.md](./i18n.md) for the current supported language list. -## Contributing - -If you'd like to add your name to this list, please open a PR adding yourself and your Github link. Thank you! - ## French + - [Spigaw](https://github.com/Spigaw) - [CaptainFrito](https://github.com/CaptainFrito) ## German + - [DavidOrtmann](https://github.com/DavidOrtmann) ## Czech + - [brbla](https://github.com/brbla) ## Portuguese (Brazil) + - [yagofarias](https://github.com/yagofarias) ## Portuguese (Portugal) + - [victordomingos](https://github.com/victordomingos) ## Italian + - [andreaturchet](https://github.com/andreaturchet) - [fragolinux](https://github.com/fragolinux) - [alan0ford](https://github.com/alan0ford) ## Russian + - [madebyKir](https://github.com/madebyKir) - [mrtnvgr](https://github.com/mrtnvgr) ## Spanish + - [yeyeto2788](https://github.com/yeyeto2788) - [Skrzakk](https://github.com/Skrzakk) - [pablohc](https://github.com/pablohc) @@ -47,25 +51,32 @@ If you'd like to add your name to this list, please open a PR adding yourself an - [lpla](https://github.com/lpla) ## Swedish + - [dawiik](https://github.com/dawiik) - [steka](https://github.com/steka) ## Romanian + - [ariel-lindemann](https://github.com/ariel-lindemann) ## Catalan + - [angeldenom](https://github.com/angeldenom) - [lpla](https://github.com/lpla) ## Finnish + - [plahteenlahti](https://github.com/plahteenlahti) ## Ukrainian + - [mirus-ua](https://github.com/mirus-ua) - [KymAndriy](https://github.com/KymAndriy) ## Belarusian + - [Dexif](https://github.com/dexif) ## Danish + - [hajisan](https://github.com/hajisan) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index daa2b10c4f..71443dfcf2 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,12 +1,17 @@ +--- +title: Troubleshooting +nav_order: 14 +--- + # Troubleshooting This document shows common issues and possible solutions while using the device features. - [Troubleshooting](#troubleshooting) - - [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) + - [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) ### Cannot See the Device on the Network @@ -14,7 +19,7 @@ This document shows common issues and possible solutions while using the device **Solutions:** -1. Verify both devices are on the correct network +1. Verify both devices are on the correct network (must be a 2.4ghz network) - Check your computer/phone Wi-Fi settings - In **Join Network** mode, your computer/phone and CrossInk must be on the same Wi-Fi network - In **Create Hotspot** mode, your computer/phone must be connected to the `CrossPoint-Reader` hotspot @@ -34,7 +39,7 @@ This document shows common issues and possible solutions while using the device 1. Move closer to the Wi-Fi router, or use Create Hotspot mode for a direct connection 2. Check signal strength on the device (should be at least `||` or better) 3. Avoid interference from other devices -4. Try a different Wi-Fi network if available +4. Try a different Wi-Fi network if available (must be a 2.4ghz network) ### Upload Fails @@ -50,7 +55,7 @@ This document shows common issues and possible solutions while using the device ### Saved Password Not Working -**Problem:** Device fails to connect with saved credentials +**Problem:** Device fails to connect with saved credentials. This often happens when swapping between firmwares or devices due to the password hashing mechanism. **Solutions:** diff --git a/USER_GUIDE.md b/docs/user-guide.md similarity index 74% rename from USER_GUIDE.md rename to docs/user-guide.md index 829f9241ca..e15e3a676b 100644 --- a/USER_GUIDE.md +++ b/docs/user-guide.md @@ -1,8 +1,16 @@ +--- +title: User Guide +nav_order: 1.5 +--- + # CrossInk User Guide -Welcome to the **CrossInk** firmware. This guide outlines the hardware controls, navigation, and reading features of the device. +Welcome to the **CrossInk** firmware. This guide covers day-to-day device use. +For focused reference material, see [Reader Features](./reader-features.md), +[Controls](./controls.md), [SD Card Fonts](./sd-card-fonts.md), +[File Transfer](./webserver.md), and [Troubleshooting](./troubleshooting.md). -- [CrossInk User Guide](#CrossInk-user-guide) +- [CrossInk User Guide](#crossink-user-guide) - [1. Hardware Overview](#1-hardware-overview) - [Button Layout](#button-layout) - [Taking a Screenshot](#taking-a-screenshot) @@ -16,10 +24,6 @@ Welcome to the **CrossInk** firmware. This guide outlines the hardware controls, - [3.4 Recent Books Screen](#34-recent-books-screen) - [3.5 File Transfer Screen](#35-file-transfer-screen) - [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers) - - [Installing the Plugin in Calibre](#installing-the-plugin-in-calibre) - - [Configuring the CrossPoint Plugin in Calibre](#configuring-the-crosspoint-plugin-in-calibre) - - [Uploading Books](#uploading-books) - - [Removing a Book](#removing-a-book) - [3.6 Settings](#36-settings) - [3.6.1 Display](#361-display) - [3.6.2 Reader](#362-reader) @@ -28,8 +32,9 @@ Welcome to the **CrossInk** firmware. This guide outlines the hardware controls, - [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) - [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds) - [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup) - - [Option A: Free Public Server (`sync.koreader.rocks`)](#option-a-free-public-server-synckoreaderrocks) - - [Option B: Self-Hosted Server (Docker Compose)](#option-b-self-hosted-server-docker-compose) + - [Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)](#option-a-crosspoint-sync-server-synccrosspointreadercom-default) + - [Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)](#option-b-legacy-public-koreader-server-synckoreaderrocks) + - [Option C: Self-Hosted Server (Docker Compose)](#option-c-self-hosted-server-docker-compose) - [3.7 Sleep Screen](#37-sleep-screen) - [Cover settings](#cover-settings) - [Custom images](#custom-images) @@ -38,7 +43,8 @@ Welcome to the **CrossInk** firmware. This guide outlines the hardware controls, - [Page Turning](#page-turning) - [Chapter Navigation](#chapter-navigation) - [Auto Page Turn](#auto-page-turn) - - [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only) + - [Tilt Page Turn (X3 and Sticky)](#tilt-page-turn-x3-and-sticky) + - [Touch Reader Controls](#touch-reader-controls) - [Footnote Navigation](#footnote-navigation) - [System Navigation](#system-navigation) - [Supported Languages](#supported-languages) @@ -59,7 +65,7 @@ The device utilises the standard buttons on the Xteink X4 (in the same layout as | **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** | | **Right Side** | **Power**, **Volume Up**, **Volume Down**, **Reset** | -Button layout can be customized in the **[Controls Settings](#363-controls)**. +Button layout can be customized in **Settings > Controls**. ### Taking a Screenshot @@ -74,7 +80,7 @@ Alternatively, while reading a book, press the **Confirm** button to open the re ### Power On / Off To turn the device on or off, **press and hold the Power button for approximately half a second**. -In the **[Controls Settings](#363-controls)** you can configure the power button to turn the device off with a short press instead of a long one. +In **Settings > Controls > Power Button** you can configure the power button to turn the device off with a short press instead of a long one. To reboot the device (for example after a firmware update or if it's frozen), press and release the Reset button, and then quickly press and hold the Power button for a few seconds. @@ -112,9 +118,11 @@ The Recent Books screen lists the most recently opened books in a chronological ### 3.5 File Transfer Screen -The File Transfer screen allows you to upload and manage files on the device. When you enter the screen, choose **Join a Network**, **Calibre Wireless**, or **Create Hotspot**. The reader then starts the web server for the selected mode. +The File Transfer screen allows you to upload and manage files on the device. +Choose **Join a Network**, **Calibre Wireless**, or **Create Hotspot** to start +the web server for the selected mode. -See the [web server docs](./docs/webserver.md) for more information on how to connect to the web server and upload files. +See the [File Transfer guide](./webserver.md) for connection and upload details. The web file manager can upload, download, rename, move, and delete files on the device. @@ -124,29 +132,33 @@ Download links for files already on the device are available in the web interfac A **Wi-Fi signal strength indicator** (dBm) is displayed on-screen during joined-network web server sessions. +The same screen also has **Receive File**, which receives a supported +book or image directly from another nearby CrossInk reader without joining a +Wi-Fi network. See [Nearby File Transfer](./nearby-file-transfer.md) for the +complete sender and receiver workflow. + > [!TIP] -> Advanced users can also manage files programmatically or via the command line using `curl`. See the [web server docs](./docs/webserver.md) for details. +> Advanced users can manage files programmatically with the same HTTP endpoints +> used by the web interface. The browser interface is the supported path for +> normal file management. ### 3.5.1 Calibre Wireless Transfers CrossInk supports sending books from Calibre using the CrossPoint Reader device plugin. -1. Install the plugin in Calibre: - - Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin. - - - Download the zip file. - - - Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file. - -2. On the device: File Transfer -> Calibre Wireless, then join a network. - -3. Make sure your computer is on the same Wi-Fi network. - -4. In Calibre, click "Send to device" to transfer books. +1. Download the current `crosspoint_reader` plugin ZIP from the + [CrossPoint Reader plugin releases](https://github.com/crosspoint-reader/calibre-plugins/releases). +2. In Calibre, open **Preferences > Plugins > Load plugin from file** and select + that ZIP. Restart Calibre if it asks you to. +3. On the device, open **File Transfer > Calibre Wireless** and join the same + 2.4 GHz Wi-Fi network as the computer. +4. Keep the Calibre Wireless screen open, then use Calibre's **Send to device** + action. The device screen shows the transfer progress and completion notice. ### 3.6 Settings -The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust: +The Settings screen groups options by purpose. The exact choices can vary by +device model and build. #### 3.6.1 Display @@ -161,6 +173,7 @@ The Settings screen allows you to configure the device's behavior. There are a f - "Reading Stats" - Recent reading stats on the sleep screen - "Minimal" - A minimal sleep screen - "Minimal Stats" - A minimal stats sleep screen on supported devices + - "Dashboard" - A dashboard-style sleep screen based on the Dashboard theme - "Quick Resume" - Keeps the current content visible while sleeping - **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected: @@ -179,11 +192,15 @@ The Settings screen allows you to configure the device's behavior. There are a f - "In Reader" - Show battery percentage everywhere except in reading mode - "Always" - Always hide battery percentage +- **Hide Clock**: On devices with a real-time clock, choose whether the clock is + shown everywhere, hidden only in the reader, or always hidden. + - **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting; options are every 1, 5, 10, 15, or 30 pages. - **UI Theme**: Set which UI theme to use: - - "Classic" - The original Crosspoint theme + - "Classic" - The original CrossInk theme - "Minimal" - A minimal theme with a large book cover + - "Dashboard" - A dashboard-style home layout - "Lyra" - A theme with simple icons featuring your current book - "Lyra Extended" - Lyra, but displays 3 books instead of 1 on the **[Home Screen](#31-home-screen)** - "Lyra Carousel" - A carousel-based Lyra home layout @@ -204,37 +221,45 @@ The Settings screen allows you to configure the device's behavior. There are a f - "Lexend Deca" (default) - "Bitter" -- **Reader Font Size**: Adjust the text size for reading. Available sizes depend on the firmware variant and can include sizes ranging from 8pt to 20pt. +- **Reader Font Size**: Adjust the text size for reading, built-in font sizes include: 10, 12, 14, and 16 pt. - **Reader Line Spacing**: Adjust the line height as a percentage. +- **Word Spacing**: In EPUB books, choose **Normal** or one of four wider + spacing levels between words. Open the reader menu, then select **Reader + Options > Font Options > Word Spacing**. Changing it reflows the current + book, so page positions may change; it is not available for TXT books. + - **Reader Screen Margin**: Controls the screen margins in Reading Mode between 5 and 40 pixels in 5-pixel increments. - **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", "Right", or "Book's Style". -- **Embedded Style**: Whether to use the EPUB file's embedded HTML and CSS stylisation and formatting; options are "ON" or "OFF". +- **Publisher Page Numbers**: Show page numbers supplied by the EPUB when the + book includes them. - **Hyphenation**: Whether to hyphenate text in Reading Mode; options are "ON" or "OFF". - **Reading Orientation**: Set the screen orientation for reading EPUB files: - "Portrait" (default) - Standard portrait orientation - "Landscape CW" - Landscape, rotated clockwise - - "Inverted" - Portrait, upside down + - "Portrait 180" - Portrait, upside down - "Landscape CCW" - Landscape, rotated counter-clockwise - **Extra Paragraph Spacing**: Set how to handle paragraph breaks: - "ON" - Vertical space will be added between paragraphs in Reading Mode - "OFF" - Paragraphs will not have vertical space added, but will have first-line indentation -- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly. - -- **Images**: Whether to display embedded images found in EPUB files; options are "Display" (default), "Placeholder", or "Suppress". - -- **Bionic Reading**: Bolds the first part of each word to create visual fixation points. This can help improve reading speed and focus; options are "ON" or "OFF" (default). +- **Reader Dark Mode**, **Embedded Style**, **Images**, **Bionic Reading**, and + **Guide Dots** are directly available from the Reader settings. See + [Reader Features](./reader-features.md) for their behavior, including the + [Bionic Reading](./reader-features.md#bionic-reading) guide. -- **Guide Dots**: Adds guide dots between words; options are "ON" or "OFF" (default). +- **Touch Reader Controls**: Enable or disable touchscreen page turns and + reader gestures on supported devices. **Disable Touchscreen** blocks touch + input while a book is open, while leaving touch available in reader menus so + you can turn it back on. -- **Customise Status Bar**: Configure the status bar displayed while reading: +- **Customize Status Bar**: Configure the status bar displayed while reading: - Chapter Page Count - Show/Hide the current page in the chapter (ex: 5/25). Page count may change based on the font size and margins set. - Book Progress Percentage - Show/Hide the current percent progress in the book. - Progress Bar - Show/Hide a progress bar for either the book or chapter. @@ -248,7 +273,8 @@ The Settings screen allows you to configure the device's behavior. There are a f - **Power Button**: Configure short-press and long-press power button actions. -- **Front Buttons**: Configure front-button remapping, orientation awareness, front-button long-press behavior, and the long-press menu action. +- **Front Buttons**: Configure front-button remapping, orientation awareness, + reader-only long-press behavior, Back action, and Menu action. - **Side Buttons**: Configure side-button layout, orientation awareness, and side-button long-press behavior. @@ -258,11 +284,11 @@ The Settings screen allows you to configure the device's behavior. There are a f - **Side Button Long-press Action**: Set whether long-pressing side buttons does nothing, skips chapters, changes font size, or changes orientation. -- **Short-press Action / Long-press Action**: Controls the effect of a short or long press of the power button: +- **Short-press Action / Long-press Action**: Controls the effect of a short or long press of the power button. Available actions include: - "Ignore" (default) - Require a long press to turn off the device - "Sleep" - A short press puts the device into sleep mode - "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off - - "Toggle Bookmark", "Reading Stats", "Mark Finished", "Refresh", "Change Font", "Guide Dots", "Bionic Reading", "Auto Page Turn", "Sync Progress", "File Transfer", "Screenshot", or "Dark Mode" - Run the matching reader action + - "Toggle Bookmark", "Reading Stats", "Mark Finished", "Refresh", "Change Font", "Guide Dots", "Bionic Reading", "Auto Page Turn", "Sync Progress", "File Transfer", "Calibre Wireless", "Join a Network", "Create Hotspot", "Screenshot", "Dark Mode", "Browse Files", or "Save Clipping" - Run the matching action - "Footnotes" - A short press in reading mode opens the footnotes submenu; if only one footnote is present on the page, the referenced page is opened directly. The short press on the power button can be used to select the footnote in the submenu, and to go back to the original page after finish reading the footnote (like the back button). - **Quick-return from footnotes**: Toggles on and off the quick return functionality from the footnotes. When the functionality it's active, a short press of the power button will act as the back button from the footnotes page. @@ -271,17 +297,29 @@ The Settings screen allows you to configure the device's behavior. There are a f - **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep. Values are in minutes, with a "Never" option at the end of the range. +- **Device**: Set the device name and time-to-sleep timeout. Devices with a + real-time clock also expose clock format, UTC offset, and a sync action. + +- **Files & Cache**: Configure hidden files, file extensions, file-browser view, + finished-book behavior, and clear the reading cache. + +- **Reading Stats**: Configure stats tracking and idle-time filtering, and + access all-time stats backup/reset actions. + - **Wi-Fi Networks**: Connect to Wi-Fi networks for file transfers and firmware updates. -- **KOReader Sync**: Options for setting up KOReader for syncing book progress. +- **KOReader Sync**: Options for setting up KOReader for syncing book progress. **Smart sync** is the default for new configurations and auto-resolves simple push/pull decisions. Existing credential files retain **Ask every time** when migrated; you can switch Sync Behavior at any time if you prefer manual confirmation. - **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below. -- **Clear Reading Cache**: Clear the internal SD card cache. +- **Check for Updates** and **SD Firmware Update**: Check for firmware updates + over Wi-Fi or install a `firmware.bin` placed on the SD card. -- **Check for updates**: Check for CrossInk firmware updates over Wi-Fi. Firmware can also be updated without a USB connection by placing a `firmware.bin` file on the SD card. - -- **Language**: Set the UI language. CrossInk supports 24 languages: English, Spanish, French, German, Czech, Brazilian Portuguese, Russian, Swedish, Romanian, Catalan, Ukrainian, Belarusian, Italian, Polish, Finnish, Danish, Dutch, Turkish, Kazakh, Hungarian, Lithuanian, Slovenian, Valencian, and Hebrew. +- **Language**: Set the UI language. CrossInk supports 28 languages: English, + Spanish, French, German, Czech, Brazilian Portuguese, Russian, Swedish, + Romanian, Catalan, Ukrainian, Belarusian, Italian, Polish, Finnish, Danish, + Dutch, Turkish, Kazakh, Hungarian, Lithuanian, Slovenian, Valencian, Hebrew, + Vietnamese, Slovak, Portuguese (Portugal), and Arabic. #### 3.6.5 OPDS Servers (Multiple Libraries) @@ -311,14 +349,14 @@ You can also manage OPDS servers from the web interface while in File Transfer m 2. Open `http:///settings`. 3. Use the **OPDS Servers** card to add, edit, or delete entries. -For web-based Wi-Fi network management, see [Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds). +For web-based Wi-Fi network management, see [File Transfer](./webserver.md). #### 3.6.6 Web Settings (Wi-Fi + OPDS) While in **File Transfer** mode, the web settings page includes management cards for both **Wi-Fi Networks** and **OPDS Servers**. 1. On device: open **File Transfer** and connect through **Join a Network** or **Create Hotspot**. -2. In a browser, open `http:///settings` or `http://crosspoint.local`. +2. In a browser, open `http:///settings` or `http://crosspoint.local/settings`. 3. In **Wi-Fi Networks**, add, edit, or delete saved network entries (SSID + optional password). 4. In **OPDS Servers**, add, edit, or delete OPDS catalogs. @@ -333,9 +371,35 @@ Behavior notes: CrossInk can sync reading progress with KOReader-compatible sync servers. It also interoperates with KOReader apps/devices when they use the same server and credentials. -##### Option A: Free Public Server (`sync.koreader.rocks`) +##### Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default) + +When **Sync Server URL** is left empty, CrossInk uses the free CrossPoint sync server at `https://sync.crosspointreader.com`. It speaks the standard KOReader sync protocol (so KOReader apps can use it too) and additionally stores an exact spine/page position for lossless CrossInk-to-CrossInk sync. + +1. On each CrossInk device: + - Go to **Settings -> System -> KOReader Sync**. + + - Set **Username** and **Password** (enter the plain password; CrossInk computes MD5 internally, and use the same values on all devices). + + - Leave **Sync Server URL** empty (or set it to `https://sync.crosspointreader.com`). + + - On the first device, run **Sign Up** once to create the account directly from the device. On every other device, just run **Authenticate**. + +Accounts are per server. Existing `sync.koreader.rocks` credentials do not exist on the CrossPoint server; either sign up again with the same username/password or use Option B to keep using the legacy server. -1. Register a user once (only if needed): +##### Option B: Legacy Public KOReader Server (`sync.koreader.rocks`) + +Use this if you already sync KOReader devices against the official public server. + +1. On each CrossInk device: + - Go to **Settings -> System -> KOReader Sync**. + + - Set **Sync Server URL** to `https://sync.koreader.rocks` (required; an empty URL now points at the CrossPoint server instead). + + - Set **Username** and **Password** to your existing KOReader Sync credentials. + + - Run **Authenticate**. + +2. If you do not have an account yet, run **Sign Up** on the device, or register once with curl: ```bash USERNAME="user" @@ -348,25 +412,9 @@ curl -i "https://sync.koreader.rocks/users/create" \ --data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}" ``` -Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices. - When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account. -2. On each device: - - Go to **Settings -> System -> KOReader Sync**. - - - Set **Username** and **Password** (enter the plain password; CrossInk computes MD5 internally, and use the same values on all devices). - - - Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server). - - - Run **Authenticate**. - -3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**. - - Choose **Apply Remote** to jump to remote progress. - - - Choose **Upload Local** to push current progress. - -##### Option B: Self-Hosted Server (Docker Compose) +##### Option C: Self-Hosted Server (Docker Compose) 1. Start a sync server: @@ -438,10 +486,12 @@ If this returns `HTTP 402` with `{"code":2002,"message":"Username is already reg If you use the HTTPS listener, use `https://:7200` (`curl -k` only for self-signed certificate testing). -5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**. - - Choose **Apply Remote** to jump to remote progress. +##### Syncing While Reading + +Once any of the options above is set up, press **Confirm** while reading to open the reader menu, then select **Sync Progress**. Alternatively, set **Settings -> Controls -> Long-press Menu** to **KOSync** and hold Confirm to launch sync directly. - - Choose **Upload Local** to push current progress. +- With **Sync Behavior** set to **Ask every time**, choose **Apply Remote** to jump to remote progress or **Upload Local** to push current progress. +- With **Sync Behavior** set to **Smart sync**, CrossInk auto-resolves simple cases: upload when no remote progress exists, confirm and leave both unchanged when local and remote progress are already synchronized, upload when local progress is further ahead, or apply remote when remote progress is further ahead. ### 3.7 Sleep Screen @@ -496,7 +546,7 @@ There are three ways to install fonts: Once installed, custom fonts appear in **Settings -> Reader -> Font Options -> Font Family** alongside the built-in fonts. -See [docs/sd-card-fonts.md](./docs/sd-card-fonts.md) for full installation details and SD card folder structure. +See [SD Card Fonts](./sd-card-fonts.md) for full installation details and SD card folder structure. --- @@ -511,7 +561,7 @@ Once you have opened a book, the button layout changes to facilitate reading. | **Previous Page** | Press **Left** _or_ **Volume Up** | | **Next Page** | Press **Right** _or_ **Volume Down** | -The role of the volume (side) buttons can be swapped in the **[Controls Settings](#363-controls)**. +The role of the volume (side) buttons can be swapped in **Settings > Controls > Side Buttons**. If the **Short-press Action** setting is set to "Page Turn", you can also turn to the next page by briefly pressing the Power button. @@ -520,15 +570,35 @@ If the **Short-press Action** setting is set to "Page Turn", you can also turn t - **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release. - **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release. -This feature can be disabled in the **[Controls Settings](#363-controls)** to help avoid changing chapters by mistake. +This feature can be disabled in **Settings > Controls > Front Buttons** to help avoid changing chapters by mistake. ### Auto Page Turn Auto Page Turn automatically advances pages at a set interval, useful for hands-free reading. This feature can be enabled and configured from the **[Reader Menu](#5-reader-menu)** while reading an EPUB. -### Tilt Page Turn (X3 only) +### Tilt Page Turn (X3 and Sticky) -On the **Xteink X3**, the gyroscope can be used to turn pages by tilting the device. This feature is available in **Settings -> Controls**. +On the **Xteink X3** and **Sticky**, the gyroscope can be used to turn pages by tilting the device. This feature and its left-right or forward-back direction are available in **Settings -> Controls**. + +### Touch Reader Controls + +On supported touchscreen devices, **Touch Reader Controls** is enabled by +default. In an open EPUB, tap the left third of the page to go back; tap the +rest of the page to go forward. You can also swipe right for the previous page +or left for the next page. The top and bottom gesture bands are reserved for +vertical gestures, so taps in those bands do not turn pages. + +Swipe down to open the reader menu and swipe up to return Home. On an X4 Pro, +which has a capacitive Home key, the vertical gestures are reversed: swipe up +to open the reader menu, and use a short press of the Home key to return Home. +A long press of that key also opens the reader menu. + +Turn **Touch Reader Controls** off in **Reader Options** to disable these +page-turn and gesture controls. **Disable Touchscreen** prevents touch input +while a book is open but keeps it available in reader menus. For the different +touch selection gestures used by [dictionary lookup](./dictionary.md#looking-up-a-word) +and [clippings](./reader-features.md#clippings-and-highlights), see those +feature guides. ### Footnote Navigation @@ -575,6 +645,8 @@ Available options include: - **Sync Progress** – Push or pull reading progress with a KOReader sync server (see [KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)). - **Reading Stats** – Open the current book's reading stats. - **Mark Finished / Mark Unfinished** – Toggle whether the current book is marked as finished. +- **Look Up Word / Lookup History** – Select words on the page and revisit recent per-book lookups when a dictionary is active. +- **Book Dictionary** – Choose a per-book dictionary override from the reader menu's settings tab. Press **Back** at any time to close the menu and return to your current page. @@ -598,13 +670,19 @@ To open bookmarks, press **Confirm** while inside a book. Then navigate to the * Bookmarks are stored as per-book `.bin` files in the `.crosspoint/bookmarks` folder. +### 5.3 Dictionary + +Dictionary lookup supports word selection, recent per-book history, chained lookups from definitions, and per-book dictionary overrides. See the [Dictionary guide](./dictionary.md) for installation and preparation instructions. + ## 6. Current Limitations & Roadmap Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates: -- **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up. +- **Cover Images:** Large cover images embedded into EPUB can take several + seconds to convert for the sleep screen and home-screen thumbnail. Use the + built-in [EPUB optimization](./webserver.md#epub-optimization) before upload + if a book is slow or memory-sensitive. - **Unsupported Image Formats:** Most JPG and PNG images in EPUBs render correctly. GIFs and progressive JPEGs are not supported and will fall back to an `[Image]` placeholder. -- **Dictionary Lookup:** Inline word lookup is not yet implemented. --- diff --git a/docs/webserver-endpoints.md b/docs/webserver-endpoints.md index 8033683475..11d58a9c29 100644 --- a/docs/webserver-endpoints.md +++ b/docs/webserver-endpoints.md @@ -39,7 +39,8 @@ Response: "rssi": -45, "freeHeap": 123456, "uptime": 3600, - "device": "X4" + "device": "X4", + "serial": "Not found" } ``` @@ -52,6 +53,7 @@ Response: | `freeHeap` | number | Free heap in bytes | | `uptime` | number | Seconds since boot | | `device` | string | `"X3"` or `"X4"` hardware detection | +| `serial` | string | Device serial number from eFuse, or `"Not found"` when unavailable | ## File Management @@ -192,8 +194,8 @@ Form parameters: | `path` | Yes, unless `paths` is provided | Single path to delete | | `paths` | Yes, unless `path` is provided | JSON array of paths to delete | -Protected items cannot be deleted. Non-empty folders are rejected. EPUB cache -data for deleted files is cleared. +Hidden and system items are protected while **Show Hidden Files** is off. +Non-empty folders are rejected. EPUB cache data for deleted files is cleared. ## Settings API @@ -332,6 +334,7 @@ Response: "name": "My Catalog", "url": "http://calibre.local:8080/opds", "username": "reader", + "filenameFormat": "author_title", "hasPassword": true } ] @@ -340,7 +343,9 @@ Response: ### `POST /api/opds` Adds or updates an OPDS server. Include `index` to update an existing entry. -If `password` is omitted during an update, the existing password is preserved. +If `password` or `filenameFormat` is omitted during an update, the existing value +is preserved. `filenameFormat` is either `author_title` (the default) or +`title_author`. ```bash curl -X POST \ diff --git a/docs/webserver.md b/docs/webserver.md index ec3d6a315d..cd421d6f02 100644 --- a/docs/webserver.md +++ b/docs/webserver.md @@ -23,18 +23,23 @@ networks or in hotspot mode when you control who is connected. 1. From the Home screen, select **File Transfer**. 2. Choose one of the available modes: -| Mode | Use when | -|------|----------| -| **Join Network** | You want the reader to join an existing Wi-Fi network. | +| Mode | Use when | +| -------------------- | ---------------------------------------------------------------------- | +| **Join Network** | You want the reader to join an existing Wi-Fi network. | | **Calibre Wireless** | You want to receive books from the CrossPoint Calibre plugin workflow. | -| **Create Hotspot** | You want the reader to create its own open Wi-Fi network. | +| **Create Hotspot** | You want the reader to create its own open Wi-Fi network. | ## Join Network Mode 1. Select **Join Network**. -2. Pick a 2.4 GHz Wi-Fi network from the scan results. -3. Enter the password if prompted. -4. Save credentials if you want the reader to reconnect automatically next time. +2. If you have saved Wi-Fi credentials, CrossPoint first tries the last + connected network, then other visible saved networks in signal-strength + order. Press **Back** to cancel or **Confirm** to stop auto-connect and show + the network list. +3. If the network list is shown, pick a 2.4 GHz Wi-Fi network from the scan + results. +4. Enter the password if prompted. +5. Save credentials if you want the reader to reconnect automatically next time. After connection, the reader shows: @@ -106,13 +111,12 @@ The default optimization path converts images for e-ink reading, limits them to the target device size, saves them as JPEG at 85% quality, and applies basic EPUB repairs such as safer SVG handling. Advanced Mode lets you pick the target device, JPEG quality, image split or rotation handling, split overlap, and -whether EPUB sections over 2,000 visible words should be split into smaller -reader sections. +whether large EPUB sections should be split into smaller reader sections. Optimization changes the EPUB file contents before upload. Note: if you use hash-based KOReader sync, this will break the syncing because it changes the epub -and therefore the hash. If optimization fails, the uploader falls back to sending -the original file. +and therefore the hash. Use filename based syncing to ensure compatibility. +If optimization fails, the uploader falls back to sending the original file. ### Settings @@ -132,15 +136,8 @@ The Fonts page lists installed SD-card font families and lets you upload the font family name, filename, and `.cpfont` magic bytes before accepting the upload. -Installed fonts appear in **Settings > Reader > Font Family** after the font -registry refreshes. - -## Command Line Use - -Power users can use `curl`, WebDAV clients, or WebSocket clients while the web -server is running. - -Endpoint details are documented in [webserver-endpoints.md](./webserver-endpoints.md). +Installed fonts appear in **Settings > Reader > Font Options > Font Family** +after the font registry refreshes. ## Security Notes @@ -158,10 +155,3 @@ Endpoint details are documented in [webserver-endpoints.md](./webserver-endpoint 3. Move closer to the router if upload progress stalls in Join Network mode. 4. Upload custom fonts through the Fonts page or copy them to `/.fonts/` or `/fonts/` on the SD card. 5. Exit File Transfer mode when finished to conserve battery. - -## Related Documentation - -- [User Guide](../USER_GUIDE.md) -- [Webserver Endpoints](./webserver-endpoints.md) -- [SD Card Fonts](./sd-card-fonts.md) -- [Troubleshooting](./troubleshooting.md) diff --git a/freeink-sdk b/freeink-sdk index 92303ba5e4..1ff020263c 160000 --- a/freeink-sdk +++ b/freeink-sdk @@ -1 +1 @@ -Subproject commit 92303ba5e4d4f762bb2f9126a3e31c303d66eb28 +Subproject commit 1ff020263cd2202ea79ce3eb811f5ac8489b8cde diff --git a/include/AppCapabilities.h b/include/AppCapabilities.h new file mode 100644 index 0000000000..09f42e389b --- /dev/null +++ b/include/AppCapabilities.h @@ -0,0 +1,21 @@ +#pragma once + +// The app deliberately has a build-time touch capability, separate from a +// runtime probe. Button-only images must not retain the touch UI merely because +// the shared source tree also builds for Sticky. +#ifndef CROSSINK_APP_CAP_TOUCH +#error "Define CROSSINK_APP_CAP_TOUCH as 0 or 1 in the PlatformIO environment" +#endif + +#if CROSSINK_APP_CAP_TOUCH != 0 && CROSSINK_APP_CAP_TOUCH != 1 +#error "CROSSINK_APP_CAP_TOUCH must be 0 or 1" +#endif + +// Native simulator BoardConfig deliberately has no FREEINK_CAP_TOUCH macro. +// Firmware builds must keep the app and SDK capability selections in lockstep. +#if !defined(SIMULATOR) +#include +#if CROSSINK_APP_CAP_TOUCH != FREEINK_CAP_TOUCH +#error "CROSSINK_APP_CAP_TOUCH must match FREEINK_CAP_TOUCH" +#endif +#endif diff --git a/include/CrossInkHalFrontlight.h b/include/CrossInkHalFrontlight.h new file mode 100644 index 0000000000..415e5c6d06 --- /dev/null +++ b/include/CrossInkHalFrontlight.h @@ -0,0 +1,54 @@ +#pragma once + +#ifdef SIMULATOR + +#include + +// The simulator has no physical light. Keep the same stateful HAL contract so +// frontlight UI code can compile and be exercised without SDK PWM hardware. +class HalFrontlight { + public: + static HalFrontlight& getInstance() { + static HalFrontlight instance; + return instance; + } + + void begin(const uint8_t brightness, const uint8_t warmth, const bool on) { + lastBrightness = brightness > 100 ? 100 : brightness; + lastWarmth = warmth > 100 ? 100 : warmth; + lit = on; + } + constexpr bool present() const { +#ifdef SIMULATOR_DEVICE_X4PRO + return true; +#else + return false; +#endif + } + constexpr bool hasColorTemperature() const { +#ifdef SIMULATOR_DEVICE_X4PRO + return true; +#else + return false; +#endif + } + void setBrightness(const uint8_t percent) { lastBrightness = percent > 100 ? 100 : percent; } + void setWarmth(const uint8_t percent) { lastWarmth = percent > 100 ? 100 : percent; } + void setOn(const bool on) { lit = on; } + uint8_t brightness() const { return lastBrightness; } + uint8_t warmth() const { return lastWarmth; } + bool isOn() const { return lit; } + + private: + uint8_t lastBrightness = 60; + uint8_t lastWarmth = 50; + bool lit = false; +}; + +#define Frontlight HalFrontlight::getInstance() + +#else + +#include + +#endif diff --git a/include/DeviceCapabilities.h b/include/DeviceCapabilities.h new file mode 100644 index 0000000000..452e2be3ca --- /dev/null +++ b/include/DeviceCapabilities.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "AppCapabilities.h" + +inline bool deviceHasEdgeSideButtons(const HalGPIO& gpio) { +#ifdef SIMULATOR + return gpio.deviceIsX3(); +#else + return gpio.hasEdgeSideButtons(); +#endif +} + +inline bool deviceUsesSideButtonHintGutters(const HalGPIO& gpio) { + if (!deviceHasEdgeSideButtons(gpio)) return false; +#if CROSSINK_APP_CAP_TOUCH + return !gpio.hasTouch(); +#else + return true; +#endif +} diff --git a/lib/AppVersion/AppVersion.h b/lib/AppVersion/AppVersion.h index 9a0cfb649c..7f318519e5 100644 --- a/lib/AppVersion/AppVersion.h +++ b/lib/AppVersion/AppVersion.h @@ -10,10 +10,6 @@ #define CROSSINK_BUILD_ENV "unknown" #endif -#ifndef CROSSINK_FIRMWARE_VARIANT -#ifdef CROSSPOINT_FIRMWARE_VARIANT -#define CROSSINK_FIRMWARE_VARIANT CROSSPOINT_FIRMWARE_VARIANT -#else -#define CROSSINK_FIRMWARE_VARIANT "unknown" -#endif +#ifndef CROSSINK_FIRMWARE_DEVICE_TYPE +#define CROSSINK_FIRMWARE_DEVICE_TYPE "unknown" #endif diff --git a/lib/DictHtmlRenderer/DictHtmlRenderer.cpp b/lib/DictHtmlRenderer/DictHtmlRenderer.cpp new file mode 100644 index 0000000000..672723df2e --- /dev/null +++ b/lib/DictHtmlRenderer/DictHtmlRenderer.cpp @@ -0,0 +1,1145 @@ +#include "DictHtmlRenderer.h" + +#include +#include + +#include +#include + +// --------------------------------------------------------------------------- +// Tag classification +// --------------------------------------------------------------------------- + +namespace { + +char asciiLower(char c) { return c >= 'A' && c <= 'Z' ? static_cast(c + ('a' - 'A')) : c; } + +bool rangeTagEquals(const char* start, int len, const char* expected) { + int i = 0; + while (i < len && expected[i]) { + if (asciiLower(start[i]) != expected[i]) return false; + i++; + } + return i == len && expected[i] == '\0'; +} + +bool isDiscardedVoidTag(const char* name, int len) { + static constexpr const char* kTags[] = {"area", "base", "col", "embed", "img", "input", + "link", "meta", "param", "source", "track", "wbr"}; + for (const char* tag : kTags) { + if (rangeTagEquals(name, len, tag)) return true; + } + return false; +} + +bool isDictionaryBullet(uint32_t cp) { + // These geometric bullets are outside the default SD-font interval. Use the + // guaranteed U+2022 glyph so dictionary definitions render consistently. + switch (cp) { + case 0x25A0: // BLACK SQUARE + case 0x25AA: // BLACK SMALL SQUARE + case 0x25C6: // BLACK DIAMOND + case 0x25CF: // BLACK CIRCLE + return true; + default: + return false; + } +} + +int encodeUtf8(uint32_t cp, char out[4]) { + if (cp <= 0x7F) { + out[0] = static_cast(cp); + return 1; + } + if (cp <= 0x7FF) { + out[0] = static_cast(0xC0 | (cp >> 6)); + out[1] = static_cast(0x80 | (cp & 0x3F)); + return 2; + } + if (cp >= 0xD800 && cp <= 0xDFFF) return 0; + if (cp <= 0xFFFF) { + out[0] = static_cast(0xE0 | (cp >> 12)); + out[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + out[2] = static_cast(0x80 | (cp & 0x3F)); + return 3; + } + if (cp <= 0x10FFFF) { + out[0] = static_cast(0xF0 | (cp >> 18)); + out[1] = static_cast(0x80 | ((cp >> 12) & 0x3F)); + out[2] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + out[3] = static_cast(0x80 | (cp & 0x3F)); + return 4; + } + return 0; +} + +} // namespace + +DictHtmlRenderer::TagAction DictHtmlRenderer::classify(const XML_Char* name) { + // HTML tag names are ASCII case-insensitive. Expat deliberately preserves + // source case, so normalize only the small tag name used for classification. + char normalizedName[48]; + size_t nameLen = strlen(name); + if (nameLen >= sizeof(normalizedName)) return TagAction::STRIP_KEEP; + for (size_t i = 0; i <= nameLen; i++) normalizedName[i] = asciiLower(name[i]); + name = normalizedName; + + // Block-strip: entire subtree discarded + if (strcmp(name, "svg") == 0) return TagAction::BLOCK_STRIP; + if (strcmp(name, "hiero") == 0) return TagAction::BLOCK_STRIP; + if (strcmp(name, "math") == 0) return TagAction::BLOCK_STRIP; + if (strcmp(name, "gallery") == 0) return TagAction::BLOCK_STRIP; + if (strcmp(name, "nowiki") == 0) return TagAction::BLOCK_STRIP; + if (strcmp(name, "poem") == 0) return TagAction::BLOCK_STRIP; + if (strcmp(name, "ref") == 0) return TagAction::BLOCK_STRIP; + if (strcmp(name, "img") == 0) return TagAction::BLOCK_STRIP; + + // Children of block-strip tags (registered to avoid unknown-tag errors) + if (strcmp(name, "defs") == 0) return TagAction::REGISTERED; + if (strcmp(name, "g") == 0) return TagAction::REGISTERED; + if (strcmp(name, "path") == 0) return TagAction::REGISTERED; + if (strcmp(name, "rect") == 0) return TagAction::REGISTERED; + if (strcmp(name, "use") == 0) return TagAction::REGISTERED; + if (strcmp(name, "table") == 0) return TagAction::REGISTERED; + if (strcmp(name, "tr") == 0) return TagAction::REGISTERED; + if (strcmp(name, "td") == 0) return TagAction::REGISTERED; + + // Inline formatting + if (strcmp(name, "b") == 0) return TagAction::FORMAT_BOLD; + if (strcmp(name, "strong") == 0) return TagAction::FORMAT_BOLD; + if (strcmp(name, "i") == 0) return TagAction::FORMAT_ITALIC; + if (strcmp(name, "em") == 0) return TagAction::FORMAT_ITALIC; + if (strcmp(name, "u") == 0) return TagAction::FORMAT_UNDERLINE; + if (strcmp(name, "s") == 0) return TagAction::FORMAT_STRIKE; + if (strcmp(name, "sup") == 0) return TagAction::FORMAT_SMALL; + if (strcmp(name, "sub") == 0) return TagAction::FORMAT_SMALL; + if (strcmp(name, "code") == 0) return TagAction::FORMAT_CODE; + if (strcmp(name, "tt") == 0) return TagAction::FORMAT_CODE; + if (strcmp(name, "small") == 0) return TagAction::FORMAT_SMALL; + if (strcmp(name, "big") == 0) return TagAction::FORMAT_SMALL; + + // Block structure + if (strcmp(name, "p") == 0) return TagAction::BLOCK_BREAK; + if (strcmp(name, "div") == 0) return TagAction::BLOCK_BREAK; + if (strcmp(name, "br") == 0 || strcmp(name, "hr") == 0) return TagAction::BLOCK_BREAK; + if (strcmp(name, "blockquote") == 0) return TagAction::BLOCK_QUOTE; + + // Lists + if (strcmp(name, "li") == 0) return TagAction::LIST_ITEM; + if (strcmp(name, "ol") == 0) return TagAction::LIST_CONTAINER; + if (strcmp(name, "ul") == 0) return TagAction::LIST_CONTAINER; + + // Headings + if (strcmp(name, "h1") == 0) return TagAction::HEADING; + if (strcmp(name, "h2") == 0) return TagAction::HEADING; + if (strcmp(name, "h3") == 0) return TagAction::HEADING; + if (strcmp(name, "h4") == 0) return TagAction::HEADING; + + // Special + if (strcmp(name, "abbr") == 0) return TagAction::ABBR; + if (strcmp(name, "var") == 0) return TagAction::VAR; + if (strcmp(name, "span") == 0) return TagAction::SPAN; + if (strcmp(name, "a") == 0) return TagAction::SPAN; // Registered; no actual anchors in dict + if (strcmp(name, "_root") == 0) return TagAction::REGISTERED; // Synthetic wrapper used by render() + + // Wikitext annotation tags: t:XX, tr:XX, lang:XX, gloss:XX, pos:XX, sc:XX, alt:XX, id:XX + if (strncmp(name, "t:", 2) == 0) return TagAction::WIKI_ANNOT; + if (strncmp(name, "tr:", 3) == 0) return TagAction::WIKI_ANNOT; + if (strncmp(name, "lang:", 5) == 0) return TagAction::WIKI_ANNOT; + if (strncmp(name, "gloss:", 6) == 0) return TagAction::WIKI_ANNOT; + if (strncmp(name, "pos:", 4) == 0) return TagAction::WIKI_ANNOT; + if (strncmp(name, "sc:", 3) == 0) return TagAction::WIKI_ANNOT; + if (strncmp(name, "alt:", 4) == 0) return TagAction::WIKI_ANNOT; + if (strncmp(name, "id:", 3) == 0) return TagAction::WIKI_ANNOT; + + return TagAction::STRIP_KEEP; // Unknown — strip tag, keep text +} + +const char* DictHtmlRenderer::findAttr(const XML_Char** atts, const char* name) { + for (int i = 0; atts[i]; i += 2) { + if (strcmp(atts[i], name) == 0) return atts[i + 1]; + } + return nullptr; +} + +// --------------------------------------------------------------------------- +// Constructor / destructor / reset +// --------------------------------------------------------------------------- + +DictHtmlRenderer::DictHtmlRenderer() { + spans.reserve(64); + textBuf.reserve(512); + tagStack.reserve(8); + htmlCarry_.reserve(128); + htmlInput_.reserve(640); + normalizedHtml_.reserve(640); +} + +DictHtmlRenderer::~DictHtmlRenderer() { + if (parser) { + XML_ParserFree(parser); + parser = nullptr; + } +} + +void DictHtmlRenderer::reset() { + spans.clear(); + textBuf.clear(); + pendingText.clear(); + htmlCarry_.clear(); + htmlInput_.clear(); + normalizedHtml_.clear(); + discardTagUntilClose_ = false; + tagStack.clear(); + parseError = false; + fmt = FormatState{}; + newlinePending = false; + listItemPending = false; + spanSink_ = SpanSink{}; // batch mode by default; streaming sets it after reset() + + // Reuse the existing parser (cheap reset) rather than free+create on every + // render. Only the first call (null parser) allocates. XML_ParserReset clears + // handlers + user data; callers re-apply them after reset (parseOpenFile, + // render). Pays off because the renderer is now a reused activity member. + if (parser) { + XML_ParserReset(parser, nullptr); + } else { + parser = XML_ParserCreate(nullptr); + } + +#ifdef DICT_HTML_RENDERER_TRACK_UNKNOWN + unknownTagCount = 0; +#endif +} + +// --------------------------------------------------------------------------- +// Text management +// --------------------------------------------------------------------------- + +void DictHtmlRenderer::pushSpan() { + if (pendingText.empty()) return; + + StyledSpan span; + span.bold = fmt.bold; + span.italic = fmt.italic; + span.underline = fmt.underline; + span.strikethrough = fmt.strikethrough; + span.indentLevel = fmt.indentLevel; + span.newlineBefore = newlinePending; + span.isListItem = listItemPending; + + if (spanSink_.fn) { + // Streaming: deliver the span immediately. text points into pendingText and + // is valid only for the duration of the call — the sink copies it. textBuf + // and the spans vector are never grown. + span.text = pendingText.c_str(); + span.textOffset = 0; + spanSink_(span); + } else { + // Batch: accumulate text into textBuf; span.text pointers fixed up after the + // full parse (textBuf reallocates as it grows). + span.textOffset = static_cast(textBuf.size()); + if (textBuf.size() + pendingText.size() + 1 > textBuf.capacity()) { + textBuf.reserve(textBuf.capacity() + 512); + } + textBuf.insert(textBuf.end(), pendingText.begin(), pendingText.end()); + textBuf.push_back('\0'); + spans.push_back(span); + } + + pendingText.clear(); + newlinePending = false; + listItemPending = false; +} + +void DictHtmlRenderer::emitText(const char* s, int len) { + if (len <= 0) return; + for (int i = 0; i < len; i++) { + if (i + 2 < len && static_cast(s[i]) >= 0xE0 && static_cast(s[i]) <= 0xEF && + (static_cast(s[i + 1]) & 0xC0) == 0x80 && + (static_cast(s[i + 2]) & 0xC0) == 0x80) { + const uint32_t cp = ((static_cast(s[i]) & 0x0F) << 12) | + ((static_cast(s[i + 1]) & 0x3F) << 6) | (static_cast(s[i + 2]) & 0x3F); + if (isDictionaryBullet(cp)) { + pendingText += "\xE2\x80\xA2"; // U+2022 BULLET + i += 2; + continue; + } + } + char c = s[i]; + if (c == '\r' || c == '\n') { + pushSpan(); + newlinePending = true; + // Treat \r\n as a single line ending + if (c == '\r' && i + 1 < len && s[i + 1] == '\n') i++; + continue; + } + if (c == '\t') c = ' '; + pendingText += c; + } +} + +void DictHtmlRenderer::flushPending() { pushSpan(); } + +// --------------------------------------------------------------------------- +// Expat callbacks +// --------------------------------------------------------------------------- + +void XMLCALL DictHtmlRenderer::onStart(void* ud, const XML_Char* name, const XML_Char** atts) { + auto* self = static_cast(ud); + if (self->parseError) return; + + TagAction action = classify(name); + + // Propagate suppression from parent + bool parentSuppresses = !self->tagStack.empty() && self->tagStack.back().suppressChildren; + + StackEntry entry{}; + entry.action = action; + entry.savedFmt = self->fmt; + entry.suppressChildren = parentSuppresses; +#ifdef DICT_HTML_RENDERER_TRACK_UNKNOWN + entry.pendingLenAtOpen = static_cast(self->pendingText.size()); + entry.unknownTagName[0] = '\0'; +#endif + + if (!parentSuppresses) { + switch (action) { + case TagAction::BLOCK_STRIP: + self->flushPending(); + entry.suppressChildren = true; + break; + + case TagAction::FORMAT_BOLD: + self->flushPending(); + self->fmt.bold = true; + break; + + case TagAction::FORMAT_ITALIC: + case TagAction::VAR: + self->flushPending(); + self->fmt.italic = true; + break; + + case TagAction::FORMAT_UNDERLINE: + self->flushPending(); + self->fmt.underline = true; + break; + + case TagAction::FORMAT_STRIKE: + self->flushPending(); + self->fmt.strikethrough = true; + break; + + case TagAction::FORMAT_SMALL: + self->flushPending(); + break; + + case TagAction::FORMAT_CODE: + self->flushPending(); + self->fmt.bold = true; + break; + + case TagAction::BLOCK_BREAK: + self->flushPending(); + self->newlinePending = true; + break; + + case TagAction::BLOCK_QUOTE: + self->flushPending(); + self->newlinePending = true; + self->fmt.indentLevel++; + break; + + case TagAction::LIST_CONTAINER: + self->flushPending(); + self->newlinePending = true; + break; + + case TagAction::LIST_ITEM: + self->flushPending(); + self->newlinePending = true; + self->listItemPending = true; + self->fmt.indentLevel++; + break; + + case TagAction::HEADING: + self->flushPending(); + self->newlinePending = true; + self->fmt.bold = true; + break; + + case TagAction::ABBR: { + self->flushPending(); // Flush preceding text before body accumulates + const char* title = findAttr(atts, "title"); + if (title) { + entry.abbrTitle = title; + } + break; + } + + case TagAction::WIKI_ANNOT: { + const char* colon = strchr(name, ':'); + if (colon && colon[1] != '\0') { + entry.wikiText = colon + 1; + } + break; + } + + case TagAction::STRIP_KEEP: +#ifdef DICT_HTML_RENDERER_TRACK_UNKNOWN + strncpy(entry.unknownTagName, name, sizeof(entry.unknownTagName) - 1); + entry.unknownTagName[sizeof(entry.unknownTagName) - 1] = '\0'; + entry.pendingLenAtOpen = static_cast(self->pendingText.size()); +#endif + break; + + default: + break; + } + } + + self->tagStack.push_back(entry); +} + +void XMLCALL DictHtmlRenderer::onEnd(void* ud, const XML_Char* name) { + (void)name; + auto* self = static_cast(ud); + if (self->parseError) return; + if (self->tagStack.empty()) return; + + StackEntry& entry = self->tagStack.back(); + bool suppressed = entry.suppressChildren || entry.action == TagAction::BLOCK_STRIP; + + if (!suppressed) { + switch (entry.action) { + case TagAction::FORMAT_BOLD: + case TagAction::FORMAT_ITALIC: + case TagAction::FORMAT_UNDERLINE: + case TagAction::FORMAT_STRIKE: + case TagAction::FORMAT_SMALL: + case TagAction::FORMAT_CODE: + case TagAction::VAR: + case TagAction::SPAN: + case TagAction::REGISTERED: + self->flushPending(); + break; + + case TagAction::BLOCK_BREAK: + self->flushPending(); + self->newlinePending = true; + break; + + case TagAction::BLOCK_QUOTE: + self->flushPending(); + self->newlinePending = true; + break; + + case TagAction::LIST_CONTAINER: + self->flushPending(); + self->newlinePending = true; + break; + + case TagAction::LIST_ITEM: + self->flushPending(); + break; + + case TagAction::HEADING: + self->flushPending(); + self->newlinePending = true; + break; + + case TagAction::ABBR: + if (!entry.abbrTitle.empty()) { + self->flushPending(); + std::string expanded = " (" + entry.abbrTitle + ")"; + self->emitText(expanded.c_str(), static_cast(expanded.size())); + self->flushPending(); + } + break; + + case TagAction::WIKI_ANNOT: + if (!entry.wikiText.empty()) { + self->flushPending(); // Flush preceding text before emitting wiki suffix + self->emitText(entry.wikiText.c_str(), static_cast(entry.wikiText.size())); + self->flushPending(); + } + break; + + case TagAction::STRIP_KEEP: +#ifdef DICT_HTML_RENDERER_TRACK_UNKNOWN + if (entry.unknownTagName[0] != '\0') { + // Extract word before (from text accumulated before tag opened) + char wordBefore[32] = {}; + // We can't easily recover the text before — use empty for now + // Extract tag contents: text accumulated between open and close + char tagContents[64] = {}; + int contStart = entry.pendingLenAtOpen; + int contLen = static_cast(self->pendingText.size()) - contStart; + if (contLen > 0 && contLen < static_cast(sizeof(tagContents))) { + memcpy(tagContents, self->pendingText.c_str() + contStart, contLen); + tagContents[contLen] = '\0'; + } + self->recordUnknownTag(entry.unknownTagName, wordBefore, tagContents, ""); + } +#endif + self->flushPending(); + break; + + default: + break; + } + + // Restore saved format (bold/italic/underline/strikethrough/indentLevel) + self->fmt = entry.savedFmt; + } + + self->tagStack.pop_back(); +} + +void XMLCALL DictHtmlRenderer::onText(void* ud, const XML_Char* s, int len) { + auto* self = static_cast(ud); + if (self->parseError) return; + + if (!self->tagStack.empty()) { + const StackEntry& top = self->tagStack.back(); + // Suppress text inside block-strip and for wiki-annot (emit from tag name, not body) + if (top.suppressChildren || top.action == TagAction::WIKI_ANNOT || top.action == TagAction::BLOCK_STRIP) { + return; + } + } + + self->emitText(s, len); +} + +// --------------------------------------------------------------------------- +// HTML entity resolver — feeds definition to expat incrementally +// --------------------------------------------------------------------------- + +// Returns the UTF-8 replacement for a known HTML named entity (matched by +// in-place strncmp — no copy, no stack buffer). Returns nullptr for unknowns. +// Empty string ("") means the entity should be silently dropped (zero-width chars). +static const char* lookupHtmlEntity(const char* name, int nameLen) { + struct E { + const char* n; + int nLen; + const char* utf8; + }; + static constexpr E kTable[] = { + {"lsqb", 4, "["}, + {"rsqb", 4, "]"}, + {"nbsp", 4, " "}, + {"ndash", 5, "\xE2\x80\x93"}, // – + {"mdash", 5, "\xE2\x80\x94"}, // — + {"hellip", 6, "\xE2\x80\xA6"}, // … + {"bull", 4, "\xE2\x80\xA2"}, // • + {"dagger", 6, "\xE2\x80\xA0"}, // † + {"sect", 4, "\xC2\xA7"}, // § + {"para", 4, "\xC2\xB6"}, // ¶ + {"prime", 5, "\xE2\x80\xB2"}, // ′ + {"Prime", 5, "\xE2\x80\xB3"}, // ″ + {"times", 5, "\xC3\x97"}, // × + {"minus", 5, "\xE2\x88\x92"}, // − + {"middot", 6, "\xC2\xB7"}, // · + {"lrm", 3, ""}, // LEFT-TO-RIGHT MARK — zero-width, drop + {"rlm", 3, ""}, // RIGHT-TO-LEFT MARK — zero-width, drop + }; + for (const auto& e : kTable) { + if (e.nLen == nameLen && strncmp(name, e.n, nameLen) == 0) return e.utf8; + } + return nullptr; +} + +void DictHtmlRenderer::normalizeHtmlChunk(const char* buf, int len, bool isLast) { + static constexpr size_t kMaxCarriedTag = 512; + + htmlInput_.assign(htmlCarry_); + htmlInput_.append(buf, static_cast(len)); + htmlCarry_.clear(); + normalizedHtml_.clear(); + + size_t i = 0; + if (discardTagUntilClose_) { + const size_t close = htmlInput_.find('>'); + if (close == std::string::npos) return; + discardTagUntilClose_ = false; + i = close + 1; + } + + while (i < htmlInput_.size()) { + if (htmlInput_[i] != '<') { + const unsigned char c = static_cast(htmlInput_[i++]); + // XML rejects these bytes even though they occur in some legacy HTML + // dictionaries. Outside discarded resource tags, a space is the least + // surprising readable fallback. + normalizedHtml_ += (c < 0x20 && c != '\t' && c != '\r' && c != '\n') ? ' ' : static_cast(c); + continue; + } + + char quote = '\0'; + size_t close = i + 1; + for (; close < htmlInput_.size(); close++) { + const char c = htmlInput_[close]; + if (quote != '\0') { + if (c == quote) quote = '\0'; + } else if (c == '\'' || c == '"') { + quote = c; + } else if (c == '>') { + break; + } + } + + if (close == htmlInput_.size()) { + if (isLast) { + // Preserve malformed trailing markup as visible text without giving + // Expat a bare '<'. + normalizedHtml_ += "<"; + normalizedHtml_.append(htmlInput_, i + 1, std::string::npos); + } else if (htmlInput_.size() - i <= kMaxCarriedTag) { + htmlCarry_.assign(htmlInput_, i, std::string::npos); + } else { + // A hostile or corrupt tag must not grow scratch to definition size. + discardTagUntilClose_ = true; + } + return; + } + + size_t nameStart = i + 1; + while (nameStart < close && (htmlInput_[nameStart] == ' ' || htmlInput_[nameStart] == '\t')) nameStart++; + const bool closing = nameStart < close && htmlInput_[nameStart] == '/'; + if (closing) nameStart++; + const size_t nameBegin = nameStart; + while (nameStart < close) { + const char c = htmlInput_[nameStart]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '/') break; + nameStart++; + } + const int nameLen = static_cast(nameStart - nameBegin); + + if (!closing && (rangeTagEquals(htmlInput_.data() + nameBegin, nameLen, "br") || + rangeTagEquals(htmlInput_.data() + nameBegin, nameLen, "hr"))) { + // Canonical XML form. HR is represented as a line break because the + // e-ink definition view has no horizontal-rule primitive. + normalizedHtml_ += "
"; + } else if (isDiscardedVoidTag(htmlInput_.data() + nameBegin, nameLen)) { + // Omit the whole element, including binary-ish resource attributes. Its + // surrounding text remains in the stream. + } else { + normalizedHtml_.append(htmlInput_, i, close - i + 1); + } + i = close + 1; + } +} + +// --------------------------------------------------------------------------- +// Public render() +// --------------------------------------------------------------------------- + +const std::vector& DictHtmlRenderer::render(const char* html, int len) { + reset(); + + if (!parser) { + parseError = true; + return spans; + } + + XML_SetUserData(parser, this); + XML_SetElementHandler(parser, onStart, onEnd); + XML_SetCharacterDataHandler(parser, onText); + + // Wrap fragment in a synthetic root element so expat sees well-formed XML + static constexpr const char kOpen[] = "<_root>"; + static constexpr const char kClose[] = ""; + + if (XML_Parse(parser, kOpen, static_cast(sizeof(kOpen) - 1), 0) != XML_STATUS_OK) { + parseError = true; + } + + normalizeHtmlChunk(html, len, true); + html = normalizedHtml_.data(); + len = static_cast(normalizedHtml_.size()); + + // Walk the definition, feeding normal text runs directly to expat and + // resolving HTML named entities inline. No heap allocation; entity names + // compared in-place with strncmp — no copy, no extra stack buffer. + if (!parseError) { + static const char* const kXmlBuiltins[] = {"amp", "lt", "gt", "quot", "apos"}; + int i = 0; + while (i < len && !parseError) { + // Feed run of non-entity characters directly (zero copy) + int runStart = i; + while (i < len && html[i] != '&') i++; + if (i > runStart) { + if (XML_Parse(parser, html + runStart, i - runStart, 0) != XML_STATUS_OK) { + parseError = true; + break; + } + } + if (i >= len) break; + + // html[i] == '&' — scan for matching ';' + int j = i + 1; + while (j < len && html[j] != ';' && html[j] != '&' && html[j] != '<' && (j - i) < 33) j++; + + if (j < len && html[j] == ';') { + int nameLen = j - i - 1; + const char* nameStart = html + i + 1; + + // Numeric references (&#NN; / &#xNN;) and XML built-ins pass through unchanged + bool passThrough = (nameLen > 0 && nameStart[0] == '#'); + if (!passThrough) { + for (const auto* b : kXmlBuiltins) { + if (static_cast(strlen(b)) == nameLen && strncmp(nameStart, b, nameLen) == 0) { + passThrough = true; + break; + } + } + } + + if (passThrough) { + if (XML_Parse(parser, html + i, j - i + 1, 0) != XML_STATUS_OK) { + parseError = true; + break; + } + } else { + const char* repl = lookupHtmlEntity(nameStart, nameLen); + if (repl && repl[0] != '\0') { + if (XML_Parse(parser, repl, static_cast(strlen(repl)), 0) != XML_STATUS_OK) { + parseError = true; + break; + } + } else if (!repl) { + // Preserve unsupported named entities visibly instead of deleting + // them. Escaping the ampersand keeps the XML stream valid. + static constexpr char kAmp[] = "&"; + if (XML_Parse(parser, kAmp, static_cast(sizeof(kAmp) - 1), 0) != XML_STATUS_OK || + XML_Parse(parser, nameStart, nameLen, 0) != XML_STATUS_OK || + XML_Parse(parser, ";", 1, 0) != XML_STATUS_OK) { + parseError = true; + break; + } + } + } + i = j + 1; + } else { + // Preserve a malformed ampersand as text without invalidating XML. + static constexpr char kAmp[] = "&"; + if (XML_Parse(parser, kAmp, static_cast(sizeof(kAmp) - 1), 0) != XML_STATUS_OK) { + parseError = true; + break; + } + i++; + } + } + } + + if (!parseError) { + if (XML_Parse(parser, kClose, static_cast(sizeof(kClose) - 1), 1) != XML_STATUS_OK) { + parseError = true; + } + } + + flushPending(); + + // textBuf is now stable — convert offsets to pointers. + // Done even on parse error so that partially accumulated spans are usable. + for (auto& s : spans) { + s.text = textBuf.data() + s.textOffset; + } + + return spans; +} + +// --------------------------------------------------------------------------- +// Chunked entity resolution (shared by render() and renderFromFile()) +// --------------------------------------------------------------------------- + +void DictHtmlRenderer::feedEntityResolved(const char* buf, int len, bool isLast, char* carry, int* carryLen) { + static const char* const kXmlBuiltins[] = {"amp", "lt", "gt", "quot", "apos"}; + int i = 0; + while (i < len && !parseError) { + int runStart = i; + while (i < len && buf[i] != '&') i++; + if (i > runStart) { + if (XML_Parse(parser, buf + runStart, i - runStart, 0) != XML_STATUS_OK) { + parseError = true; + return; + } + } + if (i >= len) break; + + // buf[i] == '&' — scan for ';' + int j = i + 1; + while (j < len && buf[j] != ';' && buf[j] != '&' && buf[j] != '<' && (j - i) < 33) j++; + + if (j >= len && !isLast) { + // Entity spans chunk boundary — carry remainder for next chunk + if (carry && carryLen) { + int remaining = len - i; + if (remaining > 63) remaining = 63; // safety cap + memcpy(carry, buf + i, remaining); + *carryLen = remaining; + } + return; + } + + if (j < len && buf[j] == ';') { + int nameLen = j - i - 1; + const char* nameStart = buf + i + 1; + + bool passThrough = (nameLen > 0 && nameStart[0] == '#'); + if (!passThrough) { + for (const auto* b : kXmlBuiltins) { + if (static_cast(strlen(b)) == nameLen && strncmp(nameStart, b, nameLen) == 0) { + passThrough = true; + break; + } + } + } + + if (passThrough) { + if (XML_Parse(parser, buf + i, j - i + 1, 0) != XML_STATUS_OK) { + parseError = true; + return; + } + } else { + const char* repl = lookupHtmlEntity(nameStart, nameLen); + if (repl && repl[0] != '\0') { + if (XML_Parse(parser, repl, static_cast(strlen(repl)), 0) != XML_STATUS_OK) { + parseError = true; + return; + } + } else if (!repl) { + static constexpr char kAmp[] = "&"; + if (XML_Parse(parser, kAmp, static_cast(sizeof(kAmp) - 1), 0) != XML_STATUS_OK || + XML_Parse(parser, nameStart, nameLen, 0) != XML_STATUS_OK || + XML_Parse(parser, ";", 1, 0) != XML_STATUS_OK) { + parseError = true; + return; + } + } + } + i = j + 1; + } else { + // Preserve a malformed ampersand as text without invalidating XML. + static constexpr char kAmp[] = "&"; + if (XML_Parse(parser, kAmp, static_cast(sizeof(kAmp) - 1), 0) != XML_STATUS_OK) { + parseError = true; + return; + } + i++; + } + } +} + +void DictHtmlRenderer::feedPlainText(const char* buf, int len, bool isLast, char* carry, int* carryLen) { + int i = 0; + while (i < len) { + if (buf[i] == '<') { + const char* close = static_cast(memchr(buf + i + 1, '>', static_cast(len - i - 1))); + if (!close) { + // normalizeHtmlChunk() carries incomplete tags, so this is only a + // defensive fallback for corrupt input. + emitText("<", 1); + i++; + continue; + } + + int nameStart = i + 1; + while (nameStart < close - buf && (buf[nameStart] == ' ' || buf[nameStart] == '\t')) nameStart++; + if (nameStart < close - buf && buf[nameStart] == '/') nameStart++; + const int nameBegin = nameStart; + while (nameStart < close - buf) { + const char c = buf[nameStart]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '/') break; + nameStart++; + } + + const int nameLen = nameStart - nameBegin; + const bool isBreak = + rangeTagEquals(buf + nameBegin, nameLen, "br") || rangeTagEquals(buf + nameBegin, nameLen, "hr") || + rangeTagEquals(buf + nameBegin, nameLen, "p") || rangeTagEquals(buf + nameBegin, nameLen, "div") || + rangeTagEquals(buf + nameBegin, nameLen, "ol") || rangeTagEquals(buf + nameBegin, nameLen, "ul") || + rangeTagEquals(buf + nameBegin, nameLen, "li") || + (nameLen == 2 && asciiLower(buf[nameBegin]) == 'h' && buf[nameBegin + 1] >= '1' && buf[nameBegin + 1] <= '6'); + if (isBreak) { + flushPending(); + newlinePending = true; + } + i = static_cast(close - buf) + 1; + continue; + } + + if (buf[i] != '&') { + const int runStart = i; + while (i < len && buf[i] != '<' && buf[i] != '&') i++; + emitText(buf + runStart, i - runStart); + continue; + } + + int end = i + 1; + while (end < len && buf[end] != ';' && buf[end] != '&' && buf[end] != '<' && end - i < 33) end++; + if (end >= len && !isLast) { + if (carry && carryLen) { + int remaining = len - i; + if (remaining > 63) remaining = 63; + memcpy(carry, buf + i, static_cast(remaining)); + *carryLen = remaining; + } + return; + } + + if (end >= len || buf[end] != ';') { + emitText("&", 1); + i++; + continue; + } + + const char* name = buf + i + 1; + const int nameLen = end - i - 1; + const char* replacement = nullptr; + if (nameLen == 3 && strncmp(name, "amp", 3) == 0) replacement = "&"; + if (nameLen == 2 && strncmp(name, "lt", 2) == 0) replacement = "<"; + if (nameLen == 2 && strncmp(name, "gt", 2) == 0) replacement = ">"; + if (nameLen == 4 && strncmp(name, "quot", 4) == 0) replacement = "\""; + if (nameLen == 4 && strncmp(name, "apos", 4) == 0) replacement = "'"; + if (!replacement) replacement = lookupHtmlEntity(name, nameLen); + + if (replacement) { + emitText(replacement, static_cast(strlen(replacement))); + } else if (nameLen > 1 && name[0] == '#') { + const bool hex = name[1] == 'x' || name[1] == 'X'; + uint32_t cp = 0; + bool valid = nameLen > (hex ? 2 : 1); + for (int digitIndex = hex ? 2 : 1; digitIndex < nameLen; digitIndex++) { + const char c = name[digitIndex]; + uint8_t digit = 0; + if (c >= '0' && c <= '9') { + digit = static_cast(c - '0'); + } else if (hex && c >= 'a' && c <= 'f') { + digit = static_cast(c - 'a' + 10); + } else if (hex && c >= 'A' && c <= 'F') { + digit = static_cast(c - 'A' + 10); + } else { + valid = false; + break; + } + cp = cp * (hex ? 16u : 10u) + digit; + if (cp > 0x10FFFF) { + valid = false; + break; + } + } + char utf8[4]; + const int utf8Len = valid && cp != 0 ? encodeUtf8(cp, utf8) : 0; + if (utf8Len > 0) { + emitText(utf8, utf8Len); + } else { + emitText(buf + i, end - i + 1); + } + } else { + // Unknown entities are more useful visible than silently deleted. + emitText(buf + i, end - i + 1); + } + i = end + 1; + } +} + +// --------------------------------------------------------------------------- +// Stream-render from .dict file (definition never held in RAM) +// --------------------------------------------------------------------------- + +void DictHtmlRenderer::parseOpenFile(HalFile& file, uint32_t size) { + XML_SetUserData(parser, this); + XML_SetElementHandler(parser, onStart, onEnd); + XML_SetCharacterDataHandler(parser, onText); + + static constexpr const char kOpen[] = "<_root>"; + static constexpr const char kClose[] = ""; + + if (XML_Parse(parser, kOpen, static_cast(sizeof(kOpen) - 1), 0) != XML_STATUS_OK) { + parseError = true; + } + + if (!parseError) { + char chunk[512]; + char entityCarry[64]; + int entityCarryLen = 0; + uint32_t remaining = size; + + while (remaining > 0 && !parseError) { + const uint32_t toRead = remaining < sizeof(chunk) ? remaining : static_cast(sizeof(chunk)); + int n = file.read(reinterpret_cast(chunk), static_cast(toRead)); + if (n <= 0) break; + remaining -= static_cast(n); + normalizeHtmlChunk(chunk, n, remaining == 0); + + if (entityCarryLen > 0) normalizedHtml_.insert(0, entityCarry, static_cast(entityCarryLen)); + entityCarryLen = 0; + feedEntityResolved(normalizedHtml_.data(), static_cast(normalizedHtml_.size()), + remaining == 0 && htmlCarry_.empty() && !discardTagUntilClose_, entityCarry, &entityCarryLen); + } + + // Flush any trailing carryover (e.g. malformed entity at end of file) + if (entityCarryLen > 0 && !parseError) { + feedEntityResolved(entityCarry, entityCarryLen, true, nullptr, nullptr); + } + } + + if (!parseError) { + if (XML_Parse(parser, kClose, static_cast(sizeof(kClose) - 1), 1) != XML_STATUS_OK) { + parseError = true; + } + } + + flushPending(); + + if (parseError) { + LOG_ERR("DHTML", "Parse failed at byte %ld: %s", static_cast(XML_GetCurrentByteIndex(parser)), + XML_ErrorString(XML_GetErrorCode(parser))); + } +} + +const std::vector& DictHtmlRenderer::renderFromFile(const char* dictPath, uint32_t offset, uint32_t size) { + reset(); + + if (!parser) { + parseError = true; + return spans; + } + + HalFile file; + if (!Storage.openFileForRead("DICT", dictPath, file)) { + LOG_ERR("DHTML", "Failed to open: %s", dictPath); + parseError = true; + return spans; + } + file.seekSet(offset); + + parseOpenFile(file, size); + file.close(); + + for (auto& s : spans) { + s.text = textBuf.data() + s.textOffset; + } + + return spans; +} + +bool DictHtmlRenderer::renderFromFileStreaming(const char* dictPath, uint32_t offset, uint32_t size, + const SpanSink& sink) { + reset(); + spanSink_ = sink; // install for this parse; pushSpan() now streams to the sink + + if (!parser) { + parseError = true; + spanSink_ = SpanSink{}; + return false; + } + + HalFile file; + if (!Storage.openFileForRead("DICT", dictPath, file)) { + LOG_ERR("DHTML", "Failed to open: %s", dictPath); + parseError = true; + spanSink_ = SpanSink{}; + return false; + } + file.seekSet(offset); + + parseOpenFile(file, size); + file.close(); + + spanSink_ = SpanSink{}; // streaming never materializes spans/textBuf — nothing to fix up + return !parseError; +} + +bool DictHtmlRenderer::renderPlainTextFromFileStreaming(const char* dictPath, uint32_t offset, uint32_t size, + const SpanSink& sink) { + reset(); + spanSink_ = sink; + + HalFile file; + if (!Storage.openFileForRead("DICT", dictPath, file)) { + LOG_ERR("DHTML", "Failed to open plain-text fallback: %s", dictPath); + spanSink_ = SpanSink{}; + return false; + } + if (!file.seekSet(offset)) { + LOG_ERR("DHTML", "Failed to seek plain-text fallback: %s", dictPath); + file.close(); + spanSink_ = SpanSink{}; + return false; + } + + // Matches the normal streaming path: one SD read buffer plus a bounded + // cross-chunk entity carry. No definition-sized fallback allocation. + char chunk[512]; + char entityCarry[64]; + int entityCarryLen = 0; + uint32_t remaining = size; + bool readOk = true; + while (remaining > 0) { + const uint32_t toRead = remaining < sizeof(chunk) ? remaining : static_cast(sizeof(chunk)); + const int n = file.read(reinterpret_cast(chunk), static_cast(toRead)); + if (n <= 0) { + readOk = false; + break; + } + remaining -= static_cast(n); + normalizeHtmlChunk(chunk, n, remaining == 0); + if (entityCarryLen > 0) normalizedHtml_.insert(0, entityCarry, static_cast(entityCarryLen)); + entityCarryLen = 0; + feedPlainText(normalizedHtml_.data(), static_cast(normalizedHtml_.size()), + remaining == 0 && htmlCarry_.empty() && !discardTagUntilClose_, entityCarry, &entityCarryLen); + } + if (entityCarryLen > 0 && readOk) feedPlainText(entityCarry, entityCarryLen, true, nullptr, nullptr); + flushPending(); + file.close(); + spanSink_ = SpanSink{}; + + if (!readOk) LOG_ERR("DHTML", "Failed reading plain-text fallback: %s", dictPath); + return readOk; +} + +// --------------------------------------------------------------------------- +// Unknown tag tracking (smoke test only) +// --------------------------------------------------------------------------- + +#ifdef DICT_HTML_RENDERER_TRACK_UNKNOWN + +void DictHtmlRenderer::recordUnknownTag(const char* tagName, const char* wordBefore, const char* tagContents, + const char* wordAfter) { + if (unknownTagCount >= MAX_UNKNOWN_TAGS) return; + UnknownTagInfo& info = unknownTags[unknownTagCount++]; + strncpy(info.tag, tagName, sizeof(info.tag) - 1); + info.tag[sizeof(info.tag) - 1] = '\0'; + strncpy(info.entry, currentEntryName ? currentEntryName : "", sizeof(info.entry) - 1); + info.entry[sizeof(info.entry) - 1] = '\0'; + strncpy(info.wordBefore, wordBefore ? wordBefore : "", sizeof(info.wordBefore) - 1); + info.wordBefore[sizeof(info.wordBefore) - 1] = '\0'; + strncpy(info.tagContents, tagContents ? tagContents : "", sizeof(info.tagContents) - 1); + info.tagContents[sizeof(info.tagContents) - 1] = '\0'; + strncpy(info.wordAfter, wordAfter ? wordAfter : "", sizeof(info.wordAfter) - 1); + info.wordAfter[sizeof(info.wordAfter) - 1] = '\0'; +} + +void DictHtmlRenderer::extractLastWord(const char* text, int len, char* out, int outSize) { + if (!text || len <= 0 || !out || outSize <= 0) { + if (out && outSize > 0) out[0] = '\0'; + return; + } + int end = len - 1; + while (end >= 0 && (text[end] == ' ' || text[end] == '\t' || text[end] == '\n')) end--; + if (end < 0) { + out[0] = '\0'; + return; + } + int start = end; + while (start > 0 && text[start - 1] != ' ' && text[start - 1] != '\t' && text[start - 1] != '\n') start--; + int wlen = end - start + 1; + if (wlen >= outSize) wlen = outSize - 1; + memcpy(out, text + start, wlen); + out[wlen] = '\0'; +} + +#endif // DICT_HTML_RENDERER_TRACK_UNKNOWN diff --git a/lib/DictHtmlRenderer/DictHtmlRenderer.h b/lib/DictHtmlRenderer/DictHtmlRenderer.h new file mode 100644 index 0000000000..47b8c090a5 --- /dev/null +++ b/lib/DictHtmlRenderer/DictHtmlRenderer.h @@ -0,0 +1,201 @@ +#pragma once + +#include + +#include +#include +#include + +class HalFile; // fwd-decl; full definition only needed in the .cpp + +/** + * A styled text span produced by DictHtmlRenderer. + */ +struct StyledSpan { + const char* text = nullptr; // Points into renderer-owned buffer; valid until next render() call + uint32_t textOffset = 0; // Internal: byte offset into textBuf; converted to text after render() + bool bold = false; + bool italic = false; + bool underline = false; + bool strikethrough = false; + bool isListItem = false; + bool newlineBefore = false; + uint8_t indentLevel = 0; +}; + +/** + * Renders StarDict HTML definitions (sametypesequence=h) into a flat vector + * of StyledSpan objects suitable for display on e-ink. + * + * Usage: + * DictHtmlRenderer renderer; + * const std::vector& spans = renderer.render(html, len); + * + * The returned reference is valid until the next call to render(). + * The renderer is reusable. + */ +class DictHtmlRenderer { + public: + DictHtmlRenderer(); + ~DictHtmlRenderer(); + + DictHtmlRenderer(const DictHtmlRenderer&) = delete; + DictHtmlRenderer& operator=(const DictHtmlRenderer&) = delete; + + const std::vector& render(const char* html, int len); + + // Stream-render from an open file. Reads definition in 512-byte chunks — the full + // definition is never held in RAM. Entity resolution handles cross-chunk boundaries. + // The file must be open for reading; caller is responsible for close. + const std::vector& renderFromFile(const char* dictPath, uint32_t offset, uint32_t size); + + // Span delivery for the streaming render path. fn is invoked once per finished + // span; span.text is valid ONLY for the duration of the call (it points into + // reusable scratch), so the sink MUST copy it immediately. Function-pointer + + // ctx (NOT std::function) per the project's binary-size rules. + struct SpanSink { + void* ctx = nullptr; + void (*fn)(void* ctx, const StyledSpan& span) = nullptr; + void operator()(const StyledSpan& span) const { fn(ctx, span); } + }; + + // Stream-render from a .dict file, delivering each span to `sink` as it is + // produced. Unlike renderFromFile(), the whole-definition textBuf and spans + // vector are NEVER materialized — peak RAM is one span's scratch. Returns true + // on success. Used by the definition view so only one page is ever resident. + bool renderFromFileStreaming(const char* dictPath, uint32_t offset, uint32_t size, const SpanSink& sink); + + // Recovery path for browser-tolerated HTML that Expat cannot parse (for + // example, repeated
  • tags without matching
  • tags). Streams the + // visible text while discarding tags and preserving basic block breaks. + bool renderPlainTextFromFileStreaming(const char* dictPath, uint32_t offset, uint32_t size, const SpanSink& sink); + +#ifdef DICT_HTML_RENDERER_TRACK_UNKNOWN + bool hasUnknownTags() const { return unknownTagCount > 0; } + + struct UnknownTagInfo { + char tag[48]; + char entry[48]; + char wordBefore[32]; + char tagContents[64]; + char wordAfter[32]; + }; + + static constexpr int MAX_UNKNOWN_TAGS = 16; + UnknownTagInfo unknownTags[MAX_UNKNOWN_TAGS]; + int unknownTagCount = 0; + + const char* currentEntryName = nullptr; +#endif + + private: + static void XMLCALL onStart(void* ud, const XML_Char* name, const XML_Char** atts); + static void XMLCALL onEnd(void* ud, const XML_Char* name); + static void XMLCALL onText(void* ud, const XML_Char* s, int len); + static const char* findAttr(const XML_Char** atts, const char* name); + + enum class TagAction { + BLOCK_STRIP, + FORMAT_BOLD, + FORMAT_ITALIC, + FORMAT_UNDERLINE, + FORMAT_STRIKE, + FORMAT_SMALL, + FORMAT_CODE, + BLOCK_BREAK, + BLOCK_QUOTE, + LIST_ITEM, + LIST_CONTAINER, + HEADING, + ABBR, + VAR, + SPAN, + WIKI_ANNOT, + REGISTERED, + STRIP_KEEP, + }; + + static TagAction classify(const XML_Char* name); + + // Format state that is saved/restored at tag boundaries. + // Does NOT include transient flags (newlinePending, listItemPending). + struct FormatState { + bool bold = false; + bool italic = false; + bool underline = false; + bool strikethrough = false; + uint8_t indentLevel = 0; + }; + + struct StackEntry { + TagAction action; + FormatState savedFmt; + bool suppressChildren; + std::string abbrTitle; + std::string wikiText; +#ifdef DICT_HTML_RENDERER_TRACK_UNKNOWN + // For unknown tag tracking: tag name recorded at open + char unknownTagName[48]; + // Pending text length at the time unknown tag opened (to extract contents later) + int pendingLenAtOpen; +#endif + }; + + void reset(); + void pushSpan(); + void emitText(const char* s, int len); + void flushPending(); + + // Shared parse driver: feeds an already-open, already-seeked file through expat + // (synthetic root + 512-byte chunk loop + entity resolution). Used by both the + // batch renderFromFile() and the streaming renderFromFileStreaming(). + void parseOpenFile(HalFile& file, uint32_t size); + + // Feed a buffer through the entity resolver into expat. If the buffer ends mid-entity, + // the partial entity is written to carry/carryLen for prepending to the next chunk. + // Set isLast=true for the final chunk (no carryover possible). + void feedEntityResolved(const char* buf, int len, bool isLast, char* carry, int* carryLen); + + // Plain-text equivalent used by the malformed-HTML recovery path. Tags are + // discarded, common/numeric entities are decoded, and text is emitted via + // the installed span sink. + void feedPlainText(const char* buf, int len, bool isLast, char* carry, int* carryLen); + + // Normalize HTML void elements before handing the fragment to Expat. StarDict + // HTML is often valid HTML but not valid XML (for example
    and + // without closing tags), and image attributes may contain binary resource IDs + // that XML rejects. The scratch strings are members so paging reuses their + // capacity instead of allocating a second definition-sized buffer. + void normalizeHtmlChunk(const char* buf, int len, bool isLast); + + std::vector tagStack; + + FormatState fmt; + + // Transient rendering flags — NOT saved/restored per tag + bool newlinePending = false; + bool listItemPending = false; + + std::vector textBuf; + + std::string pendingText; + + std::string htmlCarry_; + std::string htmlInput_; + std::string normalizedHtml_; + bool discardTagUntilClose_ = false; + + std::vector spans; + + XML_Parser parser = nullptr; + bool parseError = false; + + // When set (streaming mode), pushSpan() delivers spans here instead of + // accumulating into textBuf/spans. Null in batch mode. Cleared by reset(). + SpanSink spanSink_; + +#ifdef DICT_HTML_RENDERER_TRACK_UNKNOWN + static void extractLastWord(const char* text, int len, char* out, int outSize); + void recordUnknownTag(const char* tagName, const char* wordBefore, const char* tagContents, const char* wordAfter); +#endif +}; diff --git a/lib/EpdFont/EpdFont.cpp b/lib/EpdFont/EpdFont.cpp index 7684182952..8eb7d2c957 100644 --- a/lib/EpdFont/EpdFont.cpp +++ b/lib/EpdFont/EpdFont.cpp @@ -44,16 +44,17 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star continue; } - const int raiseBy = isCombining ? combiningMark::raiseAboveBase(glyph->top, glyph->height, lastBaseTop) : 0; + const combiningMark::Anchor anchor = combiningMark::anchorFor(cp); + const int raiseBy = isCombining ? combiningMark::raiseAboveBase(anchor, glyph->top, glyph->height, lastBaseTop) : 0; if (!isCombining && prevCp != 0) { const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP); } - const int glyphBaseX = - isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width) - : lastBaseX; + const int glyphBaseX = isCombining ? combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth, + glyph->left, glyph->width) + : lastBaseX; const int glyphBaseY = startY - raiseBy; *minX = std::min(*minX, glyphBaseX + glyph->left); @@ -195,3 +196,21 @@ const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const { } return nullptr; } + +bool EpdFont::hasCodepoint(const uint32_t cp) const { + const int count = data->intervalCount; + if (count > 0) { + const EpdUnicodeInterval* intervals = data->intervals; + const auto* end = intervals + count; + const auto it = std::upper_bound( + intervals, end, cp, [](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; }); + if (it != intervals && cp <= (it - 1)->last) return true; + } + + // Interval table miss. SD card fonts only keep the current page's glyphs in + // their interval table — ask their full RAM-resident coverage index instead. + if (data->coverageHandler) { + return data->coverageHandler(data->glyphMissCtx, cp); + } + return false; +} diff --git a/lib/EpdFont/EpdFont.h b/lib/EpdFont/EpdFont.h index 366a66110a..d9f9bd3f5a 100644 --- a/lib/EpdFont/EpdFont.h +++ b/lib/EpdFont/EpdFont.h @@ -13,6 +13,14 @@ class EpdFont { const EpdGlyph* findGlyph(uint32_t cp) const; const EpdGlyph* getGlyph(uint32_t cp) const; + /// Returns true if this font covers `cp`: either via its in-RAM interval + /// table or, for SD card fonts, via the coverageHandler that consults the + /// full RAM-resident coverage index. Unlike getGlyph(), it never performs + /// storage I/O and never falls back to the replacement glyph — it reports + /// only what this font can render. Used by the CJK UI font fallback to + /// decide whether a string needs to be routed to another font. + bool hasCodepoint(uint32_t cp) const; + /// Returns the kerning adjustment (4.4 fixed-point in pixels) between two codepoints. /// Returns 0 if no kerning data exists for the pair. int8_t getKerning(uint32_t leftCp, uint32_t rightCp) const; diff --git a/lib/EpdFont/EpdFontData.h b/lib/EpdFont/EpdFontData.h index 6a3b8a9ff0..2411708fd5 100644 --- a/lib/EpdFont/EpdFontData.h +++ b/lib/EpdFont/EpdFontData.h @@ -36,24 +36,71 @@ namespace combiningMark { constexpr int MIN_GAP_PX = 1; +/// Placement of a mark relative to its base glyph. The default heuristic — +/// centered over the base, raised clear of its top — suits Latin diacritics +/// and Arabic harakat, but misplaces the Hebrew niqqud whose identity depends +/// on position: dagesh sits inside the letter body, the shin/sin dots +/// distinguish the letter by sitting over its right/left arm, and holam hangs +/// over the left corner. "Native" anchors keep the glyph's font-designed +/// height (which may overlap the base) instead of raising it. +enum class Anchor : uint8_t { + CenterRaised, ///< centered over the base, lifted above its top (default) + CenterNative, ///< centered over the base at font-native height + RightNative, ///< right edges aligned, font-native height + LeftNative, ///< left edges aligned, font-native height +}; + +constexpr Anchor anchorFor(const uint32_t cp) { + switch (cp) { + case 0x05BC: // dagesh / mapiq / shuruk dot: inside the letter body + case 0x05BA: // holam haser for vav: straight above the vav stem + return Anchor::CenterNative; + case 0x05C1: // shin dot: over the letter's right arm + return Anchor::RightNative; + case 0x05B9: // holam: above the letter's left corner + case 0x05C2: // sin dot: over the letter's left arm + return Anchor::LeftNative; + default: + return Anchor::CenterRaised; + } +} + +/// Horizontal offset from the base bitmap's left edge to the mark bitmap's +/// left edge for a given anchor. +constexpr int anchorShift(const Anchor anchor, const int baseWidth, const int markWidth) { + switch (anchor) { + case Anchor::LeftNative: + return 0; + case Anchor::RightNative: + return baseWidth - markWidth; + default: + return baseWidth / 2 - markWidth / 2; + } +} + /// Compute the cursor-X at which to render a combining mark so its bitmap -/// is visually centered over the base glyph's bitmap. -constexpr int centerOver(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) { - return baseCursorPos + baseLeft + baseWidth / 2 - markWidth / 2 - markLeft; +/// lands at its anchor position over the base glyph's bitmap. +constexpr int anchorOver(const Anchor anchor, const int baseCursorPos, const int baseLeft, const int baseWidth, + const int markLeft, const int markWidth) { + return baseCursorPos + baseLeft + anchorShift(anchor, baseWidth, markWidth) - markLeft; } -/// Rotated-90CW variant of centerOver. In the rotated coordinate system +/// Rotated-90CW variant of anchorOver. In the rotated coordinate system /// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so /// every left/width term inverts sign. -constexpr int centerOverRotated90CW(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) { - return baseCursorPos - baseLeft - baseWidth / 2 + markWidth / 2 + markLeft; +constexpr int anchorOverRotated90CW(const Anchor anchor, const int baseCursorPos, const int baseLeft, + const int baseWidth, const int markLeft, const int markWidth) { + return baseCursorPos - baseLeft - anchorShift(anchor, baseWidth, markWidth) + markLeft; } /// For combining marks that sit entirely above the baseline, compute how many /// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom /// edge and the top of the base glyph. Returns 0 for marks that extend to or -/// below the baseline (e.g. cedilla, dot-below, ogonek). -constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) { +/// below the baseline (e.g. cedilla, dot-below, ogonek) and for anchors that +/// keep the font-native height (dagesh must stay inside the letter, the +/// shin/sin dots touch its arms). +constexpr int raiseAboveBase(const Anchor anchor, const int markTop, const int markHeight, const int baseTop) { + if (anchor != Anchor::CenterRaised) return 0; if (markTop - markHeight <= 0) return 0; const int gap = markTop - markHeight - baseTop; return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0; @@ -61,6 +108,20 @@ constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) { } // namespace combiningMark +/// GCC/Clang (the ESP32 firmware toolchain) pack structs with __attribute__((packed)). +/// MSVC (host unit tests) has no equivalent attribute and instead needs a #pragma pack +/// region achieving the same 1-byte alignment. These macros keep the on-disk font layout +/// identical across both toolchains. +#if defined(_MSC_VER) +#define EPD_PACKED_BEGIN __pragma(pack(push, 1)) +#define EPD_PACKED_END __pragma(pack(pop)) +#define EPD_PACKED_ATTR +#else +#define EPD_PACKED_BEGIN +#define EPD_PACKED_END +#define EPD_PACKED_ATTR __attribute__((packed)) +#endif + /// Fixed-point conventions used by EpdGlyph and EpdFontData: /// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel) /// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel) @@ -95,17 +156,21 @@ typedef struct { /// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search. /// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning). +EPD_PACKED_BEGIN typedef struct { uint16_t codepoint; ///< Unicode codepoint uint8_t classId; ///< 1-based kerning class ID -} __attribute__((packed)) EpdKernClassEntry; +} EPD_PACKED_ATTR EpdKernClassEntry; +EPD_PACKED_END /// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search. /// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup. +EPD_PACKED_BEGIN typedef struct { uint32_t pair; ///< Packed codepoint pair (left << 16 | right) uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph -} __attribute__((packed)) EpdLigaturePair; +} EPD_PACKED_ATTR EpdLigaturePair; +EPD_PACKED_END /// Data stored for FONT AS A WHOLE typedef struct { @@ -141,6 +206,13 @@ typedef struct { /// Context pointer for glyphMissHandler (typically SdCardFont*). Also used by /// GfxRenderer::getGlyphBitmap() to retrieve overflow bitmaps via SdCardFont. void* glyphMissCtx; + + /// Full-coverage query for fonts whose interval table only reflects what is + /// currently in RAM (SD card fonts: stub/mini data cover at most one page of + /// glyphs). Called by hasCodepoint() when the interval table misses; must + /// answer from RAM-resident data without storage I/O. Shares glyphMissCtx. + /// nullptr for fonts whose interval table is already complete (built-ins). + bool (*coverageHandler)(void* ctx, uint32_t codepoint); } EpdFontData; namespace syntheticGlyph { diff --git a/lib/EpdFont/EpdFontFamily.cpp b/lib/EpdFont/EpdFontFamily.cpp index 9b41b12018..2f3aac3f21 100644 --- a/lib/EpdFont/EpdFontFamily.cpp +++ b/lib/EpdFont/EpdFontFamily.cpp @@ -149,16 +149,17 @@ void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const continue; } - const int raiseBy = isCombining ? combiningMark::raiseAboveBase(glyph->top, glyph->height, lastBaseTop) : 0; + const auto anchor = combiningMark::anchorFor(cp); + const int raiseBy = isCombining ? combiningMark::raiseAboveBase(anchor, glyph->top, glyph->height, lastBaseTop) : 0; if (!isCombining && prevCp != 0) { const auto kernFP = getKerning(prevCp, cp, style); lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP); } - const int glyphBaseX = - isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width) - : lastBaseX; + const int glyphBaseX = isCombining ? combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth, + glyph->left, glyph->width) + : lastBaseX; const int glyphBaseY = -raiseBy; minX = std::min(minX, glyphBaseX + glyph->left); @@ -222,6 +223,10 @@ uint32_t EpdFontFamily::getFallbackCodepoint(const uint32_t cp, const Style styl return REPLACEMENT_GLYPH; } +bool EpdFontFamily::hasCodepoint(const uint32_t cp, const Style style) const { + return getFont(style)->hasCodepoint(cp); +} + int8_t EpdFontFamily::getKerning(const uint32_t leftCp, const uint32_t rightCp, const Style style) const { return getFont(style)->getKerning(leftCp, rightCp); } diff --git a/lib/EpdFont/EpdFontFamily.h b/lib/EpdFont/EpdFontFamily.h index a1cdb8e081..d5dbabfc52 100644 --- a/lib/EpdFont/EpdFontFamily.h +++ b/lib/EpdFont/EpdFontFamily.h @@ -14,6 +14,8 @@ class EpdFontFamily { STRIKETHROUGH = 8, SUP = 16, SUB = 32, + SMALL_CAPS = 64, + RUBY_CONTINUE = 128, }; struct GlyphData { const EpdFontData* fontData; @@ -30,6 +32,9 @@ class EpdFontFamily { GlyphData getGlyphData(uint32_t cp, Style style = REGULAR) const; const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const; uint32_t getFallbackCodepoint(uint32_t cp, Style style = REGULAR) const; + /// Returns true if the resolved style's font can render `cp` directly + /// (interval coverage only — see EpdFont::hasCodepoint). + bool hasCodepoint(uint32_t cp, Style style = REGULAR) const; int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const; uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const; diff --git a/lib/EpdFont/FontDecompressor.cpp b/lib/EpdFont/FontDecompressor.cpp index fefe4b2c32..b2d5a59efc 100644 --- a/lib/EpdFont/FontDecompressor.cpp +++ b/lib/EpdFont/FontDecompressor.cpp @@ -74,7 +74,7 @@ bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t gro const EpdFontGroup& group = fontData->groups[groupIndex]; const uint32_t tDecomp = millis(); - inflateReader.init(false); + inflateReader.init(); inflateReader.setSource(&fontData->bitmap[group.compressedOffset], group.compressedSize); if (!inflateReader.read(outBuf, outSize)) { stats.decompressTimeMs += millis() - tDecomp; diff --git a/lib/EpdFont/SdCardFont.cpp b/lib/EpdFont/SdCardFont.cpp index 6d659b508b..82c3eed5b9 100644 --- a/lib/EpdFont/SdCardFont.cpp +++ b/lib/EpdFont/SdCardFont.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -65,9 +66,33 @@ bool collectUniqueCodepoints(const char* text, uint32_t* codepoints, uint32_t& c return false; } +uint32_t countUtf8Codepoints(const char* text, const uint32_t limit) { + if (!text) return 0; + const auto* p = reinterpret_cast(text); + uint32_t count = 0; + while (*p && count < limit) { + if (utf8NextCodepoint(&p) == 0) break; + ++count; + } + return count; +} + const char* asCStr(const std::string& s) { return s.c_str(); } const char* asCStr(const char* s) { return s; } +constexpr size_t MINI_RETAIN_MIN_FREE_HEAP = 40U * 1024U; +constexpr size_t MINI_RETAIN_MIN_MAX_ALLOC_HEAP = 32U * 1024U; +constexpr uint8_t MINI_UNDERUSE_RUNS_BEFORE_FREE = 3; + +template +bool ensureArrayCapacity(T*& buffer, Capacity& capacity, const uint32_t needed) { + if (buffer && capacity >= needed) return true; + delete[] buffer; + buffer = new (std::nothrow) T[needed > 0 ? needed : 1]; + capacity = buffer ? static_cast(needed) : 0; + return buffer != nullptr; +} + } // namespace SdCardFont::~SdCardFont() { freeAll(); } @@ -83,11 +108,49 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) { s.miniBitmap = nullptr; s.miniIntervalCount = 0; s.miniGlyphCount = 0; + s.miniIntervalCapacity = 0; + s.miniGlyphCapacity = 0; + s.miniBitmapCapacity = 0; + s.miniBitmapUsed = 0; + s.miniUnderuseRuns = 0; + s.miniMetadataOnly = false; + s.miniHysteresisPending = false; freeStyleMiniKern(s); memset(&s.miniData, 0, sizeof(s.miniData)); s.epdFont.data = &s.stubData; } +void SdCardFont::resetStyleMiniData(PerStyle& s) { + if (ESP.getFreeHeap() < MINI_RETAIN_MIN_FREE_HEAP || ESP.getMaxAllocHeap() < MINI_RETAIN_MIN_MAX_ALLOC_HEAP) { + freeStyleMiniData(s); + return; + } + + // Underuse hysteresis, on the bitmap arena (the dominant allocation): an + // outlier page (e.g. three styles cramped together) would otherwise pin its + // high-water arena for the rest of the book. Keep while the page used at + // least 3/4 of capacity; release only after several consecutive rebuilds + // below that, so alternating dense/sparse pages never thrash. Evaluated at + // most once per rebuild (a scope both constructs and destructs through here, + // and subset hits load nothing new to judge). + if (s.miniHysteresisPending && s.miniBitmapCapacity > 0 && s.miniBitmapUsed > 0) { + s.miniHysteresisPending = false; + if (s.miniBitmapUsed < s.miniBitmapCapacity - s.miniBitmapCapacity / 4) { + if (++s.miniUnderuseRuns >= MINI_UNDERUSE_RUNS_BEFORE_FREE) { + LOG_DBG("SDCF", "Releasing underused mini buffers: used=%u capacity=%u", s.miniBitmapUsed, + s.miniBitmapCapacity); + freeStyleMiniData(s); + return; + } + } else { + s.miniUnderuseRuns = 0; + } + } + // Data (intervals/glyphs/bitmaps/kern) deliberately survives the scope: the + // next prewarm subset-checks against it, which is what lets the idle prewarm + // of page N+1 serve the actual page turn with zero SD reads. +} + void SdCardFont::freeStyleKernLigatureData(PerStyle& s) { delete[] s.kernLeftClasses; s.kernLeftClasses = nullptr; @@ -95,7 +158,8 @@ void SdCardFont::freeStyleKernLigatureData(PerStyle& s) { s.kernRightClasses = nullptr; delete[] s.ligaturePairs; s.ligaturePairs = nullptr; - s.kernLigLoaded = false; + s.kernClassesLoaded = false; + s.ligaturesLoaded = false; s.stubData.ligaturePairs = nullptr; s.stubData.ligaturePairCount = 0; @@ -121,6 +185,9 @@ void SdCardFont::freeStyleMiniKern(PerStyle& s) { s.miniKernRightEntryCount = 0; s.miniKernLeftClassCount = 0; s.miniKernRightClassCount = 0; + s.miniKernLeftCapacity = 0; + s.miniKernRightCapacity = 0; + s.miniKernMatrixCapacity = 0; } void SdCardFont::freeStyleAll(PerStyle& s) { @@ -159,29 +226,25 @@ void SdCardFont::clearOverflow() { // --- Per-style kern/ligature --- -void SdCardFont::applyKernLigaturePointers(PerStyle& s, EpdFontData& data) const { +void SdCardFont::applyKernLigaturePointers(PerStyle& s, EpdFontData& data, const bool includeKerning) const { // Kern data uses the per-page mini tables (renumbered class IDs). The full // kern matrix is never resident — see PerStyle::miniKernMatrix comment. - data.kernLeftClasses = s.miniKernLeftClasses; - data.kernRightClasses = s.miniKernRightClasses; - data.kernMatrix = s.miniKernMatrix; - data.kernLeftEntryCount = s.miniKernLeftEntryCount; - data.kernRightEntryCount = s.miniKernRightEntryCount; - data.kernLeftClassCount = s.miniKernLeftClassCount; - data.kernRightClassCount = s.miniKernRightClassCount; + data.kernLeftClasses = includeKerning ? s.miniKernLeftClasses : nullptr; + data.kernRightClasses = includeKerning ? s.miniKernRightClasses : nullptr; + data.kernMatrix = includeKerning ? s.miniKernMatrix : nullptr; + data.kernLeftEntryCount = includeKerning ? s.miniKernLeftEntryCount : 0; + data.kernRightEntryCount = includeKerning ? s.miniKernRightEntryCount : 0; + data.kernLeftClassCount = includeKerning ? s.miniKernLeftClassCount : 0; + data.kernRightClassCount = includeKerning ? s.miniKernRightClassCount : 0; // Ligatures are small (typically < 1KB) so they stay resident. data.ligaturePairs = s.ligaturePairs; data.ligaturePairCount = s.header.ligaturePairCount; } -bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) { - if (s.kernLigLoaded) return true; - bool hasKern = s.header.kernLeftEntryCount > 0; - bool hasLig = s.header.ligaturePairCount > 0; - if (!hasKern && !hasLig) { - s.kernLigLoaded = true; - return true; - } +bool SdCardFont::loadStyleKernLigatureData(PerStyle& s, const bool includeKerning) { + const bool needKern = includeKerning && s.header.kernLeftEntryCount > 0 && !s.kernClassesLoaded; + const bool needLig = s.header.ligaturePairCount > 0 && !s.ligaturesLoaded; + if (!needKern && !needLig) return true; HalFile file; if (!Storage.openFileForRead("SDCF", filePath_, file)) { @@ -189,7 +252,7 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) { return false; } - if (hasKern) { + if (needKern) { // Load only the small class-lookup tables (~3KB each). The full matrix // (~36KB contiguous for Literata) is built per-page from SD in // buildMiniKernMatrix(). @@ -218,7 +281,7 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) { } } - if (hasLig) { + if (needLig) { s.ligaturePairs = new (std::nothrow) EpdLigaturePair[s.header.ligaturePairCount]; if (!s.ligaturePairs) { LOG_ERR("SDCF", "Failed to allocate ligature pairs"); @@ -238,7 +301,8 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) { } } - s.kernLigLoaded = true; + if (needKern) s.kernClassesLoaded = true; + if (needLig) s.ligaturesLoaded = true; // Make ligatures visible to the stub (used when no mini data built yet). // Kern stays nullptr on the stub — it is only wired in miniData via @@ -246,8 +310,8 @@ bool SdCardFont::loadStyleKernLigatureData(PerStyle& s) { s.stubData.ligaturePairs = s.ligaturePairs; s.stubData.ligaturePairCount = s.header.ligaturePairCount; - LOG_DBG("SDCF", "Kern classes + lig loaded: kernL=%u, kernR=%u, ligs=%u", s.header.kernLeftEntryCount, - s.header.kernRightEntryCount, s.header.ligaturePairCount); + LOG_DBG("SDCF", "Typography data loaded: kern=%s ligs=%u", s.kernClassesLoaded ? "yes" : "no", + s.header.ligaturePairCount); return true; } @@ -275,9 +339,22 @@ static uint8_t miniLookupKernClass(const EpdKernClassEntry* entries, uint16_t co // on this page simply returns class 0 (no kerning), which was the pre-existing // behavior for any codepoint outside the kern classes. bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount) { - freeStyleMiniKern(s); + // No freeStyleMiniKern here: it zeroed the capacities, which forced the + // ensureArrayCapacity calls below to reallocate every page and defeated the + // buffer reuse. prewarmStyle is the only caller and the success path + // overwrites the contents and all four counts, so keeping the buffers is + // safe. The early returns zero the counts (buffers kept) so a page with no + // applicable kern pairs kerns as none instead of through the previous + // page's tables. + const auto resetMiniKernCounts = [&s]() { + s.miniKernLeftEntryCount = 0; + s.miniKernRightEntryCount = 0; + s.miniKernLeftClassCount = 0; + s.miniKernRightClassCount = 0; + }; if (!s.kernLeftClasses || !s.kernRightClasses || s.header.kernLeftEntryCount == 0 || s.header.kernRightEntryCount == 0) { + resetMiniKernCounts(); return true; // font has no kern classes — nothing to build } @@ -311,6 +388,7 @@ bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, ui } } if (numLeft == 0 || numRight == 0) { + resetMiniKernCounts(); return true; // no kern pairs applicable on this page } @@ -323,13 +401,12 @@ bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, ui if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++; } - // Step 4: allocate the three mini buffers. The matrix is <1KB in practice - // (<30 × <30 × 1 byte) so fragmentation is a non-issue. + // Step 4: size the three mini buffers, reusing each page's high-water + // capacity when it still fits. const uint32_t matrixBytes = static_cast(numLeft) * numRight; - s.miniKernLeftClasses = new (std::nothrow) EpdKernClassEntry[miniLeftCount]; - s.miniKernRightClasses = new (std::nothrow) EpdKernClassEntry[miniRightCount]; - s.miniKernMatrix = new (std::nothrow) int8_t[matrixBytes]; - if (!s.miniKernLeftClasses || !s.miniKernRightClasses || !s.miniKernMatrix) { + if (!ensureArrayCapacity(s.miniKernLeftClasses, s.miniKernLeftCapacity, miniLeftCount) || + !ensureArrayCapacity(s.miniKernRightClasses, s.miniKernRightCapacity, miniRightCount) || + !ensureArrayCapacity(s.miniKernMatrix, s.miniKernMatrixCapacity, matrixBytes)) { LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u, matrixBytes); freeStyleMiniKern(s); @@ -414,6 +491,14 @@ void SdCardFont::applyGlyphMissCallback(uint8_t styleIdx) { auto& s = styles_[styleIdx]; s.stubData.glyphMissHandler = &SdCardFont::onGlyphMiss; s.stubData.glyphMissCtx = &overflowCtx_[styleIdx]; + s.stubData.coverageHandler = &SdCardFont::onCoverageQuery; +} + +bool SdCardFont::onCoverageQuery(void* ctx, const uint32_t codepoint) { + const auto* octx = static_cast(ctx); + const PerStyle& s = octx->self->styles_[octx->styleIdx]; + if (!s.fullIntervals && !s.bmpIntervals) return false; // coverage index freed/never loaded + return octx->self->findGlobalGlyphIndex(s, codepoint) >= 0; } // --- Compute per-style file offsets from a base data offset --- @@ -639,9 +724,6 @@ bool SdCardFont::load(const char* path) { for (uint8_t i = 0; i < MAX_STYLES; i++) { if (!styles_[i].present) continue; const auto& h = styles_[i].header; - LOG_DBG("SDCF", " style[%u]: %u intervals, %u glyphs, advY=%u, asc=%d, desc=%d, kernL=%u, kernR=%u, ligs=%u", i, - h.intervalCount, h.glyphCount, h.advanceY, h.ascender, h.descender, h.kernLeftEntryCount, - h.kernRightEntryCount, h.ligaturePairCount); } return true; } @@ -703,7 +785,7 @@ bool SdCardFont::readAdvance(uint32_t codepoint, uint8_t style, uint16_t* outAdv // --- Prewarm --- -int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOnly) { +int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOnly, const bool includeKerning) { lastPrewarmFailed_ = false; if (!loaded_) return failPrewarm(-1); styleMask = resolveStyleMask(styleMask); @@ -764,7 +846,7 @@ int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOn if (!(styleMask & (1 << si)) || !styles_[si].present) continue; auto& s = styles_[si]; - loadStyleKernLigatureData(s); + loadStyleKernLigatureData(s, includeKerning); if (s.ligaturePairs && s.header.ligaturePairCount > 0) { for (uint8_t li = 0; li < s.header.ligaturePairCount && cpCount < MAX_PAGE_GLYPHS; li++) { uint32_t leftCp = s.ligaturePairs[li].pair >> 16; @@ -801,7 +883,7 @@ int SdCardFont::prewarm(const char* utf8Text, uint8_t styleMask, bool metadataOn int totalMissed = 0; for (uint8_t si = 0; si < MAX_STYLES; si++) { if (!(styleMask & (1 << si)) || !styles_[si].present) continue; - totalMissed += prewarmStyle(si, codepoints.get(), cpCount, metadataOnly); + totalMissed += prewarmStyle(si, codepoints.get(), cpCount, metadataOnly, includeKerning); } stats_.prewarmTotalMs = millis() - startMs; @@ -813,9 +895,44 @@ int SdCardFont::failPrewarm(const int missed) { return missed; } -int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly) { +int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly, + const bool includeKerning) { auto& s = styles_[styleIdx]; + // Idle-prewarm hit: mini data persists across PrewarmScopes (resetStyleMiniData + // keeps it), so when the previous scope -- typically the idle prewarm of this + // exact page -- already loaded every requested codepoint the font covers, this + // page needs zero SD reads. A mini built metadata-only cannot serve a full + // request (no bitmaps). Any uncovered codepoint falls through to the rebuild. + if (s.miniGlyphCount > 0 && !(s.miniMetadataOnly && !metadataOnly)) { + bool covered = true; + int missedInMini = 0; + for (uint32_t i = 0; i < cpCount && covered; i++) { + const uint32_t cp = codepoints[i]; + bool inMini = false; + for (uint32_t iv = 0; iv < s.miniIntervalCount; iv++) { + if (cp < s.miniIntervals[iv].first) break; // intervals sorted ascending + if (cp <= s.miniIntervals[iv].last) { + inMini = true; + break; + } + } + if (inMini) continue; + if (findGlobalGlyphIndex(s, cp) < 0) { + missedInMini++; // not in font coverage: the rebuild couldn't load it either + } else { + covered = false; + } + } + if (covered) { + if (!metadataOnly && loadStyleKernLigatureData(s, includeKerning)) { + const bool kerningReady = !includeKerning || s.miniKernMatrix || buildMiniKernMatrix(s, codepoints, cpCount); + if (kerningReady) applyKernLigaturePointers(s, s.miniData, includeKerning); + } + return missedInMini; + } + } + // Map codepoints to global glyph indices for this style struct CpGlyphMapping { uint32_t codepoint; @@ -845,14 +962,21 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 return missed; } - // Build mini intervals from sorted codepoints - freeStyleMiniData(s); + // Reset the visible data while retaining page-buffer capacities. + s.miniIntervalCount = 0; + s.miniGlyphCount = 0; + s.miniBitmapUsed = 0; + s.miniKernLeftEntryCount = 0; + s.miniKernRightEntryCount = 0; + s.miniKernLeftClassCount = 0; + s.miniKernRightClassCount = 0; + memset(&s.miniData, 0, sizeof(s.miniData)); + s.epdFont.data = &s.stubData; - uint32_t intervalCapacity = validCount; - s.miniIntervals = new (std::nothrow) EpdUnicodeInterval[intervalCapacity]; - if (!s.miniIntervals) { + if (!ensureArrayCapacity(s.miniIntervals, s.miniIntervalCapacity, validCount)) { LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx); delete[] mappings; + freeStyleMiniData(s); return failPrewarm(static_cast(cpCount)); } @@ -868,15 +992,14 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 } } - // Allocate mini glyph array - s.miniGlyphCount = validCount; - s.miniGlyphs = new (std::nothrow) EpdGlyph[s.miniGlyphCount]; - if (!s.miniGlyphs) { + // Allocate or reuse the mini glyph array. + if (!ensureArrayCapacity(s.miniGlyphs, s.miniGlyphCapacity, validCount)) { LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx); delete[] mappings; freeStyleMiniData(s); return failPrewarm(static_cast(cpCount)); } + s.miniGlyphCount = validCount; // Build sorted read order for sequential I/O uint32_t* readOrder = new (std::nothrow) uint32_t[validCount]; @@ -943,14 +1066,14 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 totalBitmapSize += s.miniGlyphs[i].dataLength; } - s.miniBitmap = new (std::nothrow) uint8_t[totalBitmapSize > 0 ? totalBitmapSize : 1]; - if (!s.miniBitmap) { + if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) { LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx); delete[] readOrder; delete[] mappings; freeStyleMiniData(s); return failPrewarm(static_cast(cpCount)); } + s.miniBitmapUsed = totalBitmapSize; // Read bitmap data sorted by file offset std::sort(readOrder, readOrder + validCount, @@ -1002,16 +1125,18 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 // per-page mini kern matrix restricted to class pairs reachable from this // page's codepoints. Skip during metadata-only prewarm — layout only needs // advanceX and the mini kern would be thrown away before rendering. - bool kernLigOk = false; + bool typographyOk = false; if (!metadataOnly) { - if (loadStyleKernLigatureData(s)) { - kernLigOk = buildMiniKernMatrix(s, codepoints, cpCount); + if (loadStyleKernLigatureData(s, includeKerning)) { + typographyOk = !includeKerning || buildMiniKernMatrix(s, codepoints, cpCount); } } // Populate miniData and swap + s.miniMetadataOnly = metadataOnly; + s.miniHysteresisPending = !metadataOnly; // one hysteresis evaluation per rebuild memset(&s.miniData, 0, sizeof(s.miniData)); - s.miniData.bitmap = s.miniBitmap; + s.miniData.bitmap = metadataOnly ? nullptr : s.miniBitmap; s.miniData.glyph = s.miniGlyphs; s.miniData.intervals = s.miniIntervals; s.miniData.intervalCount = s.miniIntervalCount; @@ -1019,11 +1144,12 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3 s.miniData.ascender = s.header.ascender; s.miniData.descender = s.header.descender; s.miniData.is2Bit = s.header.is2Bit; - if (kernLigOk) { - applyKernLigaturePointers(s, s.miniData); + if (typographyOk) { + applyKernLigaturePointers(s, s.miniData, includeKerning); } s.miniData.glyphMissHandler = &SdCardFont::onGlyphMiss; s.miniData.glyphMissCtx = &overflowCtx_[styleIdx]; + s.miniData.coverageHandler = &SdCardFont::onCoverageQuery; s.epdFont.data = &s.miniData; @@ -1045,7 +1171,7 @@ void SdCardFont::clearCache() { // clearPersistentCache() to wipe it. for (uint8_t i = 0; i < MAX_STYLES; i++) { if (!styles_[i].present) continue; - freeStyleMiniData(styles_[i]); + resetStyleMiniData(styles_[i]); applyGlyphMissCallback(i); } } @@ -1071,6 +1197,7 @@ void SdCardFont::clearPersistentCache() { delete[] advanceTable_[i]; advanceTable_[i] = nullptr; advanceTableSize_[i] = 0; + advanceTableCapacity_[i] = 0; } } @@ -1094,7 +1221,33 @@ bool SdCardFont::advanceTableLookup(uint8_t styleIdx, uint32_t codepoint, uint16 return false; } -void SdCardFont::mergeIntoAdvanceTable(uint8_t styleIdx, const AdvanceEntry* sortedNew, uint32_t newCount) { +bool SdCardFont::ensureAdvanceTableCapacity(const uint8_t styleIdx, const uint32_t needed) { + if (advanceTableCapacity_[styleIdx] >= needed) return true; + + uint32_t newCapacity = advanceTableCapacity_[styleIdx] == 0 ? 1 : advanceTableCapacity_[styleIdx]; + while (newCapacity < needed && newCapacity < ADVANCE_CACHE_LIMIT) { + newCapacity <<= 1; + } + if (newCapacity > ADVANCE_CACHE_LIMIT) newCapacity = ADVANCE_CACHE_LIMIT; + + auto* replacement = new (std::nothrow) AdvanceEntry[newCapacity]; + if (!replacement) { + LOG_ERR("SDCF", "Advance table grow failed (%u bytes, style %u, free=%u, maxAlloc=%u)", + static_cast(newCapacity * sizeof(AdvanceEntry)), styleIdx, ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + return false; + } + if (advanceTable_[styleIdx] && advanceTableSize_[styleIdx] > 0) { + memcpy(replacement, advanceTable_[styleIdx], advanceTableSize_[styleIdx] * sizeof(AdvanceEntry)); + } + + delete[] advanceTable_[styleIdx]; + advanceTable_[styleIdx] = replacement; + advanceTableCapacity_[styleIdx] = newCapacity; + return true; +} + +void SdCardFont::mergeIntoAdvanceTable(const uint8_t styleIdx, const AdvanceEntry* sortedNew, const uint32_t newCount) { if (newCount == 0) return; const uint32_t oldSize = advanceTableSize_[styleIdx]; if (oldSize >= ADVANCE_CACHE_LIMIT) return; // already full @@ -1105,9 +1258,11 @@ void SdCardFont::mergeIntoAdvanceTable(uint8_t styleIdx, const AdvanceEntry* sor uint32_t mergedCap = oldSize + newCount; if (mergedCap > ADVANCE_CACHE_LIMIT) mergedCap = ADVANCE_CACHE_LIMIT; - AdvanceEntry* merged = new (std::nothrow) AdvanceEntry[mergedCap]; + auto* merged = new (std::nothrow) AdvanceEntry[mergedCap]; if (!merged) { - LOG_ERR("SDCF", "mergeIntoAdvanceTable: alloc failed (%u entries) style %u", mergedCap, styleIdx); + LOG_ERR("SDCF", "Advance merge allocation failed (%u bytes, style %u, free=%u, maxAlloc=%u)", + static_cast(mergedCap * sizeof(AdvanceEntry)), styleIdx, ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); return; } @@ -1122,9 +1277,13 @@ void SdCardFont::mergeIntoAdvanceTable(uint8_t styleIdx, const AdvanceEntry* sor } } - delete[] advanceTable_[styleIdx]; - advanceTable_[styleIdx] = merged; + if (!ensureAdvanceTableCapacity(styleIdx, k)) { + delete[] merged; + return; + } + memcpy(advanceTable_[styleIdx], merged, k * sizeof(AdvanceEntry)); advanceTableSize_[styleIdx] = k; + delete[] merged; } bool SdCardFont::hasAdvanceTable() const { @@ -1174,10 +1333,9 @@ int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCoun } if (!cacheMissesRequestedCodepoint) continue; - delete[] advanceTable_[si]; - advanceTable_[si] = nullptr; advanceTableSize_[si] = 0; - LOG_DBG("SDCF", "Advance table style %u: reset full cache for active text", si); + LOG_DBG("SDCF", "Advance table style %u: reset full cache for active text (capacity=%u)", si, + advanceTableCapacity_[si]); } // For each codepoint in `codepoints`, skip those already cached, then @@ -1263,16 +1421,14 @@ int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCoun [](const AdvanceEntry& a, const AdvanceEntry& b) { return a.codepoint < b.codepoint; }); mergeIntoAdvanceTable(si, staged.get(), fetched); } - - LOG_DBG("SDCF", "Advance table style %u: +%u from SD, total=%u/%u", si, fetched, advanceTableSize_[si], - ADVANCE_CACHE_LIMIT); } return totalMissed; } template -int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask) { +int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask, + const char* extraText) { if (!loaded_) return -1; styleMask = resolveStyleMask(styleMask); if (styleMask == 0) return 0; @@ -1281,40 +1437,52 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, // +2 reserved slots for space and hyphen injected after the main scan. static constexpr uint32_t MAX_UNIQUE_CODEPOINTS = 4096; - uint32_t* codepoints = new (std::nothrow) uint32_t[MAX_UNIQUE_CODEPOINTS + 2]; + uint32_t sourceCodepointCount = 0; + for (auto it = begin; it != end && sourceCodepointCount < MAX_UNIQUE_CODEPOINTS; ++it) { + sourceCodepointCount += countUtf8Codepoints(asCStr(*it), MAX_UNIQUE_CODEPOINTS - sourceCodepointCount); + } + if (extraText && sourceCodepointCount < MAX_UNIQUE_CODEPOINTS) { + sourceCodepointCount += countUtf8Codepoints(extraText, MAX_UNIQUE_CODEPOINTS - sourceCodepointCount); + } + const uint32_t capacity = sourceCodepointCount + 2; + auto codepoints = makeUniqueNoThrow(capacity); if (!codepoints) { - LOG_ERR("SDCF", "buildAdvanceTable: failed to allocate codepoint buffer (%u bytes)", MAX_UNIQUE_CODEPOINTS * 4); + LOG_ERR("SDCF", "buildAdvanceTable: failed to allocate codepoint buffer (%u bytes)", + static_cast(capacity * sizeof(uint32_t))); return -1; } uint32_t cpCount = 0; bool hitCap = false; for (auto it = begin; it != end && !hitCap; ++it) { - hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS); + hitCap = collectUniqueCodepoints(asCStr(*it), codepoints.get(), cpCount, MAX_UNIQUE_CODEPOINTS); + } + if (extraText && !hitCap) { + hitCap = collectUniqueCodepoints(extraText, codepoints.get(), cpCount, MAX_UNIQUE_CODEPOINTS); } - if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; })) + if (includeSpace && std::none_of(codepoints.get(), codepoints.get() + cpCount, [](uint32_t c) { return c == ' '; })) codepoints[cpCount++] = ' '; - if (includeHyphen && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == '-'; })) + if (includeHyphen && std::none_of(codepoints.get(), codepoints.get() + cpCount, [](uint32_t c) { return c == '-'; })) codepoints[cpCount++] = '-'; if (hitCap) { LOG_ERR("SDCF", "buildAdvanceTable: unique codepoint cap (%u) hit, layout may be approximate", MAX_UNIQUE_CODEPOINTS); } - std::sort(codepoints, codepoints + cpCount); - int totalMissed = fetchAdvancesForCodepoints(codepoints, cpCount, styleMask); - delete[] codepoints; + std::sort(codepoints.get(), codepoints.get() + cpCount); + int totalMissed = fetchAdvancesForCodepoints(codepoints.get(), cpCount, styleMask); stats_.prewarmTotalMs = millis() - startMs; return totalMissed; } -int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) { - return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask); +int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask, const char* extraText) { + return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask, extraText); } -int SdCardFont::buildAdvanceTable(const std::vector& words, bool includeHyphen, uint8_t styleMask) { - return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask); +int SdCardFont::buildAdvanceTable(const std::deque& words, bool includeHyphen, uint8_t styleMask, + const char* extraText) { + return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask, extraText); } int SdCardFont::buildAdvanceTableForCodepoints(const uint32_t* sourceCodepoints, uint32_t cpCount, bool includeSpace, @@ -1476,9 +1644,6 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) { self->overflow_[slot].codepoint = codepoint; self->overflow_[slot].styleIdx = styleIdx; - LOG_DBG("SDCF", "Overflow: loaded U+%04X style %u on demand (slot %u/%u)", codepoint, styleIdx, slot, - OVERFLOW_CAPACITY); - return &self->overflow_[slot].glyph; } diff --git a/lib/EpdFont/SdCardFont.h b/lib/EpdFont/SdCardFont.h index e2e16771cd..518c4d7d77 100644 --- a/lib/EpdFont/SdCardFont.h +++ b/lib/EpdFont/SdCardFont.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -41,15 +42,20 @@ class SdCardFont { // styleMask: bitmask of styles to prewarm (bit 0=regular, 1=bold, 2=italic, 3=bolditalic). // Default 0x0F = all present styles. // When metadataOnly=true, only glyph metrics are loaded (no bitmap data). + // When includeKerning=false, ligatures remain active but persistent kern + // classes and per-page mini matrices are skipped (dictionary-modal policy). // Returns number of glyphs that couldn't be loaded (0 on full success). - int prewarm(const char* utf8Text, uint8_t styleMask = 0x0F, bool metadataOnly = false); + int prewarm(const char* utf8Text, uint8_t styleMask = 0x0F, bool metadataOnly = false, bool includeKerning = true); // Build a compact advance-only table for layout measurement. // Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap), // batch-reads advanceX from SD, stores in a sorted per-style table. + // extraText: optional additional codepoints to warm in the same SD pass + // (e.g. shaped Arabic presentation forms the measurement path will look up). // Returns number of codepoints not found in font coverage. - int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F); - int buildAdvanceTable(const std::vector& words, bool includeHyphen, uint8_t styleMask = 0x0F); + int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F, const char* extraText = nullptr); + int buildAdvanceTable(const std::deque& words, bool includeHyphen, uint8_t styleMask = 0x0F, + const char* extraText = nullptr); int buildAdvanceTableForCodepoints(const uint32_t* codepoints, uint32_t cpCount, bool includeSpace, bool includeHyphen, uint8_t styleMask = 0x0F); @@ -61,9 +67,9 @@ class SdCardFont { // Returns true if advance table is populated for at least one style. bool hasAdvanceTable() const; - // Free mini data for all styles and restore stub EpdFontData. - // Preserves the persistent advance cache so repeated layout passes can reuse - // previously fetched metrics. + // Reset mini data for all styles and restore stub EpdFontData. Page-sized + // allocations may be retained when heap is healthy; the persistent advance + // cache is also preserved so repeated layout passes can reuse fetched metrics. void clearCache(); // Release optional resident caches before memory-heavy work such as EPUB @@ -149,11 +155,13 @@ class SdCardFont { // Full intervals loaded from file (kept in RAM for codepoint lookup) EpdUnicodeInterval* fullIntervals = nullptr; + EPD_PACKED_BEGIN struct BmpInterval16 { uint16_t first; uint16_t last; uint16_t offset; - } __attribute__((packed)); + } EPD_PACKED_ATTR; + EPD_PACKED_END static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact"); BmpInterval16* bmpIntervals = nullptr; bool intervalsAreBmp16 = false; @@ -167,18 +175,46 @@ class SdCardFont { EpdKernClassEntry* kernLeftClasses = nullptr; EpdKernClassEntry* kernRightClasses = nullptr; EpdLigaturePair* ligaturePairs = nullptr; - bool kernLigLoaded = false; + bool kernClassesLoaded = false; + bool ligaturesLoaded = false; // Stub EpdFontData returned when not prewarmed EpdFontData stubData{}; - // Mini EpdFontData built during prewarm + // Mini EpdFontData built during prewarm. Buffers are kept-if-fits across pages + // (capacities below track allocated sizes): freeing and reallocating slightly + // different sizes on every page turn was a primary heap fragmenter — each page's + // freed hole rarely fit the next page's need, so maxAlloc eroded all session. + // The per-render PrewarmScope calls clearCache() -> resetStyleMiniData(), which + // keeps both the allocations AND the loaded data. Buffers: reuse means + // ensureArrayCapacity early-returns once capacities converge on the book's + // max, so page turns stop touching the allocator (the free/realloc-per-page + // pattern was a primary heap fragmenter). Data: the next prewarm + // subset-checks against the resident tables (see prewarmStyle), so the idle + // prewarm of page N+1 serves the actual turn with zero SD reads. Retention + // is bounded two ways in resetStyleMiniData(): a heap floor frees outright + // under pressure, and sustained underuse (an outlier page's oversized bitmap + // arena) frees after a few consecutive low-use rebuilds. freeStyleMiniData() + // remains the full teardown (zeroes capacities) for style eviction / font + // unload. EpdFontData miniData{}; EpdUnicodeInterval* miniIntervals = nullptr; EpdGlyph* miniGlyphs = nullptr; uint8_t* miniBitmap = nullptr; uint32_t miniIntervalCount = 0; uint32_t miniGlyphCount = 0; + uint32_t miniIntervalCapacity = 0; + uint32_t miniGlyphCapacity = 0; + uint32_t miniBitmapCapacity = 0; + uint32_t miniBitmapUsed = 0; + uint8_t miniUnderuseRuns = 0; + // True when the resident mini was built metadata-only (no bitmaps): it can + // serve metadata requests but a full render request must rebuild. + bool miniMetadataOnly = false; + // Set by a rebuild, consumed by resetStyleMiniData: gates the underuse + // hysteresis to one evaluation per rebuild (scopes reset twice, and subset + // hits load nothing new to judge). + bool miniHysteresisPending = false; // Per-page mini kern matrix (built by buildMiniKernMatrix on each full // prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints @@ -193,6 +229,9 @@ class SdCardFont { uint8_t miniKernLeftClassCount = 0; uint8_t miniKernRightClassCount = 0; int8_t* miniKernMatrix = nullptr; + uint16_t miniKernLeftCapacity = 0; + uint16_t miniKernRightCapacity = 0; + uint32_t miniKernMatrixCapacity = 0; // The EpdFont whose data pointer we manage EpdFont epdFont{&stubData}; @@ -239,7 +278,12 @@ class SdCardFont { static constexpr uint32_t ADVANCE_CACHE_LIMIT = 256; AdvanceEntry* advanceTable_[MAX_STYLES] = {}; uint32_t advanceTableSize_[MAX_STYLES] = {}; + uint32_t advanceTableCapacity_[MAX_STYLES] = {}; bool advanceTableLookup(uint8_t styleIdx, uint32_t codepoint, uint16_t* outAdvance) const; + // Grow geometrically so normal layout batches reuse the persistent table + // instead of repeatedly replacing it. New storage is allocated before the + // old table is released, preserving the working cache on OOM. + bool ensureAdvanceTableCapacity(uint8_t styleIdx, uint32_t needed); // Merge sortedNew (sorted by codepoint, no overlap with existing) into the // advance table for styleIdx, preserving sort order; cap-truncates the tail. void mergeIntoAdvanceTable(uint8_t styleIdx, const AdvanceEntry* sortedNew, uint32_t newCount); @@ -251,19 +295,22 @@ class SdCardFont { // Per-style helpers void freeStyleMiniData(PerStyle& s); + void resetStyleMiniData(PerStyle& s); void freeStyleAll(PerStyle& s); void freeStyleKernLigatureData(PerStyle& s); void freeStyleMiniKern(PerStyle& s); - bool loadStyleKernLigatureData(PerStyle& s); + bool loadStyleKernLigatureData(PerStyle& s, bool includeKerning); bool buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, uint32_t cpCount); - void applyKernLigaturePointers(PerStyle& s, EpdFontData& data) const; + void applyKernLigaturePointers(PerStyle& s, EpdFontData& data, bool includeKerning) const; void applyGlyphMissCallback(uint8_t styleIdx); int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const; int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask); int failPrewarm(int missed); template - int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask); - int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly); + int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask, + const char* extraText = nullptr); + int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly, + bool includeKerning); // Global helpers void freeAll(); @@ -272,4 +319,8 @@ class SdCardFont { // Static callback for EpdFontData::glyphMissHandler (per-style via OverflowContext) static const EpdGlyph* onGlyphMiss(void* ctx, uint32_t codepoint); + + // Static callback for EpdFontData::coverageHandler: answers hasCodepoint() + // from the RAM-resident full interval table, without SD I/O. + static bool onCoverageQuery(void* ctx, uint32_t codepoint); }; diff --git a/lib/EpdFont/SdCardFontManager.cpp b/lib/EpdFont/SdCardFontManager.cpp index 73060179d4..8ab746bf1c 100644 --- a/lib/EpdFont/SdCardFontManager.cpp +++ b/lib/EpdFont/SdCardFontManager.cpp @@ -28,54 +28,101 @@ int SdCardFontManager::computeFontId(uint32_t contentHash, const char* familyNam return id != 0 ? id : 1; // 0 is reserved as "not found" sentinel } -bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPointSize, - uint8_t sizeStep) { - // Unload any previously loaded family first - if (!loadedFamilyName_.empty()) { - unloadAll(renderer); - } - - const SdCardFontFileInfo* selected = family.selectFile(targetPointSize, sizeStep); - if (!selected) { - LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str()); - return false; - } +int SdCardFontManager::loadFile(const SdCardFontFileInfo& file, const char* familyName, GfxRenderer& renderer) { + return loadFilePath(file.path.c_str(), familyName, file.pointSize, renderer); +} +int SdCardFontManager::loadFilePath(const char* path, const char* familyName, uint8_t pointSize, + GfxRenderer& renderer) { auto* font = new (std::nothrow) SdCardFont(); if (!font) { - LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str()); - return false; + LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", path); + return 0; } - if (!font->load(selected->path.c_str())) { - LOG_ERR("SDMGR", "Failed to load %s", selected->path.c_str()); + if (!font->load(path)) { + LOG_ERR("SDMGR", "Failed to load %s", path); delete font; - return false; + return 0; } - int fontId = computeFontId(font->contentHash(), family.name.c_str(), selected->pointSize); + int fontId = computeFontId(font->contentHash(), familyName, pointSize); // Guard against collision with built-in font IDs (astronomically unlikely // with FNV-1a hashes, but provides a safety net) if (renderer.getFontMap().count(fontId) != 0) { - LOG_ERR("SDMGR", "Font ID %d collides with existing font, skipping %s", fontId, selected->path.c_str()); + LOG_ERR("SDMGR", "Font ID %d collides with existing font, skipping %s", fontId, path); delete font; - return false; + return 0; } renderer.registerSdCardFont(fontId, font); - loaded_.push_back({font, fontId, selected->pointSize}); + loaded_.push_back({font, fontId, pointSize}); - LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (target=%u step=%u)", selected->path.c_str(), selected->pointSize, - fontId, font->styleCount(), targetPointSize, sizeStep); + LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u", path, pointSize, fontId, font->styleCount()); EpdFontFamily fontFamily(font->getEpdFont(0), font->getEpdFont(1), font->getEpdFont(2), font->getEpdFont(3)); renderer.insertFont(fontId, fontFamily); + return fontId; +} + +bool SdCardFontManager::loadFamilyClosest(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, + uint8_t targetPointSize) { + if (!loadedFamilyName_.empty()) { + unloadAll(renderer); + } + + const SdCardFontFileInfo* selected = family.findClosestFile(targetPointSize); + if (!selected) { + LOG_ERR("SDMGR", "Family %s has no files near %u pt", family.name.c_str(), targetPointSize); + return false; + } + + if (loadFile(*selected, family.name.c_str(), renderer) == 0) { + return false; + } loadedFamilyName_ = family.name; loadedPointSize_ = selected->pointSize; return true; } +bool SdCardFontManager::loadFamilyFile(const char* path, const char* familyName, uint8_t pointSize, + GfxRenderer& renderer) { + if (!loadedFamilyName_.empty()) { + unloadAll(renderer); + } + if (loadFilePath(path, familyName, pointSize, renderer) == 0) { + return false; + } + loadedFamilyName_ = familyName; + loadedPointSize_ = pointSize; + return true; +} + +int SdCardFontManager::loadFamilyExtraSize(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, + uint8_t pointSize) { + const SdCardFontFileInfo* file = family.findFile(pointSize); + if (!file) return 0; // family has no .cpfont at this exact size + + // Reuse an already-loaded font of the same size (e.g. when a reader size + // happens to match a UI size) instead of double-loading the file. + for (const auto& lf : loaded_) { + if (lf.size == pointSize) return lf.fontId; + } + + return loadFile(*file, family.name.c_str(), renderer); +} + +int SdCardFontManager::loadFamilyExtraFile(const char* path, const char* familyName, uint8_t pointSize, + GfxRenderer& renderer) { + for (const auto& lf : loaded_) { + if (lf.size == pointSize) return lf.fontId; + } + return loadFilePath(path, familyName, pointSize, renderer); +} + void SdCardFontManager::unloadAll(GfxRenderer& renderer) { + // Drop UI CJK fallbacks before the SD fonts they point at are freed. + renderer.clearFallbackFonts(); renderer.clearSdCardFonts(); for (auto& lf : loaded_) { renderer.removeFont(lf.fontId); diff --git a/lib/EpdFont/SdCardFontManager.h b/lib/EpdFont/SdCardFontManager.h index a9f544aaa7..1c8acadcbd 100644 --- a/lib/EpdFont/SdCardFontManager.h +++ b/lib/EpdFont/SdCardFontManager.h @@ -7,6 +7,7 @@ class GfxRenderer; class SdCardFont; struct SdCardFontFamilyInfo; +struct SdCardFontFileInfo; class SdCardFontManager { public: @@ -15,12 +16,24 @@ class SdCardFontManager { SdCardFontManager(const SdCardFontManager&) = delete; SdCardFontManager& operator=(const SdCardFontManager&) = delete; - // Load the selected font file. Four-size families map the reader size step - // onto the sorted file list; other counts fall back to closest point size. - // Only one .cpfont file is loaded; other sizes remain on disk. This keeps - // resident interval + kern/ligature tables to one size's worth of memory. - // Returns true on success. - bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPointSize, uint8_t sizeStep); + // Load the file physically closest to targetPointSize. Only one .cpfont + // file is loaded; other sizes remain on disk. This keeps resident interval + // + kern/ligature tables to one size's worth of memory. + bool loadFamilyClosest(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPointSize); + + // Load a known file path without constructing a registry family. Used by + // dictionary lookup to avoid allocating a whole catalog for one family. + bool loadFamilyFile(const char* path, const char* familyName, uint8_t pointSize, GfxRenderer& renderer); + + // Additively load the .cpfont of `family` at the exact physical `pointSize` + // (used for size-matched CJK UI fallback alongside the reader-size font). + // Does not unload anything. If a font of that size is already loaded its id + // is reused. Returns the font id, or 0 if the family has no file at that size + // or loading failed. + int loadFamilyExtraSize(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t pointSize); + + // Add an exact extra-size file found by the fixed-buffer dictionary path. + int loadFamilyExtraFile(const char* path, const char* familyName, uint8_t pointSize, GfxRenderer& renderer); // Unload everything, unregister from renderer. void unloadAll(GfxRenderer& renderer); @@ -44,6 +57,11 @@ class SdCardFontManager { }; static int computeFontId(uint32_t contentHash, const char* familyName, uint8_t pointSize); + // Load+register a single .cpfont file and append it to loaded_. + // Returns the font id, or 0 on failure (allocation, read, or id collision). + int loadFile(const SdCardFontFileInfo& file, const char* familyName, GfxRenderer& renderer); + int loadFilePath(const char* path, const char* familyName, uint8_t pointSize, GfxRenderer& renderer); + std::string loadedFamilyName_; uint8_t loadedPointSize_ = 0; std::vector loaded_; diff --git a/lib/EpdFont/SdCardFontRegistry.cpp b/lib/EpdFont/SdCardFontRegistry.cpp index 8039d8a1e0..aec3008cae 100644 --- a/lib/EpdFont/SdCardFontRegistry.cpp +++ b/lib/EpdFont/SdCardFontRegistry.cpp @@ -29,16 +29,6 @@ const SdCardFontFileInfo* SdCardFontFamilyInfo::findClosestFile(uint8_t targetSi return best; } -const SdCardFontFileInfo* SdCardFontFamilyInfo::selectFile(uint8_t targetSize, uint8_t sizeStep, uint8_t style) const { - const std::vector sizes = availableSizes(); - if (!sizes.empty()) { - if (sizeStep >= sizes.size()) sizeStep = static_cast(sizes.size() - 1); - const SdCardFontFileInfo* selected = findFile(sizes[sizeStep], style); - if (selected) return selected; - } - return findClosestFile(targetSize, style); -} - bool SdCardFontFamilyInfo::hasSize(uint8_t size) const { for (const auto& f : files) { if (f.pointSize == size) return true; @@ -100,9 +90,12 @@ bool SdCardFontRegistry::parseFilename(const char* filename, uint8_t& size, uint return true; } -void SdCardFontRegistry::scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family) { +bool SdCardFontRegistry::scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family) { HalFile dir = Storage.open(dirPath); - if (!dir || !dir.isDirectory()) return; + if (!dir || !dir.isDirectory()) { + if (dir.allocationFailed()) LOG_ERR("SDREG", "Out of memory opening font directory: %s", dirPath); + return !dir.allocationFailed(); + } char nameBuffer[128]; while (true) { @@ -144,20 +137,30 @@ void SdCardFontRegistry::scanDirectory(const char* dirPath, SdCardFontFamilyInfo info.style = style; family.files.push_back(std::move(info)); } + + if (dir.allocationFailed()) { + LOG_ERR("SDREG", "Out of memory scanning font directory: %s", dirPath); + return false; + } + return true; } // Scan a single root (e.g. "/.fonts") and append its families to `out`. // Skips families whose names already exist in `out` (de-duplicates between // the hidden and visible roots — first scan wins). -void SdCardFontRegistry::scanRoot(const char* rootPath, std::vector& out) { +bool SdCardFontRegistry::scanRoot(const char* rootPath, std::vector& out) { HalFile root = Storage.open(rootPath); if (!root) { + if (root.allocationFailed()) { + LOG_ERR("SDREG", "Out of memory opening font root: %s", rootPath); + return false; + } LOG_DBG("SDREG", "Fonts directory not found: %s", rootPath); - return; + return true; } if (!root.isDirectory()) { LOG_ERR("SDREG", "Fonts path is not a directory: %s", rootPath); - return; + return true; } char nameBuffer[128]; @@ -184,27 +187,38 @@ void SdCardFontRegistry::scanRoot(const char* rootPath, std::vector(out.back().files.size()), rootPath); } } else { entry.close(); } } + + if (root.allocationFailed()) { + LOG_ERR("SDREG", "Out of memory scanning font root: %s", rootPath); + return false; + } + return true; } bool SdCardFontRegistry::discover() { + discoveryFailed_ = false; families_.clear(); - families_.reserve(MAX_SD_FAMILIES); + // Most cards have fewer than 16 families. Grow only for unusually large + // catalogs instead of permanently reserving 128 entries at boot. + families_.reserve(16); // Hidden root is scanned first so it wins on name collisions, matching the // sleep-folder pattern (/.sleep preferred over /sleep). - scanRoot(FONTS_DIR_HIDDEN, families_); - scanRoot(FONTS_DIR_VISIBLE, families_); + if (!scanRoot(FONTS_DIR_HIDDEN, families_) || !scanRoot(FONTS_DIR_VISIBLE, families_)) { + discoveryFailed_ = true; + clear(); + LOG_ERR("SDREG", "Font discovery stopped after an out-of-memory directory scan"); + return false; + } // Sort families alphabetically std::sort(families_.begin(), families_.end(), diff --git a/lib/EpdFont/SdCardFontRegistry.h b/lib/EpdFont/SdCardFontRegistry.h index f0009808bc..a1db1a9f52 100644 --- a/lib/EpdFont/SdCardFontRegistry.h +++ b/lib/EpdFont/SdCardFontRegistry.h @@ -19,7 +19,6 @@ struct SdCardFontFamilyInfo { const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const; const SdCardFontFileInfo* findClosestFile(uint8_t targetSize, uint8_t style = 0) const; - const SdCardFontFileInfo* selectFile(uint8_t targetSize, uint8_t sizeStep, uint8_t style = 0) const; bool hasSize(uint8_t size) const; std::vector availableSizes() const; }; @@ -43,9 +42,16 @@ class SdCardFontRegistry { static const char* defaultWriteRoot(); // Scan SD card, populate families_. Returns true if any families found. + // Use lastDiscoveryFailed() to distinguish an empty card from an incomplete + // scan caused by a recoverable directory-entry allocation failure. bool discover(); + bool lastDiscoveryFailed() const { return discoveryFailed_; } void clear(); + // Parse a v4 .cpfont filename without allocating. Reused by the dictionary + // font path, which scans one selected family without retaining a catalog. + static bool parseFilename(const char* filename, uint8_t& size, uint8_t& style); + const std::vector& getFamilies() const { return families_; } const SdCardFontFamilyInfo* findFamily(const std::string& name) const; int getFamilyIndex(const std::string& name) const; @@ -53,9 +59,9 @@ class SdCardFontRegistry { private: std::vector families_; // sorted alphabetically + bool discoveryFailed_ = false; - static bool parseFilename(const char* filename, uint8_t& size, uint8_t& style); - static void scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family); + static bool scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family); // Scan one root (e.g. "/.fonts"), append families to `out`, dedup by name. - static void scanRoot(const char* rootPath, std::vector& out); + static bool scanRoot(const char* rootPath, std::vector& out); }; diff --git a/lib/EpdFont/builtinFonts/all.h b/lib/EpdFont/builtinFonts/all.h index f0c2cbcd8e..aa680f8232 100644 --- a/lib/EpdFont/builtinFonts/all.h +++ b/lib/EpdFont/builtinFonts/all.h @@ -1,123 +1,47 @@ #pragma once -// Reading fonts have generated variants with identical variable names: -// default: emoji/symbol fallback + PHM CJK fallback -// OMIT_EMOJI_FONTS: primary fonts only, no emoji and no PHM CJK -// -// Generate the variants with lib/EpdFont/scripts/convert-builtin-fonts.sh. -// -// Per-size guards: -// OMIT_TEENSY_FONT - excludes 8px (Teensy) reading fonts; used by env:xlarge -// OMIT_ITTY_BITTY_FONT - excludes 9px (Itty Bitty) reading fonts; used by env:xlarge -// OMIT_TINY_FONT - excludes 10px (Tiny) reading fonts; used by env:xlarge -// OMIT_SMALL_FONT - excludes 12px (Small) reading fonts -// OMIT_MEDIUM_FONT - excludes 14px (Medium) reading fonts -// OMIT_LARGE_FONT - excludes 16px (Large) reading fonts -// OMIT_XLARGE_FONT - excludes 18px (Extra Large) reading fonts; used by env:tiny -// OMIT_HUGE_FONT - excludes 20px (Huge) reading fonts; used by all firmware envs except env:xlarge +// Built-in reading fonts are fixed at 10, 12, 14, and 16 pt. The default +// variant includes emoji/symbol and PHM CJK fallbacks; noemoji includes only +// the primary fonts. #ifdef OMIT_EMOJI_FONTS #define BUILTIN_READING_FONT_HEADER(name) #else #define BUILTIN_READING_FONT_HEADER(name) #endif -#ifndef OMIT_TEENSY_FONT -#include BUILTIN_READING_FONT_HEADER(bitter_8_bold) -#include BUILTIN_READING_FONT_HEADER(bitter_8_bolditalic) -#include BUILTIN_READING_FONT_HEADER(bitter_8_italic) -#include BUILTIN_READING_FONT_HEADER(bitter_8_regular) -#endif -#ifndef OMIT_ITTY_BITTY_FONT -#include BUILTIN_READING_FONT_HEADER(bitter_9_bold) -#include BUILTIN_READING_FONT_HEADER(bitter_9_bolditalic) -#include BUILTIN_READING_FONT_HEADER(bitter_9_italic) -#include BUILTIN_READING_FONT_HEADER(bitter_9_regular) -#endif -#ifndef OMIT_TINY_FONT #include BUILTIN_READING_FONT_HEADER(bitter_10_bold) #include BUILTIN_READING_FONT_HEADER(bitter_10_bolditalic) #include BUILTIN_READING_FONT_HEADER(bitter_10_italic) #include BUILTIN_READING_FONT_HEADER(bitter_10_regular) -#endif -#ifndef OMIT_SMALL_FONT #include BUILTIN_READING_FONT_HEADER(bitter_12_bold) #include BUILTIN_READING_FONT_HEADER(bitter_12_bolditalic) #include BUILTIN_READING_FONT_HEADER(bitter_12_italic) #include BUILTIN_READING_FONT_HEADER(bitter_12_regular) -#endif -#ifndef OMIT_MEDIUM_FONT #include BUILTIN_READING_FONT_HEADER(bitter_14_bold) #include BUILTIN_READING_FONT_HEADER(bitter_14_bolditalic) #include BUILTIN_READING_FONT_HEADER(bitter_14_italic) #include BUILTIN_READING_FONT_HEADER(bitter_14_regular) -#endif -#ifndef OMIT_LARGE_FONT #include BUILTIN_READING_FONT_HEADER(bitter_16_bold) #include BUILTIN_READING_FONT_HEADER(bitter_16_bolditalic) #include BUILTIN_READING_FONT_HEADER(bitter_16_italic) #include BUILTIN_READING_FONT_HEADER(bitter_16_regular) -#endif -#ifndef OMIT_XLARGE_FONT -#include BUILTIN_READING_FONT_HEADER(bitter_18_bold) -#include BUILTIN_READING_FONT_HEADER(bitter_18_bolditalic) -#include BUILTIN_READING_FONT_HEADER(bitter_18_italic) -#include BUILTIN_READING_FONT_HEADER(bitter_18_regular) -#endif -#ifndef OMIT_HUGE_FONT -#include BUILTIN_READING_FONT_HEADER(bitter_20_bold) -#include BUILTIN_READING_FONT_HEADER(bitter_20_bolditalic) -#include BUILTIN_READING_FONT_HEADER(bitter_20_italic) -#include BUILTIN_READING_FONT_HEADER(bitter_20_regular) -#endif -#ifndef OMIT_TEENSY_FONT -#include BUILTIN_READING_FONT_HEADER(lexenddeca_8_bold) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_8_bolditalic) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_8_italic) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_8_regular) -#endif -#ifndef OMIT_ITTY_BITTY_FONT -#include BUILTIN_READING_FONT_HEADER(lexenddeca_9_bold) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_9_bolditalic) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_9_italic) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_9_regular) -#endif -#ifndef OMIT_TINY_FONT #include BUILTIN_READING_FONT_HEADER(lexenddeca_10_bold) #include BUILTIN_READING_FONT_HEADER(lexenddeca_10_bolditalic) #include BUILTIN_READING_FONT_HEADER(lexenddeca_10_italic) #include BUILTIN_READING_FONT_HEADER(lexenddeca_10_regular) -#endif -#ifndef OMIT_SMALL_FONT #include BUILTIN_READING_FONT_HEADER(lexenddeca_12_bold) #include BUILTIN_READING_FONT_HEADER(lexenddeca_12_bolditalic) #include BUILTIN_READING_FONT_HEADER(lexenddeca_12_italic) #include BUILTIN_READING_FONT_HEADER(lexenddeca_12_regular) -#endif -#ifndef OMIT_MEDIUM_FONT #include BUILTIN_READING_FONT_HEADER(lexenddeca_14_bold) #include BUILTIN_READING_FONT_HEADER(lexenddeca_14_bolditalic) #include BUILTIN_READING_FONT_HEADER(lexenddeca_14_italic) #include BUILTIN_READING_FONT_HEADER(lexenddeca_14_regular) -#endif -#ifndef OMIT_LARGE_FONT #include BUILTIN_READING_FONT_HEADER(lexenddeca_16_bold) #include BUILTIN_READING_FONT_HEADER(lexenddeca_16_bolditalic) #include BUILTIN_READING_FONT_HEADER(lexenddeca_16_italic) #include BUILTIN_READING_FONT_HEADER(lexenddeca_16_regular) -#endif -#ifndef OMIT_XLARGE_FONT -#include BUILTIN_READING_FONT_HEADER(lexenddeca_18_bold) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_18_bolditalic) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_18_italic) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_18_regular) -#endif -#ifndef OMIT_HUGE_FONT -#include BUILTIN_READING_FONT_HEADER(lexenddeca_20_bold) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_20_bolditalic) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_20_italic) -#include BUILTIN_READING_FONT_HEADER(lexenddeca_20_regular) -#endif #undef BUILTIN_READING_FONT_HEADER diff --git a/lib/EpdFont/builtinFonts/inter_10_bold.h b/lib/EpdFont/builtinFonts/inter_10_bold.h index 3ba0086fab..094765d927 100644 --- a/lib/EpdFont/builtinFonts/inter_10_bold.h +++ b/lib/EpdFont/builtinFonts/inter_10_bold.h @@ -3,12 +3,12 @@ * name: inter_10_bold * size: 10 * mode: 1-bit - * Command used: fontconvert.py inter_10_bold 10 ../builtinFonts/source/Inter/Inter-Bold.ttf ../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-Bold.ttf --additional-intervals 0x05D0,0x05EA + * Command used: fontconvert.py inter_10_bold 10 ../builtinFonts/source/Inter/Inter-Bold.ttf ../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-Bold.ttf ../builtinFonts/source/NotoSansArabic/NotoSansArabic-Bold.ttf --additional-intervals 0x05D0,0x05EA --additional-intervals 0x060C,0x060C --additional-intervals 0x061B,0x061B --additional-intervals 0x061F,0x061F --additional-intervals 0x0621,0x0621 --additional-intervals 0x0640,0x0640 --additional-intervals 0x0660,0x0669 --additional-intervals 0x06BA,0x06BA --additional-intervals 0x06D4,0x06D4 --additional-intervals 0x06F0,0x06F9 --additional-intervals 0xFB56,0xFB59 --additional-intervals 0xFB66,0xFB69 --additional-intervals 0xFB7A,0xFB7D --additional-intervals 0xFB88,0xFB95 --additional-intervals 0xFB9E,0xFB9F --additional-intervals 0xFBA6,0xFBB1 --additional-intervals 0xFBFC,0xFBFF --additional-intervals 0xFE80,0xFEFC */ #pragma once #include "EpdFontData.h" -static const uint8_t inter_10_boldBitmaps[26580] = { +static const uint8_t inter_10_boldBitmaps[32477] = { 0x77, 0xBD, 0xEF, 0x7B, 0xDE, 0xF7, 0x9C, 0xE2, 0x7B, 0xDE, 0x20, 0x73, 0x3B, 0xDD, 0xCE, 0xE7, 0x73, 0xB9, 0xDC, 0x00, 0x0C, 0x20, 0x39, 0xC1, 0xE7, 0x07, 0x1C, 0x7F, 0xF9, 0xFF, 0xE7, 0xFF, 0x87, 0x38, 0x3C, 0xE0, 0xE3, 0x8F, 0xFF, 0xBF, 0xFC, 0xFF, 0xF0, 0xE7, 0x03, 0x9C, 0x1C, 0x70, @@ -712,965 +712,1333 @@ static const uint8_t inter_10_boldBitmaps[26580] = { 0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0xF3, 0x8E, 0x70, 0x7B, 0x9D, 0xC0, 0x1E, 0x1F, 0x9D, 0xEC, 0x70, 0x73, 0x7F, 0xFE, 0x00, 0x7D, 0xFF, 0xF8, 0x63, 0x7F, 0x3E, 0x1C, 0xEF, 0xF6, 0x77, 0x7B, 0xFD, 0xC4, 0x40, 0xFF, 0x76, 0x23, 0xEF, 0xF3, 0xFD, 0xE0, 0x33, 0x1E, 0xE7, 0x7B, 0xDC, 0xE6, 0x00, - 0x77, 0xBF, 0x8F, 0x83, 0x80, 0xF7, 0x3B, 0x8E, 0xE3, 0x70, 0x73, 0x9C, 0xE7, 0x30, 0x00, 0x73, - 0x9C, 0xE7, 0x30, 0x00, 0x77, 0xFE, 0xEC, 0xEF, 0xF6, 0x77, 0x76, 0x60, 0x43, 0x3C, 0x7F, 0x70, - 0x31, 0xEF, 0x3E, 0x7C, 0xE0, 0x77, 0xBF, 0x8F, 0x83, 0x80, 0x01, 0x81, 0xC0, 0xC0, 0xE0, 0x60, - 0x70, 0x30, 0x38, 0x18, 0x1C, 0x0C, 0x0E, 0x06, 0x07, 0x03, 0x00, 0x00, 0x30, 0x01, 0x80, 0x18, - 0x01, 0xC0, 0x0C, 0x00, 0xE0, 0x06, 0x00, 0x60, 0x07, 0x00, 0x30, 0x03, 0x80, 0x18, 0x01, 0x80, - 0x1C, 0x00, 0xC0, 0x0E, 0x00, 0x60, 0x00, 0x3B, 0x7F, 0x7F, 0x00, 0x73, 0xBD, 0xC0, 0x7F, 0x7F, - 0xBF, 0xDC, 0x6E, 0x30, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x03, 0x7D, 0xF7, 0xDF, 0x70, 0xFF, - 0xC6, 0x31, 0x80, 0x00, 0x3F, 0xBF, 0xDF, 0xED, 0xC0, 0x00, 0x1C, 0x1E, 0x0F, 0x0F, 0x6F, 0xF7, - 0xF9, 0x70, 0x78, 0x3C, 0x04, 0x00, 0x73, 0xFF, 0xBB, 0xCF, 0xFF, 0xF6, 0x30, 0x33, 0x8F, 0xFB, - 0xFF, 0x3F, 0xC3, 0x30, 0x03, 0xBF, 0xF7, 0x30, 0x80, 0x78, 0x3F, 0x87, 0xE1, 0xFF, 0xFB, 0xF1, - 0xC0, 0x00, 0x00, 0x7F, 0x76, 0x00, 0x3E, 0x7F, 0xE7, 0x1C, 0x3C, 0x1C, 0x08, 0x43, 0xE7, 0x9E, - 0xF8, 0x00, 0x04, 0xFF, 0xBE, 0x7C, 0x30, 0x40, 0xF1, 0xF9, 0xF7, 0xC8, 0x00, 0x80, 0x8F, 0x1C, - 0xFD, 0xC7, 0xFE, 0xFB, 0x6C, 0x36, 0x0E, 0x77, 0xEE, 0xEF, 0xF6, 0x21, 0x3E, 0xEF, 0x28, 0x22, - 0x3F, 0xB7, 0x7B, 0xB7, 0xF0, 0x67, 0xFF, 0xF3, 0x00, 0xE0, 0x07, 0x78, 0x0F, 0x7F, 0xFE, 0x1F, - 0xF8, 0x03, 0xC0, 0x60, 0x03, 0x3C, 0x07, 0x8F, 0xFF, 0x83, 0xFF, 0x80, 0x1C, 0x00, 0x7F, 0xFF, - 0xFF, 0xDF, 0xFE, 0x7F, 0xFF, 0xFF, 0xDF, 0xFE, 0x1F, 0x00, 0x0D, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, - 0xD8, 0x00, 0x7C, 0x00, 0x00, 0x07, 0xFC, 0x0F, 0xFF, 0x8F, 0x01, 0xE7, 0x00, 0x70, 0x00, 0x07, - 0x0F, 0xFF, 0xF3, 0xFF, 0xFF, 0x3F, 0xFF, 0xC0, 0x00, 0x70, 0x7C, 0xF9, 0xF3, 0xE7, 0xCF, 0x80, - 0x7C, 0xF9, 0xF7, 0xE7, 0xCF, 0x80, 0x1D, 0xFF, 0xFF, 0xE0, 0x7C, 0xFB, 0xBF, 0x77, 0xCF, 0x80, - 0x6C, 0xD9, 0xB3, 0x67, 0xCF, 0x80, 0x7D, 0xF6, 0xD9, 0x7D, 0xF0, 0x0C, 0x18, 0x33, 0xE7, 0xDD, - 0xBB, 0x3E, 0x7C, 0x60, 0xC1, 0x83, 0xE7, 0xCD, 0x9B, 0x36, 0x6C, 0xFF, 0x7F, 0xF7, 0x7B, 0xBD, - 0xDE, 0xEC, 0xFF, 0xCC, 0xCC, 0x66, 0xFF, 0x66, 0x77, 0x6C, 0xD9, 0xF1, 0xC3, 0x87, 0x00, 0x6C, - 0xF8, 0xE1, 0xC7, 0xCD, 0x80, 0x3C, 0x03, 0x80, 0x78, 0x07, 0x07, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, + 0x77, 0xBF, 0x8F, 0x83, 0x80, 0xF7, 0x3B, 0x8E, 0xE3, 0x70, 0x73, 0xBD, 0xEE, 0x70, 0x73, 0x9C, + 0xE7, 0x30, 0x00, 0x73, 0x9C, 0xE7, 0x30, 0x00, 0x77, 0xFE, 0xEC, 0xEF, 0xF6, 0x77, 0x76, 0x60, + 0x43, 0x3C, 0x7F, 0x70, 0x31, 0xEF, 0x3E, 0x7C, 0xE0, 0x77, 0xBF, 0x8F, 0x83, 0x80, 0x01, 0x81, + 0xC0, 0xC0, 0xE0, 0x60, 0x70, 0x30, 0x38, 0x18, 0x1C, 0x0C, 0x0E, 0x06, 0x07, 0x03, 0x00, 0x00, + 0x30, 0x01, 0x80, 0x18, 0x01, 0xC0, 0x0C, 0x00, 0xE0, 0x06, 0x00, 0x60, 0x07, 0x00, 0x30, 0x03, + 0x80, 0x18, 0x01, 0x80, 0x1C, 0x00, 0xC0, 0x0E, 0x00, 0x60, 0x00, 0x3B, 0x7F, 0x7F, 0x00, 0x73, + 0xBD, 0xC0, 0x7F, 0x7F, 0xBF, 0xDC, 0x6E, 0x30, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x03, 0x7D, + 0xF7, 0xDF, 0x70, 0xFF, 0xC6, 0x31, 0x80, 0x00, 0x3F, 0xBF, 0xDF, 0xED, 0xC0, 0x00, 0x1C, 0x1E, + 0x0F, 0x0F, 0x6F, 0xF7, 0xF9, 0x70, 0x78, 0x3C, 0x04, 0x00, 0x73, 0xFF, 0xBB, 0xCF, 0xFF, 0xF6, + 0x30, 0x33, 0x8F, 0xFB, 0xFF, 0x3F, 0xC3, 0x30, 0x03, 0xBF, 0xF7, 0x30, 0x80, 0x78, 0x3F, 0x87, + 0xE1, 0xFF, 0xFB, 0xF1, 0xC0, 0x00, 0x00, 0x7F, 0x76, 0x00, 0x3E, 0x7F, 0xE7, 0x1C, 0x3C, 0x1C, + 0x08, 0x43, 0xE7, 0x9E, 0xF8, 0x00, 0x04, 0xFF, 0xBE, 0x7C, 0x30, 0x40, 0xF1, 0xF9, 0xF7, 0xC8, + 0x00, 0x80, 0x8F, 0x1C, 0xFD, 0xC7, 0xFE, 0xFB, 0x6C, 0x36, 0x0E, 0x77, 0xEE, 0xEF, 0xF6, 0x21, + 0x3E, 0xEF, 0x28, 0x22, 0x3F, 0xB7, 0x7B, 0xB7, 0xF0, 0x67, 0xFF, 0xF3, 0x00, 0xE0, 0x07, 0x78, + 0x0F, 0x7F, 0xFE, 0x1F, 0xF8, 0x03, 0xC0, 0x60, 0x03, 0x3C, 0x07, 0x8F, 0xFF, 0x83, 0xFF, 0x80, + 0x1C, 0x00, 0x7F, 0xFF, 0xFF, 0xDF, 0xFE, 0x7F, 0xFF, 0xFF, 0xDF, 0xFE, 0x1F, 0x00, 0x0D, 0xFF, + 0xFF, 0xF7, 0xFF, 0xFF, 0xD8, 0x00, 0x7C, 0x00, 0x00, 0x07, 0xFC, 0x0F, 0xFF, 0x8F, 0x01, 0xE7, + 0x00, 0x70, 0x00, 0x07, 0x0F, 0xFF, 0xF3, 0xFF, 0xFF, 0x3F, 0xFF, 0xC0, 0x00, 0x70, 0x7C, 0xF9, + 0xF3, 0xE7, 0xCF, 0x80, 0x7C, 0xF9, 0xF7, 0xE7, 0xCF, 0x80, 0x1D, 0xFF, 0xFF, 0xE0, 0x7C, 0xFB, + 0xBF, 0x77, 0xCF, 0x80, 0x6C, 0xD9, 0xB3, 0x67, 0xCF, 0x80, 0x7D, 0xF6, 0xD9, 0x7D, 0xF0, 0x0C, + 0x18, 0x33, 0xE7, 0xDD, 0xBB, 0x3E, 0x7C, 0x60, 0xC1, 0x83, 0xE7, 0xCD, 0x9B, 0x36, 0x6C, 0xFF, + 0x7F, 0xF7, 0x7B, 0xBD, 0xDE, 0xEC, 0xFF, 0xCC, 0xCC, 0x66, 0xFF, 0x66, 0x77, 0x6C, 0xD9, 0xF1, + 0xC3, 0x87, 0x00, 0x6C, 0xF8, 0xE1, 0xC7, 0xCD, 0x80, 0x3C, 0x03, 0x80, 0x78, 0x07, 0x07, 0xFD, + 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x7F, 0xEF, 0xFD, 0xFF, 0xBC, 0x07, 0x80, 0xF0, + 0x1F, 0xFF, 0xFF, 0xFF, 0xF0, 0x3B, 0x8F, 0x78, 0xEE, 0x08, 0x87, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x7F, 0xEF, 0xFD, 0xFF, 0xBC, 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF, 0xFF, - 0xF0, 0x3B, 0x8F, 0x78, 0xEE, 0x08, 0x87, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, - 0x7F, 0xEF, 0xFD, 0xFF, 0xBC, 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF, 0xFF, 0xF0, 0x7F, 0xF8, 0x7F, - 0xFC, 0x3F, 0xFE, 0x1F, 0xFF, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0xE0, 0x0F, 0xFC, 0x07, 0xFF, - 0x03, 0xFF, 0xC1, 0xE1, 0xE0, 0xF0, 0xF0, 0x78, 0x78, 0x3C, 0x3C, 0x1E, 0xFE, 0x0F, 0x7E, 0x00, - 0x3E, 0x00, 0x18, 0x00, 0x07, 0x80, 0xE0, 0x38, 0x07, 0x07, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, - 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x00, - 0x03, 0xC0, 0x1F, 0xE0, 0x7F, 0xE1, 0xFF, 0xE7, 0xC3, 0xEF, 0x03, 0xDE, 0x00, 0x3F, 0xE0, 0x7F, - 0xC0, 0xFF, 0x81, 0xE0, 0x73, 0xC0, 0xF7, 0xC3, 0xC7, 0xFF, 0x87, 0xFE, 0x07, 0xF8, 0x01, 0x80, - 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0xFF, 0xF7, 0x87, 0xBC, 0x1D, 0xF8, 0x07, 0xF8, 0x1F, 0xF0, 0x3F, - 0xC0, 0x3E, 0xE0, 0xF7, 0x87, 0xBF, 0xFC, 0xFF, 0xC3, 0xFC, 0x03, 0x00, 0x6F, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xEF, 0xEF, 0x42, 0x18, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, - 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x00, 0xC0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, - 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3F, 0xC7, 0xF8, 0xFF, 0x1E, 0xFF, 0xDF, 0xF1, 0xFC, 0x06, 0x00, - 0x0F, 0xFC, 0x00, 0x1F, 0xF8, 0x00, 0x7F, 0xF0, 0x00, 0xFF, 0xE0, 0x01, 0xE3, 0xC0, 0x03, 0xC7, - 0x80, 0x07, 0x8F, 0xFC, 0x0F, 0x1F, 0xFC, 0x1E, 0x3F, 0xFC, 0x3C, 0x78, 0x7C, 0x70, 0xF0, 0x78, - 0xE1, 0xE0, 0xF3, 0xC3, 0xC3, 0xFF, 0x87, 0xFF, 0xBE, 0x0F, 0xFE, 0x78, 0x1F, 0xF8, 0x60, 0x38, - 0x03, 0xC0, 0xE0, 0x0F, 0x03, 0x80, 0x3C, 0x0E, 0x00, 0xF0, 0x38, 0x03, 0xC0, 0xE0, 0x0F, 0xFF, - 0xFE, 0x3F, 0xFF, 0xFE, 0xFF, 0xFF, 0xFB, 0xC0, 0xE1, 0xFF, 0x03, 0x83, 0xFC, 0x0E, 0x0F, 0xF0, - 0x38, 0x3F, 0xC0, 0xFF, 0xEF, 0x03, 0xFF, 0xBC, 0x0F, 0xF8, 0x7F, 0xF8, 0x3F, 0xFF, 0x0F, 0xFF, - 0xC3, 0xFF, 0xF0, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x7B, 0xC0, 0x1F, 0xFC, 0x07, 0xFF, 0x81, 0xFF, - 0xE0, 0x78, 0x7C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x03, 0x80, - 0x1E, 0x00, 0x70, 0x01, 0x80, 0x60, 0x3B, 0xC1, 0xEF, 0x0F, 0x3C, 0x7C, 0xF3, 0xE3, 0xCF, 0x0F, - 0x78, 0x3F, 0xE0, 0xFF, 0x03, 0xFE, 0x0F, 0x7C, 0x3C, 0xF8, 0xF1, 0xE3, 0xC3, 0xCF, 0x0F, 0xBC, - 0x1E, 0x1E, 0x00, 0x38, 0x00, 0x70, 0x01, 0xC0, 0x60, 0x3B, 0xC1, 0xFF, 0x07, 0xFC, 0x3F, 0xF1, - 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xEF, 0xF7, 0xBF, 0xFC, 0xFF, 0xF3, 0xFF, 0x8F, 0xFC, 0x3F, 0xF0, - 0xFF, 0x83, 0xFC, 0x0F, 0x1C, 0xE0, 0x3F, 0x80, 0xFC, 0x00, 0xE0, 0x70, 0x1F, 0xE0, 0xF7, 0x87, - 0xDF, 0x1E, 0x3C, 0x78, 0xF3, 0xC1, 0xEF, 0x07, 0xF8, 0x0F, 0xE0, 0x3F, 0x00, 0x7C, 0x01, 0xF0, - 0x07, 0x80, 0xFE, 0x03, 0xF0, 0x0F, 0x80, 0x60, 0x37, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, - 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, - 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, 0xFF, 0xE7, 0xFF, - 0xCF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x7F, 0xCF, 0xFE, 0xFF, 0xEF, 0xFC, 0xF0, 0x0F, 0x00, 0xF0, - 0x0F, 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, 0xEF, 0xFC, 0x7E, 0x0F, - 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF1, 0xEF, 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, - 0xF0, 0xFF, 0xFF, 0xFF, 0xEF, 0xFC, 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, - 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0x07, 0xFE, 0x03, 0xFF, - 0xC0, 0xFF, 0xF0, 0x3F, 0xFC, 0x0F, 0x0F, 0x03, 0xC3, 0xC0, 0xF0, 0xF0, 0x3C, 0x3C, 0x0F, 0x0F, - 0x03, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x3E, 0x0F, 0x3F, 0xFF, 0xEF, 0xFF, 0xFB, 0xFF, 0xFE, - 0xF0, 0x03, 0xBC, 0x00, 0xEF, 0x00, 0x39, 0xC0, 0x0E, 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, - 0xE0, 0x3C, 0x07, 0xFE, 0xFF, 0xDF, 0xFB, 0xC0, 0x78, 0x0F, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x70, - 0x30, 0x39, 0xE1, 0xE1, 0xE7, 0xC7, 0x8F, 0x8F, 0x1E, 0x3C, 0x3E, 0x79, 0xE0, 0x79, 0xE7, 0x80, - 0xF7, 0xBC, 0x03, 0xFF, 0xF0, 0x0F, 0xFF, 0xC0, 0x3F, 0xFF, 0x01, 0xE7, 0x9E, 0x07, 0x9E, 0x78, - 0x3C, 0x78, 0xF1, 0xF1, 0xE3, 0xC7, 0x87, 0x87, 0xBE, 0x1E, 0x1F, 0x07, 0x00, 0xFF, 0x0F, 0xFC, - 0xFB, 0xE7, 0x8F, 0x18, 0x78, 0x03, 0xC0, 0xFC, 0x07, 0xE0, 0x3F, 0x80, 0x3E, 0xE0, 0xF7, 0x87, - 0xBF, 0xF8, 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x60, 0x3B, 0xC1, 0xFF, 0x07, 0xFC, 0x3F, 0xF1, 0xFF, - 0xC7, 0xFF, 0x3F, 0xFD, 0xEF, 0xF7, 0xBF, 0xFC, 0xFF, 0xF3, 0xFF, 0x8F, 0xFC, 0x3F, 0xF0, 0xFF, - 0x83, 0xFC, 0x0F, 0x18, 0xC0, 0x7F, 0x00, 0xFC, 0x01, 0xC0, 0x60, 0x3B, 0xC1, 0xFF, 0x07, 0xFC, - 0x3F, 0xF1, 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xEF, 0xF7, 0xBF, 0xFC, 0xFF, 0xF3, 0xFF, 0x8F, 0xFC, - 0x3F, 0xF0, 0xFF, 0x83, 0xFC, 0x0F, 0x60, 0x3B, 0xC1, 0xEF, 0x0F, 0x3C, 0x7C, 0xF3, 0xE3, 0xCF, - 0x0F, 0x78, 0x3F, 0xE0, 0xFF, 0x03, 0xFE, 0x0F, 0x7C, 0x3C, 0xF8, 0xF1, 0xE3, 0xC3, 0xCF, 0x0F, - 0xBC, 0x1E, 0x0F, 0xFC, 0x1F, 0xFC, 0x7F, 0xF8, 0xFF, 0xF1, 0xE1, 0xE3, 0xC3, 0xC7, 0x87, 0x8F, - 0x0F, 0x1E, 0x1E, 0x3C, 0x3C, 0x78, 0x78, 0xE0, 0xF3, 0xC1, 0xFF, 0x83, 0xFE, 0x07, 0xF8, 0x0F, - 0x78, 0x0F, 0xFE, 0x07, 0xFF, 0x07, 0xFF, 0x83, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0xF8, 0xFF, 0xFE, - 0xFF, 0xFF, 0x7F, 0xFB, 0xBF, 0xFD, 0xFD, 0xFE, 0xFE, 0xFF, 0x3F, 0x7F, 0x9F, 0x3F, 0xCF, 0x9F, - 0xE3, 0x8F, 0x60, 0x3B, 0xC0, 0xEF, 0x03, 0xBC, 0x0E, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, 0xBF, 0xFE, - 0xFF, 0xFB, 0xFF, 0xEF, 0x03, 0xBC, 0x0E, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, 0xBC, 0x0E, 0x03, 0xC0, - 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, - 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, - 0x7F, 0xFB, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, 0xBC, 0x0E, 0xF0, 0x3B, - 0xC0, 0xEF, 0x03, 0xBC, 0x0E, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, 0xBC, 0x0E, 0x7E, 0x0F, 0xFC, 0xFF, - 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xFF, 0xEF, 0xFE, 0xFF, 0xCF, 0x00, 0xF0, 0x0F, - 0x00, 0xF0, 0x0F, 0x00, 0x03, 0xC0, 0x1F, 0xE0, 0x7F, 0xE1, 0xFF, 0xE7, 0xC3, 0xEF, 0x03, 0xDE, - 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xE0, 0x73, 0xC0, 0xF7, 0xC3, 0xC7, 0xFF, 0x87, 0xFE, - 0x07, 0xF8, 0x01, 0x80, 0x7F, 0xFB, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0x07, 0x80, 0x1E, 0x00, 0x78, - 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, - 0x70, 0x1F, 0xE0, 0xF7, 0x87, 0xDF, 0x1E, 0x3C, 0x78, 0xF3, 0xC1, 0xEF, 0x07, 0xF8, 0x0F, 0xE0, - 0x3F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0x80, 0xFE, 0x03, 0xF0, 0x0F, 0x80, 0x01, 0xE0, 0x00, 0xF0, - 0x03, 0xFF, 0x07, 0xFF, 0xC3, 0xFF, 0xF3, 0xCF, 0x79, 0xE7, 0x9E, 0xE3, 0xCF, 0x71, 0xE7, 0xB8, - 0xF3, 0xDE, 0x79, 0xEF, 0xBD, 0xE3, 0xFF, 0xF0, 0xFF, 0xF0, 0x3F, 0xE0, 0x03, 0xC0, 0x01, 0xE0, - 0x00, 0x70, 0x1C, 0xF0, 0x78, 0xF1, 0xF1, 0xF3, 0xC1, 0xEF, 0x01, 0xFE, 0x03, 0xF8, 0x03, 0xE0, - 0x07, 0xC0, 0x0F, 0xC0, 0x3F, 0x80, 0xF7, 0x83, 0xEF, 0x87, 0x8F, 0x1E, 0x0F, 0x7C, 0x1F, 0x60, - 0x31, 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xCF, 0x07, 0x9E, 0x0F, 0x3C, 0x1E, 0x78, 0x3C, 0xF0, 0x79, - 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xCF, 0x07, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x1E, 0x00, - 0x3C, 0x00, 0x78, 0x00, 0xE0, 0x60, 0x77, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, - 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, 0xC7, 0xDE, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x60, - 0xC1, 0xDE, 0x3C, 0x3F, 0xC7, 0x87, 0xF8, 0xF0, 0xFF, 0x1E, 0x1F, 0xE3, 0xC3, 0xFC, 0x78, 0x7F, - 0x8F, 0x0F, 0xF1, 0xE1, 0xFE, 0x3C, 0x3F, 0xC7, 0x87, 0xF8, 0xF0, 0xFF, 0x1E, 0x1F, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x60, 0xC1, 0xCF, 0x1E, 0x1E, 0xF1, 0xE1, 0xEF, 0x1E, 0x1E, 0xF1, - 0xE1, 0xEF, 0x1E, 0x1E, 0xF1, 0xE1, 0xEF, 0x1E, 0x1E, 0xF1, 0xE1, 0xEF, 0x1E, 0x1E, 0xF1, 0xE1, - 0xEF, 0x1E, 0x1E, 0xF1, 0xE1, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xF0, - 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x07, 0x7F, 0x00, 0x7F, 0x80, 0x3F, 0xC0, 0x1F, 0xE0, 0x00, - 0xF0, 0x00, 0x78, 0x00, 0x3F, 0xF0, 0x1F, 0xFC, 0x0F, 0xFF, 0x07, 0x87, 0xC3, 0xC1, 0xE1, 0xE0, - 0xF0, 0xF0, 0xF8, 0x7F, 0xF8, 0x3F, 0xFC, 0x1F, 0xF8, 0x60, 0x03, 0x78, 0x03, 0xFC, 0x01, 0xFE, - 0x00, 0xFF, 0x00, 0x7F, 0x80, 0x3F, 0xFE, 0x1F, 0xFF, 0xCF, 0xFF, 0xF7, 0xF8, 0x7B, 0xFC, 0x3D, - 0xFE, 0x1E, 0xFF, 0x0F, 0x7F, 0xFF, 0xBF, 0xFF, 0x9F, 0xFF, 0x8F, 0x60, 0x0F, 0x00, 0xF0, 0x0F, - 0x00, 0xF0, 0x0F, 0x00, 0xFF, 0x8F, 0xFE, 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, - 0xFF, 0xEF, 0xFC, 0x07, 0x80, 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xC7, 0x83, 0xCF, 0x07, 0x80, 0x07, - 0x83, 0xFF, 0x07, 0xFE, 0x07, 0xFD, 0xC0, 0x7B, 0xC1, 0xE7, 0x87, 0xCF, 0xFF, 0x0F, 0xFE, 0x0F, - 0xF0, 0x03, 0x80, 0x60, 0x1E, 0x0F, 0x0F, 0xF8, 0xF1, 0xFF, 0xCF, 0x1F, 0xFE, 0xF3, 0xE1, 0xEF, - 0x3C, 0x0F, 0xF7, 0x80, 0xFF, 0xF8, 0x0F, 0xFF, 0x80, 0xFF, 0xF8, 0x0F, 0xF7, 0x80, 0xFF, 0x3C, - 0x0F, 0xF3, 0xE1, 0xEF, 0x1F, 0xFE, 0xF1, 0xFF, 0xCF, 0x07, 0xF8, 0x00, 0x1C, 0x00, 0x03, 0xF0, - 0xFF, 0xCF, 0xFE, 0xFF, 0xF7, 0x87, 0xBC, 0x3D, 0xE1, 0xEF, 0x8F, 0x7F, 0xF9, 0xFF, 0xC7, 0xFE, - 0x7C, 0xF3, 0xC7, 0xBE, 0x3D, 0xE1, 0xFF, 0x0F, 0x0F, 0x07, 0xF9, 0xFF, 0xBC, 0xF0, 0x1E, 0x3F, - 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, 0xFF, 0xBF, 0xF1, 0x80, 0x00, 0x60, 0x0F, 0x0F, 0xE3, 0xFE, - 0x3F, 0x07, 0xB8, 0x7F, 0xE7, 0xFE, 0xF8, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, 0xFE, 0x3F, - 0xE1, 0xFC, 0x07, 0x00, 0xFC, 0x1F, 0xF3, 0xFF, 0x7B, 0xEE, 0x3D, 0xFF, 0x3F, 0xE7, 0xFE, 0xE1, - 0xDF, 0xFB, 0xFF, 0x7F, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, - 0xF0, 0x1F, 0xF0, 0x7F, 0xC3, 0xFF, 0x0F, 0xFC, 0x3C, 0xF0, 0xF3, 0xC3, 0xCF, 0x0E, 0x3C, 0x38, - 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xC0, 0xFF, 0x03, 0xF8, 0x0E, 0x0F, 0x01, 0xFC, - 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, - 0x00, 0x71, 0xC3, 0xBE, 0x79, 0xE7, 0x9E, 0xF8, 0xF7, 0xBC, 0x1F, 0xFE, 0x07, 0xFF, 0x00, 0xFF, - 0xC0, 0x7F, 0xF8, 0x3F, 0xFF, 0x0F, 0x7B, 0xC7, 0x9E, 0xFB, 0xE7, 0x9F, 0x0E, 0x07, 0xF1, 0xFF, - 0x39, 0xE0, 0x3C, 0x1F, 0x03, 0xE0, 0x7E, 0xF3, 0xDF, 0xF9, 0xFF, 0x1F, 0xC0, 0x40, 0xE0, 0xFE, - 0x3F, 0xCF, 0xF9, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xEF, 0xFD, 0xFF, 0x3F, 0xC7, 0xF8, 0xF0, 0x31, - 0x87, 0xF0, 0xFC, 0x07, 0x0E, 0x0F, 0xE3, 0xFC, 0xFF, 0x9F, 0xF7, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, - 0xDF, 0xF3, 0xFC, 0x7F, 0x8F, 0xE1, 0xFE, 0x7F, 0xCF, 0x7B, 0xCF, 0xF9, 0xFE, 0x3F, 0x87, 0xF8, - 0xF7, 0x9E, 0xFB, 0xCF, 0x78, 0xF0, 0x3F, 0xE3, 0xFF, 0x3F, 0xF3, 0xFF, 0x3C, 0xF3, 0xCF, 0x3C, - 0xF3, 0x8F, 0x78, 0xFF, 0x8F, 0xF8, 0xFF, 0x0F, 0xF8, 0x3D, 0xF8, 0xFF, 0xF1, 0xFF, 0xE3, 0xFF, - 0xEF, 0xFF, 0xDF, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xDF, 0xEF, 0xBF, 0xDF, 0x7F, 0x9C, 0xF0, 0xE0, - 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xFE, 0x3F, 0xC7, 0xF8, 0xF0, - 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, 0xFF, 0x3F, - 0xE1, 0xFC, 0x06, 0x00, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xE3, 0xFC, 0x7F, 0x8F, 0xF1, - 0xFE, 0x3F, 0xC7, 0xF8, 0xF0, 0xE7, 0x0F, 0xFC, 0xFF, 0xEF, 0xBE, 0xF1, 0xEF, 0x0F, 0xF0, 0xFF, - 0x0F, 0xF1, 0xEF, 0xFE, 0xFF, 0xCF, 0xFC, 0xF2, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x00, 0x0F, - 0x01, 0xFC, 0x3F, 0xE7, 0xDE, 0x78, 0xFF, 0x00, 0xF0, 0x0F, 0x00, 0x78, 0xF7, 0xFE, 0x3F, 0xE1, - 0xFC, 0x06, 0x00, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xE1, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, - 0xC0, 0x78, 0x0F, 0x00, 0xE0, 0x77, 0x87, 0x9C, 0x3C, 0xF1, 0xC7, 0x9E, 0x1C, 0xF0, 0xF7, 0x07, - 0xF8, 0x1F, 0x80, 0xFC, 0x03, 0xE0, 0x1E, 0x00, 0xF0, 0x1F, 0x81, 0xF8, 0x0F, 0x80, 0x78, 0x00, - 0x01, 0x80, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x07, 0xE0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7B, 0xDE, - 0x7B, 0xDE, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0x7B, 0xDE, 0x7F, 0xFE, 0x3F, 0xFC, 0x1F, 0xF8, - 0x07, 0xE0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x70, 0xE7, 0x9E, 0x79, 0xE3, 0xFC, - 0x1F, 0x81, 0xF8, 0x1F, 0x01, 0xF8, 0x3F, 0xC3, 0xBC, 0x79, 0xEF, 0x0F, 0xE1, 0xCF, 0x1E, 0xF1, - 0xEF, 0x1E, 0xF1, 0xEF, 0x1E, 0xF1, 0xEF, 0x1E, 0xF1, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x70, - 0x07, 0x00, 0x70, 0x07, 0xE1, 0xDE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, 0xE3, 0xFF, 0xFB, 0xFF, 0x3F, - 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0xE3, 0x87, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, - 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0x87, 0x3C, - 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, - 0xCF, 0x3F, 0xFF, 0xEF, 0xFF, 0xFB, 0xFF, 0xFE, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x00, 0x38, 0x00, - 0x0E, 0xFC, 0x03, 0xF8, 0x0F, 0xE0, 0x07, 0x80, 0x1F, 0xE0, 0x7F, 0xE1, 0xFF, 0x87, 0x8E, 0x1E, - 0x3C, 0x7F, 0xE1, 0xFF, 0x87, 0xFC, 0xE0, 0x1D, 0xE0, 0x3F, 0xC0, 0x7F, 0x80, 0xFF, 0xF1, 0xFF, - 0xFB, 0xFF, 0xF7, 0xF8, 0xFF, 0xF1, 0xFF, 0xFF, 0xBF, 0xFF, 0x7F, 0xFC, 0xF0, 0xE0, 0x1E, 0x03, - 0xC0, 0x78, 0x0F, 0xF1, 0xFF, 0xBF, 0xF7, 0x8F, 0xF1, 0xFF, 0xFB, 0xFF, 0x7F, 0xC0, 0x0F, 0x03, - 0xFC, 0x7F, 0xE7, 0x9E, 0x70, 0xE0, 0xFF, 0x0F, 0xF0, 0x7F, 0xF0, 0xE7, 0xFE, 0x7F, 0xC3, 0xF8, - 0x06, 0x00, 0xE0, 0x70, 0x78, 0xFE, 0x3C, 0xFF, 0x9E, 0xFB, 0xEF, 0x78, 0xF7, 0xFC, 0x3B, 0xFE, - 0x1D, 0xFF, 0x0E, 0xF7, 0x8F, 0x7B, 0xFF, 0xBC, 0xFF, 0x9E, 0x3F, 0x80, 0x07, 0x00, 0x0F, 0xE7, - 0xFD, 0xFF, 0xBF, 0xFF, 0x1E, 0xF3, 0xDF, 0xF9, 0xFF, 0x3F, 0xEF, 0xBD, 0xE7, 0xF8, 0xF0, 0x1C, - 0x01, 0xE0, 0x0E, 0x00, 0x60, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x39, 0xC3, 0xDC, 0x3D, 0xC1, 0x08, 0x0F, - 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, - 0xFC, 0x06, 0x00, 0x18, 0x01, 0xE0, 0x3F, 0xE0, 0xFF, 0x03, 0xDC, 0x1F, 0xF8, 0xFF, 0xC7, 0xFF, - 0x3C, 0x79, 0xE3, 0xCF, 0x1E, 0x78, 0xF3, 0xC7, 0x9E, 0x3C, 0xF1, 0xE7, 0x8F, 0x00, 0x78, 0x03, - 0xC0, 0x7E, 0x03, 0xE0, 0x1E, 0x00, 0x0E, 0x1E, 0x1C, 0x18, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xF0, 0xFF, - 0x0F, 0xE0, 0x78, 0xF7, 0xFE, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x0F, 0x07, 0xF9, 0xFF, 0x3C, 0xF7, - 0x80, 0xFF, 0x0F, 0xF0, 0x7F, 0x71, 0xEF, 0x3D, 0xFF, 0x9F, 0xE0, 0xE0, 0xEF, 0xF6, 0xEF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0x73, 0x3B, 0xDD, 0xE4, 0x61, 0xC0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, - 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x38, 0xF3, 0xC6, 0x38, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, - 0xF3, 0xCF, 0x3D, 0xFF, 0xBE, 0xC0, 0x1F, 0xF0, 0x07, 0xFE, 0x00, 0xFF, 0xC0, 0x1F, 0xF8, 0x03, - 0xCF, 0xF8, 0x79, 0xFF, 0x8F, 0x3F, 0xF9, 0xE7, 0x8F, 0x38, 0xF1, 0xFF, 0x1F, 0xFF, 0xE3, 0xFF, - 0x78, 0x7F, 0xC0, 0xE1, 0xC0, 0x3C, 0x78, 0x0F, 0x1E, 0x03, 0xC7, 0x80, 0xF1, 0xFC, 0x3F, 0xFF, - 0xCF, 0xFF, 0xFB, 0xFF, 0xFF, 0xF1, 0xE3, 0xFC, 0x7F, 0xFF, 0x1F, 0xFB, 0xC7, 0xFC, 0x18, 0x03, - 0xF8, 0x3F, 0xE0, 0xFE, 0x03, 0xDC, 0x1F, 0xF8, 0xFF, 0xC7, 0xFF, 0x3C, 0x79, 0xE3, 0xCF, 0x1E, - 0x78, 0xF3, 0xC7, 0x9E, 0x3C, 0xF1, 0xE7, 0x8F, 0x07, 0x01, 0xE0, 0x38, 0x06, 0x0E, 0x1F, 0xE7, - 0xFC, 0xF7, 0xBC, 0xFF, 0x9F, 0xE3, 0xF8, 0x7F, 0x8F, 0x79, 0xEF, 0xBC, 0xF7, 0x8F, 0x3C, 0x03, - 0x80, 0x78, 0x07, 0x0E, 0x0F, 0xE3, 0xFC, 0xFF, 0x9F, 0xF7, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xDF, - 0xF3, 0xFC, 0x7F, 0x8F, 0x39, 0xC0, 0xFE, 0x07, 0xE0, 0x0E, 0x0E, 0x07, 0x78, 0x79, 0xC3, 0xCF, - 0x1C, 0x79, 0xE1, 0xCF, 0x0F, 0x70, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xE0, 0x0F, 0x01, - 0xF8, 0x1F, 0x80, 0xF8, 0x07, 0x80, 0x00, 0xE1, 0xDE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, 0xE3, 0xFC, - 0x7F, 0x8F, 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xE0, 0x3C, 0x07, 0x80, 0x70, 0x60, 0xC1, 0xFC, - 0x78, 0xFF, 0x1E, 0x3F, 0xC7, 0x8F, 0xF1, 0xE3, 0xFC, 0x78, 0xFF, 0x1E, 0x3F, 0xC7, 0x8F, 0xF1, - 0xE3, 0xFC, 0x78, 0xFF, 0x1E, 0x3F, 0xC7, 0x8F, 0xF1, 0xE3, 0xFF, 0xFF, 0xF7, 0xFF, 0xF8, 0xFE, - 0xFC, 0x04, 0x0C, 0x00, 0xE1, 0xC7, 0xF3, 0xC7, 0xF3, 0xC7, 0xF3, 0xC7, 0xF3, 0xC7, 0xF3, 0xC7, - 0xF3, 0xC7, 0xF3, 0xC7, 0xF3, 0xCF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7E, 0x7E, 0x08, 0x18, 0x18, 0x00, - 0xF0, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x80, 0xF0, 0x03, 0xFF, 0x0F, 0xFE, 0x3F, 0xFC, 0xF0, 0xF3, - 0xC3, 0xCF, 0x0F, 0x3C, 0x3C, 0xFF, 0xF3, 0xFF, 0x8F, 0xFC, 0x18, 0x01, 0xE0, 0x0F, 0x00, 0x78, - 0x0F, 0xF8, 0x7F, 0xE3, 0xFF, 0x07, 0x80, 0x3F, 0xC1, 0xFF, 0x8F, 0xFC, 0x78, 0xF3, 0xC7, 0x9F, - 0xFC, 0xFF, 0xC7, 0xFC, 0x60, 0x1C, 0x1E, 0x1F, 0xF3, 0xC7, 0xFF, 0x78, 0xFF, 0xEF, 0x3C, 0x1F, - 0xE7, 0x83, 0xFD, 0xE0, 0x07, 0xFF, 0xE0, 0xFF, 0xFC, 0x1F, 0xFF, 0x83, 0xDE, 0x00, 0x79, 0xE0, - 0xFF, 0x3E, 0x3F, 0xE3, 0xFF, 0xBC, 0x7F, 0xF7, 0x83, 0xF8, 0x00, 0x1C, 0x00, 0xE0, 0x70, 0xF1, - 0xFC, 0xF3, 0xFE, 0xF7, 0xDE, 0xF7, 0x8F, 0xFF, 0xE0, 0xFF, 0xF0, 0xFF, 0xE0, 0xF7, 0x8F, 0xF7, - 0xFE, 0xF3, 0xFE, 0xF1, 0xFC, 0x00, 0x60, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, - 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x7F, 0xF0, 0xFF, 0xE1, 0xFF, 0xE7, 0xBB, 0xCF, - 0x77, 0x9E, 0xE7, 0xF9, 0xCF, 0x07, 0x00, 0xF0, 0x1F, 0x81, 0xF8, 0x1F, 0x83, 0xFC, 0x3D, 0xC3, - 0xFE, 0x7F, 0xE7, 0x6E, 0xF6, 0xFF, 0x6F, 0x60, 0x3C, 0x0F, 0x07, 0xC0, 0xF0, 0x7E, 0x0F, 0x07, - 0xE0, 0xF0, 0xFF, 0x0F, 0x0F, 0xF0, 0xF1, 0xEF, 0x0F, 0x1E, 0x78, 0xF1, 0xE7, 0x8F, 0xFF, 0xF8, - 0xFF, 0xFF, 0xCF, 0xFF, 0xFC, 0xF7, 0xFF, 0xCF, 0x7B, 0xDE, 0xF7, 0xBD, 0xEF, 0xFB, 0xDE, 0xE0, - 0x70, 0xF0, 0xF0, 0xF0, 0xF8, 0xF1, 0xF8, 0xF1, 0xF8, 0xF3, 0xFC, 0xFF, 0xDC, 0xFF, 0xFE, 0xFF, - 0xFE, 0xF7, 0xFE, 0xFF, 0x6F, 0xFF, 0x6F, 0x3F, 0xFE, 0x3F, 0xFF, 0x0F, 0xFF, 0x07, 0xFF, 0x81, - 0xE7, 0x80, 0x73, 0xC0, 0x3F, 0xC0, 0x1F, 0xE0, 0x3F, 0xFE, 0x3F, 0xFF, 0xBE, 0xF7, 0xDE, 0x79, - 0xEF, 0x3C, 0x77, 0x9E, 0x3B, 0xCF, 0x1D, 0xE7, 0x8E, 0x7F, 0xF9, 0xFF, 0xE3, 0xFF, 0x0F, 0x38, - 0x1F, 0xE0, 0x7F, 0x07, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x9E, 0xFE, 0x7B, 0xF9, 0xEF, 0x60, 0xFF, - 0xF9, 0xE1, 0xFF, 0xF3, 0xC3, 0xFF, 0xE7, 0x83, 0xFF, 0x8F, 0x07, 0x8E, 0x1E, 0x07, 0xBC, 0x3C, - 0x07, 0x70, 0x7F, 0xFF, 0xF0, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFB, 0xCF, 0xFD, 0xF7, 0x9E, 0x38, - 0xFF, 0x3C, 0x71, 0xFE, 0x78, 0xE3, 0xFC, 0xF1, 0xC7, 0xF9, 0xE3, 0x8F, 0xE1, 0xFF, 0xCF, 0x1F, - 0xFC, 0xF1, 0xFF, 0xCF, 0x0F, 0x78, 0xF0, 0x77, 0x0F, 0xFF, 0xF0, 0xFF, 0xFF, 0xCF, 0xFF, 0xFE, - 0xF3, 0xFD, 0xEF, 0x79, 0xCE, 0xF7, 0x9C, 0xEF, 0x79, 0xCE, 0x79, 0xCF, 0xE1, 0xF0, 0x38, 0x78, - 0x1F, 0xC7, 0xF9, 0xFF, 0x03, 0xC0, 0xF0, 0x78, 0xFE, 0x3F, 0x0F, 0xE0, 0x3C, 0x0F, 0x03, 0xCF, - 0xFF, 0xFB, 0xFC, 0xFC, 0x38, 0x0F, 0x01, 0xC0, 0x30, 0x00, 0x7B, 0xCF, 0xE1, 0xF0, 0x38, 0x7E, - 0x1F, 0xE7, 0xFC, 0xFF, 0x03, 0xCF, 0xF3, 0xF8, 0xFF, 0x03, 0xC3, 0xF3, 0xFD, 0xFE, 0xFE, 0x3E, - 0x07, 0xC1, 0xF0, 0x08, 0x00, 0x61, 0x87, 0x79, 0xE3, 0xFC, 0xF1, 0xFE, 0x78, 0xFF, 0x3C, 0x7F, - 0x9E, 0x3F, 0xCF, 0x1D, 0xE7, 0x9E, 0x7B, 0xCF, 0x3F, 0xFF, 0x8F, 0xFF, 0x83, 0xFF, 0x00, 0x3C, - 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x80, 0x07, 0x03, 0x8E, 0x3F, 0x9C, 0x7F, 0x38, - 0xFE, 0x71, 0xFC, 0xE3, 0xF9, 0xC7, 0xF3, 0x8F, 0xE7, 0x3F, 0xCE, 0x7F, 0xFF, 0xF7, 0xFF, 0xC7, - 0xFF, 0x01, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, - 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x78, - 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x0F, 0x01, 0xFC, - 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0xEF, 0xFF, 0xFF, 0x3F, 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, - 0x00, 0x70, 0x06, 0xF0, 0x1F, 0x78, 0x1F, 0x78, 0x3E, 0x78, 0x3C, 0x3C, 0x3C, 0x3C, 0x78, 0x3C, + 0xF0, 0x7F, 0xF8, 0x7F, 0xFC, 0x3F, 0xFE, 0x1F, 0xFF, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0xE0, + 0x0F, 0xFC, 0x07, 0xFF, 0x03, 0xFF, 0xC1, 0xE1, 0xE0, 0xF0, 0xF0, 0x78, 0x78, 0x3C, 0x3C, 0x1E, + 0xFE, 0x0F, 0x7E, 0x00, 0x3E, 0x00, 0x18, 0x00, 0x07, 0x80, 0xE0, 0x38, 0x07, 0x07, 0xFD, 0xFF, + 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, + 0x03, 0xC0, 0x78, 0x00, 0x03, 0xC0, 0x1F, 0xE0, 0x7F, 0xE1, 0xFF, 0xE7, 0xC3, 0xEF, 0x03, 0xDE, + 0x00, 0x3F, 0xE0, 0x7F, 0xC0, 0xFF, 0x81, 0xE0, 0x73, 0xC0, 0xF7, 0xC3, 0xC7, 0xFF, 0x87, 0xFE, + 0x07, 0xF8, 0x01, 0x80, 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0xFF, 0xF7, 0x87, 0xBC, 0x1D, 0xF8, 0x07, + 0xF8, 0x1F, 0xF0, 0x3F, 0xC0, 0x3E, 0xE0, 0xF7, 0x87, 0xBF, 0xFC, 0xFF, 0xC3, 0xFC, 0x03, 0x00, + 0x6F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xEF, 0xEF, 0x42, 0x18, 0x3C, 0x3C, 0x3C, + 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x00, 0xC0, 0x3C, 0x07, + 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3F, 0xC7, 0xF8, 0xFF, 0x1E, 0xFF, 0xDF, + 0xF1, 0xFC, 0x06, 0x00, 0x0F, 0xFC, 0x00, 0x1F, 0xF8, 0x00, 0x7F, 0xF0, 0x00, 0xFF, 0xE0, 0x01, + 0xE3, 0xC0, 0x03, 0xC7, 0x80, 0x07, 0x8F, 0xFC, 0x0F, 0x1F, 0xFC, 0x1E, 0x3F, 0xFC, 0x3C, 0x78, + 0x7C, 0x70, 0xF0, 0x78, 0xE1, 0xE0, 0xF3, 0xC3, 0xC3, 0xFF, 0x87, 0xFF, 0xBE, 0x0F, 0xFE, 0x78, + 0x1F, 0xF8, 0x60, 0x38, 0x03, 0xC0, 0xE0, 0x0F, 0x03, 0x80, 0x3C, 0x0E, 0x00, 0xF0, 0x38, 0x03, + 0xC0, 0xE0, 0x0F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFE, 0xFF, 0xFF, 0xFB, 0xC0, 0xE1, 0xFF, 0x03, 0x83, + 0xFC, 0x0E, 0x0F, 0xF0, 0x38, 0x3F, 0xC0, 0xFF, 0xEF, 0x03, 0xFF, 0xBC, 0x0F, 0xF8, 0x7F, 0xF8, + 0x3F, 0xFF, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x7B, 0xC0, 0x1F, 0xFC, + 0x07, 0xFF, 0x81, 0xFF, 0xE0, 0x78, 0x7C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, + 0x1E, 0x0F, 0x03, 0x80, 0x1E, 0x00, 0x70, 0x01, 0x80, 0x60, 0x3B, 0xC1, 0xEF, 0x0F, 0x3C, 0x7C, + 0xF3, 0xE3, 0xCF, 0x0F, 0x78, 0x3F, 0xE0, 0xFF, 0x03, 0xFE, 0x0F, 0x7C, 0x3C, 0xF8, 0xF1, 0xE3, + 0xC3, 0xCF, 0x0F, 0xBC, 0x1E, 0x1E, 0x00, 0x38, 0x00, 0x70, 0x01, 0xC0, 0x60, 0x3B, 0xC1, 0xFF, + 0x07, 0xFC, 0x3F, 0xF1, 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xEF, 0xF7, 0xBF, 0xFC, 0xFF, 0xF3, 0xFF, + 0x8F, 0xFC, 0x3F, 0xF0, 0xFF, 0x83, 0xFC, 0x0F, 0x1C, 0xE0, 0x3F, 0x80, 0xFC, 0x00, 0xE0, 0x70, + 0x1F, 0xE0, 0xF7, 0x87, 0xDF, 0x1E, 0x3C, 0x78, 0xF3, 0xC1, 0xEF, 0x07, 0xF8, 0x0F, 0xE0, 0x3F, + 0x00, 0x7C, 0x01, 0xF0, 0x07, 0x80, 0xFE, 0x03, 0xF0, 0x0F, 0x80, 0x60, 0x37, 0x83, 0xFC, 0x1F, + 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x03, 0x80, 0x0F, 0x80, + 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xFF, + 0xE1, 0xFF, 0xE7, 0xFF, 0xCF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x7F, 0xCF, 0xFE, 0xFF, 0xEF, 0xFC, + 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xEF, 0xFC, 0x7E, 0x0F, 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF1, 0xEF, 0xFC, 0xFF, 0xEF, + 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, 0xEF, 0xFC, 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, + 0x07, 0xFE, 0x03, 0xFF, 0xC0, 0xFF, 0xF0, 0x3F, 0xFC, 0x0F, 0x0F, 0x03, 0xC3, 0xC0, 0xF0, 0xF0, + 0x3C, 0x3C, 0x0F, 0x0F, 0x03, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x3E, 0x0F, 0x3F, 0xFF, 0xEF, + 0xFF, 0xFB, 0xFF, 0xFE, 0xF0, 0x03, 0xBC, 0x00, 0xEF, 0x00, 0x39, 0xC0, 0x0E, 0x7F, 0xDF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0xFE, 0xFF, 0xDF, 0xFB, 0xC0, 0x78, 0x0F, 0x01, 0xFF, + 0xFF, 0xFF, 0xFF, 0x70, 0x30, 0x39, 0xE1, 0xE1, 0xE7, 0xC7, 0x8F, 0x8F, 0x1E, 0x3C, 0x3E, 0x79, + 0xE0, 0x79, 0xE7, 0x80, 0xF7, 0xBC, 0x03, 0xFF, 0xF0, 0x0F, 0xFF, 0xC0, 0x3F, 0xFF, 0x01, 0xE7, + 0x9E, 0x07, 0x9E, 0x78, 0x3C, 0x78, 0xF1, 0xF1, 0xE3, 0xC7, 0x87, 0x87, 0xBE, 0x1E, 0x1F, 0x07, + 0x00, 0xFF, 0x0F, 0xFC, 0xFB, 0xE7, 0x8F, 0x18, 0x78, 0x03, 0xC0, 0xFC, 0x07, 0xE0, 0x3F, 0x80, + 0x3E, 0xE0, 0xF7, 0x87, 0xBF, 0xF8, 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x60, 0x3B, 0xC1, 0xFF, 0x07, + 0xFC, 0x3F, 0xF1, 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xEF, 0xF7, 0xBF, 0xFC, 0xFF, 0xF3, 0xFF, 0x8F, + 0xFC, 0x3F, 0xF0, 0xFF, 0x83, 0xFC, 0x0F, 0x18, 0xC0, 0x7F, 0x00, 0xFC, 0x01, 0xC0, 0x60, 0x3B, + 0xC1, 0xFF, 0x07, 0xFC, 0x3F, 0xF1, 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xEF, 0xF7, 0xBF, 0xFC, 0xFF, + 0xF3, 0xFF, 0x8F, 0xFC, 0x3F, 0xF0, 0xFF, 0x83, 0xFC, 0x0F, 0x60, 0x3B, 0xC1, 0xEF, 0x0F, 0x3C, + 0x7C, 0xF3, 0xE3, 0xCF, 0x0F, 0x78, 0x3F, 0xE0, 0xFF, 0x03, 0xFE, 0x0F, 0x7C, 0x3C, 0xF8, 0xF1, + 0xE3, 0xC3, 0xCF, 0x0F, 0xBC, 0x1E, 0x0F, 0xFC, 0x1F, 0xFC, 0x7F, 0xF8, 0xFF, 0xF1, 0xE1, 0xE3, + 0xC3, 0xC7, 0x87, 0x8F, 0x0F, 0x1E, 0x1E, 0x3C, 0x3C, 0x78, 0x78, 0xE0, 0xF3, 0xC1, 0xFF, 0x83, + 0xFE, 0x07, 0xF8, 0x0F, 0x78, 0x0F, 0xFE, 0x07, 0xFF, 0x07, 0xFF, 0x83, 0xFF, 0xE1, 0xFF, 0xF1, + 0xFF, 0xF8, 0xFF, 0xFE, 0xFF, 0xFF, 0x7F, 0xFB, 0xBF, 0xFD, 0xFD, 0xFE, 0xFE, 0xFF, 0x3F, 0x7F, + 0x9F, 0x3F, 0xCF, 0x9F, 0xE3, 0x8F, 0x60, 0x3B, 0xC0, 0xEF, 0x03, 0xBC, 0x0E, 0xF0, 0x3B, 0xC0, + 0xEF, 0x03, 0xBF, 0xFE, 0xFF, 0xFB, 0xFF, 0xEF, 0x03, 0xBC, 0x0E, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, + 0xBC, 0x0E, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, + 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, + 0x0F, 0xF0, 0x01, 0x80, 0x7F, 0xFB, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, + 0xBC, 0x0E, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, 0xBC, 0x0E, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, 0xBC, 0x0E, + 0x7E, 0x0F, 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xFF, 0xEF, 0xFE, 0xFF, + 0xCF, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0x03, 0xC0, 0x1F, 0xE0, 0x7F, 0xE1, 0xFF, 0xE7, + 0xC3, 0xEF, 0x03, 0xDE, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xE0, 0x73, 0xC0, 0xF7, 0xC3, + 0xC7, 0xFF, 0x87, 0xFE, 0x07, 0xF8, 0x01, 0x80, 0x7F, 0xFB, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0x07, + 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, + 0x00, 0x78, 0x01, 0xE0, 0x70, 0x1F, 0xE0, 0xF7, 0x87, 0xDF, 0x1E, 0x3C, 0x78, 0xF3, 0xC1, 0xEF, + 0x07, 0xF8, 0x0F, 0xE0, 0x3F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0x80, 0xFE, 0x03, 0xF0, 0x0F, 0x80, + 0x01, 0xE0, 0x00, 0xF0, 0x03, 0xFF, 0x07, 0xFF, 0xC3, 0xFF, 0xF3, 0xCF, 0x79, 0xE7, 0x9E, 0xE3, + 0xCF, 0x71, 0xE7, 0xB8, 0xF3, 0xDE, 0x79, 0xEF, 0xBD, 0xE3, 0xFF, 0xF0, 0xFF, 0xF0, 0x3F, 0xE0, + 0x03, 0xC0, 0x01, 0xE0, 0x00, 0x70, 0x1C, 0xF0, 0x78, 0xF1, 0xF1, 0xF3, 0xC1, 0xEF, 0x01, 0xFE, + 0x03, 0xF8, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0xC0, 0x3F, 0x80, 0xF7, 0x83, 0xEF, 0x87, 0x8F, 0x1E, + 0x0F, 0x7C, 0x1F, 0x60, 0x31, 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xCF, 0x07, 0x9E, 0x0F, 0x3C, 0x1E, + 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xCF, 0x07, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xE0, 0x60, 0x77, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, + 0x07, 0xF8, 0x3F, 0xC1, 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, 0xC7, 0xDE, 0x00, 0xF0, 0x07, 0x80, 0x3C, + 0x01, 0xE0, 0x0F, 0x60, 0xC1, 0xDE, 0x3C, 0x3F, 0xC7, 0x87, 0xF8, 0xF0, 0xFF, 0x1E, 0x1F, 0xE3, + 0xC3, 0xFC, 0x78, 0x7F, 0x8F, 0x0F, 0xF1, 0xE1, 0xFE, 0x3C, 0x3F, 0xC7, 0x87, 0xF8, 0xF0, 0xFF, + 0x1E, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x60, 0xC1, 0xCF, 0x1E, 0x1E, 0xF1, 0xE1, + 0xEF, 0x1E, 0x1E, 0xF1, 0xE1, 0xEF, 0x1E, 0x1E, 0xF1, 0xE1, 0xEF, 0x1E, 0x1E, 0xF1, 0xE1, 0xEF, + 0x1E, 0x1E, 0xF1, 0xE1, 0xEF, 0x1E, 0x1E, 0xF1, 0xE1, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x07, 0x7F, 0x00, 0x7F, 0x80, 0x3F, + 0xC0, 0x1F, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3F, 0xF0, 0x1F, 0xFC, 0x0F, 0xFF, 0x07, 0x87, + 0xC3, 0xC1, 0xE1, 0xE0, 0xF0, 0xF0, 0xF8, 0x7F, 0xF8, 0x3F, 0xFC, 0x1F, 0xF8, 0x60, 0x03, 0x78, + 0x03, 0xFC, 0x01, 0xFE, 0x00, 0xFF, 0x00, 0x7F, 0x80, 0x3F, 0xFE, 0x1F, 0xFF, 0xCF, 0xFF, 0xF7, + 0xF8, 0x7B, 0xFC, 0x3D, 0xFE, 0x1E, 0xFF, 0x0F, 0x7F, 0xFF, 0xBF, 0xFF, 0x9F, 0xFF, 0x8F, 0x60, + 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xFF, 0x8F, 0xFE, 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, + 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, 0xEF, 0xFC, 0x07, 0x80, 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xC7, 0x83, + 0xCF, 0x07, 0x80, 0x07, 0x83, 0xFF, 0x07, 0xFE, 0x07, 0xFD, 0xC0, 0x7B, 0xC1, 0xE7, 0x87, 0xCF, + 0xFF, 0x0F, 0xFE, 0x0F, 0xF0, 0x03, 0x80, 0x60, 0x1E, 0x0F, 0x0F, 0xF8, 0xF1, 0xFF, 0xCF, 0x1F, + 0xFE, 0xF3, 0xE1, 0xEF, 0x3C, 0x0F, 0xF7, 0x80, 0xFF, 0xF8, 0x0F, 0xFF, 0x80, 0xFF, 0xF8, 0x0F, + 0xF7, 0x80, 0xFF, 0x3C, 0x0F, 0xF3, 0xE1, 0xEF, 0x1F, 0xFE, 0xF1, 0xFF, 0xCF, 0x07, 0xF8, 0x00, + 0x1C, 0x00, 0x03, 0xF0, 0xFF, 0xCF, 0xFE, 0xFF, 0xF7, 0x87, 0xBC, 0x3D, 0xE1, 0xEF, 0x8F, 0x7F, + 0xF9, 0xFF, 0xC7, 0xFE, 0x7C, 0xF3, 0xC7, 0xBE, 0x3D, 0xE1, 0xFF, 0x0F, 0x0F, 0x07, 0xF9, 0xFF, + 0xBC, 0xF0, 0x1E, 0x3F, 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, 0xFF, 0xBF, 0xF1, 0x80, 0x00, 0x60, + 0x0F, 0x0F, 0xE3, 0xFE, 0x3F, 0x07, 0xB8, 0x7F, 0xE7, 0xFE, 0xF8, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, + 0x78, 0xF7, 0xFE, 0x3F, 0xE1, 0xFC, 0x07, 0x00, 0xFC, 0x1F, 0xF3, 0xFF, 0x7B, 0xEE, 0x3D, 0xFF, + 0x3F, 0xE7, 0xFE, 0xE1, 0xDF, 0xFB, 0xFF, 0x7F, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0x1F, 0xF0, 0x7F, 0xC3, 0xFF, 0x0F, 0xFC, 0x3C, 0xF0, 0xF3, 0xC3, + 0xCF, 0x0E, 0x3C, 0x38, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xC0, 0xFF, 0x03, 0xF8, + 0x0E, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, + 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x71, 0xC3, 0xBE, 0x79, 0xE7, 0x9E, 0xF8, 0xF7, 0xBC, 0x1F, 0xFE, + 0x07, 0xFF, 0x00, 0xFF, 0xC0, 0x7F, 0xF8, 0x3F, 0xFF, 0x0F, 0x7B, 0xC7, 0x9E, 0xFB, 0xE7, 0x9F, + 0x0E, 0x07, 0xF1, 0xFF, 0x39, 0xE0, 0x3C, 0x1F, 0x03, 0xE0, 0x7E, 0xF3, 0xDF, 0xF9, 0xFF, 0x1F, + 0xC0, 0x40, 0xE0, 0xFE, 0x3F, 0xCF, 0xF9, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xEF, 0xFD, 0xFF, 0x3F, + 0xC7, 0xF8, 0xF0, 0x31, 0x87, 0xF0, 0xFC, 0x07, 0x0E, 0x0F, 0xE3, 0xFC, 0xFF, 0x9F, 0xF7, 0xFF, + 0xFF, 0xFF, 0xFE, 0xFF, 0xDF, 0xF3, 0xFC, 0x7F, 0x8F, 0xE1, 0xFE, 0x7F, 0xCF, 0x7B, 0xCF, 0xF9, + 0xFE, 0x3F, 0x87, 0xF8, 0xF7, 0x9E, 0xFB, 0xCF, 0x78, 0xF0, 0x3F, 0xE3, 0xFF, 0x3F, 0xF3, 0xFF, + 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0x8F, 0x78, 0xFF, 0x8F, 0xF8, 0xFF, 0x0F, 0xF8, 0x3D, 0xF8, 0xFF, + 0xF1, 0xFF, 0xE3, 0xFF, 0xEF, 0xFF, 0xDF, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xDF, 0xEF, 0xBF, 0xDF, + 0x7F, 0x9C, 0xF0, 0xE0, 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xFE, + 0x3F, 0xC7, 0xF8, 0xF0, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, + 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xE3, + 0xFC, 0x7F, 0x8F, 0xF1, 0xFE, 0x3F, 0xC7, 0xF8, 0xF0, 0xE7, 0x0F, 0xFC, 0xFF, 0xEF, 0xBE, 0xF1, + 0xEF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF1, 0xEF, 0xFE, 0xFF, 0xCF, 0xFC, 0xF2, 0x0F, 0x00, 0xF0, 0x0F, + 0x00, 0xE0, 0x00, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDE, 0x78, 0xFF, 0x00, 0xF0, 0x0F, 0x00, 0x78, + 0xF7, 0xFE, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xE1, 0xE0, 0x3C, 0x07, + 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x00, 0xE0, 0x77, 0x87, 0x9C, 0x3C, 0xF1, 0xC7, 0x9E, + 0x1C, 0xF0, 0xF7, 0x07, 0xF8, 0x1F, 0x80, 0xFC, 0x03, 0xE0, 0x1E, 0x00, 0xF0, 0x1F, 0x81, 0xF8, + 0x0F, 0x80, 0x78, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x07, 0xE0, 0x1F, 0xF8, + 0x3F, 0xFC, 0x7B, 0xDE, 0x7B, 0xDE, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0x7B, 0xDE, 0x7F, 0xFE, + 0x3F, 0xFC, 0x1F, 0xF8, 0x07, 0xE0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x70, 0xE7, + 0x9E, 0x79, 0xE3, 0xFC, 0x1F, 0x81, 0xF8, 0x1F, 0x01, 0xF8, 0x3F, 0xC3, 0xBC, 0x79, 0xEF, 0x0F, + 0xE1, 0xCF, 0x1E, 0xF1, 0xEF, 0x1E, 0xF1, 0xEF, 0x1E, 0xF1, 0xEF, 0x1E, 0xF1, 0xEF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0xE1, 0xDE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, 0xE3, + 0xFF, 0xFB, 0xFF, 0x3F, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0xE3, 0x87, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, + 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xE3, 0x87, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, + 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3F, 0xFF, 0xEF, 0xFF, 0xFB, 0xFF, 0xFE, 0x00, 0x03, 0x80, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0x0E, 0xFC, 0x03, 0xF8, 0x0F, 0xE0, 0x07, 0x80, 0x1F, 0xE0, 0x7F, 0xE1, + 0xFF, 0x87, 0x8E, 0x1E, 0x3C, 0x7F, 0xE1, 0xFF, 0x87, 0xFC, 0xE0, 0x1D, 0xE0, 0x3F, 0xC0, 0x7F, + 0x80, 0xFF, 0xF1, 0xFF, 0xFB, 0xFF, 0xF7, 0xF8, 0xFF, 0xF1, 0xFF, 0xFF, 0xBF, 0xFF, 0x7F, 0xFC, + 0xF0, 0xE0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0xF1, 0xFF, 0xBF, 0xF7, 0x8F, 0xF1, 0xFF, 0xFB, 0xFF, + 0x7F, 0xC0, 0x0F, 0x03, 0xFC, 0x7F, 0xE7, 0x9E, 0x70, 0xE0, 0xFF, 0x0F, 0xF0, 0x7F, 0xF0, 0xE7, + 0xFE, 0x7F, 0xC3, 0xF8, 0x06, 0x00, 0xE0, 0x70, 0x78, 0xFE, 0x3C, 0xFF, 0x9E, 0xFB, 0xEF, 0x78, + 0xF7, 0xFC, 0x3B, 0xFE, 0x1D, 0xFF, 0x0E, 0xF7, 0x8F, 0x7B, 0xFF, 0xBC, 0xFF, 0x9E, 0x3F, 0x80, + 0x07, 0x00, 0x0F, 0xE7, 0xFD, 0xFF, 0xBF, 0xFF, 0x1E, 0xF3, 0xDF, 0xF9, 0xFF, 0x3F, 0xEF, 0xBD, + 0xE7, 0xF8, 0xF0, 0x1C, 0x01, 0xE0, 0x0E, 0x00, 0x60, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x39, 0xC3, 0xDC, + 0x3D, 0xC1, 0x08, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, + 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x18, 0x01, 0xE0, 0x3F, 0xE0, 0xFF, 0x03, 0xDC, 0x1F, + 0xF8, 0xFF, 0xC7, 0xFF, 0x3C, 0x79, 0xE3, 0xCF, 0x1E, 0x78, 0xF3, 0xC7, 0x9E, 0x3C, 0xF1, 0xE7, + 0x8F, 0x00, 0x78, 0x03, 0xC0, 0x7E, 0x03, 0xE0, 0x1E, 0x00, 0x0E, 0x1E, 0x1C, 0x18, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, + 0x70, 0xFF, 0xF0, 0xFF, 0x0F, 0xE0, 0x78, 0xF7, 0xFE, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x0F, 0x07, + 0xF9, 0xFF, 0x3C, 0xF7, 0x80, 0xFF, 0x0F, 0xF0, 0x7F, 0x71, 0xEF, 0x3D, 0xFF, 0x9F, 0xE0, 0xE0, + 0xEF, 0xF6, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x73, 0x3B, 0xDD, 0xE4, 0x61, 0xC0, 0xF0, 0x78, + 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x38, 0xF3, 0xC6, 0x38, 0xF3, 0xCF, + 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3D, 0xFF, 0xBE, 0xC0, 0x1F, 0xF0, 0x07, 0xFE, 0x00, 0xFF, + 0xC0, 0x1F, 0xF8, 0x03, 0xCF, 0xF8, 0x79, 0xFF, 0x8F, 0x3F, 0xF9, 0xE7, 0x8F, 0x38, 0xF1, 0xFF, + 0x1F, 0xFF, 0xE3, 0xFF, 0x78, 0x7F, 0xC0, 0xE1, 0xC0, 0x3C, 0x78, 0x0F, 0x1E, 0x03, 0xC7, 0x80, + 0xF1, 0xFC, 0x3F, 0xFF, 0xCF, 0xFF, 0xFB, 0xFF, 0xFF, 0xF1, 0xE3, 0xFC, 0x7F, 0xFF, 0x1F, 0xFB, + 0xC7, 0xFC, 0x18, 0x03, 0xF8, 0x3F, 0xE0, 0xFE, 0x03, 0xDC, 0x1F, 0xF8, 0xFF, 0xC7, 0xFF, 0x3C, + 0x79, 0xE3, 0xCF, 0x1E, 0x78, 0xF3, 0xC7, 0x9E, 0x3C, 0xF1, 0xE7, 0x8F, 0x07, 0x01, 0xE0, 0x38, + 0x06, 0x0E, 0x1F, 0xE7, 0xFC, 0xF7, 0xBC, 0xFF, 0x9F, 0xE3, 0xF8, 0x7F, 0x8F, 0x79, 0xEF, 0xBC, + 0xF7, 0x8F, 0x3C, 0x03, 0x80, 0x78, 0x07, 0x0E, 0x0F, 0xE3, 0xFC, 0xFF, 0x9F, 0xF7, 0xFF, 0xFF, + 0xFF, 0xFE, 0xFF, 0xDF, 0xF3, 0xFC, 0x7F, 0x8F, 0x39, 0xC0, 0xFE, 0x07, 0xE0, 0x0E, 0x0E, 0x07, + 0x78, 0x79, 0xC3, 0xCF, 0x1C, 0x79, 0xE1, 0xCF, 0x0F, 0x70, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x3E, + 0x01, 0xE0, 0x0F, 0x01, 0xF8, 0x1F, 0x80, 0xF8, 0x07, 0x80, 0x00, 0xE1, 0xDE, 0x3F, 0xC7, 0xF8, + 0xFF, 0x1F, 0xE3, 0xFC, 0x7F, 0x8F, 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xE0, 0x3C, 0x07, 0x80, + 0x70, 0x60, 0xC1, 0xFC, 0x78, 0xFF, 0x1E, 0x3F, 0xC7, 0x8F, 0xF1, 0xE3, 0xFC, 0x78, 0xFF, 0x1E, + 0x3F, 0xC7, 0x8F, 0xF1, 0xE3, 0xFC, 0x78, 0xFF, 0x1E, 0x3F, 0xC7, 0x8F, 0xF1, 0xE3, 0xFF, 0xFF, + 0xF7, 0xFF, 0xF8, 0xFE, 0xFC, 0x04, 0x0C, 0x00, 0xE1, 0xC7, 0xF3, 0xC7, 0xF3, 0xC7, 0xF3, 0xC7, + 0xF3, 0xC7, 0xF3, 0xC7, 0xF3, 0xC7, 0xF3, 0xC7, 0xF3, 0xCF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7E, 0x7E, + 0x08, 0x18, 0x18, 0x00, 0xF0, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x80, 0xF0, 0x03, 0xFF, 0x0F, 0xFE, + 0x3F, 0xFC, 0xF0, 0xF3, 0xC3, 0xCF, 0x0F, 0x3C, 0x3C, 0xFF, 0xF3, 0xFF, 0x8F, 0xFC, 0x18, 0x01, + 0xE0, 0x0F, 0x00, 0x78, 0x0F, 0xF8, 0x7F, 0xE3, 0xFF, 0x07, 0x80, 0x3F, 0xC1, 0xFF, 0x8F, 0xFC, + 0x78, 0xF3, 0xC7, 0x9F, 0xFC, 0xFF, 0xC7, 0xFC, 0x60, 0x1C, 0x1E, 0x1F, 0xF3, 0xC7, 0xFF, 0x78, + 0xFF, 0xEF, 0x3C, 0x1F, 0xE7, 0x83, 0xFD, 0xE0, 0x07, 0xFF, 0xE0, 0xFF, 0xFC, 0x1F, 0xFF, 0x83, + 0xDE, 0x00, 0x79, 0xE0, 0xFF, 0x3E, 0x3F, 0xE3, 0xFF, 0xBC, 0x7F, 0xF7, 0x83, 0xF8, 0x00, 0x1C, + 0x00, 0xE0, 0x70, 0xF1, 0xFC, 0xF3, 0xFE, 0xF7, 0xDE, 0xF7, 0x8F, 0xFF, 0xE0, 0xFF, 0xF0, 0xFF, + 0xE0, 0xF7, 0x8F, 0xF7, 0xFE, 0xF3, 0xFE, 0xF1, 0xFC, 0x00, 0x60, 0x03, 0x80, 0x0F, 0x80, 0x1F, + 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x7F, 0xF0, 0xFF, 0xE1, + 0xFF, 0xE7, 0xBB, 0xCF, 0x77, 0x9E, 0xE7, 0xF9, 0xCF, 0x07, 0x00, 0xF0, 0x1F, 0x81, 0xF8, 0x1F, + 0x83, 0xFC, 0x3D, 0xC3, 0xFE, 0x7F, 0xE7, 0x6E, 0xF6, 0xFF, 0x6F, 0x60, 0x3C, 0x0F, 0x07, 0xC0, + 0xF0, 0x7E, 0x0F, 0x07, 0xE0, 0xF0, 0xFF, 0x0F, 0x0F, 0xF0, 0xF1, 0xEF, 0x0F, 0x1E, 0x78, 0xF1, + 0xE7, 0x8F, 0xFF, 0xF8, 0xFF, 0xFF, 0xCF, 0xFF, 0xFC, 0xF7, 0xFF, 0xCF, 0x7B, 0xDE, 0xF7, 0xBD, + 0xEF, 0xFB, 0xDE, 0xE0, 0x70, 0xF0, 0xF0, 0xF0, 0xF8, 0xF1, 0xF8, 0xF1, 0xF8, 0xF3, 0xFC, 0xFF, + 0xDC, 0xFF, 0xFE, 0xFF, 0xFE, 0xF7, 0xFE, 0xFF, 0x6F, 0xFF, 0x6F, 0x3F, 0xFE, 0x3F, 0xFF, 0x0F, + 0xFF, 0x07, 0xFF, 0x81, 0xE7, 0x80, 0x73, 0xC0, 0x3F, 0xC0, 0x1F, 0xE0, 0x3F, 0xFE, 0x3F, 0xFF, + 0xBE, 0xF7, 0xDE, 0x79, 0xEF, 0x3C, 0x77, 0x9E, 0x3B, 0xCF, 0x1D, 0xE7, 0x8E, 0x7F, 0xF9, 0xFF, + 0xE3, 0xFF, 0x0F, 0x38, 0x1F, 0xE0, 0x7F, 0x07, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x9E, 0xFE, 0x7B, + 0xF9, 0xEF, 0x60, 0xFF, 0xF9, 0xE1, 0xFF, 0xF3, 0xC3, 0xFF, 0xE7, 0x83, 0xFF, 0x8F, 0x07, 0x8E, + 0x1E, 0x07, 0xBC, 0x3C, 0x07, 0x70, 0x7F, 0xFF, 0xF0, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFB, 0xCF, + 0xFD, 0xF7, 0x9E, 0x38, 0xFF, 0x3C, 0x71, 0xFE, 0x78, 0xE3, 0xFC, 0xF1, 0xC7, 0xF9, 0xE3, 0x8F, + 0xE1, 0xFF, 0xCF, 0x1F, 0xFC, 0xF1, 0xFF, 0xCF, 0x0F, 0x78, 0xF0, 0x77, 0x0F, 0xFF, 0xF0, 0xFF, + 0xFF, 0xCF, 0xFF, 0xFE, 0xF3, 0xFD, 0xEF, 0x79, 0xCE, 0xF7, 0x9C, 0xEF, 0x79, 0xCE, 0x79, 0xCF, + 0xE1, 0xF0, 0x38, 0x78, 0x1F, 0xC7, 0xF9, 0xFF, 0x03, 0xC0, 0xF0, 0x78, 0xFE, 0x3F, 0x0F, 0xE0, + 0x3C, 0x0F, 0x03, 0xCF, 0xFF, 0xFB, 0xFC, 0xFC, 0x38, 0x0F, 0x01, 0xC0, 0x30, 0x00, 0x7B, 0xCF, + 0xE1, 0xF0, 0x38, 0x7E, 0x1F, 0xE7, 0xFC, 0xFF, 0x03, 0xCF, 0xF3, 0xF8, 0xFF, 0x03, 0xC3, 0xF3, + 0xFD, 0xFE, 0xFE, 0x3E, 0x07, 0xC1, 0xF0, 0x08, 0x00, 0x61, 0x87, 0x79, 0xE3, 0xFC, 0xF1, 0xFE, + 0x78, 0xFF, 0x3C, 0x7F, 0x9E, 0x3F, 0xCF, 0x1D, 0xE7, 0x9E, 0x7B, 0xCF, 0x3F, 0xFF, 0x8F, 0xFF, + 0x83, 0xFF, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x80, 0x07, 0x03, 0x8E, + 0x3F, 0x9C, 0x7F, 0x38, 0xFE, 0x71, 0xFC, 0xE3, 0xF9, 0xC7, 0xF3, 0x8F, 0xE7, 0x3F, 0xCE, 0x7F, + 0xFF, 0xF7, 0xFF, 0xC7, 0xFF, 0x01, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x03, + 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x7F, 0xFE, 0x7F, + 0xFE, 0x7F, 0xFE, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, + 0x80, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0xEF, 0xFF, 0xFF, 0x3F, 0x78, 0xF7, 0xFF, + 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x70, 0x06, 0xF0, 0x1F, 0x78, 0x1F, 0x78, 0x3E, 0x78, 0x3C, 0x3C, + 0x3C, 0x3C, 0x78, 0x3C, 0x78, 0x1E, 0x78, 0x1E, 0xF0, 0x1E, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x0F, + 0xE0, 0x07, 0xE0, 0x07, 0xC0, 0xE0, 0x37, 0x87, 0xDC, 0x3E, 0xF3, 0xE7, 0x9E, 0x1C, 0xE0, 0xFF, + 0x07, 0xF8, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x1E, 0x00, 0x3D, 0xC0, 0x1F, 0xC0, 0x0E, 0xE0, 0x06, + 0xE0, 0x70, 0x06, 0xF0, 0x1F, 0x78, 0x1F, 0x78, 0x3E, 0x78, 0x3C, 0x3C, 0x3C, 0x3C, 0x78, 0x3C, 0x78, 0x1E, 0x78, 0x1E, 0xF0, 0x1E, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x0F, 0xE0, 0x07, 0xE0, 0x07, - 0xC0, 0xE0, 0x37, 0x87, 0xDC, 0x3E, 0xF3, 0xE7, 0x9E, 0x1C, 0xE0, 0xFF, 0x07, 0xF8, 0x1F, 0x80, - 0xFC, 0x07, 0xE0, 0x1E, 0x00, 0x3D, 0xC0, 0x1F, 0xC0, 0x0E, 0xE0, 0x06, 0xE0, 0x70, 0x06, 0xF0, - 0x1F, 0x78, 0x1F, 0x78, 0x3E, 0x78, 0x3C, 0x3C, 0x3C, 0x3C, 0x78, 0x3C, 0x78, 0x1E, 0x78, 0x1E, - 0xF0, 0x1E, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x0F, 0xE0, 0x07, 0xE0, 0x07, 0xC0, 0x77, 0x03, 0xFC, - 0x0E, 0xE0, 0x37, 0x0E, 0x03, 0x78, 0x7D, 0xC3, 0xEF, 0x3E, 0x79, 0xE1, 0xCE, 0x0F, 0xF0, 0x7F, - 0x81, 0xF8, 0x0F, 0xC0, 0x7E, 0x01, 0xE0, 0x03, 0xC0, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x1F, 0xF8, - 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x7C, 0x3E, 0xE0, 0x77, 0x81, 0xEF, 0x0F, 0x78, 0x1E, 0xF0, 0xF7, - 0x01, 0xE7, 0x9E, 0x70, 0x0E, 0x79, 0xE7, 0x01, 0xE3, 0x9E, 0x78, 0x1E, 0x3F, 0xC7, 0x81, 0xE3, - 0xFC, 0x7C, 0x3E, 0x1F, 0x83, 0xFF, 0xC1, 0xF8, 0x1F, 0xF8, 0x1F, 0x80, 0xFF, 0x00, 0xF0, 0x01, - 0x80, 0x0F, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x78, - 0x00, 0x0F, 0x07, 0x07, 0x0F, 0xE7, 0x87, 0x8F, 0xF9, 0xE3, 0xCF, 0xBE, 0xF1, 0xE7, 0x8F, 0x39, - 0xE7, 0x87, 0x9E, 0xF3, 0xC3, 0xCF, 0x71, 0xE1, 0xE3, 0xF8, 0x78, 0xF1, 0xFC, 0x3F, 0xF8, 0xFC, - 0x0F, 0xF8, 0x3E, 0x03, 0xF8, 0x1F, 0x00, 0x60, 0x0F, 0x00, 0x00, 0x1F, 0x80, 0x00, 0x1F, 0x80, - 0x00, 0x0F, 0xC0, 0x00, 0x03, 0x80, 0x00, 0x03, 0xC0, 0x7F, 0xC3, 0xFF, 0x8F, 0xFF, 0x7C, 0x3D, - 0xE0, 0xF7, 0x80, 0x1C, 0x00, 0x70, 0x01, 0xE0, 0x77, 0x83, 0xDF, 0xBF, 0x3F, 0xFC, 0x7F, 0xF0, - 0x7B, 0xC0, 0x0F, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDE, 0x78, 0xFF, 0x00, 0xF0, 0x0F, 0x00, 0x78, - 0x07, 0xF0, 0x3F, 0x01, 0xF0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x00, 0xE0, 0x0E, 0x1D, 0xC1, - 0xF8, 0x07, 0x80, 0x3E, 0x07, 0xFE, 0xE2, 0xFC, 0x03, 0xC0, 0x1F, 0x03, 0xF8, 0x71, 0x07, 0x00, - 0x03, 0xBF, 0xDF, 0xEC, 0x00, 0x1F, 0xFF, 0xE3, 0xDB, 0xB0, 0x6F, 0xE0, 0x7C, 0x7F, 0xF1, 0xE0, - 0x00, 0x70, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x00, 0x3C, 0x03, 0xC3, 0x60, 0x36, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x7E, 0xC0, 0x06, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x0E, 0x00, 0xE0, 0xF8, 0x0F, 0x86, 0xC0, 0x64, 0x00, 0x70, 0x00, 0x07, 0xC0, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x60, 0x01, 0x86, 0x00, 0x18, 0x61, 0xC0, 0xC0, - 0x78, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x08, 0xF0, 0x00, 0xF3, 0x00, 0x07, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0xE0, 0x30, 0x3C, 0x63, 0x83, 0x06, 0x18, 0x00, - 0x60, 0x00, 0x04, 0x00, 0x00, 0x40, 0x00, 0x18, 0xC0, 0x3F, 0x80, 0x3F, 0x00, 0x38, 0x06, 0x03, - 0x9E, 0x0F, 0xBC, 0x1F, 0x78, 0x7E, 0xF1, 0xFD, 0xE3, 0xFB, 0xCF, 0xF7, 0xBD, 0xEF, 0x7B, 0xDF, - 0xE7, 0xBF, 0xCF, 0x7F, 0x1E, 0xFC, 0x3D, 0xF8, 0x7F, 0xE0, 0xFF, 0x81, 0xF0, 0x01, 0xC0, 0x07, - 0x80, 0x0F, 0x00, 0x1C, 0x31, 0x81, 0xFC, 0x0F, 0xC0, 0x1C, 0x0E, 0x0E, 0x78, 0xF3, 0xCF, 0x9E, - 0x7C, 0xF7, 0xE7, 0xFF, 0x3F, 0xF9, 0xFB, 0xCF, 0xDE, 0x7C, 0xFF, 0xC7, 0xFE, 0x3E, 0x00, 0xF0, - 0x07, 0x80, 0x38, 0x01, 0xC0, 0x18, 0x00, 0xF0, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x80, 0xF0, 0x03, - 0xFF, 0x0F, 0xFE, 0x3F, 0xFC, 0xF0, 0xF3, 0xC3, 0xCF, 0x0F, 0x3C, 0x3C, 0xFF, 0xF3, 0xFF, 0x8F, - 0xFC, 0x3C, 0x01, 0xE0, 0x3F, 0xE1, 0xFF, 0x8F, 0xFC, 0x1E, 0x00, 0xFF, 0x07, 0xFE, 0x3F, 0xF1, - 0xE3, 0xCF, 0x1E, 0x7F, 0xF3, 0xFF, 0x1F, 0xF0, 0x7E, 0x0F, 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, - 0x0F, 0xF6, 0xFF, 0x7F, 0xFF, 0xFF, 0xFE, 0xFF, 0xCF, 0x0E, 0xF0, 0x4F, 0x00, 0xF0, 0x0F, 0x00, - 0xE7, 0x0F, 0xFC, 0xFF, 0xEF, 0xBE, 0xF1, 0xEF, 0x0F, 0xF0, 0xFF, 0x2F, 0xF7, 0xEF, 0xFE, 0xFF, - 0xEF, 0xFC, 0xF2, 0x6F, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x00, 0x01, 0xC0, 0x3C, 0x07, 0xBF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, - 0xF0, 0x1E, 0x03, 0xC0, 0x00, 0x03, 0x07, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0xF0, 0xF0, - 0xF0, 0xF0, 0xF0, 0xF0, 0x1F, 0xF1, 0xFF, 0xCF, 0xFE, 0x7F, 0xF3, 0xC0, 0x1E, 0x00, 0xF0, 0x1F, - 0xF8, 0xFF, 0xC7, 0xFC, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3F, 0xCF, - 0xF3, 0xFC, 0xFF, 0x3C, 0x1F, 0xCF, 0xF9, 0xFC, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7F, 0xCF, 0xFE, - 0xFF, 0xEF, 0xFE, 0xF0, 0x0F, 0x00, 0xF7, 0x0F, 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, - 0xFF, 0x0F, 0xF7, 0xFF, 0x7E, 0x07, 0xC0, 0x60, 0xFF, 0x1F, 0xE3, 0xFC, 0x7F, 0x8F, 0x01, 0xEC, - 0x3F, 0xE7, 0xFE, 0xF3, 0xDE, 0x3B, 0xC7, 0x78, 0xE0, 0x3C, 0x1F, 0x03, 0xE0, 0x70, 0x70, 0x30, - 0x38, 0xF0, 0xF0, 0xF1, 0xF1, 0xE3, 0xE1, 0xE3, 0xC7, 0x83, 0xE7, 0x9E, 0x03, 0xCF, 0x3C, 0x03, - 0xDE, 0xF0, 0x07, 0xFF, 0xE0, 0x0F, 0xFF, 0xC0, 0x1F, 0xFF, 0x80, 0x79, 0xE7, 0x80, 0xF3, 0xCF, - 0x03, 0xC7, 0x8F, 0x0F, 0x8F, 0x1F, 0xDE, 0x1E, 0x1F, 0xFC, 0x3C, 0x3F, 0x00, 0x00, 0x1E, 0x00, - 0x00, 0x3C, 0x00, 0x00, 0x78, 0x00, 0x00, 0xE0, 0x71, 0xC3, 0x9F, 0x3C, 0xF1, 0xE7, 0xBE, 0x1E, - 0xF7, 0x81, 0xFF, 0xE0, 0x3F, 0xF8, 0x03, 0xFF, 0x00, 0xFF, 0xF0, 0x3F, 0xFF, 0x07, 0xBD, 0xFD, - 0xE7, 0xBF, 0xFC, 0xF3, 0xF0, 0x00, 0x1E, 0x00, 0x03, 0xC0, 0x00, 0x78, 0x00, 0x0E, 0x07, 0x00, - 0xFF, 0x0F, 0xFC, 0xFB, 0xE7, 0x8F, 0x18, 0x78, 0x03, 0xC0, 0xFC, 0x07, 0xE0, 0x3F, 0x80, 0x3E, - 0xE0, 0xF7, 0x87, 0xBF, 0xF8, 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x1C, 0x00, 0x70, 0x0F, 0x80, 0x30, - 0x00, 0x0E, 0x07, 0xF1, 0xFF, 0x39, 0xE0, 0x3C, 0x1F, 0x03, 0xE0, 0x7E, 0xF3, 0xDF, 0xF9, 0xFF, - 0x1F, 0xC0, 0xE0, 0x1E, 0x00, 0xC0, 0x78, 0x0E, 0x00, 0x60, 0x39, 0xE0, 0xF3, 0xC3, 0xC7, 0x8F, - 0x8F, 0x3E, 0x1E, 0x78, 0x3D, 0xE0, 0x7F, 0xC0, 0xFF, 0x01, 0xFF, 0x03, 0xDF, 0x07, 0x9F, 0x0F, - 0x1E, 0x1E, 0x1F, 0xBC, 0x3F, 0xF8, 0x3F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xE0, 0xE1, - 0xEF, 0x3E, 0xF3, 0xCF, 0x78, 0xFF, 0x8F, 0xF0, 0xFE, 0x0F, 0xF0, 0xF7, 0x8F, 0x7F, 0xF3, 0xFF, - 0x1F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x07, 0x60, 0x3B, 0xC1, 0xEF, 0x0F, 0xBC, 0x3C, 0xFD, 0xE3, - 0xF7, 0x8F, 0xFC, 0x3F, 0xF0, 0xFF, 0xC3, 0xFF, 0x0F, 0xFE, 0x3F, 0x78, 0xFC, 0xF3, 0xC3, 0xCF, - 0x07, 0xBC, 0x1F, 0xE0, 0xF7, 0x87, 0xBC, 0x79, 0xFB, 0xCF, 0xFC, 0x7F, 0xE3, 0xFE, 0x1F, 0xF8, - 0xFF, 0xC7, 0xEF, 0x3C, 0x7D, 0xE1, 0xE0, 0x7F, 0x03, 0xBF, 0xC1, 0xFF, 0xF0, 0xFB, 0xFC, 0x3C, - 0x0F, 0x1E, 0x03, 0xCF, 0x80, 0xF7, 0xC0, 0x3F, 0xE0, 0x0F, 0xF8, 0x03, 0xFF, 0x00, 0xF3, 0xC0, - 0x3C, 0x78, 0x0F, 0x1F, 0x03, 0xC3, 0xC0, 0xF0, 0x78, 0x3C, 0x1F, 0xFC, 0x3F, 0xF9, 0xFF, 0xE7, - 0x87, 0xBC, 0x1F, 0xF0, 0x7F, 0x81, 0xFC, 0x07, 0xF8, 0x1F, 0xF0, 0x7B, 0xE1, 0xE7, 0x87, 0x9F, - 0x60, 0x39, 0xE0, 0x73, 0xC0, 0xE7, 0x81, 0xCF, 0x03, 0x9E, 0x07, 0x3C, 0x0E, 0x7F, 0xFC, 0xFF, - 0xF9, 0xFF, 0xF3, 0xC0, 0xE7, 0x81, 0xCF, 0x03, 0x9E, 0x07, 0xFC, 0x0F, 0xF8, 0x1F, 0x00, 0x1E, - 0x00, 0x3C, 0x00, 0x78, 0x00, 0x70, 0xE0, 0xE7, 0x8F, 0x3C, 0x79, 0xE3, 0xCF, 0x1E, 0x7F, 0xF3, - 0xFF, 0x9F, 0xFC, 0xF1, 0xE7, 0x8F, 0xBC, 0x7F, 0xE3, 0xF0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, - 0x60, 0x3F, 0xEF, 0x03, 0xFE, 0xF0, 0x3F, 0xEF, 0x03, 0xFE, 0xF0, 0x38, 0x0F, 0x03, 0x80, 0xF0, - 0x38, 0x0F, 0xFF, 0x80, 0xFF, 0xF8, 0x0F, 0xFF, 0x80, 0xF0, 0x38, 0x0F, 0x03, 0x80, 0xF0, 0x38, - 0x0F, 0x03, 0x80, 0xF0, 0x38, 0x0F, 0x03, 0x80, 0xE0, 0xFB, 0xC7, 0xFF, 0x1F, 0xFC, 0x78, 0xF1, - 0xE3, 0xFF, 0x8F, 0xFE, 0x3F, 0xF8, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x78, 0x7F, 0xF8, 0x03, - 0xFF, 0xE0, 0x0F, 0xFF, 0x80, 0x3F, 0xFE, 0x00, 0xF0, 0x38, 0x03, 0xC0, 0xE0, 0x0F, 0x03, 0xBC, - 0x3C, 0x0F, 0xFC, 0xF0, 0x3F, 0xFB, 0xC0, 0xFF, 0xEF, 0x03, 0xC7, 0xFC, 0x0F, 0x0F, 0xF0, 0x38, - 0x3F, 0xC0, 0xE1, 0xFF, 0x03, 0xBF, 0xBC, 0x0E, 0xFE, 0x00, 0x03, 0xE0, 0x00, 0x04, 0x00, 0xFF, - 0xC0, 0x3F, 0xF8, 0x0F, 0xFE, 0x03, 0xFF, 0x80, 0xF1, 0xE0, 0x3C, 0x7F, 0x8F, 0x1F, 0xFB, 0xC7, - 0xFE, 0xF1, 0xE3, 0xFC, 0x78, 0xFF, 0x1E, 0x3F, 0xC7, 0x8F, 0x00, 0x0F, 0x80, 0x03, 0xE0, 0x00, - 0x70, 0x00, 0x00, 0x03, 0x00, 0x00, 0x3C, 0x00, 0x01, 0xF1, 0xF8, 0x0F, 0xCF, 0xF0, 0x7C, 0x7F, - 0xE1, 0xE1, 0xE7, 0xC7, 0x8F, 0x0F, 0x1C, 0x3C, 0x3C, 0x70, 0xF0, 0xF1, 0xC3, 0xC3, 0xC7, 0x8F, - 0x9F, 0x1E, 0x1E, 0xF8, 0x7C, 0x7F, 0xC0, 0xFF, 0xFF, 0xE1, 0xFF, 0xFF, 0x83, 0xFF, 0xFE, 0x01, - 0xC0, 0xE0, 0x06, 0x00, 0x0F, 0x00, 0x0F, 0xBF, 0x0F, 0xBF, 0xC7, 0x9F, 0xE7, 0x9E, 0x7B, 0xCF, - 0x3D, 0xE7, 0x9E, 0x79, 0xFE, 0x3F, 0xFF, 0x8F, 0xFF, 0xE3, 0xFF, 0xF0, 0x38, 0x60, 0x03, 0xC0, - 0x1F, 0xE0, 0x7F, 0xE1, 0xFF, 0xE7, 0xC3, 0xEF, 0x03, 0xDE, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, - 0x01, 0xE0, 0x73, 0xC0, 0xF7, 0xC3, 0xC7, 0xFF, 0x87, 0xFE, 0x07, 0xF8, 0x01, 0x80, 0x03, 0xC0, - 0x03, 0x80, 0x1F, 0x00, 0x18, 0x00, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDE, 0x78, 0xFF, 0x00, 0xF0, - 0x0F, 0x00, 0x78, 0xF7, 0xFE, 0x3F, 0xE1, 0xFC, 0x07, 0x00, 0x78, 0x01, 0x80, 0x78, 0x07, 0x00, - 0x7F, 0xFB, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, - 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x01, 0xC0, 0x07, 0x00, - 0x1C, 0x00, 0x70, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xE1, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, - 0xF0, 0x7E, 0x0F, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x38, 0x70, 0x1D, 0xF0, 0x7D, 0xE0, 0xF1, 0xE3, - 0xC3, 0xC7, 0x83, 0xDE, 0x07, 0xFC, 0x07, 0xF0, 0x07, 0xE0, 0x0F, 0x80, 0x0F, 0x00, 0x1C, 0x00, - 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0xE0, 0x7F, 0x0F, 0x70, 0xF7, 0x8E, 0x79, 0xE3, 0x9E, - 0x3D, 0xC3, 0xFC, 0x1F, 0xC1, 0xF8, 0x1F, 0x80, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x07, - 0x00, 0x70, 0x1D, 0xF0, 0x7D, 0xE0, 0xF1, 0xE3, 0xC3, 0xC7, 0x83, 0xDE, 0x07, 0xFC, 0x07, 0xF0, - 0x07, 0xE0, 0x0F, 0x80, 0x0F, 0x00, 0xFF, 0x81, 0xFF, 0x03, 0xFE, 0x00, 0xE0, 0x01, 0xC0, 0xE0, - 0x7F, 0x0F, 0x70, 0xF7, 0x8E, 0x79, 0xE3, 0x9E, 0x3D, 0xC3, 0xFC, 0x1F, 0xC1, 0xF8, 0x1F, 0x83, - 0xFC, 0x3F, 0xC3, 0xFC, 0x0F, 0x00, 0xF0, 0x07, 0x00, 0x70, 0x1C, 0x78, 0x3C, 0x3C, 0x7C, 0x3E, - 0x78, 0x1E, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x07, 0xC0, 0x07, 0xE0, 0x0F, 0xE0, 0x1E, - 0xF0, 0x3E, 0xF8, 0x3C, 0x7F, 0x78, 0x3F, 0xF8, 0x3F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, - 0x07, 0x70, 0xE3, 0xCF, 0x1E, 0x78, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x7C, 0x03, 0xF0, 0x3F, 0xC1, - 0xDF, 0xDE, 0x7F, 0xE1, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0E, 0x7F, 0xF1, 0x8F, 0xFF, 0xBC, - 0xFF, 0xFB, 0xCF, 0xFF, 0xBC, 0x07, 0x83, 0xC0, 0x78, 0x3C, 0x07, 0x83, 0xC0, 0x78, 0x3C, 0x07, - 0x83, 0xC0, 0x78, 0x3C, 0x07, 0x83, 0xC0, 0x78, 0x3C, 0x07, 0x83, 0xC0, 0x7F, 0xFF, 0x07, 0xFF, - 0xF0, 0x7F, 0xFF, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0E, 0xFF, 0x39, 0xFE, - 0xF3, 0xFD, 0xE7, 0xF3, 0xC3, 0xC7, 0x87, 0x8F, 0x0F, 0x1E, 0x1E, 0x3C, 0x3C, 0x78, 0x7F, 0xF8, - 0xFF, 0xF9, 0xFF, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x60, 0x73, 0xC1, 0xEF, 0x07, - 0xBC, 0x1E, 0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0xBE, 0x3E, 0x7F, 0xF9, 0xFF, 0xE1, 0xF7, 0x80, 0x1E, - 0x00, 0x78, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x07, 0xE1, 0xCF, - 0x1E, 0xF1, 0xEF, 0x1E, 0xF1, 0xEF, 0x1E, 0xFF, 0xE7, 0xFE, 0x3F, 0xE0, 0x1F, 0x01, 0xF0, 0x1F, - 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x07, 0x60, 0x77, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xFB, 0x3F, - 0xDD, 0xFF, 0xFF, 0x7F, 0xFB, 0xFF, 0xC7, 0xDE, 0x0E, 0xF0, 0x77, 0x80, 0x3C, 0x01, 0xE0, 0x0F, - 0xE1, 0xDE, 0x3F, 0xC7, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0x3F, 0xE1, 0xFC, 0x3F, 0x80, - 0xF0, 0x60, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0x1F, 0xFC, 0xFF, 0xF7, - 0x87, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0x60, 0x1E, 0x03, 0xC0, 0x78, - 0x0F, 0x71, 0xFF, 0xBF, 0xF7, 0xFF, 0xF1, 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, 0xE3, 0xFC, 0x7F, - 0x8F, 0x00, 0x78, 0x00, 0x7F, 0x80, 0x3F, 0xF0, 0x1F, 0xFE, 0x0F, 0x87, 0xBB, 0xC0, 0xFE, 0xF0, - 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xF0, 0xF0, 0x00, 0x3C, 0x0F, 0x0F, 0x87, 0x81, 0xFF, - 0xE0, 0x3F, 0xF0, 0x07, 0xF8, 0x00, 0x30, 0x00, 0x00, 0xE0, 0x07, 0xF0, 0x1F, 0xF7, 0x7C, 0xFE, - 0xF1, 0xFF, 0xFF, 0xDF, 0xFF, 0x87, 0xFF, 0x0F, 0x0C, 0x1F, 0x7C, 0x1F, 0xF0, 0x1F, 0xC0, 0x06, - 0x00, 0x00, 0x78, 0x00, 0x7F, 0x80, 0x3F, 0xF0, 0x1F, 0xFE, 0x0F, 0x87, 0xBB, 0xC0, 0xFE, 0xF0, - 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xF0, 0xF0, 0x00, 0x3C, 0x0F, 0x0F, 0x87, 0x81, 0xFF, - 0xE0, 0x3F, 0xF0, 0x07, 0xF8, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xC0, 0x00, 0xE0, - 0x07, 0xF0, 0x1F, 0xF7, 0x7C, 0xFE, 0xF1, 0xFF, 0xFF, 0xDF, 0xFF, 0x87, 0xFF, 0x0F, 0x0C, 0x1F, - 0x7C, 0x1F, 0xF0, 0x1F, 0xC0, 0x0E, 0x00, 0x1E, 0x00, 0x0C, 0x00, 0x78, 0x00, 0xE0, 0x6F, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xCE, 0x00, 0x07, 0xF8, 0x00, 0x0F, 0xC0, 0x00, 0x0C, - 0x00, 0x70, 0x30, 0x39, 0xE1, 0xE1, 0xE7, 0xC7, 0x8F, 0x8F, 0x1E, 0x3C, 0x3E, 0x79, 0xE0, 0x79, - 0xE7, 0x80, 0xF7, 0xBC, 0x03, 0xFF, 0xF0, 0x0F, 0xFF, 0xC0, 0x3F, 0xFF, 0x01, 0xE7, 0x9E, 0x07, - 0x9E, 0x78, 0x3C, 0x78, 0xF1, 0xF1, 0xE3, 0xC7, 0x87, 0x87, 0xBE, 0x1E, 0x1F, 0x07, 0x38, 0x01, - 0xFE, 0x00, 0x3F, 0x00, 0x07, 0x00, 0x71, 0xC3, 0xBE, 0x79, 0xE7, 0x9E, 0xF8, 0xF7, 0xBC, 0x1F, - 0xFE, 0x07, 0xFF, 0x00, 0xFF, 0xC0, 0x7F, 0xF8, 0x3F, 0xFF, 0x0F, 0x7B, 0xC7, 0x9E, 0xFB, 0xE7, - 0x9F, 0x60, 0x77, 0x87, 0xBC, 0x7D, 0xE7, 0xCF, 0x3C, 0x7B, 0xE3, 0xFE, 0x1F, 0xE0, 0xFF, 0xC7, - 0xFF, 0xBC, 0x7D, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0x00, 0xF0, 0x1F, 0x80, 0xF8, - 0x07, 0x00, 0xE1, 0xFE, 0x7F, 0xCF, 0x7B, 0xCF, 0xF9, 0xFE, 0x3F, 0xE7, 0xFE, 0xF1, 0xFE, 0x1F, - 0xC3, 0xF8, 0xF0, 0x3E, 0x07, 0x80, 0xE0, 0x00, 0x0F, 0xFC, 0x07, 0xFF, 0x07, 0xFF, 0x83, 0xFF, - 0xC1, 0xE1, 0xE0, 0xF0, 0xF0, 0x78, 0x78, 0x3C, 0x3C, 0x1E, 0x1E, 0x0F, 0x0F, 0x07, 0x87, 0x83, - 0x83, 0xC3, 0xC1, 0xE7, 0xE0, 0xFB, 0xE0, 0x7D, 0xE0, 0x3E, 0x00, 0x0F, 0x00, 0x07, 0x00, 0x07, - 0x80, 0x01, 0x80, 0x3F, 0xE0, 0xFF, 0xC3, 0xFF, 0x0F, 0xFC, 0x3C, 0xF0, 0xF3, 0xC3, 0xCF, 0x0E, - 0x3C, 0x78, 0xF3, 0xE3, 0xEF, 0x8F, 0xBC, 0x3E, 0x00, 0x78, 0x01, 0xC0, 0x0F, 0x00, 0x18, 0x60, - 0x3B, 0xC0, 0xEF, 0x03, 0xBC, 0x0E, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, 0xBF, 0xFE, 0xFF, 0xFB, 0xFF, - 0xEF, 0x03, 0xBC, 0x0E, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, 0xFC, 0x0F, 0x00, 0x38, 0x01, 0xE0, 0x1F, - 0x80, 0x7E, 0x00, 0xE0, 0xE0, 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, - 0xFE, 0x3F, 0xC7, 0xF8, 0xF0, 0x1E, 0x03, 0xC1, 0xF8, 0x3E, 0x07, 0x80, 0x60, 0x38, 0xF0, 0x38, - 0xF0, 0x38, 0xF0, 0x38, 0xF0, 0x38, 0xF0, 0x38, 0xF0, 0x38, 0xFF, 0xF8, 0xFF, 0xF8, 0xFF, 0xF8, - 0xF0, 0x38, 0xF0, 0x38, 0xF0, 0x38, 0xF0, 0x3E, 0xF0, 0x3E, 0xF0, 0x3E, 0x00, 0x1E, 0x00, 0x1C, - 0x00, 0x3C, 0x00, 0x38, 0xE0, 0xE7, 0x8F, 0x3C, 0x79, 0xE3, 0xCF, 0x1E, 0x7F, 0xF3, 0xFF, 0x9F, - 0xFC, 0xF1, 0xE7, 0x8F, 0xFC, 0x7F, 0xE3, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0x80, 0x1C, 0x60, 0x73, - 0xC1, 0xEF, 0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0xBE, 0x3E, 0x7F, 0xF9, 0xFF, 0xE1, - 0xF7, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, - 0x07, 0xE1, 0xCF, 0x1E, 0xF1, 0xEF, 0x1E, 0xF1, 0xEF, 0x1E, 0xFF, 0xE7, 0xFE, 0x3F, 0xE0, 0x1F, - 0x01, 0xF0, 0x1F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x07, 0x78, 0x0F, 0x9F, 0x81, 0xF3, 0xF0, 0x7E, - 0x7E, 0x0F, 0xCF, 0xE1, 0xF9, 0xFC, 0x7F, 0x3F, 0x8F, 0xE7, 0xFB, 0xFC, 0xFF, 0x7F, 0x9E, 0xEF, - 0xF3, 0xDF, 0xDE, 0x7B, 0xFB, 0xCF, 0x3F, 0x79, 0xE7, 0xCF, 0xFC, 0xF9, 0xFF, 0x8E, 0x3F, 0x00, - 0x01, 0xC0, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x01, 0xC0, 0xF8, 0x3C, 0x7E, 0x3F, 0x3F, 0x1F, 0x9F, - 0x8F, 0xCF, 0xEF, 0xE7, 0xF7, 0xF3, 0xFB, 0xF9, 0xFF, 0xFC, 0xFF, 0xDE, 0x7B, 0xEF, 0xFD, 0xF7, - 0xFE, 0x73, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x38, 0x00, 0x1C, 0x6F, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x0C, 0x60, 0x1F, 0xC0, 0x1F, 0x80, 0x1C, 0x00, 0x38, 0x00, 0xF8, 0x01, 0xF8, - 0x07, 0xF0, 0x0F, 0xE0, 0x1D, 0xE0, 0x7B, 0xC0, 0xF7, 0x81, 0xE7, 0x87, 0x8F, 0x0F, 0xFE, 0x1F, - 0xFE, 0x7F, 0xFC, 0xF0, 0x79, 0xE0, 0x7F, 0x80, 0xF0, 0x39, 0xC7, 0xF8, 0x7E, 0x03, 0x00, 0xF0, - 0x7F, 0x9F, 0xFB, 0xCF, 0x01, 0xE3, 0xFD, 0xFF, 0xBE, 0xFF, 0x1F, 0xE7, 0xDF, 0xFB, 0xFF, 0x18, - 0x00, 0x0E, 0x60, 0x1D, 0xE0, 0x3B, 0xC0, 0x23, 0x00, 0x38, 0x00, 0xF8, 0x01, 0xF8, 0x07, 0xF0, - 0x0F, 0xE0, 0x1D, 0xE0, 0x7B, 0xC0, 0xF7, 0x81, 0xE7, 0x87, 0x8F, 0x0F, 0xFE, 0x1F, 0xFE, 0x7F, - 0xFC, 0xF0, 0x79, 0xE0, 0x7F, 0x80, 0xF0, 0x39, 0xC7, 0xF8, 0xEF, 0x08, 0x40, 0xF0, 0x7F, 0x9F, - 0xFB, 0xCF, 0x01, 0xE3, 0xFD, 0xFF, 0xBE, 0xFF, 0x1F, 0xE7, 0xDF, 0xFB, 0xFF, 0x18, 0x00, 0x01, - 0xFF, 0xF0, 0x1F, 0xFF, 0xC0, 0xFF, 0xFE, 0x0F, 0x7F, 0xE0, 0x7B, 0x80, 0x03, 0xDC, 0x00, 0x3C, - 0xE0, 0x01, 0xE7, 0xFE, 0x1E, 0x3F, 0xF0, 0xF1, 0xFF, 0x8F, 0xFE, 0x00, 0x7F, 0xF0, 0x03, 0xFF, - 0x80, 0x3C, 0x1F, 0xFD, 0xE0, 0xFF, 0xFE, 0x07, 0xFF, 0x0F, 0x1E, 0x07, 0xFF, 0xE1, 0xFF, 0xFE, - 0x3C, 0xF9, 0xE0, 0x1E, 0x1C, 0x3F, 0xFF, 0x9F, 0xFF, 0xF3, 0xFF, 0xFE, 0xF1, 0xE1, 0xDE, 0x7E, - 0x79, 0xFF, 0xFF, 0x3F, 0xBF, 0xC0, 0xC0, 0xC0, 0x31, 0x87, 0xF0, 0x7C, 0x07, 0x07, 0xFD, 0xFF, - 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x7F, 0xEF, 0xFD, 0xFF, 0xBC, 0x07, 0x80, 0xF0, 0x1F, - 0xFF, 0xFF, 0xFF, 0xF0, 0x39, 0xC1, 0xFC, 0x1F, 0x80, 0x70, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, - 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x03, 0x80, - 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xE7, 0x83, 0xCF, 0x07, 0xC0, 0x07, 0xBF, 0xFF, 0x7F, 0xFE, 0xFF, - 0xFD, 0xC0, 0x7B, 0xC1, 0xF7, 0xC7, 0xC7, 0xFF, 0x87, 0xFE, 0x07, 0xF0, 0x03, 0x80, 0x0F, 0x03, - 0xFC, 0x7F, 0xE7, 0x9E, 0x00, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0x70, 0xF7, 0xFE, 0x3F, 0xC1, 0xF8, - 0x06, 0x00, 0x0E, 0xE0, 0x3D, 0xE0, 0x3B, 0xC0, 0x22, 0x00, 0x38, 0x03, 0xFE, 0x0F, 0xFE, 0x3F, - 0xFE, 0x78, 0x3C, 0xF0, 0x7C, 0x00, 0x7B, 0xFF, 0xF7, 0xFF, 0xEF, 0xFF, 0xDC, 0x07, 0xBC, 0x1F, - 0x7C, 0x7C, 0x7F, 0xF8, 0x7F, 0xE0, 0x7F, 0x00, 0x38, 0x00, 0x19, 0xC3, 0xDC, 0x3D, 0xC1, 0x88, - 0x0F, 0x03, 0xFC, 0x7F, 0xE7, 0x9E, 0x00, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0x70, 0xF7, 0xFE, 0x3F, - 0xC1, 0xF8, 0x06, 0x00, 0x01, 0xCE, 0x00, 0x07, 0xF8, 0x00, 0x1D, 0xE0, 0x00, 0x21, 0x00, 0x70, - 0x30, 0x39, 0xE1, 0xE1, 0xE7, 0xC7, 0x8F, 0x8F, 0x1E, 0x3C, 0x3E, 0x79, 0xE0, 0x79, 0xE7, 0x80, - 0xF7, 0xBC, 0x03, 0xFF, 0xF0, 0x0F, 0xFF, 0xC0, 0x3F, 0xFF, 0x01, 0xE7, 0x9E, 0x07, 0x9E, 0x78, - 0x3C, 0x78, 0xF1, 0xF1, 0xE3, 0xC7, 0x87, 0x87, 0xBE, 0x1E, 0x1F, 0x07, 0x38, 0x01, 0xFE, 0x00, - 0x77, 0x80, 0x08, 0x40, 0x71, 0xC3, 0xBE, 0x79, 0xE7, 0x9E, 0xF8, 0xF7, 0xBC, 0x1F, 0xFE, 0x07, - 0xFF, 0x00, 0xFF, 0xC0, 0x7F, 0xF8, 0x3F, 0xFF, 0x0F, 0x7B, 0xC7, 0x9E, 0xFB, 0xE7, 0x9F, 0x1C, - 0xC0, 0xEF, 0x07, 0x78, 0x11, 0x80, 0x70, 0x0F, 0xF0, 0xFF, 0xCF, 0xBE, 0x78, 0xF1, 0x87, 0x80, - 0x3C, 0x0F, 0xC0, 0x7E, 0x03, 0xF8, 0x03, 0xEE, 0x0F, 0x78, 0x7B, 0xFF, 0x8F, 0xFC, 0x3F, 0xC0, - 0x70, 0x00, 0x3B, 0x8F, 0x71, 0xEE, 0x08, 0x80, 0xE0, 0x7F, 0x1F, 0xF3, 0x9E, 0x03, 0xC1, 0xF0, - 0x3E, 0x07, 0xEF, 0x3D, 0xFF, 0x9F, 0xF1, 0xFC, 0x04, 0x00, 0x3F, 0xE3, 0xFF, 0x9F, 0xFC, 0x7F, + 0xC0, 0x77, 0x03, 0xFC, 0x0E, 0xE0, 0x37, 0x0E, 0x03, 0x78, 0x7D, 0xC3, 0xEF, 0x3E, 0x79, 0xE1, + 0xCE, 0x0F, 0xF0, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x7E, 0x01, 0xE0, 0x03, 0xC0, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x7C, 0x3E, 0xE0, 0x77, 0x81, 0xEF, 0x0F, + 0x78, 0x1E, 0xF0, 0xF7, 0x01, 0xE7, 0x9E, 0x70, 0x0E, 0x79, 0xE7, 0x01, 0xE3, 0x9E, 0x78, 0x1E, + 0x3F, 0xC7, 0x81, 0xE3, 0xFC, 0x7C, 0x3E, 0x1F, 0x83, 0xFF, 0xC1, 0xF8, 0x1F, 0xF8, 0x1F, 0x80, + 0xFF, 0x00, 0xF0, 0x01, 0x80, 0x0F, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x07, + 0xC0, 0x00, 0x00, 0x78, 0x00, 0x0F, 0x07, 0x07, 0x0F, 0xE7, 0x87, 0x8F, 0xF9, 0xE3, 0xCF, 0xBE, + 0xF1, 0xE7, 0x8F, 0x39, 0xE7, 0x87, 0x9E, 0xF3, 0xC3, 0xCF, 0x71, 0xE1, 0xE3, 0xF8, 0x78, 0xF1, + 0xFC, 0x3F, 0xF8, 0xFC, 0x0F, 0xF8, 0x3E, 0x03, 0xF8, 0x1F, 0x00, 0x60, 0x0F, 0x00, 0x00, 0x1F, + 0x80, 0x00, 0x1F, 0x80, 0x00, 0x0F, 0xC0, 0x00, 0x03, 0x80, 0x00, 0x03, 0xC0, 0x7F, 0xC3, 0xFF, + 0x8F, 0xFF, 0x7C, 0x3D, 0xE0, 0xF7, 0x80, 0x1C, 0x00, 0x70, 0x01, 0xE0, 0x77, 0x83, 0xDF, 0xBF, + 0x3F, 0xFC, 0x7F, 0xF0, 0x7B, 0xC0, 0x0F, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDE, 0x78, 0xFF, 0x00, + 0xF0, 0x0F, 0x00, 0x78, 0x07, 0xF0, 0x3F, 0x01, 0xF0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x00, + 0xE0, 0x0E, 0x1D, 0xC1, 0xF8, 0x07, 0x80, 0x3E, 0x07, 0xFE, 0xE2, 0xFC, 0x03, 0xC0, 0x1F, 0x03, + 0xF8, 0x71, 0x07, 0x00, 0x03, 0xBF, 0xDF, 0xEC, 0x00, 0x1F, 0xFF, 0xE3, 0xDB, 0xB0, 0x6F, 0xE0, + 0x7C, 0x7F, 0xF1, 0xE0, 0x00, 0x70, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x00, 0x3C, 0x03, 0xC3, 0x60, + 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x7E, 0xC0, 0x06, 0xC0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0xF8, 0x0F, 0x86, 0xC0, 0x64, 0x00, 0x70, + 0x00, 0x07, 0xC0, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x60, 0x01, 0x86, 0x00, + 0x18, 0x61, 0xC0, 0xC0, 0x78, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x08, 0xF0, + 0x00, 0xF3, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0xE0, 0x30, 0x3C, 0x63, + 0x83, 0x06, 0x18, 0x00, 0x60, 0x00, 0x04, 0x00, 0x00, 0x40, 0x00, 0x18, 0xC0, 0x3F, 0x80, 0x3F, + 0x00, 0x38, 0x06, 0x03, 0x9E, 0x0F, 0xBC, 0x1F, 0x78, 0x7E, 0xF1, 0xFD, 0xE3, 0xFB, 0xCF, 0xF7, + 0xBD, 0xEF, 0x7B, 0xDF, 0xE7, 0xBF, 0xCF, 0x7F, 0x1E, 0xFC, 0x3D, 0xF8, 0x7F, 0xE0, 0xFF, 0x81, + 0xF0, 0x01, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x1C, 0x31, 0x81, 0xFC, 0x0F, 0xC0, 0x1C, 0x0E, 0x0E, + 0x78, 0xF3, 0xCF, 0x9E, 0x7C, 0xF7, 0xE7, 0xFF, 0x3F, 0xF9, 0xFB, 0xCF, 0xDE, 0x7C, 0xFF, 0xC7, + 0xFE, 0x3E, 0x00, 0xF0, 0x07, 0x80, 0x38, 0x01, 0xC0, 0x18, 0x00, 0xF0, 0x0F, 0xF8, 0x3F, 0xE0, + 0xFF, 0x80, 0xF0, 0x03, 0xFF, 0x0F, 0xFE, 0x3F, 0xFC, 0xF0, 0xF3, 0xC3, 0xCF, 0x0F, 0x3C, 0x3C, + 0xFF, 0xF3, 0xFF, 0x8F, 0xFC, 0x3C, 0x01, 0xE0, 0x3F, 0xE1, 0xFF, 0x8F, 0xFC, 0x1E, 0x00, 0xFF, + 0x07, 0xFE, 0x3F, 0xF1, 0xE3, 0xCF, 0x1E, 0x7F, 0xF3, 0xFF, 0x1F, 0xF0, 0x7E, 0x0F, 0xFC, 0xFF, + 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF6, 0xFF, 0x7F, 0xFF, 0xFF, 0xFE, 0xFF, 0xCF, 0x0E, 0xF0, 0x4F, + 0x00, 0xF0, 0x0F, 0x00, 0xE7, 0x0F, 0xFC, 0xFF, 0xEF, 0xBE, 0xF1, 0xEF, 0x0F, 0xF0, 0xFF, 0x2F, + 0xF7, 0xEF, 0xFE, 0xFF, 0xEF, 0xFC, 0xF2, 0x6F, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x00, 0x01, 0xC0, + 0x3C, 0x07, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, + 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x00, 0x03, 0x07, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0x1F, 0xF1, 0xFF, 0xCF, 0xFE, 0x7F, 0xF3, 0xC0, + 0x1E, 0x00, 0xF0, 0x1F, 0xF8, 0xFF, 0xC7, 0xFC, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, + 0x07, 0x80, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3C, 0x1F, 0xCF, 0xF9, 0xFC, 0x3C, 0x0F, 0x03, 0xC0, + 0xF0, 0x7F, 0xCF, 0xFE, 0xFF, 0xEF, 0xFE, 0xF0, 0x0F, 0x00, 0xF7, 0x0F, 0xFC, 0xFF, 0xEF, 0xFF, + 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF7, 0xFF, 0x7E, 0x07, 0xC0, 0x60, 0xFF, 0x1F, 0xE3, 0xFC, + 0x7F, 0x8F, 0x01, 0xEC, 0x3F, 0xE7, 0xFE, 0xF3, 0xDE, 0x3B, 0xC7, 0x78, 0xE0, 0x3C, 0x1F, 0x03, + 0xE0, 0x70, 0x70, 0x30, 0x38, 0xF0, 0xF0, 0xF1, 0xF1, 0xE3, 0xE1, 0xE3, 0xC7, 0x83, 0xE7, 0x9E, + 0x03, 0xCF, 0x3C, 0x03, 0xDE, 0xF0, 0x07, 0xFF, 0xE0, 0x0F, 0xFF, 0xC0, 0x1F, 0xFF, 0x80, 0x79, + 0xE7, 0x80, 0xF3, 0xCF, 0x03, 0xC7, 0x8F, 0x0F, 0x8F, 0x1F, 0xDE, 0x1E, 0x1F, 0xFC, 0x3C, 0x3F, + 0x00, 0x00, 0x1E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x78, 0x00, 0x00, 0xE0, 0x71, 0xC3, 0x9F, 0x3C, + 0xF1, 0xE7, 0xBE, 0x1E, 0xF7, 0x81, 0xFF, 0xE0, 0x3F, 0xF8, 0x03, 0xFF, 0x00, 0xFF, 0xF0, 0x3F, + 0xFF, 0x07, 0xBD, 0xFD, 0xE7, 0xBF, 0xFC, 0xF3, 0xF0, 0x00, 0x1E, 0x00, 0x03, 0xC0, 0x00, 0x78, + 0x00, 0x0E, 0x07, 0x00, 0xFF, 0x0F, 0xFC, 0xFB, 0xE7, 0x8F, 0x18, 0x78, 0x03, 0xC0, 0xFC, 0x07, + 0xE0, 0x3F, 0x80, 0x3E, 0xE0, 0xF7, 0x87, 0xBF, 0xF8, 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x1C, 0x00, + 0x70, 0x0F, 0x80, 0x30, 0x00, 0x0E, 0x07, 0xF1, 0xFF, 0x39, 0xE0, 0x3C, 0x1F, 0x03, 0xE0, 0x7E, + 0xF3, 0xDF, 0xF9, 0xFF, 0x1F, 0xC0, 0xE0, 0x1E, 0x00, 0xC0, 0x78, 0x0E, 0x00, 0x60, 0x39, 0xE0, + 0xF3, 0xC3, 0xC7, 0x8F, 0x8F, 0x3E, 0x1E, 0x78, 0x3D, 0xE0, 0x7F, 0xC0, 0xFF, 0x01, 0xFF, 0x03, + 0xDF, 0x07, 0x9F, 0x0F, 0x1E, 0x1E, 0x1F, 0xBC, 0x3F, 0xF8, 0x3F, 0x00, 0x1E, 0x00, 0x3C, 0x00, + 0x78, 0x00, 0xE0, 0xE1, 0xEF, 0x3E, 0xF3, 0xCF, 0x78, 0xFF, 0x8F, 0xF0, 0xFE, 0x0F, 0xF0, 0xF7, + 0x8F, 0x7F, 0xF3, 0xFF, 0x1F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x07, 0x60, 0x3B, 0xC1, 0xEF, 0x0F, + 0xBC, 0x3C, 0xFD, 0xE3, 0xF7, 0x8F, 0xFC, 0x3F, 0xF0, 0xFF, 0xC3, 0xFF, 0x0F, 0xFE, 0x3F, 0x78, + 0xFC, 0xF3, 0xC3, 0xCF, 0x07, 0xBC, 0x1F, 0xE0, 0xF7, 0x87, 0xBC, 0x79, 0xFB, 0xCF, 0xFC, 0x7F, + 0xE3, 0xFE, 0x1F, 0xF8, 0xFF, 0xC7, 0xEF, 0x3C, 0x7D, 0xE1, 0xE0, 0x7F, 0x03, 0xBF, 0xC1, 0xFF, + 0xF0, 0xFB, 0xFC, 0x3C, 0x0F, 0x1E, 0x03, 0xCF, 0x80, 0xF7, 0xC0, 0x3F, 0xE0, 0x0F, 0xF8, 0x03, + 0xFF, 0x00, 0xF3, 0xC0, 0x3C, 0x78, 0x0F, 0x1F, 0x03, 0xC3, 0xC0, 0xF0, 0x78, 0x3C, 0x1F, 0xFC, + 0x3F, 0xF9, 0xFF, 0xE7, 0x87, 0xBC, 0x1F, 0xF0, 0x7F, 0x81, 0xFC, 0x07, 0xF8, 0x1F, 0xF0, 0x7B, + 0xE1, 0xE7, 0x87, 0x9F, 0x60, 0x39, 0xE0, 0x73, 0xC0, 0xE7, 0x81, 0xCF, 0x03, 0x9E, 0x07, 0x3C, + 0x0E, 0x7F, 0xFC, 0xFF, 0xF9, 0xFF, 0xF3, 0xC0, 0xE7, 0x81, 0xCF, 0x03, 0x9E, 0x07, 0xFC, 0x0F, + 0xF8, 0x1F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x00, 0x70, 0xE0, 0xE7, 0x8F, 0x3C, 0x79, 0xE3, + 0xCF, 0x1E, 0x7F, 0xF3, 0xFF, 0x9F, 0xFC, 0xF1, 0xE7, 0x8F, 0xBC, 0x7F, 0xE3, 0xF0, 0x07, 0x00, + 0x38, 0x01, 0xC0, 0x0E, 0x60, 0x3F, 0xEF, 0x03, 0xFE, 0xF0, 0x3F, 0xEF, 0x03, 0xFE, 0xF0, 0x38, + 0x0F, 0x03, 0x80, 0xF0, 0x38, 0x0F, 0xFF, 0x80, 0xFF, 0xF8, 0x0F, 0xFF, 0x80, 0xF0, 0x38, 0x0F, + 0x03, 0x80, 0xF0, 0x38, 0x0F, 0x03, 0x80, 0xF0, 0x38, 0x0F, 0x03, 0x80, 0xE0, 0xFB, 0xC7, 0xFF, + 0x1F, 0xFC, 0x78, 0xF1, 0xE3, 0xFF, 0x8F, 0xFE, 0x3F, 0xF8, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, + 0x78, 0x7F, 0xF8, 0x03, 0xFF, 0xE0, 0x0F, 0xFF, 0x80, 0x3F, 0xFE, 0x00, 0xF0, 0x38, 0x03, 0xC0, + 0xE0, 0x0F, 0x03, 0xBC, 0x3C, 0x0F, 0xFC, 0xF0, 0x3F, 0xFB, 0xC0, 0xFF, 0xEF, 0x03, 0xC7, 0xFC, + 0x0F, 0x0F, 0xF0, 0x38, 0x3F, 0xC0, 0xE1, 0xFF, 0x03, 0xBF, 0xBC, 0x0E, 0xFE, 0x00, 0x03, 0xE0, + 0x00, 0x04, 0x00, 0xFF, 0xC0, 0x3F, 0xF8, 0x0F, 0xFE, 0x03, 0xFF, 0x80, 0xF1, 0xE0, 0x3C, 0x7F, + 0x8F, 0x1F, 0xFB, 0xC7, 0xFE, 0xF1, 0xE3, 0xFC, 0x78, 0xFF, 0x1E, 0x3F, 0xC7, 0x8F, 0x00, 0x0F, + 0x80, 0x03, 0xE0, 0x00, 0x70, 0x00, 0x00, 0x03, 0x00, 0x00, 0x3C, 0x00, 0x01, 0xF1, 0xF8, 0x0F, + 0xCF, 0xF0, 0x7C, 0x7F, 0xE1, 0xE1, 0xE7, 0xC7, 0x8F, 0x0F, 0x1C, 0x3C, 0x3C, 0x70, 0xF0, 0xF1, + 0xC3, 0xC3, 0xC7, 0x8F, 0x9F, 0x1E, 0x1E, 0xF8, 0x7C, 0x7F, 0xC0, 0xFF, 0xFF, 0xE1, 0xFF, 0xFF, + 0x83, 0xFF, 0xFE, 0x01, 0xC0, 0xE0, 0x06, 0x00, 0x0F, 0x00, 0x0F, 0xBF, 0x0F, 0xBF, 0xC7, 0x9F, + 0xE7, 0x9E, 0x7B, 0xCF, 0x3D, 0xE7, 0x9E, 0x79, 0xFE, 0x3F, 0xFF, 0x8F, 0xFF, 0xE3, 0xFF, 0xF0, + 0x38, 0x60, 0x03, 0xC0, 0x1F, 0xE0, 0x7F, 0xE1, 0xFF, 0xE7, 0xC3, 0xEF, 0x03, 0xDE, 0x00, 0x38, + 0x00, 0x70, 0x00, 0xE0, 0x01, 0xE0, 0x73, 0xC0, 0xF7, 0xC3, 0xC7, 0xFF, 0x87, 0xFE, 0x07, 0xF8, + 0x01, 0x80, 0x03, 0xC0, 0x03, 0x80, 0x1F, 0x00, 0x18, 0x00, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDE, + 0x78, 0xFF, 0x00, 0xF0, 0x0F, 0x00, 0x78, 0xF7, 0xFE, 0x3F, 0xE1, 0xFC, 0x07, 0x00, 0x78, 0x01, + 0x80, 0x78, 0x07, 0x00, 0x7F, 0xFB, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0x07, 0x80, 0x1E, 0x00, 0x78, + 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x01, 0xF0, + 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x70, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xE1, 0xE0, 0x3C, 0x07, + 0x80, 0xF0, 0x1E, 0x03, 0xF0, 0x7E, 0x0F, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x38, 0x70, 0x1D, 0xF0, + 0x7D, 0xE0, 0xF1, 0xE3, 0xC3, 0xC7, 0x83, 0xDE, 0x07, 0xFC, 0x07, 0xF0, 0x07, 0xE0, 0x0F, 0x80, + 0x0F, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0xE0, 0x7F, 0x0F, 0x70, 0xF7, + 0x8E, 0x79, 0xE3, 0x9E, 0x3D, 0xC3, 0xFC, 0x1F, 0xC1, 0xF8, 0x1F, 0x80, 0xF0, 0x0F, 0x00, 0xF0, + 0x0F, 0x00, 0xF0, 0x07, 0x00, 0x70, 0x1D, 0xF0, 0x7D, 0xE0, 0xF1, 0xE3, 0xC3, 0xC7, 0x83, 0xDE, + 0x07, 0xFC, 0x07, 0xF0, 0x07, 0xE0, 0x0F, 0x80, 0x0F, 0x00, 0xFF, 0x81, 0xFF, 0x03, 0xFE, 0x00, + 0xE0, 0x01, 0xC0, 0xE0, 0x7F, 0x0F, 0x70, 0xF7, 0x8E, 0x79, 0xE3, 0x9E, 0x3D, 0xC3, 0xFC, 0x1F, + 0xC1, 0xF8, 0x1F, 0x83, 0xFC, 0x3F, 0xC3, 0xFC, 0x0F, 0x00, 0xF0, 0x07, 0x00, 0x70, 0x1C, 0x78, + 0x3C, 0x3C, 0x7C, 0x3E, 0x78, 0x1E, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x07, 0xC0, 0x07, + 0xE0, 0x0F, 0xE0, 0x1E, 0xF0, 0x3E, 0xF8, 0x3C, 0x7F, 0x78, 0x3F, 0xF8, 0x3F, 0x00, 0x0F, 0x00, + 0x0F, 0x00, 0x0F, 0x00, 0x07, 0x70, 0xE3, 0xCF, 0x1E, 0x78, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x7C, + 0x03, 0xF0, 0x3F, 0xC1, 0xDF, 0xDE, 0x7F, 0xE1, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0E, 0x7F, + 0xF1, 0x8F, 0xFF, 0xBC, 0xFF, 0xFB, 0xCF, 0xFF, 0xBC, 0x07, 0x83, 0xC0, 0x78, 0x3C, 0x07, 0x83, + 0xC0, 0x78, 0x3C, 0x07, 0x83, 0xC0, 0x78, 0x3C, 0x07, 0x83, 0xC0, 0x78, 0x3C, 0x07, 0x83, 0xC0, + 0x7F, 0xFF, 0x07, 0xFF, 0xF0, 0x7F, 0xFF, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, + 0x0E, 0xFF, 0x39, 0xFE, 0xF3, 0xFD, 0xE7, 0xF3, 0xC3, 0xC7, 0x87, 0x8F, 0x0F, 0x1E, 0x1E, 0x3C, + 0x3C, 0x78, 0x7F, 0xF8, 0xFF, 0xF9, 0xFF, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x60, + 0x73, 0xC1, 0xEF, 0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0xBE, 0x3E, 0x7F, 0xF9, 0xFF, + 0xE1, 0xF7, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0xF0, 0x03, + 0xC0, 0x07, 0xE1, 0xCF, 0x1E, 0xF1, 0xEF, 0x1E, 0xF1, 0xEF, 0x1E, 0xFF, 0xE7, 0xFE, 0x3F, 0xE0, + 0x1F, 0x01, 0xF0, 0x1F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x07, 0x60, 0x77, 0x83, 0xFC, 0x1F, 0xE0, + 0xFF, 0x07, 0xFB, 0x3F, 0xDD, 0xFF, 0xFF, 0x7F, 0xFB, 0xFF, 0xC7, 0xDE, 0x0E, 0xF0, 0x77, 0x80, + 0x3C, 0x01, 0xE0, 0x0F, 0xE1, 0xDE, 0x3F, 0xC7, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0x3F, + 0xE1, 0xFC, 0x3F, 0x80, 0xF0, 0x60, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, + 0x1F, 0xFC, 0xFF, 0xF7, 0x87, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0x60, + 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x71, 0xFF, 0xBF, 0xF7, 0xFF, 0xF1, 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, + 0x1F, 0xE3, 0xFC, 0x7F, 0x8F, 0x00, 0x78, 0x00, 0x7F, 0x80, 0x3F, 0xF0, 0x1F, 0xFE, 0x0F, 0x87, + 0xBB, 0xC0, 0xFE, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xF0, 0xF0, 0x00, 0x3C, 0x0F, + 0x0F, 0x87, 0x81, 0xFF, 0xE0, 0x3F, 0xF0, 0x07, 0xF8, 0x00, 0x30, 0x00, 0x00, 0xE0, 0x07, 0xF0, + 0x1F, 0xF7, 0x7C, 0xFE, 0xF1, 0xFF, 0xFF, 0xDF, 0xFF, 0x87, 0xFF, 0x0F, 0x0C, 0x1F, 0x7C, 0x1F, + 0xF0, 0x1F, 0xC0, 0x06, 0x00, 0x00, 0x78, 0x00, 0x7F, 0x80, 0x3F, 0xF0, 0x1F, 0xFE, 0x0F, 0x87, + 0xBB, 0xC0, 0xFE, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xF0, 0xF0, 0x00, 0x3C, 0x0F, + 0x0F, 0x87, 0x81, 0xFF, 0xE0, 0x3F, 0xF0, 0x07, 0xF8, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, + 0x01, 0xC0, 0x00, 0xE0, 0x07, 0xF0, 0x1F, 0xF7, 0x7C, 0xFE, 0xF1, 0xFF, 0xFF, 0xDF, 0xFF, 0x87, + 0xFF, 0x0F, 0x0C, 0x1F, 0x7C, 0x1F, 0xF0, 0x1F, 0xC0, 0x0E, 0x00, 0x1E, 0x00, 0x0C, 0x00, 0x78, + 0x00, 0xE0, 0x6F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xCE, 0x00, 0x07, 0xF8, 0x00, + 0x0F, 0xC0, 0x00, 0x0C, 0x00, 0x70, 0x30, 0x39, 0xE1, 0xE1, 0xE7, 0xC7, 0x8F, 0x8F, 0x1E, 0x3C, + 0x3E, 0x79, 0xE0, 0x79, 0xE7, 0x80, 0xF7, 0xBC, 0x03, 0xFF, 0xF0, 0x0F, 0xFF, 0xC0, 0x3F, 0xFF, + 0x01, 0xE7, 0x9E, 0x07, 0x9E, 0x78, 0x3C, 0x78, 0xF1, 0xF1, 0xE3, 0xC7, 0x87, 0x87, 0xBE, 0x1E, + 0x1F, 0x07, 0x38, 0x01, 0xFE, 0x00, 0x3F, 0x00, 0x07, 0x00, 0x71, 0xC3, 0xBE, 0x79, 0xE7, 0x9E, + 0xF8, 0xF7, 0xBC, 0x1F, 0xFE, 0x07, 0xFF, 0x00, 0xFF, 0xC0, 0x7F, 0xF8, 0x3F, 0xFF, 0x0F, 0x7B, + 0xC7, 0x9E, 0xFB, 0xE7, 0x9F, 0x60, 0x77, 0x87, 0xBC, 0x7D, 0xE7, 0xCF, 0x3C, 0x7B, 0xE3, 0xFE, + 0x1F, 0xE0, 0xFF, 0xC7, 0xFF, 0xBC, 0x7D, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0x00, + 0xF0, 0x1F, 0x80, 0xF8, 0x07, 0x00, 0xE1, 0xFE, 0x7F, 0xCF, 0x7B, 0xCF, 0xF9, 0xFE, 0x3F, 0xE7, + 0xFE, 0xF1, 0xFE, 0x1F, 0xC3, 0xF8, 0xF0, 0x3E, 0x07, 0x80, 0xE0, 0x00, 0x0F, 0xFC, 0x07, 0xFF, + 0x07, 0xFF, 0x83, 0xFF, 0xC1, 0xE1, 0xE0, 0xF0, 0xF0, 0x78, 0x78, 0x3C, 0x3C, 0x1E, 0x1E, 0x0F, + 0x0F, 0x07, 0x87, 0x83, 0x83, 0xC3, 0xC1, 0xE7, 0xE0, 0xFB, 0xE0, 0x7D, 0xE0, 0x3E, 0x00, 0x0F, + 0x00, 0x07, 0x00, 0x07, 0x80, 0x01, 0x80, 0x3F, 0xE0, 0xFF, 0xC3, 0xFF, 0x0F, 0xFC, 0x3C, 0xF0, + 0xF3, 0xC3, 0xCF, 0x0E, 0x3C, 0x78, 0xF3, 0xE3, 0xEF, 0x8F, 0xBC, 0x3E, 0x00, 0x78, 0x01, 0xC0, + 0x0F, 0x00, 0x18, 0x60, 0x3B, 0xC0, 0xEF, 0x03, 0xBC, 0x0E, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, 0xBF, + 0xFE, 0xFF, 0xFB, 0xFF, 0xEF, 0x03, 0xBC, 0x0E, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, 0xFC, 0x0F, 0x00, + 0x38, 0x01, 0xE0, 0x1F, 0x80, 0x7E, 0x00, 0xE0, 0xE0, 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, 0xFF, + 0xFF, 0xFF, 0xFF, 0xF1, 0xFE, 0x3F, 0xC7, 0xF8, 0xF0, 0x1E, 0x03, 0xC1, 0xF8, 0x3E, 0x07, 0x80, + 0x60, 0x38, 0xF0, 0x38, 0xF0, 0x38, 0xF0, 0x38, 0xF0, 0x38, 0xF0, 0x38, 0xF0, 0x38, 0xFF, 0xF8, + 0xFF, 0xF8, 0xFF, 0xF8, 0xF0, 0x38, 0xF0, 0x38, 0xF0, 0x38, 0xF0, 0x3E, 0xF0, 0x3E, 0xF0, 0x3E, + 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x38, 0xE0, 0xE7, 0x8F, 0x3C, 0x79, 0xE3, 0xCF, 0x1E, + 0x7F, 0xF3, 0xFF, 0x9F, 0xFC, 0xF1, 0xE7, 0x8F, 0xFC, 0x7F, 0xE3, 0xE0, 0x0F, 0x00, 0x78, 0x03, + 0x80, 0x1C, 0x60, 0x73, 0xC1, 0xEF, 0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0xBE, 0x3E, + 0x7F, 0xF9, 0xFF, 0xE1, 0xF7, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x3C, + 0x00, 0xF0, 0x03, 0xC0, 0x07, 0xE1, 0xCF, 0x1E, 0xF1, 0xEF, 0x1E, 0xF1, 0xEF, 0x1E, 0xFF, 0xE7, + 0xFE, 0x3F, 0xE0, 0x1F, 0x01, 0xF0, 0x1F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x07, 0x78, 0x0F, 0x9F, + 0x81, 0xF3, 0xF0, 0x7E, 0x7E, 0x0F, 0xCF, 0xE1, 0xF9, 0xFC, 0x7F, 0x3F, 0x8F, 0xE7, 0xFB, 0xFC, + 0xFF, 0x7F, 0x9E, 0xEF, 0xF3, 0xDF, 0xDE, 0x7B, 0xFB, 0xCF, 0x3F, 0x79, 0xE7, 0xCF, 0xFC, 0xF9, + 0xFF, 0x8E, 0x3F, 0x00, 0x01, 0xC0, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x01, 0xC0, 0xF8, 0x3C, 0x7E, + 0x3F, 0x3F, 0x1F, 0x9F, 0x8F, 0xCF, 0xEF, 0xE7, 0xF7, 0xF3, 0xFB, 0xF9, 0xFF, 0xFC, 0xFF, 0xDE, + 0x7B, 0xEF, 0xFD, 0xF7, 0xFE, 0x73, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x38, 0x00, 0x1C, 0x6F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0C, 0x60, 0x1F, 0xC0, 0x1F, 0x80, 0x1C, 0x00, 0x38, + 0x00, 0xF8, 0x01, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1D, 0xE0, 0x7B, 0xC0, 0xF7, 0x81, 0xE7, 0x87, + 0x8F, 0x0F, 0xFE, 0x1F, 0xFE, 0x7F, 0xFC, 0xF0, 0x79, 0xE0, 0x7F, 0x80, 0xF0, 0x39, 0xC7, 0xF8, + 0x7E, 0x03, 0x00, 0xF0, 0x7F, 0x9F, 0xFB, 0xCF, 0x01, 0xE3, 0xFD, 0xFF, 0xBE, 0xFF, 0x1F, 0xE7, + 0xDF, 0xFB, 0xFF, 0x18, 0x00, 0x0E, 0x60, 0x1D, 0xE0, 0x3B, 0xC0, 0x23, 0x00, 0x38, 0x00, 0xF8, + 0x01, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1D, 0xE0, 0x7B, 0xC0, 0xF7, 0x81, 0xE7, 0x87, 0x8F, 0x0F, + 0xFE, 0x1F, 0xFE, 0x7F, 0xFC, 0xF0, 0x79, 0xE0, 0x7F, 0x80, 0xF0, 0x39, 0xC7, 0xF8, 0xEF, 0x08, + 0x40, 0xF0, 0x7F, 0x9F, 0xFB, 0xCF, 0x01, 0xE3, 0xFD, 0xFF, 0xBE, 0xFF, 0x1F, 0xE7, 0xDF, 0xFB, + 0xFF, 0x18, 0x00, 0x01, 0xFF, 0xF0, 0x1F, 0xFF, 0xC0, 0xFF, 0xFE, 0x0F, 0x7F, 0xE0, 0x7B, 0x80, + 0x03, 0xDC, 0x00, 0x3C, 0xE0, 0x01, 0xE7, 0xFE, 0x1E, 0x3F, 0xF0, 0xF1, 0xFF, 0x8F, 0xFE, 0x00, + 0x7F, 0xF0, 0x03, 0xFF, 0x80, 0x3C, 0x1F, 0xFD, 0xE0, 0xFF, 0xFE, 0x07, 0xFF, 0x0F, 0x1E, 0x07, + 0xFF, 0xE1, 0xFF, 0xFE, 0x3C, 0xF9, 0xE0, 0x1E, 0x1C, 0x3F, 0xFF, 0x9F, 0xFF, 0xF3, 0xFF, 0xFE, + 0xF1, 0xE1, 0xDE, 0x7E, 0x79, 0xFF, 0xFF, 0x3F, 0xBF, 0xC0, 0xC0, 0xC0, 0x31, 0x87, 0xF0, 0x7C, + 0x07, 0x07, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x7F, 0xEF, 0xFD, 0xFF, 0xBC, + 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF, 0xFF, 0xF0, 0x39, 0xC1, 0xFC, 0x1F, 0x80, 0x70, 0x0F, 0x01, + 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, + 0x06, 0x00, 0x03, 0x80, 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xE7, 0x83, 0xCF, 0x07, 0xC0, 0x07, 0xBF, + 0xFF, 0x7F, 0xFE, 0xFF, 0xFD, 0xC0, 0x7B, 0xC1, 0xF7, 0xC7, 0xC7, 0xFF, 0x87, 0xFE, 0x07, 0xF0, + 0x03, 0x80, 0x0F, 0x03, 0xFC, 0x7F, 0xE7, 0x9E, 0x00, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0x70, 0xF7, + 0xFE, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x0E, 0xE0, 0x3D, 0xE0, 0x3B, 0xC0, 0x22, 0x00, 0x38, 0x03, + 0xFE, 0x0F, 0xFE, 0x3F, 0xFE, 0x78, 0x3C, 0xF0, 0x7C, 0x00, 0x7B, 0xFF, 0xF7, 0xFF, 0xEF, 0xFF, + 0xDC, 0x07, 0xBC, 0x1F, 0x7C, 0x7C, 0x7F, 0xF8, 0x7F, 0xE0, 0x7F, 0x00, 0x38, 0x00, 0x19, 0xC3, + 0xDC, 0x3D, 0xC1, 0x88, 0x0F, 0x03, 0xFC, 0x7F, 0xE7, 0x9E, 0x00, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, + 0x70, 0xF7, 0xFE, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x01, 0xCE, 0x00, 0x07, 0xF8, 0x00, 0x1D, 0xE0, + 0x00, 0x21, 0x00, 0x70, 0x30, 0x39, 0xE1, 0xE1, 0xE7, 0xC7, 0x8F, 0x8F, 0x1E, 0x3C, 0x3E, 0x79, + 0xE0, 0x79, 0xE7, 0x80, 0xF7, 0xBC, 0x03, 0xFF, 0xF0, 0x0F, 0xFF, 0xC0, 0x3F, 0xFF, 0x01, 0xE7, + 0x9E, 0x07, 0x9E, 0x78, 0x3C, 0x78, 0xF1, 0xF1, 0xE3, 0xC7, 0x87, 0x87, 0xBE, 0x1E, 0x1F, 0x07, + 0x38, 0x01, 0xFE, 0x00, 0x77, 0x80, 0x08, 0x40, 0x71, 0xC3, 0xBE, 0x79, 0xE7, 0x9E, 0xF8, 0xF7, + 0xBC, 0x1F, 0xFE, 0x07, 0xFF, 0x00, 0xFF, 0xC0, 0x7F, 0xF8, 0x3F, 0xFF, 0x0F, 0x7B, 0xC7, 0x9E, + 0xFB, 0xE7, 0x9F, 0x1C, 0xC0, 0xEF, 0x07, 0x78, 0x11, 0x80, 0x70, 0x0F, 0xF0, 0xFF, 0xCF, 0xBE, + 0x78, 0xF1, 0x87, 0x80, 0x3C, 0x0F, 0xC0, 0x7E, 0x03, 0xF8, 0x03, 0xEE, 0x0F, 0x78, 0x7B, 0xFF, + 0x8F, 0xFC, 0x3F, 0xC0, 0x70, 0x00, 0x3B, 0x8F, 0x71, 0xEE, 0x08, 0x80, 0xE0, 0x7F, 0x1F, 0xF3, + 0x9E, 0x03, 0xC1, 0xF0, 0x3E, 0x07, 0xEF, 0x3D, 0xFF, 0x9F, 0xF1, 0xFC, 0x04, 0x00, 0x3F, 0xE3, + 0xFF, 0x9F, 0xFC, 0x7F, 0xE0, 0x3E, 0x03, 0xE0, 0x3E, 0x01, 0xF8, 0x0F, 0xF0, 0x07, 0x80, 0x1E, + 0xE0, 0xF7, 0x87, 0xBF, 0xF8, 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x3F, 0xE3, 0xFF, 0x9F, 0xFC, 0x7F, 0xE0, 0x3E, 0x03, 0xE0, 0x3E, 0x01, 0xF8, 0x0F, 0xF0, 0x07, 0x80, 0x1E, 0xE0, 0xF7, 0x87, 0xBF, - 0xF8, 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x3F, 0xE3, 0xFF, 0x9F, 0xFC, 0x7F, 0xE0, 0x3E, 0x03, 0xE0, - 0x3E, 0x01, 0xF8, 0x0F, 0xF0, 0x07, 0x80, 0x1E, 0xE0, 0xF7, 0x87, 0xBF, 0xF8, 0xFF, 0xC3, 0xFC, - 0x07, 0x00, 0x0F, 0x80, 0x7F, 0x01, 0xFC, 0x00, 0x00, 0x60, 0x3B, 0xC1, 0xFF, 0x07, 0xFC, 0x3F, - 0xF1, 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xEF, 0xF7, 0xBF, 0xFC, 0xFF, 0xF3, 0xFF, 0x8F, 0xFC, 0x3F, - 0xF0, 0xFF, 0x83, 0xFC, 0x0F, 0x1F, 0x07, 0xF0, 0xFE, 0x00, 0x0E, 0x0F, 0xE3, 0xFC, 0xFF, 0x9F, - 0xF7, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xDF, 0xF3, 0xFC, 0x7F, 0x8F, 0x1C, 0xC0, 0x77, 0x81, 0xDE, - 0x02, 0x30, 0x60, 0x3B, 0xC1, 0xFF, 0x07, 0xFC, 0x3F, 0xF1, 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xEF, - 0xF7, 0xBF, 0xFC, 0xFF, 0xF3, 0xFF, 0x8F, 0xFC, 0x3F, 0xF0, 0xFF, 0x83, 0xFC, 0x0F, 0x3B, 0x8F, - 0x79, 0xEE, 0x08, 0x8E, 0x0F, 0xE3, 0xFC, 0xFF, 0x9F, 0xF7, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xDF, - 0xF3, 0xFC, 0x7F, 0x8F, 0x0E, 0x70, 0x0F, 0xF0, 0x0E, 0xF0, 0x04, 0x20, 0x03, 0xC0, 0x0F, 0xF0, - 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, - 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x19, 0xC3, - 0xDC, 0x3D, 0xC1, 0x88, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, - 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, - 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x78, 0x1E, 0x78, 0x1E, - 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, - 0x78, 0xFF, 0xEF, 0xFF, 0xFF, 0x3F, 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x0E, 0x70, - 0x0F, 0xF0, 0x0E, 0xF0, 0x04, 0x20, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, - 0x78, 0x1E, 0x78, 0x1E, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, - 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x19, 0xC3, 0xDC, 0x3D, 0xC1, 0x88, 0x0F, 0x01, + 0xF8, 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x0F, 0x80, 0x7F, 0x01, 0xFC, 0x00, 0x00, 0x60, 0x3B, 0xC1, + 0xFF, 0x07, 0xFC, 0x3F, 0xF1, 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xEF, 0xF7, 0xBF, 0xFC, 0xFF, 0xF3, + 0xFF, 0x8F, 0xFC, 0x3F, 0xF0, 0xFF, 0x83, 0xFC, 0x0F, 0x1F, 0x07, 0xF0, 0xFE, 0x00, 0x0E, 0x0F, + 0xE3, 0xFC, 0xFF, 0x9F, 0xF7, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xDF, 0xF3, 0xFC, 0x7F, 0x8F, 0x1C, + 0xC0, 0x77, 0x81, 0xDE, 0x02, 0x30, 0x60, 0x3B, 0xC1, 0xFF, 0x07, 0xFC, 0x3F, 0xF1, 0xFF, 0xC7, + 0xFF, 0x3F, 0xFD, 0xEF, 0xF7, 0xBF, 0xFC, 0xFF, 0xF3, 0xFF, 0x8F, 0xFC, 0x3F, 0xF0, 0xFF, 0x83, + 0xFC, 0x0F, 0x3B, 0x8F, 0x79, 0xEE, 0x08, 0x8E, 0x0F, 0xE3, 0xFC, 0xFF, 0x9F, 0xF7, 0xFF, 0xFF, + 0xFF, 0xFE, 0xFF, 0xDF, 0xF3, 0xFC, 0x7F, 0x8F, 0x0E, 0x70, 0x0F, 0xF0, 0x0E, 0xF0, 0x04, 0x20, + 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, + 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, + 0x01, 0x80, 0x19, 0xC3, 0xDC, 0x3D, 0xC1, 0x88, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, + 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x03, 0xC0, 0x0F, 0xF0, + 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, + 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0xEF, 0xFF, 0xFF, 0x3F, 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, - 0x06, 0x00, 0x0E, 0xE0, 0x3D, 0xE0, 0x7B, 0x80, 0x22, 0x00, 0x78, 0x03, 0xFE, 0x0F, 0xFE, 0x3F, - 0xFC, 0x78, 0x3C, 0xF0, 0x78, 0x00, 0x78, 0x3F, 0xF0, 0x7F, 0xE0, 0x7F, 0xDC, 0x07, 0xBC, 0x1E, - 0x78, 0x7C, 0xFF, 0xF0, 0xFF, 0xE0, 0xFF, 0x00, 0x38, 0x00, 0x39, 0x83, 0xBC, 0x3B, 0xC1, 0x18, - 0x0F, 0x03, 0xFC, 0x7F, 0xE7, 0x9E, 0x70, 0xE0, 0xFF, 0x0F, 0xF0, 0x7F, 0xF0, 0xE7, 0xFE, 0x7F, - 0xC3, 0xF8, 0x06, 0x00, 0x0F, 0xC0, 0x3F, 0x00, 0xFC, 0x00, 0x00, 0x70, 0x1F, 0xE0, 0xF7, 0x87, + 0x06, 0x00, 0x0E, 0x70, 0x0F, 0xF0, 0x0E, 0xF0, 0x04, 0x20, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, + 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x78, 0x1E, + 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x19, 0xC3, 0xDC, 0x3D, + 0xC1, 0x88, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0xEF, 0xFF, 0xFF, 0x3F, 0x78, 0xF7, + 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x0E, 0xE0, 0x3D, 0xE0, 0x7B, 0x80, 0x22, 0x00, 0x78, 0x03, + 0xFE, 0x0F, 0xFE, 0x3F, 0xFC, 0x78, 0x3C, 0xF0, 0x78, 0x00, 0x78, 0x3F, 0xF0, 0x7F, 0xE0, 0x7F, + 0xDC, 0x07, 0xBC, 0x1E, 0x78, 0x7C, 0xFF, 0xF0, 0xFF, 0xE0, 0xFF, 0x00, 0x38, 0x00, 0x39, 0x83, + 0xBC, 0x3B, 0xC1, 0x18, 0x0F, 0x03, 0xFC, 0x7F, 0xE7, 0x9E, 0x70, 0xE0, 0xFF, 0x0F, 0xF0, 0x7F, + 0xF0, 0xE7, 0xFE, 0x7F, 0xC3, 0xF8, 0x06, 0x00, 0x0F, 0xC0, 0x3F, 0x00, 0xFC, 0x00, 0x00, 0x70, + 0x1F, 0xE0, 0xF7, 0x87, 0xDF, 0x1E, 0x3C, 0x78, 0xF3, 0xC1, 0xEF, 0x07, 0xF8, 0x0F, 0xE0, 0x3F, + 0x00, 0x7C, 0x01, 0xF0, 0x07, 0x80, 0xFE, 0x03, 0xF0, 0x0F, 0x80, 0x1F, 0x80, 0xFC, 0x07, 0xE0, + 0x00, 0x0E, 0x07, 0x78, 0x79, 0xC3, 0xCF, 0x1C, 0x79, 0xE1, 0xCF, 0x0F, 0x70, 0x7F, 0x81, 0xF8, + 0x0F, 0xC0, 0x3E, 0x01, 0xE0, 0x0F, 0x01, 0xF8, 0x1F, 0x80, 0xF8, 0x07, 0x80, 0x00, 0x1C, 0xE0, + 0x7B, 0x81, 0xEE, 0x02, 0x10, 0x70, 0x1F, 0xE0, 0xF7, 0x87, 0xDF, 0x1E, 0x3C, 0x78, 0xF3, 0xC1, + 0xEF, 0x07, 0xF8, 0x0F, 0xE0, 0x3F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0x80, 0xFE, 0x03, 0xF0, 0x0F, + 0x80, 0x39, 0xC1, 0xEE, 0x0F, 0x70, 0x21, 0x0E, 0x07, 0x78, 0x79, 0xC3, 0xCF, 0x1C, 0x79, 0xE1, + 0xCF, 0x0F, 0x70, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xE0, 0x0F, 0x01, 0xF8, 0x1F, 0x80, + 0xF8, 0x07, 0x80, 0x00, 0x0F, 0xF0, 0x3B, 0x81, 0xFC, 0x07, 0x70, 0x70, 0x1F, 0xE0, 0xF7, 0x87, 0xDF, 0x1E, 0x3C, 0x78, 0xF3, 0xC1, 0xEF, 0x07, 0xF8, 0x0F, 0xE0, 0x3F, 0x00, 0x7C, 0x01, 0xF0, - 0x07, 0x80, 0xFE, 0x03, 0xF0, 0x0F, 0x80, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x00, 0x0E, 0x07, 0x78, + 0x07, 0x80, 0xFE, 0x03, 0xF0, 0x0F, 0x80, 0x1F, 0xE0, 0xEE, 0x0F, 0xE0, 0x77, 0x0E, 0x07, 0x78, 0x79, 0xC3, 0xCF, 0x1C, 0x79, 0xE1, 0xCF, 0x0F, 0x70, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, - 0xE0, 0x0F, 0x01, 0xF8, 0x1F, 0x80, 0xF8, 0x07, 0x80, 0x00, 0x1C, 0xE0, 0x7B, 0x81, 0xEE, 0x02, - 0x10, 0x70, 0x1F, 0xE0, 0xF7, 0x87, 0xDF, 0x1E, 0x3C, 0x78, 0xF3, 0xC1, 0xEF, 0x07, 0xF8, 0x0F, - 0xE0, 0x3F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0x80, 0xFE, 0x03, 0xF0, 0x0F, 0x80, 0x39, 0xC1, 0xEE, - 0x0F, 0x70, 0x21, 0x0E, 0x07, 0x78, 0x79, 0xC3, 0xCF, 0x1C, 0x79, 0xE1, 0xCF, 0x0F, 0x70, 0x7F, - 0x81, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xE0, 0x0F, 0x01, 0xF8, 0x1F, 0x80, 0xF8, 0x07, 0x80, 0x00, - 0x0F, 0xF0, 0x3B, 0x81, 0xFC, 0x07, 0x70, 0x70, 0x1F, 0xE0, 0xF7, 0x87, 0xDF, 0x1E, 0x3C, 0x78, - 0xF3, 0xC1, 0xEF, 0x07, 0xF8, 0x0F, 0xE0, 0x3F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0x80, 0xFE, 0x03, - 0xF0, 0x0F, 0x80, 0x1F, 0xE0, 0xEE, 0x0F, 0xE0, 0x77, 0x0E, 0x07, 0x78, 0x79, 0xC3, 0xCF, 0x1C, - 0x79, 0xE1, 0xCF, 0x0F, 0x70, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xE0, 0x0F, 0x01, 0xF8, - 0x1F, 0x80, 0xF8, 0x07, 0x80, 0x00, 0x19, 0xC1, 0xEE, 0x0F, 0x70, 0x31, 0x06, 0x07, 0x78, 0x3F, - 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0x7D, 0xE0, 0x0F, - 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x33, 0x8F, 0x71, 0xEE, 0x18, 0x8E, 0x1D, 0xE3, 0xFC, - 0x7F, 0x8F, 0xF1, 0xFE, 0x3F, 0xFF, 0xBF, 0xF3, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x7F, 0xDF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xF0, - 0x3E, 0x07, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0xF0, - 0xF0, 0xF0, 0xF8, 0xF8, 0xF8, 0x78, 0x78, 0x78, 0x38, 0x0E, 0x70, 0x07, 0xF8, 0x03, 0xDC, 0x00, - 0x84, 0x06, 0x00, 0x37, 0x80, 0x3F, 0xC0, 0x1F, 0xE0, 0x0F, 0xF0, 0x07, 0xF8, 0x03, 0xFF, 0xE1, - 0xFF, 0xFC, 0xFF, 0xFF, 0x7F, 0x87, 0xBF, 0xC3, 0xDF, 0xE1, 0xEF, 0xF0, 0xF7, 0xFF, 0xFB, 0xFF, - 0xF9, 0xFF, 0xF8, 0xF0, 0x0C, 0xE0, 0x3D, 0xC0, 0x7B, 0x80, 0x62, 0x0E, 0x01, 0xDE, 0x03, 0xFC, - 0x07, 0xF8, 0x0F, 0xFF, 0x1F, 0xFF, 0xBF, 0xFF, 0x7F, 0x8F, 0xFF, 0x1F, 0xFF, 0xFB, 0xFF, 0xF7, - 0xFF, 0xCF, 0x3F, 0xF1, 0xFF, 0xCF, 0xFE, 0x7F, 0xE3, 0xC0, 0x1E, 0x00, 0xF0, 0x1F, 0xF0, 0xFF, - 0x87, 0xFC, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x0E, 0x00, 0xF0, 0x1F, - 0x80, 0xF8, 0x03, 0x80, 0x00, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3C, 0x1F, 0xCF, 0xF9, 0xFC, 0x3C, - 0x0F, 0x83, 0xE0, 0xF8, 0x0E, 0x07, 0x87, 0xE1, 0xF0, 0x38, 0x00, 0x70, 0x1C, 0x78, 0x3C, 0x3C, - 0x7C, 0x3E, 0x78, 0x1E, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x07, 0xC0, 0x07, 0xE0, 0x0F, - 0xE0, 0x1E, 0xF0, 0x3E, 0xF8, 0x3C, 0x7E, 0x78, 0x3F, 0xF8, 0x3F, 0x00, 0x07, 0x00, 0x0F, 0x00, - 0x3F, 0x00, 0x3E, 0x00, 0x38, 0x70, 0xFE, 0x3D, 0xEF, 0x1F, 0xE3, 0xF8, 0x3E, 0x07, 0xC0, 0xFC, - 0x3F, 0x8F, 0x79, 0xEF, 0x78, 0xF0, 0x1E, 0x03, 0xC1, 0xF8, 0x3E, 0x07, 0x80, 0x70, 0x1C, 0xF0, - 0x78, 0xF1, 0xF1, 0xF3, 0xC1, 0xEF, 0x01, 0xFE, 0x03, 0xF8, 0x3F, 0xFE, 0x7F, 0xFC, 0x1F, 0xC0, - 0x3F, 0x80, 0xF7, 0x83, 0xEF, 0x87, 0x8F, 0x1E, 0x0F, 0x7C, 0x1F, 0x70, 0xE7, 0x9E, 0x79, 0xE3, - 0xFC, 0x1F, 0x87, 0xFE, 0x7F, 0xE1, 0xF8, 0x3F, 0xC3, 0xBC, 0x79, 0xEF, 0x0F, 0xF8, 0xF3, 0xC7, - 0x9F, 0x3C, 0x79, 0xE1, 0xEF, 0x3F, 0xF9, 0xFF, 0xDE, 0xFC, 0xF7, 0xC7, 0x9E, 0x3C, 0xF9, 0xE3, - 0xEF, 0x0F, 0x00, 0xFF, 0x8F, 0xFC, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, - 0xC0, 0x3C, 0x03, 0xCF, 0xFF, 0xFF, 0xF0, 0x7C, 0x3F, 0x03, 0x81, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, - 0x0F, 0x07, 0x8F, 0xDE, 0xEE, 0x78, 0xFF, 0xEF, 0xFE, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, - 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0xFF, 0xCF, 0xFE, 0x01, 0xF0, 0x0F, - 0x00, 0xF0, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xF0, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xFF, 0x7F, 0x87, 0x03, 0x03, 0x81, 0xC0, 0xE0, 0x78, 0x3C, 0x1E, - 0x0F, 0x07, 0x81, 0xC0, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, - 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xF0, 0xF7, 0xCF, 0x7E, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, - 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x79, 0xE7, 0xFE, 0x3F, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0x3F, 0xE0, 0x7C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, - 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xFF, 0x1F, 0xF0, 0x1F, 0x01, 0xE0, 0x1E, 0x03, 0xC0, 0x78, - 0x0F, 0x01, 0xE0, 0x78, 0x1F, 0x7F, 0xCF, 0xF0, 0xF0, 0x1E, 0x03, 0xC0, 0x7F, 0xFF, 0xFE, 0x03, - 0xC0, 0xF0, 0x1E, 0x03, 0xC0, 0xF0, 0x1E, 0x07, 0xC0, 0xF0, 0x1E, 0x07, 0xC0, 0xF0, 0xFF, 0xEF, - 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, - 0xFF, 0xF0, 0x79, 0xF0, 0xFF, 0xE3, 0xE7, 0x8F, 0x1E, 0x3C, 0x38, 0xF0, 0xE3, 0xC3, 0x8E, 0x0E, - 0x78, 0x39, 0xE0, 0xE7, 0x83, 0x9E, 0x7E, 0x79, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xF0, 0x7C, 0xFC, 0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x7F, 0xFF, 0xE0, 0x7F, 0xE3, - 0xFF, 0x86, 0x3C, 0x70, 0xF7, 0x87, 0xBC, 0x3D, 0xE1, 0xEF, 0x0F, 0x78, 0x7B, 0xC3, 0xCF, 0x3C, - 0x7F, 0xE0, 0xFC, 0x00, 0xF0, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x38, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, - 0x1C, 0xF1, 0xEE, 0x1F, 0xE3, 0xFC, 0xFF, 0x8F, 0xE0, 0xFF, 0x8F, 0xFE, 0xF3, 0xEF, 0x1E, 0xF0, - 0xFF, 0x0F, 0xFC, 0xF7, 0xCF, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, - 0x0F, 0x00, 0xF0, 0xFF, 0x8F, 0xFE, 0xF1, 0xEF, 0x0F, 0xF0, 0xFF, 0xCF, 0x7C, 0xF0, 0x0F, 0x00, - 0xF0, 0x0F, 0x01, 0xEF, 0xFE, 0xFF, 0x80, 0xF0, 0xEF, 0x0E, 0x78, 0xE7, 0x8E, 0x78, 0xE3, 0xCE, - 0x3D, 0xE3, 0xFE, 0x1F, 0xC1, 0xF0, 0x1E, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x07, 0x80, 0x78, 0x07, - 0x80, 0xF8, 0xF7, 0x8F, 0x7C, 0xF3, 0xEF, 0x1E, 0xF1, 0xFF, 0x0F, 0xE0, 0xF8, 0x07, 0xC0, 0x3C, - 0x03, 0xEF, 0xFE, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0x0F, 0x01, 0xF0, 0x1E, 0xF1, 0xEF, - 0x3E, 0xF3, 0xCF, 0x3C, 0xF7, 0xCF, 0x78, 0xF7, 0x8F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x00, 0xFF, - 0x3F, 0xE0, 0x7C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, - 0xF7, 0x9F, 0xEF, 0x3F, 0xDE, 0x7F, 0xBC, 0xFF, 0x79, 0xFE, 0xF3, 0xFF, 0xE7, 0xFF, 0x8F, 0xF0, - 0x1E, 0xF0, 0x79, 0xF1, 0xF1, 0xFF, 0xC1, 0xFF, 0x00, 0xFF, 0xCF, 0xFE, 0x19, 0xF3, 0x8F, 0x78, - 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x78, 0xFF, 0x8F, 0xF8, 0xF0, 0x03, 0x80, 0x0F, - 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, - 0xFF, 0xE1, 0xFF, 0xE7, 0xFF, 0xCF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x00, 0x00, 0x07, 0x00, 0x0F, - 0x00, 0x1E, 0x00, 0x18, 0x00, 0x0F, 0x07, 0xF9, 0xFF, 0xBC, 0xF0, 0x1E, 0x3F, 0xDF, 0xFB, 0xEF, - 0xF1, 0xFE, 0x7D, 0xFF, 0xBF, 0xF1, 0x80, 0x1C, 0x03, 0xC0, 0x78, 0x06, 0x00, 0x07, 0x80, 0x0F, - 0x80, 0x07, 0x00, 0x0C, 0x00, 0x38, 0x00, 0xF8, 0x01, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1D, 0xE0, - 0x7B, 0xC0, 0xF7, 0x81, 0xE7, 0x87, 0x8F, 0x0F, 0xFE, 0x1F, 0xFE, 0x7F, 0xFC, 0xF0, 0x79, 0xE0, - 0x7F, 0x80, 0xF0, 0x0F, 0x01, 0xE0, 0x1C, 0x03, 0x00, 0xF0, 0x7F, 0x9F, 0xFB, 0xCF, 0x01, 0xE3, - 0xFD, 0xFF, 0xBE, 0xFF, 0x1F, 0xE7, 0xDF, 0xFB, 0xFF, 0x18, 0x00, 0x00, 0x00, 0x00, 0x70, 0x01, - 0xC0, 0x03, 0x80, 0x38, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0x38, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, - 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, 0xFF, - 0xE7, 0xFF, 0xCF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0x1C, 0x0F, 0x01, - 0xF8, 0x3F, 0xC3, 0x9C, 0x0F, 0x03, 0xFC, 0x7F, 0xE7, 0x9E, 0x01, 0xE1, 0xFE, 0x7F, 0xE7, 0xDE, - 0xF1, 0xEF, 0x3E, 0x7F, 0xE7, 0xFE, 0x18, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x01, 0xC0, 0x03, 0x80, - 0x38, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0x38, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, - 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, 0xFF, 0xE7, 0xFF, 0xCF, - 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x00, 0x00, 0x70, 0x07, 0x00, 0x60, 0xF0, 0x3F, 0x0F, 0xF1, 0xCE, - 0x0F, 0x07, 0xF9, 0xFF, 0xBC, 0xF0, 0x1E, 0x3F, 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, 0xFF, 0xBF, - 0xF1, 0x80, 0x00, 0x00, 0x01, 0xE0, 0x03, 0xC0, 0x03, 0x00, 0x38, 0x00, 0xF8, 0x03, 0xF8, 0x07, - 0x38, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, - 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, 0xFF, 0xE7, 0xFF, 0xCF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x00, - 0x00, 0x78, 0x0F, 0x00, 0xE0, 0xF0, 0x3F, 0x0F, 0xF1, 0xCE, 0x0F, 0x07, 0xF9, 0xFF, 0xBC, 0xF0, - 0x1E, 0x3F, 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, 0xFF, 0xBF, 0xF1, 0x80, 0x07, 0x70, 0x1F, 0xC0, - 0x33, 0x00, 0x1C, 0x00, 0x7C, 0x01, 0xFC, 0x03, 0x9C, 0x01, 0xC0, 0x07, 0xC0, 0x0F, 0xC0, 0x3F, - 0x80, 0x7F, 0x00, 0xEF, 0x03, 0xDE, 0x07, 0xBC, 0x0F, 0x3C, 0x3C, 0x78, 0x7F, 0xF0, 0xFF, 0xF3, - 0xFF, 0xE7, 0x83, 0xCF, 0x03, 0xFC, 0x07, 0x80, 0x1F, 0xC7, 0xF8, 0xCE, 0x07, 0x81, 0xF8, 0x7F, - 0x8E, 0x70, 0x78, 0x3F, 0xCF, 0xFD, 0xE7, 0x80, 0xF1, 0xFE, 0xFF, 0xDF, 0x7F, 0x8F, 0xF3, 0xEF, - 0xFD, 0xFF, 0x8C, 0x00, 0x03, 0xC0, 0x0F, 0xC0, 0x3B, 0xC0, 0x63, 0x80, 0x38, 0x00, 0xF8, 0x01, - 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1D, 0xE0, 0x7B, 0xC0, 0xF7, 0x81, 0xE7, 0x87, 0x8F, 0x0F, 0xFE, - 0x1F, 0xFE, 0x7F, 0xFC, 0xF0, 0x79, 0xE0, 0x7F, 0x80, 0xF0, 0x00, 0x00, 0x70, 0x00, 0xF0, 0x01, - 0xE0, 0x01, 0x80, 0x0F, 0x03, 0xF0, 0xFF, 0x1C, 0xE0, 0xF0, 0x7F, 0x9F, 0xFB, 0xCF, 0x01, 0xE3, - 0xFD, 0xFF, 0xBE, 0xFF, 0x1F, 0xE7, 0xDF, 0xFB, 0xFF, 0x18, 0x01, 0xC0, 0x3C, 0x07, 0x80, 0x60, - 0x00, 0x00, 0x03, 0xC0, 0x07, 0x00, 0x0C, 0x00, 0xC6, 0x01, 0xDC, 0x01, 0xF8, 0x01, 0xE0, 0x03, + 0xE0, 0x0F, 0x01, 0xF8, 0x1F, 0x80, 0xF8, 0x07, 0x80, 0x00, 0x19, 0xC1, 0xEE, 0x0F, 0x70, 0x31, + 0x06, 0x07, 0x78, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, + 0xFC, 0x7D, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x33, 0x8F, 0x71, 0xEE, 0x18, + 0x8E, 0x1D, 0xE3, 0xFC, 0x7F, 0x8F, 0xF1, 0xFE, 0x3F, 0xFF, 0xBF, 0xF3, 0xFE, 0x03, 0xC0, 0x78, + 0x0F, 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, + 0x78, 0x0F, 0x01, 0xF0, 0x3E, 0x07, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF8, 0xF8, 0xF8, 0x78, 0x78, 0x78, 0x38, 0x0E, 0x70, 0x07, + 0xF8, 0x03, 0xDC, 0x00, 0x84, 0x06, 0x00, 0x37, 0x80, 0x3F, 0xC0, 0x1F, 0xE0, 0x0F, 0xF0, 0x07, + 0xF8, 0x03, 0xFF, 0xE1, 0xFF, 0xFC, 0xFF, 0xFF, 0x7F, 0x87, 0xBF, 0xC3, 0xDF, 0xE1, 0xEF, 0xF0, + 0xF7, 0xFF, 0xFB, 0xFF, 0xF9, 0xFF, 0xF8, 0xF0, 0x0C, 0xE0, 0x3D, 0xC0, 0x7B, 0x80, 0x62, 0x0E, + 0x01, 0xDE, 0x03, 0xFC, 0x07, 0xF8, 0x0F, 0xFF, 0x1F, 0xFF, 0xBF, 0xFF, 0x7F, 0x8F, 0xFF, 0x1F, + 0xFF, 0xFB, 0xFF, 0xF7, 0xFF, 0xCF, 0x3F, 0xF1, 0xFF, 0xCF, 0xFE, 0x7F, 0xE3, 0xC0, 0x1E, 0x00, + 0xF0, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, + 0x0E, 0x00, 0xF0, 0x1F, 0x80, 0xF8, 0x03, 0x80, 0x00, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3C, 0x1F, + 0xCF, 0xF9, 0xFC, 0x3C, 0x0F, 0x83, 0xE0, 0xF8, 0x0E, 0x07, 0x87, 0xE1, 0xF0, 0x38, 0x00, 0x70, + 0x1C, 0x78, 0x3C, 0x3C, 0x7C, 0x3E, 0x78, 0x1E, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x07, + 0xC0, 0x07, 0xE0, 0x0F, 0xE0, 0x1E, 0xF0, 0x3E, 0xF8, 0x3C, 0x7E, 0x78, 0x3F, 0xF8, 0x3F, 0x00, + 0x07, 0x00, 0x0F, 0x00, 0x3F, 0x00, 0x3E, 0x00, 0x38, 0x70, 0xFE, 0x3D, 0xEF, 0x1F, 0xE3, 0xF8, + 0x3E, 0x07, 0xC0, 0xFC, 0x3F, 0x8F, 0x79, 0xEF, 0x78, 0xF0, 0x1E, 0x03, 0xC1, 0xF8, 0x3E, 0x07, + 0x80, 0x70, 0x1C, 0xF0, 0x78, 0xF1, 0xF1, 0xF3, 0xC1, 0xEF, 0x01, 0xFE, 0x03, 0xF8, 0x3F, 0xFE, + 0x7F, 0xFC, 0x1F, 0xC0, 0x3F, 0x80, 0xF7, 0x83, 0xEF, 0x87, 0x8F, 0x1E, 0x0F, 0x7C, 0x1F, 0x70, + 0xE7, 0x9E, 0x79, 0xE3, 0xFC, 0x1F, 0x87, 0xFE, 0x7F, 0xE1, 0xF8, 0x3F, 0xC3, 0xBC, 0x79, 0xEF, + 0x0F, 0xF8, 0xF3, 0xC7, 0x9F, 0x3C, 0x79, 0xE1, 0xEF, 0x3F, 0xF9, 0xFF, 0xDE, 0xFC, 0xF7, 0xC7, + 0x9E, 0x3C, 0xF9, 0xE3, 0xEF, 0x0F, 0x00, 0xFF, 0x8F, 0xFC, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, + 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xCF, 0xFF, 0xFF, 0xF0, 0x7C, 0x3F, 0x03, 0x81, 0xE0, + 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x8F, 0xDE, 0xEE, 0x78, 0xFF, 0xEF, 0xFE, 0x03, 0xC0, 0x3C, + 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0xFF, 0xCF, + 0xFE, 0x01, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, + 0xF0, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xFF, 0x7F, 0x87, 0x03, 0x03, 0x81, 0xC0, + 0xE0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x81, 0xC0, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, + 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xF0, 0xF7, 0xCF, 0x7E, 0xF0, + 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x79, 0xE7, 0xFE, 0x3F, 0xC0, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xE0, 0x7C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, + 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xFF, 0x1F, 0xF0, 0x1F, 0x01, 0xE0, + 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x78, 0x1F, 0x7F, 0xCF, 0xF0, 0xF0, 0x1E, 0x03, 0xC0, + 0x7F, 0xFF, 0xFE, 0x03, 0xC0, 0xF0, 0x1E, 0x03, 0xC0, 0xF0, 0x1E, 0x07, 0xC0, 0xF0, 0x1E, 0x07, + 0xC0, 0xF0, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, + 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, 0xF0, 0x79, 0xF0, 0xFF, 0xE3, 0xE7, 0x8F, 0x1E, 0x3C, 0x38, 0xF0, + 0xE3, 0xC3, 0x8E, 0x0E, 0x78, 0x39, 0xE0, 0xE7, 0x83, 0x9E, 0x7E, 0x79, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x7C, 0xFC, 0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x7F, + 0xFF, 0xE0, 0x7F, 0xE3, 0xFF, 0x86, 0x3C, 0x70, 0xF7, 0x87, 0xBC, 0x3D, 0xE1, 0xEF, 0x0F, 0x78, + 0x7B, 0xC3, 0xCF, 0x3C, 0x7F, 0xE0, 0xFC, 0x00, 0xF0, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x38, 0xF3, + 0xCF, 0x3C, 0xF3, 0xCF, 0x1C, 0xF1, 0xEE, 0x1F, 0xE3, 0xFC, 0xFF, 0x8F, 0xE0, 0xFF, 0x8F, 0xFE, + 0xF3, 0xEF, 0x1E, 0xF0, 0xFF, 0x0F, 0xFC, 0xF7, 0xCF, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, + 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0xFF, 0x8F, 0xFE, 0xF1, 0xEF, 0x0F, 0xF0, 0xFF, 0xCF, + 0x7C, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x01, 0xEF, 0xFE, 0xFF, 0x80, 0xF0, 0xEF, 0x0E, 0x78, 0xE7, + 0x8E, 0x78, 0xE3, 0xCE, 0x3D, 0xE3, 0xFE, 0x1F, 0xC1, 0xF0, 0x1E, 0x00, 0xF0, 0x0F, 0x00, 0xF0, + 0x07, 0x80, 0x78, 0x07, 0x80, 0xF8, 0xF7, 0x8F, 0x7C, 0xF3, 0xEF, 0x1E, 0xF1, 0xFF, 0x0F, 0xE0, + 0xF8, 0x07, 0xC0, 0x3C, 0x03, 0xEF, 0xFE, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0x0F, 0x01, + 0xF0, 0x1E, 0xF1, 0xEF, 0x3E, 0xF3, 0xCF, 0x3C, 0xF7, 0xCF, 0x78, 0xF7, 0x8F, 0x00, 0xF0, 0x0F, + 0x00, 0xF0, 0x00, 0xFF, 0x3F, 0xE0, 0x7C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, + 0x3C, 0x0F, 0x03, 0xC0, 0xF7, 0x9F, 0xEF, 0x3F, 0xDE, 0x7F, 0xBC, 0xFF, 0x79, 0xFE, 0xF3, 0xFF, + 0xE7, 0xFF, 0x8F, 0xF0, 0x1E, 0xF0, 0x79, 0xF1, 0xF1, 0xFF, 0xC1, 0xFF, 0x00, 0xFF, 0xCF, 0xFE, + 0x19, 0xF3, 0x8F, 0x78, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x78, 0xFF, 0x8F, 0xF8, + 0xF0, 0x47, 0xFE, 0xFF, 0x00, 0x38, 0xE7, 0x9E, 0x7B, 0xEF, 0x80, 0x00, 0x00, 0x1C, 0xFB, 0xEF, + 0x8C, 0x00, 0x07, 0xF9, 0xFF, 0xFF, 0xEF, 0xBD, 0xF0, 0x3F, 0x03, 0xF0, 0x3F, 0x01, 0xE0, 0x3C, + 0x00, 0x00, 0xE0, 0x3E, 0x07, 0xC0, 0x78, 0x06, 0x00, 0x00, 0x0F, 0xC7, 0xE1, 0xF8, 0x78, 0x1E, + 0x07, 0xF8, 0xFE, 0xFF, 0xBF, 0xC7, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x31, 0xEF, 0x9E, 0x30, + 0x00, 0x03, 0xCF, 0x1E, 0x79, 0xE7, 0x9E, 0x78, 0xE3, 0x8F, 0x3C, 0xF3, 0xC0, 0x20, 0x38, 0x7F, + 0x1F, 0xFF, 0xFF, 0xBF, 0xE7, 0xE1, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x80, + 0x00, 0x00, 0x0F, 0x77, 0xF7, 0x7F, 0xFF, 0x7F, 0xF7, 0xFF, 0x7D, 0xC7, 0x80, 0x78, 0x07, 0x80, + 0x78, 0x03, 0x80, 0x38, 0x03, 0x80, 0x38, 0x00, 0x00, 0x04, 0x07, 0x03, 0xE1, 0xF8, 0xF8, 0x3C, + 0x0F, 0xC1, 0xF8, 0x3E, 0x1F, 0x0F, 0x03, 0xC0, 0xFF, 0x9F, 0xF7, 0xFC, 0x00, 0x00, 0x0F, 0xC7, + 0xF9, 0xFE, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFF, 0xF7, 0xF8, 0xFC, 0x40, 0x07, 0xFC, 0x7F, + 0xC7, 0xFC, 0x0F, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1E, 0x01, 0xE0, 0x1E, 0x01, 0xE0, + 0x0E, 0x00, 0xE0, 0x08, 0x20, 0x4F, 0x0F, 0xF0, 0xFF, 0x1E, 0x79, 0xE7, 0x9E, 0x39, 0xC3, 0xFC, + 0x3F, 0xC3, 0xF8, 0x1F, 0x81, 0xF8, 0x1F, 0x00, 0xF0, 0x0F, 0x00, 0x80, 0x03, 0x00, 0xF0, 0x0F, + 0x00, 0xF8, 0x1F, 0x81, 0xF8, 0x1F, 0x83, 0xFC, 0x3F, 0xC3, 0x9C, 0x79, 0xE7, 0x9E, 0xF1, 0xEF, + 0x0F, 0xF0, 0xF2, 0x04, 0x08, 0x0F, 0xC7, 0xF3, 0xFE, 0xF7, 0xBD, 0xEF, 0xFB, 0xFE, 0x7F, 0x87, + 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x1C, 0x00, 0x08, 0x00, 0x78, 0x00, 0xF3, 0x80, 0xE7, 0x01, + 0xFE, 0x03, 0xFC, 0x07, 0xF8, 0x0F, 0xF0, 0x3C, 0xFD, 0xF9, 0xFF, 0xE1, 0xFF, 0x81, 0xFE, 0x00, + 0x7E, 0xFC, 0x31, 0xEF, 0x9E, 0x30, 0x00, 0x03, 0xCF, 0x1E, 0x79, 0xE7, 0x9E, 0x78, 0xE3, 0x8F, + 0x3C, 0xF3, 0xC0, 0x20, 0x38, 0x7F, 0x1F, 0xFF, 0xFF, 0xBF, 0xE7, 0xE1, 0xE0, 0x78, 0x1E, 0x07, + 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x80, 0x00, 0x00, 0x0F, 0x77, 0xF7, 0x7F, 0xFF, 0x7F, 0xF7, 0xFF, + 0x7D, 0xC7, 0x80, 0x78, 0x07, 0x80, 0x78, 0x03, 0x80, 0x38, 0x03, 0x80, 0x38, 0x00, 0x00, 0x01, + 0xC7, 0x3C, 0x7F, 0xC7, 0xFC, 0x7F, 0xE7, 0xFE, 0x7F, 0xE3, 0xFC, 0x38, 0x03, 0xC0, 0x3C, 0x03, + 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0xF0, 0x1F, 0x81, 0xFC, + 0x3F, 0xC7, 0x9E, 0x79, 0xE7, 0x0E, 0xF6, 0xFF, 0x6F, 0xFF, 0xE7, 0xFE, 0x7F, 0xC0, 0x04, 0x0F, + 0xC7, 0xF1, 0xFC, 0x70, 0x1C, 0x07, 0xF9, 0xFE, 0x3F, 0xC7, 0xC3, 0xE1, 0xF0, 0x7C, 0x3E, 0x0F, + 0x00, 0xC0, 0x20, 0x4F, 0x0F, 0xF0, 0xFF, 0x1E, 0x79, 0xE7, 0x9E, 0x39, 0xC3, 0xFC, 0x3F, 0xC3, + 0xF8, 0x1F, 0x81, 0xF8, 0x1F, 0x00, 0xF0, 0x0F, 0x00, 0x80, 0x03, 0x00, 0xF0, 0x0F, 0x00, 0xF8, + 0x1F, 0x81, 0xF8, 0x1F, 0x83, 0xFC, 0x3F, 0xC3, 0x9C, 0x79, 0xE7, 0x9E, 0xF1, 0xEF, 0x0F, 0xF0, + 0xF2, 0x04, 0x08, 0x0F, 0xC7, 0xF3, 0xFE, 0xF7, 0xBD, 0xEF, 0xFB, 0xFE, 0x7F, 0x87, 0xF0, 0x3C, + 0x0F, 0x03, 0xC0, 0xF0, 0x1C, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, + 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, 0xFF, 0xE7, 0xFF, 0xCF, 0x07, 0x9E, + 0x07, 0xF8, 0x0F, 0x00, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x18, 0x00, 0x0F, 0x07, 0xF9, + 0xFF, 0xBC, 0xF0, 0x1E, 0x3F, 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, 0xFF, 0xBF, 0xF1, 0x80, 0x1C, + 0x03, 0xC0, 0x78, 0x06, 0x00, 0x07, 0x80, 0x0F, 0x80, 0x07, 0x00, 0x0C, 0x00, 0x38, 0x00, 0xF8, + 0x01, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1D, 0xE0, 0x7B, 0xC0, 0xF7, 0x81, 0xE7, 0x87, 0x8F, 0x0F, + 0xFE, 0x1F, 0xFE, 0x7F, 0xFC, 0xF0, 0x79, 0xE0, 0x7F, 0x80, 0xF0, 0x0F, 0x01, 0xE0, 0x1C, 0x03, + 0x00, 0xF0, 0x7F, 0x9F, 0xFB, 0xCF, 0x01, 0xE3, 0xFD, 0xFF, 0xBE, 0xFF, 0x1F, 0xE7, 0xDF, 0xFB, + 0xFF, 0x18, 0x00, 0x00, 0x00, 0x00, 0x70, 0x01, 0xC0, 0x03, 0x80, 0x38, 0x00, 0xF8, 0x03, 0xF8, + 0x07, 0x38, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x0F, + 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, 0xFF, 0xE7, 0xFF, 0xCF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, + 0x00, 0x00, 0x0E, 0x00, 0xE0, 0x1C, 0x0F, 0x01, 0xF8, 0x3F, 0xC3, 0x9C, 0x0F, 0x03, 0xFC, 0x7F, + 0xE7, 0x9E, 0x01, 0xE1, 0xFE, 0x7F, 0xE7, 0xDE, 0xF1, 0xEF, 0x3E, 0x7F, 0xE7, 0xFE, 0x18, 0x00, + 0x00, 0x00, 0x01, 0xC0, 0x01, 0xC0, 0x03, 0x80, 0x38, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0x38, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, 0xFF, 0xE7, 0xFF, 0xCF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x00, 0x00, 0x70, - 0x1C, 0x03, 0x03, 0x9C, 0x77, 0x87, 0xE0, 0x78, 0x0F, 0x07, 0xF9, 0xFF, 0xBC, 0xF0, 0x1E, 0x3F, - 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, 0xFF, 0xBF, 0xF1, 0x80, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, - 0x1C, 0x00, 0xC6, 0x01, 0xDC, 0x03, 0xF8, 0x01, 0xE0, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, + 0x07, 0x00, 0x60, 0xF0, 0x3F, 0x0F, 0xF1, 0xCE, 0x0F, 0x07, 0xF9, 0xFF, 0xBC, 0xF0, 0x1E, 0x3F, + 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, 0xFF, 0xBF, 0xF1, 0x80, 0x00, 0x00, 0x01, 0xE0, 0x03, 0xC0, + 0x03, 0x00, 0x38, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0x38, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, 0xFF, 0xE7, - 0xFF, 0xCF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x00, 0x03, 0x80, 0x38, 0x07, 0x03, 0x1C, 0x77, 0x87, - 0xE0, 0x78, 0x0F, 0x07, 0xF9, 0xFF, 0xBC, 0xF0, 0x1E, 0x3F, 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, - 0xFF, 0xBF, 0xF1, 0x80, 0x00, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x0C, 0x00, 0xC6, 0x01, 0xDC, 0x01, - 0xF8, 0x01, 0xE0, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, - 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, 0xFF, 0xE7, 0xFF, 0xCF, 0x07, 0x9E, 0x07, 0xF8, - 0x0F, 0x00, 0x01, 0xE0, 0x3C, 0x03, 0x03, 0x9C, 0x77, 0x87, 0xE0, 0x78, 0x0F, 0x07, 0xF9, 0xFF, - 0xBC, 0xF0, 0x1E, 0x3F, 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, 0xFF, 0xBF, 0xF1, 0x80, 0x07, 0x70, - 0x1F, 0xC0, 0x33, 0x80, 0x63, 0x00, 0xEE, 0x00, 0xFC, 0x00, 0xF0, 0x01, 0xC0, 0x07, 0xC0, 0x0F, - 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xEF, 0x03, 0xDE, 0x07, 0xBC, 0x0F, 0x3C, 0x3C, 0x78, 0x7F, 0xF0, - 0xFF, 0xF3, 0xFF, 0xE7, 0x83, 0xCF, 0x03, 0xFC, 0x07, 0x80, 0x1F, 0xC7, 0xF8, 0xCE, 0x1C, 0xE3, - 0xFC, 0x3F, 0x03, 0xC0, 0x78, 0x3F, 0xCF, 0xFD, 0xE7, 0x80, 0xF1, 0xFE, 0xFF, 0xDF, 0x7F, 0x8F, - 0xF3, 0xEF, 0xFD, 0xFF, 0x8C, 0x00, 0x0C, 0x60, 0x1F, 0xC0, 0x1F, 0x80, 0x1C, 0x00, 0x38, 0x00, - 0xF8, 0x01, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1D, 0xE0, 0x7B, 0xC0, 0xF7, 0x81, 0xE7, 0x87, 0x8F, - 0x0F, 0xFE, 0x1F, 0xFE, 0x7F, 0xFC, 0xF0, 0x79, 0xE0, 0x7F, 0x80, 0xF0, 0x00, 0x00, 0x70, 0x00, - 0xF0, 0x01, 0xE0, 0x01, 0x80, 0x39, 0xC7, 0xF8, 0x7E, 0x03, 0x00, 0xF0, 0x7F, 0x9F, 0xFB, 0xCF, - 0x01, 0xE3, 0xFD, 0xFF, 0xBE, 0xFF, 0x1F, 0xE7, 0xDF, 0xFB, 0xFF, 0x18, 0x01, 0xC0, 0x3C, 0x07, - 0x80, 0x60, 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0xFE, 0xFF, 0xDF, 0xFB, - 0xC0, 0x78, 0x0F, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0xC0, 0x78, 0x07, 0x00, 0x40, 0x0F, - 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, - 0xFC, 0x06, 0x00, 0x70, 0x0F, 0x00, 0xF0, 0x06, 0x00, 0x1E, 0x03, 0xE0, 0x18, 0x07, 0x07, 0xFD, - 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x7F, 0xEF, 0xFD, 0xFF, 0xBC, 0x07, 0x80, 0xF0, - 0x1F, 0xFF, 0xFF, 0xFF, 0xF0, 0x0F, 0x00, 0xF0, 0x07, 0x00, 0x60, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, - 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x1D, - 0x87, 0xF0, 0xFE, 0x00, 0x07, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x7F, 0xEF, - 0xFD, 0xFF, 0xBC, 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF, 0xFF, 0xF0, 0x1C, 0xC1, 0xFC, 0x3F, 0x80, - 0x00, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, - 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x00, 0x00, 0x1C, 0x07, 0x00, 0xC0, 0xE0, 0x3E, 0x0F, 0xE3, 0xDE, - 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0xFE, 0xFF, 0xDF, 0xFB, 0xC0, 0x78, - 0x0F, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x0C, 0x0F, 0x00, 0xF8, 0x1F, - 0xC3, 0x9C, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, - 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x00, 0x00, 0x70, 0x07, 0x00, 0xE0, 0xE0, 0x3E, 0x0F, 0xE3, - 0xDE, 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0xFE, 0xFF, 0xDF, 0xFB, 0xC0, - 0x78, 0x0F, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x1C, 0x01, 0xC0, 0x0E, 0x0F, 0x00, 0xF8, - 0x1F, 0xC3, 0x9C, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, - 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x00, 0x00, 0x78, 0x0F, 0x00, 0xC0, 0xE0, 0x3E, 0x0F, - 0xE3, 0xDE, 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0xFE, 0xFF, 0xDF, 0xFB, - 0xC0, 0x78, 0x0F, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x3C, 0x01, 0xC0, 0x0C, 0x0F, 0x00, - 0xF8, 0x1F, 0xC3, 0x9C, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, - 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x3D, 0x87, 0xF0, 0xDC, 0x07, 0x01, 0xF0, 0x7F, - 0x1E, 0xF3, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3F, 0xF7, 0xFE, 0xFF, 0xDE, - 0x03, 0xC0, 0x78, 0x0F, 0xFF, 0xFF, 0xFF, 0xF8, 0x1E, 0xC3, 0xFC, 0x3B, 0x80, 0xF0, 0x0F, 0x81, - 0xFC, 0x39, 0xC0, 0xF0, 0x1F, 0xC3, 0xFE, 0x79, 0xE7, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0x0E, - 0x7D, 0xF3, 0xFE, 0x1F, 0xC0, 0x60, 0x0E, 0x03, 0xE0, 0xEE, 0x18, 0xC7, 0xFD, 0xFF, 0xFF, 0xFF, - 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x7F, 0xEF, 0xFD, 0xFF, 0xBC, 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF, - 0xFF, 0xF0, 0x00, 0x1C, 0x07, 0x80, 0x70, 0x04, 0x00, 0x0F, 0x01, 0xF8, 0x3F, 0xC3, 0x9C, 0x0F, - 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, - 0xFC, 0x06, 0x00, 0x70, 0x0F, 0x00, 0xF0, 0x06, 0x00, 0x7B, 0xCE, 0x63, 0x3D, 0xEF, 0x7B, 0xDE, - 0xF7, 0xBD, 0xEF, 0x7B, 0xDE, 0xF0, 0xF7, 0xCE, 0x67, 0x3D, 0xEF, 0x7B, 0xDE, 0xF7, 0xBD, 0xEF, - 0x6F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0E, 0xFF, 0x60, 0xEF, 0xF6, 0xEF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0x0E, 0xFF, 0x60, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, - 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, - 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0x80, - 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, 0xFF, 0x3F, - 0xE1, 0xFC, 0x06, 0x00, 0x70, 0x0F, 0x00, 0xF0, 0x02, 0x00, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0xC0, - 0x01, 0x80, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, - 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, - 0x0F, 0xF0, 0x01, 0x80, 0x0F, 0x00, 0xF0, 0x07, 0x00, 0x60, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, - 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x38, 0x00, 0x70, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, 0x0E, 0x70, 0x03, 0xC0, + 0xFF, 0xCF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x00, 0x00, 0x78, 0x0F, 0x00, 0xE0, 0xF0, 0x3F, 0x0F, + 0xF1, 0xCE, 0x0F, 0x07, 0xF9, 0xFF, 0xBC, 0xF0, 0x1E, 0x3F, 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, + 0xFF, 0xBF, 0xF1, 0x80, 0x07, 0x70, 0x1F, 0xC0, 0x33, 0x00, 0x1C, 0x00, 0x7C, 0x01, 0xFC, 0x03, + 0x9C, 0x01, 0xC0, 0x07, 0xC0, 0x0F, 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xEF, 0x03, 0xDE, 0x07, 0xBC, + 0x0F, 0x3C, 0x3C, 0x78, 0x7F, 0xF0, 0xFF, 0xF3, 0xFF, 0xE7, 0x83, 0xCF, 0x03, 0xFC, 0x07, 0x80, + 0x1F, 0xC7, 0xF8, 0xCE, 0x07, 0x81, 0xF8, 0x7F, 0x8E, 0x70, 0x78, 0x3F, 0xCF, 0xFD, 0xE7, 0x80, + 0xF1, 0xFE, 0xFF, 0xDF, 0x7F, 0x8F, 0xF3, 0xEF, 0xFD, 0xFF, 0x8C, 0x00, 0x03, 0xC0, 0x0F, 0xC0, + 0x3B, 0xC0, 0x63, 0x80, 0x38, 0x00, 0xF8, 0x01, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1D, 0xE0, 0x7B, + 0xC0, 0xF7, 0x81, 0xE7, 0x87, 0x8F, 0x0F, 0xFE, 0x1F, 0xFE, 0x7F, 0xFC, 0xF0, 0x79, 0xE0, 0x7F, + 0x80, 0xF0, 0x00, 0x00, 0x70, 0x00, 0xF0, 0x01, 0xE0, 0x01, 0x80, 0x0F, 0x03, 0xF0, 0xFF, 0x1C, + 0xE0, 0xF0, 0x7F, 0x9F, 0xFB, 0xCF, 0x01, 0xE3, 0xFD, 0xFF, 0xBE, 0xFF, 0x1F, 0xE7, 0xDF, 0xFB, + 0xFF, 0x18, 0x01, 0xC0, 0x3C, 0x07, 0x80, 0x60, 0x00, 0x00, 0x03, 0xC0, 0x07, 0x00, 0x0C, 0x00, + 0xC6, 0x01, 0xDC, 0x01, 0xF8, 0x01, 0xE0, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, + 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, 0xFF, 0xE7, 0xFF, 0xCF, + 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x03, 0x03, 0x9C, 0x77, 0x87, 0xE0, 0x78, + 0x0F, 0x07, 0xF9, 0xFF, 0xBC, 0xF0, 0x1E, 0x3F, 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, 0xFF, 0xBF, + 0xF1, 0x80, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0xC6, 0x01, 0xDC, 0x03, 0xF8, 0x01, + 0xE0, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, + 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, 0xFF, 0xE7, 0xFF, 0xCF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x00, + 0x03, 0x80, 0x38, 0x07, 0x03, 0x1C, 0x77, 0x87, 0xE0, 0x78, 0x0F, 0x07, 0xF9, 0xFF, 0xBC, 0xF0, + 0x1E, 0x3F, 0xDF, 0xFB, 0xEF, 0xF1, 0xFE, 0x7D, 0xFF, 0xBF, 0xF1, 0x80, 0x00, 0x00, 0x07, 0x00, + 0x0F, 0x00, 0x0C, 0x00, 0xC6, 0x01, 0xDC, 0x01, 0xF8, 0x01, 0xE0, 0x03, 0x80, 0x0F, 0x80, 0x1F, + 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xFF, 0xE1, + 0xFF, 0xE7, 0xFF, 0xCF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x00, 0x01, 0xE0, 0x3C, 0x03, 0x03, 0x9C, + 0x77, 0x87, 0xE0, 0x78, 0x0F, 0x07, 0xF9, 0xFF, 0xBC, 0xF0, 0x1E, 0x3F, 0xDF, 0xFB, 0xEF, 0xF1, + 0xFE, 0x7D, 0xFF, 0xBF, 0xF1, 0x80, 0x07, 0x70, 0x1F, 0xC0, 0x33, 0x80, 0x63, 0x00, 0xEE, 0x00, + 0xFC, 0x00, 0xF0, 0x01, 0xC0, 0x07, 0xC0, 0x0F, 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xEF, 0x03, 0xDE, + 0x07, 0xBC, 0x0F, 0x3C, 0x3C, 0x78, 0x7F, 0xF0, 0xFF, 0xF3, 0xFF, 0xE7, 0x83, 0xCF, 0x03, 0xFC, + 0x07, 0x80, 0x1F, 0xC7, 0xF8, 0xCE, 0x1C, 0xE3, 0xFC, 0x3F, 0x03, 0xC0, 0x78, 0x3F, 0xCF, 0xFD, + 0xE7, 0x80, 0xF1, 0xFE, 0xFF, 0xDF, 0x7F, 0x8F, 0xF3, 0xEF, 0xFD, 0xFF, 0x8C, 0x00, 0x0C, 0x60, + 0x1F, 0xC0, 0x1F, 0x80, 0x1C, 0x00, 0x38, 0x00, 0xF8, 0x01, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1D, + 0xE0, 0x7B, 0xC0, 0xF7, 0x81, 0xE7, 0x87, 0x8F, 0x0F, 0xFE, 0x1F, 0xFE, 0x7F, 0xFC, 0xF0, 0x79, + 0xE0, 0x7F, 0x80, 0xF0, 0x00, 0x00, 0x70, 0x00, 0xF0, 0x01, 0xE0, 0x01, 0x80, 0x39, 0xC7, 0xF8, + 0x7E, 0x03, 0x00, 0xF0, 0x7F, 0x9F, 0xFB, 0xCF, 0x01, 0xE3, 0xFD, 0xFF, 0xBE, 0xFF, 0x1F, 0xE7, + 0xDF, 0xFB, 0xFF, 0x18, 0x01, 0xC0, 0x3C, 0x07, 0x80, 0x60, 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x01, 0xE0, 0x3C, 0x07, 0xFE, 0xFF, 0xDF, 0xFB, 0xC0, 0x78, 0x0F, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x01, 0xC0, 0x78, 0x07, 0x00, 0x40, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x70, 0x0F, 0x00, 0xF0, 0x06, + 0x00, 0x1E, 0x03, 0xE0, 0x18, 0x07, 0x07, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, + 0x7F, 0xEF, 0xFD, 0xFF, 0xBC, 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF, 0xFF, 0xF0, 0x0F, 0x00, 0xF0, + 0x07, 0x00, 0x60, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, + 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x1D, 0x87, 0xF0, 0xFE, 0x00, 0x07, 0xFD, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x7F, 0xEF, 0xFD, 0xFF, 0xBC, 0x07, 0x80, 0xF0, 0x1F, 0xFF, + 0xFF, 0xFF, 0xF0, 0x1C, 0xC1, 0xFC, 0x3F, 0x80, 0x00, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x00, 0x00, 0x1C, + 0x07, 0x00, 0xC0, 0xE0, 0x3E, 0x0F, 0xE3, 0xDE, 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xE0, + 0x3C, 0x07, 0xFE, 0xFF, 0xDF, 0xFB, 0xC0, 0x78, 0x0F, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x0F, 0x00, 0xE0, 0x0C, 0x0F, 0x00, 0xF8, 0x1F, 0xC3, 0x9C, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, + 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x00, 0x00, + 0x70, 0x07, 0x00, 0xE0, 0xE0, 0x3E, 0x0F, 0xE3, 0xDE, 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, + 0xE0, 0x3C, 0x07, 0xFE, 0xFF, 0xDF, 0xFB, 0xC0, 0x78, 0x0F, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0x00, 0x1C, 0x01, 0xC0, 0x0E, 0x0F, 0x00, 0xF8, 0x1F, 0xC3, 0x9C, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, + 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x00, + 0x00, 0x78, 0x0F, 0x00, 0xC0, 0xE0, 0x3E, 0x0F, 0xE3, 0xDE, 0x7F, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x01, 0xE0, 0x3C, 0x07, 0xFE, 0xFF, 0xDF, 0xFB, 0xC0, 0x78, 0x0F, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x3C, 0x01, 0xC0, 0x0C, 0x0F, 0x00, 0xF8, 0x1F, 0xC3, 0x9C, 0x0F, 0x01, 0xFC, 0x3F, + 0xE7, 0x9E, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, + 0x3D, 0x87, 0xF0, 0xDC, 0x07, 0x01, 0xF0, 0x7F, 0x1E, 0xF3, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, + 0x0F, 0x01, 0xE0, 0x3F, 0xF7, 0xFE, 0xFF, 0xDE, 0x03, 0xC0, 0x78, 0x0F, 0xFF, 0xFF, 0xFF, 0xF8, + 0x1E, 0xC3, 0xFC, 0x3B, 0x80, 0xF0, 0x0F, 0x81, 0xFC, 0x39, 0xC0, 0xF0, 0x1F, 0xC3, 0xFE, 0x79, + 0xE7, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0x0E, 0x7D, 0xF3, 0xFE, 0x1F, 0xC0, 0x60, 0x0E, 0x03, + 0xE0, 0xEE, 0x18, 0xC7, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x7F, 0xEF, 0xFD, + 0xFF, 0xBC, 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x1C, 0x07, 0x80, 0x70, 0x04, + 0x00, 0x0F, 0x01, 0xF8, 0x3F, 0xC3, 0x9C, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x9E, 0x70, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0x70, 0xE7, 0xDF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x70, 0x0F, 0x00, 0xF0, 0x06, + 0x00, 0x7B, 0xCE, 0x63, 0x3D, 0xEF, 0x7B, 0xDE, 0xF7, 0xBD, 0xEF, 0x7B, 0xDE, 0xF0, 0xF7, 0xCE, + 0x67, 0x3D, 0xEF, 0x7B, 0xDE, 0xF7, 0xBD, 0xEF, 0x6F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x0E, 0xFF, 0x60, 0xEF, 0xF6, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0E, 0xFF, 0x60, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, - 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x0C, 0x07, 0x00, 0xF8, 0x1F, 0xC3, 0x9C, 0x0F, 0x01, 0xFC, 0x3F, - 0xE7, 0xDF, 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, - 0x00, 0x00, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x30, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, 0x0E, 0x70, - 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, - 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, - 0x01, 0x80, 0x00, 0x00, 0x1C, 0x01, 0xC0, 0x0E, 0x07, 0x00, 0xF8, 0x1F, 0xC3, 0x9C, 0x0F, 0x01, - 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, - 0x06, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0x70, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, - 0x0E, 0x70, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, - 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, - 0x0F, 0xF0, 0x01, 0x80, 0x00, 0x00, 0x1C, 0x01, 0xE0, 0x0C, 0x07, 0x00, 0xF8, 0x1F, 0xC3, 0x9C, - 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, 0xFF, 0x3F, - 0xE1, 0xFC, 0x06, 0x00, 0x07, 0xF0, 0x0F, 0xF0, 0x0C, 0xE0, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, + 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0x80, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, + 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x70, 0x0F, 0x00, 0xF0, + 0x02, 0x00, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, + 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, + 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x0F, 0x00, 0xF0, 0x07, + 0x00, 0x60, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, + 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x38, 0x00, 0x70, 0x03, 0xC0, + 0x07, 0xE0, 0x0F, 0xF0, 0x0E, 0x70, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, + 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, + 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x0C, 0x07, 0x00, + 0xF8, 0x1F, 0xC3, 0x9C, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, + 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x30, + 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, 0x0E, 0x70, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, + 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, + 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x00, 0x00, 0x1C, 0x01, 0xC0, 0x0E, + 0x07, 0x00, 0xF8, 0x1F, 0xC3, 0x9C, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0x0F, 0xF0, + 0xFF, 0x0F, 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xF0, + 0x00, 0x70, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, 0x0E, 0x70, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, + 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, + 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x00, 0x00, 0x1C, 0x01, + 0xE0, 0x0C, 0x07, 0x00, 0xF8, 0x1F, 0xC3, 0x9C, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, + 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x07, 0xF0, 0x0F, 0xF0, + 0x0C, 0xE0, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, 0x0E, 0x70, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, + 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, + 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x1E, 0xC1, 0xFC, 0x1B, + 0x80, 0x70, 0x0F, 0x81, 0xFC, 0x39, 0xC0, 0xF0, 0x1F, 0xC3, 0xFE, 0x7D, 0xF7, 0x8F, 0xF0, 0xFF, + 0x0F, 0xF0, 0xF7, 0x8F, 0x7F, 0xF3, 0xFE, 0x1F, 0xC0, 0x60, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, 0x0E, 0x70, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, - 0x0F, 0xF0, 0x01, 0x80, 0x1E, 0xC1, 0xFC, 0x1B, 0x80, 0x70, 0x0F, 0x81, 0xFC, 0x39, 0xC0, 0xF0, - 0x1F, 0xC3, 0xFE, 0x7D, 0xF7, 0x8F, 0xF0, 0xFF, 0x0F, 0xF0, 0xF7, 0x8F, 0x7F, 0xF3, 0xFE, 0x1F, - 0xC0, 0x60, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, 0x0E, 0x70, 0x03, 0xC0, 0x0F, 0xF0, 0x1F, 0xF8, + 0x0F, 0xF0, 0x01, 0x80, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0x80, 0x0F, 0x01, 0xF8, 0x3D, + 0xC3, 0x8C, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, + 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x70, 0x0F, 0x00, 0xF0, 0x02, 0x00, 0x00, 0xE0, 0x01, 0xE0, + 0x01, 0xCE, 0x01, 0x8F, 0x03, 0xFE, 0x0F, 0xFE, 0x1F, 0xFE, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, + 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, + 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x03, 0xC0, 0x1C, 0x01, 0xCE, 0x0E, 0x70, 0xFF, 0x8F, 0xF8, + 0xFF, 0x8F, 0xBE, 0x78, 0xF7, 0x87, 0xBC, 0x3D, 0xE1, 0xE7, 0x8F, 0x3F, 0xF8, 0xFF, 0x83, 0xF8, + 0x06, 0x00, 0x07, 0x00, 0x07, 0x80, 0x03, 0x8E, 0x01, 0x8F, 0x03, 0xFE, 0x0F, 0xFE, 0x1F, 0xFE, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, - 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x03, 0xC0, 0x03, 0xC0, - 0x03, 0xC0, 0x01, 0x80, 0x0F, 0x01, 0xF8, 0x3D, 0xC3, 0x8C, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0xDF, - 0x78, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0x78, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0x70, 0x0F, - 0x00, 0xF0, 0x02, 0x00, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xCE, 0x01, 0x8F, 0x03, 0xFE, 0x0F, 0xFE, - 0x1F, 0xFE, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, - 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x03, 0xC0, - 0x1C, 0x01, 0xCE, 0x0E, 0x70, 0xFF, 0x8F, 0xF8, 0xFF, 0x8F, 0xBE, 0x78, 0xF7, 0x87, 0xBC, 0x3D, - 0xE1, 0xE7, 0x8F, 0x3F, 0xF8, 0xFF, 0x83, 0xF8, 0x06, 0x00, 0x07, 0x00, 0x07, 0x80, 0x03, 0x8E, - 0x01, 0x8F, 0x03, 0xFE, 0x0F, 0xFE, 0x1F, 0xFE, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, - 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, - 0x0F, 0xF0, 0x01, 0x80, 0x1C, 0x00, 0x70, 0x03, 0x8E, 0x0C, 0x70, 0xFF, 0x8F, 0xF8, 0xFF, 0x8F, - 0xBE, 0x78, 0xF7, 0x87, 0xBC, 0x3D, 0xE1, 0xE7, 0x8F, 0x3F, 0xF8, 0xFF, 0x83, 0xF8, 0x06, 0x00, - 0x03, 0xC0, 0x03, 0xC0, 0x01, 0xCE, 0x01, 0x8F, 0x03, 0xFE, 0x0F, 0xFE, 0x1F, 0xFE, 0x3F, 0xFC, - 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, - 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x0F, 0x00, 0x78, 0x01, 0xCE, 0x0C, - 0x70, 0xFF, 0x8F, 0xF8, 0xFF, 0x8F, 0xBE, 0x78, 0xF7, 0x87, 0xBC, 0x3D, 0xE1, 0xE7, 0x8F, 0x3F, - 0xF8, 0xFF, 0x83, 0xF8, 0x06, 0x00, 0x07, 0x30, 0x0F, 0xF0, 0x0F, 0xEE, 0x00, 0x0F, 0x03, 0xFE, - 0x0F, 0xFE, 0x1F, 0xFE, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, - 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, - 0x0C, 0x80, 0xFC, 0x06, 0xEE, 0x00, 0x70, 0xFF, 0x8F, 0xF8, 0xFF, 0x8F, 0xBE, 0x78, 0xF7, 0x87, - 0xBC, 0x3D, 0xE1, 0xE7, 0x8F, 0x3F, 0xF8, 0xFF, 0x83, 0xF8, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x0F, + 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x1C, 0x00, 0x70, 0x03, + 0x8E, 0x0C, 0x70, 0xFF, 0x8F, 0xF8, 0xFF, 0x8F, 0xBE, 0x78, 0xF7, 0x87, 0xBC, 0x3D, 0xE1, 0xE7, + 0x8F, 0x3F, 0xF8, 0xFF, 0x83, 0xF8, 0x06, 0x00, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0xCE, 0x01, 0x8F, 0x03, 0xFE, 0x0F, 0xFE, 0x1F, 0xFE, 0x3F, 0xFC, 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, - 0x01, 0x80, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0x80, 0x00, 0x38, 0x01, 0xC3, 0xFE, 0x3F, - 0xE3, 0xFE, 0x3E, 0xF9, 0xE3, 0xDE, 0x1E, 0xF0, 0xF7, 0x87, 0x9E, 0x3C, 0xFF, 0xE3, 0xFE, 0x0F, - 0xE0, 0x18, 0x00, 0xE0, 0x0F, 0x00, 0x78, 0x00, 0x80, 0x60, 0x3F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, - 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x8F, 0xBF, 0xF9, - 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x38, 0x01, 0xE0, 0x0E, 0x00, 0x20, 0x00, 0xE0, 0xFE, 0x3F, 0xC7, - 0xF8, 0xFF, 0x1F, 0xE3, 0xFC, 0x7F, 0x8F, 0xF1, 0xFF, 0xFD, 0xFF, 0xBF, 0xF0, 0x80, 0x1C, 0x03, - 0xC0, 0x70, 0x04, 0x00, 0x0F, 0x00, 0x7C, 0x00, 0xC0, 0x06, 0x06, 0x03, 0xF8, 0x3F, 0xC1, 0xFE, - 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF8, 0xFB, - 0xFF, 0x9F, 0xFC, 0x3F, 0xC0, 0x70, 0x00, 0x1E, 0x03, 0xE0, 0x38, 0x07, 0x0E, 0x0F, 0xE3, 0xFC, - 0x7F, 0x8F, 0xF1, 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, 0xFF, 0xDF, 0xFB, 0xFF, 0x08, 0x00, 0x03, - 0xC0, 0x07, 0x00, 0x1E, 0x38, 0x38, 0xF6, 0x03, 0xFE, 0x0F, 0xFC, 0x1F, 0x78, 0x3C, 0xF0, 0x79, + 0x01, 0x80, 0x0F, 0x00, 0x78, 0x01, 0xCE, 0x0C, 0x70, 0xFF, 0x8F, 0xF8, 0xFF, 0x8F, 0xBE, 0x78, + 0xF7, 0x87, 0xBC, 0x3D, 0xE1, 0xE7, 0x8F, 0x3F, 0xF8, 0xFF, 0x83, 0xF8, 0x06, 0x00, 0x07, 0x30, + 0x0F, 0xF0, 0x0F, 0xEE, 0x00, 0x0F, 0x03, 0xFE, 0x0F, 0xFE, 0x1F, 0xFE, 0x3F, 0xFC, 0x7C, 0x3E, + 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, + 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x0C, 0x80, 0xFC, 0x06, 0xEE, 0x00, 0x70, 0xFF, + 0x8F, 0xF8, 0xFF, 0x8F, 0xBE, 0x78, 0xF7, 0x87, 0xBC, 0x3D, 0xE1, 0xE7, 0x8F, 0x3F, 0xF8, 0xFF, + 0x83, 0xF8, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x0F, 0x03, 0xFE, 0x0F, 0xFE, 0x1F, 0xFE, 0x3F, 0xFC, + 0x7C, 0x3E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x1E, 0x78, 0x1E, + 0x7C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, + 0x01, 0x80, 0x00, 0x38, 0x01, 0xC3, 0xFE, 0x3F, 0xE3, 0xFE, 0x3E, 0xF9, 0xE3, 0xDE, 0x1E, 0xF0, + 0xF7, 0x87, 0x9E, 0x3C, 0xFF, 0xE3, 0xFE, 0x0F, 0xE0, 0x18, 0x00, 0xE0, 0x0F, 0x00, 0x78, 0x00, + 0x80, 0x60, 0x3F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, + 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x8F, 0xBF, 0xF9, 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x38, 0x01, 0xE0, + 0x0E, 0x00, 0x20, 0x00, 0xE0, 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, 0xE3, 0xFC, 0x7F, 0x8F, 0xF1, + 0xFF, 0xFD, 0xFF, 0xBF, 0xF0, 0x80, 0x1C, 0x03, 0xC0, 0x70, 0x04, 0x00, 0x0F, 0x00, 0x7C, 0x00, + 0xC0, 0x06, 0x06, 0x03, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, + 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF8, 0xFB, 0xFF, 0x9F, 0xFC, 0x3F, 0xC0, 0x70, 0x00, 0x1E, + 0x03, 0xE0, 0x38, 0x07, 0x0E, 0x0F, 0xE3, 0xFC, 0x7F, 0x8F, 0xF1, 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, + 0x1F, 0xFF, 0xDF, 0xFB, 0xFF, 0x08, 0x00, 0x03, 0xC0, 0x07, 0x00, 0x1E, 0x38, 0x38, 0xF6, 0x03, + 0xFE, 0x0F, 0xFC, 0x1F, 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xCF, 0x07, 0x9E, + 0x0F, 0x3C, 0x1E, 0x78, 0x3C, 0xF8, 0xF8, 0xFF, 0xE1, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, 0x07, + 0x80, 0x38, 0x03, 0x8E, 0x1C, 0x7E, 0x0F, 0xF8, 0xFB, 0xC7, 0xDE, 0x3C, 0xF1, 0xE7, 0x8F, 0x3C, + 0x79, 0xE3, 0xCF, 0x1E, 0x7F, 0xF1, 0xFF, 0x8F, 0xFC, 0x08, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x3C, + 0x38, 0x38, 0xF6, 0x03, 0xFE, 0x0F, 0xFC, 0x1F, 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF3, 0xC1, 0xE7, + 0x83, 0xCF, 0x07, 0x9E, 0x0F, 0x3C, 0x1E, 0x78, 0x3C, 0xF8, 0xF8, 0xFF, 0xE1, 0xFF, 0xC0, 0xFF, + 0x00, 0x70, 0x00, 0x3C, 0x00, 0xE0, 0x07, 0x8E, 0x1C, 0x7E, 0x0F, 0xF8, 0xFB, 0xC7, 0xDE, 0x3C, + 0xF1, 0xE7, 0x8F, 0x3C, 0x79, 0xE3, 0xCF, 0x1E, 0x7F, 0xF1, 0xFF, 0x8F, 0xFC, 0x08, 0x00, 0x0F, + 0x00, 0x1F, 0x00, 0x0C, 0x38, 0x18, 0xF6, 0x03, 0xFE, 0x0F, 0xFC, 0x1F, 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xCF, 0x07, 0x9E, 0x0F, 0x3C, 0x1E, 0x78, 0x3C, 0xF8, 0xF8, 0xFF, - 0xE1, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, 0x07, 0x80, 0x38, 0x03, 0x8E, 0x1C, 0x7E, 0x0F, 0xF8, + 0xE1, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x8E, 0x1C, 0x7E, 0x0F, 0xF8, 0xFB, 0xC7, 0xDE, 0x3C, 0xF1, 0xE7, 0x8F, 0x3C, 0x79, 0xE3, 0xCF, 0x1E, 0x7F, 0xF1, 0xFF, 0x8F, - 0xFC, 0x08, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x3C, 0x38, 0x38, 0xF6, 0x03, 0xFE, 0x0F, 0xFC, 0x1F, + 0xFC, 0x08, 0x00, 0x0E, 0xC0, 0x3F, 0x80, 0x7F, 0x38, 0x00, 0xF6, 0x03, 0xFE, 0x0F, 0xFC, 0x1F, 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xCF, 0x07, 0x9E, 0x0F, 0x3C, 0x1E, 0x78, - 0x3C, 0xF8, 0xF8, 0xFF, 0xE1, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, 0x3C, 0x00, 0xE0, 0x07, 0x8E, - 0x1C, 0x7E, 0x0F, 0xF8, 0xFB, 0xC7, 0xDE, 0x3C, 0xF1, 0xE7, 0x8F, 0x3C, 0x79, 0xE3, 0xCF, 0x1E, - 0x7F, 0xF1, 0xFF, 0x8F, 0xFC, 0x08, 0x00, 0x0F, 0x00, 0x1F, 0x00, 0x0C, 0x38, 0x18, 0xF6, 0x03, - 0xFE, 0x0F, 0xFC, 0x1F, 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xCF, 0x07, 0x9E, - 0x0F, 0x3C, 0x1E, 0x78, 0x3C, 0xF8, 0xF8, 0xFF, 0xE1, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, 0x1E, - 0x00, 0xF8, 0x03, 0x8E, 0x1C, 0x7E, 0x0F, 0xF8, 0xFB, 0xC7, 0xDE, 0x3C, 0xF1, 0xE7, 0x8F, 0x3C, - 0x79, 0xE3, 0xCF, 0x1E, 0x7F, 0xF1, 0xFF, 0x8F, 0xFC, 0x08, 0x00, 0x0E, 0xC0, 0x3F, 0x80, 0x7F, - 0x38, 0x00, 0xF6, 0x03, 0xFE, 0x0F, 0xFC, 0x1F, 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF3, 0xC1, 0xE7, - 0x83, 0xCF, 0x07, 0x9E, 0x0F, 0x3C, 0x1E, 0x78, 0x3C, 0xF8, 0xF8, 0xFF, 0xE1, 0xFF, 0xC0, 0xFF, - 0x00, 0x70, 0x00, 0x1D, 0x81, 0xFC, 0x0F, 0xEE, 0x00, 0x7E, 0x0F, 0xF8, 0xFB, 0xC7, 0xDE, 0x3C, - 0xF1, 0xE7, 0x8F, 0x3C, 0x79, 0xE3, 0xCF, 0x1E, 0x7F, 0xF1, 0xFF, 0x8F, 0xFC, 0x08, 0x00, 0x00, - 0x0E, 0x00, 0x3D, 0x80, 0xFF, 0x83, 0xFF, 0x07, 0xDE, 0x0F, 0x3C, 0x1E, 0x78, 0x3C, 0xF0, 0x79, - 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xCF, 0x07, 0x9E, 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, - 0xC0, 0x1C, 0x00, 0x38, 0x00, 0x78, 0x00, 0xE0, 0x00, 0x80, 0x00, 0x00, 0x38, 0x01, 0xF8, 0x3F, - 0xE3, 0xEF, 0x1F, 0x78, 0xF3, 0xC7, 0x9E, 0x3C, 0xF1, 0xE7, 0x8F, 0x3C, 0x79, 0xFF, 0xC7, 0xFE, - 0x3F, 0xF0, 0x20, 0x01, 0xC0, 0x0F, 0x00, 0x70, 0x01, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x1E, 0x00, - 0x1C, 0x07, 0x01, 0xDF, 0x07, 0xDE, 0x0F, 0x1E, 0x3C, 0x3C, 0x78, 0x3D, 0xE0, 0x7F, 0xC0, 0x7F, - 0x00, 0x7E, 0x00, 0xF8, 0x00, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, - 0x1C, 0x00, 0xF0, 0x03, 0x80, 0x0C, 0x0E, 0x07, 0x78, 0x79, 0xC3, 0xCF, 0x1C, 0x79, 0xE1, 0xCF, - 0x0F, 0x70, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xE0, 0x0F, 0x01, 0xF8, 0x1F, 0x80, 0xF8, - 0x07, 0x80, 0x00, 0x70, 0x1D, 0xF0, 0x7D, 0xE0, 0xF1, 0xE3, 0xC3, 0xC7, 0x83, 0xDE, 0x07, 0xFC, - 0x07, 0xF0, 0x07, 0xE0, 0x0F, 0x80, 0x0F, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, - 0xC0, 0x00, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x1C, 0x00, 0x10, 0x00, 0xE0, 0x77, 0x87, 0x9C, 0x3C, - 0xF1, 0xC7, 0x9E, 0x1C, 0xF0, 0xF7, 0x07, 0xF8, 0x1F, 0x80, 0xFC, 0x03, 0xE0, 0x1E, 0x00, 0xF0, - 0x1F, 0xF1, 0xFB, 0xCF, 0x9C, 0x78, 0x40, 0x07, 0x80, 0x0F, 0x80, 0x06, 0x00, 0x1C, 0x07, 0x01, - 0xDF, 0x07, 0xDE, 0x0F, 0x1E, 0x3C, 0x3C, 0x78, 0x3D, 0xE0, 0x7F, 0xC0, 0x7F, 0x00, 0x7E, 0x00, - 0xF8, 0x00, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x0F, 0x00, 0x78, - 0x01, 0xC0, 0x0C, 0x0E, 0x07, 0x78, 0x79, 0xC3, 0xCF, 0x1C, 0x79, 0xE1, 0xCF, 0x0F, 0x70, 0x7F, - 0x81, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xE0, 0x0F, 0x01, 0xF8, 0x1F, 0x80, 0xF8, 0x07, 0x80, 0x00, - 0x07, 0x60, 0x1F, 0xC0, 0x3F, 0x80, 0x00, 0x07, 0x01, 0xDF, 0x07, 0xDE, 0x0F, 0x1E, 0x3C, 0x3C, - 0x78, 0x3D, 0xE0, 0x7F, 0xC0, 0x7F, 0x00, 0x7E, 0x00, 0xF8, 0x00, 0xF0, 0x01, 0xC0, 0x03, 0x80, - 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x1C, 0xC0, 0xFE, 0x0F, 0xE0, 0x00, 0x0E, 0x07, 0x78, 0x79, - 0xC3, 0xCF, 0x1C, 0x79, 0xE1, 0xCF, 0x0F, 0x70, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xE0, - 0x0F, 0x01, 0xF8, 0x1F, 0x80, 0xF8, 0x07, 0x80, 0x00, 0xFE, 0xFF, 0xFF, 0x00, 0xFE, 0xFF, 0xFF, - 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, - 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3C, 0xF8, 0xF8, 0xFF, 0xE1, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, 0x1D, 0x81, 0xFC, 0x0F, 0xEE, + 0x00, 0x7E, 0x0F, 0xF8, 0xFB, 0xC7, 0xDE, 0x3C, 0xF1, 0xE7, 0x8F, 0x3C, 0x79, 0xE3, 0xCF, 0x1E, + 0x7F, 0xF1, 0xFF, 0x8F, 0xFC, 0x08, 0x00, 0x00, 0x0E, 0x00, 0x3D, 0x80, 0xFF, 0x83, 0xFF, 0x07, + 0xDE, 0x0F, 0x3C, 0x1E, 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xCF, 0x07, 0x9E, + 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x38, 0x00, 0x78, 0x00, 0xE0, + 0x00, 0x80, 0x00, 0x00, 0x38, 0x01, 0xF8, 0x3F, 0xE3, 0xEF, 0x1F, 0x78, 0xF3, 0xC7, 0x9E, 0x3C, + 0xF1, 0xE7, 0x8F, 0x3C, 0x79, 0xFF, 0xC7, 0xFE, 0x3F, 0xF0, 0x20, 0x01, 0xC0, 0x0F, 0x00, 0x70, + 0x01, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x1C, 0x07, 0x01, 0xDF, 0x07, 0xDE, 0x0F, 0x1E, + 0x3C, 0x3C, 0x78, 0x3D, 0xE0, 0x7F, 0xC0, 0x7F, 0x00, 0x7E, 0x00, 0xF8, 0x00, 0xF0, 0x01, 0xC0, + 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0xF0, 0x03, 0x80, 0x0C, 0x0E, 0x07, + 0x78, 0x79, 0xC3, 0xCF, 0x1C, 0x79, 0xE1, 0xCF, 0x0F, 0x70, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x3E, + 0x01, 0xE0, 0x0F, 0x01, 0xF8, 0x1F, 0x80, 0xF8, 0x07, 0x80, 0x00, 0x70, 0x1D, 0xF0, 0x7D, 0xE0, + 0xF1, 0xE3, 0xC3, 0xC7, 0x83, 0xDE, 0x07, 0xFC, 0x07, 0xF0, 0x07, 0xE0, 0x0F, 0x80, 0x0F, 0x00, + 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x1C, + 0x00, 0x10, 0x00, 0xE0, 0x77, 0x87, 0x9C, 0x3C, 0xF1, 0xC7, 0x9E, 0x1C, 0xF0, 0xF7, 0x07, 0xF8, + 0x1F, 0x80, 0xFC, 0x03, 0xE0, 0x1E, 0x00, 0xF0, 0x1F, 0xF1, 0xFB, 0xCF, 0x9C, 0x78, 0x40, 0x07, + 0x80, 0x0F, 0x80, 0x06, 0x00, 0x1C, 0x07, 0x01, 0xDF, 0x07, 0xDE, 0x0F, 0x1E, 0x3C, 0x3C, 0x78, + 0x3D, 0xE0, 0x7F, 0xC0, 0x7F, 0x00, 0x7E, 0x00, 0xF8, 0x00, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x07, + 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x0F, 0x00, 0x78, 0x01, 0xC0, 0x0C, 0x0E, 0x07, 0x78, 0x79, 0xC3, + 0xCF, 0x1C, 0x79, 0xE1, 0xCF, 0x0F, 0x70, 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xE0, 0x0F, + 0x01, 0xF8, 0x1F, 0x80, 0xF8, 0x07, 0x80, 0x00, 0x07, 0x60, 0x1F, 0xC0, 0x3F, 0x80, 0x00, 0x07, + 0x01, 0xDF, 0x07, 0xDE, 0x0F, 0x1E, 0x3C, 0x3C, 0x78, 0x3D, 0xE0, 0x7F, 0xC0, 0x7F, 0x00, 0x7E, + 0x00, 0xF8, 0x00, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x1C, 0xC0, + 0xFE, 0x0F, 0xE0, 0x00, 0x0E, 0x07, 0x78, 0x79, 0xC3, 0xCF, 0x1C, 0x79, 0xE1, 0xCF, 0x0F, 0x70, + 0x7F, 0x81, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xE0, 0x0F, 0x01, 0xF8, 0x1F, 0x80, 0xF8, 0x07, 0x80, + 0x00, 0xFE, 0xFF, 0xFF, 0x00, 0xFE, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xC0, 0x11, 0xDC, 0xE7, 0x3B, 0xC0, 0x33, 0xDC, 0xEF, 0x73, 0x80, 0x7F, 0xFF, 0xEE, - 0xE0, 0x63, 0x9C, 0xE7, 0x9C, 0xE0, 0x11, 0x8E, 0xE7, 0x39, 0xCE, 0x77, 0x9D, 0xEF, 0x78, 0x00, - 0x33, 0xBF, 0xDD, 0xEE, 0xEF, 0x77, 0x3B, 0x9C, 0x00, 0x73, 0x7B, 0xFD, 0xDE, 0xEE, 0x77, 0x73, - 0xB8, 0x00, 0x63, 0x1D, 0xE7, 0x79, 0xCE, 0x7B, 0x8E, 0xE3, 0x98, 0x00, 0x0C, 0x07, 0x81, 0xE1, - 0xFE, 0xFF, 0xFF, 0xF1, 0xE0, 0x78, 0x1E, 0x07, 0x80, 0xE0, 0x30, 0x00, 0x00, 0x0C, 0x07, 0x81, - 0xE1, 0xFF, 0xFF, 0xFF, 0xF1, 0xE0, 0x78, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x1E, 0x07, 0x80, 0xE0, - 0x30, 0x18, 0x7C, 0x7E, 0xFE, 0xFE, 0x7E, 0x7C, 0x18, 0x03, 0x8F, 0x3F, 0xFF, 0xCC, 0x00, 0x27, - 0xBD, 0xE2, 0x00, 0x20, 0xDE, 0x3F, 0xC7, 0xF8, 0xF2, 0x08, 0x20, 0xC1, 0x3C, 0x79, 0xEF, 0x1E, - 0x7B, 0xC7, 0x8E, 0x20, 0x81, 0x00, 0x27, 0xBD, 0xE2, 0x00, 0x1C, 0x02, 0x00, 0x1F, 0x81, 0xC0, - 0x07, 0xF0, 0xE0, 0x03, 0x9C, 0x70, 0x00, 0xE7, 0x1C, 0x00, 0x39, 0xCE, 0x00, 0x07, 0xE7, 0x00, - 0x01, 0xFB, 0x80, 0x00, 0x08, 0xE3, 0x07, 0x00, 0x73, 0xE3, 0xE0, 0x39, 0xFD, 0xFC, 0x0E, 0x77, - 0x77, 0x07, 0x1C, 0xDD, 0xC3, 0x87, 0x77, 0x70, 0xE1, 0xFD, 0xFC, 0x70, 0x3E, 0x3E, 0x00, 0x03, - 0x06, 0x00, 0x1C, 0x02, 0x00, 0x00, 0x1F, 0x81, 0xC0, 0x00, 0x07, 0xF0, 0xE0, 0x00, 0x03, 0x9C, - 0x70, 0x00, 0x00, 0xE7, 0x1C, 0x00, 0x00, 0x39, 0xCE, 0x00, 0x00, 0x07, 0xE7, 0x00, 0x00, 0x01, - 0xFB, 0x80, 0x00, 0x00, 0x08, 0xE3, 0x07, 0x06, 0x00, 0x73, 0xE3, 0xE7, 0xE0, 0x39, 0xFD, 0xFD, - 0xFC, 0x0E, 0x77, 0x77, 0x67, 0x07, 0x1C, 0xDD, 0xF9, 0xC3, 0x87, 0x77, 0x76, 0x70, 0xE1, 0xFD, - 0xFD, 0xFC, 0x70, 0x3E, 0x3E, 0x7E, 0x00, 0x03, 0x06, 0x06, 0x00, 0x6F, 0xEE, 0xEE, 0xE0, 0x61, - 0xBC, 0xEE, 0x3B, 0x8E, 0xE3, 0xB9, 0xCE, 0x70, 0x61, 0x8D, 0xE7, 0x3F, 0x8E, 0x7F, 0x1C, 0xEE, - 0x39, 0xDC, 0xE3, 0xB9, 0xC7, 0x00, 0xEE, 0xEF, 0x77, 0x70, 0xE3, 0x39, 0xEE, 0x3B, 0xCE, 0x73, - 0x9C, 0xE7, 0x18, 0xE3, 0x0D, 0xCF, 0x3F, 0x8E, 0x7F, 0x9C, 0xF7, 0x38, 0xEE, 0x71, 0xDC, 0x63, - 0x80, 0x1C, 0x0F, 0x87, 0x71, 0xCE, 0xE3, 0x80, 0x0F, 0x0F, 0x0F, 0x8F, 0x87, 0x83, 0xC0, 0xF0, - 0x3C, 0x0F, 0x07, 0x80, 0x78, 0x7C, 0x3C, 0x1E, 0x0F, 0x0F, 0x1E, 0x3C, 0x78, 0x70, 0x03, 0x00, - 0x0F, 0x00, 0x7E, 0x81, 0xFF, 0x81, 0xEF, 0x0F, 0xFF, 0x3D, 0xFF, 0x7B, 0xFE, 0x7F, 0xFC, 0x3D, - 0xE0, 0xFF, 0xC0, 0xFD, 0x80, 0x78, 0x00, 0x60, 0x00, 0x70, 0xDE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, - 0xE3, 0xFC, 0x7F, 0x8F, 0xF1, 0xFE, 0x3D, 0xC7, 0xB8, 0xE2, 0x0D, 0xE3, 0xFC, 0x7F, 0x8F, 0x20, - 0x00, 0x0E, 0x07, 0xF1, 0xFF, 0x3D, 0xFF, 0x1E, 0x3B, 0xC7, 0x78, 0x7E, 0x0F, 0x81, 0xE0, 0x38, - 0x07, 0x00, 0x40, 0x1C, 0x07, 0x80, 0x70, 0x04, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x03, - 0xBF, 0xFF, 0xE3, 0xFF, 0xF0, 0x3F, 0xF0, 0x0F, 0xFC, 0x0F, 0xFF, 0xCF, 0xFF, 0xF9, 0xC0, 0x0E, - 0x01, 0x00, 0xE0, 0x78, 0x1C, 0x0F, 0x03, 0x81, 0xE0, 0xF8, 0x3E, 0x1F, 0xC7, 0x79, 0xCC, 0x03, - 0x00, 0x3F, 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0xC0, 0x3F, 0x00, 0x30, 0x1F, 0x3E, 0x7C, 0xFB, 0xFF, - 0xFF, 0xFF, 0x9F, 0x3E, 0x38, 0x60, 0x7E, 0xFD, 0xFB, 0xF0, 0x00, 0x08, 0x00, 0xE0, 0x03, 0x80, - 0x1C, 0x00, 0xE0, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x03, 0x80, 0x1C, - 0x00, 0xE0, 0x07, 0x00, 0x1C, 0x00, 0x03, 0xFF, 0xFF, 0xF3, 0xCF, 0x3C, 0xF3, 0xFF, 0xFF, 0xF3, - 0xCF, 0x3C, 0xF3, 0xFF, 0xFF, 0x01, 0xF7, 0xDF, 0x1C, 0x71, 0xC7, 0x1D, 0xF7, 0xDF, 0x1C, 0x71, - 0xC7, 0x1D, 0xF7, 0xDF, 0x0F, 0x00, 0xE0, 0x7F, 0x87, 0xF1, 0xFF, 0x1F, 0xF3, 0xFF, 0x7D, 0xFF, - 0x1E, 0xF1, 0xE0, 0x3C, 0x03, 0xC0, 0xF8, 0x0F, 0x03, 0xE0, 0x3E, 0x0F, 0x80, 0xF8, 0x1E, 0x03, - 0xC0, 0x38, 0x07, 0x80, 0x70, 0x07, 0x00, 0x40, 0x0C, 0x01, 0xC0, 0x3C, 0x07, 0xC0, 0x78, 0x07, - 0x00, 0xF0, 0x04, 0x00, 0x40, 0x0F, 0x03, 0x1F, 0xE3, 0xDF, 0xF1, 0xEF, 0xFC, 0xFF, 0x1E, 0x78, - 0x0F, 0x3C, 0x0F, 0x9E, 0x0F, 0x8F, 0x0F, 0x87, 0x87, 0x83, 0xC3, 0x81, 0xE1, 0xC0, 0x60, 0x40, - 0x30, 0x70, 0x3C, 0x7C, 0x1E, 0x1C, 0x0F, 0x04, 0x00, 0x00, 0x70, 0x38, 0x78, 0x7F, 0x3C, 0x7F, - 0xDE, 0x7F, 0xFF, 0x3C, 0x7F, 0x80, 0x3F, 0xC0, 0x3D, 0xE0, 0x3E, 0xF0, 0x3E, 0x78, 0x3C, 0x1C, - 0x1E, 0x0E, 0x0F, 0x02, 0x03, 0x07, 0x83, 0xC3, 0xC1, 0xE1, 0xE0, 0xF0, 0x20, 0x00, 0x00, 0x7F, - 0xFF, 0xFF, 0xEF, 0xF0, 0x38, 0x3C, 0x1E, 0x0F, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, - 0x1E, 0x06, 0x00, 0xFC, 0x1F, 0xF3, 0xFF, 0x7F, 0xFE, 0xFF, 0xDF, 0xFB, 0xFF, 0x7F, 0xEF, 0xDD, - 0xF3, 0xB8, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0x1F, 0xDF, 0xF7, 0xFF, 0xF7, 0xFD, 0xFF, - 0x7F, 0xDD, 0xFF, 0x3F, 0xC7, 0xF0, 0xFE, 0x1F, 0xF3, 0xFF, 0x73, 0xEE, 0x7D, 0xCF, 0xB9, 0xF7, - 0xFE, 0xFF, 0x8F, 0xC0, 0x08, 0x0E, 0x1B, 0x7F, 0xF7, 0xF1, 0xFB, 0xFE, 0xDF, 0x1C, 0x06, 0x00, - 0x77, 0xBC, 0xE0, 0x00, 0x00, 0x73, 0x9C, 0xF3, 0x9C, 0xE0, 0x0F, 0xFC, 0x0F, 0xFF, 0xCF, 0xFF, - 0xF9, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x11, 0xDC, 0xE7, 0x3B, 0xC0, + 0x33, 0xDC, 0xEF, 0x73, 0x80, 0x7F, 0xFF, 0xEE, 0xE0, 0x63, 0x9C, 0xE7, 0x9C, 0xE0, 0x11, 0x8E, + 0xE7, 0x39, 0xCE, 0x77, 0x9D, 0xEF, 0x78, 0x00, 0x33, 0xBF, 0xDD, 0xEE, 0xEF, 0x77, 0x3B, 0x9C, + 0x00, 0x73, 0x7B, 0xFD, 0xDE, 0xEE, 0x77, 0x73, 0xB8, 0x00, 0x63, 0x1D, 0xE7, 0x79, 0xCE, 0x7B, + 0x8E, 0xE3, 0x98, 0x00, 0x0C, 0x07, 0x81, 0xE1, 0xFE, 0xFF, 0xFF, 0xF1, 0xE0, 0x78, 0x1E, 0x07, + 0x80, 0xE0, 0x30, 0x00, 0x00, 0x0C, 0x07, 0x81, 0xE1, 0xFF, 0xFF, 0xFF, 0xF1, 0xE0, 0x78, 0xFF, + 0xFF, 0xFF, 0xFC, 0x78, 0x1E, 0x07, 0x80, 0xE0, 0x30, 0x18, 0x7C, 0x7E, 0xFE, 0xFE, 0x7E, 0x7C, + 0x18, 0x03, 0x8F, 0x3F, 0xFF, 0xCC, 0x00, 0x27, 0xBD, 0xE2, 0x00, 0x20, 0xDE, 0x3F, 0xC7, 0xF8, + 0xF2, 0x08, 0x20, 0xC1, 0x3C, 0x79, 0xEF, 0x1E, 0x7B, 0xC7, 0x8E, 0x20, 0x81, 0x00, 0x27, 0xBD, + 0xE2, 0x00, 0x1C, 0x02, 0x00, 0x1F, 0x81, 0xC0, 0x07, 0xF0, 0xE0, 0x03, 0x9C, 0x70, 0x00, 0xE7, + 0x1C, 0x00, 0x39, 0xCE, 0x00, 0x07, 0xE7, 0x00, 0x01, 0xFB, 0x80, 0x00, 0x08, 0xE3, 0x07, 0x00, + 0x73, 0xE3, 0xE0, 0x39, 0xFD, 0xFC, 0x0E, 0x77, 0x77, 0x07, 0x1C, 0xDD, 0xC3, 0x87, 0x77, 0x70, + 0xE1, 0xFD, 0xFC, 0x70, 0x3E, 0x3E, 0x00, 0x03, 0x06, 0x00, 0x1C, 0x02, 0x00, 0x00, 0x1F, 0x81, + 0xC0, 0x00, 0x07, 0xF0, 0xE0, 0x00, 0x03, 0x9C, 0x70, 0x00, 0x00, 0xE7, 0x1C, 0x00, 0x00, 0x39, + 0xCE, 0x00, 0x00, 0x07, 0xE7, 0x00, 0x00, 0x01, 0xFB, 0x80, 0x00, 0x00, 0x08, 0xE3, 0x07, 0x06, + 0x00, 0x73, 0xE3, 0xE7, 0xE0, 0x39, 0xFD, 0xFD, 0xFC, 0x0E, 0x77, 0x77, 0x67, 0x07, 0x1C, 0xDD, + 0xF9, 0xC3, 0x87, 0x77, 0x76, 0x70, 0xE1, 0xFD, 0xFD, 0xFC, 0x70, 0x3E, 0x3E, 0x7E, 0x00, 0x03, + 0x06, 0x06, 0x00, 0x6F, 0xEE, 0xEE, 0xE0, 0x61, 0xBC, 0xEE, 0x3B, 0x8E, 0xE3, 0xB9, 0xCE, 0x70, + 0x61, 0x8D, 0xE7, 0x3F, 0x8E, 0x7F, 0x1C, 0xEE, 0x39, 0xDC, 0xE3, 0xB9, 0xC7, 0x00, 0xEE, 0xEF, + 0x77, 0x70, 0xE3, 0x39, 0xEE, 0x3B, 0xCE, 0x73, 0x9C, 0xE7, 0x18, 0xE3, 0x0D, 0xCF, 0x3F, 0x8E, + 0x7F, 0x9C, 0xF7, 0x38, 0xEE, 0x71, 0xDC, 0x63, 0x80, 0x1C, 0x0F, 0x87, 0x71, 0xCE, 0xE3, 0x80, + 0x0F, 0x0F, 0x0F, 0x8F, 0x87, 0x83, 0xC0, 0xF0, 0x3C, 0x0F, 0x07, 0x80, 0x78, 0x7C, 0x3C, 0x1E, + 0x0F, 0x0F, 0x1E, 0x3C, 0x78, 0x70, 0x03, 0x00, 0x0F, 0x00, 0x7E, 0x81, 0xFF, 0x81, 0xEF, 0x0F, + 0xFF, 0x3D, 0xFF, 0x7B, 0xFE, 0x7F, 0xFC, 0x3D, 0xE0, 0xFF, 0xC0, 0xFD, 0x80, 0x78, 0x00, 0x60, + 0x00, 0x70, 0xDE, 0x3F, 0xC7, 0xF8, 0xFF, 0x1F, 0xE3, 0xFC, 0x7F, 0x8F, 0xF1, 0xFE, 0x3D, 0xC7, + 0xB8, 0xE2, 0x0D, 0xE3, 0xFC, 0x7F, 0x8F, 0x20, 0x00, 0x0E, 0x07, 0xF1, 0xFF, 0x3D, 0xFF, 0x1E, + 0x3B, 0xC7, 0x78, 0x7E, 0x0F, 0x81, 0xE0, 0x38, 0x07, 0x00, 0x40, 0x1C, 0x07, 0x80, 0x70, 0x04, + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x03, 0xBF, 0xFF, 0xE3, 0xFF, 0xF0, 0x3F, 0xF0, 0x0F, + 0xFC, 0x0F, 0xFF, 0xCF, 0xFF, 0xF9, 0xC0, 0x0E, 0x01, 0x00, 0xE0, 0x78, 0x1C, 0x0F, 0x03, 0x81, + 0xE0, 0xF8, 0x3E, 0x1F, 0xC7, 0x79, 0xCC, 0x03, 0x00, 0x3F, 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0xC0, + 0x3F, 0x00, 0x30, 0x1F, 0x3E, 0x7C, 0xFB, 0xFF, 0xFF, 0xFF, 0x9F, 0x3E, 0x38, 0x60, 0x7E, 0xFD, + 0xFB, 0xF0, 0x00, 0x08, 0x00, 0xE0, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x03, 0x80, 0x1C, 0x00, 0xE0, + 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x1C, 0x00, 0x03, 0xFF, + 0xFF, 0xF3, 0xCF, 0x3C, 0xF3, 0xFF, 0xFF, 0xF3, 0xCF, 0x3C, 0xF3, 0xFF, 0xFF, 0x01, 0xF7, 0xDF, + 0x1C, 0x71, 0xC7, 0x1D, 0xF7, 0xDF, 0x1C, 0x71, 0xC7, 0x1D, 0xF7, 0xDF, 0x0F, 0x00, 0xE0, 0x7F, + 0x87, 0xF1, 0xFF, 0x1F, 0xF3, 0xFF, 0x7D, 0xFF, 0x1E, 0xF1, 0xE0, 0x3C, 0x03, 0xC0, 0xF8, 0x0F, + 0x03, 0xE0, 0x3E, 0x0F, 0x80, 0xF8, 0x1E, 0x03, 0xC0, 0x38, 0x07, 0x80, 0x70, 0x07, 0x00, 0x40, + 0x0C, 0x01, 0xC0, 0x3C, 0x07, 0xC0, 0x78, 0x07, 0x00, 0xF0, 0x04, 0x00, 0x40, 0x0F, 0x03, 0x1F, + 0xE3, 0xDF, 0xF1, 0xEF, 0xFC, 0xFF, 0x1E, 0x78, 0x0F, 0x3C, 0x0F, 0x9E, 0x0F, 0x8F, 0x0F, 0x87, + 0x87, 0x83, 0xC3, 0x81, 0xE1, 0xC0, 0x60, 0x40, 0x30, 0x70, 0x3C, 0x7C, 0x1E, 0x1C, 0x0F, 0x04, + 0x00, 0x00, 0x70, 0x38, 0x78, 0x7F, 0x3C, 0x7F, 0xDE, 0x7F, 0xFF, 0x3C, 0x7F, 0x80, 0x3F, 0xC0, + 0x3D, 0xE0, 0x3E, 0xF0, 0x3E, 0x78, 0x3C, 0x1C, 0x1E, 0x0E, 0x0F, 0x02, 0x03, 0x07, 0x83, 0xC3, + 0xC1, 0xE1, 0xE0, 0xF0, 0x20, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xEF, 0xF0, 0x38, 0x3C, 0x1E, 0x0F, + 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x1E, 0x06, 0x00, 0xFC, 0x1F, 0xF3, 0xFF, 0x7F, + 0xFE, 0xFF, 0xDF, 0xFB, 0xFF, 0x7F, 0xEF, 0xDD, 0xF3, 0xB8, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, + 0x00, 0x1F, 0xDF, 0xF7, 0xFF, 0xF7, 0xFD, 0xFF, 0x7F, 0xDD, 0xFF, 0x3F, 0xC7, 0xF0, 0xFE, 0x1F, + 0xF3, 0xFF, 0x73, 0xEE, 0x7D, 0xCF, 0xB9, 0xF7, 0xFE, 0xFF, 0x8F, 0xC0, 0x08, 0x0E, 0x1B, 0x7F, + 0xF7, 0xF1, 0xFB, 0xFE, 0xDF, 0x1C, 0x06, 0x00, 0x77, 0xBC, 0xE0, 0x00, 0x00, 0x73, 0x9C, 0xF3, + 0x9C, 0xE0, 0x0F, 0xFC, 0x0F, 0xFF, 0xCF, 0xFF, 0xF9, 0xC0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x18, 0x00, 0xEF, 0xFF, 0xF9, 0xFF, 0xFC, 0x1F, 0xFC, 0x00, 0x08, 0x0E, 0x1B, 0x7F, 0xF7, 0xF1, - 0xFB, 0xFE, 0xDF, 0x1C, 0x06, 0x02, 0x01, 0x86, 0xDF, 0xFD, 0xFC, 0x7E, 0xFF, 0xB7, 0xC3, 0x01, - 0x80, 0x00, 0x08, 0x00, 0x39, 0x80, 0xE7, 0x83, 0x8F, 0x8E, 0x1E, 0x1C, 0x18, 0x70, 0x01, 0xC0, - 0x03, 0x80, 0x0E, 0x00, 0x38, 0x70, 0x71, 0xF1, 0xC3, 0xE7, 0x07, 0x8E, 0x02, 0x38, 0x00, 0x18, - 0x01, 0xF8, 0x0F, 0xFF, 0xBF, 0xFF, 0x03, 0xF8, 0x01, 0x80, 0x0F, 0xFC, 0x0F, 0xFF, 0xCF, 0xFF, - 0xF9, 0xC0, 0x0E, 0x07, 0x03, 0xFC, 0x3F, 0xE3, 0xFE, 0x7F, 0xF7, 0xFF, 0x7F, 0xF3, 0xFE, 0x3F, - 0xE3, 0xFE, 0x07, 0x00, 0x61, 0x8C, 0x37, 0x9C, 0xF3, 0xB8, 0xE7, 0x9D, 0xC7, 0x38, 0xEE, 0x39, - 0xC7, 0x73, 0x8E, 0x3B, 0x9C, 0x71, 0x80, 0x1E, 0x0F, 0xC7, 0xF9, 0xCE, 0xF3, 0xDC, 0xE7, 0x39, - 0xFE, 0x3F, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFC, 0x07, 0x03, 0xC1, 0xF0, 0xFC, 0x3F, 0x1D, 0xC7, - 0xF9, 0xFE, 0x07, 0x00, 0x7E, 0xFE, 0xFE, 0xFC, 0xFF, 0xE7, 0xE7, 0xFF, 0x7E, 0x00, 0x1E, 0x1F, - 0x9F, 0xEF, 0xE7, 0xFB, 0x9D, 0xCE, 0xFF, 0x3F, 0x00, 0x00, 0x7F, 0xFF, 0x7F, 0x0F, 0x0E, 0x1C, - 0x1C, 0x38, 0x78, 0x1E, 0x3F, 0x9F, 0xEF, 0xF3, 0xF3, 0xFD, 0xCE, 0xFF, 0x3F, 0x00, 0x00, 0x1E, - 0x3F, 0x9F, 0xFE, 0x77, 0xFB, 0xFD, 0xDE, 0xFE, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x38, 0xFF, 0xFF, - 0x38, 0x38, 0x00, 0xFF, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x03, 0x9D, 0xCE, 0x73, 0x9C, - 0xE3, 0x9C, 0x60, 0x03, 0x9C, 0xE3, 0x9C, 0xE7, 0x7B, 0x9C, 0xC0, 0xDC, 0xFE, 0xFF, 0xEF, 0xEF, - 0xEF, 0xEF, 0x1E, 0x0F, 0xC7, 0xF9, 0xCE, 0xF3, 0xDC, 0xE7, 0x39, 0xFE, 0x3F, 0x00, 0x00, 0x1D, - 0xF7, 0xD7, 0x1C, 0x71, 0xC7, 0x1C, 0x3E, 0x3F, 0x9F, 0xCC, 0xE0, 0xF0, 0xF0, 0xF0, 0xFF, 0x7F, - 0x80, 0x1E, 0x3F, 0x9F, 0xE0, 0xF1, 0xF0, 0xFD, 0xCE, 0xFF, 0x3F, 0x00, 0x00, 0x07, 0x03, 0xC1, - 0xF0, 0xFC, 0x3F, 0x1D, 0xC7, 0xF9, 0xFE, 0x07, 0x00, 0x7E, 0xFE, 0xFE, 0xFC, 0xFF, 0xE7, 0xE7, - 0xFF, 0x7E, 0x00, 0x1E, 0x1F, 0x9F, 0xEF, 0xE7, 0xFB, 0x9D, 0xCE, 0xFF, 0x3F, 0x00, 0x00, 0x7F, - 0xFF, 0x7F, 0x0F, 0x0E, 0x1C, 0x1C, 0x38, 0x78, 0x1E, 0x3F, 0x9F, 0xEF, 0xF3, 0xF3, 0xFD, 0xCE, - 0xFF, 0x3F, 0x00, 0x00, 0x1E, 0x3F, 0x9F, 0xFE, 0x77, 0xFB, 0xFD, 0xDE, 0xFE, 0x3E, 0x00, 0x00, - 0x00, 0x38, 0x38, 0xFF, 0xFF, 0x38, 0x38, 0x00, 0xFF, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, - 0x03, 0x9D, 0xCE, 0x73, 0x9C, 0xE3, 0x9C, 0x60, 0x03, 0x9C, 0xE3, 0x9C, 0xE7, 0x7B, 0x9C, 0xC0, - 0x3C, 0x7E, 0x7F, 0x7F, 0xFF, 0xFF, 0x7F, 0x00, 0x1C, 0x7F, 0x7F, 0xFF, 0xFF, 0x7F, 0x3E, 0x00, - 0x1C, 0x3F, 0x9F, 0xDE, 0xFF, 0x7B, 0xF8, 0xF8, 0x00, 0x67, 0x3B, 0x9F, 0x87, 0xC3, 0xE3, 0xFB, - 0xDC, 0x7C, 0xFE, 0x47, 0xFF, 0xFF, 0xE7, 0xFF, 0x7E, 0x3C, 0xE0, 0xE0, 0xFC, 0xFE, 0xFF, 0xEF, - 0xEF, 0xEF, 0xEF, 0xE0, 0xE0, 0xEE, 0xFE, 0xFC, 0xF8, 0xFC, 0xFE, 0xEE, 0xFF, 0xFF, 0xFF, 0xE0, - 0xDD, 0xCF, 0xFE, 0xFF, 0xEE, 0xEF, 0xEE, 0xFE, 0xEF, 0xEE, 0xF0, 0xDC, 0xFE, 0xFF, 0xEF, 0xEF, - 0xEF, 0xEF, 0xFC, 0xFE, 0xFF, 0xE7, 0xE7, 0xFF, 0xFE, 0xE0, 0xE0, 0xE0, 0x3C, 0x7E, 0x76, 0x7E, - 0x3F, 0x7F, 0x7E, 0x00, 0x73, 0xEF, 0xBE, 0x71, 0xC7, 0x9E, 0x00, 0x0F, 0x80, 0x7F, 0x81, 0xFF, - 0x83, 0xCF, 0x07, 0x0E, 0x0E, 0x7F, 0xDC, 0xFF, 0xB9, 0xFF, 0x7B, 0x80, 0xFF, 0xF8, 0xFF, 0xF0, - 0xFF, 0xE0, 0x38, 0x00, 0x7F, 0xC0, 0xFF, 0x81, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0xB0, 0x0F, - 0x60, 0x7F, 0x81, 0xFF, 0x87, 0xFF, 0x9F, 0x6F, 0xBC, 0xBF, 0x7B, 0x60, 0xE6, 0x81, 0xCB, 0x03, - 0xB6, 0x07, 0xE9, 0xCF, 0xF3, 0xDF, 0x6F, 0x1F, 0xFE, 0x1F, 0xF8, 0x1F, 0xE0, 0x6E, 0x00, 0xD0, - 0x00, 0x03, 0xC0, 0x1F, 0xE0, 0x7F, 0xE1, 0xFF, 0xE7, 0xC3, 0xCF, 0x03, 0x9E, 0x00, 0x39, 0xFC, - 0x73, 0xF8, 0xE7, 0xF1, 0xEF, 0x03, 0xDC, 0xE7, 0xFB, 0xC7, 0xFF, 0x87, 0xFE, 0x07, 0xF8, 0x01, - 0x80, 0x1F, 0xF9, 0xFF, 0xCF, 0xFE, 0x7F, 0xF3, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0xFF, 0x3F, 0xF9, - 0xFF, 0xCF, 0x00, 0x78, 0x07, 0xF0, 0x7F, 0x80, 0xF0, 0x07, 0x80, 0x03, 0x80, 0x7F, 0x07, 0xFC, - 0x7F, 0xE3, 0xC7, 0x9E, 0x31, 0xFF, 0x0F, 0xF8, 0x3C, 0x03, 0xFE, 0x1F, 0xF0, 0x78, 0x03, 0xC7, - 0xBF, 0xFD, 0xFF, 0xCF, 0xFE, 0x00, 0x00, 0x00, 0x38, 0x39, 0xFF, 0x1F, 0xFF, 0xEF, 0xFF, 0xF7, - 0x9F, 0x7B, 0xCF, 0x1F, 0xE7, 0x8F, 0xF3, 0xC7, 0xFB, 0xE3, 0xFD, 0xF1, 0xFE, 0xF8, 0xFF, 0xFC, - 0x7F, 0xFE, 0x3C, 0x70, 0x00, 0x38, 0x00, 0x38, 0x0C, 0x7C, 0x1E, 0x7C, 0x1E, 0x7E, 0x1E, 0x7F, - 0x1E, 0x7F, 0x1E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7B, 0xDE, 0xFF, 0xFF, 0xFF, 0xFF, 0x78, 0xFE, 0x78, - 0x7E, 0x78, 0x7E, 0x78, 0x3E, 0x78, 0x3E, 0x7E, 0x00, 0x0F, 0xFC, 0x00, 0xFF, 0xE3, 0x8F, 0xFF, - 0x38, 0xF0, 0xFF, 0xEF, 0x0F, 0xFF, 0xF0, 0xFF, 0xFF, 0x0F, 0x78, 0xFF, 0xE3, 0x8F, 0xFE, 0x38, - 0xFF, 0xC3, 0x8F, 0x00, 0x38, 0xF0, 0x03, 0x8F, 0x00, 0x3E, 0xF0, 0x03, 0xFF, 0x00, 0x1F, 0x00, - 0x00, 0x00, 0x7E, 0x00, 0x01, 0xFF, 0x80, 0x03, 0xFF, 0x80, 0x07, 0xFF, 0x80, 0x0F, 0x0F, 0x0F, - 0x1E, 0x1E, 0x7F, 0x3C, 0x3D, 0xFF, 0x78, 0xFB, 0x8E, 0xFF, 0xEF, 0x81, 0xFF, 0xCF, 0xF3, 0xFF, - 0x1F, 0xF7, 0x9E, 0x0F, 0xFF, 0x1E, 0x71, 0xFE, 0x3D, 0xF7, 0xFC, 0x3D, 0xFF, 0x78, 0x79, 0xFC, - 0x00, 0x00, 0xE0, 0x60, 0x30, 0x3B, 0x83, 0xC3, 0xDC, 0x1E, 0x1E, 0xF1, 0xF0, 0xE7, 0x8F, 0xC7, - 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xBF, 0x9C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xF3, 0xF8, - 0x3F, 0x9F, 0x81, 0xF8, 0xFC, 0x07, 0xC3, 0xE0, 0x3E, 0x1F, 0x01, 0xE0, 0xF0, 0xFF, 0xC3, 0xFF, - 0xFB, 0xFF, 0xFD, 0xFE, 0x1F, 0xFF, 0x77, 0xFF, 0xFB, 0xFF, 0xFD, 0xFF, 0xFE, 0xFF, 0xFF, 0x77, - 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xFF, 0xE0, 0x00, 0x60, 0x1F, 0xE0, 0x7F, 0x81, 0xFE, 0x0E, 0xF0, - 0xFF, 0xC7, 0xFF, 0x1F, 0x7C, 0x78, 0xF3, 0xC3, 0xCF, 0x0F, 0x3C, 0x3C, 0x78, 0xF1, 0xFF, 0xC3, - 0xFF, 0x0F, 0xFC, 0x04, 0x00, 0x00, 0x07, 0xFF, 0x1F, 0xFC, 0x7F, 0xF0, 0x01, 0xE0, 0x1F, 0xE0, - 0xFF, 0x87, 0xFE, 0x1E, 0x10, 0xF0, 0x07, 0xFE, 0x3F, 0xF8, 0x3C, 0x01, 0xFF, 0x0F, 0xF8, 0x0F, - 0x00, 0x1E, 0x10, 0x7F, 0xE0, 0xFF, 0x81, 0xFE, 0x00, 0xC0, 0x1C, 0x0F, 0x1E, 0x0F, 0x8F, 0x0F, - 0x87, 0x8F, 0x83, 0xCF, 0x81, 0xEF, 0x83, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFE, 0x3F, 0xFF, 0x0F, - 0xFC, 0x07, 0xDF, 0x03, 0xC7, 0xC1, 0xE1, 0xE0, 0xF0, 0x78, 0x78, 0x3E, 0x7F, 0xFB, 0xFF, 0xEF, - 0xFF, 0xBF, 0xFE, 0x07, 0x80, 0x1F, 0x80, 0x7E, 0x03, 0xF0, 0x1F, 0xE0, 0x7F, 0x80, 0xFC, 0x07, - 0xE0, 0x1F, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, - 0x7E, 0x00, 0x00, 0x0F, 0xFF, 0x00, 0x00, 0x3F, 0xFE, 0x00, 0x00, 0xFE, 0x7C, 0x00, 0x00, 0x78, - 0x78, 0x3C, 0x01, 0xE1, 0xE3, 0xFC, 0x07, 0x83, 0x9F, 0xF8, 0x1E, 0x0F, 0x7B, 0xE0, 0x78, 0x3F, - 0xC3, 0xC1, 0xE0, 0xFF, 0x0F, 0x3F, 0x83, 0xBC, 0x3D, 0xFE, 0x1E, 0xF0, 0xFF, 0x78, 0x7B, 0xC7, - 0xFD, 0xFF, 0xCF, 0xFE, 0x7F, 0xFF, 0x3F, 0xF9, 0xFB, 0xF8, 0xFF, 0xC1, 0x83, 0x03, 0xC8, 0x00, - 0x00, 0x0F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x03, 0x80, 0x00, 0x3F, - 0x00, 0x7F, 0xC0, 0xFF, 0xC1, 0xFF, 0xC7, 0xFF, 0xDF, 0xFF, 0xCF, 0x1E, 0x7F, 0xFF, 0xFF, 0xFE, - 0x79, 0xF0, 0xFF, 0xC1, 0xFF, 0x83, 0xFC, 0x07, 0x80, 0x0F, 0x00, 0x1E, 0x00, 0x01, 0x80, 0x03, - 0x00, 0x0F, 0x00, 0x7F, 0x81, 0xFF, 0x87, 0xFF, 0x9F, 0x6F, 0xBC, 0xCF, 0x79, 0x80, 0xE3, 0x01, - 0xC7, 0xFB, 0x8F, 0xF7, 0x9F, 0xEF, 0x33, 0xDF, 0x6F, 0x1F, 0xFE, 0x1F, 0xF8, 0x1F, 0xE0, 0x06, - 0x00, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, - 0x07, 0xBC, 0x3F, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC, 0xF1, 0xF7, 0xFF, 0xFF, 0xFF, 0xEF, 0x07, 0x9E, - 0x07, 0xF8, 0x0F, 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0x7F, 0xE3, 0x8F, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, - 0x00, 0x07, 0xFF, 0xFF, 0xFE, 0xF0, 0x07, 0x8F, 0x3F, 0xF8, 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x01, - 0x80, 0x03, 0x00, 0x0F, 0x00, 0x7F, 0x81, 0xFF, 0x87, 0xFF, 0x9F, 0x6F, 0xBC, 0xCF, 0x79, 0x80, - 0xE3, 0x01, 0xC6, 0x03, 0x8C, 0x07, 0x99, 0xCF, 0x33, 0xDF, 0x6F, 0x1F, 0xFE, 0x1F, 0xF8, 0x1F, - 0xE0, 0x06, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x7F, 0xFB, 0xFF, 0xEF, 0xFF, 0x9F, 0xFE, 0xFF, 0xFB, - 0xFF, 0xEF, 0xFF, 0x81, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, - 0x78, 0x01, 0xE0, 0x3F, 0xE7, 0xFE, 0x7F, 0xE7, 0xF8, 0x7F, 0xE7, 0xFE, 0xFF, 0xE0, 0x78, 0x7F, - 0x87, 0xF0, 0x7C, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x38, 0x07, 0x80, 0x7F, 0x07, - 0xF0, 0xFE, 0x0F, 0xA0, 0xFF, 0x0F, 0xF0, 0xF8, 0x0F, 0x8F, 0x78, 0xF7, 0x9E, 0x79, 0xE7, 0xFC, - 0x7F, 0xC7, 0xF8, 0x3C, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x3F, 0xF1, 0xFF, 0xC7, - 0xFF, 0xBD, 0xEE, 0xF7, 0xBF, 0xDE, 0xFF, 0x7B, 0xFD, 0xEF, 0x1F, 0xC0, 0x3F, 0xF0, 0x7F, 0xF0, - 0xFF, 0xE1, 0xE1, 0xE3, 0xC3, 0xC7, 0x87, 0x8F, 0x1F, 0x7F, 0xFC, 0xFF, 0xF1, 0xFF, 0x83, 0xFE, - 0x07, 0xFC, 0x0F, 0xF8, 0x07, 0x80, 0x0F, 0x00, 0x04, 0x80, 0x3F, 0x00, 0xFC, 0x0F, 0xF8, 0x7F, - 0xF1, 0xFF, 0xEF, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0x78, 0x03, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0xB0, 0x7F, 0x0F, 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, - 0xF1, 0xEF, 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, 0xEF, 0xFC, 0x1B, - 0x01, 0xB0, 0x03, 0x00, 0x07, 0x80, 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x00, 0xFF, 0xFF, - 0xFF, 0xFF, 0x7F, 0xFF, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x80, 0x01, 0x00, - 0x01, 0x00, 0x07, 0x00, 0x1F, 0x00, 0x7F, 0x01, 0xFF, 0x07, 0xFF, 0x1E, 0xEF, 0x79, 0xDF, 0xE3, - 0x9E, 0x87, 0x18, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x01, 0x80, - 0x01, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, - 0x00, 0x3C, 0x00, 0x78, 0x00, 0xF0, 0x01, 0xE0, 0x01, 0xC0, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, - 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x21, 0xC6, 0xE3, 0x9F, 0xE7, 0x7D, 0xEF, - 0xF1, 0xFF, 0xC1, 0xFF, 0x01, 0xFC, 0x01, 0xF0, 0x01, 0xC0, 0x00, 0x00, 0x02, 0x00, 0x80, 0x07, - 0x01, 0xC0, 0x0F, 0x81, 0xE0, 0x1F, 0x00, 0xF0, 0x3E, 0x00, 0x78, 0x7C, 0x00, 0x3C, 0xFF, 0xFF, - 0xFE, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFE, 0x3C, 0x00, 0x3C, 0x1E, 0x00, 0x78, 0x0F, 0x00, 0xF0, - 0x07, 0x81, 0xE0, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x1C, 0x00, 0xF8, 0x07, 0xF0, - 0x3F, 0xE1, 0xFF, 0xCF, 0xF7, 0xBD, 0xEF, 0x67, 0x98, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, - 0x1E, 0x00, 0x78, 0x01, 0xE0, 0xE7, 0x9B, 0xDE, 0xF7, 0xF7, 0x8F, 0xFC, 0x1F, 0xE0, 0x3F, 0x00, - 0x78, 0x00, 0xC0, 0x00, 0x00, 0xFF, 0xEF, 0xFE, 0xFF, 0xEF, 0xFE, 0xFC, 0x0F, 0xE0, 0xEF, 0x0E, - 0x78, 0xE3, 0xCE, 0x1E, 0xE0, 0xF4, 0x06, 0x00, 0x00, 0x7F, 0xF7, 0xFF, 0x7F, 0xF7, 0xFF, 0x03, - 0xF0, 0x7F, 0x0F, 0x71, 0xE7, 0x3C, 0x77, 0x87, 0xF0, 0x76, 0x02, 0x00, 0x00, 0x00, 0x06, 0x07, - 0xF0, 0x77, 0x87, 0x3C, 0x71, 0xE7, 0x0F, 0x70, 0x7F, 0x03, 0xF7, 0xFF, 0x7F, 0xF7, 0xFF, 0x00, - 0x0E, 0x06, 0xE0, 0xFE, 0x1E, 0xE3, 0xCE, 0x78, 0xEF, 0x0F, 0xE0, 0xFC, 0x0F, 0xFE, 0xFF, 0xEF, - 0xFE, 0x00, 0x03, 0x80, 0x00, 0x1F, 0x00, 0xC0, 0xFC, 0x0F, 0x01, 0xE0, 0xF8, 0x07, 0x0F, 0x80, - 0x3C, 0xF8, 0x01, 0xCF, 0x80, 0x1E, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0x1F, 0xFF, 0xE0, 0x78, 0x00, - 0x01, 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1E, 0x00, 0x00, 0x70, 0x00, 0x01, 0x00, 0x00, 0x0E, 0x00, - 0x00, 0xF8, 0x00, 0x07, 0xE0, 0x60, 0x1E, 0x03, 0xC0, 0x70, 0x0F, 0x83, 0xC0, 0x1F, 0x07, 0x00, - 0x3E, 0x1E, 0x00, 0x7C, 0x7F, 0xFF, 0xF8, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0x00, 0x00, 0x78, 0x00, - 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xF0, 0x00, 0x03, 0x80, 0x00, 0x04, 0x00, 0x08, 0x00, 0x0C, - 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0xFE, 0x70, 0x07, 0x18, 0x03, - 0x84, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, - 0x08, 0x00, 0x06, 0x00, 0x03, 0x80, 0x01, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xC0, 0x1C, - 0xE0, 0x0C, 0x70, 0x04, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, - 0x00, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x03, 0x38, 0x3D, 0xC3, 0xCE, 0x3C, 0x73, - 0xFF, 0x9F, 0xFC, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x18, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x38, 0x00, - 0x1C, 0x00, 0x0E, 0x00, 0x87, 0x00, 0x63, 0x80, 0x39, 0xC0, 0x1E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xC0, 0x01, 0xC0, 0x00, 0xC0, 0x00, 0x40, 0x00, 0x00, 0xFF, 0xE1, 0xFF, 0xC3, 0xFF, 0x87, - 0xFF, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x3F, - 0xF0, 0x7F, 0xC0, 0x7F, 0x00, 0x7C, 0x00, 0x70, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0xC0, 0x00, - 0xE0, 0x00, 0x70, 0x80, 0x38, 0xC0, 0x1C, 0xE0, 0x0E, 0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, - 0xFF, 0xE7, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x7F, 0xE0, 0x1F, - 0x3E, 0x03, 0x81, 0xE0, 0xF0, 0x1C, 0x1C, 0x03, 0xCB, 0xB0, 0x7B, 0xFF, 0x00, 0xFF, 0xE0, 0x0F, - 0xF8, 0x00, 0xFE, 0x00, 0x0F, 0x80, 0x00, 0x60, 0x00, 0x0F, 0xE0, 0x07, 0xFF, 0x00, 0xF9, 0xF0, - 0x3C, 0x0E, 0x07, 0x01, 0xE1, 0xE0, 0x1C, 0x3C, 0x1B, 0xA0, 0x07, 0xFE, 0x00, 0xFF, 0xC0, 0x0F, - 0xF0, 0x00, 0xFC, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x07, 0x80, 0x0F, 0x80, - 0x1F, 0xF0, 0x1F, 0xF8, 0x0F, 0xFC, 0x07, 0x9E, 0x03, 0x8E, 0x01, 0x8F, 0x00, 0x07, 0xE0, 0x07, - 0xE0, 0x07, 0xF0, 0x0F, 0x70, 0x0E, 0x78, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xE0, 0x01, 0x00, - 0x01, 0x80, 0x01, 0xC0, 0x01, 0xE0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xF0, 0x79, 0xE0, 0x71, 0xC0, - 0xF1, 0x80, 0xE0, 0x00, 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0x70, 0x0E, 0x78, 0x3E, 0x3F, 0xFC, - 0x1F, 0xF8, 0x0F, 0xE0, 0x00, 0x60, 0x01, 0xE0, 0x03, 0xE0, 0x03, 0xEF, 0xFF, 0xFF, 0xFF, 0xC3, - 0x1F, 0x0F, 0x7C, 0x3E, 0xF0, 0xF8, 0xC3, 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x07, 0xC0, 0x07, 0x80, - 0x06, 0x00, 0x0C, 0x00, 0x3C, 0x00, 0xF8, 0x03, 0xE0, 0x0F, 0xFF, 0xFF, 0xFF, 0xDF, 0x18, 0x1F, - 0x78, 0x1E, 0xF8, 0x18, 0xFB, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0x30, - 0x00, 0x00, 0x03, 0x00, 0x07, 0x80, 0x0F, 0x80, 0x1F, 0x00, 0x3F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, - 0xF0, 0x00, 0x7F, 0xFF, 0x3F, 0xFF, 0x1F, 0xFE, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x80, 0x01, 0x00, - 0x00, 0x00, 0x01, 0x80, 0x01, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0xFF, 0xF8, 0xFF, 0xFC, 0xFF, 0xFE, - 0x00, 0x0F, 0xFF, 0xFE, 0xFF, 0xFC, 0xFF, 0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x01, 0xC0, 0x00, 0x80, - 0x00, 0x00, 0x00, 0x03, 0x01, 0x80, 0x07, 0x81, 0xC0, 0x0F, 0x81, 0xE0, 0x1F, 0x00, 0xF0, 0x3F, - 0xFF, 0xF8, 0x7F, 0xFF, 0xFC, 0xFF, 0xFF, 0xFE, 0xF0, 0x00, 0x0F, 0x7F, 0xFF, 0xFE, 0x3F, 0xFF, - 0xFC, 0x1F, 0xFF, 0xF8, 0x0F, 0x00, 0xF0, 0x07, 0x81, 0xE0, 0x03, 0x81, 0xC0, 0x01, 0x00, 0x80, - 0x04, 0x01, 0xC0, 0x7C, 0x1F, 0xC7, 0xFC, 0xFF, 0x83, 0x81, 0xFC, 0x3F, 0x87, 0xF0, 0xFE, 0x1F, - 0xC3, 0xF8, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x1F, 0xC3, 0xF8, 0x7F, 0x03, 0x81, - 0xFC, 0x3F, 0x87, 0xF0, 0x38, 0x3F, 0xF7, 0xFC, 0x7F, 0x07, 0xC0, 0x70, 0x04, 0x00, 0x60, 0x40, - 0x1E, 0x18, 0x03, 0xC7, 0x00, 0x79, 0xE0, 0x0F, 0x7F, 0xFF, 0xEF, 0xFF, 0xFD, 0xFF, 0xFF, 0x9E, - 0x00, 0xF1, 0xC0, 0x1E, 0x18, 0x01, 0x80, 0x00, 0x00, 0x00, 0x20, 0xE0, 0x06, 0x1C, 0x00, 0xE3, - 0x80, 0x1E, 0x77, 0xFF, 0xEF, 0xFF, 0xFD, 0xDF, 0xFF, 0x38, 0x01, 0xC7, 0x00, 0x30, 0xE0, 0x04, - 0x1C, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x3E, 0x00, 0x03, 0xF8, 0x00, 0x3D, - 0xE0, 0x03, 0xC7, 0x80, 0x3C, 0x1E, 0x03, 0xC0, 0x78, 0x3F, 0x07, 0xE3, 0xF8, 0x3F, 0x9F, 0xC1, - 0xFC, 0x0E, 0x0E, 0x00, 0x70, 0x70, 0x03, 0x83, 0x80, 0x1C, 0x1C, 0x00, 0xFF, 0xE0, 0x07, 0xFF, - 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x3E, 0x00, 0x03, 0xF8, 0x00, 0x3D, 0xE0, 0x03, 0xC7, - 0x80, 0x3C, 0x1E, 0x03, 0xC0, 0x78, 0x3F, 0x07, 0xE3, 0xF8, 0x3F, 0x9F, 0xC1, 0xFC, 0x0E, 0x0E, - 0x00, 0x7F, 0xF0, 0x03, 0xFF, 0x80, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x00, 0x3F, 0xF8, - 0x01, 0xC1, 0xC0, 0x0F, 0xFE, 0x00, 0x7F, 0xF0, 0x00, 0x1E, 0x03, 0xF8, 0x3F, 0xC3, 0x3E, 0x01, - 0xE1, 0xFE, 0x3F, 0xE7, 0xFF, 0x79, 0xFF, 0x0F, 0xF0, 0xFF, 0x0E, 0xF1, 0xE7, 0xFE, 0x7F, 0xC3, - 0xF8, 0x06, 0x00, 0x07, 0x88, 0x7F, 0xE3, 0xFF, 0x0F, 0xFE, 0x78, 0xF9, 0xE6, 0xE7, 0x33, 0x9F, - 0x8E, 0x7C, 0x78, 0xFF, 0xC3, 0xFF, 0x1B, 0xF8, 0x43, 0x00, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, - 0x7F, 0x00, 0xFE, 0x01, 0xFE, 0x07, 0xBC, 0x0F, 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xF1, 0xE1, 0xE1, - 0xE7, 0x83, 0xCF, 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xFF, - 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, - 0xF0, 0xFF, 0x0F, 0xE0, 0xF0, 0x7F, 0xF7, 0xFF, 0xFF, 0xFE, 0xFF, 0xF3, 0xC0, 0x0F, 0x00, 0x7C, - 0x01, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF8, 0x0F, 0x80, 0x7F, - 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xEF, 0xFF, 0xFF, 0xF7, 0xFE, 0x00, 0x1C, 0x00, 0x78, 0x00, 0xE0, - 0x03, 0x80, 0x0E, 0x00, 0x3C, 0x00, 0x70, 0x01, 0xC0, 0x07, 0x00, 0x1E, 0x00, 0x38, 0x00, 0xE0, - 0x03, 0x80, 0x07, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x60, 0x07, 0x00, 0xF0, 0x0E, 0x00, 0xE0, 0x0E, - 0x01, 0xC0, 0x1C, 0xF9, 0xCF, 0xB8, 0xFB, 0x81, 0xF8, 0x1F, 0x81, 0xF0, 0x0F, 0x00, 0xF0, 0x00, - 0x00, 0x01, 0xF0, 0xFC, 0x3F, 0xDF, 0xE7, 0xFF, 0xFE, 0x71, 0xF8, 0xFF, 0x0F, 0x0F, 0x71, 0xF8, - 0xF7, 0xFF, 0xFE, 0x7F, 0xFF, 0xE3, 0xF8, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x07, 0xC7, 0xE3, 0xF1, - 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x0F, - 0x1F, 0x8F, 0xC7, 0xC0, 0x27, 0xBD, 0xE2, 0x00, 0x00, 0x27, 0xBD, 0xE2, 0x00, 0x38, 0x77, 0xE7, - 0xFF, 0xFE, 0x7E, 0xE3, 0xC3, 0xC7, 0x7E, 0xFF, 0xFF, 0xE7, 0xE6, 0x1C, 0x01, 0xC0, 0x38, 0x7F, - 0xE7, 0xFE, 0x7F, 0xE0, 0x70, 0x0E, 0x07, 0xFE, 0x7F, 0xE7, 0xFE, 0x1C, 0x03, 0x80, 0x00, 0x00, - 0x0F, 0x0F, 0xF7, 0xFE, 0xFF, 0x0F, 0xE0, 0x7F, 0xE0, 0xFF, 0x01, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, - 0x7F, 0xE0, 0x00, 0x0F, 0x00, 0xFF, 0x07, 0xFE, 0x0F, 0xF0, 0x7F, 0x7F, 0xEF, 0xF0, 0xF8, 0x00, - 0x00, 0xFF, 0xFF, 0xFF, 0x7F, 0xE0, 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, - 0x80, 0x0F, 0x80, 0x7E, 0x00, 0xF0, 0x00, 0x78, 0x0F, 0x00, 0x01, 0xE0, 0xF0, 0x00, 0x07, 0x87, - 0x00, 0xE0, 0x1C, 0x70, 0x07, 0x00, 0xF3, 0x80, 0x38, 0x03, 0xBC, 0x01, 0xC0, 0x1F, 0xC0, 0x0E, - 0x00, 0x7E, 0x00, 0x70, 0x03, 0xF0, 0xFF, 0xFE, 0x1F, 0x87, 0xFF, 0xF0, 0xFC, 0x3F, 0xFF, 0x87, - 0xE0, 0x07, 0x00, 0x3F, 0x00, 0x38, 0x01, 0xFC, 0x01, 0xC0, 0x1C, 0xE0, 0x0E, 0x00, 0xE7, 0x80, - 0x70, 0x0F, 0x1C, 0x03, 0x80, 0x70, 0xF0, 0x00, 0x07, 0x83, 0xC0, 0x00, 0x78, 0x0F, 0x80, 0x0F, - 0x80, 0x3F, 0x01, 0xF8, 0x00, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, - 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x80, 0x0F, 0x80, 0x7E, 0x00, 0xF0, - 0x00, 0x78, 0x0F, 0x00, 0x01, 0xE0, 0xF0, 0x00, 0x07, 0x87, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, - 0xF3, 0x80, 0x00, 0x03, 0xBC, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x7E, 0x07, 0xFF, 0x03, 0xF0, - 0x3F, 0xF8, 0x1F, 0x81, 0xFF, 0xC0, 0xFC, 0x0F, 0xFE, 0x07, 0xE0, 0x00, 0x00, 0x3F, 0x00, 0x00, - 0x01, 0xFC, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0xE7, 0x80, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x70, - 0xF0, 0x00, 0x07, 0x83, 0xC0, 0x00, 0x78, 0x0F, 0x80, 0x0F, 0x80, 0x3F, 0x01, 0xF8, 0x00, 0xFF, - 0xFF, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, - 0xC0, 0x00, 0xFF, 0xFF, 0x80, 0x0F, 0x80, 0x7E, 0x00, 0xF0, 0x00, 0x78, 0x0F, 0x00, 0x01, 0xE0, - 0xF0, 0x00, 0x07, 0x87, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0xF3, 0x81, 0x83, 0x03, 0xBC, 0x1E, - 0x3C, 0x1F, 0xC0, 0x7F, 0xC0, 0x7E, 0x01, 0xFC, 0x03, 0xF0, 0x07, 0xC0, 0x1F, 0x80, 0x3E, 0x00, - 0xFC, 0x03, 0xF8, 0x07, 0xE0, 0x3F, 0xE0, 0x3F, 0x03, 0xC7, 0x81, 0xFC, 0x0C, 0x18, 0x1C, 0xE0, - 0x00, 0x00, 0xE7, 0x80, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x07, 0x83, 0xC0, 0x00, - 0x78, 0x0F, 0x80, 0x0F, 0x80, 0x3F, 0x01, 0xF8, 0x00, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xF0, 0x00, - 0x01, 0xFC, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x80, 0x0F, - 0x80, 0x7E, 0x00, 0xF0, 0x00, 0x78, 0x0F, 0x00, 0x01, 0xE0, 0xF0, 0x00, 0x07, 0x87, 0x00, 0x00, - 0x1C, 0x70, 0x00, 0x60, 0xF3, 0x80, 0x07, 0x83, 0xBC, 0x00, 0x3C, 0x1F, 0xC0, 0x03, 0xC0, 0x7E, - 0x00, 0x3C, 0x03, 0xF0, 0x03, 0xC0, 0x1F, 0x80, 0x3E, 0x00, 0xFC, 0x03, 0xE0, 0x07, 0xE0, 0x1E, - 0x00, 0x3F, 0x01, 0xE0, 0x01, 0xFC, 0x1E, 0x00, 0x1C, 0xE0, 0xE0, 0x00, 0xE7, 0x83, 0x00, 0x0F, - 0x1C, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x07, 0x83, 0xC0, 0x00, 0x78, 0x0F, 0x80, 0x0F, 0x80, 0x3F, - 0x01, 0xF8, 0x00, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x1F, 0x71, - 0xFF, 0xCE, 0x3E, 0x70, 0xE3, 0x80, 0x7F, 0xFF, 0xFF, 0xE7, 0x0F, 0x38, 0x79, 0xC3, 0xCE, 0x1E, - 0x70, 0xF3, 0x87, 0x9C, 0x3C, 0xE1, 0xE7, 0x0F, 0x1F, 0x78, 0xFD, 0xE3, 0x87, 0x8E, 0x1E, 0x38, - 0x7B, 0xFD, 0xEF, 0xF7, 0x8E, 0x1E, 0x38, 0x78, 0xE1, 0xE3, 0x87, 0x8E, 0x1E, 0x38, 0x78, 0xE1, - 0xE3, 0x87, 0xCE, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0xEF, 0xFF, 0xF9, 0xFF, 0xFC, 0x1F, + 0xFC, 0x00, 0x08, 0x0E, 0x1B, 0x7F, 0xF7, 0xF1, 0xFB, 0xFE, 0xDF, 0x1C, 0x06, 0x02, 0x01, 0x86, + 0xDF, 0xFD, 0xFC, 0x7E, 0xFF, 0xB7, 0xC3, 0x01, 0x80, 0x00, 0x08, 0x00, 0x39, 0x80, 0xE7, 0x83, + 0x8F, 0x8E, 0x1E, 0x1C, 0x18, 0x70, 0x01, 0xC0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x70, 0x71, 0xF1, + 0xC3, 0xE7, 0x07, 0x8E, 0x02, 0x38, 0x00, 0x18, 0x01, 0xF8, 0x0F, 0xFF, 0xBF, 0xFF, 0x03, 0xF8, + 0x01, 0x80, 0x0F, 0xFC, 0x0F, 0xFF, 0xCF, 0xFF, 0xF9, 0xC0, 0x0E, 0x07, 0x03, 0xFC, 0x3F, 0xE3, + 0xFE, 0x7F, 0xF7, 0xFF, 0x7F, 0xF3, 0xFE, 0x3F, 0xE3, 0xFE, 0x07, 0x00, 0x61, 0x8C, 0x37, 0x9C, + 0xF3, 0xB8, 0xE7, 0x9D, 0xC7, 0x38, 0xEE, 0x39, 0xC7, 0x73, 0x8E, 0x3B, 0x9C, 0x71, 0x80, 0x1E, + 0x0F, 0xC7, 0xF9, 0xCE, 0xF3, 0xDC, 0xE7, 0x39, 0xFE, 0x3F, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFC, + 0x07, 0x03, 0xC1, 0xF0, 0xFC, 0x3F, 0x1D, 0xC7, 0xF9, 0xFE, 0x07, 0x00, 0x7E, 0xFE, 0xFE, 0xFC, + 0xFF, 0xE7, 0xE7, 0xFF, 0x7E, 0x00, 0x1E, 0x1F, 0x9F, 0xEF, 0xE7, 0xFB, 0x9D, 0xCE, 0xFF, 0x3F, + 0x00, 0x00, 0x7F, 0xFF, 0x7F, 0x0F, 0x0E, 0x1C, 0x1C, 0x38, 0x78, 0x1E, 0x3F, 0x9F, 0xEF, 0xF3, + 0xF3, 0xFD, 0xCE, 0xFF, 0x3F, 0x00, 0x00, 0x1E, 0x3F, 0x9F, 0xFE, 0x77, 0xFB, 0xFD, 0xDE, 0xFE, + 0x3E, 0x00, 0x00, 0x00, 0x38, 0x38, 0xFF, 0xFF, 0x38, 0x38, 0x00, 0xFF, 0xFF, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0x03, 0x9D, 0xCE, 0x73, 0x9C, 0xE3, 0x9C, 0x60, 0x03, 0x9C, 0xE3, 0x9C, 0xE7, + 0x7B, 0x9C, 0xC0, 0xDC, 0xFE, 0xFF, 0xEF, 0xEF, 0xEF, 0xEF, 0x1E, 0x0F, 0xC7, 0xF9, 0xCE, 0xF3, + 0xDC, 0xE7, 0x39, 0xFE, 0x3F, 0x00, 0x00, 0x1D, 0xF7, 0xD7, 0x1C, 0x71, 0xC7, 0x1C, 0x3E, 0x3F, + 0x9F, 0xCC, 0xE0, 0xF0, 0xF0, 0xF0, 0xFF, 0x7F, 0x80, 0x1E, 0x3F, 0x9F, 0xE0, 0xF1, 0xF0, 0xFD, + 0xCE, 0xFF, 0x3F, 0x00, 0x00, 0x07, 0x03, 0xC1, 0xF0, 0xFC, 0x3F, 0x1D, 0xC7, 0xF9, 0xFE, 0x07, + 0x00, 0x7E, 0xFE, 0xFE, 0xFC, 0xFF, 0xE7, 0xE7, 0xFF, 0x7E, 0x00, 0x1E, 0x1F, 0x9F, 0xEF, 0xE7, + 0xFB, 0x9D, 0xCE, 0xFF, 0x3F, 0x00, 0x00, 0x7F, 0xFF, 0x7F, 0x0F, 0x0E, 0x1C, 0x1C, 0x38, 0x78, + 0x1E, 0x3F, 0x9F, 0xEF, 0xF3, 0xF3, 0xFD, 0xCE, 0xFF, 0x3F, 0x00, 0x00, 0x1E, 0x3F, 0x9F, 0xFE, + 0x77, 0xFB, 0xFD, 0xDE, 0xFE, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x38, 0xFF, 0xFF, 0x38, 0x38, 0x00, + 0xFF, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0x03, 0x9D, 0xCE, 0x73, 0x9C, 0xE3, 0x9C, 0x60, + 0x03, 0x9C, 0xE3, 0x9C, 0xE7, 0x7B, 0x9C, 0xC0, 0x3C, 0x7E, 0x7F, 0x7F, 0xFF, 0xFF, 0x7F, 0x00, + 0x1C, 0x7F, 0x7F, 0xFF, 0xFF, 0x7F, 0x3E, 0x00, 0x1C, 0x3F, 0x9F, 0xDE, 0xFF, 0x7B, 0xF8, 0xF8, + 0x00, 0x67, 0x3B, 0x9F, 0x87, 0xC3, 0xE3, 0xFB, 0xDC, 0x7C, 0xFE, 0x47, 0xFF, 0xFF, 0xE7, 0xFF, + 0x7E, 0x3C, 0xE0, 0xE0, 0xFC, 0xFE, 0xFF, 0xEF, 0xEF, 0xEF, 0xEF, 0xE0, 0xE0, 0xEE, 0xFE, 0xFC, + 0xF8, 0xFC, 0xFE, 0xEE, 0xFF, 0xFF, 0xFF, 0xE0, 0xDD, 0xCF, 0xFE, 0xFF, 0xEE, 0xEF, 0xEE, 0xFE, + 0xEF, 0xEE, 0xF0, 0xDC, 0xFE, 0xFF, 0xEF, 0xEF, 0xEF, 0xEF, 0xFC, 0xFE, 0xFF, 0xE7, 0xE7, 0xFF, + 0xFE, 0xE0, 0xE0, 0xE0, 0x3C, 0x7E, 0x76, 0x7E, 0x3F, 0x7F, 0x7E, 0x00, 0x73, 0xEF, 0xBE, 0x71, + 0xC7, 0x9E, 0x00, 0x0F, 0x80, 0x7F, 0x81, 0xFF, 0x83, 0xCF, 0x07, 0x0E, 0x0E, 0x7F, 0xDC, 0xFF, + 0xB9, 0xFF, 0x7B, 0x80, 0xFF, 0xF8, 0xFF, 0xF0, 0xFF, 0xE0, 0x38, 0x00, 0x7F, 0xC0, 0xFF, 0x81, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0xB0, 0x0F, 0x60, 0x7F, 0x81, 0xFF, 0x87, 0xFF, 0x9F, 0x6F, + 0xBC, 0xBF, 0x7B, 0x60, 0xE6, 0x81, 0xCB, 0x03, 0xB6, 0x07, 0xE9, 0xCF, 0xF3, 0xDF, 0x6F, 0x1F, + 0xFE, 0x1F, 0xF8, 0x1F, 0xE0, 0x6E, 0x00, 0xD0, 0x00, 0x03, 0xC0, 0x1F, 0xE0, 0x7F, 0xE1, 0xFF, + 0xE7, 0xC3, 0xCF, 0x03, 0x9E, 0x00, 0x39, 0xFC, 0x73, 0xF8, 0xE7, 0xF1, 0xEF, 0x03, 0xDC, 0xE7, + 0xFB, 0xC7, 0xFF, 0x87, 0xFE, 0x07, 0xF8, 0x01, 0x80, 0x1F, 0xF9, 0xFF, 0xCF, 0xFE, 0x7F, 0xF3, + 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0xFF, 0x3F, 0xF9, 0xFF, 0xCF, 0x00, 0x78, 0x07, 0xF0, 0x7F, 0x80, + 0xF0, 0x07, 0x80, 0x03, 0x80, 0x7F, 0x07, 0xFC, 0x7F, 0xE3, 0xC7, 0x9E, 0x31, 0xFF, 0x0F, 0xF8, + 0x3C, 0x03, 0xFE, 0x1F, 0xF0, 0x78, 0x03, 0xC7, 0xBF, 0xFD, 0xFF, 0xCF, 0xFE, 0x00, 0x00, 0x00, + 0x38, 0x39, 0xFF, 0x1F, 0xFF, 0xEF, 0xFF, 0xF7, 0x9F, 0x7B, 0xCF, 0x1F, 0xE7, 0x8F, 0xF3, 0xC7, + 0xFB, 0xE3, 0xFD, 0xF1, 0xFE, 0xF8, 0xFF, 0xFC, 0x7F, 0xFE, 0x3C, 0x70, 0x00, 0x38, 0x00, 0x38, + 0x0C, 0x7C, 0x1E, 0x7C, 0x1E, 0x7E, 0x1E, 0x7F, 0x1E, 0x7F, 0x1E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7B, + 0xDE, 0xFF, 0xFF, 0xFF, 0xFF, 0x78, 0xFE, 0x78, 0x7E, 0x78, 0x7E, 0x78, 0x3E, 0x78, 0x3E, 0x7E, + 0x00, 0x0F, 0xFC, 0x00, 0xFF, 0xE3, 0x8F, 0xFF, 0x38, 0xF0, 0xFF, 0xEF, 0x0F, 0xFF, 0xF0, 0xFF, + 0xFF, 0x0F, 0x78, 0xFF, 0xE3, 0x8F, 0xFE, 0x38, 0xFF, 0xC3, 0x8F, 0x00, 0x38, 0xF0, 0x03, 0x8F, + 0x00, 0x3E, 0xF0, 0x03, 0xFF, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x01, 0xFF, 0x80, 0x03, + 0xFF, 0x80, 0x07, 0xFF, 0x80, 0x0F, 0x0F, 0x0F, 0x1E, 0x1E, 0x7F, 0x3C, 0x3D, 0xFF, 0x78, 0xFB, + 0x8E, 0xFF, 0xEF, 0x81, 0xFF, 0xCF, 0xF3, 0xFF, 0x1F, 0xF7, 0x9E, 0x0F, 0xFF, 0x1E, 0x71, 0xFE, + 0x3D, 0xF7, 0xFC, 0x3D, 0xFF, 0x78, 0x79, 0xFC, 0x00, 0x00, 0xE0, 0x60, 0x30, 0x3B, 0x83, 0xC3, + 0xDC, 0x1E, 0x1E, 0xF1, 0xF0, 0xE7, 0x8F, 0xC7, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xBF, 0x9C, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xF3, 0xF8, 0x3F, 0x9F, 0x81, 0xF8, 0xFC, 0x07, 0xC3, 0xE0, + 0x3E, 0x1F, 0x01, 0xE0, 0xF0, 0xFF, 0xC3, 0xFF, 0xFB, 0xFF, 0xFD, 0xFE, 0x1F, 0xFF, 0x77, 0xFF, + 0xFB, 0xFF, 0xFD, 0xFF, 0xFE, 0xFF, 0xFF, 0x77, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xFF, 0xE0, 0x00, + 0x60, 0x1F, 0xE0, 0x7F, 0x81, 0xFE, 0x0E, 0xF0, 0xFF, 0xC7, 0xFF, 0x1F, 0x7C, 0x78, 0xF3, 0xC3, + 0xCF, 0x0F, 0x3C, 0x3C, 0x78, 0xF1, 0xFF, 0xC3, 0xFF, 0x0F, 0xFC, 0x04, 0x00, 0x00, 0x07, 0xFF, + 0x1F, 0xFC, 0x7F, 0xF0, 0x01, 0xE0, 0x1F, 0xE0, 0xFF, 0x87, 0xFE, 0x1E, 0x10, 0xF0, 0x07, 0xFE, + 0x3F, 0xF8, 0x3C, 0x01, 0xFF, 0x0F, 0xF8, 0x0F, 0x00, 0x1E, 0x10, 0x7F, 0xE0, 0xFF, 0x81, 0xFE, + 0x00, 0xC0, 0x1C, 0x0F, 0x1E, 0x0F, 0x8F, 0x0F, 0x87, 0x8F, 0x83, 0xCF, 0x81, 0xEF, 0x83, 0xFF, + 0xF9, 0xFF, 0xFC, 0xFF, 0xFE, 0x3F, 0xFF, 0x0F, 0xFC, 0x07, 0xDF, 0x03, 0xC7, 0xC1, 0xE1, 0xE0, + 0xF0, 0x78, 0x78, 0x3E, 0x7F, 0xFB, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0x07, 0x80, 0x1F, 0x80, 0x7E, + 0x03, 0xF0, 0x1F, 0xE0, 0x7F, 0x80, 0xFC, 0x07, 0xE0, 0x1F, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, + 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x0F, 0xFF, 0x00, 0x00, 0x3F, + 0xFE, 0x00, 0x00, 0xFE, 0x7C, 0x00, 0x00, 0x78, 0x78, 0x3C, 0x01, 0xE1, 0xE3, 0xFC, 0x07, 0x83, + 0x9F, 0xF8, 0x1E, 0x0F, 0x7B, 0xE0, 0x78, 0x3F, 0xC3, 0xC1, 0xE0, 0xFF, 0x0F, 0x3F, 0x83, 0xBC, + 0x3D, 0xFE, 0x1E, 0xF0, 0xFF, 0x78, 0x7B, 0xC7, 0xFD, 0xFF, 0xCF, 0xFE, 0x7F, 0xFF, 0x3F, 0xF9, + 0xFB, 0xF8, 0xFF, 0xC1, 0x83, 0x03, 0xC8, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x03, 0x80, 0x00, 0x3F, 0x00, 0x7F, 0xC0, 0xFF, 0xC1, 0xFF, 0xC7, 0xFF, + 0xDF, 0xFF, 0xCF, 0x1E, 0x7F, 0xFF, 0xFF, 0xFE, 0x79, 0xF0, 0xFF, 0xC1, 0xFF, 0x83, 0xFC, 0x07, + 0x80, 0x0F, 0x00, 0x1E, 0x00, 0x01, 0x80, 0x03, 0x00, 0x0F, 0x00, 0x7F, 0x81, 0xFF, 0x87, 0xFF, + 0x9F, 0x6F, 0xBC, 0xCF, 0x79, 0x80, 0xE3, 0x01, 0xC7, 0xFB, 0x8F, 0xF7, 0x9F, 0xEF, 0x33, 0xDF, + 0x6F, 0x1F, 0xFE, 0x1F, 0xF8, 0x1F, 0xE0, 0x06, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x80, 0x0F, + 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xDE, 0x07, 0xBC, 0x3F, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC, + 0xF1, 0xF7, 0xFF, 0xFF, 0xFF, 0xEF, 0x07, 0x9E, 0x07, 0xF8, 0x0F, 0x07, 0x80, 0xFF, 0x0F, 0xFC, + 0x7F, 0xE3, 0x8F, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x07, 0xFF, 0xFF, 0xFE, 0xF0, 0x07, 0x8F, + 0x3F, 0xF8, 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x01, 0x80, 0x03, 0x00, 0x0F, 0x00, 0x7F, 0x81, 0xFF, + 0x87, 0xFF, 0x9F, 0x6F, 0xBC, 0xCF, 0x79, 0x80, 0xE3, 0x01, 0xC6, 0x03, 0x8C, 0x07, 0x99, 0xCF, + 0x33, 0xDF, 0x6F, 0x1F, 0xFE, 0x1F, 0xF8, 0x1F, 0xE0, 0x06, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x7F, + 0xFB, 0xFF, 0xEF, 0xFF, 0x9F, 0xFE, 0xFF, 0xFB, 0xFF, 0xEF, 0xFF, 0x81, 0xE0, 0x07, 0x80, 0x1E, + 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x3F, 0xE7, 0xFE, 0x7F, 0xE7, + 0xF8, 0x7F, 0xE7, 0xFE, 0xFF, 0xE0, 0x78, 0x7F, 0x87, 0xF0, 0x7C, 0x03, 0xC0, 0x1E, 0x01, 0xF0, + 0x0F, 0x80, 0x7C, 0x38, 0x07, 0x80, 0x7F, 0x07, 0xF0, 0xFE, 0x0F, 0xA0, 0xFF, 0x0F, 0xF0, 0xF8, + 0x0F, 0x8F, 0x78, 0xF7, 0x9E, 0x79, 0xE7, 0xFC, 0x7F, 0xC7, 0xF8, 0x3C, 0x00, 0x07, 0x80, 0x1E, + 0x00, 0x78, 0x03, 0xF0, 0x3F, 0xF1, 0xFF, 0xC7, 0xFF, 0xBD, 0xEE, 0xF7, 0xBF, 0xDE, 0xFF, 0x7B, + 0xFD, 0xEF, 0x1F, 0xC0, 0x3F, 0xF0, 0x7F, 0xF0, 0xFF, 0xE1, 0xE1, 0xE3, 0xC3, 0xC7, 0x87, 0x8F, + 0x1F, 0x7F, 0xFC, 0xFF, 0xF1, 0xFF, 0x83, 0xFE, 0x07, 0xFC, 0x0F, 0xF8, 0x07, 0x80, 0x0F, 0x00, + 0x04, 0x80, 0x3F, 0x00, 0xFC, 0x0F, 0xF8, 0x7F, 0xF1, 0xFF, 0xEF, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, + 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0xB0, 0x7F, + 0x0F, 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF1, 0xEF, 0xFC, 0xFF, 0xEF, 0xFF, 0xF0, 0xFF, + 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, 0xEF, 0xFC, 0x1B, 0x01, 0xB0, 0x03, 0x00, 0x07, 0x80, 0x0F, 0x80, + 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x3C, 0x00, 0x1E, 0x00, + 0x0F, 0x00, 0x07, 0x80, 0x03, 0x80, 0x01, 0x00, 0x01, 0x00, 0x07, 0x00, 0x1F, 0x00, 0x7F, 0x01, + 0xFF, 0x07, 0xFF, 0x1E, 0xEF, 0x79, 0xDF, 0xE3, 0x9E, 0x87, 0x18, 0x0E, 0x00, 0x1C, 0x00, 0x38, + 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x01, 0x80, 0x01, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, + 0x00, 0x3C, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xF0, 0x01, 0xE0, + 0x01, 0xC0, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, + 0xE0, 0x21, 0xC6, 0xE3, 0x9F, 0xE7, 0x7D, 0xEF, 0xF1, 0xFF, 0xC1, 0xFF, 0x01, 0xFC, 0x01, 0xF0, + 0x01, 0xC0, 0x00, 0x00, 0x02, 0x00, 0x80, 0x07, 0x01, 0xC0, 0x0F, 0x81, 0xE0, 0x1F, 0x00, 0xF0, + 0x3E, 0x00, 0x78, 0x7C, 0x00, 0x3C, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFE, 0x3C, + 0x00, 0x3C, 0x1E, 0x00, 0x78, 0x0F, 0x00, 0xF0, 0x07, 0x81, 0xE0, 0x03, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x1C, 0x00, 0xF8, 0x07, 0xF0, 0x3F, 0xE1, 0xFF, 0xCF, 0xF7, 0xBD, 0xEF, 0x67, + 0x98, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0xE7, 0x9B, 0xDE, + 0xF7, 0xF7, 0x8F, 0xFC, 0x1F, 0xE0, 0x3F, 0x00, 0x78, 0x00, 0xC0, 0x00, 0x00, 0xFF, 0xEF, 0xFE, + 0xFF, 0xEF, 0xFE, 0xFC, 0x0F, 0xE0, 0xEF, 0x0E, 0x78, 0xE3, 0xCE, 0x1E, 0xE0, 0xF4, 0x06, 0x00, + 0x00, 0x7F, 0xF7, 0xFF, 0x7F, 0xF7, 0xFF, 0x03, 0xF0, 0x7F, 0x0F, 0x71, 0xE7, 0x3C, 0x77, 0x87, + 0xF0, 0x76, 0x02, 0x00, 0x00, 0x00, 0x06, 0x07, 0xF0, 0x77, 0x87, 0x3C, 0x71, 0xE7, 0x0F, 0x70, + 0x7F, 0x03, 0xF7, 0xFF, 0x7F, 0xF7, 0xFF, 0x00, 0x0E, 0x06, 0xE0, 0xFE, 0x1E, 0xE3, 0xCE, 0x78, + 0xEF, 0x0F, 0xE0, 0xFC, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0x00, 0x03, 0x80, 0x00, 0x1F, 0x00, 0xC0, + 0xFC, 0x0F, 0x01, 0xE0, 0xF8, 0x07, 0x0F, 0x80, 0x3C, 0xF8, 0x01, 0xCF, 0x80, 0x1E, 0xFF, 0xFF, + 0xF7, 0xFF, 0xFF, 0x1F, 0xFF, 0xE0, 0x78, 0x00, 0x01, 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1E, 0x00, + 0x00, 0x70, 0x00, 0x01, 0x00, 0x00, 0x0E, 0x00, 0x00, 0xF8, 0x00, 0x07, 0xE0, 0x60, 0x1E, 0x03, + 0xC0, 0x70, 0x0F, 0x83, 0xC0, 0x1F, 0x07, 0x00, 0x3E, 0x1E, 0x00, 0x7C, 0x7F, 0xFF, 0xF8, 0xFF, + 0xFF, 0xE0, 0xFF, 0xFF, 0x00, 0x00, 0x78, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xF0, 0x00, + 0x03, 0x80, 0x00, 0x04, 0x00, 0x08, 0x00, 0x0C, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x0F, 0xFF, 0xFF, + 0xFF, 0xFD, 0xFF, 0xFE, 0x70, 0x07, 0x18, 0x03, 0x84, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, + 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x08, 0x00, 0x06, 0x00, 0x03, 0x80, 0x01, 0xEF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xC0, 0x1C, 0xE0, 0x0C, 0x70, 0x04, 0x38, 0x00, 0x1C, 0x00, + 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, + 0x07, 0x03, 0x38, 0x3D, 0xC3, 0xCE, 0x3C, 0x73, 0xFF, 0x9F, 0xFC, 0x78, 0x01, 0xE0, 0x07, 0x80, + 0x18, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x87, 0x00, 0x63, 0x80, + 0x39, 0xC0, 0x1E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x01, 0xC0, 0x00, 0xC0, 0x00, 0x40, + 0x00, 0x00, 0xFF, 0xE1, 0xFF, 0xC3, 0xFF, 0x87, 0xFF, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, + 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x3F, 0xF0, 0x7F, 0xC0, 0x7F, 0x00, 0x7C, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x80, 0x38, 0xC0, 0x1C, 0xE0, + 0x0E, 0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xFF, 0xE7, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, + 0x00, 0x00, 0x00, 0xFE, 0x00, 0x7F, 0xE0, 0x1F, 0x3E, 0x03, 0x81, 0xE0, 0xF0, 0x1C, 0x1C, 0x03, + 0xCB, 0xB0, 0x7B, 0xFF, 0x00, 0xFF, 0xE0, 0x0F, 0xF8, 0x00, 0xFE, 0x00, 0x0F, 0x80, 0x00, 0x60, + 0x00, 0x0F, 0xE0, 0x07, 0xFF, 0x00, 0xF9, 0xF0, 0x3C, 0x0E, 0x07, 0x01, 0xE1, 0xE0, 0x1C, 0x3C, + 0x1B, 0xA0, 0x07, 0xFE, 0x00, 0xFF, 0xC0, 0x0F, 0xF0, 0x00, 0xFC, 0x00, 0x0F, 0x00, 0x00, 0xC0, + 0x01, 0x80, 0x03, 0x80, 0x07, 0x80, 0x0F, 0x80, 0x1F, 0xF0, 0x1F, 0xF8, 0x0F, 0xFC, 0x07, 0x9E, + 0x03, 0x8E, 0x01, 0x8F, 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0x70, 0x0E, 0x78, 0x3E, + 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xE0, 0x01, 0x00, 0x01, 0x80, 0x01, 0xC0, 0x01, 0xE0, 0x0F, 0xF0, + 0x1F, 0xF8, 0x3F, 0xF0, 0x79, 0xE0, 0x71, 0xC0, 0xF1, 0x80, 0xE0, 0x00, 0xE0, 0x07, 0xE0, 0x07, + 0xF0, 0x0F, 0x70, 0x0E, 0x78, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x0F, 0xE0, 0x00, 0x60, 0x01, 0xE0, + 0x03, 0xE0, 0x03, 0xEF, 0xFF, 0xFF, 0xFF, 0xC3, 0x1F, 0x0F, 0x7C, 0x3E, 0xF0, 0xF8, 0xC3, 0xFF, + 0xFF, 0xFF, 0xF7, 0xC0, 0x07, 0xC0, 0x07, 0x80, 0x06, 0x00, 0x0C, 0x00, 0x3C, 0x00, 0xF8, 0x03, + 0xE0, 0x0F, 0xFF, 0xFF, 0xFF, 0xDF, 0x18, 0x1F, 0x78, 0x1E, 0xF8, 0x18, 0xFB, 0xFF, 0xFF, 0xFF, + 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0x30, 0x00, 0x00, 0x03, 0x00, 0x07, 0x80, 0x0F, 0x80, + 0x1F, 0x00, 0x3F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x7F, 0xFF, 0x3F, 0xFF, 0x1F, 0xFE, + 0x0F, 0x00, 0x07, 0x80, 0x03, 0x80, 0x01, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0xC0, 0x01, 0xE0, + 0x00, 0xF0, 0xFF, 0xF8, 0xFF, 0xFC, 0xFF, 0xFE, 0x00, 0x0F, 0xFF, 0xFE, 0xFF, 0xFC, 0xFF, 0xF8, + 0x00, 0xF0, 0x01, 0xE0, 0x01, 0xC0, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x01, 0x80, 0x07, 0x81, + 0xC0, 0x0F, 0x81, 0xE0, 0x1F, 0x00, 0xF0, 0x3F, 0xFF, 0xF8, 0x7F, 0xFF, 0xFC, 0xFF, 0xFF, 0xFE, + 0xF0, 0x00, 0x0F, 0x7F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x1F, 0xFF, 0xF8, 0x0F, 0x00, 0xF0, 0x07, + 0x81, 0xE0, 0x03, 0x81, 0xC0, 0x01, 0x00, 0x80, 0x04, 0x01, 0xC0, 0x7C, 0x1F, 0xC7, 0xFC, 0xFF, + 0x83, 0x81, 0xFC, 0x3F, 0x87, 0xF0, 0xFE, 0x1F, 0xC3, 0xF8, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, + 0xC0, 0x38, 0x1F, 0xC3, 0xF8, 0x7F, 0x03, 0x81, 0xFC, 0x3F, 0x87, 0xF0, 0x38, 0x3F, 0xF7, 0xFC, + 0x7F, 0x07, 0xC0, 0x70, 0x04, 0x00, 0x60, 0x40, 0x1E, 0x18, 0x03, 0xC7, 0x00, 0x79, 0xE0, 0x0F, + 0x7F, 0xFF, 0xEF, 0xFF, 0xFD, 0xFF, 0xFF, 0x9E, 0x00, 0xF1, 0xC0, 0x1E, 0x18, 0x01, 0x80, 0x00, + 0x00, 0x00, 0x20, 0xE0, 0x06, 0x1C, 0x00, 0xE3, 0x80, 0x1E, 0x77, 0xFF, 0xEF, 0xFF, 0xFD, 0xDF, + 0xFF, 0x38, 0x01, 0xC7, 0x00, 0x30, 0xE0, 0x04, 0x1C, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x03, + 0x80, 0x00, 0x3E, 0x00, 0x03, 0xF8, 0x00, 0x3D, 0xE0, 0x03, 0xC7, 0x80, 0x3C, 0x1E, 0x03, 0xC0, + 0x78, 0x3F, 0x07, 0xE3, 0xF8, 0x3F, 0x9F, 0xC1, 0xFC, 0x0E, 0x0E, 0x00, 0x70, 0x70, 0x03, 0x83, + 0x80, 0x1C, 0x1C, 0x00, 0xFF, 0xE0, 0x07, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x3E, + 0x00, 0x03, 0xF8, 0x00, 0x3D, 0xE0, 0x03, 0xC7, 0x80, 0x3C, 0x1E, 0x03, 0xC0, 0x78, 0x3F, 0x07, + 0xE3, 0xF8, 0x3F, 0x9F, 0xC1, 0xFC, 0x0E, 0x0E, 0x00, 0x7F, 0xF0, 0x03, 0xFF, 0x80, 0x1F, 0xF8, + 0x00, 0x00, 0x00, 0x07, 0xFF, 0x00, 0x3F, 0xF8, 0x01, 0xC1, 0xC0, 0x0F, 0xFE, 0x00, 0x7F, 0xF0, + 0x00, 0x1E, 0x03, 0xF8, 0x3F, 0xC3, 0x3E, 0x01, 0xE1, 0xFE, 0x3F, 0xE7, 0xFF, 0x79, 0xFF, 0x0F, + 0xF0, 0xFF, 0x0E, 0xF1, 0xE7, 0xFE, 0x7F, 0xC3, 0xF8, 0x06, 0x00, 0x07, 0x88, 0x7F, 0xE3, 0xFF, + 0x0F, 0xFE, 0x78, 0xF9, 0xE6, 0xE7, 0x33, 0x9F, 0x8E, 0x7C, 0x78, 0xFF, 0xC3, 0xFF, 0x1B, 0xF8, + 0x43, 0x00, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xFE, 0x07, 0xBC, 0x0F, + 0x78, 0x1E, 0x78, 0x78, 0xF0, 0xF1, 0xE1, 0xE1, 0xE7, 0x83, 0xCF, 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, + 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xE0, 0xF0, 0x7F, 0xF7, 0xFF, + 0xFF, 0xFE, 0xFF, 0xF3, 0xC0, 0x0F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0xF0, 0x0F, + 0x00, 0xF0, 0x0F, 0x00, 0xF8, 0x0F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xEF, 0xFF, 0xFF, + 0xF7, 0xFE, 0x00, 0x1C, 0x00, 0x78, 0x00, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x3C, 0x00, 0x70, 0x01, + 0xC0, 0x07, 0x00, 0x1E, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80, 0x07, 0x00, 0x1C, 0x00, 0x00, 0x00, + 0x60, 0x07, 0x00, 0xF0, 0x0E, 0x00, 0xE0, 0x0E, 0x01, 0xC0, 0x1C, 0xF9, 0xCF, 0xB8, 0xFB, 0x81, + 0xF8, 0x1F, 0x81, 0xF0, 0x0F, 0x00, 0xF0, 0x00, 0x00, 0x01, 0xF0, 0xFC, 0x3F, 0xDF, 0xE7, 0xFF, + 0xFE, 0x71, 0xF8, 0xFF, 0x0F, 0x0F, 0x71, 0xF8, 0xF7, 0xFF, 0xFE, 0x7F, 0xFF, 0xE3, 0xF8, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xC7, 0xE3, 0xF1, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, + 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x0F, 0x1F, 0x8F, 0xC7, 0xC0, 0x27, 0xBD, 0xE2, 0x00, + 0x00, 0x27, 0xBD, 0xE2, 0x00, 0x38, 0x77, 0xE7, 0xFF, 0xFE, 0x7E, 0xE3, 0xC3, 0xC7, 0x7E, 0xFF, + 0xFF, 0xE7, 0xE6, 0x1C, 0x01, 0xC0, 0x38, 0x7F, 0xE7, 0xFE, 0x7F, 0xE0, 0x70, 0x0E, 0x07, 0xFE, + 0x7F, 0xE7, 0xFE, 0x1C, 0x03, 0x80, 0x00, 0x00, 0x0F, 0x0F, 0xF7, 0xFE, 0xFF, 0x0F, 0xE0, 0x7F, + 0xE0, 0xFF, 0x01, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0x7F, 0xE0, 0x00, 0x0F, 0x00, 0xFF, 0x07, 0xFE, + 0x0F, 0xF0, 0x7F, 0x7F, 0xEF, 0xF0, 0xF8, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x7F, 0xE0, 0x00, 0x3F, + 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x80, 0x0F, 0x80, 0x7E, 0x00, 0xF0, 0x00, 0x78, + 0x0F, 0x00, 0x01, 0xE0, 0xF0, 0x00, 0x07, 0x87, 0x00, 0xE0, 0x1C, 0x70, 0x07, 0x00, 0xF3, 0x80, + 0x38, 0x03, 0xBC, 0x01, 0xC0, 0x1F, 0xC0, 0x0E, 0x00, 0x7E, 0x00, 0x70, 0x03, 0xF0, 0xFF, 0xFE, + 0x1F, 0x87, 0xFF, 0xF0, 0xFC, 0x3F, 0xFF, 0x87, 0xE0, 0x07, 0x00, 0x3F, 0x00, 0x38, 0x01, 0xFC, + 0x01, 0xC0, 0x1C, 0xE0, 0x0E, 0x00, 0xE7, 0x80, 0x70, 0x0F, 0x1C, 0x03, 0x80, 0x70, 0xF0, 0x00, + 0x07, 0x83, 0xC0, 0x00, 0x78, 0x0F, 0x80, 0x0F, 0x80, 0x3F, 0x01, 0xF8, 0x00, 0xFF, 0xFF, 0x80, + 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, + 0xFF, 0xFF, 0x80, 0x0F, 0x80, 0x7E, 0x00, 0xF0, 0x00, 0x78, 0x0F, 0x00, 0x01, 0xE0, 0xF0, 0x00, + 0x07, 0x87, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0xF3, 0x80, 0x00, 0x03, 0xBC, 0x00, 0x00, 0x1F, + 0xC0, 0x00, 0x00, 0x7E, 0x07, 0xFF, 0x03, 0xF0, 0x3F, 0xF8, 0x1F, 0x81, 0xFF, 0xC0, 0xFC, 0x0F, + 0xFE, 0x07, 0xE0, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, + 0xE7, 0x80, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x07, 0x83, 0xC0, 0x00, 0x78, 0x0F, + 0x80, 0x0F, 0x80, 0x3F, 0x01, 0xF8, 0x00, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, + 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x80, 0x0F, 0x80, 0x7E, + 0x00, 0xF0, 0x00, 0x78, 0x0F, 0x00, 0x01, 0xE0, 0xF0, 0x00, 0x07, 0x87, 0x00, 0x00, 0x1C, 0x70, + 0x00, 0x00, 0xF3, 0x81, 0x83, 0x03, 0xBC, 0x1E, 0x3C, 0x1F, 0xC0, 0x7F, 0xC0, 0x7E, 0x01, 0xFC, + 0x03, 0xF0, 0x07, 0xC0, 0x1F, 0x80, 0x3E, 0x00, 0xFC, 0x03, 0xF8, 0x07, 0xE0, 0x3F, 0xE0, 0x3F, + 0x03, 0xC7, 0x81, 0xFC, 0x0C, 0x18, 0x1C, 0xE0, 0x00, 0x00, 0xE7, 0x80, 0x00, 0x0F, 0x1C, 0x00, + 0x00, 0x70, 0xF0, 0x00, 0x07, 0x83, 0xC0, 0x00, 0x78, 0x0F, 0x80, 0x0F, 0x80, 0x3F, 0x01, 0xF8, + 0x00, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x00, + 0x07, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x80, 0x0F, 0x80, 0x7E, 0x00, 0xF0, 0x00, 0x78, 0x0F, 0x00, + 0x01, 0xE0, 0xF0, 0x00, 0x07, 0x87, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x60, 0xF3, 0x80, 0x07, 0x83, + 0xBC, 0x00, 0x3C, 0x1F, 0xC0, 0x03, 0xC0, 0x7E, 0x00, 0x3C, 0x03, 0xF0, 0x03, 0xC0, 0x1F, 0x80, + 0x3E, 0x00, 0xFC, 0x03, 0xE0, 0x07, 0xE0, 0x1E, 0x00, 0x3F, 0x01, 0xE0, 0x01, 0xFC, 0x1E, 0x00, + 0x1C, 0xE0, 0xE0, 0x00, 0xE7, 0x83, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x07, 0x83, + 0xC0, 0x00, 0x78, 0x0F, 0x80, 0x0F, 0x80, 0x3F, 0x01, 0xF8, 0x00, 0xFF, 0xFF, 0x80, 0x01, 0xFF, + 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x1F, 0x71, 0xFF, 0xCE, 0x3E, 0x70, 0xE3, 0x80, 0x7F, 0xFF, + 0xFF, 0xE7, 0x0F, 0x38, 0x79, 0xC3, 0xCE, 0x1E, 0x70, 0xF3, 0x87, 0x9C, 0x3C, 0xE1, 0xE7, 0x0F, + 0x1F, 0x78, 0xFD, 0xE3, 0x87, 0x8E, 0x1E, 0x38, 0x7B, 0xFD, 0xEF, 0xF7, 0x8E, 0x1E, 0x38, 0x78, + 0xE1, 0xE3, 0x87, 0x8E, 0x1E, 0x38, 0x78, 0xE1, 0xE3, 0x87, 0xCE, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x07, 0x9C, 0x00, 0x3C, 0xE0, 0x00, 0xEF, 0x00, 0x07, 0xF8, 0x00, 0x3F, 0xE0, 0x03, 0xEF, 0xFF, + 0xFE, 0x7F, 0xFF, 0xF1, 0xFF, 0xFE, 0x01, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x1D, 0x80, 0x00, 0xFC, + 0x00, 0x03, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, + 0x00, 0x00, 0x07, 0x00, 0x01, 0x83, 0xC0, 0x00, 0xF0, 0xF0, 0x00, 0x7C, 0x3E, 0x00, 0x7F, 0x07, + 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0x8F, 0xC1, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xE0, 0x00, 0x01, 0xF8, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x07, 0x00, 0x00, 0x01, 0xC0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3C, 0x78, 0xF0, 0xE1, 0xE3, 0xC7, 0xFE, 0xFD, 0xF0, 0x00, 0x0F, 0xDF, + 0x9F, 0x1C, 0x38, 0x00, 0x00, 0x03, 0xC0, 0x70, 0x0E, 0x03, 0xE1, 0xFF, 0xBF, 0xFF, 0xBE, 0x00, + 0x00, 0x01, 0xF8, 0x3F, 0x03, 0xC0, 0x70, 0x0E, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, 0x00, + 0x3E, 0x00, 0x01, 0xF8, 0x00, 0x0F, 0xC0, 0x00, 0xFC, 0x00, 0x01, 0x80, 0x00, 0x00, 0x1E, 0x70, + 0x00, 0xF3, 0x80, 0x03, 0xBC, 0x00, 0x1F, 0xE0, 0x00, 0xFF, 0x80, 0x0F, 0xBF, 0xFF, 0xF9, 0xFF, + 0xFF, 0xC7, 0xFF, 0xF8, 0x07, 0xFE, 0x00, 0x00, 0x40, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0xC0, + 0x00, 0x01, 0xF0, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x70, 0x00, 0x00, 0x1C, 0x00, 0x06, 0x0F, 0x00, 0x03, 0xC3, 0xC0, 0x01, 0xF0, 0xF8, 0x01, + 0xFC, 0x1F, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFC, 0xFF, 0xFE, 0x3F, 0x07, 0xF8, 0x00, 0x00, 0x61, + 0x87, 0xDF, 0xFF, 0xF7, 0x00, 0x39, 0xE3, 0xCF, 0x3C, 0xF3, 0xFF, 0xFB, 0xC0, 0x00, 0x10, 0x03, + 0x00, 0x7C, 0x0F, 0x83, 0xF0, 0x7E, 0x07, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x70, 0x0E, 0x03, 0xE1, + 0xFF, 0xBF, 0xFF, 0xBE, 0x00, 0x00, 0x7C, 0x01, 0xFF, 0x01, 0xFF, 0xE3, 0xFF, 0xE0, 0x1F, 0xC0, + 0xFF, 0x87, 0xF8, 0x1F, 0x80, 0x7C, 0x00, 0xF0, 0x03, 0xCF, 0x87, 0x9F, 0x0F, 0x1C, 0x1E, 0x38, + 0x3C, 0x00, 0x7E, 0x06, 0x7F, 0xFC, 0x7F, 0xF8, 0x7F, 0xF0, 0x0E, 0x00, 0x7C, 0x00, 0x3F, 0xE0, + 0x07, 0xFF, 0x81, 0xFF, 0xF0, 0x01, 0xFC, 0x01, 0xFF, 0x01, 0xFF, 0x80, 0xFC, 0xF0, 0x7C, 0x3F, + 0x9E, 0x07, 0xFF, 0x3E, 0xFF, 0xCF, 0x80, 0xF1, 0xC0, 0x3C, 0x70, 0x0F, 0x00, 0x03, 0xF0, 0x30, + 0x7F, 0xFC, 0x0F, 0xFF, 0x01, 0xFF, 0xC0, 0x07, 0x00, 0x00, 0x03, 0xF8, 0x1F, 0xF0, 0xFF, 0xF0, + 0x3F, 0x80, 0xFC, 0x3F, 0xFF, 0xF8, 0xFF, 0x07, 0xE0, 0x00, 0x00, 0x00, 0x01, 0xF8, 0x0F, 0xC0, + 0x3E, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x7F, 0xC0, 0x7F, 0xF8, 0x03, + 0xF8, 0x01, 0xF8, 0x0F, 0xF8, 0xFF, 0xFE, 0xFF, 0x7F, 0xFC, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x1F, + 0x80, 0x1F, 0x80, 0x0F, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x10, 0x0E, 0x03, 0xF0, 0xFC, + 0x3F, 0x0F, 0x81, 0x80, 0x00, 0x02, 0x03, 0xC0, 0xF8, 0x1E, 0x03, 0xC0, 0xF0, 0x3D, 0x8F, 0x7F, + 0xDF, 0xF7, 0xF8, 0x00, 0x10, 0x00, 0xE0, 0x03, 0xF0, 0x0F, 0xC0, 0x3F, 0x00, 0xF8, 0x01, 0x80, + 0x00, 0x00, 0x02, 0x00, 0x3C, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0xC0, 0x0F, 0x00, 0x3C, 0x18, 0xF8, + 0x7F, 0xFD, 0xFF, 0xF7, 0xFF, 0xC0, 0x00, 0x06, 0x07, 0x81, 0x83, 0xF1, 0xF8, 0xFC, 0x00, 0x00, + 0x06, 0x07, 0x83, 0xC0, 0xE0, 0x78, 0x3C, 0x1C, 0x1E, 0x1F, 0x3F, 0x3F, 0x1F, 0x06, 0x00, 0x06, + 0x00, 0x70, 0x03, 0x80, 0x37, 0x03, 0xF8, 0x0D, 0x80, 0x00, 0x00, 0x00, 0x06, 0x00, 0x78, 0x03, + 0xC0, 0x0E, 0x00, 0x78, 0x03, 0xF8, 0x1F, 0xC1, 0xFE, 0x1F, 0x03, 0xF0, 0x3F, 0x01, 0xF0, 0x06, + 0x00, 0x00, 0x08, 0x06, 0x03, 0xE1, 0xF1, 0xF8, 0xFC, 0x38, 0x00, 0x00, 0x03, 0x03, 0xC1, 0xE0, + 0x70, 0x3C, 0x1E, 0x0E, 0x0F, 0x0F, 0x9F, 0x9F, 0x8F, 0x83, 0x00, 0x08, 0x00, 0xC0, 0x07, 0xC0, + 0x3F, 0x01, 0xF8, 0x1F, 0x80, 0x30, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0xC0, 0x1E, 0x00, 0x70, + 0x03, 0xC0, 0x1F, 0xC0, 0xFE, 0x0F, 0xF0, 0xF8, 0x1F, 0x81, 0xF8, 0x0F, 0x80, 0x30, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x3E, 0x00, 0x0F, 0xE0, 0x01, 0xFC, 0x00, 0x3F, 0x00, 0x03, 0xC0, 0x00, 0x3E, + 0x07, 0x01, 0xF0, 0x70, 0x0F, 0x8F, 0x00, 0x78, 0xF0, 0x03, 0xCF, 0x80, 0x3C, 0x7F, 0xFF, 0xC7, + 0xFF, 0xF8, 0x3F, 0xFF, 0x00, 0x7F, 0x80, 0x00, 0x00, 0xC0, 0x00, 0x0F, 0x80, 0x00, 0xFE, 0x00, + 0x07, 0xF0, 0x00, 0x3F, 0x00, 0x01, 0xF0, 0x00, 0x03, 0xC0, 0x1C, 0x0F, 0x80, 0x70, 0x1F, 0x03, + 0xC0, 0x3E, 0x0F, 0x00, 0x7C, 0x3E, 0x00, 0xF8, 0x7F, 0xFF, 0xF9, 0xFF, 0xFF, 0xF3, 0xFF, 0xF7, + 0xC1, 0xFF, 0x00, 0x01, 0x81, 0xE1, 0xFD, 0xFE, 0xFC, 0x3E, 0x0F, 0xC1, 0xF8, 0x3E, 0x07, 0xC0, + 0xF0, 0x3C, 0xFF, 0x3F, 0xCF, 0xE0, 0x00, 0x01, 0x80, 0x3C, 0x07, 0xF0, 0xFF, 0x0F, 0xE0, 0x7C, + 0x03, 0xE0, 0x0F, 0x80, 0x3E, 0x00, 0xF8, 0x03, 0xC0, 0x1F, 0x8F, 0xFF, 0x7F, 0xFF, 0xFB, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x07, 0x80, 0x01, 0xF8, 0x00, 0x7E, 0xC0, 0x07, 0xBE, + 0x00, 0x2F, 0xE0, 0x01, 0xFC, 0x00, 0x3F, 0x00, 0x03, 0xC0, 0x00, 0x3E, 0x07, 0x01, 0xF0, 0x70, + 0x0F, 0x8F, 0x00, 0x78, 0xF0, 0x03, 0xCF, 0x80, 0x3C, 0x7F, 0xFF, 0xC7, 0xFF, 0xF8, 0x3F, 0xFF, + 0x00, 0x7F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0xF8, 0x00, 0x07, 0xC0, 0x00, 0x7C, + 0xC0, 0x01, 0xCF, 0x80, 0x02, 0xFE, 0x00, 0x07, 0xF0, 0x00, 0x3F, 0x00, 0x01, 0xF0, 0x00, 0x03, + 0xC0, 0x1C, 0x0F, 0x80, 0x70, 0x1F, 0x03, 0xC0, 0x3E, 0x0F, 0x00, 0x7C, 0x3E, 0x00, 0xF8, 0x7F, + 0xFF, 0xF9, 0xFF, 0xFF, 0xF3, 0xFF, 0xF7, 0xC1, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x3E, 0x1F, 0x87, + 0xCC, 0xE7, 0x93, 0xF9, 0xFE, 0x7E, 0x0F, 0x81, 0xF8, 0x1F, 0x81, 0xF0, 0x1F, 0x01, 0xE0, 0x3C, + 0x7F, 0x8F, 0xF1, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0xF8, 0x0F, 0xC0, 0x7C, 0xC1, 0xCF, + 0x04, 0xFE, 0x0F, 0xF0, 0x7F, 0x01, 0xF0, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x78, + 0x01, 0xF8, 0x7F, 0xF9, 0xFF, 0xF7, 0xF7, 0x80, 0x00, 0x00, 0x08, 0x00, 0x78, 0x00, 0xF3, 0x80, + 0xE7, 0x01, 0xFE, 0x03, 0xFC, 0x07, 0xF8, 0x0F, 0xF0, 0x3C, 0xFD, 0xF9, 0xFF, 0xE1, 0xFF, 0x81, + 0xFE, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x00, 0xF0, 0x38, 0x0E, 0x07, 0x01, 0xE1, 0xE0, 0x3F, + 0xBC, 0x07, 0xFF, 0x80, 0xFF, 0xF0, 0x3C, 0x0F, 0xDF, 0x81, 0xFF, 0xE0, 0x1F, 0xFC, 0x01, 0xFE, + 0x00, 0x00, 0x07, 0x03, 0xE0, 0xFC, 0x3F, 0x1F, 0xE7, 0x3B, 0xCE, 0xF3, 0xBF, 0xE7, 0xF8, 0xFC, + 0x00, 0x00, 0x06, 0x00, 0xFE, 0x07, 0xF8, 0x1F, 0xF0, 0xF3, 0xC3, 0xC7, 0x8F, 0x1F, 0xFC, 0x3F, + 0x70, 0x7C, 0x00, 0x00, 0x39, 0xE3, 0xCF, 0x3C, 0xF3, 0xFF, 0xFB, 0xC3, 0x1C, 0x71, 0xC0, 0x30, + 0x03, 0xC0, 0x3E, 0x3D, 0xF3, 0xFF, 0xBF, 0x9F, 0xE0, 0xFE, 0x03, 0xF0, 0x1F, 0x00, 0x78, 0x01, + 0x00, 0x03, 0x00, 0x03, 0x80, 0x07, 0xC0, 0x07, 0xF0, 0x0F, 0xF8, 0x0E, 0xFC, 0x0E, 0xFE, 0x0E, + 0xFE, 0x0F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC, 0x10, 0x30, 0x01, 0xE0, 0x03, 0xF0, 0x07, + 0xF8, 0x07, 0xF8, 0x0F, 0x78, 0x0F, 0x70, 0x0E, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, + 0x3C, 0x0F, 0xFC, 0x07, 0xF8, 0x07, 0xF8, 0x01, 0xF0, 0x08, 0x00, 0xE0, 0x0F, 0xC0, 0x7F, 0x03, + 0xFC, 0x3F, 0xF1, 0xCF, 0xCF, 0xFF, 0x7F, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x08, 0x00, 0x07, + 0x80, 0x7F, 0x01, 0xFC, 0x0F, 0x70, 0x3D, 0xC1, 0xE7, 0x07, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF7, 0x8E, 0x0F, 0xF8, 0x3F, 0xE0, 0x7F, 0x80, 0x78, 0x00, 0x03, 0x80, 0x00, 0x7C, 0x00, 0x0F, + 0xE0, 0x01, 0xFE, 0x00, 0x3C, 0xF0, 0x07, 0xCE, 0x00, 0xF8, 0x00, 0x0F, 0x00, 0x00, 0xFF, 0xFF, + 0xE7, 0xFF, 0xFE, 0x3F, 0xFF, 0xE0, 0x7F, 0xF0, 0x07, 0xF8, 0xFF, 0xF7, 0xFF, 0xBF, 0xF8, 0xF0, + 0x03, 0xC0, 0x0F, 0xFF, 0xBF, 0xFE, 0x7F, 0xF8, 0x0F, 0x00, 0x18, 0x00, 0x03, 0xC0, 0x00, 0x38, + 0x00, 0x03, 0xC0, 0x00, 0x3E, 0x00, 0x03, 0xC0, 0x00, 0x03, 0x80, 0x00, 0x7C, 0x00, 0x0F, 0xE0, + 0x01, 0xFE, 0x00, 0x3C, 0xF0, 0x07, 0xCE, 0x00, 0xF8, 0x00, 0x0F, 0x00, 0x00, 0xFF, 0xFF, 0xE7, + 0xFF, 0xFE, 0x3F, 0xFF, 0xE0, 0x7F, 0xF0, 0x18, 0x00, 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x01, + 0xE0, 0x00, 0x7F, 0x8F, 0xFF, 0x7F, 0xFB, 0xFF, 0x8F, 0x00, 0x3C, 0x00, 0xFF, 0xFB, 0xFF, 0xE7, + 0xFF, 0x80, 0xF0, 0x00, 0x1F, 0x80, 0x1F, 0xC0, 0x1F, 0xC0, 0x1F, 0x20, 0x0E, 0x03, 0x87, 0x01, + 0xC3, 0xE1, 0xE1, 0xFC, 0xF0, 0x3F, 0x78, 0x07, 0xBC, 0x03, 0xCF, 0xDF, 0xE7, 0xFF, 0xE1, 0xFF, + 0xE0, 0x3F, 0xC0, 0x00, 0x00, 0x0E, 0x00, 0x01, 0xC0, 0xF8, 0x78, 0x1F, 0xEF, 0x03, 0xFF, 0xE0, + 0x1F, 0xBC, 0x03, 0xC3, 0xFB, 0xF8, 0x7F, 0xFE, 0x07, 0xFF, 0x80, 0x3F, 0xC0, 0x00, 0x3C, 0x78, + 0xF0, 0xE1, 0xE3, 0xC7, 0xFE, 0xFD, 0xF0, 0x00, 0x0E, 0xFF, 0xFB, 0x80, 0x00, 0x03, 0xC0, 0x70, + 0x0E, 0x03, 0xE1, 0xFF, 0xBF, 0xFF, 0xBE, 0x00, 0x00, 0x01, 0xF8, 0x7F, 0x87, 0xE0, 0x00, 0x0F, + 0xC7, 0xE1, 0xF8, 0x78, 0x1E, 0x07, 0xF8, 0xFE, 0xFF, 0xBF, 0xC7, 0x80, 0x00, 0x00, 0x3F, 0x9F, + 0xC0, 0xC0, 0x00, 0xE0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, + 0x0F, 0x07, 0x80, 0x00, 0x3F, 0x8F, 0xE0, 0x30, 0x38, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, + 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0xE1, 0xFC, 0x3E, 0x00, 0x00, 0x37, 0xBD, + 0xEF, 0xF8, 0x0E, 0x7B, 0xDE, 0xF7, 0xBD, 0xEF, 0x7B, 0xDE, 0xF7, 0x80, 0x30, 0x78, 0x3C, 0x1F, + 0x0F, 0x87, 0x81, 0xC0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, + 0x1E, 0x0F, 0xE3, 0xF8, 0xF8, 0x00, 0x0C, 0x07, 0x81, 0xC0, 0x78, 0x1E, 0x07, 0x80, 0xE0, 0xFC, + 0x3F, 0x9F, 0xE7, 0x3D, 0xCF, 0x7F, 0xDF, 0xF3, 0xFC, 0x3E, 0x7F, 0xBF, 0xC7, 0xE1, 0xE0, 0x0C, + 0x00, 0xF0, 0x07, 0x00, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x00, 0x01, 0xC0, 0x3F, 0x01, 0xFC, 0x1F, + 0xE0, 0xE7, 0x87, 0x3C, 0x3F, 0xFD, 0xFF, 0xE7, 0xFF, 0x0F, 0x83, 0xFC, 0x3F, 0xC0, 0xFC, 0x07, + 0x80, 0x00, 0xE7, 0xBD, 0xEF, 0x7B, 0xDE, 0xF7, 0xBD, 0xEF, 0x7B, 0xDE, 0x37, 0xBD, 0xFF, 0xF8, + 0x00, 0xE0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xFE, 0x7F, + 0x3E, 0x30, 0x70, 0xF0, 0xF8, 0xF8, 0xF0, 0x00, 0x0C, 0x00, 0x1E, 0x00, 0x0F, 0x07, 0xE7, 0xC7, + 0xF3, 0xE7, 0xF1, 0xE7, 0xC8, 0x03, 0x80, 0xE1, 0xC0, 0x70, 0xF8, 0x78, 0x7F, 0x3C, 0x0F, 0xDE, + 0x01, 0xEF, 0x00, 0xF3, 0xF7, 0xF9, 0xFF, 0xF8, 0x7F, 0xF8, 0x0F, 0xF0, 0x00, 0x0C, 0x00, 0x07, + 0x80, 0x00, 0xF0, 0x00, 0x1F, 0x00, 0x03, 0xE0, 0x00, 0x78, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, + 0x70, 0x3E, 0x1E, 0x07, 0xFB, 0xC0, 0xFF, 0xF8, 0x07, 0xEF, 0x00, 0xF0, 0xFE, 0xFE, 0x1F, 0xFF, + 0x81, 0xFF, 0xE0, 0x0F, 0xF0, 0x00, 0x31, 0xCF, 0x3E, 0xFB, 0xC0, 0x0E, 0x78, 0xF3, 0xCF, 0x3C, + 0xFF, 0xFE, 0xF0, 0x00, 0x18, 0x0F, 0x07, 0x80, 0xF0, 0x3C, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x0F, + 0x03, 0xC0, 0xF0, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xE0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xFE, 0x7F, 0x3E, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x9C, 0x00, 0x3C, 0xE0, 0x00, 0xEF, 0x00, 0x07, 0xF8, 0x00, + 0x3F, 0xE0, 0x03, 0xEF, 0xFF, 0xFE, 0x7F, 0xFF, 0xF1, 0xFF, 0xFE, 0x01, 0xFF, 0x80, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x07, + 0x00, 0x01, 0x83, 0xC0, 0x00, 0xF0, 0xF0, 0x00, 0x7C, 0x3E, 0x00, 0x7F, 0x07, 0xFF, 0xFF, 0xF9, + 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0x8F, 0xC1, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, + 0x00, 0x70, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x39, 0xE3, 0xCF, 0x3C, 0xF3, 0xFF, 0xFB, 0xC0, 0x00, + 0x71, 0xC7, 0x00, 0x00, 0x07, 0x03, 0xC0, 0xF0, 0x3C, 0x3F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x03, + 0x80, 0xF0, 0x38, 0x00, 0x02, 0x1F, 0xC7, 0xF0, 0xDC, 0x00, 0x00, 0x01, 0xC0, 0xF8, 0x3F, 0x0F, + 0xC7, 0xF9, 0xCE, 0xF3, 0xBC, 0xEF, 0xF9, 0xFE, 0x3F, 0x00, 0x00, 0x09, 0x00, 0x7E, 0x03, 0xFC, + 0x06, 0x60, 0x00, 0x00, 0x0E, 0x00, 0x3C, 0x01, 0xF0, 0x1F, 0xC1, 0xFF, 0x07, 0xFC, 0x3C, 0xF0, + 0xF3, 0xC3, 0xFF, 0x07, 0xFC, 0x1F, 0xFE, 0x1F, 0xFC, 0x03, 0xE0, 0x00, 0x00, 0x00, 0x88, 0x00, + 0x0E, 0xE0, 0x00, 0x7F, 0x00, 0x03, 0xB8, 0x00, 0x00, 0x0F, 0x38, 0x00, 0x79, 0xC0, 0x01, 0xDE, + 0x00, 0x0F, 0xF0, 0x00, 0x7F, 0xC0, 0x07, 0xDF, 0xFF, 0xFC, 0xFF, 0xFF, 0xE3, 0xFF, 0xFC, 0x03, + 0xFF, 0x00, 0x00, 0x48, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x03, 0x70, 0x00, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0x00, 0x07, 0x00, 0x01, 0x83, 0xC0, 0x00, 0xF0, 0xF0, 0x00, 0x7C, 0x3E, + 0x00, 0x7F, 0x07, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0x8F, 0xC1, 0xFE, 0x00, 0x00, + 0x24, 0x7E, 0xFF, 0x66, 0x00, 0x1C, 0x3C, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x7E, 0x7C, 0x78, 0x00, + 0x12, 0x0F, 0xE1, 0xFC, 0x1B, 0x80, 0x00, 0x00, 0x07, 0x80, 0xE0, 0x1C, 0x07, 0xC3, 0xFF, 0x7F, + 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x1C, 0x00, 0x01, 0xB8, 0x00, 0x1F, + 0xC0, 0x00, 0x6C, 0x00, 0x00, 0x03, 0xCE, 0x00, 0x1E, 0x70, 0x00, 0x77, 0x80, 0x03, 0xFC, 0x00, + 0x1F, 0xF0, 0x01, 0xF7, 0xFF, 0xFF, 0x3F, 0xFF, 0xF8, 0xFF, 0xFF, 0x00, 0xFF, 0xC0, 0x00, 0x30, + 0x00, 0x00, 0x1E, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x3F, + 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x70, 0x00, 0x18, 0x3C, 0x00, 0x0F, 0x0F, 0x00, + 0x07, 0xC3, 0xE0, 0x07, 0xF0, 0x7F, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0xF3, 0xFF, 0xF8, 0xFC, 0x1F, + 0xE0, 0x00, 0x30, 0xE3, 0xBF, 0xFF, 0xB0, 0x0E, 0x78, 0xF3, 0xCF, 0x3C, 0xFF, 0xFE, 0xF0, 0x00, + 0x0C, 0x03, 0xC0, 0x30, 0x1F, 0x83, 0xF0, 0x7E, 0x00, 0x00, 0x00, 0x1E, 0x03, 0x80, 0x70, 0x1F, + 0x0F, 0xFD, 0xFF, 0xFD, 0xF0, 0x00, 0x7C, 0x03, 0xFE, 0x07, 0xFF, 0x9F, 0xFE, 0x01, 0xF8, 0x1F, + 0xE1, 0xFC, 0x0F, 0xC0, 0x7C, 0x01, 0xE0, 0x0F, 0x1C, 0x3C, 0x70, 0xF1, 0xC3, 0xC7, 0x0F, 0x00, + 0x3F, 0x06, 0x7F, 0xFC, 0xFF, 0xF1, 0xFF, 0x80, 0x60, 0x7C, 0x00, 0x3F, 0xE0, 0x07, 0xFF, 0x81, + 0xFF, 0xE0, 0x01, 0xF8, 0x01, 0xFE, 0x01, 0xFF, 0x00, 0xFD, 0xE0, 0x7C, 0x3F, 0x9E, 0x0F, 0xEF, + 0x1D, 0xFB, 0xC7, 0x00, 0xF1, 0xC0, 0x3C, 0x70, 0x0F, 0x00, 0x03, 0xF0, 0x60, 0x7F, 0xFC, 0x0F, + 0xFF, 0x01, 0xFF, 0x80, 0x06, 0x00, 0x00, 0x03, 0xF8, 0x1F, 0xF0, 0xFF, 0xF0, 0x3F, 0x80, 0xFC, + 0x3F, 0xFF, 0xF8, 0xFF, 0x07, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x07, 0x80, 0x1C, 0x00, 0x00, + 0x00, 0x7F, 0x00, 0x7F, 0xC0, 0x7F, 0xF8, 0x03, 0xF8, 0x01, 0xF8, 0x0F, 0xF8, 0xFF, 0xFE, 0xFF, + 0x7F, 0xFC, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x7C, 0x03, 0xFE, + 0x07, 0xFF, 0x9F, 0xFE, 0x01, 0xF8, 0x1F, 0xE1, 0xFC, 0x0F, 0xC0, 0x7C, 0x01, 0xE0, 0x0F, 0x00, + 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3F, 0x06, 0x7F, 0xFC, 0xFF, 0xF1, 0xFF, 0x80, 0x60, + 0x7C, 0x00, 0x3F, 0xE0, 0x07, 0xFF, 0x81, 0xFF, 0xE0, 0x01, 0xF8, 0x01, 0xFE, 0x01, 0xFF, 0x00, + 0xFD, 0xE0, 0x7C, 0x3F, 0x9E, 0x0F, 0xEF, 0x01, 0xFB, 0xC0, 0x00, 0xF0, 0x00, 0x3C, 0x00, 0x0F, + 0x00, 0x03, 0xF0, 0x60, 0x7F, 0xFC, 0x0F, 0xFF, 0x01, 0xFF, 0x80, 0x06, 0x00, 0x00, 0x03, 0xF8, + 0x1F, 0xF0, 0xFF, 0xF0, 0x3F, 0x80, 0xFC, 0x3F, 0xFF, 0xF8, 0xFF, 0x07, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x7F, 0x00, 0x7F, 0xC0, 0x7F, 0xF8, 0x03, 0xF8, 0x01, 0xF8, 0x0F, 0xF8, 0xFF, 0xFE, 0xFF, + 0x7F, 0xFC, 0x3E, 0x00, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x00, 0xC0, 0x00, 0x01, 0xF0, 0x0F, + 0xF8, 0x1F, 0xFE, 0x7F, 0xF8, 0x07, 0xE0, 0x7F, 0x87, 0xF0, 0x3F, 0x01, 0xF0, 0x07, 0x80, 0x3C, + 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xFC, 0x19, 0xFF, 0xF3, 0xFF, 0xC7, 0xFE, 0x01, + 0x80, 0x03, 0x00, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x0F, 0xF8, + 0x01, 0xFF, 0xE0, 0x7F, 0xF8, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0xC0, 0x3F, 0x78, 0x1F, 0x0F, + 0xE7, 0x83, 0xFB, 0xC0, 0x7E, 0xF0, 0x00, 0x3C, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xFC, 0x18, + 0x1F, 0xFF, 0x03, 0xFF, 0xC0, 0x7F, 0xE0, 0x01, 0x80, 0x00, 0x18, 0x01, 0xC0, 0x0E, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x01, 0xFC, 0x0F, 0xF8, 0x7F, 0xF8, 0x1F, 0xC0, 0x7E, 0x1F, 0xFF, 0xFC, 0x7F, + 0x83, 0xF0, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7F, 0x00, 0x7F, 0xC0, 0x7F, 0xF8, 0x03, 0xF8, 0x01, 0xF8, 0x0F, 0xF8, 0xFF, 0xFE, 0xFF, 0x7F, + 0xFC, 0x3E, 0x00, 0x00, 0x02, 0x03, 0xC0, 0xF8, 0x1E, 0x03, 0xC0, 0xF0, 0x3D, 0x8F, 0x7F, 0xDF, + 0xF7, 0xF8, 0x00, 0x02, 0x00, 0x3C, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0xC0, 0x0F, 0x00, 0x3C, 0x18, + 0xF8, 0x7F, 0xFD, 0xFF, 0xF7, 0xFF, 0xC0, 0x00, 0x0C, 0x07, 0x01, 0xC0, 0x70, 0x00, 0x00, 0x80, + 0xF0, 0x3E, 0x07, 0x80, 0xF0, 0x3C, 0x0F, 0x63, 0xDF, 0xF7, 0xFD, 0xFE, 0x00, 0x00, 0x0C, 0x00, + 0x70, 0x01, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0xF0, 0x03, 0xE0, 0x07, 0x80, 0x1F, 0x00, + 0x3C, 0x00, 0xF0, 0x63, 0xE1, 0xFF, 0xF7, 0xFF, 0xDF, 0xFF, 0x00, 0x00, 0x06, 0x07, 0x83, 0xC0, + 0xE0, 0x78, 0x3C, 0x1C, 0x1E, 0x1F, 0x3F, 0x3F, 0x1F, 0x06, 0x00, 0x06, 0x00, 0x78, 0x03, 0xC0, + 0x0E, 0x00, 0x78, 0x03, 0xF8, 0x1F, 0xC1, 0xFE, 0x1F, 0x03, 0xF0, 0x3F, 0x01, 0xF0, 0x06, 0x00, + 0x00, 0x06, 0x07, 0x83, 0xC0, 0xC0, 0x00, 0x00, 0x18, 0x1E, 0x0F, 0x03, 0x81, 0xE0, 0xF0, 0x70, + 0x78, 0x7C, 0xFC, 0xFC, 0x7C, 0x18, 0x00, 0x04, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0x00, 0x00, + 0x00, 0x18, 0x01, 0xE0, 0x0F, 0x00, 0x38, 0x01, 0xE0, 0x0F, 0xE0, 0x7F, 0x07, 0xF8, 0x7C, 0x0F, + 0xC0, 0xFC, 0x07, 0xC0, 0x18, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x01, + 0xE0, 0x03, 0xC3, 0x8F, 0x00, 0x3C, 0x38, 0xF7, 0x01, 0xC3, 0x8F, 0x70, 0x1E, 0x78, 0xFF, 0x01, + 0xFF, 0xFF, 0xF0, 0x1F, 0xFF, 0xEF, 0x01, 0xFE, 0xFC, 0xF0, 0x3C, 0x00, 0x07, 0xCF, 0xC0, 0x00, + 0x7F, 0xF8, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0x00, 0xE0, 0x00, 0x3C, 0x38, 0xF0, 0x00, 0x3C, 0x38, 0xF0, 0x70, 0x1C, 0x38, 0xF0, 0x70, + 0x1E, 0x78, 0xF8, 0xF0, 0x1F, 0xFF, 0xFF, 0xF0, 0x1F, 0xFF, 0xFF, 0xF0, 0x1F, 0xEF, 0xDF, 0xF0, + 0x3C, 0x00, 0x00, 0x7C, 0xFC, 0x00, 0x00, 0x7F, 0xF8, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x1F, + 0xE0, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x78, 0x00, 0x07, 0x8E, 0x3C, 0xF3, 0xC7, 0x9E, 0x78, + 0xE3, 0xCF, 0x3E, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xFB, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xE0, 0x00, 0x03, 0xC0, 0x71, 0xE7, 0x81, 0xE3, 0xCF, 0x03, 0xC7, 0x1E, 0x07, 0x9F, 0x3E, 0x3F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xBE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x1D, 0x80, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x0D, 0x80, + 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x01, 0xE0, 0x03, 0xC3, 0x8F, 0x00, 0x3C, + 0x38, 0xF7, 0x01, 0xC3, 0x8F, 0x70, 0x1E, 0x78, 0xFF, 0x01, 0xFF, 0xFF, 0xF0, 0x1F, 0xFF, 0xEF, + 0x01, 0xFE, 0xFC, 0xF0, 0x3C, 0x00, 0x07, 0xCF, 0xC0, 0x00, 0x7F, 0xF8, 0x00, 0x03, 0xFF, 0x80, + 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x01, 0xD8, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x3C, 0x38, 0xF0, 0x00, 0x3C, 0x38, + 0xF0, 0x70, 0x1C, 0x38, 0xF0, 0x70, 0x1E, 0x78, 0xF8, 0xF0, 0x1F, 0xFF, 0xFF, 0xF0, 0x1F, 0xFF, + 0xFF, 0xF0, 0x1F, 0xEF, 0xDF, 0xF0, 0x3C, 0x00, 0x00, 0x7C, 0xFC, 0x00, 0x00, 0x7F, 0xF8, 0x00, + 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0F, 0x00, 0x00, 0xC0, + 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x01, 0xB8, 0x00, 0x00, 0x70, 0x00, 0x1E, 0x00, 0x01, 0xE3, 0x8F, + 0x3C, 0xF1, 0xE7, 0x9E, 0x38, 0xF3, 0xCF, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFE, 0xFC, 0x00, + 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0xE0, 0x00, 0x01, 0xC0, 0x00, 0x06, 0xE0, 0x00, 0x1F, 0xC0, + 0x00, 0x1B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x01, 0xE0, 0x38, 0xF3, 0xC0, 0xF1, + 0xE7, 0x81, 0xE3, 0x8F, 0x03, 0xCF, 0x9F, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xDF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x07, 0xFC, 0x00, 0x78, + 0x7F, 0xF0, 0x03, 0xC7, 0xE7, 0xB8, 0x0E, 0x7C, 0x3D, 0xC0, 0x7B, 0xC3, 0xFE, 0x03, 0xFF, 0xFF, + 0xF0, 0x1F, 0xFF, 0xF7, 0x80, 0xFF, 0xFE, 0x3C, 0x0F, 0x0F, 0x80, 0xFD, 0xF8, 0x00, 0x07, 0xFF, + 0x80, 0x00, 0x1F, 0xFC, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, + 0x07, 0xF0, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x3C, 0x3F, 0xF8, 0x00, 0x3C, 0x7E, 0x78, 0x70, 0x1C, + 0xF8, 0x78, 0x70, 0x1E, 0xF0, 0xF8, 0xF0, 0x1F, 0xFF, 0xFF, 0xF0, 0x1F, 0xFF, 0xFF, 0xF0, 0x1F, + 0xFF, 0xFF, 0xF0, 0x3C, 0x3E, 0x00, 0x7E, 0xFC, 0x00, 0x00, 0x7F, 0xF8, 0x00, 0x00, 0x3F, 0xF8, + 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x3F, 0xE3, 0xC7, 0xFF, + 0x3C, 0xFC, 0xF3, 0x9F, 0x0F, 0x7F, 0xE1, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xEE, 0xFF, 0xF8, 0x01, + 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x80, 0x00, 0xFF, 0x81, 0xE3, 0xFF, 0x83, 0xCF, 0xCF, + 0x07, 0x3E, 0x1E, 0x1F, 0xF8, 0x7C, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xFF, 0xFC, 0x07, + 0xF0, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x03, 0xFE, 0x00, + 0x3C, 0x3F, 0xF8, 0x01, 0xE3, 0xF3, 0xDC, 0x07, 0x3E, 0x1E, 0xE0, 0x3D, 0xE1, 0xFF, 0x01, 0xFF, + 0xFF, 0xF8, 0x0F, 0xFF, 0xFB, 0xC0, 0x7F, 0xFF, 0x1E, 0x07, 0x87, 0xC0, 0x7E, 0xFC, 0x00, 0x03, + 0xFF, 0xC0, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, + 0x00, 0x03, 0x80, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x00, 0x00, 0x07, 0xF0, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x3C, 0x3F, 0xF8, 0x00, + 0x3C, 0x7E, 0x78, 0x70, 0x1C, 0xF8, 0x78, 0x70, 0x1E, 0xF0, 0xF8, 0xF0, 0x1F, 0xFF, 0xFF, 0xF0, + 0x1F, 0xFF, 0xFF, 0xF0, 0x1F, 0xFF, 0xFF, 0xF0, 0x3C, 0x3E, 0x00, 0x7E, 0xFC, 0x00, 0x00, 0x7F, + 0xF8, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x70, + 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xC0, 0x03, 0xFE, 0x3C, + 0x7F, 0xF3, 0xCF, 0xCF, 0x39, 0xF0, 0xF7, 0xFE, 0x1F, 0xFF, 0xFF, 0xEF, 0xFF, 0xFE, 0xEF, 0xFF, + 0x80, 0x1F, 0xC0, 0x00, 0x02, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x1F, 0xF0, 0x3C, 0x7F, 0xF0, 0x79, 0xF9, 0xE0, + 0xE7, 0xC3, 0xC3, 0xFF, 0x0F, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0x80, 0xFE, + 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x3C, 0x20, + 0x1E, 0xFC, 0x0F, 0xFF, 0x07, 0xFF, 0xC3, 0xF9, 0xE1, 0xF0, 0xF0, 0xF0, 0xFF, 0xFF, 0xFB, 0xFF, + 0xFD, 0xFF, 0xF8, 0x1F, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, + 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x0F, 0x08, 0x00, 0xF7, 0xE0, 0x0F, 0xFF, 0x00, 0xFF, 0xF8, 0x0F, + 0xE7, 0x80, 0xF8, 0x78, 0x0F, 0x0F, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xE0, + 0x00, 0x38, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x07, 0x80, 0x07, 0x00, 0x0E, 0x1C, 0x1D, 0xFC, + 0x3F, 0xFC, 0x7F, 0xFC, 0xFC, 0x79, 0xF0, 0xF7, 0xC7, 0xFF, 0xFF, 0xBF, 0xFE, 0x7F, 0xF8, 0x00, + 0x00, 0x38, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x0E, 0x00, 0x03, 0x87, + 0x00, 0xEF, 0xE0, 0x3F, 0xFC, 0x0F, 0xFF, 0x83, 0xF1, 0xE0, 0xF8, 0x78, 0x7C, 0x7E, 0x3F, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x0C, 0x03, 0x87, 0x01, 0xC3, + 0x80, 0xE1, 0xC0, 0x70, 0x00, 0x3C, 0x20, 0x1E, 0xFC, 0x0F, 0xFF, 0x07, 0xFF, 0xC3, 0xF9, 0xE1, + 0xF0, 0xF0, 0xF0, 0xFF, 0xFF, 0xFB, 0xFF, 0xFD, 0xFF, 0xF8, 0x1F, 0xE0, 0x00, 0x0E, 0x00, 0x00, + 0xE1, 0x80, 0x0E, 0x3C, 0x00, 0xE3, 0xC0, 0x0E, 0x18, 0x00, 0xE0, 0x00, 0x0F, 0x08, 0x00, 0xF7, + 0xE0, 0x0F, 0xFF, 0x00, 0xFF, 0xF8, 0x0F, 0xE7, 0x80, 0xF8, 0x78, 0x0F, 0x0F, 0x8F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xE0, 0x00, 0x38, 0x00, 0xF0, 0x81, 0xE3, 0x83, 0xC7, 0x07, + 0x8E, 0x07, 0x00, 0x0E, 0x1C, 0x1D, 0xFC, 0x3F, 0xFC, 0x7F, 0xFC, 0xFC, 0x79, 0xF0, 0xF7, 0xC7, + 0xFF, 0xFF, 0xBF, 0xFE, 0x7F, 0xF8, 0x00, 0x00, 0x38, 0x00, 0x1E, 0x10, 0x07, 0x8E, 0x01, 0xE3, + 0x80, 0x78, 0xE0, 0x0E, 0x00, 0x03, 0x87, 0x00, 0xEF, 0xE0, 0x3F, 0xFC, 0x0F, 0xFF, 0x83, 0xF1, + 0xE0, 0xF8, 0x78, 0x7C, 0x7E, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x1F, + 0x00, 0xFE, 0x07, 0xF0, 0x1F, 0xC0, 0x70, 0x01, 0xC1, 0x07, 0xFC, 0x1F, 0xF8, 0x3F, 0xE0, 0xFC, + 0x07, 0xC0, 0x3E, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF8, 0x19, 0xFF, 0xE3, 0xFF, + 0xC7, 0xFE, 0x03, 0x80, 0x0F, 0xC0, 0xFF, 0x83, 0xFE, 0x1E, 0x78, 0x3D, 0xE0, 0xFF, 0x81, 0xFC, + 0x03, 0xFF, 0x3F, 0xFD, 0xFF, 0xF7, 0x80, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x83, 0x3F, 0xFC, + 0x7F, 0xF0, 0xFF, 0x80, 0x70, 0x00, 0x07, 0x03, 0xF0, 0xFE, 0x3F, 0xC7, 0x80, 0xF0, 0x1E, 0x31, + 0xFF, 0xFF, 0xFF, 0xFB, 0xFC, 0x00, 0x00, 0x07, 0xC0, 0x3F, 0xC0, 0xFF, 0xC1, 0xE7, 0x83, 0xEF, + 0x03, 0xFC, 0x03, 0xF8, 0x3F, 0xFE, 0x7F, 0xFE, 0xFB, 0xF8, 0x00, 0x00, 0x06, 0x00, 0x3C, 0x00, + 0xF0, 0x01, 0x80, 0x00, 0x00, 0x7C, 0x03, 0xF8, 0x1F, 0xC0, 0x7F, 0x01, 0xC0, 0x07, 0x04, 0x1F, + 0xF0, 0x7F, 0xE0, 0xFF, 0x83, 0xF0, 0x1F, 0x00, 0xF8, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, + 0x03, 0xE0, 0x67, 0xFF, 0x8F, 0xFF, 0x1F, 0xF8, 0x0E, 0x00, 0x03, 0x00, 0x1C, 0x00, 0x78, 0x01, + 0xC0, 0x00, 0x00, 0x3F, 0x03, 0xFE, 0x0F, 0xF8, 0x79, 0xE0, 0xF7, 0x83, 0xFE, 0x07, 0xF0, 0x0F, + 0xFC, 0xFF, 0xF7, 0xFF, 0xDE, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3E, 0x0C, 0xFF, 0xF1, 0xFF, + 0xC3, 0xFE, 0x01, 0xC0, 0x06, 0x01, 0xC0, 0x3C, 0x07, 0x00, 0x00, 0x0E, 0x07, 0xE1, 0xFC, 0x7F, + 0x8F, 0x01, 0xE0, 0x3C, 0x63, 0xFF, 0xFF, 0xFF, 0xF7, 0xF8, 0x00, 0x00, 0x03, 0x00, 0x0F, 0x00, + 0x1E, 0x00, 0x1C, 0x00, 0x00, 0x00, 0xF8, 0x07, 0xF8, 0x1F, 0xF8, 0x3C, 0xF0, 0x7D, 0xE0, 0x7F, + 0x80, 0x7F, 0x07, 0xFF, 0xCF, 0xFF, 0xDF, 0x7F, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x0E, 0x00, + 0x00, 0xF0, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x00, 0xFE, 0x00, + 0x0F, 0xF0, 0x00, 0x7B, 0xDC, 0x03, 0x9E, 0xE0, 0x1F, 0xFF, 0x00, 0xFF, 0xF8, 0x07, 0xFF, 0xE0, + 0x07, 0xEF, 0xFF, 0xFE, 0x7F, 0xFF, 0xF1, 0xFF, 0xFF, 0x01, 0xFF, 0x80, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x78, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xC0, 0x00, 0x07, 0xF0, 0x70, 0x03, 0xFC, 0x38, 0x03, 0xDE, 0x3C, 0x01, 0xE7, 0x1E, 0x00, + 0xF7, 0x8F, 0x80, 0x3F, 0xC3, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0x07, 0xFC, + 0x00, 0x00, 0x04, 0x03, 0x80, 0xE0, 0x38, 0x00, 0x00, 0x01, 0xF0, 0xFE, 0x7F, 0x9E, 0xF7, 0xBD, + 0xFF, 0x7F, 0xCF, 0xF0, 0xFF, 0xFE, 0xFF, 0xBF, 0x80, 0x00, 0x06, 0x00, 0x70, 0x03, 0xC0, 0x1C, + 0x00, 0x00, 0x00, 0x00, 0x7C, 0x07, 0xF0, 0x7F, 0xC3, 0xDE, 0x1E, 0xF0, 0xF7, 0x83, 0xFC, 0x7F, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x22, 0x00, 0x3B, 0x80, 0x1F, 0xC0, 0x0E, 0xC0, 0x00, + 0x00, 0x00, 0xE0, 0x01, 0xF8, 0x00, 0xFE, 0x00, 0xFF, 0x38, 0x73, 0xDC, 0x3D, 0xFE, 0x1F, 0xFF, + 0x0F, 0xFF, 0x83, 0xFF, 0xC0, 0x3D, 0xF8, 0x7E, 0x7F, 0xFE, 0x1F, 0xFE, 0x07, 0xFC, 0x00, 0x00, + 0x20, 0x00, 0x07, 0xF0, 0x00, 0x7F, 0x00, 0x07, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, + 0x00, 0x07, 0xF0, 0x00, 0xFF, 0x00, 0x0F, 0x78, 0x70, 0xE7, 0x87, 0x0F, 0xFE, 0xF0, 0xFF, 0xFF, + 0x07, 0xFF, 0xF0, 0x0F, 0x8F, 0x80, 0xF0, 0x7F, 0xFE, 0x07, 0xFF, 0xE0, 0x3F, 0xF8, 0x00, 0xFE, + 0x00, 0x11, 0x0E, 0xE3, 0xF8, 0xEE, 0x00, 0x00, 0x01, 0xF0, 0xFE, 0x7F, 0x9E, 0xF7, 0xBD, 0xFF, + 0x7F, 0xCF, 0xF0, 0xFF, 0xFE, 0xFF, 0xBF, 0x80, 0x00, 0x11, 0x01, 0xDC, 0x0F, 0xE0, 0x37, 0x00, + 0x00, 0x00, 0x00, 0x7C, 0x07, 0xF0, 0x7F, 0xC3, 0xDE, 0x1E, 0xF0, 0xF7, 0x83, 0xFC, 0x7F, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x00, 0x07, 0x00, 0x18, 0xE0, 0x07, + 0x1C, 0x01, 0xE3, 0x80, 0x38, 0x78, 0x07, 0x8F, 0x70, 0x71, 0xEE, 0x3E, 0x3F, 0xC7, 0x87, 0xF8, + 0x40, 0xFF, 0x80, 0x3E, 0xFF, 0xFF, 0x9F, 0xFF, 0xF1, 0xFF, 0xFC, 0x07, 0xFC, 0x00, 0x00, 0x01, + 0xC0, 0x00, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x0C, 0x70, 0x00, 0x71, 0xC0, 0x03, 0xC7, 0x00, 0x0E, + 0x1E, 0x00, 0x3C, 0x78, 0x70, 0x71, 0xE1, 0xC7, 0xC7, 0x8F, 0x1E, 0x1E, 0x3C, 0x20, 0x78, 0xF8, + 0x03, 0xE1, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0x07, 0xFC, 0x00, 0x01, 0x81, 0xE1, + 0xFD, 0xFE, 0xFC, 0x3E, 0x0F, 0xC1, 0xF8, 0x3E, 0x07, 0xC0, 0xF0, 0x3C, 0xFF, 0x3F, 0xCF, 0xE0, + 0x00, 0x01, 0x80, 0x3C, 0x07, 0xF0, 0xFF, 0x0F, 0xE0, 0x7C, 0x03, 0xE0, 0x0F, 0x80, 0x3E, 0x00, + 0xF8, 0x03, 0xC0, 0x1F, 0x8F, 0xFF, 0x7F, 0xFF, 0xFB, 0xC0, 0x00, 0x00, 0x1C, 0x00, 0x38, 0x00, + 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x80, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x7B, + 0x80, 0xF7, 0x01, 0xFE, 0x03, 0xFC, 0x07, 0xF8, 0x0E, 0xF0, 0x3C, 0xF8, 0xF9, 0xFF, 0xE1, 0xFF, + 0x80, 0xFE, 0x00, 0x00, 0x1C, 0x00, 0x03, 0x80, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x01, 0xC0, 0x00, + 0x38, 0x00, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x03, 0xC0, 0x00, 0x78, 0x38, 0x0F, 0x07, + 0x01, 0xE1, 0xE0, 0x3F, 0xBC, 0x07, 0xF7, 0x80, 0xFE, 0xF0, 0x3C, 0x0F, 0x8F, 0x81, 0xFF, 0xE0, + 0x1F, 0xF8, 0x00, 0xFE, 0x00, 0x39, 0xE7, 0x9E, 0x38, 0xE3, 0x8E, 0x38, 0xE3, 0x8E, 0x3B, 0xEF, + 0xBC, 0x00, 0x38, 0x3C, 0x1E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x83, + 0xC7, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x06, 0x03, 0xF0, 0xFF, 0x3F, 0xE7, 0x9E, 0x23, 0xC7, 0xF9, + 0xFF, 0x7F, 0xFF, 0x3B, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0x70, 0x0E, 0x00, 0x00, + 0x00, 0x03, 0xC0, 0x0F, 0xE0, 0x1F, 0xE0, 0x3F, 0xF0, 0x7C, 0x70, 0x7E, 0x78, 0xFF, 0xFE, 0xFF, + 0xFF, 0xF7, 0xFF, 0xF1, 0xC0, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x78, + 0x00, 0x78, 0x00, 0x00, 0x00, 0x07, 0x80, 0xFC, 0x1F, 0xE3, 0xFE, 0x3C, 0xE7, 0xCE, 0xFF, 0xEF, + 0xFE, 0xEF, 0xC0, 0x00, 0x07, 0x80, 0x3F, 0x80, 0x7F, 0x01, 0xFF, 0x03, 0x9E, 0x0F, 0x9E, 0x3F, + 0xFF, 0xFF, 0xFF, 0xEF, 0xFE, 0x07, 0x00, 0x03, 0x00, 0x07, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x00, + 0x80, 0x07, 0x80, 0x0F, 0x38, 0x0E, 0x70, 0x1F, 0xE0, 0x3F, 0xC0, 0x7F, 0x80, 0xFF, 0x03, 0xCF, + 0xDF, 0x9F, 0xFE, 0x1F, 0xF8, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0x70, 0x00, 0x1E, 0x00, 0x01, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x0F, 0x03, 0x80, 0xE0, 0x70, 0x1E, 0x1E, 0x03, 0xFB, 0xC0, + 0x7F, 0xF8, 0x0F, 0xFF, 0x03, 0xC0, 0xFD, 0xF8, 0x1F, 0xFE, 0x01, 0xFF, 0xC0, 0x1F, 0xE0, 0x00, + 0x31, 0xE7, 0x8C, 0x00, 0xE7, 0x8F, 0x3C, 0xF3, 0xCF, 0xFF, 0xEF, 0x00, 0x18, 0x0F, 0x03, 0xC0, + 0x70, 0x00, 0x00, 0x01, 0xC0, 0xF0, 0x3C, 0x0F, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x07, + 0x03, 0xE0, 0xFC, 0x3F, 0x1F, 0xE7, 0x3B, 0xCE, 0xF3, 0xBF, 0xE7, 0xF8, 0xFC, 0x00, 0x00, 0x03, + 0x80, 0x0F, 0x00, 0x7C, 0x07, 0xF0, 0x7F, 0xC1, 0xFF, 0x0F, 0x3C, 0x3C, 0xF0, 0xFF, 0xC1, 0xFF, + 0x07, 0xFF, 0x87, 0xFF, 0x00, 0xF8, 0x00, 0x00, 0x08, 0x00, 0xE0, 0x0F, 0xC0, 0x7F, 0x03, 0xFC, + 0x3F, 0xF1, 0xCF, 0xCF, 0xFF, 0x7F, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x08, 0x00, 0x07, 0x80, + 0x7F, 0x01, 0xFC, 0x0F, 0x70, 0x3D, 0xC1, 0xE7, 0x07, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, + 0x8E, 0x0F, 0xF8, 0x3F, 0xE0, 0x7F, 0x80, 0x78, 0x00, 0x0E, 0x0F, 0xC3, 0xF9, 0xFE, 0x73, 0xDC, + 0xF7, 0xFD, 0xFF, 0x3F, 0xC3, 0xE7, 0xFB, 0xFC, 0x7E, 0x1E, 0x00, 0x0E, 0x01, 0xF8, 0x0F, 0xE0, + 0xFF, 0x07, 0x3C, 0x39, 0xE1, 0xFF, 0xEF, 0xFF, 0x3F, 0xF8, 0x7C, 0x1F, 0xE1, 0xFE, 0x07, 0xE0, + 0x3C, 0x00, 0x00, 0x1F, 0x80, 0x1F, 0xC0, 0x1F, 0xC0, 0x1F, 0x20, 0x0E, 0x03, 0x87, 0x01, 0xC3, + 0xE1, 0xE1, 0xFC, 0xF0, 0x3F, 0x78, 0x07, 0xBC, 0x03, 0xCF, 0xDF, 0xE7, 0xFF, 0xE1, 0xFF, 0xE0, + 0x3F, 0xC0, 0x00, 0x00, 0x0E, 0x00, 0x01, 0xC0, 0xF8, 0x78, 0x1F, 0xEF, 0x03, 0xFF, 0xE0, 0x1F, + 0xBC, 0x03, 0xC3, 0xFB, 0xF8, 0x7F, 0xFE, 0x07, 0xFF, 0x80, 0x3F, 0xC0, 0x00, 0x00, 0x1F, 0x80, + 0x1F, 0xC0, 0x1F, 0xC0, 0x1F, 0x20, 0x0E, 0x03, 0x87, 0x01, 0xC3, 0xE1, 0xE1, 0xFC, 0xF0, 0x3F, + 0x78, 0x07, 0xBC, 0x03, 0xCF, 0xDF, 0xE7, 0xFF, 0xE1, 0xFF, 0xE0, 0x3F, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x07, 0x70, 0x03, 0xF8, 0x01, 0xDC, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x01, 0xC0, 0xF8, 0x78, + 0x1F, 0xEF, 0x03, 0xFF, 0xE0, 0x1F, 0xBC, 0x03, 0xC3, 0xFB, 0xF8, 0x7F, 0xFE, 0x07, 0xFF, 0x80, + 0x3F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xC0, 0x03, 0xFC, 0x00, 0x3F, 0x00, 0x3C, 0x78, + 0xF0, 0xE1, 0xE3, 0xC7, 0xFE, 0xFD, 0xF0, 0x00, 0x0E, 0xFF, 0xFB, 0x80, 0x00, 0x03, 0xC0, 0x70, + 0x0E, 0x03, 0xE1, 0xFF, 0xBF, 0xFF, 0xBE, 0x00, 0x00, 0x01, 0xF8, 0x7F, 0x87, 0xE0, 0x00, 0x00, + 0xFE, 0x31, 0xFD, 0xE0, 0x31, 0xC0, 0xC3, 0x83, 0xC7, 0x07, 0xCE, 0x07, 0xFC, 0x07, 0xF8, 0x03, + 0xF0, 0x03, 0xE0, 0x0F, 0xE0, 0x3F, 0xC0, 0xFB, 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0xF8, 0x00, 0x00, + 0x7F, 0x00, 0xFF, 0x70, 0x0E, 0xF0, 0x00, 0xF0, 0x20, 0x70, 0x70, 0x70, 0x78, 0x70, 0x7C, 0x70, + 0x3E, 0x70, 0x1F, 0x70, 0x0F, 0xF0, 0x07, 0xF0, 0x03, 0xF8, 0x07, 0xF8, 0x7F, 0xFF, 0x7F, 0xFF, + 0x7F, 0x3F, 0x00, 0x00, 0x70, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0xCF, 0x9E, 0x78, 0x70, 0xC3, 0x8F, + 0x1C, 0x7C, 0xE1, 0xFF, 0x07, 0xF8, 0x0F, 0xC0, 0x3E, 0x03, 0xF8, 0x3F, 0xC3, 0xEE, 0x3F, 0xF1, + 0xFF, 0x8F, 0xF8, 0x18, 0x00, 0x3C, 0x00, 0x38, 0x00, 0x3C, 0x00, 0x3E, 0x70, 0x3C, 0xF0, 0x00, + 0xF0, 0x20, 0x70, 0x70, 0x70, 0x78, 0x70, 0x7C, 0x70, 0x3E, 0x70, 0x1F, 0x70, 0x0F, 0xF0, 0x07, + 0xF0, 0x03, 0xF8, 0x07, 0xF8, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0x3F, 0x00, 0x00, 0x00, 0x60, 0x0F, + 0x00, 0x38, 0x61, 0xC7, 0x8E, 0x3E, 0x70, 0xFF, 0x83, 0xFC, 0x07, 0xE0, 0x1F, 0x01, 0xFC, 0x1F, + 0xE1, 0xF7, 0x1F, 0xF8, 0xFF, 0xC7, 0xFC, 0x0C, 0x00, 0xE0, 0x0F, 0x00, 0x7C, 0x03, 0xE0, 0x1E, + 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0xF0, 0x00, 0xF0, 0x20, 0x70, 0x70, 0x70, 0x78, 0x70, 0x7C, + 0x70, 0x3E, 0x70, 0x1F, 0x70, 0x0F, 0xF0, 0x07, 0xF0, 0x03, 0xF8, 0x07, 0xF8, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0x3F, 0x1C, 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x00, 0x60, 0x0F, 0x00, 0x38, 0x61, 0xC7, 0x8E, 0x3E, 0x70, 0xFF, 0x83, 0xFC, 0x07, 0xE0, + 0x1F, 0x01, 0xFC, 0x1F, 0xE1, 0xF7, 0x1F, 0xF8, 0xFF, 0xC7, 0xFC, 0x00, 0x70, 0x00, 0xF0, 0x00, + 0xF0, 0x20, 0x70, 0x70, 0x70, 0x78, 0x70, 0x7C, 0x70, 0x3E, 0x70, 0x1F, 0x70, 0x0F, 0xF0, 0x07, + 0xF0, 0x03, 0xF8, 0x07, 0xF8, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0x3F, 0x00, 0x00, }; static const EpdGlyph inter_10_boldGlyphs[] = { @@ -2165,656 +2533,851 @@ static const EpdGlyph inter_10_boldGlyphs[] = { { 10, 5, 0, -4, 17, 7, 11241 }, // U+030B { 9, 4, 0, 0, 16, 5, 11248 }, // U+030C { 9, 4, 0, 1, 16, 5, 11253 }, // U+030F - { 5, 7, 0, 4, 10, 5, 11258 }, // U+0313 - { 5, 7, 0, 4, 10, 5, 11263 }, // U+0315 - { 4, 6, 0, -5, 16, 3, 11268 }, // U+031B - { 4, 4, 94, 1, -1, 2, 11271 }, // U+0323 - { 4, 5, 0, -9, -1, 3, 11273 }, // U+0326 - { 5, 6, 0, 2, 1, 4, 11276 }, // U+0327 - { 6, 6, 0, 0, 1, 5, 11280 }, // U+0328 - { 9, 4, 0, 0, -1, 5, 11285 }, // U+032C - { 9, 15, 0, -11, 13, 17, 11290 }, // U+0337 - { 13, 17, 0, -13, 16, 28, 11307 }, // U+0338 - { 8, 4, 0, -11, 16, 4, 11335 }, // U+0342 - { 5, 4, 0, 0, 13, 3, 11339 }, // U+0343 - { 9, 5, 0, -11, 17, 6, 11342 }, // U+0346 - { 8, 6, 0, -10, -2, 6, 11348 }, // U+0347 - { 6, 6, 0, -9, 0, 5, 11354 }, // U+0348 - { 5, 5, 0, -9, -1, 4, 11359 }, // U+0349 - { 9, 6, 0, -11, 17, 7, 11363 }, // U+034A - { 9, 10, 0, -11, 21, 12, 11370 }, // U+034B - { 9, 6, 0, -11, 18, 7, 11382 }, // U+034C - { 11, 5, 0, -12, -1, 7, 11389 }, // U+034D - { 5, 6, 0, -9, 0, 4, 11396 }, // U+034E - { 0, 0, 0, 0, 0, 0, 11400 }, // U+034F - { 9, 9, 0, -10, 21, 11, 11400 }, // U+0350 - { 3, 5, 0, -8, 16, 2, 11411 }, // U+0351 - { 8, 8, 0, -11, 19, 8, 11413 }, // U+0352 - { 6, 6, 0, -9, 0, 5, 11421 }, // U+0353 - { 6, 6, 0, -9, 0, 5, 11426 }, // U+0354 - { 7, 6, 0, -10, 0, 6, 11431 }, // U+0355 - { 12, 6, 0, -11, 0, 9, 11437 }, // U+0356 - { 4, 6, 0, 0, 18, 3, 11446 }, // U+0357 - { 4, 4, 0, 1, 16, 2, 11449 }, // U+0358 - { 5, 6, 0, -9, 0, 4, 11451 }, // U+0359 - { 9, 5, 0, -11, 0, 6, 11455 }, // U+035A - { 5, 5, 0, -9, 17, 4, 11461 }, // U+035B - { 16, 5, 0, -9, 0, 10, 11465 }, // U+035C - { 17, 5, 0, -9, 16, 11, 11475 }, // U+035D - { 13, 3, 0, -13, 16, 5, 11486 }, // U+035E - { 13, 3, 0, -13, -1, 5, 11491 }, // U+035F - { 22, 4, 0, -12, 16, 11, 11496 }, // U+0360 - { 17, 5, 0, -9, 16, 11, 11507 }, // U+0361 - { 19, 5, 0, -9, -1, 12, 11518 }, // U+0362 - { 7, 6, 0, -10, 18, 6, 11530 }, // U+0363 - { 7, 6, 0, -10, 18, 6, 11536 }, // U+0364 - { 3, 9, 0, -8, 21, 4, 11542 }, // U+0365 - { 7, 6, 0, -10, 18, 6, 11546 }, // U+0366 - { 7, 6, 0, -10, 18, 6, 11552 }, // U+0367 - { 6, 6, 0, -10, 18, 5, 11558 }, // U+0368 - { 7, 9, 0, -10, 21, 8, 11563 }, // U+0369 - { 7, 9, 0, -10, 21, 8, 11571 }, // U+036A - { 9, 6, 0, -11, 18, 7, 11579 }, // U+036B - { 4, 6, 0, -8, 18, 3, 11586 }, // U+036C - { 4, 8, 0, 0, 20, 4, 11589 }, // U+036D - { 7, 6, 0, -10, 18, 6, 11593 }, // U+036E - { 7, 6, 0, -10, 18, 6, 11599 }, // U+036F - { 11, 20, 202, 1, 20, 28, 11605 }, // U+0400 - { 11, 20, 202, 1, 20, 28, 11633 }, // U+0401 - { 17, 18, 284, 0, 16, 39, 11661 }, // U+0402 - { 11, 20, 192, 1, 20, 28, 11700 }, // U+0403 - { 15, 17, 246, 0, 16, 32, 11728 }, // U+0404 - { 13, 17, 218, 0, 16, 28, 11760 }, // U+0405 - { 4, 16, 92, 1, 16, 8, 11788 }, // U+0406 - { 8, 20, 92, -1, 20, 20, 11796 }, // U+0407 - { 11, 17, 193, 0, 16, 24, 11816 }, // U+0408 - { 23, 16, 370, 0, 16, 46, 11840 }, // U+0409 - { 22, 16, 373, 1, 16, 44, 11886 }, // U+040A - { 18, 16, 296, 0, 16, 36, 11930 }, // U+040B - { 14, 20, 237, 1, 20, 35, 11966 }, // U+040C - { 14, 20, 251, 1, 20, 35, 12001 }, // U+040D - { 14, 20, 228, 0, 20, 35, 12036 }, // U+040E - { 13, 20, 241, 1, 16, 33, 12071 }, // U+040F - { 15, 16, 246, 0, 16, 30, 12104 }, // U+0410 - { 12, 16, 214, 1, 16, 24, 12134 }, // U+0411 - { 12, 16, 219, 1, 16, 24, 12158 }, // U+0412 - { 11, 16, 192, 1, 16, 22, 12182 }, // U+0413 - { 18, 20, 285, 0, 16, 45, 12204 }, // U+0414 - { 11, 16, 202, 1, 16, 22, 12249 }, // U+0415 - { 22, 16, 351, 0, 16, 44, 12271 }, // U+0416 - { 13, 17, 213, 0, 16, 28, 12315 }, // U+0417 - { 14, 16, 251, 1, 16, 28, 12343 }, // U+0418 - { 14, 20, 251, 1, 20, 35, 12371 }, // U+0419 - { 14, 16, 237, 1, 16, 28, 12406 }, // U+041A - { 15, 16, 248, 0, 16, 30, 12434 }, // U+041B - { 17, 16, 308, 1, 16, 34, 12464 }, // U+041C - { 14, 16, 247, 1, 16, 28, 12498 }, // U+041D - { 16, 17, 255, 0, 16, 34, 12526 }, // U+041E - { 14, 16, 247, 1, 16, 28, 12560 }, // U+041F - { 12, 16, 215, 1, 16, 24, 12588 }, // U+0420 - { 15, 17, 246, 0, 16, 32, 12612 }, // U+0421 - { 14, 16, 220, 0, 16, 28, 12644 }, // U+0422 - { 14, 16, 228, 0, 16, 28, 12672 }, // U+0423 - { 17, 17, 277, 0, 16, 37, 12700 }, // U+0424 - { 15, 16, 243, 0, 16, 30, 12737 }, // U+0425 - { 15, 20, 254, 1, 16, 38, 12767 }, // U+0426 - { 13, 16, 234, 1, 16, 26, 12805 }, // U+0427 - { 19, 16, 328, 1, 16, 38, 12831 }, // U+0428 - { 20, 20, 337, 1, 16, 50, 12869 }, // U+0429 - { 17, 16, 275, 0, 16, 34, 12919 }, // U+042A - { 17, 16, 303, 1, 16, 34, 12953 }, // U+042B - { 12, 16, 219, 1, 16, 24, 12987 }, // U+042C - { 15, 17, 246, 0, 16, 32, 13011 }, // U+042D - { 20, 17, 346, 1, 16, 43, 13043 }, // U+042E - { 13, 16, 218, 0, 16, 26, 13086 }, // U+042F - { 11, 13, 191, 0, 12, 18, 13112 }, // U+0430 - { 12, 17, 201, 0, 16, 26, 13130 }, // U+0431 - { 11, 12, 189, 1, 12, 17, 13156 }, // U+0432 - { 8, 12, 150, 1, 12, 12, 13173 }, // U+0433 - { 14, 16, 219, 0, 12, 28, 13185 }, // U+0434 - { 12, 13, 196, 0, 12, 20, 13213 }, // U+0435 - { 18, 12, 286, 0, 12, 27, 13233 }, // U+0436 - { 11, 13, 171, 0, 12, 18, 13260 }, // U+0437 - { 11, 12, 206, 1, 12, 17, 13278 }, // U+0438 - { 11, 16, 206, 1, 16, 22, 13295 }, // U+0439 - { 11, 12, 192, 1, 12, 17, 13317 }, // U+043A - { 12, 12, 200, 0, 12, 18, 13334 }, // U+043B - { 15, 12, 268, 1, 12, 23, 13352 }, // U+043C - { 11, 12, 205, 1, 12, 17, 13375 }, // U+043D - { 12, 13, 202, 0, 12, 20, 13392 }, // U+043E - { 11, 12, 202, 1, 12, 17, 13412 }, // U+043F - { 12, 17, 208, 1, 12, 26, 13429 }, // U+0440 - { 12, 13, 194, 0, 12, 20, 13455 }, // U+0441 - { 11, 12, 170, 0, 12, 17, 13475 }, // U+0442 - { 13, 17, 197, 0, 12, 28, 13492 }, // U+0443 - { 16, 21, 256, 0, 16, 42, 13520 }, // U+0444 - { 12, 12, 190, 0, 12, 18, 13562 }, // U+0445 - { 12, 16, 217, 1, 12, 24, 13580 }, // U+0446 - { 11, 12, 199, 1, 12, 17, 13604 }, // U+0447 - { 16, 12, 285, 1, 12, 24, 13621 }, // U+0448 - { 18, 16, 294, 1, 12, 36, 13645 }, // U+0449 - { 14, 12, 223, 0, 12, 21, 13681 }, // U+044A - { 15, 12, 264, 1, 12, 23, 13702 }, // U+044B - { 11, 12, 193, 1, 12, 17, 13725 }, // U+044C - { 12, 13, 194, 0, 12, 20, 13742 }, // U+044D - { 17, 13, 286, 1, 12, 28, 13762 }, // U+044E - { 11, 12, 191, 0, 12, 17, 13790 }, // U+044F - { 12, 17, 196, 0, 16, 26, 13807 }, // U+0450 - { 12, 17, 196, 0, 16, 26, 13833 }, // U+0451 - { 13, 21, 205, -1, 16, 35, 13859 }, // U+0452 - { 8, 16, 150, 1, 16, 16, 13894 }, // U+0453 - { 12, 13, 194, 0, 12, 20, 13910 }, // U+0454 - { 11, 13, 184, 0, 12, 18, 13930 }, // U+0455 - { 4, 16, 89, 1, 16, 8, 13948 }, // U+0456 - { 9, 16, 89, -2, 16, 18, 13956 }, // U+0457 - { 6, 21, 89, -1, 16, 16, 13974 }, // U+0458 - { 19, 12, 311, 0, 12, 29, 13990 }, // U+0459 - { 18, 12, 305, 1, 12, 27, 14019 }, // U+045A - { 13, 16, 205, -1, 16, 26, 14046 }, // U+045B - { 11, 16, 192, 1, 16, 22, 14072 }, // U+045C - { 11, 16, 206, 1, 16, 22, 14094 }, // U+045D - { 13, 21, 197, 0, 16, 35, 14116 }, // U+045E - { 11, 16, 202, 1, 12, 22, 14151 }, // U+045F - { 18, 17, 324, 1, 16, 39, 14173 }, // U+0460 - { 16, 13, 292, 1, 12, 26, 14212 }, // U+0461 - { 14, 16, 236, 0, 16, 28, 14238 }, // U+0462 - { 13, 16, 212, 0, 16, 26, 14266 }, // U+0463 - { 19, 17, 332, 1, 16, 41, 14292 }, // U+0464 - { 16, 13, 274, 1, 12, 26, 14333 }, // U+0465 - { 15, 16, 246, 0, 16, 30, 14359 }, // U+0466 - { 12, 12, 195, 0, 12, 18, 14389 }, // U+0467 - { 20, 16, 329, 1, 16, 40, 14407 }, // U+0468 - { 16, 12, 276, 1, 12, 24, 14447 }, // U+0469 - { 17, 16, 292, 1, 16, 34, 14471 }, // U+046A - { 14, 12, 248, 1, 12, 21, 14505 }, // U+046B - { 23, 16, 393, 1, 16, 46, 14526 }, // U+046C - { 20, 12, 338, 1, 12, 30, 14572 }, // U+046D - { 10, 25, 188, 1, 20, 32, 14602 }, // U+046E - { 10, 21, 179, 0, 16, 27, 14634 }, // U+046F - { 17, 16, 296, 1, 16, 34, 14661 }, // U+0470 - { 15, 19, 275, 1, 14, 36, 14695 }, // U+0471 - { 16, 17, 255, 0, 16, 34, 14731 }, // U+0472 - { 12, 13, 202, 0, 12, 20, 14765 }, // U+0473 - { 16, 16, 250, 0, 16, 32, 14785 }, // U+0474 - { 13, 12, 203, 0, 12, 20, 14817 }, // U+0475 - { 16, 20, 250, 0, 20, 40, 14837 }, // U+0476 - { 13, 16, 203, 0, 16, 26, 14877 }, // U+0477 - { 28, 21, 452, 0, 16, 74, 14903 }, // U+0478 - { 25, 17, 393, 0, 12, 54, 14977 }, // U+0479 - { 14, 16, 238, 0, 16, 28, 15031 }, // U+0480 - { 12, 16, 193, 0, 12, 24, 15059 }, // U+0481 - { 12, 14, 221, 1, 14, 21, 15083 }, // U+0482 - { 9, 4, 0, -11, 16, 5, 15104 }, // U+0483 - { 8, 3, 0, -10, 16, 3, 15109 }, // U+0484 - { 3, 4, 0, -7, 17, 2, 15112 }, // U+0485 - { 3, 4, 0, -7, 17, 2, 15114 }, // U+0486 - { 9, 3, 0, -11, 16, 4, 15116 }, // U+0487 - { 21, 19, 0, -17, 15, 50, 15120 }, // U+0488 - { 20, 21, 0, -16, 16, 53, 15170 }, // U+0489 - { 15, 24, 251, 1, 20, 45, 15223 }, // U+048A - { 13, 20, 206, 1, 16, 33, 15268 }, // U+048B - { 14, 16, 236, 0, 16, 28, 15301 }, // U+048C - { 13, 14, 212, 0, 14, 23, 15329 }, // U+048D - { 12, 16, 218, 1, 16, 24, 15352 }, // U+048E - { 12, 17, 202, 1, 12, 26, 15376 }, // U+048F - { 11, 19, 192, 1, 19, 27, 15402 }, // U+0490 - { 8, 15, 150, 1, 15, 15, 15429 }, // U+0491 - { 13, 16, 192, -1, 16, 26, 15444 }, // U+0492 - { 10, 12, 150, -1, 12, 15, 15470 }, // U+0493 - { 12, 18, 220, 1, 16, 27, 15485 }, // U+0494 - { 11, 16, 189, 1, 12, 22, 15512 }, // U+0495 - { 23, 20, 363, 0, 16, 58, 15534 }, // U+0496 - { 19, 16, 296, 0, 12, 38, 15592 }, // U+0497 - { 13, 21, 213, 0, 16, 35, 15630 }, // U+0498 - { 11, 17, 171, 0, 12, 24, 15665 }, // U+0499 - { 15, 20, 252, 1, 16, 38, 15689 }, // U+049A - { 12, 16, 204, 1, 12, 24, 15727 }, // U+049B - { 14, 16, 237, 1, 16, 28, 15751 }, // U+049C - { 13, 12, 209, 1, 12, 20, 15779 }, // U+049D - { 18, 16, 293, 0, 16, 36, 15799 }, // U+04A0 - { 14, 12, 223, 0, 12, 21, 15835 }, // U+04A1 - { 15, 20, 256, 1, 16, 38, 15856 }, // U+04A2 - { 13, 16, 214, 1, 12, 26, 15894 }, // U+04A3 - { 20, 16, 333, 1, 16, 40, 15920 }, // U+04A4 - { 14, 12, 236, 1, 12, 21, 15960 }, // U+04A5 - { 22, 18, 373, 1, 16, 50, 15981 }, // U+04A6 - { 18, 16, 312, 1, 12, 36, 16031 }, // U+04A7 - { 22, 17, 347, 0, 16, 47, 16067 }, // U+04A8 - { 17, 13, 278, 0, 12, 28, 16114 }, // U+04A9 - { 15, 21, 246, 0, 16, 40, 16142 }, // U+04AA - { 12, 17, 194, 0, 12, 26, 16182 }, // U+04AB - { 14, 20, 220, 0, 16, 35, 16208 }, // U+04AC - { 11, 16, 170, 0, 12, 22, 16243 }, // U+04AD - { 15, 16, 240, 0, 16, 30, 16265 }, // U+04AE - { 12, 17, 196, 0, 12, 26, 16295 }, // U+04AF - { 15, 16, 240, 0, 16, 30, 16321 }, // U+04B0 - { 12, 17, 196, 0, 12, 26, 16351 }, // U+04B1 - { 16, 20, 255, 0, 16, 40, 16377 }, // U+04B2 - { 13, 16, 202, 0, 12, 26, 16417 }, // U+04B3 - { 20, 20, 316, 0, 16, 50, 16443 }, // U+04B4 - { 15, 16, 236, 0, 12, 30, 16493 }, // U+04B5 - { 14, 20, 243, 1, 16, 35, 16523 }, // U+04B6 - { 12, 16, 208, 1, 12, 24, 16558 }, // U+04B7 - { 13, 16, 234, 1, 16, 26, 16582 }, // U+04B8 - { 11, 12, 199, 1, 12, 17, 16608 }, // U+04B9 - { 13, 16, 234, 1, 16, 26, 16625 }, // U+04BA - { 11, 16, 205, 1, 16, 22, 16651 }, // U+04BB - { 18, 17, 296, 0, 16, 39, 16673 }, // U+04BC - { 15, 13, 249, 0, 12, 25, 16712 }, // U+04BD - { 18, 20, 296, 0, 16, 45, 16737 }, // U+04BE - { 15, 17, 249, 0, 12, 32, 16782 }, // U+04BF - { 4, 16, 92, 1, 16, 8, 16814 }, // U+04C0 - { 22, 20, 351, 0, 20, 55, 16822 }, // U+04C1 - { 18, 16, 286, 0, 16, 36, 16877 }, // U+04C2 - { 13, 20, 230, 1, 16, 33, 16913 }, // U+04C3 - { 11, 16, 205, 1, 12, 22, 16946 }, // U+04C4 - { 17, 20, 248, 0, 16, 43, 16968 }, // U+04C5 - { 14, 16, 200, 0, 12, 28, 17011 }, // U+04C6 - { 14, 21, 247, 1, 16, 37, 17039 }, // U+04C7 - { 11, 17, 205, 1, 12, 24, 17076 }, // U+04C8 - { 16, 20, 247, 1, 16, 40, 17100 }, // U+04C9 - { 13, 16, 205, 1, 12, 26, 17140 }, // U+04CA - { 14, 20, 243, 1, 16, 35, 17166 }, // U+04CB - { 12, 16, 208, 1, 12, 24, 17201 }, // U+04CC - { 19, 20, 308, 1, 16, 48, 17225 }, // U+04CD - { 17, 16, 268, 1, 12, 34, 17273 }, // U+04CE - { 4, 16, 89, 1, 16, 8, 17307 }, // U+04CF - { 15, 20, 246, 0, 20, 38, 17315 }, // U+04D0 - { 11, 17, 191, 0, 16, 24, 17353 }, // U+04D1 - { 15, 20, 246, 0, 20, 38, 17377 }, // U+04D2 - { 11, 17, 191, 0, 16, 24, 17415 }, // U+04D3 - { 21, 16, 340, 0, 16, 42, 17439 }, // U+04D4 - { 19, 13, 302, 0, 12, 31, 17481 }, // U+04D5 - { 11, 20, 202, 1, 20, 28, 17512 }, // U+04D6 - { 12, 17, 196, 0, 16, 26, 17540 }, // U+04D7 - { 15, 17, 248, 0, 16, 32, 17566 }, // U+04D8 - { 12, 13, 196, 0, 12, 20, 17598 }, // U+04D9 - { 15, 21, 248, 0, 20, 40, 17618 }, // U+04DA - { 12, 17, 196, 0, 16, 26, 17658 }, // U+04DB - { 22, 20, 351, 0, 20, 55, 17684 }, // U+04DC - { 18, 16, 286, 0, 16, 36, 17739 }, // U+04DD - { 13, 21, 213, 0, 20, 35, 17775 }, // U+04DE - { 11, 17, 171, 0, 16, 24, 17810 }, // U+04DF - { 13, 17, 213, 0, 16, 28, 17834 }, // U+04E0 - { 13, 17, 213, 0, 12, 28, 17862 }, // U+04E1 - { 14, 20, 251, 1, 20, 35, 17890 }, // U+04E2 - { 11, 16, 206, 1, 16, 22, 17925 }, // U+04E3 - { 14, 20, 251, 1, 20, 35, 17947 }, // U+04E4 - { 11, 16, 206, 1, 16, 22, 17982 }, // U+04E5 - { 16, 21, 255, 0, 20, 42, 18004 }, // U+04E6 - { 12, 17, 202, 0, 16, 26, 18046 }, // U+04E7 - { 16, 17, 255, 0, 16, 34, 18072 }, // U+04E8 - { 12, 13, 202, 0, 12, 20, 18106 }, // U+04E9 - { 16, 21, 255, 0, 20, 42, 18126 }, // U+04EA - { 12, 17, 202, 0, 16, 26, 18168 }, // U+04EB - { 15, 21, 246, 0, 20, 40, 18194 }, // U+04EC - { 12, 17, 194, 0, 16, 26, 18234 }, // U+04ED - { 14, 20, 228, 0, 20, 35, 18260 }, // U+04EE - { 13, 21, 197, 0, 16, 35, 18295 }, // U+04EF - { 14, 20, 228, 0, 20, 35, 18330 }, // U+04F0 - { 13, 21, 197, 0, 16, 35, 18365 }, // U+04F1 - { 14, 20, 228, 0, 20, 35, 18400 }, // U+04F2 - { 13, 21, 197, 0, 16, 35, 18435 }, // U+04F3 - { 13, 20, 234, 1, 20, 33, 18470 }, // U+04F4 - { 11, 16, 199, 1, 16, 22, 18503 }, // U+04F5 - { 11, 20, 192, 1, 16, 28, 18525 }, // U+04F6 - { 8, 16, 150, 1, 12, 16, 18553 }, // U+04F7 - { 17, 20, 303, 1, 20, 43, 18569 }, // U+04F8 - { 15, 16, 264, 1, 16, 30, 18612 }, // U+04F9 - { 13, 21, 205, 0, 16, 35, 18642 }, // U+04FA - { 10, 17, 150, -1, 12, 22, 18677 }, // U+04FB - { 16, 21, 261, 0, 16, 42, 18699 }, // U+04FC - { 11, 17, 179, 0, 12, 24, 18741 }, // U+04FD - { 15, 16, 243, 0, 16, 30, 18765 }, // U+04FE - { 12, 12, 190, 0, 12, 18, 18795 }, // U+04FF - { 13, 13, 227, 1, 13, 22, 18813 }, // U+05D0 - { 12, 13, 188, 0, 13, 20, 18835 }, // U+05D1 - { 9, 13, 151, 0, 13, 15, 18855 }, // U+05D2 - { 12, 13, 183, 0, 13, 20, 18870 }, // U+05D3 - { 12, 13, 219, 1, 13, 20, 18890 }, // U+05D4 - { 4, 13, 99, 1, 13, 7, 18910 }, // U+05D5 - { 9, 13, 135, 0, 13, 15, 18917 }, // U+05D6 - { 12, 13, 226, 1, 13, 20, 18932 }, // U+05D7 - { 12, 13, 220, 1, 13, 20, 18952 }, // U+05D8 - { 4, 8, 94, 1, 13, 4, 18972 }, // U+05D9 - { 10, 17, 176, 0, 13, 22, 18976 }, // U+05DA - { 11, 13, 181, 0, 13, 18, 18998 }, // U+05DB - { 11, 16, 172, 0, 16, 22, 19016 }, // U+05DC - { 12, 13, 226, 1, 13, 20, 19038 }, // U+05DD - { 14, 13, 230, 0, 13, 23, 19058 }, // U+05DE - { 4, 17, 99, 1, 13, 9, 19081 }, // U+05DF - { 7, 13, 133, 0, 13, 12, 19090 }, // U+05E0 - { 13, 13, 221, 0, 13, 22, 19102 }, // U+05E1 - { 12, 14, 205, 0, 13, 21, 19124 }, // U+05E2 - { 12, 17, 216, 1, 13, 26, 19145 }, // U+05E3 - { 12, 13, 218, 1, 13, 20, 19171 }, // U+05E4 - { 12, 17, 194, 0, 13, 26, 19191 }, // U+05E5 - { 12, 13, 205, 0, 13, 20, 19217 }, // U+05E6 - { 12, 17, 213, 1, 13, 26, 19237 }, // U+05E7 - { 10, 13, 179, 0, 13, 17, 19263 }, // U+05E8 - { 15, 13, 267, 1, 13, 25, 19280 }, // U+05E9 - { 12, 13, 222, 1, 13, 20, 19305 }, // U+05EA - { 15, 21, 246, 0, 16, 40, 19325 }, // U+1EA0 - { 11, 17, 191, 0, 12, 24, 19365 }, // U+1EA1 - { 15, 20, 246, 0, 20, 38, 19389 }, // U+1EA2 - { 11, 17, 191, 0, 16, 24, 19427 }, // U+1EA3 - { 15, 24, 246, 0, 24, 45, 19451 }, // U+1EA4 - { 12, 21, 191, 0, 20, 32, 19496 }, // U+1EA5 - { 15, 24, 246, 0, 24, 45, 19528 }, // U+1EA6 - { 11, 21, 191, 0, 20, 29, 19573 }, // U+1EA7 - { 15, 24, 246, 0, 24, 45, 19602 }, // U+1EA8 - { 11, 21, 191, 0, 20, 29, 19647 }, // U+1EA9 - { 15, 23, 246, 0, 23, 44, 19676 }, // U+1EAA - { 11, 20, 191, 0, 19, 28, 19720 }, // U+1EAB - { 15, 25, 246, 0, 20, 47, 19748 }, // U+1EAC - { 11, 21, 191, 0, 16, 29, 19795 }, // U+1EAD - { 15, 24, 246, 0, 24, 45, 19824 }, // U+1EAE - { 11, 21, 191, 0, 20, 29, 19869 }, // U+1EAF - { 15, 24, 246, 0, 24, 45, 19898 }, // U+1EB0 - { 11, 21, 191, 0, 20, 29, 19943 }, // U+1EB1 - { 15, 24, 246, 0, 24, 45, 19972 }, // U+1EB2 - { 11, 21, 191, 0, 20, 29, 20017 }, // U+1EB3 - { 15, 23, 246, 0, 23, 44, 20046 }, // U+1EB4 - { 11, 20, 191, 0, 19, 28, 20090 }, // U+1EB5 - { 15, 25, 246, 0, 20, 47, 20118 }, // U+1EB6 - { 11, 21, 191, 0, 16, 29, 20165 }, // U+1EB7 - { 11, 21, 202, 1, 16, 29, 20194 }, // U+1EB8 - { 12, 17, 196, 0, 12, 26, 20223 }, // U+1EB9 - { 11, 20, 202, 1, 20, 28, 20249 }, // U+1EBA - { 12, 17, 196, 0, 16, 26, 20277 }, // U+1EBB - { 11, 20, 202, 1, 20, 28, 20303 }, // U+1EBC - { 12, 17, 196, 0, 16, 26, 20331 }, // U+1EBD - { 11, 24, 202, 1, 24, 33, 20357 }, // U+1EBE - { 12, 21, 196, 0, 20, 32, 20390 }, // U+1EBF - { 11, 24, 202, 1, 24, 33, 20422 }, // U+1EC0 - { 12, 21, 196, 0, 20, 32, 20455 }, // U+1EC1 - { 11, 24, 202, 1, 24, 33, 20487 }, // U+1EC2 - { 12, 21, 196, 0, 20, 32, 20520 }, // U+1EC3 - { 11, 23, 202, 1, 23, 32, 20552 }, // U+1EC4 - { 12, 20, 196, 0, 19, 30, 20584 }, // U+1EC5 - { 11, 25, 202, 1, 20, 35, 20614 }, // U+1EC6 - { 12, 21, 196, 0, 16, 32, 20649 }, // U+1EC7 - { 5, 20, 92, 0, 20, 13, 20681 }, // U+1EC8 - { 5, 16, 89, 0, 16, 10, 20694 }, // U+1EC9 - { 4, 21, 92, 1, 16, 11, 20704 }, // U+1ECA - { 4, 21, 89, 1, 16, 11, 20715 }, // U+1ECB - { 16, 21, 255, 0, 16, 42, 20726 }, // U+1ECC - { 12, 17, 202, 0, 12, 26, 20768 }, // U+1ECD - { 16, 21, 255, 0, 20, 42, 20794 }, // U+1ECE - { 12, 17, 202, 0, 16, 26, 20836 }, // U+1ECF - { 16, 25, 255, 0, 24, 50, 20862 }, // U+1ED0 - { 12, 21, 202, 0, 20, 32, 20912 }, // U+1ED1 - { 16, 25, 255, 0, 24, 50, 20944 }, // U+1ED2 - { 12, 21, 202, 0, 20, 32, 20994 }, // U+1ED3 - { 16, 25, 255, 0, 24, 50, 21026 }, // U+1ED4 - { 12, 21, 202, 0, 20, 32, 21076 }, // U+1ED5 - { 16, 24, 255, 0, 23, 48, 21108 }, // U+1ED6 - { 12, 20, 202, 0, 19, 30, 21156 }, // U+1ED7 - { 16, 25, 255, 0, 20, 50, 21186 }, // U+1ED8 - { 12, 21, 202, 0, 16, 32, 21236 }, // U+1ED9 - { 16, 21, 257, 0, 20, 42, 21268 }, // U+1EDA - { 13, 17, 202, 0, 16, 28, 21310 }, // U+1EDB - { 16, 21, 257, 0, 20, 42, 21338 }, // U+1EDC - { 13, 17, 202, 0, 16, 28, 21380 }, // U+1EDD - { 16, 21, 257, 0, 20, 42, 21408 }, // U+1EDE - { 13, 17, 202, 0, 16, 28, 21450 }, // U+1EDF - { 16, 21, 257, 0, 20, 42, 21478 }, // U+1EE0 - { 13, 17, 202, 0, 16, 28, 21520 }, // U+1EE1 - { 16, 23, 257, 0, 18, 46, 21548 }, // U+1EE2 - { 13, 19, 202, 0, 14, 31, 21594 }, // U+1EE3 - { 13, 21, 242, 1, 16, 35, 21625 }, // U+1EE4 - { 11, 17, 205, 1, 12, 24, 21660 }, // U+1EE5 - { 13, 21, 242, 1, 20, 35, 21684 }, // U+1EE6 - { 11, 17, 205, 1, 16, 24, 21719 }, // U+1EE7 - { 15, 21, 241, 1, 20, 40, 21743 }, // U+1EE8 - { 13, 17, 219, 1, 16, 28, 21783 }, // U+1EE9 - { 15, 21, 241, 1, 20, 40, 21811 }, // U+1EEA - { 13, 17, 219, 1, 16, 28, 21851 }, // U+1EEB - { 15, 21, 241, 1, 20, 40, 21879 }, // U+1EEC - { 13, 17, 219, 1, 16, 28, 21919 }, // U+1EED - { 15, 21, 241, 1, 20, 40, 21947 }, // U+1EEE - { 13, 17, 219, 1, 16, 28, 21987 }, // U+1EEF - { 15, 23, 241, 1, 18, 44, 22015 }, // U+1EF0 - { 13, 19, 219, 1, 14, 31, 22059 }, // U+1EF1 - { 15, 20, 240, 0, 20, 38, 22090 }, // U+1EF2 - { 13, 21, 197, 0, 16, 35, 22128 }, // U+1EF3 - { 15, 21, 240, 0, 16, 40, 22163 }, // U+1EF4 - { 13, 17, 197, 0, 12, 28, 22203 }, // U+1EF5 - { 15, 20, 240, 0, 20, 38, 22231 }, // U+1EF6 - { 13, 21, 197, 0, 16, 35, 22269 }, // U+1EF7 - { 15, 20, 240, 0, 20, 38, 22304 }, // U+1EF8 - { 13, 21, 197, 0, 16, 35, 22342 }, // U+1EF9 - { 0, 0, 189, 0, 0, 0, 22377 }, // U+2000 - { 0, 0, 376, 0, 0, 0, 22377 }, // U+2001 - { 0, 0, 167, 0, 0, 0, 22377 }, // U+2002 - { 0, 0, 333, 0, 0, 0, 22377 }, // U+2003 - { 0, 0, 111, 0, 0, 0, 22377 }, // U+2004 - { 0, 0, 83, 0, 0, 0, 22377 }, // U+2005 - { 0, 0, 55, 0, 0, 0, 22377 }, // U+2006 - { 0, 0, 215, 0, 0, 0, 22377 }, // U+2007 - { 0, 0, 105, 0, 0, 0, 22377 }, // U+2008 - { 0, 0, 51, 0, 0, 0, 22377 }, // U+2009 - { 0, 0, 25, 0, 0, 0, 22377 }, // U+200A - { 0, 0, 0, 0, 0, 0, 22377 }, // U+200B - { 0, 0, 0, 0, 0, 0, 22377 }, // U+200C - { 0, 0, 0, 0, 0, 0, 22377 }, // U+200D - { 0, 0, 0, 0, 0, 0, 22377 }, // U+200E - { 0, 0, 0, 0, 0, 0, 22377 }, // U+200F - { 8, 4, 150, 1, 8, 4, 22377 }, // U+2010 - { 8, 4, 150, 1, 8, 4, 22381 }, // U+2011 - { 13, 3, 215, 0, 8, 5, 22385 }, // U+2012 - { 11, 4, 167, 0, 8, 6, 22390 }, // U+2013 - { 21, 4, 333, 0, 8, 11, 22396 }, // U+2014 - { 21, 4, 333, 0, 8, 11, 22407 }, // U+2015 - { 8, 26, 186, 2, 21, 26, 22418 }, // U+2016 - { 10, 5, 157, 0, 0, 7, 22444 }, // U+2017 - { 5, 8, 97, 1, 16, 5, 22451 }, // U+2018 - { 5, 8, 97, 1, 16, 5, 22456 }, // U+2019 - { 4, 8, 90, 1, 3, 4, 22461 }, // U+201A - { 5, 8, 97, 0, 16, 5, 22465 }, // U+201B - { 10, 8, 172, 1, 16, 10, 22470 }, // U+201C - { 9, 8, 169, 1, 16, 9, 22480 }, // U+201D - { 9, 8, 162, 1, 3, 9, 22489 }, // U+201E - { 10, 8, 169, 0, 16, 10, 22498 }, // U+201F - { 10, 13, 193, 1, 16, 17, 22508 }, // U+2020 - { 10, 16, 193, 1, 16, 20, 22525 }, // U+2021 - { 8, 8, 155, 1, 10, 8, 22545 }, // U+2022 - { 6, 8, 155, 2, 10, 6, 22553 }, // U+2023 - { 5, 5, 105, 1, 4, 4, 22559 }, // U+2024 - { 11, 5, 209, 1, 4, 7, 22563 }, // U+2025 - { 18, 5, 314, 1, 4, 12, 22570 }, // U+2026 - { 5, 5, 105, 1, 10, 4, 22582 }, // U+2027 - { 0, 0, 0, 0, 0, 0, 22586 }, // U+202A - { 0, 0, 0, 0, 0, 0, 22586 }, // U+202B - { 0, 0, 0, 0, 0, 0, 22586 }, // U+202C - { 0, 0, 0, 0, 0, 0, 22586 }, // U+202D - { 0, 0, 0, 0, 0, 0, 22586 }, // U+202E - { 0, 0, 51, 0, 0, 0, 22586 }, // U+202F - { 26, 17, 457, 1, 16, 56, 22586 }, // U+2030 - { 34, 17, 581, 1, 16, 73, 22642 }, // U+2031 - { 4, 7, 90, 1, 16, 4, 22715 }, // U+2032 - { 10, 7, 179, 1, 16, 9, 22719 }, // U+2033 - { 15, 7, 268, 1, 16, 14, 22728 }, // U+2034 - { 4, 7, 90, 1, 16, 4, 22742 }, // U+2035 - { 10, 7, 179, 1, 16, 9, 22746 }, // U+2036 - { 15, 7, 268, 1, 16, 14, 22755 }, // U+2037 - { 10, 5, 181, 1, -1, 7, 22769 }, // U+2038 - { 9, 10, 141, 0, 11, 12, 22776 }, // U+2039 - { 8, 10, 141, 0, 11, 10, 22788 }, // U+203A - { 15, 14, 268, 1, 14, 27, 22798 }, // U+203B - { 11, 17, 212, 1, 16, 24, 22825 }, // U+203C - { 11, 17, 185, 0, 16, 24, 22849 }, // U+203D - { 11, 3, 164, 0, 18, 5, 22873 }, // U+203E - { 18, 4, 281, 0, -1, 9, 22878 }, // U+203F - { 18, 4, 281, 0, 16, 9, 22887 }, // U+2040 - { 10, 12, 157, 0, 7, 15, 22896 }, // U+2041 - { 14, 13, 253, 1, 14, 23, 22911 }, // U+2042 - { 7, 4, 155, 1, 8, 4, 22934 }, // U+2043 - { 14, 16, 68, -5, 16, 28, 22938 }, // U+2044 - { 6, 20, 122, 1, 17, 15, 22966 }, // U+2045 - { 6, 20, 122, 0, 17, 15, 22981 }, // U+2046 - { 23, 17, 374, 0, 16, 49, 22996 }, // U+2047 - { 17, 17, 293, 0, 16, 37, 23045 }, // U+2048 - { 17, 17, 293, 1, 16, 37, 23082 }, // U+2049 - { 9, 17, 153, 0, 12, 20, 23119 }, // U+204A - { 11, 16, 198, 1, 16, 22, 23139 }, // U+204B - { 10, 10, 201, 1, 11, 13, 23161 }, // U+204C - { 11, 10, 201, 1, 11, 14, 23174 }, // U+204D - { 9, 10, 185, 1, 8, 12, 23188 }, // U+204E - { 5, 15, 107, 1, 11, 10, 23200 }, // U+204F - { 18, 21, 281, 0, 16, 48, 23210 }, // U+2050 - { 9, 20, 190, 2, 16, 23, 23258 }, // U+2051 - { 15, 16, 265, 1, 16, 30, 23281 }, // U+2052 - { 14, 6, 252, 1, 10, 11, 23311 }, // U+2053 - { 18, 4, 281, 0, -1, 9, 23322 }, // U+2054 - { 12, 11, 237, 1, 11, 17, 23331 }, // U+2055 - { 21, 7, 357, 1, 16, 19, 23348 }, // U+2057 - { 0, 0, 97, 0, 0, 0, 23367 }, // U+205F - { 0, 0, 0, 0, 0, 0, 23367 }, // U+2060 - { 0, 0, 0, 0, 0, 0, 23367 }, // U+2061 - { 0, 0, 0, 0, 0, 0, 23367 }, // U+2062 - { 0, 0, 0, 0, 0, 0, 23367 }, // U+2063 - { 0, 0, 0, 0, 0, 0, 23367 }, // U+2064 - { 0, 0, 0, 0, 0, 0, 23367 }, // U+2066 - { 0, 0, 0, 0, 0, 0, 23367 }, // U+2067 - { 0, 0, 0, 0, 0, 0, 23367 }, // U+2068 - { 0, 0, 0, 0, 0, 0, 23367 }, // U+2069 - { 0, 0, 0, 0, 0, 0, 23367 }, // U+206A - { 0, 0, 0, 0, 0, 0, 23367 }, // U+206B - { 0, 0, 0, 0, 0, 0, 23367 }, // U+206C - { 0, 0, 0, 0, 0, 0, 23367 }, // U+206D - { 0, 0, 0, 0, 0, 0, 23367 }, // U+206E - { 0, 0, 0, 0, 0, 0, 23367 }, // U+206F - { 10, 10, 160, 0, 18, 13, 23367 }, // U+2070 - { 3, 10, 72, 1, 19, 4, 23380 }, // U+2071 - { 10, 9, 157, 0, 18, 12, 23384 }, // U+2074 - { 8, 10, 152, 1, 18, 10, 23396 }, // U+2075 - { 9, 10, 153, 0, 18, 12, 23406 }, // U+2076 - { 8, 9, 137, 0, 18, 9, 23418 }, // U+2077 - { 9, 10, 155, 0, 18, 12, 23427 }, // U+2078 - { 9, 10, 153, 0, 18, 12, 23439 }, // U+2079 - { 8, 8, 154, 1, 17, 8, 23451 }, // U+207A - { 8, 2, 154, 1, 14, 2, 23459 }, // U+207B - { 8, 6, 154, 1, 16, 6, 23461 }, // U+207C - { 5, 12, 93, 1, 19, 8, 23467 }, // U+207D - { 5, 12, 93, 0, 19, 8, 23475 }, // U+207E - { 8, 7, 149, 1, 16, 7, 23483 }, // U+207F - { 10, 10, 160, 0, 6, 13, 23490 }, // U+2080 - { 6, 9, 106, 0, 6, 7, 23503 }, // U+2081 - { 9, 9, 149, 0, 6, 11, 23510 }, // U+2082 - { 9, 10, 152, 0, 6, 12, 23521 }, // U+2083 - { 10, 9, 157, 0, 6, 12, 23533 }, // U+2084 - { 8, 10, 152, 1, 6, 10, 23545 }, // U+2085 - { 9, 10, 153, 0, 6, 12, 23555 }, // U+2086 - { 8, 9, 137, 0, 6, 9, 23567 }, // U+2087 - { 9, 10, 155, 0, 6, 12, 23576 }, // U+2088 - { 9, 10, 153, 0, 6, 12, 23588 }, // U+2089 - { 8, 8, 154, 1, 5, 8, 23600 }, // U+208A - { 8, 2, 154, 1, 2, 2, 23608 }, // U+208B - { 8, 6, 154, 1, 4, 6, 23610 }, // U+208C - { 5, 12, 93, 1, 7, 8, 23616 }, // U+208D - { 5, 12, 93, 0, 7, 8, 23624 }, // U+208E - { 8, 8, 137, 0, 4, 8, 23632 }, // U+2090 - { 8, 8, 140, 0, 4, 8, 23640 }, // U+2091 - { 9, 8, 144, 0, 4, 9, 23648 }, // U+2092 - { 9, 7, 139, 0, 4, 8, 23657 }, // U+2093 - { 8, 9, 158, 1, 4, 9, 23665 }, // U+2094 - { 8, 9, 149, 1, 6, 9, 23674 }, // U+2095 - { 8, 9, 138, 1, 6, 9, 23683 }, // U+2096 - { 3, 9, 72, 1, 6, 4, 23692 }, // U+2097 - { 12, 7, 211, 1, 4, 11, 23696 }, // U+2098 - { 8, 7, 149, 1, 4, 7, 23707 }, // U+2099 - { 8, 10, 148, 1, 4, 10, 23714 }, // U+209A - { 8, 8, 134, 0, 4, 8, 23724 }, // U+209B - { 6, 9, 91, 0, 5, 7, 23732 }, // U+209C - { 15, 17, 252, 0, 16, 32, 23739 }, // U+20A0 - { 15, 20, 246, 0, 18, 38, 23771 }, // U+20A1 - { 15, 17, 240, 0, 16, 32, 23809 }, // U+20A2 - { 13, 16, 204, -1, 16, 26, 23841 }, // U+20A3 - { 13, 16, 211, 0, 16, 26, 23867 }, // U+20A4 - { 17, 16, 286, 1, 14, 34, 23893 }, // U+20A5 - { 16, 16, 251, 0, 16, 32, 23927 }, // U+20A6 - { 20, 17, 332, 1, 16, 43, 23959 }, // U+20A7 - { 23, 17, 388, 1, 16, 49, 24002 }, // U+20A8 - { 21, 16, 345, 0, 16, 42, 24051 }, // U+20A9 - { 17, 12, 305, 1, 12, 26, 24093 }, // U+20AA - { 14, 21, 208, 0, 16, 37, 24119 }, // U+20AB - { 14, 17, 227, 0, 16, 30, 24156 }, // U+20AC - { 17, 16, 265, 0, 16, 34, 24186 }, // U+20AD - { 14, 16, 220, 0, 16, 28, 24220 }, // U+20AE - { 30, 23, 483, 0, 18, 87, 24248 }, // U+20AF - { 15, 16, 236, 0, 16, 30, 24335 }, // U+20B1 - { 15, 21, 249, 0, 18, 40, 24365 }, // U+20B2 - { 15, 16, 246, 0, 16, 30, 24405 }, // U+20B3 - { 13, 17, 244, 1, 16, 28, 24435 }, // U+20B4 - { 15, 21, 246, 0, 18, 40, 24463 }, // U+20B5 - { 14, 16, 221, 0, 16, 28, 24503 }, // U+20B8 - { 12, 16, 189, 0, 16, 24, 24531 }, // U+20B9 - { 12, 17, 213, 1, 16, 26, 24555 }, // U+20BA - { 14, 12, 252, 1, 12, 21, 24581 }, // U+20BC - { 15, 16, 245, 0, 16, 30, 24602 }, // U+20BD - { 14, 16, 252, 1, 16, 28, 24632 }, // U+20BE - { 12, 20, 219, 1, 18, 30, 24660 }, // U+20BF - { 16, 15, 316, 2, 14, 30, 24690 }, // U+2190 - { 15, 16, 300, 2, 16, 30, 24720 }, // U+2191 - { 16, 15, 316, 2, 14, 30, 24750 }, // U+2192 - { 15, 17, 300, 2, 16, 32, 24780 }, // U+2193 - { 24, 15, 445, 2, 14, 45, 24812 }, // U+2194 - { 14, 25, 285, 2, 19, 44, 24857 }, // U+2195 - { 12, 13, 256, 2, 12, 20, 24901 }, // U+2196 - { 12, 13, 256, 2, 12, 20, 24921 }, // U+2197 - { 12, 12, 256, 2, 12, 18, 24941 }, // U+2198 - { 12, 12, 256, 2, 12, 18, 24959 }, // U+2199 - { 21, 17, 384, 2, 16, 45, 24977 }, // U+21A9 - { 22, 17, 384, 1, 16, 47, 25022 }, // U+21AA - { 17, 16, 335, 2, 16, 34, 25069 }, // U+21B0 - { 17, 16, 335, 2, 16, 34, 25103 }, // U+21B1 - { 13, 15, 300, 3, 15, 25, 25137 }, // U+21B2 - { 17, 15, 335, 2, 16, 32, 25162 }, // U+21B3 - { 15, 17, 297, 2, 16, 32, 25194 }, // U+21B4 - { 17, 15, 335, 2, 16, 32, 25226 }, // U+21B5 - { 19, 13, 300, 0, 14, 31, 25258 }, // U+21B6 - { 19, 13, 300, 0, 14, 31, 25289 }, // U+21B7 - { 16, 19, 316, 2, 19, 38, 25320 }, // U+21BA - { 16, 19, 316, 2, 19, 38, 25358 }, // U+21BB - { 15, 16, 300, 2, 16, 30, 25396 }, // U+21C4 - { 15, 16, 300, 2, 16, 30, 25426 }, // U+21C6 - { 16, 16, 316, 2, 15, 32, 25456 }, // U+21D0 - { 16, 16, 316, 2, 15, 32, 25488 }, // U+21D2 - { 24, 16, 445, 2, 15, 48, 25520 }, // U+21D4 - { 11, 16, 210, 1, 16, 22, 25568 }, // U+21DE - { 11, 17, 210, 1, 16, 24, 25590 }, // U+21DF - { 19, 11, 348, 1, 13, 27, 25614 }, // U+21E4 - { 19, 11, 348, 1, 13, 27, 25641 }, // U+21E5 - { 21, 17, 365, 1, 17, 45, 25668 }, // U+21E7 - { 21, 21, 365, 1, 17, 56, 25713 }, // U+21EA - { 12, 17, 221, 1, 16, 26, 25769 }, // U+2202 - { 14, 13, 228, 0, 12, 23, 25795 }, // U+2205 - { 15, 16, 244, 0, 16, 30, 25818 }, // U+2206 - { 12, 19, 254, 2, 16, 29, 25848 }, // U+220F - { 13, 19, 245, 1, 16, 31, 25877 }, // U+2211 - { 12, 4, 224, 1, 8, 6, 25908 }, // U+2212 - { 15, 15, 49, -6, 15, 29, 25914 }, // U+2215 - { 12, 16, 221, 1, 16, 24, 25943 }, // U+221A - { 20, 11, 325, 0, 12, 28, 25967 }, // U+221E - { 9, 22, 127, -1, 17, 25, 25995 }, // U+222B - { 5, 13, 105, 1, 14, 9, 26020 }, // U+2236 - { 12, 10, 224, 1, 11, 15, 26029 }, // U+2248 - { 12, 12, 224, 1, 12, 18, 26044 }, // U+2260 - { 12, 13, 224, 1, 13, 20, 26062 }, // U+2264 - { 12, 13, 224, 1, 13, 20, 26082 }, // U+2265 - { 29, 29, 463, 0, 22, 106, 26102 }, // U+2295 - { 29, 29, 463, 0, 22, 106, 26208 }, // U+2296 - { 29, 29, 463, 0, 22, 106, 26314 }, // U+2297 - { 29, 29, 463, 0, 22, 106, 26420 }, // U+2298 - { 13, 16, 216, 0, 16, 26, 26526 }, // U+FB01 - { 14, 16, 221, 0, 16, 28, 26552 }, // U+FB02 + { 5, 6, 0, -2, 15, 4, 11258 }, // U+0312 + { 5, 7, 0, 4, 10, 5, 11262 }, // U+0313 + { 5, 7, 0, 4, 10, 5, 11267 }, // U+0315 + { 4, 6, 0, -5, 16, 3, 11272 }, // U+031B + { 4, 4, 94, 1, -1, 2, 11275 }, // U+0323 + { 4, 5, 0, -9, -1, 3, 11277 }, // U+0326 + { 5, 6, 0, 2, 1, 4, 11280 }, // U+0327 + { 6, 6, 0, 0, 1, 5, 11284 }, // U+0328 + { 9, 4, 0, 0, -1, 5, 11289 }, // U+032C + { 9, 15, 0, -11, 13, 17, 11294 }, // U+0337 + { 13, 17, 0, -13, 16, 28, 11311 }, // U+0338 + { 8, 4, 0, -11, 16, 4, 11339 }, // U+0342 + { 5, 4, 0, 0, 13, 3, 11343 }, // U+0343 + { 9, 5, 0, -11, 17, 6, 11346 }, // U+0346 + { 8, 6, 0, -10, -2, 6, 11352 }, // U+0347 + { 6, 6, 0, -9, 0, 5, 11358 }, // U+0348 + { 5, 5, 0, -9, -1, 4, 11363 }, // U+0349 + { 9, 6, 0, -11, 17, 7, 11367 }, // U+034A + { 9, 10, 0, -11, 21, 12, 11374 }, // U+034B + { 9, 6, 0, -11, 18, 7, 11386 }, // U+034C + { 11, 5, 0, -12, -1, 7, 11393 }, // U+034D + { 5, 6, 0, -9, 0, 4, 11400 }, // U+034E + { 0, 0, 0, 0, 0, 0, 11404 }, // U+034F + { 9, 9, 0, -10, 21, 11, 11404 }, // U+0350 + { 3, 5, 0, -8, 16, 2, 11415 }, // U+0351 + { 8, 8, 0, -11, 19, 8, 11417 }, // U+0352 + { 6, 6, 0, -9, 0, 5, 11425 }, // U+0353 + { 6, 6, 0, -9, 0, 5, 11430 }, // U+0354 + { 7, 6, 0, -10, 0, 6, 11435 }, // U+0355 + { 12, 6, 0, -11, 0, 9, 11441 }, // U+0356 + { 4, 6, 0, 0, 18, 3, 11450 }, // U+0357 + { 4, 4, 0, 1, 16, 2, 11453 }, // U+0358 + { 5, 6, 0, -9, 0, 4, 11455 }, // U+0359 + { 9, 5, 0, -11, 0, 6, 11459 }, // U+035A + { 5, 5, 0, -9, 17, 4, 11465 }, // U+035B + { 16, 5, 0, -9, 0, 10, 11469 }, // U+035C + { 17, 5, 0, -9, 16, 11, 11479 }, // U+035D + { 13, 3, 0, -13, 16, 5, 11490 }, // U+035E + { 13, 3, 0, -13, -1, 5, 11495 }, // U+035F + { 22, 4, 0, -12, 16, 11, 11500 }, // U+0360 + { 17, 5, 0, -9, 16, 11, 11511 }, // U+0361 + { 19, 5, 0, -9, -1, 12, 11522 }, // U+0362 + { 7, 6, 0, -10, 18, 6, 11534 }, // U+0363 + { 7, 6, 0, -10, 18, 6, 11540 }, // U+0364 + { 3, 9, 0, -8, 21, 4, 11546 }, // U+0365 + { 7, 6, 0, -10, 18, 6, 11550 }, // U+0366 + { 7, 6, 0, -10, 18, 6, 11556 }, // U+0367 + { 6, 6, 0, -10, 18, 5, 11562 }, // U+0368 + { 7, 9, 0, -10, 21, 8, 11567 }, // U+0369 + { 7, 9, 0, -10, 21, 8, 11575 }, // U+036A + { 9, 6, 0, -11, 18, 7, 11583 }, // U+036B + { 4, 6, 0, -8, 18, 3, 11590 }, // U+036C + { 4, 8, 0, 0, 20, 4, 11593 }, // U+036D + { 7, 6, 0, -10, 18, 6, 11597 }, // U+036E + { 7, 6, 0, -10, 18, 6, 11603 }, // U+036F + { 11, 20, 202, 1, 20, 28, 11609 }, // U+0400 + { 11, 20, 202, 1, 20, 28, 11637 }, // U+0401 + { 17, 18, 284, 0, 16, 39, 11665 }, // U+0402 + { 11, 20, 192, 1, 20, 28, 11704 }, // U+0403 + { 15, 17, 246, 0, 16, 32, 11732 }, // U+0404 + { 13, 17, 218, 0, 16, 28, 11764 }, // U+0405 + { 4, 16, 92, 1, 16, 8, 11792 }, // U+0406 + { 8, 20, 92, -1, 20, 20, 11800 }, // U+0407 + { 11, 17, 193, 0, 16, 24, 11820 }, // U+0408 + { 23, 16, 370, 0, 16, 46, 11844 }, // U+0409 + { 22, 16, 373, 1, 16, 44, 11890 }, // U+040A + { 18, 16, 296, 0, 16, 36, 11934 }, // U+040B + { 14, 20, 237, 1, 20, 35, 11970 }, // U+040C + { 14, 20, 251, 1, 20, 35, 12005 }, // U+040D + { 14, 20, 228, 0, 20, 35, 12040 }, // U+040E + { 13, 20, 241, 1, 16, 33, 12075 }, // U+040F + { 15, 16, 246, 0, 16, 30, 12108 }, // U+0410 + { 12, 16, 214, 1, 16, 24, 12138 }, // U+0411 + { 12, 16, 219, 1, 16, 24, 12162 }, // U+0412 + { 11, 16, 192, 1, 16, 22, 12186 }, // U+0413 + { 18, 20, 285, 0, 16, 45, 12208 }, // U+0414 + { 11, 16, 202, 1, 16, 22, 12253 }, // U+0415 + { 22, 16, 351, 0, 16, 44, 12275 }, // U+0416 + { 13, 17, 213, 0, 16, 28, 12319 }, // U+0417 + { 14, 16, 251, 1, 16, 28, 12347 }, // U+0418 + { 14, 20, 251, 1, 20, 35, 12375 }, // U+0419 + { 14, 16, 237, 1, 16, 28, 12410 }, // U+041A + { 15, 16, 248, 0, 16, 30, 12438 }, // U+041B + { 17, 16, 308, 1, 16, 34, 12468 }, // U+041C + { 14, 16, 247, 1, 16, 28, 12502 }, // U+041D + { 16, 17, 255, 0, 16, 34, 12530 }, // U+041E + { 14, 16, 247, 1, 16, 28, 12564 }, // U+041F + { 12, 16, 215, 1, 16, 24, 12592 }, // U+0420 + { 15, 17, 246, 0, 16, 32, 12616 }, // U+0421 + { 14, 16, 220, 0, 16, 28, 12648 }, // U+0422 + { 14, 16, 228, 0, 16, 28, 12676 }, // U+0423 + { 17, 17, 277, 0, 16, 37, 12704 }, // U+0424 + { 15, 16, 243, 0, 16, 30, 12741 }, // U+0425 + { 15, 20, 254, 1, 16, 38, 12771 }, // U+0426 + { 13, 16, 234, 1, 16, 26, 12809 }, // U+0427 + { 19, 16, 328, 1, 16, 38, 12835 }, // U+0428 + { 20, 20, 337, 1, 16, 50, 12873 }, // U+0429 + { 17, 16, 275, 0, 16, 34, 12923 }, // U+042A + { 17, 16, 303, 1, 16, 34, 12957 }, // U+042B + { 12, 16, 219, 1, 16, 24, 12991 }, // U+042C + { 15, 17, 246, 0, 16, 32, 13015 }, // U+042D + { 20, 17, 346, 1, 16, 43, 13047 }, // U+042E + { 13, 16, 218, 0, 16, 26, 13090 }, // U+042F + { 11, 13, 191, 0, 12, 18, 13116 }, // U+0430 + { 12, 17, 201, 0, 16, 26, 13134 }, // U+0431 + { 11, 12, 189, 1, 12, 17, 13160 }, // U+0432 + { 8, 12, 150, 1, 12, 12, 13177 }, // U+0433 + { 14, 16, 219, 0, 12, 28, 13189 }, // U+0434 + { 12, 13, 196, 0, 12, 20, 13217 }, // U+0435 + { 18, 12, 286, 0, 12, 27, 13237 }, // U+0436 + { 11, 13, 171, 0, 12, 18, 13264 }, // U+0437 + { 11, 12, 206, 1, 12, 17, 13282 }, // U+0438 + { 11, 16, 206, 1, 16, 22, 13299 }, // U+0439 + { 11, 12, 192, 1, 12, 17, 13321 }, // U+043A + { 12, 12, 200, 0, 12, 18, 13338 }, // U+043B + { 15, 12, 268, 1, 12, 23, 13356 }, // U+043C + { 11, 12, 205, 1, 12, 17, 13379 }, // U+043D + { 12, 13, 202, 0, 12, 20, 13396 }, // U+043E + { 11, 12, 202, 1, 12, 17, 13416 }, // U+043F + { 12, 17, 208, 1, 12, 26, 13433 }, // U+0440 + { 12, 13, 194, 0, 12, 20, 13459 }, // U+0441 + { 11, 12, 170, 0, 12, 17, 13479 }, // U+0442 + { 13, 17, 197, 0, 12, 28, 13496 }, // U+0443 + { 16, 21, 256, 0, 16, 42, 13524 }, // U+0444 + { 12, 12, 190, 0, 12, 18, 13566 }, // U+0445 + { 12, 16, 217, 1, 12, 24, 13584 }, // U+0446 + { 11, 12, 199, 1, 12, 17, 13608 }, // U+0447 + { 16, 12, 285, 1, 12, 24, 13625 }, // U+0448 + { 18, 16, 294, 1, 12, 36, 13649 }, // U+0449 + { 14, 12, 223, 0, 12, 21, 13685 }, // U+044A + { 15, 12, 264, 1, 12, 23, 13706 }, // U+044B + { 11, 12, 193, 1, 12, 17, 13729 }, // U+044C + { 12, 13, 194, 0, 12, 20, 13746 }, // U+044D + { 17, 13, 286, 1, 12, 28, 13766 }, // U+044E + { 11, 12, 191, 0, 12, 17, 13794 }, // U+044F + { 12, 17, 196, 0, 16, 26, 13811 }, // U+0450 + { 12, 17, 196, 0, 16, 26, 13837 }, // U+0451 + { 13, 21, 205, -1, 16, 35, 13863 }, // U+0452 + { 8, 16, 150, 1, 16, 16, 13898 }, // U+0453 + { 12, 13, 194, 0, 12, 20, 13914 }, // U+0454 + { 11, 13, 184, 0, 12, 18, 13934 }, // U+0455 + { 4, 16, 89, 1, 16, 8, 13952 }, // U+0456 + { 9, 16, 89, -2, 16, 18, 13960 }, // U+0457 + { 6, 21, 89, -1, 16, 16, 13978 }, // U+0458 + { 19, 12, 311, 0, 12, 29, 13994 }, // U+0459 + { 18, 12, 305, 1, 12, 27, 14023 }, // U+045A + { 13, 16, 205, -1, 16, 26, 14050 }, // U+045B + { 11, 16, 192, 1, 16, 22, 14076 }, // U+045C + { 11, 16, 206, 1, 16, 22, 14098 }, // U+045D + { 13, 21, 197, 0, 16, 35, 14120 }, // U+045E + { 11, 16, 202, 1, 12, 22, 14155 }, // U+045F + { 18, 17, 324, 1, 16, 39, 14177 }, // U+0460 + { 16, 13, 292, 1, 12, 26, 14216 }, // U+0461 + { 14, 16, 236, 0, 16, 28, 14242 }, // U+0462 + { 13, 16, 212, 0, 16, 26, 14270 }, // U+0463 + { 19, 17, 332, 1, 16, 41, 14296 }, // U+0464 + { 16, 13, 274, 1, 12, 26, 14337 }, // U+0465 + { 15, 16, 246, 0, 16, 30, 14363 }, // U+0466 + { 12, 12, 195, 0, 12, 18, 14393 }, // U+0467 + { 20, 16, 329, 1, 16, 40, 14411 }, // U+0468 + { 16, 12, 276, 1, 12, 24, 14451 }, // U+0469 + { 17, 16, 292, 1, 16, 34, 14475 }, // U+046A + { 14, 12, 248, 1, 12, 21, 14509 }, // U+046B + { 23, 16, 393, 1, 16, 46, 14530 }, // U+046C + { 20, 12, 338, 1, 12, 30, 14576 }, // U+046D + { 10, 25, 188, 1, 20, 32, 14606 }, // U+046E + { 10, 21, 179, 0, 16, 27, 14638 }, // U+046F + { 17, 16, 296, 1, 16, 34, 14665 }, // U+0470 + { 15, 19, 275, 1, 14, 36, 14699 }, // U+0471 + { 16, 17, 255, 0, 16, 34, 14735 }, // U+0472 + { 12, 13, 202, 0, 12, 20, 14769 }, // U+0473 + { 16, 16, 250, 0, 16, 32, 14789 }, // U+0474 + { 13, 12, 203, 0, 12, 20, 14821 }, // U+0475 + { 16, 20, 250, 0, 20, 40, 14841 }, // U+0476 + { 13, 16, 203, 0, 16, 26, 14881 }, // U+0477 + { 28, 21, 452, 0, 16, 74, 14907 }, // U+0478 + { 25, 17, 393, 0, 12, 54, 14981 }, // U+0479 + { 14, 16, 238, 0, 16, 28, 15035 }, // U+0480 + { 12, 16, 193, 0, 12, 24, 15063 }, // U+0481 + { 12, 14, 221, 1, 14, 21, 15087 }, // U+0482 + { 9, 4, 0, -11, 16, 5, 15108 }, // U+0483 + { 8, 3, 0, -10, 16, 3, 15113 }, // U+0484 + { 3, 4, 0, -7, 17, 2, 15116 }, // U+0485 + { 3, 4, 0, -7, 17, 2, 15118 }, // U+0486 + { 9, 3, 0, -11, 16, 4, 15120 }, // U+0487 + { 21, 19, 0, -17, 15, 50, 15124 }, // U+0488 + { 20, 21, 0, -16, 16, 53, 15174 }, // U+0489 + { 15, 24, 251, 1, 20, 45, 15227 }, // U+048A + { 13, 20, 206, 1, 16, 33, 15272 }, // U+048B + { 14, 16, 236, 0, 16, 28, 15305 }, // U+048C + { 13, 14, 212, 0, 14, 23, 15333 }, // U+048D + { 12, 16, 218, 1, 16, 24, 15356 }, // U+048E + { 12, 17, 202, 1, 12, 26, 15380 }, // U+048F + { 11, 19, 192, 1, 19, 27, 15406 }, // U+0490 + { 8, 15, 150, 1, 15, 15, 15433 }, // U+0491 + { 13, 16, 192, -1, 16, 26, 15448 }, // U+0492 + { 10, 12, 150, -1, 12, 15, 15474 }, // U+0493 + { 12, 18, 220, 1, 16, 27, 15489 }, // U+0494 + { 11, 16, 189, 1, 12, 22, 15516 }, // U+0495 + { 23, 20, 363, 0, 16, 58, 15538 }, // U+0496 + { 19, 16, 296, 0, 12, 38, 15596 }, // U+0497 + { 13, 21, 213, 0, 16, 35, 15634 }, // U+0498 + { 11, 17, 171, 0, 12, 24, 15669 }, // U+0499 + { 15, 20, 252, 1, 16, 38, 15693 }, // U+049A + { 12, 16, 204, 1, 12, 24, 15731 }, // U+049B + { 14, 16, 237, 1, 16, 28, 15755 }, // U+049C + { 13, 12, 209, 1, 12, 20, 15783 }, // U+049D + { 18, 16, 293, 0, 16, 36, 15803 }, // U+04A0 + { 14, 12, 223, 0, 12, 21, 15839 }, // U+04A1 + { 15, 20, 256, 1, 16, 38, 15860 }, // U+04A2 + { 13, 16, 214, 1, 12, 26, 15898 }, // U+04A3 + { 20, 16, 333, 1, 16, 40, 15924 }, // U+04A4 + { 14, 12, 236, 1, 12, 21, 15964 }, // U+04A5 + { 22, 18, 373, 1, 16, 50, 15985 }, // U+04A6 + { 18, 16, 312, 1, 12, 36, 16035 }, // U+04A7 + { 22, 17, 347, 0, 16, 47, 16071 }, // U+04A8 + { 17, 13, 278, 0, 12, 28, 16118 }, // U+04A9 + { 15, 21, 246, 0, 16, 40, 16146 }, // U+04AA + { 12, 17, 194, 0, 12, 26, 16186 }, // U+04AB + { 14, 20, 220, 0, 16, 35, 16212 }, // U+04AC + { 11, 16, 170, 0, 12, 22, 16247 }, // U+04AD + { 15, 16, 240, 0, 16, 30, 16269 }, // U+04AE + { 12, 17, 196, 0, 12, 26, 16299 }, // U+04AF + { 15, 16, 240, 0, 16, 30, 16325 }, // U+04B0 + { 12, 17, 196, 0, 12, 26, 16355 }, // U+04B1 + { 16, 20, 255, 0, 16, 40, 16381 }, // U+04B2 + { 13, 16, 202, 0, 12, 26, 16421 }, // U+04B3 + { 20, 20, 316, 0, 16, 50, 16447 }, // U+04B4 + { 15, 16, 236, 0, 12, 30, 16497 }, // U+04B5 + { 14, 20, 243, 1, 16, 35, 16527 }, // U+04B6 + { 12, 16, 208, 1, 12, 24, 16562 }, // U+04B7 + { 13, 16, 234, 1, 16, 26, 16586 }, // U+04B8 + { 11, 12, 199, 1, 12, 17, 16612 }, // U+04B9 + { 13, 16, 234, 1, 16, 26, 16629 }, // U+04BA + { 11, 16, 205, 1, 16, 22, 16655 }, // U+04BB + { 18, 17, 296, 0, 16, 39, 16677 }, // U+04BC + { 15, 13, 249, 0, 12, 25, 16716 }, // U+04BD + { 18, 20, 296, 0, 16, 45, 16741 }, // U+04BE + { 15, 17, 249, 0, 12, 32, 16786 }, // U+04BF + { 4, 16, 92, 1, 16, 8, 16818 }, // U+04C0 + { 22, 20, 351, 0, 20, 55, 16826 }, // U+04C1 + { 18, 16, 286, 0, 16, 36, 16881 }, // U+04C2 + { 13, 20, 230, 1, 16, 33, 16917 }, // U+04C3 + { 11, 16, 205, 1, 12, 22, 16950 }, // U+04C4 + { 17, 20, 248, 0, 16, 43, 16972 }, // U+04C5 + { 14, 16, 200, 0, 12, 28, 17015 }, // U+04C6 + { 14, 21, 247, 1, 16, 37, 17043 }, // U+04C7 + { 11, 17, 205, 1, 12, 24, 17080 }, // U+04C8 + { 16, 20, 247, 1, 16, 40, 17104 }, // U+04C9 + { 13, 16, 205, 1, 12, 26, 17144 }, // U+04CA + { 14, 20, 243, 1, 16, 35, 17170 }, // U+04CB + { 12, 16, 208, 1, 12, 24, 17205 }, // U+04CC + { 19, 20, 308, 1, 16, 48, 17229 }, // U+04CD + { 17, 16, 268, 1, 12, 34, 17277 }, // U+04CE + { 4, 16, 89, 1, 16, 8, 17311 }, // U+04CF + { 15, 20, 246, 0, 20, 38, 17319 }, // U+04D0 + { 11, 17, 191, 0, 16, 24, 17357 }, // U+04D1 + { 15, 20, 246, 0, 20, 38, 17381 }, // U+04D2 + { 11, 17, 191, 0, 16, 24, 17419 }, // U+04D3 + { 21, 16, 340, 0, 16, 42, 17443 }, // U+04D4 + { 19, 13, 302, 0, 12, 31, 17485 }, // U+04D5 + { 11, 20, 202, 1, 20, 28, 17516 }, // U+04D6 + { 12, 17, 196, 0, 16, 26, 17544 }, // U+04D7 + { 15, 17, 248, 0, 16, 32, 17570 }, // U+04D8 + { 12, 13, 196, 0, 12, 20, 17602 }, // U+04D9 + { 15, 21, 248, 0, 20, 40, 17622 }, // U+04DA + { 12, 17, 196, 0, 16, 26, 17662 }, // U+04DB + { 22, 20, 351, 0, 20, 55, 17688 }, // U+04DC + { 18, 16, 286, 0, 16, 36, 17743 }, // U+04DD + { 13, 21, 213, 0, 20, 35, 17779 }, // U+04DE + { 11, 17, 171, 0, 16, 24, 17814 }, // U+04DF + { 13, 17, 213, 0, 16, 28, 17838 }, // U+04E0 + { 13, 17, 213, 0, 12, 28, 17866 }, // U+04E1 + { 14, 20, 251, 1, 20, 35, 17894 }, // U+04E2 + { 11, 16, 206, 1, 16, 22, 17929 }, // U+04E3 + { 14, 20, 251, 1, 20, 35, 17951 }, // U+04E4 + { 11, 16, 206, 1, 16, 22, 17986 }, // U+04E5 + { 16, 21, 255, 0, 20, 42, 18008 }, // U+04E6 + { 12, 17, 202, 0, 16, 26, 18050 }, // U+04E7 + { 16, 17, 255, 0, 16, 34, 18076 }, // U+04E8 + { 12, 13, 202, 0, 12, 20, 18110 }, // U+04E9 + { 16, 21, 255, 0, 20, 42, 18130 }, // U+04EA + { 12, 17, 202, 0, 16, 26, 18172 }, // U+04EB + { 15, 21, 246, 0, 20, 40, 18198 }, // U+04EC + { 12, 17, 194, 0, 16, 26, 18238 }, // U+04ED + { 14, 20, 228, 0, 20, 35, 18264 }, // U+04EE + { 13, 21, 197, 0, 16, 35, 18299 }, // U+04EF + { 14, 20, 228, 0, 20, 35, 18334 }, // U+04F0 + { 13, 21, 197, 0, 16, 35, 18369 }, // U+04F1 + { 14, 20, 228, 0, 20, 35, 18404 }, // U+04F2 + { 13, 21, 197, 0, 16, 35, 18439 }, // U+04F3 + { 13, 20, 234, 1, 20, 33, 18474 }, // U+04F4 + { 11, 16, 199, 1, 16, 22, 18507 }, // U+04F5 + { 11, 20, 192, 1, 16, 28, 18529 }, // U+04F6 + { 8, 16, 150, 1, 12, 16, 18557 }, // U+04F7 + { 17, 20, 303, 1, 20, 43, 18573 }, // U+04F8 + { 15, 16, 264, 1, 16, 30, 18616 }, // U+04F9 + { 13, 21, 205, 0, 16, 35, 18646 }, // U+04FA + { 10, 17, 150, -1, 12, 22, 18681 }, // U+04FB + { 16, 21, 261, 0, 16, 42, 18703 }, // U+04FC + { 11, 17, 179, 0, 12, 24, 18745 }, // U+04FD + { 15, 16, 243, 0, 16, 30, 18769 }, // U+04FE + { 12, 12, 190, 0, 12, 18, 18799 }, // U+04FF + { 13, 13, 227, 1, 13, 22, 18817 }, // U+05D0 + { 12, 13, 188, 0, 13, 20, 18839 }, // U+05D1 + { 9, 13, 151, 0, 13, 15, 18859 }, // U+05D2 + { 12, 13, 183, 0, 13, 20, 18874 }, // U+05D3 + { 12, 13, 219, 1, 13, 20, 18894 }, // U+05D4 + { 4, 13, 99, 1, 13, 7, 18914 }, // U+05D5 + { 9, 13, 135, 0, 13, 15, 18921 }, // U+05D6 + { 12, 13, 226, 1, 13, 20, 18936 }, // U+05D7 + { 12, 13, 220, 1, 13, 20, 18956 }, // U+05D8 + { 4, 8, 94, 1, 13, 4, 18976 }, // U+05D9 + { 10, 17, 176, 0, 13, 22, 18980 }, // U+05DA + { 11, 13, 181, 0, 13, 18, 19002 }, // U+05DB + { 11, 16, 172, 0, 16, 22, 19020 }, // U+05DC + { 12, 13, 226, 1, 13, 20, 19042 }, // U+05DD + { 14, 13, 230, 0, 13, 23, 19062 }, // U+05DE + { 4, 17, 99, 1, 13, 9, 19085 }, // U+05DF + { 7, 13, 133, 0, 13, 12, 19094 }, // U+05E0 + { 13, 13, 221, 0, 13, 22, 19106 }, // U+05E1 + { 12, 14, 205, 0, 13, 21, 19128 }, // U+05E2 + { 12, 17, 216, 1, 13, 26, 19149 }, // U+05E3 + { 12, 13, 218, 1, 13, 20, 19175 }, // U+05E4 + { 12, 17, 194, 0, 13, 26, 19195 }, // U+05E5 + { 12, 13, 205, 0, 13, 20, 19221 }, // U+05E6 + { 12, 17, 213, 1, 13, 26, 19241 }, // U+05E7 + { 10, 13, 179, 0, 13, 17, 19267 }, // U+05E8 + { 15, 13, 267, 1, 13, 25, 19284 }, // U+05E9 + { 12, 13, 222, 1, 13, 20, 19309 }, // U+05EA + { 4, 7, 108, 1, 6, 4, 19329 }, // U+060C + { 6, 16, 113, 0, 15, 12, 19333 }, // U+061B + { 11, 17, 171, 0, 16, 24, 19345 }, // U+061F + { 10, 12, 157, 0, 10, 15, 19369 }, // U+0621 + { 8, 4, 96, 0, 3, 4, 19384 }, // U+0640 + { 6, 6, 191, 3, 9, 5, 19388 }, // U+0660 + { 6, 16, 191, 3, 15, 12, 19393 }, // U+0661 + { 10, 16, 191, 1, 15, 20, 19405 }, // U+0662 + { 12, 16, 191, 0, 15, 24, 19425 }, // U+0663 + { 10, 16, 191, 1, 15, 20, 19449 }, // U+0664 + { 10, 12, 191, 1, 12, 15, 19469 }, // U+0665 + { 12, 16, 191, 0, 15, 24, 19484 }, // U+0666 + { 12, 16, 191, 0, 15, 24, 19508 }, // U+0667 + { 12, 16, 191, 0, 15, 24, 19532 }, // U+0668 + { 10, 15, 191, 1, 15, 19, 19556 }, // U+0669 + { 15, 13, 247, 0, 8, 25, 19575 }, // U+06BA + { 7, 2, 108, 0, 2, 2, 19600 }, // U+06D4 + { 6, 6, 191, 3, 9, 5, 19602 }, // U+06F0 + { 6, 16, 191, 3, 15, 12, 19607 }, // U+06F1 + { 10, 16, 191, 1, 15, 20, 19619 }, // U+06F2 + { 12, 16, 191, 0, 15, 24, 19639 }, // U+06F3 + { 12, 16, 191, 0, 15, 24, 19663 }, // U+06F4 + { 12, 15, 191, 0, 15, 23, 19687 }, // U+06F5 + { 10, 16, 191, 1, 15, 20, 19710 }, // U+06F6 + { 12, 16, 191, 0, 15, 24, 19730 }, // U+06F7 + { 12, 16, 191, 0, 15, 24, 19754 }, // U+06F8 + { 10, 15, 191, 1, 15, 19, 19778 }, // U+06F9 + { 15, 21, 246, 0, 16, 40, 19797 }, // U+1EA0 + { 11, 17, 191, 0, 12, 24, 19837 }, // U+1EA1 + { 15, 20, 246, 0, 20, 38, 19861 }, // U+1EA2 + { 11, 17, 191, 0, 16, 24, 19899 }, // U+1EA3 + { 15, 24, 246, 0, 24, 45, 19923 }, // U+1EA4 + { 12, 21, 191, 0, 20, 32, 19968 }, // U+1EA5 + { 15, 24, 246, 0, 24, 45, 20000 }, // U+1EA6 + { 11, 21, 191, 0, 20, 29, 20045 }, // U+1EA7 + { 15, 24, 246, 0, 24, 45, 20074 }, // U+1EA8 + { 11, 21, 191, 0, 20, 29, 20119 }, // U+1EA9 + { 15, 23, 246, 0, 23, 44, 20148 }, // U+1EAA + { 11, 20, 191, 0, 19, 28, 20192 }, // U+1EAB + { 15, 25, 246, 0, 20, 47, 20220 }, // U+1EAC + { 11, 21, 191, 0, 16, 29, 20267 }, // U+1EAD + { 15, 24, 246, 0, 24, 45, 20296 }, // U+1EAE + { 11, 21, 191, 0, 20, 29, 20341 }, // U+1EAF + { 15, 24, 246, 0, 24, 45, 20370 }, // U+1EB0 + { 11, 21, 191, 0, 20, 29, 20415 }, // U+1EB1 + { 15, 24, 246, 0, 24, 45, 20444 }, // U+1EB2 + { 11, 21, 191, 0, 20, 29, 20489 }, // U+1EB3 + { 15, 23, 246, 0, 23, 44, 20518 }, // U+1EB4 + { 11, 20, 191, 0, 19, 28, 20562 }, // U+1EB5 + { 15, 25, 246, 0, 20, 47, 20590 }, // U+1EB6 + { 11, 21, 191, 0, 16, 29, 20637 }, // U+1EB7 + { 11, 21, 202, 1, 16, 29, 20666 }, // U+1EB8 + { 12, 17, 196, 0, 12, 26, 20695 }, // U+1EB9 + { 11, 20, 202, 1, 20, 28, 20721 }, // U+1EBA + { 12, 17, 196, 0, 16, 26, 20749 }, // U+1EBB + { 11, 20, 202, 1, 20, 28, 20775 }, // U+1EBC + { 12, 17, 196, 0, 16, 26, 20803 }, // U+1EBD + { 11, 24, 202, 1, 24, 33, 20829 }, // U+1EBE + { 12, 21, 196, 0, 20, 32, 20862 }, // U+1EBF + { 11, 24, 202, 1, 24, 33, 20894 }, // U+1EC0 + { 12, 21, 196, 0, 20, 32, 20927 }, // U+1EC1 + { 11, 24, 202, 1, 24, 33, 20959 }, // U+1EC2 + { 12, 21, 196, 0, 20, 32, 20992 }, // U+1EC3 + { 11, 23, 202, 1, 23, 32, 21024 }, // U+1EC4 + { 12, 20, 196, 0, 19, 30, 21056 }, // U+1EC5 + { 11, 25, 202, 1, 20, 35, 21086 }, // U+1EC6 + { 12, 21, 196, 0, 16, 32, 21121 }, // U+1EC7 + { 5, 20, 92, 0, 20, 13, 21153 }, // U+1EC8 + { 5, 16, 89, 0, 16, 10, 21166 }, // U+1EC9 + { 4, 21, 92, 1, 16, 11, 21176 }, // U+1ECA + { 4, 21, 89, 1, 16, 11, 21187 }, // U+1ECB + { 16, 21, 255, 0, 16, 42, 21198 }, // U+1ECC + { 12, 17, 202, 0, 12, 26, 21240 }, // U+1ECD + { 16, 21, 255, 0, 20, 42, 21266 }, // U+1ECE + { 12, 17, 202, 0, 16, 26, 21308 }, // U+1ECF + { 16, 25, 255, 0, 24, 50, 21334 }, // U+1ED0 + { 12, 21, 202, 0, 20, 32, 21384 }, // U+1ED1 + { 16, 25, 255, 0, 24, 50, 21416 }, // U+1ED2 + { 12, 21, 202, 0, 20, 32, 21466 }, // U+1ED3 + { 16, 25, 255, 0, 24, 50, 21498 }, // U+1ED4 + { 12, 21, 202, 0, 20, 32, 21548 }, // U+1ED5 + { 16, 24, 255, 0, 23, 48, 21580 }, // U+1ED6 + { 12, 20, 202, 0, 19, 30, 21628 }, // U+1ED7 + { 16, 25, 255, 0, 20, 50, 21658 }, // U+1ED8 + { 12, 21, 202, 0, 16, 32, 21708 }, // U+1ED9 + { 16, 21, 257, 0, 20, 42, 21740 }, // U+1EDA + { 13, 17, 202, 0, 16, 28, 21782 }, // U+1EDB + { 16, 21, 257, 0, 20, 42, 21810 }, // U+1EDC + { 13, 17, 202, 0, 16, 28, 21852 }, // U+1EDD + { 16, 21, 257, 0, 20, 42, 21880 }, // U+1EDE + { 13, 17, 202, 0, 16, 28, 21922 }, // U+1EDF + { 16, 21, 257, 0, 20, 42, 21950 }, // U+1EE0 + { 13, 17, 202, 0, 16, 28, 21992 }, // U+1EE1 + { 16, 23, 257, 0, 18, 46, 22020 }, // U+1EE2 + { 13, 19, 202, 0, 14, 31, 22066 }, // U+1EE3 + { 13, 21, 242, 1, 16, 35, 22097 }, // U+1EE4 + { 11, 17, 205, 1, 12, 24, 22132 }, // U+1EE5 + { 13, 21, 242, 1, 20, 35, 22156 }, // U+1EE6 + { 11, 17, 205, 1, 16, 24, 22191 }, // U+1EE7 + { 15, 21, 241, 1, 20, 40, 22215 }, // U+1EE8 + { 13, 17, 219, 1, 16, 28, 22255 }, // U+1EE9 + { 15, 21, 241, 1, 20, 40, 22283 }, // U+1EEA + { 13, 17, 219, 1, 16, 28, 22323 }, // U+1EEB + { 15, 21, 241, 1, 20, 40, 22351 }, // U+1EEC + { 13, 17, 219, 1, 16, 28, 22391 }, // U+1EED + { 15, 21, 241, 1, 20, 40, 22419 }, // U+1EEE + { 13, 17, 219, 1, 16, 28, 22459 }, // U+1EEF + { 15, 23, 241, 1, 18, 44, 22487 }, // U+1EF0 + { 13, 19, 219, 1, 14, 31, 22531 }, // U+1EF1 + { 15, 20, 240, 0, 20, 38, 22562 }, // U+1EF2 + { 13, 21, 197, 0, 16, 35, 22600 }, // U+1EF3 + { 15, 21, 240, 0, 16, 40, 22635 }, // U+1EF4 + { 13, 17, 197, 0, 12, 28, 22675 }, // U+1EF5 + { 15, 20, 240, 0, 20, 38, 22703 }, // U+1EF6 + { 13, 21, 197, 0, 16, 35, 22741 }, // U+1EF7 + { 15, 20, 240, 0, 20, 38, 22776 }, // U+1EF8 + { 13, 21, 197, 0, 16, 35, 22814 }, // U+1EF9 + { 0, 0, 189, 0, 0, 0, 22849 }, // U+2000 + { 0, 0, 376, 0, 0, 0, 22849 }, // U+2001 + { 0, 0, 167, 0, 0, 0, 22849 }, // U+2002 + { 0, 0, 333, 0, 0, 0, 22849 }, // U+2003 + { 0, 0, 111, 0, 0, 0, 22849 }, // U+2004 + { 0, 0, 83, 0, 0, 0, 22849 }, // U+2005 + { 0, 0, 55, 0, 0, 0, 22849 }, // U+2006 + { 0, 0, 215, 0, 0, 0, 22849 }, // U+2007 + { 0, 0, 105, 0, 0, 0, 22849 }, // U+2008 + { 0, 0, 51, 0, 0, 0, 22849 }, // U+2009 + { 0, 0, 25, 0, 0, 0, 22849 }, // U+200A + { 0, 0, 0, 0, 0, 0, 22849 }, // U+200B + { 0, 0, 0, 0, 0, 0, 22849 }, // U+200C + { 0, 0, 0, 0, 0, 0, 22849 }, // U+200D + { 0, 0, 0, 0, 0, 0, 22849 }, // U+200E + { 0, 0, 0, 0, 0, 0, 22849 }, // U+200F + { 8, 4, 150, 1, 8, 4, 22849 }, // U+2010 + { 8, 4, 150, 1, 8, 4, 22853 }, // U+2011 + { 13, 3, 215, 0, 8, 5, 22857 }, // U+2012 + { 11, 4, 167, 0, 8, 6, 22862 }, // U+2013 + { 21, 4, 333, 0, 8, 11, 22868 }, // U+2014 + { 21, 4, 333, 0, 8, 11, 22879 }, // U+2015 + { 8, 26, 186, 2, 21, 26, 22890 }, // U+2016 + { 10, 5, 157, 0, 0, 7, 22916 }, // U+2017 + { 5, 8, 97, 1, 16, 5, 22923 }, // U+2018 + { 5, 8, 97, 1, 16, 5, 22928 }, // U+2019 + { 4, 8, 90, 1, 3, 4, 22933 }, // U+201A + { 5, 8, 97, 0, 16, 5, 22937 }, // U+201B + { 10, 8, 172, 1, 16, 10, 22942 }, // U+201C + { 9, 8, 169, 1, 16, 9, 22952 }, // U+201D + { 9, 8, 162, 1, 3, 9, 22961 }, // U+201E + { 10, 8, 169, 0, 16, 10, 22970 }, // U+201F + { 10, 13, 193, 1, 16, 17, 22980 }, // U+2020 + { 10, 16, 193, 1, 16, 20, 22997 }, // U+2021 + { 8, 8, 155, 1, 10, 8, 23017 }, // U+2022 + { 6, 8, 155, 2, 10, 6, 23025 }, // U+2023 + { 5, 5, 105, 1, 4, 4, 23031 }, // U+2024 + { 11, 5, 209, 1, 4, 7, 23035 }, // U+2025 + { 18, 5, 314, 1, 4, 12, 23042 }, // U+2026 + { 5, 5, 105, 1, 10, 4, 23054 }, // U+2027 + { 0, 0, 0, 0, 0, 0, 23058 }, // U+202A + { 0, 0, 0, 0, 0, 0, 23058 }, // U+202B + { 0, 0, 0, 0, 0, 0, 23058 }, // U+202C + { 0, 0, 0, 0, 0, 0, 23058 }, // U+202D + { 0, 0, 0, 0, 0, 0, 23058 }, // U+202E + { 0, 0, 51, 0, 0, 0, 23058 }, // U+202F + { 26, 17, 457, 1, 16, 56, 23058 }, // U+2030 + { 34, 17, 581, 1, 16, 73, 23114 }, // U+2031 + { 4, 7, 90, 1, 16, 4, 23187 }, // U+2032 + { 10, 7, 179, 1, 16, 9, 23191 }, // U+2033 + { 15, 7, 268, 1, 16, 14, 23200 }, // U+2034 + { 4, 7, 90, 1, 16, 4, 23214 }, // U+2035 + { 10, 7, 179, 1, 16, 9, 23218 }, // U+2036 + { 15, 7, 268, 1, 16, 14, 23227 }, // U+2037 + { 10, 5, 181, 1, -1, 7, 23241 }, // U+2038 + { 9, 10, 141, 0, 11, 12, 23248 }, // U+2039 + { 8, 10, 141, 0, 11, 10, 23260 }, // U+203A + { 15, 14, 268, 1, 14, 27, 23270 }, // U+203B + { 11, 17, 212, 1, 16, 24, 23297 }, // U+203C + { 11, 17, 185, 0, 16, 24, 23321 }, // U+203D + { 11, 3, 164, 0, 18, 5, 23345 }, // U+203E + { 18, 4, 281, 0, -1, 9, 23350 }, // U+203F + { 18, 4, 281, 0, 16, 9, 23359 }, // U+2040 + { 10, 12, 157, 0, 7, 15, 23368 }, // U+2041 + { 14, 13, 253, 1, 14, 23, 23383 }, // U+2042 + { 7, 4, 155, 1, 8, 4, 23406 }, // U+2043 + { 14, 16, 68, -5, 16, 28, 23410 }, // U+2044 + { 6, 20, 122, 1, 17, 15, 23438 }, // U+2045 + { 6, 20, 122, 0, 17, 15, 23453 }, // U+2046 + { 23, 17, 374, 0, 16, 49, 23468 }, // U+2047 + { 17, 17, 293, 0, 16, 37, 23517 }, // U+2048 + { 17, 17, 293, 1, 16, 37, 23554 }, // U+2049 + { 9, 17, 153, 0, 12, 20, 23591 }, // U+204A + { 11, 16, 198, 1, 16, 22, 23611 }, // U+204B + { 10, 10, 201, 1, 11, 13, 23633 }, // U+204C + { 11, 10, 201, 1, 11, 14, 23646 }, // U+204D + { 9, 10, 185, 1, 8, 12, 23660 }, // U+204E + { 5, 15, 107, 1, 11, 10, 23672 }, // U+204F + { 18, 21, 281, 0, 16, 48, 23682 }, // U+2050 + { 9, 20, 190, 2, 16, 23, 23730 }, // U+2051 + { 15, 16, 265, 1, 16, 30, 23753 }, // U+2052 + { 14, 6, 252, 1, 10, 11, 23783 }, // U+2053 + { 18, 4, 281, 0, -1, 9, 23794 }, // U+2054 + { 12, 11, 237, 1, 11, 17, 23803 }, // U+2055 + { 21, 7, 357, 1, 16, 19, 23820 }, // U+2057 + { 0, 0, 97, 0, 0, 0, 23839 }, // U+205F + { 0, 0, 0, 0, 0, 0, 23839 }, // U+2060 + { 0, 0, 0, 0, 0, 0, 23839 }, // U+2061 + { 0, 0, 0, 0, 0, 0, 23839 }, // U+2062 + { 0, 0, 0, 0, 0, 0, 23839 }, // U+2063 + { 0, 0, 0, 0, 0, 0, 23839 }, // U+2064 + { 0, 0, 0, 0, 0, 0, 23839 }, // U+2066 + { 0, 0, 0, 0, 0, 0, 23839 }, // U+2067 + { 0, 0, 0, 0, 0, 0, 23839 }, // U+2068 + { 0, 0, 0, 0, 0, 0, 23839 }, // U+2069 + { 0, 0, 0, 0, 0, 0, 23839 }, // U+206A + { 0, 0, 0, 0, 0, 0, 23839 }, // U+206B + { 0, 0, 0, 0, 0, 0, 23839 }, // U+206C + { 0, 0, 0, 0, 0, 0, 23839 }, // U+206D + { 0, 0, 0, 0, 0, 0, 23839 }, // U+206E + { 0, 0, 0, 0, 0, 0, 23839 }, // U+206F + { 10, 10, 160, 0, 18, 13, 23839 }, // U+2070 + { 3, 10, 72, 1, 19, 4, 23852 }, // U+2071 + { 10, 9, 157, 0, 18, 12, 23856 }, // U+2074 + { 8, 10, 152, 1, 18, 10, 23868 }, // U+2075 + { 9, 10, 153, 0, 18, 12, 23878 }, // U+2076 + { 8, 9, 137, 0, 18, 9, 23890 }, // U+2077 + { 9, 10, 155, 0, 18, 12, 23899 }, // U+2078 + { 9, 10, 153, 0, 18, 12, 23911 }, // U+2079 + { 8, 8, 154, 1, 17, 8, 23923 }, // U+207A + { 8, 2, 154, 1, 14, 2, 23931 }, // U+207B + { 8, 6, 154, 1, 16, 6, 23933 }, // U+207C + { 5, 12, 93, 1, 19, 8, 23939 }, // U+207D + { 5, 12, 93, 0, 19, 8, 23947 }, // U+207E + { 8, 7, 149, 1, 16, 7, 23955 }, // U+207F + { 10, 10, 160, 0, 6, 13, 23962 }, // U+2080 + { 6, 9, 106, 0, 6, 7, 23975 }, // U+2081 + { 9, 9, 149, 0, 6, 11, 23982 }, // U+2082 + { 9, 10, 152, 0, 6, 12, 23993 }, // U+2083 + { 10, 9, 157, 0, 6, 12, 24005 }, // U+2084 + { 8, 10, 152, 1, 6, 10, 24017 }, // U+2085 + { 9, 10, 153, 0, 6, 12, 24027 }, // U+2086 + { 8, 9, 137, 0, 6, 9, 24039 }, // U+2087 + { 9, 10, 155, 0, 6, 12, 24048 }, // U+2088 + { 9, 10, 153, 0, 6, 12, 24060 }, // U+2089 + { 8, 8, 154, 1, 5, 8, 24072 }, // U+208A + { 8, 2, 154, 1, 2, 2, 24080 }, // U+208B + { 8, 6, 154, 1, 4, 6, 24082 }, // U+208C + { 5, 12, 93, 1, 7, 8, 24088 }, // U+208D + { 5, 12, 93, 0, 7, 8, 24096 }, // U+208E + { 8, 8, 137, 0, 4, 8, 24104 }, // U+2090 + { 8, 8, 140, 0, 4, 8, 24112 }, // U+2091 + { 9, 8, 144, 0, 4, 9, 24120 }, // U+2092 + { 9, 7, 139, 0, 4, 8, 24129 }, // U+2093 + { 8, 9, 158, 1, 4, 9, 24137 }, // U+2094 + { 8, 9, 149, 1, 6, 9, 24146 }, // U+2095 + { 8, 9, 138, 1, 6, 9, 24155 }, // U+2096 + { 3, 9, 72, 1, 6, 4, 24164 }, // U+2097 + { 12, 7, 211, 1, 4, 11, 24168 }, // U+2098 + { 8, 7, 149, 1, 4, 7, 24179 }, // U+2099 + { 8, 10, 148, 1, 4, 10, 24186 }, // U+209A + { 8, 8, 134, 0, 4, 8, 24196 }, // U+209B + { 6, 9, 91, 0, 5, 7, 24204 }, // U+209C + { 15, 17, 252, 0, 16, 32, 24211 }, // U+20A0 + { 15, 20, 246, 0, 18, 38, 24243 }, // U+20A1 + { 15, 17, 240, 0, 16, 32, 24281 }, // U+20A2 + { 13, 16, 204, -1, 16, 26, 24313 }, // U+20A3 + { 13, 16, 211, 0, 16, 26, 24339 }, // U+20A4 + { 17, 16, 286, 1, 14, 34, 24365 }, // U+20A5 + { 16, 16, 251, 0, 16, 32, 24399 }, // U+20A6 + { 20, 17, 332, 1, 16, 43, 24431 }, // U+20A7 + { 23, 17, 388, 1, 16, 49, 24474 }, // U+20A8 + { 21, 16, 345, 0, 16, 42, 24523 }, // U+20A9 + { 17, 12, 305, 1, 12, 26, 24565 }, // U+20AA + { 14, 21, 208, 0, 16, 37, 24591 }, // U+20AB + { 14, 17, 227, 0, 16, 30, 24628 }, // U+20AC + { 17, 16, 265, 0, 16, 34, 24658 }, // U+20AD + { 14, 16, 220, 0, 16, 28, 24692 }, // U+20AE + { 30, 23, 483, 0, 18, 87, 24720 }, // U+20AF + { 15, 16, 236, 0, 16, 30, 24807 }, // U+20B1 + { 15, 21, 249, 0, 18, 40, 24837 }, // U+20B2 + { 15, 16, 246, 0, 16, 30, 24877 }, // U+20B3 + { 13, 17, 244, 1, 16, 28, 24907 }, // U+20B4 + { 15, 21, 246, 0, 18, 40, 24935 }, // U+20B5 + { 14, 16, 221, 0, 16, 28, 24975 }, // U+20B8 + { 12, 16, 189, 0, 16, 24, 25003 }, // U+20B9 + { 12, 17, 213, 1, 16, 26, 25027 }, // U+20BA + { 14, 12, 252, 1, 12, 21, 25053 }, // U+20BC + { 15, 16, 245, 0, 16, 30, 25074 }, // U+20BD + { 14, 16, 252, 1, 16, 28, 25104 }, // U+20BE + { 12, 20, 219, 1, 18, 30, 25132 }, // U+20BF + { 16, 15, 316, 2, 14, 30, 25162 }, // U+2190 + { 15, 16, 300, 2, 16, 30, 25192 }, // U+2191 + { 16, 15, 316, 2, 14, 30, 25222 }, // U+2192 + { 15, 17, 300, 2, 16, 32, 25252 }, // U+2193 + { 24, 15, 445, 2, 14, 45, 25284 }, // U+2194 + { 14, 25, 285, 2, 19, 44, 25329 }, // U+2195 + { 12, 13, 256, 2, 12, 20, 25373 }, // U+2196 + { 12, 13, 256, 2, 12, 20, 25393 }, // U+2197 + { 12, 12, 256, 2, 12, 18, 25413 }, // U+2198 + { 12, 12, 256, 2, 12, 18, 25431 }, // U+2199 + { 21, 17, 384, 2, 16, 45, 25449 }, // U+21A9 + { 22, 17, 384, 1, 16, 47, 25494 }, // U+21AA + { 17, 16, 335, 2, 16, 34, 25541 }, // U+21B0 + { 17, 16, 335, 2, 16, 34, 25575 }, // U+21B1 + { 13, 15, 300, 3, 15, 25, 25609 }, // U+21B2 + { 17, 15, 335, 2, 16, 32, 25634 }, // U+21B3 + { 15, 17, 297, 2, 16, 32, 25666 }, // U+21B4 + { 17, 15, 335, 2, 16, 32, 25698 }, // U+21B5 + { 19, 13, 300, 0, 14, 31, 25730 }, // U+21B6 + { 19, 13, 300, 0, 14, 31, 25761 }, // U+21B7 + { 16, 19, 316, 2, 19, 38, 25792 }, // U+21BA + { 16, 19, 316, 2, 19, 38, 25830 }, // U+21BB + { 15, 16, 300, 2, 16, 30, 25868 }, // U+21C4 + { 15, 16, 300, 2, 16, 30, 25898 }, // U+21C6 + { 16, 16, 316, 2, 15, 32, 25928 }, // U+21D0 + { 16, 16, 316, 2, 15, 32, 25960 }, // U+21D2 + { 24, 16, 445, 2, 15, 48, 25992 }, // U+21D4 + { 11, 16, 210, 1, 16, 22, 26040 }, // U+21DE + { 11, 17, 210, 1, 16, 24, 26062 }, // U+21DF + { 19, 11, 348, 1, 13, 27, 26086 }, // U+21E4 + { 19, 11, 348, 1, 13, 27, 26113 }, // U+21E5 + { 21, 17, 365, 1, 17, 45, 26140 }, // U+21E7 + { 21, 21, 365, 1, 17, 56, 26185 }, // U+21EA + { 12, 17, 221, 1, 16, 26, 26241 }, // U+2202 + { 14, 13, 228, 0, 12, 23, 26267 }, // U+2205 + { 15, 16, 244, 0, 16, 30, 26290 }, // U+2206 + { 12, 19, 254, 2, 16, 29, 26320 }, // U+220F + { 13, 19, 245, 1, 16, 31, 26349 }, // U+2211 + { 12, 4, 224, 1, 8, 6, 26380 }, // U+2212 + { 15, 15, 49, -6, 15, 29, 26386 }, // U+2215 + { 12, 16, 221, 1, 16, 24, 26415 }, // U+221A + { 20, 11, 325, 0, 12, 28, 26439 }, // U+221E + { 9, 22, 127, -1, 17, 25, 26467 }, // U+222B + { 5, 13, 105, 1, 14, 9, 26492 }, // U+2236 + { 12, 10, 224, 1, 11, 15, 26501 }, // U+2248 + { 12, 12, 224, 1, 12, 18, 26516 }, // U+2260 + { 12, 13, 224, 1, 13, 20, 26534 }, // U+2264 + { 12, 13, 224, 1, 13, 20, 26554 }, // U+2265 + { 29, 29, 463, 0, 22, 106, 26574 }, // U+2295 + { 29, 29, 463, 0, 22, 106, 26680 }, // U+2296 + { 29, 29, 463, 0, 22, 106, 26786 }, // U+2297 + { 29, 29, 463, 0, 22, 106, 26892 }, // U+2298 + { 13, 16, 216, 0, 16, 26, 26998 }, // U+FB01 + { 14, 16, 221, 0, 16, 28, 27024 }, // U+FB02 + { 21, 18, 341, 0, 10, 48, 27052 }, // U+FB56 + { 26, 17, 381, 0, 9, 56, 27100 }, // U+FB57 + { 7, 18, 120, 0, 10, 16, 27156 }, // U+FB58 + { 11, 16, 136, 0, 8, 22, 27172 }, // U+FB59 + { 21, 17, 341, 0, 16, 45, 27194 }, // U+FB66 + { 26, 17, 381, 0, 16, 56, 27239 }, // U+FB67 + { 6, 19, 106, 0, 18, 15, 27295 }, // U+FB68 + { 11, 17, 136, 0, 16, 24, 27310 }, // U+FB69 + { 15, 20, 229, 0, 11, 38, 27334 }, // U+FB7A + { 18, 20, 253, 0, 11, 45, 27372 }, // U+FB7B + { 13, 18, 210, 0, 10, 30, 27417 }, // U+FB7C + { 16, 18, 218, 0, 10, 36, 27447 }, // U+FB7D + { 10, 20, 172, 0, 19, 25, 27483 }, // U+FB88 + { 14, 20, 190, 0, 19, 35, 27508 }, // U+FB89 + { 9, 21, 136, -1, 16, 24, 27543 }, // U+FB8A + { 13, 21, 150, -1, 16, 35, 27567 }, // U+FB8B + { 9, 22, 136, -1, 17, 25, 27602 }, // U+FB8C + { 13, 22, 150, -1, 17, 36, 27627 }, // U+FB8D + { 20, 16, 303, 0, 15, 40, 27663 }, // U+FB8E + { 22, 16, 317, 0, 15, 44, 27703 }, // U+FB8F + { 10, 16, 151, 0, 15, 20, 27747 }, // U+FB90 + { 13, 16, 169, 0, 15, 26, 27767 }, // U+FB91 + { 20, 20, 303, 0, 19, 50, 27793 }, // U+FB92 + { 22, 20, 317, 0, 19, 55, 27843 }, // U+FB93 + { 11, 20, 151, -1, 19, 28, 27898 }, // U+FB94 + { 14, 20, 169, -1, 19, 35, 27926 }, // U+FB95 + { 15, 13, 247, 0, 8, 25, 27961 }, // U+FB9E + { 19, 13, 268, 0, 8, 31, 27986 }, // U+FB9F + { 10, 13, 157, 0, 12, 17, 28017 }, // U+FBA6 + { 14, 10, 195, 0, 9, 18, 28034 }, // U+FBA7 + { 6, 14, 106, 0, 10, 11, 28052 }, // U+FBA8 + { 13, 11, 172, 0, 5, 18, 28063 }, // U+FBA9 + { 16, 13, 257, 0, 12, 26, 28081 }, // U+FBAA + { 16, 15, 228, 0, 10, 30, 28107 }, // U+FBAB + { 13, 13, 214, 0, 12, 22, 28137 }, // U+FBAC + { 14, 15, 190, 0, 10, 27, 28159 }, // U+FBAD + { 20, 12, 314, 0, 11, 30, 28186 }, // U+FBAE + { 14, 10, 187, 0, 3, 18, 28216 }, // U+FBAF + { 20, 18, 314, 0, 17, 45, 28234 }, // U+FBB0 + { 14, 16, 187, 0, 9, 28, 28279 }, // U+FBB1 + { 17, 15, 274, 0, 10, 32, 28307 }, // U+FBFC + { 19, 11, 267, 0, 6, 27, 28339 }, // U+FBFD + { 7, 15, 120, 0, 10, 14, 28366 }, // U+FBFE + { 11, 13, 136, 0, 8, 18, 28380 }, // U+FBFF + { 10, 12, 157, 0, 10, 15, 28398 }, // U+FE80 + { 9, 19, 90, -2, 19, 22, 28413 }, // U+FE81 + { 10, 21, 106, -1, 20, 27, 28435 }, // U+FE82 + { 5, 21, 90, 0, 21, 14, 28462 }, // U+FE83 + { 9, 23, 106, 0, 22, 26, 28476 }, // U+FE84 + { 10, 20, 166, 0, 15, 25, 28502 }, // U+FE85 + { 13, 21, 175, 0, 16, 35, 28527 }, // U+FE86 + { 5, 23, 91, 1, 16, 15, 28562 }, // U+FE87 + { 8, 23, 106, 1, 16, 23, 28577 }, // U+FE88 + { 17, 17, 274, 0, 12, 37, 28600 }, // U+FE89 + { 19, 17, 267, 0, 12, 41, 28637 }, // U+FE8A + { 6, 18, 106, 0, 17, 14, 28678 }, // U+FE8B + { 10, 16, 128, 0, 15, 20, 28692 }, // U+FE8C + { 4, 16, 91, 1, 16, 8, 28712 }, // U+FE8D + { 8, 17, 106, 1, 16, 17, 28720 }, // U+FE8E + { 21, 15, 341, 0, 10, 40, 28737 }, // U+FE8F + { 26, 14, 381, 0, 9, 46, 28777 }, // U+FE90 + { 6, 15, 106, 0, 10, 12, 28823 }, // U+FE91 + { 10, 13, 128, 0, 8, 17, 28835 }, // U+FE92 + { 10, 18, 157, 0, 17, 23, 28852 }, // U+FE93 + { 14, 19, 185, 0, 18, 34, 28875 }, // U+FE94 + { 21, 14, 341, 0, 13, 37, 28909 }, // U+FE95 + { 26, 14, 381, 0, 13, 46, 28946 }, // U+FE96 + { 8, 16, 106, -1, 15, 16, 28992 }, // U+FE97 + { 11, 14, 136, 0, 13, 20, 29008 }, // U+FE98 + { 21, 16, 341, 0, 15, 42, 29028 }, // U+FE99 + { 26, 16, 381, 0, 15, 52, 29070 }, // U+FE9A + { 6, 18, 106, 0, 17, 14, 29122 }, // U+FE9B + { 11, 16, 136, 0, 15, 22, 29136 }, // U+FE9C + { 14, 20, 229, 0, 11, 35, 29158 }, // U+FE9D + { 18, 20, 246, 0, 11, 45, 29193 }, // U+FE9E + { 13, 15, 210, 0, 10, 25, 29238 }, // U+FE9F + { 16, 15, 218, 0, 10, 30, 29263 }, // U+FEA0 + { 14, 20, 229, 0, 11, 35, 29293 }, // U+FEA1 + { 18, 20, 246, 0, 11, 45, 29328 }, // U+FEA2 + { 13, 11, 210, 0, 10, 18, 29373 }, // U+FEA3 + { 16, 11, 218, 0, 10, 22, 29391 }, // U+FEA4 + { 14, 25, 229, 0, 16, 44, 29413 }, // U+FEA5 + { 18, 25, 246, 0, 16, 57, 29457 }, // U+FEA6 + { 13, 16, 210, 0, 15, 26, 29514 }, // U+FEA7 + { 16, 16, 218, 0, 15, 32, 29540 }, // U+FEA8 + { 10, 12, 172, 0, 11, 15, 29572 }, // U+FEA9 + { 14, 12, 190, 0, 11, 21, 29587 }, // U+FEAA + { 10, 17, 172, 0, 16, 22, 29608 }, // U+FEAB + { 14, 17, 190, 0, 16, 30, 29630 }, // U+FEAC + { 9, 13, 136, -1, 8, 15, 29660 }, // U+FEAD + { 13, 13, 150, -1, 8, 22, 29675 }, // U+FEAE + { 9, 19, 136, -1, 14, 22, 29697 }, // U+FEAF + { 13, 19, 150, -1, 14, 31, 29719 }, // U+FEB0 + { 28, 15, 457, 0, 10, 53, 29750 }, // U+FEB1 + { 32, 14, 480, 0, 9, 56, 29803 }, // U+FEB2 + { 19, 11, 317, 0, 10, 27, 29859 }, // U+FEB3 + { 23, 10, 340, 0, 9, 29, 29886 }, // U+FEB4 + { 28, 21, 457, 0, 16, 74, 29915 }, // U+FEB5 + { 32, 21, 480, 0, 16, 84, 29989 }, // U+FEB6 + { 19, 17, 317, 0, 16, 41, 30073 }, // U+FEB7 + { 23, 17, 340, 0, 16, 49, 30114 }, // U+FEB8 + { 29, 15, 474, 0, 10, 55, 30163 }, // U+FEB9 + { 32, 15, 481, 0, 10, 60, 30218 }, // U+FEBA + { 20, 11, 325, 0, 10, 28, 30278 }, // U+FEBB + { 23, 11, 335, 0, 10, 32, 30306 }, // U+FEBC + { 29, 20, 474, 0, 15, 73, 30338 }, // U+FEBD + { 32, 20, 481, 0, 15, 80, 30411 }, // U+FEBE + { 20, 16, 325, 0, 15, 40, 30491 }, // U+FEBF + { 23, 16, 335, 0, 15, 46, 30531 }, // U+FEC0 + { 17, 17, 281, 0, 16, 37, 30577 }, // U+FEC1 + { 20, 17, 287, 0, 16, 43, 30614 }, // U+FEC2 + { 15, 17, 246, 0, 16, 32, 30657 }, // U+FEC3 + { 18, 17, 254, 0, 16, 39, 30689 }, // U+FEC4 + { 17, 17, 281, 0, 16, 37, 30728 }, // U+FEC5 + { 20, 17, 287, 0, 16, 43, 30765 }, // U+FEC6 + { 15, 17, 246, 0, 16, 32, 30808 }, // U+FEC7 + { 18, 17, 254, 0, 16, 39, 30840 }, // U+FEC8 + { 14, 21, 190, 0, 12, 37, 30879 }, // U+FEC9 + { 14, 19, 190, 0, 10, 34, 30916 }, // U+FECA + { 11, 12, 186, 0, 11, 17, 30950 }, // U+FECB + { 15, 11, 184, -1, 10, 21, 30967 }, // U+FECC + { 14, 26, 190, 0, 17, 46, 30988 }, // U+FECD + { 14, 24, 190, 0, 15, 42, 31034 }, // U+FECE + { 11, 17, 186, 0, 16, 24, 31076 }, // U+FECF + { 15, 16, 184, -1, 15, 30, 31100 }, // U+FED0 + { 21, 19, 341, 0, 18, 50, 31130 }, // U+FED1 + { 25, 17, 372, 0, 16, 54, 31180 }, // U+FED2 + { 10, 19, 168, 0, 18, 24, 31234 }, // U+FED3 + { 13, 17, 176, 0, 16, 28, 31258 }, // U+FED4 + { 17, 19, 277, 0, 15, 41, 31286 }, // U+FED5 + { 20, 20, 285, 0, 14, 50, 31327 }, // U+FED6 + { 10, 19, 168, 0, 18, 24, 31377 }, // U+FED7 + { 13, 17, 176, 0, 16, 28, 31401 }, // U+FED8 + { 19, 17, 310, 0, 16, 41, 31429 }, // U+FED9 + { 22, 17, 325, 0, 16, 47, 31470 }, // U+FEDA + { 10, 16, 151, 0, 15, 20, 31517 }, // U+FEDB + { 13, 16, 169, 0, 15, 26, 31537 }, // U+FEDC + { 15, 21, 247, 0, 16, 40, 31563 }, // U+FEDD + { 19, 21, 264, 0, 16, 50, 31603 }, // U+FEDE + { 6, 17, 99, 0, 16, 13, 31653 }, // U+FEDF + { 9, 17, 115, 0, 16, 20, 31666 }, // U+FEE0 + { 11, 19, 183, 0, 10, 27, 31686 }, // U+FEE1 + { 16, 18, 220, 0, 9, 36, 31713 }, // U+FEE2 + { 12, 10, 194, 0, 9, 15, 31749 }, // U+FEE3 + { 15, 10, 208, 0, 9, 19, 31764 }, // U+FEE4 + { 15, 17, 247, 0, 12, 32, 31783 }, // U+FEE5 + { 19, 17, 268, 0, 12, 41, 31815 }, // U+FEE6 + { 6, 16, 106, 0, 15, 12, 31856 }, // U+FEE7 + { 10, 14, 128, 0, 13, 18, 31868 }, // U+FEE8 + { 10, 13, 157, 0, 12, 17, 31886 }, // U+FEE9 + { 14, 14, 185, 0, 13, 25, 31903 }, // U+FEEA + { 13, 13, 214, 0, 12, 22, 31928 }, // U+FEEB + { 14, 15, 190, 0, 10, 27, 31950 }, // U+FEEC + { 10, 14, 166, 0, 9, 18, 31977 }, // U+FEED + { 13, 14, 175, 0, 9, 23, 31995 }, // U+FEEE + { 17, 15, 274, 0, 10, 32, 32018 }, // U+FEEF + { 19, 11, 267, 0, 6, 27, 32050 }, // U+FEF0 + { 17, 20, 274, 0, 10, 43, 32077 }, // U+FEF1 + { 19, 16, 267, 0, 6, 38, 32120 }, // U+FEF2 + { 7, 15, 120, 0, 10, 14, 32158 }, // U+FEF3 + { 11, 13, 136, 0, 8, 18, 32172 }, // U+FEF4 + { 15, 17, 211, -2, 17, 32, 32190 }, // U+FEF5 + { 16, 19, 227, 0, 18, 38, 32222 }, // U+FEF6 + { 13, 19, 211, 0, 19, 31, 32260 }, // U+FEF7 + { 16, 21, 227, 0, 20, 42, 32291 }, // U+FEF8 + { 13, 23, 211, 0, 16, 38, 32333 }, // U+FEF9 + { 16, 23, 227, 0, 16, 46, 32371 }, // U+FEFA + { 13, 16, 211, 0, 16, 26, 32417 }, // U+FEFB + { 16, 17, 227, 0, 16, 34, 32443 }, // U+FEFC }; static const EpdUnicodeInterval inter_10_boldIntervals[] = { - { 0x0, 0x7F, 0x0 }, - { 0x80, 0xFF, 0x80 }, - { 0x100, 0x148, 0x100 }, + { 0x0, 0x148, 0x0 }, { 0x14A, 0x17F, 0x149 }, { 0x1A0, 0x1A1, 0x17F }, { 0x1AF, 0x1B0, 0x181 }, @@ -2822,59 +3385,75 @@ static const EpdUnicodeInterval inter_10_boldIntervals[] = { { 0x300, 0x304, 0x1DE }, { 0x306, 0x30C, 0x1E3 }, { 0x30F, 0x30F, 0x1EA }, - { 0x313, 0x313, 0x1EB }, - { 0x315, 0x315, 0x1EC }, - { 0x31B, 0x31B, 0x1ED }, - { 0x323, 0x323, 0x1EE }, - { 0x326, 0x328, 0x1EF }, - { 0x32C, 0x32C, 0x1F2 }, - { 0x337, 0x338, 0x1F3 }, - { 0x342, 0x343, 0x1F5 }, - { 0x346, 0x36F, 0x1F7 }, - { 0x400, 0x479, 0x221 }, - { 0x480, 0x49D, 0x29B }, - { 0x4A0, 0x4FF, 0x2B9 }, - { 0x5D0, 0x5EA, 0x319 }, - { 0x1EA0, 0x1EF9, 0x334 }, - { 0x2000, 0x2027, 0x38E }, - { 0x202A, 0x2055, 0x3B6 }, - { 0x2057, 0x2057, 0x3E2 }, - { 0x205F, 0x2064, 0x3E3 }, - { 0x2066, 0x206F, 0x3E9 }, - { 0x2070, 0x2071, 0x3F3 }, - { 0x2074, 0x208E, 0x3F5 }, - { 0x2090, 0x209C, 0x410 }, - { 0x20A0, 0x20AF, 0x41D }, - { 0x20B1, 0x20B5, 0x42D }, - { 0x20B8, 0x20BA, 0x432 }, - { 0x20BC, 0x20BF, 0x435 }, - { 0x2190, 0x2199, 0x439 }, - { 0x21A9, 0x21AA, 0x443 }, - { 0x21B0, 0x21B7, 0x445 }, - { 0x21BA, 0x21BB, 0x44D }, - { 0x21C4, 0x21C4, 0x44F }, - { 0x21C6, 0x21C6, 0x450 }, - { 0x21D0, 0x21D0, 0x451 }, - { 0x21D2, 0x21D2, 0x452 }, - { 0x21D4, 0x21D4, 0x453 }, - { 0x21DE, 0x21DF, 0x454 }, - { 0x21E4, 0x21E5, 0x456 }, - { 0x21E7, 0x21E7, 0x458 }, - { 0x21EA, 0x21EA, 0x459 }, - { 0x2202, 0x2202, 0x45A }, - { 0x2205, 0x2206, 0x45B }, - { 0x220F, 0x220F, 0x45D }, - { 0x2211, 0x2212, 0x45E }, - { 0x2215, 0x2215, 0x460 }, - { 0x221A, 0x221A, 0x461 }, - { 0x221E, 0x221E, 0x462 }, - { 0x222B, 0x222B, 0x463 }, - { 0x2236, 0x2236, 0x464 }, - { 0x2248, 0x2248, 0x465 }, - { 0x2260, 0x2260, 0x466 }, - { 0x2264, 0x2265, 0x467 }, - { 0x2295, 0x2298, 0x469 }, - { 0xFB01, 0xFB02, 0x46D }, + { 0x312, 0x313, 0x1EB }, + { 0x315, 0x315, 0x1ED }, + { 0x31B, 0x31B, 0x1EE }, + { 0x323, 0x323, 0x1EF }, + { 0x326, 0x328, 0x1F0 }, + { 0x32C, 0x32C, 0x1F3 }, + { 0x337, 0x338, 0x1F4 }, + { 0x342, 0x343, 0x1F6 }, + { 0x346, 0x36F, 0x1F8 }, + { 0x400, 0x479, 0x222 }, + { 0x480, 0x49D, 0x29C }, + { 0x4A0, 0x4FF, 0x2BA }, + { 0x5D0, 0x5EA, 0x31A }, + { 0x60C, 0x60C, 0x335 }, + { 0x61B, 0x61B, 0x336 }, + { 0x61F, 0x61F, 0x337 }, + { 0x621, 0x621, 0x338 }, + { 0x640, 0x640, 0x339 }, + { 0x660, 0x669, 0x33A }, + { 0x6BA, 0x6BA, 0x344 }, + { 0x6D4, 0x6D4, 0x345 }, + { 0x6F0, 0x6F9, 0x346 }, + { 0x1EA0, 0x1EF9, 0x350 }, + { 0x2000, 0x2027, 0x3AA }, + { 0x202A, 0x2055, 0x3D2 }, + { 0x2057, 0x2057, 0x3FE }, + { 0x205F, 0x2064, 0x3FF }, + { 0x2066, 0x2071, 0x405 }, + { 0x2074, 0x208E, 0x411 }, + { 0x2090, 0x209C, 0x42C }, + { 0x20A0, 0x20AF, 0x439 }, + { 0x20B1, 0x20B5, 0x449 }, + { 0x20B8, 0x20BA, 0x44E }, + { 0x20BC, 0x20BF, 0x451 }, + { 0x2190, 0x2199, 0x455 }, + { 0x21A9, 0x21AA, 0x45F }, + { 0x21B0, 0x21B7, 0x461 }, + { 0x21BA, 0x21BB, 0x469 }, + { 0x21C4, 0x21C4, 0x46B }, + { 0x21C6, 0x21C6, 0x46C }, + { 0x21D0, 0x21D0, 0x46D }, + { 0x21D2, 0x21D2, 0x46E }, + { 0x21D4, 0x21D4, 0x46F }, + { 0x21DE, 0x21DF, 0x470 }, + { 0x21E4, 0x21E5, 0x472 }, + { 0x21E7, 0x21E7, 0x474 }, + { 0x21EA, 0x21EA, 0x475 }, + { 0x2202, 0x2202, 0x476 }, + { 0x2205, 0x2206, 0x477 }, + { 0x220F, 0x220F, 0x479 }, + { 0x2211, 0x2212, 0x47A }, + { 0x2215, 0x2215, 0x47C }, + { 0x221A, 0x221A, 0x47D }, + { 0x221E, 0x221E, 0x47E }, + { 0x222B, 0x222B, 0x47F }, + { 0x2236, 0x2236, 0x480 }, + { 0x2248, 0x2248, 0x481 }, + { 0x2260, 0x2260, 0x482 }, + { 0x2264, 0x2265, 0x483 }, + { 0x2295, 0x2298, 0x485 }, + { 0xFB01, 0xFB02, 0x489 }, + { 0xFB56, 0xFB59, 0x48B }, + { 0xFB66, 0xFB69, 0x48F }, + { 0xFB7A, 0xFB7D, 0x493 }, + { 0xFB88, 0xFB95, 0x497 }, + { 0xFB9E, 0xFB9F, 0x4A5 }, + { 0xFBA6, 0xFBB1, 0x4A7 }, + { 0xFBFC, 0xFBFF, 0x4B3 }, + { 0xFE80, 0xFEFC, 0x4B7 }, }; static const EpdKernClassEntry inter_10_boldKernLeftClasses[] = { @@ -3655,6 +4234,10 @@ static const EpdKernClassEntry inter_10_boldKernLeftClasses[] = { { 0x2264, 20 }, // U+2264 { 0x2265, 20 }, // U+2265 { 0xFB02, 153 }, // U+FB02 + { 0xFB8C, 154 }, // U+FB8C + { 0xFEAD, 154 }, // U+FEAD + { 0xFEAE, 154 }, // U+FEAE + { 0xFEAF, 154 }, // U+FEAF }; static const EpdKernClassEntry inter_10_boldKernRightClasses[] = { @@ -4434,169 +5017,191 @@ static const EpdKernClassEntry inter_10_boldKernRightClasses[] = { { 0x2260, 27 }, // U+2260 { 0x2264, 27 }, // U+2264 { 0x2265, 27 }, // U+2265 + { 0xFB8E, 146 }, // U+FB8E + { 0xFBA6, 146 }, // U+FBA6 + { 0xFBAA, 146 }, // U+FBAA + { 0xFE8D, 147 }, // U+FE8D + { 0xFEA3, 146 }, // U+FEA3 + { 0xFEA9, 148 }, // U+FEA9 + { 0xFEB1, 148 }, // U+FEB1 + { 0xFEB3, 148 }, // U+FEB3 + { 0xFEB9, 148 }, // U+FEB9 + { 0xFEBB, 148 }, // U+FEBB + { 0xFEC1, 148 }, // U+FEC1 + { 0xFEC3, 148 }, // U+FEC3 + { 0xFEC9, 149 }, // U+FEC9 + { 0xFECB, 147 }, // U+FECB + { 0xFECD, 149 }, // U+FECD + { 0xFEDB, 146 }, // U+FEDB + { 0xFEDF, 147 }, // U+FEDF + { 0xFEE1, 146 }, // U+FEE1 + { 0xFEE3, 146 }, // U+FEE3 + { 0xFEE9, 146 }, // U+FEE9 + { 0xFEEB, 146 }, // U+FEEB }; static const int8_t inter_10_boldKernMatrix[] = { - 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -30, 0, -30, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, -55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, -3, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 15, 15, 0, 0, -3, -3, -3, 0, 0, 0, 0, 15, 0, 0, -5, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -23, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, -21, -13, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, 0, 0, -13, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, -2, 0, 0, 0, 0, 5, 0, 0, 4, -2, 0, 0, 0, 0, 5, 0, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -45, 0, -45, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, -5, -32, 0, -5, -32, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -2, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -26, 0, -15, -9, -14, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -45, -45, -26, 0, 0, 0, 0, 0, 0, 0, -36, -10, 0, 0, -50, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -10, -4, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -31, 0, -7, 0, -2, -9, -8, -4, 0, 0, 0, -34, -15, 0, 0, -15, 0, -11, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, -7, -15, 0, -11, 0, 0, 0, -14, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, -32, 0, -45, -45, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -21, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -10, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -10, 0, -13, -11, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 6, 0, 6, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -15, 0, -17, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 3, 0, 12, 0, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 13, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -11, 0, -11, 0, 0, -14, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -13, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -7, 0, -7, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, -19, 0, 1, -15, 0, 0, 1, 0, -42, 0, -42, 0, -5, 0, 0, -6, -19, -3, 7, -5, -3, -11, -30, 0, 0, 0, -36, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 1, -53, -15, -17, 0, 0, 0, -17, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -15, -17, 0, 0, 0, 30, -17, 0, 0, -15, 0, 0, 30, 30, 0, -17, 0, 0, 0, 0, 0, 0, 0, 30, -36, -36, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -15, 0, -23, -21, -17, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -15, -15, -15, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -26, 0, -25, -25, -19, -30, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, -11, -11, -17, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -11, 0, -14, -9, -18, -15, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 8, 0, 0, 0, -35, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -11, 0, -13, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, -13, -34, 0, 0, 0, 0, - 0, 0, -26, 0, -5, 0, 0, 0, 0, -32, -11, 0, -11, 0, 4, -2, -20, 0, 0, 0, 0, 0, 0, 0, 0, -13, -17, -21, -14, 5, 0, -14, 0, -30, -8, -28, -25, 0, -28, 0, -32, 0, 0, -8, -4, 1, 0, 0, -4, -10, 0, -23, -23, 0, 0, -11, 0, -25, -11, -11, 0, -24, -32, 0, 0, -18, -11, 0, -11, 0, 0, 0, -11, 0, 0, -8, 1, 1, 1, 0, -8, 0, -23, 0, 0, 0, 0, 0, 0, -8, -30, -30, -49, 0, 0, 1, 0, 0, 0, 0, 0, 0, -14, 0, -30, 0, 0, -10, -22, -10, -4, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, -43, -34, 0, 0, 0, -11, 0, -34, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -17, -17, -17, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 2, 0, 0, 0, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 15, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -24, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, -11, -6, -8, 0, 0, 0, -6, 0, 0, -11, -11, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -6, -6, -8, 0, 0, 0, 0, -8, -11, -11, -6, 0, 0, 0, 0, 0, -8, 0, 0, 0, -11, 0, 0, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, -10, 0, -7, 0, -5, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -10, -10, -10, 0, 0, 0, 12, 11, 0, 0, -5, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, -26, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, -14, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 2, 0, 0, 0, 0, -11, -13, -21, 0, 0, -26, 0, 0, -17, -43, 0, -5, 0, 0, -11, 0, -41, 0, 0, 0, 0, 0, -11, 0, 0, -13, 2, 2, 2, 28, -13, -11, -13, 0, 0, 0, 28, 28, 0, -13, 0, 0, 0, -11, 0, 2, 28, 28, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, - 0, 0, -30, 0, 0, 0, 0, 0, 0, -57, -22, 0, -39, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -9, 0, 0, -9, 0, -30, 0, -26, -12, 0, -32, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -35, 0, -32, -21, -66, 0, -27, -59, 0, 0, -26, -43, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -30, 0, 0, -7, -42, -7, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, -28, -29, 0, -15, -15, -11, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -7, -13, -7, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -5, -5, -4, 0, 0, 0, 0, -4, 0, 0, -5, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -7, 0, 0, -10, 0, 0, 0, -2, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -5, 0, 0, 0, 15, -5, 0, 0, -2, 0, 0, 15, 15, 0, -5, -2, -2, -2, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -2, 0, -11, 0, 0, 0, -19, -11, -19, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -30, 0, -11, -30, 0, 0, 0, 0, 0, 2, 0, 0, -19, -27, -29, 0, 0, 0, -20, -26, -31, -27, -22, -22, -12, -17, -19, 0, 0, -43, -19, 0, 0, 0, 0, -61, 0, -46, 0, -17, 0, 0, 0, -11, -40, -40, -46, 0, 0, 0, 30, -46, -48, -34, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -40, 0, 0, 47, 30, -45, -45, 0, 0, -11, 0, 0, 0, 0, -31, -19, -31, -26, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -19, -32, -19, -32, -18, -10, 0, 0, 0, -11, 0, 0, 0, 0, -21, -25, -21, 0, -25, -28, 0, -14, -32, 0, 0, 5, 0, 0, 0, 0, 0, -26, -21, -22, 0, 0, 0, -5, -15, 0, 0, 0, 0, 0, 0, -19, 0, 0, -19, -40, 0, 0, 0, 0, 0, 0, -32, 0, 0, -10, 0, 0, -13, -21, -34, -22, 0, 0, 0, 28, -37, 0, 0, -21, 0, 0, 28, 28, 0, -22, 0, 0, 0, 0, 0, 0, 13, 28, -40, -40, 0, 0, -14, 0, 0, 0, 0, 0, -6, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, -13, -32, 0, 0, 0, 0, 0, 0, 0, -47, -51, 0, 0, 0, 0, - 0, 0, 0, 0, -2, 0, -17, 0, 0, 0, -14, -32, -14, -32, -10, 0, 0, 0, 0, -11, 0, 0, 0, 0, -17, -25, -21, 0, -9, -25, 0, -9, -21, 0, 0, 2, 7, 0, 0, 0, 0, 0, -19, -17, 0, 0, 0, -11, -11, 0, 0, 0, 0, 0, 0, -14, 0, 0, -19, -14, 0, 0, 0, 0, 0, 0, -25, 0, 0, -10, 0, 0, 0, -19, -19, -17, 0, 0, 0, 0, -17, 0, 0, -19, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, -34, -37, 0, 0, 0, 0, - 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, -14, 0, -14, 0, 0, -13, 0, 0, -7, -13, 0, 0, -7, 0, 0, -19, -17, 0, -18, 0, 0, -18, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, -19, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -13, -31, 0, -2, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, -15, -19, -19, -9, 0, 0, 0, 0, -9, 0, 0, -19, 0, 0, 0, 0, 0, -9, -10, -10, -10, 0, 0, 0, 28, 0, 0, 0, 0, -7, -35, -11, -10, 0, 0, -21, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, - 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, -21, -21, -21, -21, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -15, -28, 0, -15, -11, 2, 0, 0, 0, 0, 0, 0, 0, 0, -35, -37, 0, -7, 0, -16, -23, 0, -13, 0, 0, 0, 0, -21, 0, 0, -30, -38, 0, -3, 0, 0, -13, 0, -38, 0, -21, 0, 0, 0, -28, -57, -52, -37, -7, -19, -7, 0, -37, -13, 0, -66, 0, 0, 0, 0, 0, -56, 2, 2, 2, -13, 0, -7, 28, 0, 0, 0, 0, 0, -15, 0, 2, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -54, 0, 0, 0, 0, 0, 0, 0, -58, -63, 0, 0, 0, -11, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, -11, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, -11, -12, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 28, 28, 0, 0, 0, 0, 0, 0, 0, 0, 11, 28, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, -2, 0, 0, 0, 0, 5, 0, 0, 4, -2, 0, 0, 0, 0, 5, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -26, 0, -5, 0, 0, 0, 0, -32, -11, 0, -11, 0, 4, -2, -20, 0, 0, 0, 0, 0, 0, 0, 0, -13, -17, -21, -14, 5, 0, -14, 0, -43, -8, -47, -44, 0, -28, 0, -32, 0, 0, -8, -4, 1, 0, 0, -4, -10, 0, -40, -23, 0, 0, -11, 0, -25, -11, -11, 0, -24, -32, 0, 0, -18, -11, 0, -11, 0, 0, 0, -11, 0, 0, -8, 1, 1, 1, 0, -8, 0, -40, 0, 0, 0, 0, 0, 0, -8, -43, -43, -43, 0, 0, 1, 0, 0, 0, 0, 0, 0, -14, 0, -43, 0, 0, -10, -22, -10, -4, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, -58, -53, 0, 0, 0, -11, 0, -53, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, -15, -36, 0, -15, -19, -15, 0, -15, -15, 0, 0, 0, 0, -13, 0, 9, -13, 0, -19, -13, -26, 0, 0, 0, 0, -23, 0, 0, 0, 0, 4, 32, 10, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, -26, 0, 0, 9, 0, 0, 32, 0, -19, -19, -19, 0, 9, 4, 0, 0, 19, 19, 0, -15, -13, -36, -17, 13, 13, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, - 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, -17, -17, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -15, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -8, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -29, 0, -22, -17, -9, -38, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, -7, -7, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -6, 3, 0, 0, 0, 0, 0, 0, -29, -29, -29, 0, 0, 0, 14, 0, 0, 0, -9, 0, 0, 0, -29, 0, 0, 0, -4, 0, 0, -75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -21, -7, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -19, -7, -19, -4, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -21, 0, 0, 0, 0, 0, 1, 0, 0, -5, -7, -6, -15, 2, 1, 0, -6, -13, 0, 0, 7, 0, 0, -7, 0, 0, -13, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, -6, -7, -7, -6, 2, 2, 2, 34, -6, 0, 0, -7, 0, 0, 34, 47, 1, -6, 0, 0, 0, 0, 0, 2, 15, 45, -27, -27, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, -6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, -30, 0, 0, -6, -19, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 33, 22, 0, 0, 0, 0, 0, 0, 0, 0, 29, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, -19, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, -19, 0, 0, -17, -42, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 3, 0, 0, 0, 0, 0, 0, -7, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -21, -11, -21, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -13, 4, 0, -19, -19, 0, 0, 0, 0, 0, -11, 0, 0, -4, -7, 7, 2, 4, 2, 0, 7, 0, 5, 3, 5, 0, -11, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -4, -4, -7, 2, 2, 2, 4, -7, 0, 5, -4, 0, 4, 4, 4, 4, -7, -19, -19, -19, 0, 4, 2, 12, 4, 0, 0, 0, 0, 0, 0, -19, -8, -8, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 5, -26, 0, 0, 0, -21, 5, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, - 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, -15, -11, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -25, -25, -25, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 1, -1, 1, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -17, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -6, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 1, 0, 0, -11, 0, -1, -1, 0, -2, 0, 0, 0, 2, -3, -11, 0, 2, 0, -2, -6, 0, 7, 7, 0, 0, -6, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -2, 2, 2, -3, 0, 0, 0, 0, -3, 0, 7, 2, 0, 1, 0, 0, 2, -3, -11, -11, -11, 0, 1, 0, 15, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -6, 0, -6, -2, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -13, 5, 0, 0, 0, -2, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, -17, -42, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -13, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -15, -12, -15, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -13, 0, 0, -23, 1, 0, -21, -11, 0, 0, 0, 0, -11, 0, 0, -26, -4, -6, 5, 0, 0, 0, -1, 6, 0, 0, 0, 0, 0, -12, 0, 0, -13, -12, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -4, -4, -6, 0, 0, 0, 6, -6, 0, 0, -4, 0, 1, 6, 6, 0, -6, -11, -32, -11, 0, 1, 0, 1, 6, 0, 0, 0, 0, 0, 0, -11, -15, -15, 6, 0, 6, -1, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 2, -26, 0, 0, 0, -40, 2, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -25, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, -19, -17, 0, 0, 0, 0, 0, -11, 0, 0, -1, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -5, 0, 0, 0, 0, -5, 0, 0, -1, 0, 0, 0, 0, 0, -5, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, -26, 0, 0, 0, -25, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -1, -7, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, -7, 0, 0, -11, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -1, -1, -7, 0, 0, 0, 0, -7, 0, 0, -1, 0, 0, 0, 0, 0, -7, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -7, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -2, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -26, 0, -15, -9, -27, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -45, -45, -26, 0, 0, 0, 0, 0, 0, 0, -27, -10, 0, 0, -50, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -17, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -2, -24, 0, -2, -24, 0, 0, 0, 0, -2, -3, 0, 0, 0, -4, -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, -9, 0, 0, 0, 0, -9, 0, 0, -4, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -2, 0, 0, 0, 0, 0, 0, 0, -4, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, -17, -42, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -30, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -2, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -52, 0, -28, -9, -29, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -52, -52, -52, 0, 0, 0, 0, 0, 0, 0, -29, -10, 0, 0, -52, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -17, -15, 0, 0, -17, -11, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -43, 0, -21, -19, -13, -30, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -43, -43, -43, 0, 0, 0, 0, 0, -11, -11, -30, -15, 0, 0, -66, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -40, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -11, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -30, 0, -13, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -13, -19, 0, -17, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, -42, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -17, -17, -17, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 35, 34, 0, 0, 0, 0, 0, 0, 0, 0, 25, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 22, 22, 4, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 33, 22, 33, 55, 0, 0, 0, 0, 0, 22, 55, 55, 33, 0, 30, 30, 30, 0, 47, 33, 17, 55, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, -17, -17, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -43, -43, -43, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -15, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -8, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -46, 0, -22, -17, -9, -38, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, -7, -7, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -6, 3, 0, 0, 0, 0, 0, 0, -46, -46, -46, 0, 0, 0, 14, 0, 0, 0, -9, 0, 0, 0, -46, 0, 0, 0, -4, 0, 0, -75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -21, -7, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, -17, -17, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -50, -50, -50, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -15, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 11, 0, 0, 0, 0, 3, 41, 19, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 13, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 13, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 6, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 26, 14, 0, 0, 0, 0, 0, 10, 14, 14, 9, 0, 13, 13, 13, 0, 10, 27, 15, 14, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 13, 13, 0, 0, 0, 0, 0, 13, 13, 13, 13, 0, 11, 11, 11, 0, 11, 11, 13, 13, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 33, 4, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 36, 36, 36, 55, 0, 0, 0, 0, 0, 22, 55, 55, 33, 0, 30, 30, 30, 0, 47, 36, 36, 55, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 36, 4, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 36, 36, 36, 55, 0, 0, 0, 0, 0, 22, 55, 55, 36, 0, 30, 30, 30, 0, 47, 36, 17, 55, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 22, 4, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 36, 36, 36, 55, 0, 0, 0, 0, 0, 22, 55, 55, 33, 0, 30, 30, 30, 0, 47, 36, 17, 55, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 35, 34, 0, 0, 0, 0, 0, 0, 0, 0, 25, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -30, 0, 0, 0, 0, 0, 0, -59, -38, 0, -38, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -9, 0, 0, -9, 0, -30, 0, -26, -12, 0, -32, 0, -59, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -37, 0, -32, -21, -38, 0, -27, -59, 0, 0, -26, -38, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -30, 0, 0, -7, -42, -7, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, -28, -29, 0, -15, -15, -11, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 3, 0, 12, 0, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 27, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -30, 0, 0, 0, 0, 0, 0, -57, -34, 0, -34, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -9, 0, 0, -9, 0, -30, 0, -26, -12, 0, -32, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -33, 0, -32, -21, -34, 0, -27, -59, 0, 0, -26, -34, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -30, 0, 0, -7, -42, -7, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, -28, -29, 0, -15, -15, -11, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 3, 0, 11, 0, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 13, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -30, 0, 0, 0, 0, 0, 0, -55, -22, 0, -39, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -9, 0, 0, -9, 0, -30, 0, -26, -12, 0, -47, 0, -55, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -35, 0, -32, -21, -66, 0, -27, -55, 0, 0, -26, -43, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -30, 0, 0, -7, -42, -7, 0, -47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, -28, -40, 0, -15, -15, -11, 0, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 11, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 3, 0, 11, 0, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 13, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -2, 0, -11, 0, 0, 0, -32, -11, -32, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -30, 0, -11, -30, 0, 0, 0, 0, 0, 2, 0, 0, -19, -27, -29, 0, 0, 0, -20, -26, -31, -47, -22, -22, -29, -30, -32, 0, 0, -43, -32, 0, 0, 0, 0, -47, 0, -32, 0, -17, 0, 0, 0, -11, -40, -40, -46, 0, 0, 0, 30, -46, -47, -34, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -47, 0, 0, 47, 30, -45, -45, 0, 0, -11, 0, 0, 0, 0, -31, -19, -31, -26, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, - 0, 0, 0, 0, -2, 0, -11, 0, 0, 0, -32, -11, -32, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -30, 0, -11, -30, 0, 0, 0, 0, 0, 2, 0, 0, -19, -27, -29, 0, 0, 0, -20, -26, -31, -27, -22, -22, -12, -17, -32, 0, 0, -43, -32, 0, 0, 0, 0, -61, 0, -32, 0, -17, 0, 0, 0, -11, -40, -40, -46, 0, 0, 0, 30, -46, -48, -34, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -40, 0, 0, 47, 30, -45, -45, 0, 0, -11, 0, 0, 0, 0, -31, -19, -31, -26, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, - 0, 0, 0, 0, -2, 0, -11, 0, 0, 0, -19, -11, -19, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -51, 0, -11, -70, 0, 0, 0, 0, 0, 2, 0, 0, -19, -27, -29, 0, 0, 0, -20, -26, -31, -27, -22, -22, -12, -17, -19, 0, 0, -43, -19, 0, 0, 0, 0, -61, 0, -46, 0, -17, 0, 0, 0, -11, -40, -40, -46, 0, 0, 0, 30, -46, -48, -34, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -40, 0, 0, 47, 30, -45, -45, 0, 0, -11, 0, 0, 0, 0, -31, -19, -31, -26, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -21, -14, 0, 0, -20, -20, -13, -20, -19, -19, -19, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, -20, -20, -21, 0, 0, 0, 0, -21, -20, -19, -20, 0, 0, 0, 0, 0, -21, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 3, 8, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 10, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, -19, 0, 6, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -7, -24, -24, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 35, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -8, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -55, 0, -22, -17, -9, -38, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, -7, -7, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -6, 3, 0, 0, 0, 0, 0, 0, -55, -55, -55, 0, 0, 0, 14, 0, 0, 0, -9, 0, 0, 0, -55, 0, 0, 0, -4, 0, 0, -75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -21, -7, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -13, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 24, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 29, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 33, 36, 4, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 33, 33, 33, 55, 0, 0, 0, 0, 0, 22, 55, 55, 36, 0, 30, 30, 30, 0, 47, 33, 32, 68, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -19, -19, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, -39, 0, -39, 0, 0, -13, 0, 0, -7, -13, 0, 0, -7, 0, 0, -17, -17, 0, -18, 0, 0, -18, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, -19, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, 0, 0, -30, -39, 0, -2, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, -15, -19, -19, -9, 0, 0, 0, 0, -9, 0, 0, -19, 0, 0, 0, 0, 0, -9, -10, -10, -10, 0, 0, 0, 28, 0, 0, 0, 0, -7, -35, -11, -10, 0, 0, -21, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -11, 0, -14, -9, -18, -15, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 8, 0, 0, 0, -35, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -11, 0, -30, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, -13, -34, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -11, 0, 0, 0, 0, 0, 0, -11, -11, 6, -11, 6, 0, -6, 0, 0, 0, -4, 0, 0, -3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -6, -10, 0, 0, 0, 0, -10, 0, -9, -10, 0, 0, -11, 0, 0, 0, -11, 0, 0, -11, 0, 0, -10, -11, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, -9, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -14, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 6, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -42, -11, -42, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -13, 4, 0, -19, -19, 0, 0, 0, 0, 0, -11, 0, 0, -4, -7, 7, 2, 4, 2, 0, 7, 0, 5, 3, 5, 0, -11, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -4, -4, -7, 2, 2, 2, 4, -7, 0, 5, -4, 0, 4, 4, 4, 4, -7, -19, -19, -19, 0, 4, 2, 12, 4, 0, 0, 0, 0, 0, 0, -19, -8, -8, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 5, -51, 0, 0, 0, -42, 5, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, -36, 0, -9, -57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -12, -9, -6, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, -36, -36, -36, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -36, 0, 0, -11, 0, -39, 0, -57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, -21, -21, -21, -21, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -15, -28, 0, -15, -11, 2, 0, 0, 0, 0, 0, 0, 0, 0, -35, -73, 0, -7, 0, -16, -23, 0, -13, 0, 0, 0, 0, -21, 0, 0, -30, -38, 0, -3, 0, 0, -13, 0, -38, 0, -21, 0, 0, 0, -28, -57, -52, -73, -7, -19, -7, 0, -73, -13, 0, -66, 0, 0, 0, 0, 0, -73, 2, 2, 2, -13, 0, -7, 28, 0, 0, 0, 0, 0, -15, 0, 2, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -54, 0, 0, 0, 0, 0, 0, 0, -58, -63, 0, 0, 0, -11, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -7, -13, 3, -7, 0, -7, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -3, 0, -3, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -7, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, -10, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, -3, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 33, 22, 0, 0, 0, 0, 0, 0, 0, 0, 25, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -8, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -29, 0, -22, -17, -9, -38, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, -7, -7, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -6, 3, 0, 0, 0, 0, 0, 0, -29, -29, -29, 0, 0, 0, 27, 0, 0, 0, -9, 0, 0, 0, -29, 0, 0, 0, -4, 0, 0, -75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -21, -7, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, -21, -33, -21, -33, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -15, -28, 0, -15, -11, 2, 0, 0, 0, 0, 0, 0, 0, 0, -35, -37, 0, -7, 0, -16, -23, 0, -13, 0, 0, 0, 0, -21, 0, 0, -30, -38, 0, -3, 0, 0, -13, 0, -38, 0, -21, 0, 0, 0, -28, -57, -52, -37, -7, -19, -7, 0, -37, -13, 0, -66, 0, 0, 0, 0, 0, -56, 2, 2, 2, -13, 0, -7, 28, 0, 0, 0, 0, 0, -15, 0, 2, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -33, 0, 0, 0, 0, 0, 0, 0, -58, -63, 0, 0, 0, -11, - 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, -35, 0, -35, -32, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, -49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, -21, -38, 0, 0, 0, 0, 0, -47, -28, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, -35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, -42, 0, 0, -23, 0, 0, 0, -18, -43, -18, -43, -47, -15, 0, 0, 0, -32, 0, -21, -13, 0, -19, -32, -19, 0, -19, -46, 0, -19, -42, 0, 0, 0, 0, 0, 0, 0, 0, -13, -19, -21, 0, 0, 0, -10, -23, 3, -13, 3, 0, 0, 0, -33, 0, 0, -28, -18, 0, 0, 0, 0, -28, -4, -18, 0, 0, -23, 0, 0, 0, -19, -19, -21, 0, 0, 0, 0, -21, -13, 3, -19, 0, 0, 0, 0, 0, -21, 0, 0, 0, -13, 0, 0, 0, 0, -17, -30, 0, 0, -19, 0, 0, -40, -21, 3, 0, 3, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, -30, -21, 0, -36, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, 0, 0, 0, 0, 0, -17, -11, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 17, 17, 0, 0, 0, -21, -49, 21, 21, -17, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -42, -32, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 0, -13, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, - -10, -4, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -31, 0, -7, 0, -2, -9, -8, -4, 0, 0, 0, -34, -15, 0, 0, -15, 0, -11, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, -7, -15, 0, -11, 0, 0, 0, -14, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, -32, 0, -45, -61, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 4, -2, 0, -11, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -13, 0, -19, 0, -47, -34, 0, -58, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 2, 0, -19, -19, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -6, -24, 0, 0, 0, 0, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -5, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -23, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -30, 0, -30, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, -55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, -3, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 15, 15, 0, 0, -3, -3, -3, 0, 0, 0, 0, 15, 0, 0, -5, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -23, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, -21, -13, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, 0, 0, -13, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, -2, 0, 0, 0, 0, 5, 0, 0, 4, -2, 0, 0, 0, 0, 5, 0, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -45, 0, -45, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, -5, -32, 0, -5, -32, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -2, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -26, 0, -15, -9, -14, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -45, -45, -26, 0, 0, 0, 0, 0, 0, 0, -36, -10, 0, 0, -50, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -10, -4, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -31, 0, -7, 0, -2, -9, -8, -4, 0, 0, 0, -34, -15, 0, 0, -15, 0, -11, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, -7, -15, 0, -11, 0, 0, 0, -14, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, -32, 0, -45, -45, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -21, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -10, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -10, 0, -13, -11, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 6, 0, 6, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -15, 0, -17, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 3, 0, 12, 0, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 13, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -11, 0, -11, 0, 0, -14, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -13, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -7, 0, -7, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -19, 0, 1, -15, 0, 0, 1, 0, -42, 0, -42, 0, -5, 0, 0, -6, -19, -3, 7, -5, -3, -11, -30, 0, 0, 0, -36, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 1, -53, -15, -17, 0, 0, 0, -17, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -15, -17, 0, 0, 0, 30, -17, 0, 0, -15, 0, 0, 30, 30, 0, -17, 0, 0, 0, 0, 0, 0, 0, 30, -36, -36, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -15, 0, -23, -21, -17, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -15, -15, -15, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -26, 0, -25, -25, -19, -30, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, -11, -11, -17, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -11, 0, -14, -9, -18, -15, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 8, 0, 0, 0, -35, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -11, 0, -13, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, -13, -34, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -26, 0, -5, 0, 0, 0, 0, -32, -11, 0, -11, 0, 4, -2, -20, 0, 0, 0, 0, 0, 0, 0, 0, -13, -17, -21, -14, 5, 0, -14, 0, -30, -8, -28, -25, 0, -28, 0, -32, 0, 0, -8, -4, 1, 0, 0, -4, -10, 0, -23, -23, 0, 0, -11, 0, -25, -11, -11, 0, -24, -32, 0, 0, -18, -11, 0, -11, 0, 0, 0, -11, 0, 0, -8, 1, 1, 1, 0, -8, 0, -23, 0, 0, 0, 0, 0, 0, -8, -30, -30, -49, 0, 0, 1, 0, 0, 0, 0, 0, 0, -14, 0, -30, 0, 0, -10, -22, -10, -4, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, -43, -34, 0, 0, 0, -11, 0, -34, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -17, -17, -17, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 2, 0, 0, 0, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 15, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -24, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, -11, -6, -8, 0, 0, 0, -6, 0, 0, -11, -11, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -6, -6, -8, 0, 0, 0, 0, -8, -11, -11, -6, 0, 0, 0, 0, 0, -8, 0, 0, 0, -11, 0, 0, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, -10, 0, -7, 0, -5, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -10, -10, -10, 0, 0, 0, 12, 11, 0, 0, -5, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, -26, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, -14, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 2, 0, 0, 0, 0, -11, -13, -21, 0, 0, -26, 0, 0, -17, -43, 0, -5, 0, 0, -11, 0, -41, 0, 0, 0, 0, 0, -11, 0, 0, -13, 2, 2, 2, 28, -13, -11, -13, 0, 0, 0, 28, 28, 0, -13, 0, 0, 0, -11, 0, 2, 28, 28, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, + 0, 0, -30, 0, 0, 0, 0, 0, 0, -57, -22, 0, -39, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -9, 0, 0, -9, 0, -30, 0, -26, -12, 0, -32, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -35, 0, -32, -21, -66, 0, -27, -59, 0, 0, -26, -43, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -30, 0, 0, -7, -42, -7, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, -28, -29, 0, -15, -15, -11, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -7, -13, -7, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -5, -5, -4, 0, 0, 0, 0, -4, 0, 0, -5, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -7, 0, 0, -10, 0, 0, 0, -2, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -5, 0, 0, 0, 15, -5, 0, 0, -2, 0, 0, 15, 15, 0, -5, -2, -2, -2, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2, 0, -11, 0, 0, 0, -19, -11, -19, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -30, 0, -11, -30, 0, 0, 0, 0, 0, 2, 0, 0, -19, -27, -29, 0, 0, 0, -20, -26, -31, -27, -22, -22, -12, -17, -19, 0, 0, -43, -19, 0, 0, 0, 0, -61, 0, -46, 0, -17, 0, 0, 0, -11, -40, -40, -46, 0, 0, 0, 30, -46, -48, -34, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -40, 0, 0, 47, 30, -45, -45, 0, 0, -11, 0, 0, 0, 0, -31, -19, -31, -26, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -19, -32, -19, -32, -18, -10, 0, 0, 0, -11, 0, 0, 0, 0, -21, -25, -21, 0, -25, -28, 0, -14, -32, 0, 0, 5, 0, 0, 0, 0, 0, -26, -21, -22, 0, 0, 0, -5, -15, 0, 0, 0, 0, 0, 0, -19, 0, 0, -19, -40, 0, 0, 0, 0, 0, 0, -32, 0, 0, -10, 0, 0, -13, -21, -34, -22, 0, 0, 0, 28, -37, 0, 0, -21, 0, 0, 28, 28, 0, -22, 0, 0, 0, 0, 0, 0, 13, 28, -40, -40, 0, 0, -14, 0, 0, 0, 0, 0, -6, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, -13, -32, 0, 0, 0, 0, 0, 0, 0, -47, -51, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2, 0, -17, 0, 0, 0, -14, -32, -14, -32, -10, 0, 0, 0, 0, -11, 0, 0, 0, 0, -17, -25, -21, 0, -9, -25, 0, -9, -21, 0, 0, 2, 7, 0, 0, 0, 0, 0, -19, -17, 0, 0, 0, -11, -11, 0, 0, 0, 0, 0, 0, -14, 0, 0, -19, -14, 0, 0, 0, 0, 0, 0, -25, 0, 0, -10, 0, 0, 0, -19, -19, -17, 0, 0, 0, 0, -17, 0, 0, -19, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, -34, -37, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, -14, 0, -14, 0, 0, -13, 0, 0, -7, -13, 0, 0, -7, 0, 0, -19, -17, 0, -18, 0, 0, -18, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, -19, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -13, -31, 0, -2, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, -15, -19, -19, -9, 0, 0, 0, 0, -9, 0, 0, -19, 0, 0, 0, 0, 0, -9, -10, -10, -10, 0, 0, 0, 28, 0, 0, 0, 0, -7, -35, -11, -10, 0, 0, -21, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, -21, -21, -21, -21, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -15, -28, 0, -15, -11, 2, 0, 0, 0, 0, 0, 0, 0, 0, -35, -37, 0, -7, 0, -16, -23, 0, -13, 0, 0, 0, 0, -21, 0, 0, -30, -38, 0, -3, 0, 0, -13, 0, -38, 0, -21, 0, 0, 0, -28, -57, -52, -37, -7, -19, -7, 0, -37, -13, 0, -66, 0, 0, 0, 0, 0, -56, 2, 2, 2, -13, 0, -7, 28, 0, 0, 0, 0, 0, -15, 0, 2, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -54, 0, 0, 0, 0, 0, 0, 0, -58, -63, 0, 0, 0, -11, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, -11, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, -11, -12, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 28, 28, 0, 0, 0, 0, 0, 0, 0, 0, 11, 28, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, -2, 0, 0, 0, 0, 5, 0, 0, 4, -2, 0, 0, 0, 0, 5, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -26, 0, -5, 0, 0, 0, 0, -32, -11, 0, -11, 0, 4, -2, -20, 0, 0, 0, 0, 0, 0, 0, 0, -13, -17, -21, -14, 5, 0, -14, 0, -43, -8, -47, -44, 0, -28, 0, -32, 0, 0, -8, -4, 1, 0, 0, -4, -10, 0, -40, -23, 0, 0, -11, 0, -25, -11, -11, 0, -24, -32, 0, 0, -18, -11, 0, -11, 0, 0, 0, -11, 0, 0, -8, 1, 1, 1, 0, -8, 0, -40, 0, 0, 0, 0, 0, 0, -8, -43, -43, -43, 0, 0, 1, 0, 0, 0, 0, 0, 0, -14, 0, -43, 0, 0, -10, -22, -10, -4, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, -58, -53, 0, 0, 0, -11, 0, -53, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, -15, -36, 0, -15, -19, -15, 0, -15, -15, 0, 0, 0, 0, -13, 0, 9, -13, 0, -19, -13, -26, 0, 0, 0, 0, -23, 0, 0, 0, 0, 4, 32, 10, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, -26, 0, 0, 9, 0, 0, 32, 0, -19, -19, -19, 0, 9, 4, 0, 0, 19, 19, 0, -15, -13, -36, -17, 13, 13, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, + 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, -17, -17, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -15, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -8, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -29, 0, -22, -17, -9, -38, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, -7, -7, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -6, 3, 0, 0, 0, 0, 0, 0, -29, -29, -29, 0, 0, 0, 14, 0, 0, 0, -9, 0, 0, 0, -29, 0, 0, 0, -4, 0, 0, -75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -21, -7, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -19, -7, -19, -4, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -21, 0, 0, 0, 0, 0, 1, 0, 0, -5, -7, -6, -15, 2, 1, 0, -6, -13, 0, 0, 7, 0, 0, -7, 0, 0, -13, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, -6, -7, -7, -6, 2, 2, 2, 34, -6, 0, 0, -7, 0, 0, 34, 47, 1, -6, 0, 0, 0, 0, 0, 2, 15, 45, -27, -27, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, -6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, -30, 0, 0, -6, -19, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 33, 22, 0, 0, 0, 0, 0, 0, 0, 0, 29, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, -19, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, -19, 0, 0, -17, -42, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 3, 0, 0, 0, 0, 0, 0, -7, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -21, -11, -21, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -13, 4, 0, -19, -19, 0, 0, 0, 0, 0, -11, 0, 0, -4, -7, 7, 2, 4, 2, 0, 7, 0, 5, 3, 5, 0, -11, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -4, -4, -7, 2, 2, 2, 4, -7, 0, 5, -4, 0, 4, 4, 4, 4, -7, -19, -19, -19, 0, 4, 2, 12, 4, 0, 0, 0, 0, 0, 0, -19, -8, -8, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 5, -26, 0, 0, 0, -21, 5, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, -15, -11, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -25, -25, -25, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 1, -1, 1, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -17, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -6, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 1, 0, 0, -11, 0, -1, -1, 0, -2, 0, 0, 0, 2, -3, -11, 0, 2, 0, -2, -6, 0, 7, 7, 0, 0, -6, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -2, 2, 2, -3, 0, 0, 0, 0, -3, 0, 7, 2, 0, 1, 0, 0, 2, -3, -11, -11, -11, 0, 1, 0, 15, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -6, 0, -6, -2, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -13, 5, 0, 0, 0, -2, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, -17, -42, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -13, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -15, -12, -15, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -13, 0, 0, -23, 1, 0, -21, -11, 0, 0, 0, 0, -11, 0, 0, -26, -4, -6, 5, 0, 0, 0, -1, 6, 0, 0, 0, 0, 0, -12, 0, 0, -13, -12, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -4, -4, -6, 0, 0, 0, 6, -6, 0, 0, -4, 0, 1, 6, 6, 0, -6, -11, -32, -11, 0, 1, 0, 1, 6, 0, 0, 0, 0, 0, 0, -11, -15, -15, 6, 0, 6, -1, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 2, -26, 0, 0, 0, -40, 2, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -25, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, -19, -17, 0, 0, 0, 0, 0, -11, 0, 0, -1, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -5, 0, 0, 0, 0, -5, 0, 0, -1, 0, 0, 0, 0, 0, -5, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, -26, 0, 0, 0, -25, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -1, -7, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, -7, 0, 0, -11, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -1, -1, -7, 0, 0, 0, 0, -7, 0, 0, -1, 0, 0, 0, 0, 0, -7, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -7, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -2, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -26, 0, -15, -9, -27, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -45, -45, -26, 0, 0, 0, 0, 0, 0, 0, -27, -10, 0, 0, -50, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -17, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -2, -24, 0, -2, -24, 0, 0, 0, 0, -2, -3, 0, 0, 0, -4, -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, -9, 0, 0, 0, 0, -9, 0, 0, -4, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -2, 0, 0, 0, 0, 0, 0, 0, -4, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, -17, -42, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -30, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -2, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -52, 0, -28, -9, -29, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -52, -52, -52, 0, 0, 0, 0, 0, 0, 0, -29, -10, 0, 0, -52, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -17, -15, 0, 0, -17, -11, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -43, 0, -21, -19, -13, -30, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -43, -43, -43, 0, 0, 0, 0, 0, -11, -11, -30, -15, 0, 0, -66, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -40, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -11, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -30, 0, -13, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -13, -19, 0, -17, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, -42, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -17, -17, -17, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 35, 34, 0, 0, 0, 0, 0, 0, 0, 0, 25, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 22, 22, 4, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 33, 22, 33, 55, 0, 0, 0, 0, 0, 22, 55, 55, 33, 0, 30, 30, 30, 0, 47, 33, 17, 55, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, -17, -17, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -43, -43, -43, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -15, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -8, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -46, 0, -22, -17, -9, -38, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, -7, -7, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -6, 3, 0, 0, 0, 0, 0, 0, -46, -46, -46, 0, 0, 0, 14, 0, 0, 0, -9, 0, 0, 0, -46, 0, 0, 0, -4, 0, 0, -75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -21, -7, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, -17, -17, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -50, -50, -50, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -15, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 11, 0, 0, 0, 0, 3, 41, 19, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 13, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 13, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 6, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 26, 14, 0, 0, 0, 0, 0, 10, 14, 14, 9, 0, 13, 13, 13, 0, 10, 27, 15, 14, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 13, 13, 0, 0, 0, 0, 0, 13, 13, 13, 13, 0, 11, 11, 11, 0, 11, 11, 13, 13, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 33, 4, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 36, 36, 36, 55, 0, 0, 0, 0, 0, 22, 55, 55, 33, 0, 30, 30, 30, 0, 47, 36, 36, 55, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 36, 4, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 36, 36, 36, 55, 0, 0, 0, 0, 0, 22, 55, 55, 36, 0, 30, 30, 30, 0, 47, 36, 17, 55, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 22, 4, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 36, 36, 36, 55, 0, 0, 0, 0, 0, 22, 55, 55, 33, 0, 30, 30, 30, 0, 47, 36, 17, 55, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 35, 34, 0, 0, 0, 0, 0, 0, 0, 0, 25, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -30, 0, 0, 0, 0, 0, 0, -59, -38, 0, -38, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -9, 0, 0, -9, 0, -30, 0, -26, -12, 0, -32, 0, -59, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -37, 0, -32, -21, -38, 0, -27, -59, 0, 0, -26, -38, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -30, 0, 0, -7, -42, -7, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, -28, -29, 0, -15, -15, -11, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 3, 0, 12, 0, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 27, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -30, 0, 0, 0, 0, 0, 0, -57, -34, 0, -34, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -9, 0, 0, -9, 0, -30, 0, -26, -12, 0, -32, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -33, 0, -32, -21, -34, 0, -27, -59, 0, 0, -26, -34, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -30, 0, 0, -7, -42, -7, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, -28, -29, 0, -15, -15, -11, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 3, 0, 11, 0, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 13, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -30, 0, 0, 0, 0, 0, 0, -55, -22, 0, -39, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -9, 0, 0, -9, 0, -30, 0, -26, -12, 0, -47, 0, -55, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -35, 0, -32, -21, -66, 0, -27, -55, 0, 0, -26, -43, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -30, 0, 0, -7, -42, -7, 0, -47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, -28, -40, 0, -15, -15, -11, 0, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 11, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 3, 0, 11, 0, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 13, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2, 0, -11, 0, 0, 0, -32, -11, -32, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -30, 0, -11, -30, 0, 0, 0, 0, 0, 2, 0, 0, -19, -27, -29, 0, 0, 0, -20, -26, -31, -47, -22, -22, -29, -30, -32, 0, 0, -43, -32, 0, 0, 0, 0, -47, 0, -32, 0, -17, 0, 0, 0, -11, -40, -40, -46, 0, 0, 0, 30, -46, -47, -34, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -47, 0, 0, 47, 30, -45, -45, 0, 0, -11, 0, 0, 0, 0, -31, -19, -31, -26, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2, 0, -11, 0, 0, 0, -32, -11, -32, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -30, 0, -11, -30, 0, 0, 0, 0, 0, 2, 0, 0, -19, -27, -29, 0, 0, 0, -20, -26, -31, -27, -22, -22, -12, -17, -32, 0, 0, -43, -32, 0, 0, 0, 0, -61, 0, -32, 0, -17, 0, 0, 0, -11, -40, -40, -46, 0, 0, 0, 30, -46, -48, -34, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -40, 0, 0, 47, 30, -45, -45, 0, 0, -11, 0, 0, 0, 0, -31, -19, -31, -26, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2, 0, -11, 0, 0, 0, -19, -11, -19, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -51, 0, -11, -70, 0, 0, 0, 0, 0, 2, 0, 0, -19, -27, -29, 0, 0, 0, -20, -26, -31, -27, -22, -22, -12, -17, -19, 0, 0, -43, -19, 0, 0, 0, 0, -61, 0, -46, 0, -17, 0, 0, 0, -11, -40, -40, -46, 0, 0, 0, 30, -46, -48, -34, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -40, 0, 0, 47, 30, -45, -45, 0, 0, -11, 0, 0, 0, 0, -31, -19, -31, -26, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -21, -14, 0, 0, -20, -20, -13, -20, -19, -19, -19, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, -20, -20, -21, 0, 0, 0, 0, -21, -20, -19, -20, 0, 0, 0, 0, 0, -21, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 3, 8, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 10, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, -19, 0, 6, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -7, -24, -24, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 35, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -8, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -55, 0, -22, -17, -9, -38, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, -7, -7, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -6, 3, 0, 0, 0, 0, 0, 0, -55, -55, -55, 0, 0, 0, 14, 0, 0, 0, -9, 0, 0, 0, -55, 0, 0, 0, -4, 0, 0, -75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -21, -7, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -13, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 24, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 29, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 33, 36, 4, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 33, 33, 33, 55, 0, 0, 0, 0, 0, 22, 55, 55, 36, 0, 30, 30, 30, 0, 47, 33, 32, 68, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -19, -19, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, -39, 0, -39, 0, 0, -13, 0, 0, -7, -13, 0, 0, -7, 0, 0, -17, -17, 0, -18, 0, 0, -18, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, -19, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, 0, 0, -30, -39, 0, -2, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, -15, -19, -19, -9, 0, 0, 0, 0, -9, 0, 0, -19, 0, 0, 0, 0, 0, -9, -10, -10, -10, 0, 0, 0, 28, 0, 0, 0, 0, -7, -35, -11, -10, 0, 0, -21, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -11, 0, -14, -9, -18, -15, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 8, 0, 0, 0, -35, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -11, 0, -30, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, -13, -34, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -11, 0, 0, 0, 0, 0, 0, -11, -11, 6, -11, 6, 0, -6, 0, 0, 0, -4, 0, 0, -3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -6, -10, 0, 0, 0, 0, -10, 0, -9, -10, 0, 0, -11, 0, 0, 0, -11, 0, 0, -11, 0, 0, -10, -11, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, -9, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -14, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 6, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -42, -11, -42, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -13, 4, 0, -19, -19, 0, 0, 0, 0, 0, -11, 0, 0, -4, -7, 7, 2, 4, 2, 0, 7, 0, 5, 3, 5, 0, -11, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -4, -4, -7, 2, 2, 2, 4, -7, 0, 5, -4, 0, 4, 4, 4, 4, -7, -19, -19, -19, 0, 4, 2, 12, 4, 0, 0, 0, 0, 0, 0, -19, -8, -8, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 5, -51, 0, 0, 0, -42, 5, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, -36, 0, -9, -57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -12, -9, -6, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, -36, -36, -36, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -36, 0, 0, -11, 0, -39, 0, -57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, -21, -21, -21, -21, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -15, -28, 0, -15, -11, 2, 0, 0, 0, 0, 0, 0, 0, 0, -35, -73, 0, -7, 0, -16, -23, 0, -13, 0, 0, 0, 0, -21, 0, 0, -30, -38, 0, -3, 0, 0, -13, 0, -38, 0, -21, 0, 0, 0, -28, -57, -52, -73, -7, -19, -7, 0, -73, -13, 0, -66, 0, 0, 0, 0, 0, -73, 2, 2, 2, -13, 0, -7, 28, 0, 0, 0, 0, 0, -15, 0, 2, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -54, 0, 0, 0, 0, 0, 0, 0, -58, -63, 0, 0, 0, -11, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -7, -13, 3, -7, 0, -7, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -3, 0, -3, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -7, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, -10, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, -3, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 33, 22, 0, 0, 0, 0, 0, 0, 0, 0, 25, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -8, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -29, 0, -22, -17, -9, -38, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, -7, -7, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -6, 3, 0, 0, 0, 0, 0, 0, -29, -29, -29, 0, 0, 0, 27, 0, 0, 0, -9, 0, 0, 0, -29, 0, 0, 0, -4, 0, 0, -75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -21, -7, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, -21, -33, -21, -33, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -15, -28, 0, -15, -11, 2, 0, 0, 0, 0, 0, 0, 0, 0, -35, -37, 0, -7, 0, -16, -23, 0, -13, 0, 0, 0, 0, -21, 0, 0, -30, -38, 0, -3, 0, 0, -13, 0, -38, 0, -21, 0, 0, 0, -28, -57, -52, -37, -7, -19, -7, 0, -37, -13, 0, -66, 0, 0, 0, 0, 0, -56, 2, 2, 2, -13, 0, -7, 28, 0, 0, 0, 0, 0, -15, 0, 2, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -33, 0, 0, 0, 0, 0, 0, 0, -58, -63, 0, 0, 0, -11, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, -35, 0, -35, -32, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, -49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, -21, -38, 0, 0, 0, 0, 0, -47, -28, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, -35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -42, 0, 0, -23, 0, 0, 0, -18, -43, -18, -43, -47, -15, 0, 0, 0, -32, 0, -21, -13, 0, -19, -32, -19, 0, -19, -46, 0, -19, -42, 0, 0, 0, 0, 0, 0, 0, 0, -13, -19, -21, 0, 0, 0, -10, -23, 3, -13, 3, 0, 0, 0, -33, 0, 0, -28, -18, 0, 0, 0, 0, -28, -4, -18, 0, 0, -23, 0, 0, 0, -19, -19, -21, 0, 0, 0, 0, -21, -13, 3, -19, 0, 0, 0, 0, 0, -21, 0, 0, 0, -13, 0, 0, 0, 0, -17, -30, 0, 0, -19, 0, 0, -40, -21, 3, 0, 3, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, -30, -21, 0, -36, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, 0, 0, 0, 0, 0, -17, -11, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 17, 17, 0, 0, 0, -21, -49, 21, 21, -17, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -42, -32, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 0, -13, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, + -10, -4, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -31, 0, -7, 0, -2, -9, -8, -4, 0, 0, 0, -34, -15, 0, 0, -15, 0, -11, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, -7, -15, 0, -11, 0, 0, 0, -14, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, -32, 0, -45, -61, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 4, -2, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -13, 0, -19, 0, -47, -34, 0, -58, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 2, 0, -19, -19, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -6, -24, 0, 0, 0, 0, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -5, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -23, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -8, -10, 17, }; static constexpr EpdFontData inter_10_bold = { inter_10_boldBitmaps, inter_10_boldGlyphs, inter_10_boldIntervals, - 63, + 77, 25, 21, -6, @@ -4607,10 +5212,10 @@ static constexpr EpdFontData inter_10_bold = { inter_10_boldKernLeftClasses, inter_10_boldKernRightClasses, inter_10_boldKernMatrix, - 777, - 776, - 153, - 145, + 781, + 797, + 154, + 149, nullptr, 0, }; diff --git a/lib/EpdFont/builtinFonts/inter_10_regular.h b/lib/EpdFont/builtinFonts/inter_10_regular.h index adc3b34d9f..f8ffc3f426 100644 --- a/lib/EpdFont/builtinFonts/inter_10_regular.h +++ b/lib/EpdFont/builtinFonts/inter_10_regular.h @@ -3,12 +3,12 @@ * name: inter_10_regular * size: 10 * mode: 1-bit - * Command used: fontconvert.py inter_10_regular 10 ../builtinFonts/source/Inter/Inter-Regular.ttf ../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-Regular.ttf --additional-intervals 0x05D0,0x05EA + * Command used: fontconvert.py inter_10_regular 10 ../builtinFonts/source/Inter/Inter-Regular.ttf ../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-Regular.ttf ../builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf --additional-intervals 0x05D0,0x05EA --additional-intervals 0x060C,0x060C --additional-intervals 0x061B,0x061B --additional-intervals 0x061F,0x061F --additional-intervals 0x0621,0x0621 --additional-intervals 0x0640,0x0640 --additional-intervals 0x0660,0x0669 --additional-intervals 0x06BA,0x06BA --additional-intervals 0x06D4,0x06D4 --additional-intervals 0x06F0,0x06F9 --additional-intervals 0xFB56,0xFB59 --additional-intervals 0xFB66,0xFB69 --additional-intervals 0xFB7A,0xFB7D --additional-intervals 0xFB88,0xFB95 --additional-intervals 0xFB9E,0xFB9F --additional-intervals 0xFBA6,0xFBB1 --additional-intervals 0xFBFC,0xFBFF --additional-intervals 0xFE80,0xFEFC */ #pragma once #include "EpdFontData.h" -static const uint8_t inter_10_regularBitmaps[24456] = { +static const uint8_t inter_10_regularBitmaps[29283] = { 0x6E, 0xEE, 0xEE, 0x66, 0x66, 0x66, 0x06, 0xEE, 0x00, 0xCB, 0x7D, 0xF7, 0xDF, 0x7D, 0x80, 0x04, 0x20, 0x63, 0x03, 0x18, 0x18, 0xC1, 0xC6, 0x3F, 0xFD, 0xFF, 0xE3, 0x18, 0x18, 0xC0, 0xC6, 0x3F, 0xFD, 0xFF, 0xE3, 0x1C, 0x18, 0xC0, 0xC6, 0x06, 0x30, 0x04, 0x00, 0x60, 0x0E, 0x03, 0xFC, 0x7F, @@ -655,889 +655,1191 @@ static const uint8_t inter_10_regularBitmaps[24456] = { 0x38, 0x03, 0x80, 0x3B, 0x83, 0xFE, 0x3F, 0xE3, 0x87, 0x38, 0x73, 0x87, 0x38, 0x73, 0x87, 0x38, 0x73, 0x87, 0x38, 0x73, 0x87, 0xE6, 0x73, 0x77, 0x64, 0x38, 0xFB, 0xB8, 0x00, 0x65, 0xFB, 0x70, 0xFF, 0xF0, 0xCF, 0xF7, 0x80, 0xDF, 0x80, 0x66, 0xE7, 0x66, 0xEF, 0x66, 0x01, 0xEF, 0xF3, 0xFD, - 0xE0, 0x00, 0x00, 0x36, 0x7E, 0x6C, 0xCC, 0xCE, 0xF9, 0xE0, 0x00, 0xEE, 0x6E, 0x36, 0x32, 0x66, - 0x66, 0xEE, 0xC0, 0x66, 0x66, 0xEE, 0xC0, 0x37, 0x76, 0x60, 0xDF, 0x80, 0x33, 0x76, 0x60, 0x43, - 0x1C, 0x6F, 0x30, 0x13, 0x98, 0xF7, 0x9C, 0xCE, 0xF9, 0xE0, 0x00, 0x01, 0x80, 0xC0, 0xC0, 0x60, - 0x60, 0x30, 0x30, 0x18, 0x18, 0x0C, 0x0C, 0x06, 0x06, 0x03, 0x01, 0x00, 0x00, 0x30, 0x06, 0x00, - 0x60, 0x0C, 0x01, 0xC0, 0x18, 0x03, 0x00, 0x30, 0x06, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x38, 0x03, - 0x00, 0x60, 0x06, 0x00, 0xC0, 0x00, 0x65, 0xFB, 0x70, 0x66, 0xEC, 0x00, 0xFF, 0xFF, 0xC3, 0xC3, - 0xC3, 0xFF, 0xFD, 0xFF, 0xF7, 0xC0, 0xDE, 0xF7, 0xBD, 0x80, 0xFF, 0xCE, 0x71, 0x00, 0x00, 0x6F, - 0xFF, 0xFE, 0xB0, 0x1C, 0x0E, 0x0E, 0xCF, 0xE6, 0xF2, 0xE0, 0x70, 0x10, 0x7F, 0xFF, 0x7B, 0xFF, - 0xC4, 0x33, 0x1F, 0xE7, 0xF9, 0xCE, 0x33, 0x00, 0x00, 0xC7, 0xBF, 0x30, 0xC0, 0xC0, 0x78, 0x3F, - 0x83, 0xE0, 0xF3, 0xFB, 0xE1, 0xC0, 0x3F, 0x76, 0x18, 0x7E, 0x7E, 0x66, 0x18, 0x18, 0x00, 0x4B, - 0xE7, 0x9E, 0xF8, 0x00, 0x00, 0xFF, 0xBC, 0x3C, 0x30, 0x03, 0xC7, 0xCF, 0xF3, 0x00, 0x00, 0x8F, - 0x1C, 0x7D, 0xC3, 0xF6, 0xFB, 0x6C, 0x32, 0x0E, 0x63, 0x6E, 0x00, 0xDF, 0x80, 0x20, 0xCF, 0x8C, - 0x78, 0x00, 0x66, 0x7F, 0xB6, 0x5B, 0xE7, 0xF0, 0x67, 0x3C, 0xE3, 0x00, 0x40, 0x02, 0x70, 0x0E, - 0x3F, 0xFC, 0x0F, 0xF0, 0x40, 0x02, 0x70, 0x0E, 0x3F, 0xFC, 0x0F, 0xF0, 0x7F, 0xF9, 0xFF, 0xE7, - 0xFF, 0x80, 0x7F, 0xF9, 0xFF, 0xE7, 0xFF, 0x80, 0x3E, 0x00, 0x37, 0xFF, 0xFF, 0xEF, 0xFF, 0xE4, - 0x00, 0x08, 0x00, 0x00, 0x1F, 0xF8, 0x3F, 0xFC, 0x70, 0x0E, 0x00, 0x0E, 0x00, 0x03, 0x9F, 0xFF, - 0xE0, 0x00, 0xE0, 0x00, 0xC0, 0x79, 0xF7, 0xFF, 0xFD, 0xF0, 0x79, 0xFF, 0xFF, 0x7D, 0xE0, 0xFF, - 0xFF, 0x7B, 0xFC, 0xF3, 0xFD, 0xE0, 0xCF, 0x3C, 0xF3, 0x7D, 0xF0, 0x79, 0xFC, 0x30, 0xFD, 0xE0, - 0x0C, 0x30, 0xDF, 0xFF, 0x3C, 0xFF, 0x7C, 0x43, 0x0C, 0x3E, 0xFF, 0x3C, 0xF3, 0xCC, 0xFF, 0x7F, - 0xF6, 0x7B, 0x3D, 0x9E, 0xCC, 0xFF, 0xEE, 0xEC, 0x66, 0xF7, 0x66, 0x77, 0xED, 0xB7, 0x9E, 0x38, - 0xC0, 0xD9, 0xE7, 0x1C, 0xFB, 0x60, 0x18, 0x03, 0x80, 0x30, 0x02, 0x07, 0xFD, 0xFF, 0xFF, 0xF7, - 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x7F, 0xEF, 0xFD, 0xFF, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, - 0xFF, 0xF0, 0x31, 0x87, 0x70, 0xEE, 0x00, 0x07, 0xFD, 0xFF, 0xFF, 0xF7, 0x00, 0xE0, 0x1C, 0x03, - 0x80, 0x7F, 0xEF, 0xFD, 0xFF, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, 0xF0, 0x7F, 0xF0, - 0x3F, 0xFC, 0x1F, 0xFE, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1D, 0xE0, 0x0F, 0xFC, 0x07, - 0xFF, 0x03, 0xC3, 0x81, 0xC0, 0xE0, 0xE0, 0x70, 0x70, 0x38, 0x38, 0x1C, 0x1C, 0x3C, 0x0E, 0x7C, - 0x00, 0x3C, 0x00, 0x03, 0x01, 0xC0, 0x60, 0x10, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x38, 0x0E, - 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0x07, 0x80, 0x7F, 0x83, - 0xFF, 0x1E, 0x1E, 0x60, 0x3B, 0x80, 0x6E, 0x00, 0x3F, 0xE0, 0xFF, 0x83, 0x80, 0x0E, 0x00, 0x38, - 0x06, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x0E, 0x03, 0xFC, 0x7F, 0xCE, 0x0E, - 0xE0, 0x6E, 0x00, 0xF0, 0x07, 0xE0, 0x3F, 0xC0, 0x7E, 0x00, 0xEC, 0x06, 0xE0, 0x6E, 0x0E, 0x7F, - 0xE3, 0xFC, 0x0E, 0x00, 0x5F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x66, 0xEE, 0xE6, 0x00, 0x10, 0x38, - 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x00, 0xC0, - 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x36, 0x0D, 0x83, 0x60, 0xDC, 0x77, - 0xFC, 0xFE, 0x04, 0x00, 0x07, 0xF8, 0x00, 0x1F, 0xF8, 0x00, 0x3F, 0xF0, 0x00, 0x60, 0xE0, 0x00, - 0xC1, 0xC0, 0x01, 0x83, 0x80, 0x07, 0x07, 0xF0, 0x0E, 0x0F, 0xFC, 0x1C, 0x1F, 0xFC, 0x38, 0x38, - 0x38, 0x70, 0x70, 0x30, 0xE0, 0xE0, 0x61, 0x81, 0xC0, 0xC7, 0x03, 0x83, 0x9E, 0x07, 0xFE, 0x38, - 0x0F, 0xF8, 0x40, 0x10, 0x03, 0x80, 0xE0, 0x0E, 0x03, 0x80, 0x38, 0x0E, 0x00, 0xE0, 0x38, 0x03, - 0x80, 0xE0, 0x0E, 0x03, 0xF8, 0x3F, 0xFF, 0xFC, 0xFF, 0xFF, 0xFB, 0x80, 0xE0, 0xEE, 0x03, 0x81, - 0xF8, 0x0E, 0x07, 0xE0, 0x38, 0x1F, 0x80, 0xE0, 0xEE, 0x03, 0xFF, 0xB8, 0x0F, 0xFC, 0x7F, 0xF8, - 0x3F, 0xFC, 0x1F, 0xFE, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0xE0, 0x0F, 0xFC, 0x07, - 0xFF, 0x03, 0xC1, 0xC1, 0xC0, 0xE0, 0xE0, 0x70, 0x70, 0x38, 0x38, 0x1C, 0x1C, 0x0E, 0x0E, 0x07, - 0x03, 0x80, 0x18, 0x01, 0xC0, 0x0C, 0x04, 0x03, 0x70, 0x3B, 0x83, 0x9C, 0x38, 0xE1, 0xC7, 0x1C, - 0x39, 0xC1, 0xFC, 0x0F, 0xE0, 0x7F, 0x83, 0x9E, 0x1C, 0x70, 0xE1, 0xC7, 0x07, 0x38, 0x3D, 0xC0, - 0xE0, 0x0E, 0x00, 0x70, 0x01, 0xC0, 0x06, 0x04, 0x01, 0x70, 0x1F, 0x81, 0xFC, 0x1F, 0xE0, 0xFF, - 0x0F, 0xF8, 0xEF, 0xC7, 0x7E, 0x73, 0xF3, 0x9F, 0xB8, 0xFF, 0x87, 0xFC, 0x3F, 0xC1, 0xFC, 0x0F, - 0xE0, 0x70, 0x18, 0xC0, 0xFE, 0x03, 0xE0, 0x04, 0x04, 0x01, 0x70, 0x1D, 0xC1, 0xCE, 0x0E, 0x38, - 0x61, 0xC7, 0x06, 0x38, 0x3B, 0x80, 0xDC, 0x07, 0xC0, 0x3E, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x0F, - 0x80, 0x78, 0x00, 0x40, 0x37, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, - 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x38, - 0x01, 0xC0, 0x04, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, - 0x07, 0x38, 0x18, 0x60, 0xE1, 0xC3, 0xFF, 0x0F, 0xFC, 0x70, 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, - 0x7F, 0xCF, 0xFC, 0xFF, 0xCE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xF8, 0xFF, 0xCE, 0x0E, 0xE0, - 0x6E, 0x07, 0xE0, 0x6E, 0x0E, 0xFF, 0xEF, 0xF8, 0x7E, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E, 0xE0, 0x6E, - 0x06, 0xE0, 0xEF, 0xFC, 0xFF, 0xCF, 0xFE, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x0F, 0xFF, 0xEF, 0xFC, - 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, - 0x38, 0x0E, 0x03, 0x80, 0x07, 0xFC, 0x0F, 0xFE, 0x0F, 0xFE, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, - 0x0E, 0x0E, 0x0C, 0x0E, 0x0C, 0x0E, 0x0C, 0x0E, 0x1C, 0x0E, 0x1C, 0x0E, 0x1C, 0x0E, 0x38, 0x0E, - 0x7F, 0xFF, 0x7F, 0xFF, 0x60, 0x03, 0x60, 0x03, 0x60, 0x03, 0x60, 0x03, 0x7F, 0xDF, 0xFF, 0xFF, - 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0xFE, 0xFF, 0xDF, 0xF3, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, - 0xFF, 0xFF, 0x60, 0x60, 0x67, 0x06, 0x0E, 0x38, 0x61, 0xC3, 0x86, 0x1C, 0x1C, 0x63, 0x81, 0xC6, - 0x30, 0x0E, 0x67, 0x00, 0x7F, 0xE0, 0x07, 0xFE, 0x00, 0xFF, 0xF0, 0x0C, 0x67, 0x01, 0xC6, 0x38, - 0x38, 0x61, 0xC3, 0x86, 0x1C, 0x70, 0x60, 0xEE, 0x06, 0x0E, 0x0E, 0x07, 0xF1, 0xFF, 0x70, 0xEE, - 0x0C, 0x01, 0x80, 0x70, 0x7C, 0x0F, 0x80, 0x78, 0x03, 0x80, 0x7E, 0x0F, 0xC1, 0xDF, 0xF1, 0xFC, - 0x0E, 0x00, 0x40, 0x17, 0x01, 0xF8, 0x1F, 0xC1, 0xFE, 0x0F, 0xF0, 0xFF, 0x8E, 0xFC, 0x77, 0xE7, - 0x3F, 0x39, 0xFB, 0x8F, 0xF8, 0x7F, 0xC3, 0xFC, 0x1F, 0xC0, 0xFE, 0x07, 0x0C, 0xC0, 0x7E, 0x03, - 0xE0, 0x04, 0x04, 0x01, 0x70, 0x1F, 0x81, 0xFC, 0x1F, 0xE0, 0xFF, 0x0F, 0xF8, 0xEF, 0xC7, 0x7E, - 0x73, 0xF3, 0x9F, 0xB8, 0xFF, 0x87, 0xFC, 0x3F, 0xC1, 0xFC, 0x0F, 0xE0, 0x70, 0x40, 0x37, 0x03, - 0xB8, 0x39, 0xC3, 0x8E, 0x1C, 0x71, 0xC3, 0x9C, 0x1F, 0xC0, 0xFE, 0x07, 0xF8, 0x39, 0xE1, 0xC7, - 0x0E, 0x1C, 0x70, 0x73, 0x83, 0xDC, 0x0E, 0x07, 0xF8, 0x3F, 0xF0, 0xFF, 0xC3, 0x07, 0x0C, 0x1C, - 0x30, 0x71, 0xC1, 0xC7, 0x07, 0x1C, 0x1C, 0x70, 0x71, 0xC1, 0xC7, 0x07, 0x1C, 0x1C, 0xE0, 0x77, - 0x81, 0xDC, 0x07, 0x60, 0x07, 0xF0, 0x07, 0xF0, 0x0F, 0xF8, 0x0F, 0xF8, 0x1F, 0xFC, 0x1F, 0xFC, - 0x1B, 0xEC, 0x3B, 0xEE, 0x33, 0xE6, 0x73, 0xE7, 0x73, 0xE7, 0x63, 0xE3, 0xE3, 0xE3, 0xC3, 0xE3, - 0xC3, 0xE1, 0xC3, 0x40, 0x17, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFF, 0xFF, - 0xFF, 0xFF, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0x07, 0x80, 0x7F, - 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7E, 0x01, - 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x7F, 0xF7, 0xFF, 0xFF, 0xFF, - 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, - 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0x7E, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E, 0xE0, 0x6E, 0x07, 0xE0, 0x6E, - 0x0E, 0xFF, 0xEF, 0xFC, 0xFF, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x07, 0x80, 0x7F, - 0x83, 0xFF, 0x1E, 0x1E, 0x60, 0x3B, 0x80, 0x6E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, - 0x38, 0x06, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x7F, 0xF3, 0xFF, 0xDF, 0xFE, - 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, - 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x40, 0x17, 0x01, 0xDC, 0x1C, 0xE0, 0xE3, 0x86, 0x1C, 0x70, 0x63, - 0x83, 0xB8, 0x0D, 0xC0, 0x7C, 0x03, 0xE0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0xF8, 0x07, 0x80, 0x03, - 0x80, 0x07, 0x00, 0x7F, 0xC1, 0xFF, 0xC7, 0xB9, 0xDC, 0x71, 0xF8, 0xE3, 0xE1, 0xC3, 0xC3, 0x87, - 0x87, 0x0F, 0x8E, 0x3F, 0x1C, 0x77, 0xBB, 0xC7, 0xFF, 0x07, 0xFC, 0x01, 0xC0, 0x03, 0x80, 0x60, - 0x19, 0xC0, 0xE3, 0x87, 0x8E, 0x1C, 0x1C, 0xE0, 0x3F, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x80, 0x1F, - 0x00, 0xFC, 0x07, 0x38, 0x1C, 0xF0, 0xE1, 0xC7, 0x03, 0xB8, 0x07, 0x40, 0x33, 0x80, 0xEE, 0x03, - 0xB8, 0x0E, 0xE0, 0x3B, 0x80, 0xEE, 0x03, 0xB8, 0x0E, 0xE0, 0x3B, 0x80, 0xEE, 0x03, 0xB8, 0x0E, - 0xE0, 0x3B, 0x80, 0xEF, 0xFF, 0xFF, 0xFF, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, 0x03, 0x40, 0x2E, - 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x7F, 0xF3, 0xFF, 0x0F, 0x70, 0x07, - 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x40, 0xC1, 0xB8, 0x30, 0x6E, 0x0C, 0x1B, 0x83, 0x06, 0xE0, - 0xC1, 0xB8, 0x30, 0x6E, 0x0C, 0x1B, 0x83, 0x06, 0xE0, 0xC1, 0xB8, 0x30, 0x6E, 0x0C, 0x1B, 0x83, - 0x06, 0xE0, 0xC1, 0xB8, 0x30, 0x6F, 0xFF, 0xFB, 0xFF, 0xFE, 0x40, 0xC1, 0x9C, 0x18, 0x33, 0x83, - 0x06, 0x70, 0x60, 0xCE, 0x0C, 0x19, 0xC1, 0x83, 0x38, 0x30, 0x67, 0x06, 0x0C, 0xE0, 0xC1, 0x9C, - 0x18, 0x33, 0x83, 0x06, 0x70, 0x60, 0xCE, 0x0C, 0x19, 0xC1, 0x83, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, - 0x00, 0x00, 0xE0, 0x00, 0x1C, 0x00, 0x03, 0x80, 0x00, 0x20, 0x7E, 0x00, 0x7F, 0x00, 0x7F, 0x00, - 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0xE0, 0x07, 0xFC, 0x07, 0xFE, 0x07, 0x07, 0x07, 0x07, - 0x07, 0x03, 0x07, 0x07, 0x07, 0x07, 0x07, 0xFE, 0x07, 0xFC, 0x40, 0x02, 0xE0, 0x07, 0xE0, 0x07, - 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xFE, 0x07, 0xFF, 0xC7, 0xFF, 0xE7, 0xE0, 0xE7, 0xE0, 0x77, - 0xE0, 0x77, 0xE0, 0x77, 0xE0, 0xE7, 0xFF, 0xE7, 0xFF, 0xC7, 0x40, 0x0E, 0x00, 0xE0, 0x0E, 0x00, - 0xE0, 0x0E, 0x00, 0xFE, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x0E, 0xFF, - 0xEF, 0xFC, 0x0F, 0x01, 0xFE, 0x1F, 0xF9, 0xE0, 0xEE, 0x03, 0xE0, 0x1C, 0x00, 0x61, 0xFF, 0x0F, - 0xF8, 0x00, 0xC0, 0x07, 0x80, 0x7E, 0x03, 0xB8, 0x79, 0xFF, 0x87, 0xF8, 0x06, 0x00, 0x40, 0x3C, - 0x1C, 0x1F, 0xE3, 0x87, 0xFE, 0x71, 0xE0, 0xEE, 0x38, 0x0D, 0xCE, 0x01, 0xF9, 0xC0, 0x3F, 0xF8, - 0x03, 0xFF, 0x00, 0x7F, 0xE0, 0x0F, 0x9C, 0x03, 0xF3, 0x80, 0x7E, 0x38, 0x1D, 0xC7, 0x87, 0xB8, - 0x7F, 0xE7, 0x07, 0xF8, 0x00, 0x18, 0x00, 0x03, 0xE1, 0xFF, 0x3F, 0xF7, 0x07, 0x70, 0x76, 0x07, - 0x70, 0x77, 0x07, 0x3F, 0xF1, 0xFF, 0x1C, 0x71, 0xC7, 0x38, 0x73, 0x87, 0x70, 0x77, 0x07, 0x0E, - 0x0F, 0xE7, 0xFD, 0xC3, 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, 0xD8, 0x77, 0xFC, 0xFF, 0x0C, 0x00, - 0x00, 0x40, 0x0E, 0x0F, 0xC3, 0xF8, 0x38, 0x07, 0xF8, 0x7F, 0xC7, 0x8E, 0x70, 0x66, 0x06, 0x60, - 0x76, 0x06, 0x60, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x7C, 0x3F, 0xEF, 0xFB, 0x87, 0xE1, - 0xBF, 0xEF, 0xFB, 0x87, 0xE1, 0xF8, 0x7F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xE0, 0xE0, 0xE0, 0xE0, - 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x31, 0xC1, 0x8E, 0x1C, 0x70, 0xE3, - 0x87, 0x1C, 0x38, 0xE3, 0x87, 0x3F, 0xFF, 0xFF, 0xFE, 0x03, 0xF0, 0x1F, 0x80, 0xF8, 0x06, 0x0F, - 0x03, 0xF8, 0xFF, 0x38, 0x76, 0x06, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0E, 0x1C, 0xFF, 0x8F, 0xE0, - 0x60, 0x61, 0x83, 0x38, 0xE3, 0x8E, 0x73, 0x87, 0x3B, 0x81, 0xDD, 0x80, 0x7F, 0xC0, 0x1F, 0xC0, - 0x1F, 0xF0, 0x1D, 0xDC, 0x1C, 0xEF, 0x1C, 0x73, 0x8E, 0x38, 0xE0, 0x0E, 0x0F, 0xC7, 0xF9, 0x86, - 0x03, 0x83, 0xE0, 0xF0, 0x0E, 0x01, 0xD8, 0x77, 0xF8, 0xFC, 0x00, 0x00, 0x40, 0xF8, 0x7E, 0x3F, - 0x8F, 0xE7, 0xFB, 0xFE, 0xDF, 0xF7, 0xF9, 0xFC, 0x7F, 0x1F, 0x87, 0x33, 0x0F, 0xC1, 0xE0, 0x10, - 0x40, 0xF8, 0x7E, 0x3F, 0x8F, 0xE7, 0xFB, 0xFE, 0xDF, 0xF7, 0xF9, 0xFC, 0x7F, 0x1F, 0x87, 0x41, - 0xF8, 0xFE, 0x3B, 0x9C, 0xEE, 0x3F, 0x0F, 0xC3, 0xF8, 0xEF, 0x39, 0xCE, 0x3B, 0x87, 0x1F, 0xC7, - 0xFC, 0xFF, 0x9C, 0x73, 0x8E, 0x71, 0xCE, 0x39, 0xC7, 0x30, 0xE6, 0x1F, 0xC3, 0xF0, 0x70, 0x60, - 0x1B, 0xC0, 0xFF, 0x07, 0xFE, 0x1F, 0xF8, 0x7F, 0xF3, 0xFF, 0xCD, 0xFB, 0x77, 0xEF, 0xDF, 0x9E, - 0x7E, 0x79, 0xF8, 0xC7, 0x40, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xFF, 0xFF, 0xFF, 0x87, 0xE1, 0xF8, - 0x7E, 0x1F, 0x87, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, - 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, - 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0x4E, 0x1F, 0xF3, 0xFF, 0x70, 0x6E, 0x0F, 0xC1, 0xF8, 0x3F, - 0x07, 0xE0, 0xFE, 0x3B, 0xFF, 0x7F, 0xCE, 0x61, 0xC0, 0x38, 0x07, 0x00, 0x40, 0x00, 0x0F, 0x03, - 0xF8, 0xFF, 0x38, 0x77, 0x04, 0xC0, 0x18, 0x03, 0x00, 0x70, 0x6E, 0x1C, 0xFF, 0x0F, 0xC0, 0x60, - 0xFF, 0xBF, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x40, - 0x7C, 0x1D, 0x83, 0xB8, 0x63, 0x1C, 0x73, 0x8E, 0x60, 0xDC, 0x1F, 0x03, 0xE0, 0x3C, 0x07, 0x00, - 0xE0, 0x18, 0x0F, 0x03, 0xC0, 0x70, 0x00, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x0F, 0xC0, - 0x7F, 0x83, 0xFF, 0x1C, 0xCE, 0x63, 0x19, 0x8C, 0x66, 0x31, 0xD8, 0xC6, 0x73, 0x19, 0xCC, 0xE3, - 0xFF, 0x07, 0xF8, 0x07, 0x80, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x60, 0xCE, 0x38, 0xC7, - 0x1D, 0xC1, 0xF0, 0x1E, 0x03, 0x80, 0xF8, 0x1B, 0x87, 0x71, 0xC7, 0x70, 0x70, 0x40, 0xDC, 0x3B, - 0x87, 0x70, 0xEE, 0x1D, 0xC3, 0xB8, 0x77, 0x0E, 0xE1, 0xDC, 0x3B, 0xFF, 0xFF, 0xF0, 0x06, 0x00, - 0xC0, 0x18, 0x03, 0x40, 0xB8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x77, 0xFD, 0xFF, 0x1F, 0xC0, 0x70, - 0x1C, 0x07, 0x41, 0x05, 0xC7, 0x1F, 0x8E, 0x3F, 0x1C, 0x7E, 0x38, 0xFC, 0x71, 0xF8, 0xE3, 0xF1, - 0xC7, 0xE3, 0x8F, 0xC7, 0x1F, 0xFF, 0xFF, 0xFF, 0xF0, 0x41, 0x04, 0xE3, 0x8E, 0xE3, 0x8E, 0xE3, - 0x8E, 0xE3, 0x8E, 0xE3, 0x8E, 0xE3, 0x8E, 0xE3, 0x8E, 0xE3, 0x8E, 0xE3, 0x8E, 0xFF, 0xFF, 0xFF, - 0xFF, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0xF8, 0x07, 0xE0, 0x3F, 0x00, 0x38, 0x01, - 0xF8, 0x0F, 0xF0, 0x7F, 0xC3, 0x87, 0x1C, 0x38, 0xE1, 0xC7, 0xFC, 0x3F, 0xC0, 0x40, 0x1F, 0x01, - 0xF8, 0x0F, 0xC0, 0x7F, 0xC3, 0xFF, 0xDF, 0xFE, 0xFC, 0x3F, 0xE1, 0xFF, 0x0F, 0xFF, 0xEF, 0xFE, - 0x70, 0x40, 0x38, 0x0E, 0x03, 0x80, 0xFC, 0x3F, 0xEF, 0xFB, 0x87, 0xE1, 0xF8, 0x7F, 0xFB, 0xFC, - 0x0E, 0x07, 0xF1, 0xFF, 0x38, 0x62, 0x0E, 0x1F, 0xC3, 0xF8, 0x7F, 0x60, 0xEE, 0x39, 0xFF, 0x1F, - 0xC0, 0xE0, 0x40, 0xF0, 0xE1, 0xFC, 0xE3, 0xFE, 0xE7, 0x0E, 0xE7, 0x06, 0xFE, 0x07, 0xFE, 0x07, - 0xFE, 0x07, 0xE7, 0x06, 0xE7, 0x0E, 0xE3, 0xFC, 0xE1, 0xF8, 0x00, 0x60, 0x0F, 0xCF, 0xF7, 0xFD, - 0x83, 0x60, 0xDC, 0x37, 0xFC, 0xFF, 0x38, 0xCC, 0x37, 0x0F, 0x83, 0x1C, 0x01, 0x80, 0x38, 0x03, - 0x00, 0xF0, 0x3F, 0x8F, 0xF3, 0x87, 0x60, 0x6F, 0xFD, 0xFF, 0xB0, 0x06, 0x00, 0xE1, 0xCF, 0xF8, - 0xFE, 0x06, 0x00, 0x19, 0x87, 0x38, 0x66, 0x00, 0x00, 0xF0, 0x3F, 0x8F, 0xF3, 0x87, 0x60, 0x6F, - 0xFD, 0xFF, 0xB0, 0x06, 0x00, 0xE1, 0xCF, 0xF8, 0xFE, 0x06, 0x00, 0x20, 0x0E, 0x03, 0xF8, 0x7F, - 0x07, 0x70, 0xFF, 0x9F, 0xF3, 0x87, 0x70, 0xEE, 0x1D, 0xC3, 0xB8, 0x77, 0x0E, 0xE1, 0xDC, 0x3B, - 0x87, 0x00, 0xE0, 0x1C, 0x07, 0x81, 0xE0, 0x18, 0x0E, 0x0C, 0x1C, 0x18, 0x7F, 0xFF, 0xFF, 0xE0, - 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x0F, 0x03, 0xF8, 0xFF, 0x38, 0x77, 0x04, 0xFE, - 0x1F, 0xC3, 0xF8, 0x70, 0x6E, 0x1C, 0xFF, 0x0F, 0xC0, 0x60, 0x1C, 0x3F, 0xBF, 0xFC, 0x7E, 0x07, - 0xE1, 0xFC, 0x1F, 0x03, 0xE1, 0xFF, 0xEF, 0xE1, 0xC0, 0xDF, 0x85, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, - 0xDF, 0xB8, 0x01, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x31, 0xCE, 0x01, - 0x1C, 0xE7, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xFD, 0xE4, 0x00, 0x1F, 0xE0, 0x03, 0xFC, 0x00, - 0xFF, 0x80, 0x1C, 0x70, 0x03, 0x8F, 0xE0, 0x71, 0xFF, 0x0E, 0x3F, 0xF1, 0xC7, 0x06, 0x38, 0xE0, - 0xC6, 0x1C, 0x3B, 0xC3, 0xFF, 0x70, 0x7F, 0xC0, 0x40, 0xC0, 0x70, 0xE0, 0x38, 0x70, 0x1C, 0x38, - 0x0E, 0x1C, 0x07, 0xFF, 0xF3, 0xFF, 0xFD, 0xC3, 0x8F, 0xE1, 0xC3, 0xF0, 0xE1, 0xF8, 0x7F, 0xDC, - 0x3F, 0xE0, 0x20, 0x1F, 0xC3, 0xF8, 0x7E, 0x07, 0x70, 0xFF, 0x9F, 0xF3, 0x87, 0x70, 0xEE, 0x1D, - 0xC3, 0xB8, 0x77, 0x0E, 0xE1, 0xDC, 0x3B, 0x87, 0x06, 0x03, 0x80, 0xC0, 0x20, 0x41, 0xF8, 0xFE, - 0x3B, 0x9C, 0xEE, 0x3F, 0x0F, 0xC3, 0xF8, 0xEF, 0x39, 0xCE, 0x3B, 0x87, 0x38, 0x06, 0x01, 0xC0, - 0x30, 0x40, 0xF8, 0x7E, 0x3F, 0x8F, 0xE7, 0xFB, 0xFE, 0xDF, 0xF7, 0xF9, 0xFC, 0x7F, 0x1F, 0x87, - 0x19, 0x83, 0xF0, 0x7C, 0x02, 0x04, 0x07, 0xC1, 0xD8, 0x3B, 0x86, 0x31, 0xC7, 0x38, 0xE6, 0x0D, - 0xC1, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0E, 0x01, 0x80, 0xF0, 0x3C, 0x07, 0x00, 0x40, 0xF8, 0x7E, - 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, - 0x30, 0x40, 0x82, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, - 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0x73, 0xC7, 0x7F, 0xFE, 0x3F, - 0xFE, 0x0C, 0x10, 0x43, 0x0D, 0xC7, 0x1B, 0x8E, 0x37, 0x1C, 0x6E, 0x38, 0xDC, 0x71, 0xB8, 0xE3, - 0x71, 0xC6, 0xE3, 0x8D, 0xC7, 0x19, 0xFF, 0xF3, 0xF7, 0xC1, 0x82, 0x00, 0x10, 0x00, 0xE0, 0x03, - 0x80, 0x3F, 0xE0, 0xFF, 0x83, 0xFC, 0x03, 0xF8, 0x0F, 0xFC, 0x3F, 0xF8, 0xE0, 0xE3, 0x81, 0xCE, - 0x07, 0x38, 0x1C, 0xE0, 0xE3, 0xFF, 0x8F, 0xF8, 0x10, 0x03, 0x80, 0x38, 0x03, 0x80, 0xFF, 0x0F, - 0xF0, 0xFF, 0x03, 0x80, 0x3F, 0x83, 0xFE, 0x3F, 0xF3, 0x87, 0x38, 0x73, 0x87, 0x3F, 0xE3, 0xFC, - 0x40, 0x38, 0x38, 0x3F, 0xCE, 0x3F, 0xFB, 0x8E, 0x0E, 0xE7, 0x01, 0xF9, 0xC0, 0x7E, 0x60, 0x03, - 0xFF, 0xE0, 0xFF, 0xF8, 0x3F, 0xFC, 0x0E, 0x60, 0x03, 0x9C, 0x07, 0xE7, 0x01, 0xF8, 0xE0, 0xEE, - 0x1F, 0xF3, 0x83, 0xF8, 0x00, 0x18, 0x00, 0x40, 0xE1, 0xC7, 0xF3, 0x9F, 0xF7, 0x38, 0x6E, 0xE0, - 0xDF, 0xFC, 0x3F, 0xF8, 0x7F, 0xF0, 0xEE, 0x0D, 0xCE, 0x3B, 0x9F, 0xF7, 0x1F, 0xC0, 0x0E, 0x00, - 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x07, 0x38, 0x18, 0x60, - 0xFF, 0xC3, 0xFF, 0x0F, 0xFC, 0x73, 0x39, 0xCC, 0xE6, 0x31, 0xF8, 0xC7, 0x06, 0x00, 0xE0, 0x0F, - 0x01, 0xF0, 0x1F, 0x81, 0xB8, 0x39, 0x83, 0xFC, 0x3F, 0xC7, 0x6E, 0x66, 0xEE, 0x66, 0x40, 0x30, - 0x38, 0x0E, 0x0E, 0x07, 0x83, 0x81, 0xF0, 0xE0, 0xEC, 0x38, 0x3B, 0x0E, 0x0C, 0xE3, 0x87, 0x18, - 0xE1, 0xC7, 0x3F, 0xFF, 0xCF, 0xFF, 0xF3, 0xFF, 0xFE, 0xE3, 0x3B, 0xB9, 0xCE, 0x6E, 0x73, 0x9F, - 0xB8, 0xE7, 0x40, 0xC1, 0xC1, 0xC3, 0x87, 0x87, 0x0F, 0x8E, 0x3F, 0x1C, 0x76, 0x3F, 0xCE, 0x7F, - 0xFC, 0xFF, 0xFD, 0xDD, 0xBB, 0xBB, 0x37, 0x66, 0x70, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1C, - 0x38, 0x1C, 0x70, 0x0E, 0x70, 0x07, 0xE0, 0x0F, 0xF0, 0x3F, 0xFC, 0x7F, 0xFE, 0xE3, 0x8E, 0xE3, - 0x86, 0xE3, 0x86, 0xE3, 0x86, 0xE3, 0x86, 0xE3, 0x86, 0x3F, 0xF0, 0xFF, 0xC1, 0xFF, 0x07, 0x38, - 0x0E, 0xE0, 0x3F, 0x03, 0xFF, 0x1F, 0xFE, 0x73, 0x9D, 0x8E, 0x76, 0x39, 0xD8, 0xE7, 0x40, 0xFF, - 0xF3, 0x83, 0xFF, 0xCE, 0x0F, 0xFE, 0x38, 0x1C, 0x38, 0xE0, 0x31, 0xC3, 0x80, 0xE7, 0x0E, 0x01, - 0xF8, 0x3F, 0xFF, 0xF0, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xEE, 0x18, 0x63, 0xB8, 0xE1, 0x86, 0xE3, - 0x86, 0x1F, 0x8E, 0x18, 0x7E, 0x38, 0x61, 0xF8, 0xE1, 0x87, 0x41, 0xFF, 0xB8, 0x7F, 0xEE, 0x1F, - 0xF3, 0x83, 0x9C, 0xE0, 0xEE, 0x3F, 0xFF, 0x0F, 0xFF, 0xFB, 0xFF, 0xFF, 0xE7, 0x39, 0xF9, 0xCE, - 0x3E, 0x73, 0x8F, 0x9C, 0xE3, 0x33, 0x1F, 0x87, 0x81, 0x87, 0x07, 0xF1, 0xFC, 0x07, 0x03, 0x81, - 0xC0, 0xE7, 0xE3, 0xF0, 0x7C, 0x0E, 0x07, 0x03, 0x81, 0xCF, 0xFF, 0xEF, 0x87, 0x03, 0x80, 0xE0, - 0x20, 0x00, 0x33, 0x0F, 0xC1, 0xE0, 0x00, 0x7C, 0x1F, 0xC7, 0xF8, 0x06, 0x01, 0x8F, 0xE3, 0xF0, - 0x7E, 0x01, 0x80, 0x63, 0xF9, 0xFC, 0x70, 0x38, 0x07, 0x81, 0xE0, 0x18, 0x00, 0x41, 0x82, 0xE1, - 0x87, 0xE1, 0x87, 0xE1, 0x87, 0xE1, 0x87, 0xE1, 0x87, 0x61, 0x87, 0x61, 0x86, 0x71, 0x8E, 0x3D, - 0xBC, 0x1F, 0xFC, 0x07, 0xF0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x03, 0x00, 0x0C, - 0x04, 0x30, 0xB8, 0xC7, 0xE3, 0x1F, 0x8C, 0x7E, 0x31, 0xF8, 0xC7, 0xE3, 0x1F, 0x8C, 0x7E, 0x31, - 0xDC, 0xCE, 0x7F, 0xF8, 0xFF, 0xC0, 0x78, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x07, - 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFE, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x0F, 0x01, 0xFC, - 0x3F, 0xE7, 0x0E, 0x70, 0x67, 0xE7, 0x7F, 0xF6, 0x3F, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, - 0x00, 0x40, 0x05, 0xC0, 0x39, 0x80, 0x73, 0x81, 0xC7, 0x03, 0x87, 0x06, 0x0E, 0x1C, 0x0C, 0x38, - 0x1C, 0x60, 0x39, 0xC0, 0x33, 0x00, 0x7E, 0x00, 0xFC, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x40, - 0x3E, 0x0F, 0x60, 0xF7, 0x1C, 0x71, 0xC3, 0x18, 0x3B, 0x81, 0xB8, 0x1F, 0x01, 0xF0, 0x0E, 0x00, - 0xE0, 0x1B, 0x80, 0x3B, 0x00, 0x37, 0x00, 0x26, 0x04, 0x00, 0x5C, 0x03, 0x98, 0x07, 0x38, 0x1C, - 0x70, 0x38, 0x70, 0x60, 0xE1, 0xC0, 0xC3, 0x81, 0xC6, 0x03, 0x9C, 0x03, 0x30, 0x07, 0xE0, 0x0F, - 0xC0, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x76, 0x07, 0x70, 0x3B, 0x01, 0xB0, 0x40, 0x3E, 0x0F, - 0x60, 0xF7, 0x1C, 0x71, 0xC3, 0x18, 0x3B, 0x81, 0xB8, 0x1F, 0x01, 0xF0, 0x0E, 0x00, 0xE0, 0x07, - 0x80, 0x00, 0x07, 0xF8, 0x00, 0x03, 0xFF, 0x00, 0x01, 0xE1, 0xE0, 0x00, 0x70, 0x39, 0x80, 0xF8, - 0x07, 0x70, 0x7E, 0x01, 0xCC, 0x1F, 0x00, 0x73, 0x86, 0xC0, 0x1C, 0xE3, 0xB0, 0x07, 0x18, 0xCE, - 0x01, 0xC7, 0x73, 0x80, 0x70, 0xDC, 0x70, 0x38, 0x36, 0x1E, 0x1E, 0x0F, 0x83, 0xFF, 0x01, 0xC0, - 0x7F, 0x80, 0x70, 0x03, 0x00, 0x1C, 0x00, 0x00, 0x06, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x03, 0xC0, - 0x00, 0x00, 0xE0, 0x00, 0x0F, 0x0C, 0x06, 0x3F, 0x9C, 0x1C, 0xFF, 0x98, 0x3B, 0x87, 0x38, 0xE7, - 0x06, 0x71, 0xCC, 0x0E, 0x63, 0x18, 0x1C, 0xEE, 0x30, 0x38, 0xDC, 0x70, 0x61, 0xB0, 0xE1, 0xC3, - 0xE0, 0xFF, 0x03, 0x80, 0xFC, 0x07, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x18, 0x00, 0x01, 0xF0, 0x00, - 0x03, 0xC0, 0x00, 0x07, 0x00, 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0xE0, 0xEE, 0x03, 0xF0, 0x0B, 0x80, - 0x18, 0x00, 0xC0, 0x07, 0x00, 0x38, 0x0E, 0xE0, 0xF7, 0xFF, 0x9F, 0xFC, 0x3E, 0xE0, 0x07, 0x0F, - 0x03, 0xF8, 0xFF, 0x38, 0x76, 0x06, 0xC0, 0x18, 0x03, 0x00, 0x70, 0x0E, 0x00, 0xF8, 0x0F, 0x80, - 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x00, 0xC0, 0x38, 0x66, 0x0F, 0xC0, 0xF8, 0x0F, 0x81, 0xBF, 0x73, - 0xFC, 0x07, 0x80, 0xFC, 0x1B, 0x87, 0x10, 0xC0, 0x00, 0x03, 0xFF, 0xFF, 0xE0, 0x1F, 0x7F, 0xFC, - 0x60, 0xDB, 0xB0, 0x77, 0x64, 0x78, 0x7F, 0xF1, 0xE0, 0x00, 0x70, 0x00, 0x07, 0xC0, 0x00, 0x00, - 0x00, 0x78, 0x07, 0x83, 0x60, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, - 0xF6, 0xC0, 0x06, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0xF8, 0x0F, - 0x86, 0xC0, 0x48, 0x00, 0x70, 0x00, 0x07, 0xC0, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, - 0x00, 0x60, 0x00, 0x86, 0x00, 0x0C, 0x61, 0xC0, 0xC0, 0x38, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x08, 0xF8, 0x00, 0xF1, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, - 0x00, 0xE0, 0x30, 0x1C, 0x23, 0x81, 0x06, 0x18, 0x00, 0x60, 0x00, 0x06, 0x00, 0x00, 0x40, 0x00, - 0x0C, 0xC0, 0x1F, 0x80, 0x3E, 0x00, 0x10, 0x04, 0x01, 0x1C, 0x07, 0x38, 0x1E, 0x70, 0x7C, 0xE0, - 0xF9, 0xC3, 0xF3, 0x8E, 0xE7, 0x1D, 0xCE, 0x73, 0x9C, 0xE7, 0x3B, 0x8E, 0x7E, 0x1C, 0xFC, 0x39, - 0xF0, 0x73, 0xC0, 0xFF, 0x81, 0xF0, 0x00, 0xC0, 0x01, 0x80, 0x07, 0x00, 0x04, 0x33, 0x03, 0xF0, - 0x1E, 0x00, 0x40, 0x40, 0xCE, 0x1C, 0xE3, 0xCE, 0x3C, 0xE7, 0xCE, 0xFC, 0xED, 0xCF, 0xDC, 0xF9, - 0xCF, 0x1C, 0xF1, 0xFE, 0x1F, 0x00, 0x60, 0x0E, 0x00, 0xE0, 0x0C, 0x10, 0x00, 0xE0, 0x03, 0x80, - 0x3F, 0xE0, 0xFF, 0x83, 0xFC, 0x03, 0xF8, 0x0F, 0xFC, 0x3F, 0xF8, 0xE0, 0xE3, 0x81, 0xCE, 0x07, - 0x38, 0x1C, 0xE0, 0xE3, 0xFF, 0x8F, 0xF8, 0x38, 0x03, 0x80, 0xFF, 0x0F, 0xF0, 0xFF, 0x03, 0x80, - 0x3F, 0x83, 0xFE, 0x3F, 0xF3, 0x87, 0x38, 0x73, 0x87, 0x3F, 0xE3, 0xFC, 0x7E, 0x0F, 0xFC, 0xFF, - 0xEE, 0x0E, 0xE0, 0x6E, 0x07, 0xE3, 0x6E, 0x3E, 0xFF, 0xEF, 0xFC, 0xFF, 0xEE, 0x0C, 0xE0, 0x0E, - 0x00, 0xE0, 0x0E, 0x00, 0x4E, 0x1F, 0xF3, 0xFF, 0x70, 0x6E, 0x0F, 0xC1, 0xF8, 0x3F, 0x07, 0xE3, - 0xFE, 0x7B, 0xFF, 0x7F, 0xFE, 0x6D, 0xC0, 0x38, 0x07, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x30, 0x0D, - 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, - 0xE0, 0x38, 0x0E, 0x00, 0x00, 0x07, 0x07, 0x7F, 0xFF, 0xFF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, - 0xE0, 0xE0, 0xE0, 0x1F, 0xF3, 0xFF, 0x3F, 0xF3, 0x80, 0x38, 0x03, 0x80, 0x38, 0x07, 0xF8, 0xFF, - 0x87, 0xF8, 0x38, 0x03, 0x80, 0x38, 0x03, 0x80, 0x38, 0x03, 0x80, 0x3F, 0xBF, 0xDF, 0xEE, 0x07, - 0x07, 0xF3, 0xF9, 0xF8, 0x70, 0x38, 0x1C, 0x0E, 0x00, 0x7F, 0xCF, 0xFC, 0xFF, 0xCE, 0x00, 0xE0, - 0x0E, 0x00, 0xE7, 0x0F, 0xFC, 0xFF, 0xEF, 0x0E, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE1, 0xEE, - 0x7C, 0x07, 0x80, 0x7F, 0x3F, 0xCF, 0xF3, 0x80, 0xE0, 0x3B, 0x0F, 0xF3, 0xFE, 0xE1, 0xB8, 0x6E, - 0x1F, 0x86, 0x03, 0x87, 0xE1, 0xF0, 0x30, 0x60, 0x60, 0x63, 0x83, 0x07, 0x0E, 0x18, 0x70, 0x70, - 0xC3, 0x81, 0xC6, 0x38, 0x0E, 0x31, 0x80, 0x39, 0x9C, 0x00, 0xFF, 0xC0, 0x07, 0xFE, 0x00, 0x7F, - 0xF8, 0x03, 0x19, 0xC0, 0x38, 0xC7, 0x03, 0x86, 0x1C, 0x1C, 0x30, 0xE1, 0xC1, 0x83, 0xDC, 0x0C, - 0x1E, 0x00, 0x00, 0x30, 0x00, 0x01, 0x80, 0x00, 0x0C, 0x00, 0x00, 0x60, 0x61, 0x83, 0x38, 0xE3, - 0x8E, 0x73, 0x87, 0x3B, 0x81, 0xDD, 0x80, 0x7F, 0xC0, 0x1F, 0xC0, 0x1F, 0xF0, 0x1D, 0xDC, 0x1C, - 0xEF, 0x1C, 0x73, 0xEE, 0x38, 0xF0, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x03, 0x0E, 0x07, - 0xF1, 0xFF, 0x70, 0xEE, 0x0C, 0x01, 0x80, 0x70, 0x7C, 0x0F, 0x80, 0x78, 0x03, 0x80, 0x7E, 0x0F, - 0xC1, 0xDF, 0xF1, 0xFC, 0x0E, 0x00, 0xE0, 0x0C, 0x07, 0x80, 0xE0, 0x0E, 0x0F, 0xC7, 0xF9, 0x86, - 0x03, 0x83, 0xE0, 0xF0, 0x0E, 0x01, 0xD8, 0x77, 0xF8, 0xFC, 0x0C, 0x03, 0xC0, 0x30, 0x3C, 0x0C, - 0x00, 0x40, 0x33, 0x81, 0xCE, 0x0E, 0x38, 0x70, 0xE1, 0xC3, 0x8E, 0x0E, 0x70, 0x3F, 0x80, 0xFE, - 0x03, 0xFC, 0x0E, 0x78, 0x38, 0xE0, 0xE1, 0xC3, 0x83, 0x8E, 0x0F, 0xB8, 0x1F, 0x00, 0x1C, 0x00, - 0x70, 0x01, 0xC0, 0x06, 0x41, 0xDC, 0x7B, 0x8E, 0x73, 0x8E, 0xE1, 0xF8, 0x3F, 0x07, 0xF0, 0xEF, - 0x1C, 0xE3, 0x8F, 0xF0, 0xF0, 0x0E, 0x01, 0xC0, 0x38, 0x03, 0x40, 0x37, 0x03, 0xB8, 0x39, 0xC1, - 0xCE, 0x1C, 0x76, 0xE3, 0xFE, 0x1F, 0xF0, 0xFF, 0x07, 0xFC, 0x3F, 0x71, 0xDB, 0x8E, 0x0E, 0x70, - 0x73, 0x81, 0xDC, 0x0E, 0x40, 0x7C, 0x1F, 0x87, 0x7D, 0xCF, 0xB9, 0xFE, 0x3F, 0xC7, 0xF8, 0xFB, - 0x9F, 0x3B, 0x87, 0x70, 0x70, 0x7E, 0x03, 0x3F, 0x83, 0x9F, 0xC1, 0xC0, 0xE1, 0xC0, 0x71, 0xC0, - 0x39, 0xC0, 0x1C, 0xE0, 0x0F, 0xE0, 0x07, 0xF0, 0x03, 0xF8, 0x01, 0xCE, 0x00, 0xE3, 0x80, 0x71, - 0xE0, 0x38, 0x70, 0x1C, 0x1C, 0x0E, 0x07, 0xF8, 0x3F, 0xE3, 0xFF, 0x3C, 0x39, 0xC1, 0xDC, 0x0F, - 0xC0, 0x7E, 0x03, 0xF8, 0x1D, 0xC0, 0xE7, 0x07, 0x1C, 0x38, 0x70, 0x40, 0x11, 0xC0, 0x73, 0x80, - 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0E, 0x7F, 0xFC, 0xFF, 0xF9, 0xC0, 0x73, 0x80, 0xE7, - 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0F, 0x70, 0x1E, 0x00, 0x0C, 0x00, 0x18, 0x00, 0x30, 0x00, - 0x60, 0x40, 0xDC, 0x3B, 0x87, 0x70, 0xEE, 0x1D, 0xFF, 0xBF, 0xF7, 0x0E, 0xE1, 0xDC, 0x3B, 0x87, - 0xF0, 0xF0, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x40, 0x1F, 0xDC, 0x07, 0xFF, 0x80, 0xFF, 0xF0, 0x1C, - 0x0E, 0x03, 0x81, 0xC0, 0x70, 0x38, 0x0E, 0x07, 0xFF, 0xC0, 0xFF, 0xF8, 0x1C, 0x07, 0x03, 0x80, - 0xE0, 0x70, 0x1C, 0x0E, 0x03, 0x81, 0xC0, 0x70, 0x38, 0x0E, 0x07, 0x01, 0xC0, 0x40, 0xFF, 0x0F, - 0xF8, 0x7F, 0xC3, 0x8E, 0x1C, 0x7F, 0xE3, 0xFF, 0x1C, 0x38, 0xE1, 0xC7, 0x0E, 0x38, 0x71, 0xC3, - 0x80, 0x7F, 0xF0, 0x07, 0xFF, 0xC0, 0x3F, 0xFE, 0x01, 0xC0, 0x70, 0x0E, 0x03, 0x80, 0x70, 0x1C, - 0x03, 0x80, 0xEE, 0x1C, 0x07, 0xFC, 0xE0, 0x3F, 0xF7, 0x01, 0xC1, 0xF8, 0x0E, 0x0F, 0xC0, 0x70, - 0x3E, 0x03, 0x81, 0xF0, 0x1C, 0x1F, 0x80, 0xE1, 0xFC, 0x07, 0x7E, 0x00, 0x03, 0xE0, 0x7F, 0xC0, - 0x3F, 0xF0, 0x0F, 0xFC, 0x03, 0x87, 0x00, 0xE1, 0xC0, 0x38, 0x7F, 0x0E, 0x1F, 0xF3, 0x87, 0xFE, - 0xE1, 0xC3, 0xB8, 0x70, 0x6E, 0x1C, 0x1B, 0x87, 0x06, 0x00, 0x03, 0x80, 0x03, 0xC0, 0x00, 0xE0, - 0x06, 0x00, 0x03, 0xC0, 0x00, 0xF8, 0x70, 0x3C, 0x3F, 0x87, 0x0F, 0xF9, 0xC3, 0x87, 0x38, 0x70, - 0x76, 0x0E, 0x0E, 0xC1, 0xC1, 0xD8, 0x38, 0x3B, 0x87, 0x0E, 0x70, 0x71, 0xC7, 0x0F, 0x70, 0xF0, - 0xFC, 0x0F, 0xFF, 0xF0, 0xFF, 0xFF, 0x03, 0x83, 0x80, 0x0C, 0x00, 0x1E, 0x00, 0x3C, 0xF8, 0x71, - 0xFC, 0x73, 0x8E, 0x63, 0x8E, 0x63, 0x0E, 0x63, 0x8E, 0x73, 0x8C, 0x71, 0xFC, 0x3F, 0xFF, 0x1F, - 0xFF, 0x03, 0x8C, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x60, 0x3B, 0x80, 0x6E, 0x00, 0x30, - 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x38, 0x06, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, - 0x00, 0x0E, 0x00, 0x0C, 0x01, 0xE0, 0x03, 0x00, 0x0F, 0x03, 0xF8, 0xFF, 0x38, 0x77, 0x04, 0xC0, - 0x18, 0x03, 0x00, 0x70, 0x6E, 0x1C, 0xFF, 0x0F, 0xC0, 0x60, 0x0F, 0x00, 0x60, 0x3C, 0x06, 0x00, - 0x7F, 0xF3, 0xFF, 0xDF, 0xFE, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, - 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1E, 0x00, 0xF0, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, - 0x00, 0xFF, 0xBF, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xF0, 0x3C, - 0x07, 0x01, 0xC0, 0x70, 0x18, 0x40, 0x0B, 0x80, 0xF7, 0x03, 0x8E, 0x1C, 0x38, 0x70, 0x73, 0x80, - 0xFC, 0x03, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, - 0xC0, 0x40, 0x7C, 0x1D, 0x83, 0xB8, 0x63, 0x1C, 0x73, 0x8E, 0x60, 0xDC, 0x1F, 0x03, 0xE0, 0x3C, - 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x06, 0x00, 0x40, 0x0B, 0x80, 0xF7, 0x03, 0x8E, 0x1C, - 0x38, 0x70, 0x73, 0x80, 0xFC, 0x03, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0x30, 0x07, 0xF8, 0x1F, 0xE0, - 0x7F, 0x80, 0x30, 0x00, 0xC0, 0x40, 0x7C, 0x1D, 0x83, 0xB8, 0x63, 0x1C, 0x73, 0x8E, 0x60, 0xDC, - 0x1F, 0x03, 0xE0, 0x3C, 0x1F, 0xC3, 0xF8, 0x3F, 0x03, 0x80, 0x70, 0x06, 0x00, 0x60, 0x18, 0xE0, - 0x70, 0xE1, 0xE1, 0xC3, 0x81, 0xCE, 0x01, 0xF8, 0x03, 0xF0, 0x03, 0xC0, 0x07, 0x80, 0x0F, 0x80, - 0x3F, 0x00, 0xE7, 0x01, 0xCF, 0x07, 0x0E, 0x1C, 0x0F, 0xF0, 0x0F, 0x00, 0x0E, 0x00, 0x1C, 0x00, - 0x38, 0x00, 0x60, 0x60, 0xC7, 0x1C, 0x31, 0xC3, 0xB8, 0x1F, 0x00, 0xF0, 0x0E, 0x01, 0xF0, 0x1B, - 0x83, 0xB8, 0x71, 0xFE, 0x0F, 0x00, 0x70, 0x07, 0x00, 0x70, 0x03, 0x7F, 0xF1, 0x8F, 0xFF, 0x31, - 0xFF, 0xC6, 0x03, 0x80, 0xC0, 0x70, 0x18, 0x0E, 0x03, 0x01, 0xC0, 0x60, 0x38, 0x0C, 0x07, 0x01, - 0x80, 0xE0, 0x30, 0x1C, 0x06, 0x03, 0x80, 0xC0, 0x70, 0x18, 0x0E, 0x03, 0x01, 0xFF, 0xF8, 0x3F, - 0xFF, 0x00, 0x00, 0xE0, 0x00, 0x1C, 0x00, 0x03, 0x80, 0x00, 0x20, 0xFE, 0x33, 0xFC, 0xCF, 0xF3, - 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xFF, 0xC7, 0xFF, - 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, 0x02, 0x40, 0x23, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, - 0x81, 0xCE, 0x07, 0x3C, 0x1C, 0x7F, 0xF0, 0xFF, 0xC0, 0xF7, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, - 0x07, 0x80, 0x1F, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, 0x06, 0x40, 0x9C, 0x3B, 0x87, 0x70, 0xEE, - 0x1D, 0xC3, 0x9F, 0xF3, 0xFE, 0x1F, 0xC0, 0x38, 0x07, 0x80, 0xF0, 0x06, 0x00, 0xC0, 0x18, 0x03, - 0x40, 0x2E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE6, 0x7F, 0x67, 0x7F, 0xF3, 0xFF, 0x0F, - 0x70, 0x67, 0x06, 0x70, 0x07, 0x00, 0x70, 0x07, 0x40, 0xB8, 0x7E, 0x1F, 0x87, 0xED, 0xFB, 0x77, - 0xFD, 0xFF, 0x1F, 0xC3, 0x70, 0xDC, 0x07, 0x40, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, - 0xEF, 0x8F, 0xFE, 0xFF, 0xFE, 0x07, 0xE0, 0x7E, 0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x3E, 0x03, 0x40, - 0x38, 0x0E, 0x03, 0x80, 0xEE, 0x3F, 0xEF, 0xFB, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, - 0x7E, 0x1F, 0x87, 0x00, 0xF0, 0x01, 0xFE, 0x01, 0xFF, 0x81, 0xE1, 0xE0, 0xC0, 0x76, 0xE0, 0x1B, - 0x70, 0x0D, 0xFF, 0xFF, 0x7F, 0xFF, 0x9F, 0xFF, 0x87, 0x00, 0x03, 0x80, 0x60, 0xE0, 0x70, 0x78, - 0x78, 0x1F, 0xF8, 0x07, 0xF8, 0x00, 0x60, 0x00, 0x01, 0xC0, 0x1F, 0xC0, 0x7F, 0xB3, 0x87, 0xCC, - 0x1F, 0xFF, 0xF7, 0xFF, 0xC7, 0x00, 0x0C, 0x00, 0x38, 0x70, 0x7F, 0x80, 0xFC, 0x00, 0xC0, 0x00, - 0xF0, 0x01, 0xFE, 0x01, 0xFF, 0x81, 0xE1, 0xE0, 0xC0, 0x76, 0xE0, 0x1B, 0x70, 0x0D, 0xFF, 0xFF, - 0x7F, 0xFF, 0x9F, 0xFF, 0x87, 0x00, 0x03, 0x80, 0x60, 0xE0, 0x70, 0x78, 0x78, 0x1F, 0xF8, 0x07, - 0xF8, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x01, 0xC0, 0x1F, 0xC0, 0x7F, 0xB3, - 0x87, 0xCC, 0x1F, 0xFF, 0xF7, 0xFF, 0xC7, 0x00, 0x0C, 0x00, 0x38, 0x70, 0x7F, 0x80, 0xFC, 0x00, - 0xC0, 0x03, 0x80, 0x07, 0x00, 0x78, 0x00, 0xC0, 0x5F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x98, - 0x00, 0x1F, 0x80, 0x01, 0xF0, 0x00, 0x04, 0x00, 0x60, 0x60, 0x67, 0x06, 0x0E, 0x38, 0x61, 0xC3, - 0x86, 0x1C, 0x1C, 0x63, 0x81, 0xC6, 0x30, 0x0E, 0x67, 0x00, 0x7F, 0xE0, 0x07, 0xFE, 0x00, 0xFF, - 0xF0, 0x0C, 0x67, 0x01, 0xC6, 0x38, 0x38, 0x61, 0xC3, 0x86, 0x1C, 0x70, 0x60, 0xEE, 0x06, 0x0E, - 0x06, 0x30, 0x03, 0xF0, 0x00, 0xF8, 0x00, 0x10, 0x06, 0x18, 0x33, 0x8E, 0x38, 0xE7, 0x38, 0x73, - 0xB8, 0x1D, 0xD8, 0x07, 0xFC, 0x01, 0xFC, 0x01, 0xFF, 0x01, 0xDD, 0xC1, 0xCE, 0xF1, 0xC7, 0x38, - 0xE3, 0x8E, 0x40, 0x3E, 0x07, 0xE0, 0xEE, 0x1C, 0xE3, 0x8E, 0x78, 0xE7, 0x0F, 0xF0, 0xFF, 0xCF, - 0xFE, 0xE0, 0xFE, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x00, 0xF0, 0x3E, 0x03, 0xC0, 0x00, - 0x41, 0xDC, 0x7B, 0x8E, 0x73, 0x8E, 0xE1, 0xF8, 0x3F, 0xE7, 0xFE, 0xE1, 0xDC, 0x1B, 0x83, 0x70, - 0x60, 0x3C, 0x0F, 0x00, 0xC0, 0x07, 0xF8, 0x0F, 0xFC, 0x0F, 0xFC, 0x0C, 0x1C, 0x0C, 0x1C, 0x0C, - 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x38, - 0x1C, 0x78, 0x1F, 0x70, 0x1F, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0C, 0x1F, 0xC1, 0xFF, - 0x0F, 0xF8, 0x71, 0xC3, 0x8E, 0x1C, 0x70, 0xE3, 0x87, 0x1C, 0x30, 0xE1, 0x87, 0x3C, 0x3F, 0xC1, - 0xE0, 0x07, 0x00, 0x38, 0x01, 0x80, 0x0C, 0x40, 0x17, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, - 0x1F, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, - 0x07, 0x00, 0x38, 0x01, 0xC0, 0x3E, 0x01, 0xE0, 0x0E, 0x00, 0x40, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, - 0xFF, 0xFF, 0xFF, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0x01, 0xC0, 0x70, 0x3C, 0x1E, 0x07, 0x00, - 0x40, 0x11, 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0E, 0x7F, 0xFC, 0xFF, - 0xF9, 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0F, 0xF0, 0x1F, 0x00, 0x0C, - 0x00, 0x38, 0x00, 0x70, 0x00, 0xC0, 0x40, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCF, 0xFC, 0xFF, - 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xFE, 0x1F, 0x00, 0x60, 0x0E, 0x00, 0xE0, 0x0C, 0x40, 0x23, - 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x3C, 0x1C, 0x7F, 0xF0, 0xFF, 0xC0, - 0xF7, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, 0x07, 0x80, 0x1F, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, - 0x06, 0x40, 0x9C, 0x3B, 0x87, 0x70, 0xEE, 0x1D, 0xC3, 0x9F, 0xF3, 0xFE, 0x1F, 0xC0, 0x38, 0x07, - 0x80, 0xF0, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x60, 0x07, 0x3C, 0x01, 0xCF, 0x00, 0xF3, 0xE0, 0x3C, - 0xF8, 0x1F, 0x3F, 0x07, 0xCF, 0xC1, 0xB3, 0xB0, 0xEC, 0xEE, 0x33, 0x39, 0x9C, 0xCE, 0x77, 0x33, - 0x9D, 0x8C, 0xE3, 0xE3, 0x38, 0xF0, 0xCE, 0x3C, 0x3F, 0x87, 0x0F, 0x00, 0x01, 0xC0, 0x00, 0x60, - 0x00, 0x38, 0x00, 0x04, 0x60, 0x18, 0xF0, 0x3C, 0xF0, 0x7C, 0xF8, 0x7C, 0xF8, 0x7C, 0xFC, 0xFC, - 0xFC, 0xDC, 0xED, 0xDC, 0xEF, 0xDC, 0xE7, 0x9C, 0xE7, 0x9E, 0xE3, 0x1E, 0x00, 0x0E, 0x00, 0x0E, - 0x00, 0x0C, 0x00, 0x0C, 0x5F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0C, 0xC0, 0x3F, 0x00, 0x7C, 0x00, - 0x40, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x07, 0x38, 0x18, - 0x60, 0xE1, 0xC3, 0xFF, 0x0F, 0xFC, 0x70, 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, 0x31, 0x87, 0xE1, - 0xF0, 0x10, 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, 0xD8, 0x77, 0xFC, - 0xFF, 0x0C, 0x00, 0x0C, 0xC0, 0x73, 0x80, 0xCE, 0x00, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, - 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x07, 0x38, 0x18, 0x60, 0xE1, 0xC3, 0xFF, 0x0F, 0xFC, 0x70, - 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, 0x31, 0x8E, 0xE3, 0xB8, 0x00, 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, - 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, 0xD8, 0x77, 0xFC, 0xFF, 0x0C, 0x00, 0x00, 0xFF, 0xE0, 0x1F, - 0xFF, 0x03, 0xFF, 0xF0, 0x3B, 0x00, 0x03, 0x30, 0x00, 0x73, 0x00, 0x06, 0x30, 0x00, 0xE3, 0xFE, - 0x0C, 0x3F, 0xE1, 0xC3, 0xFE, 0x1F, 0xF0, 0x03, 0xFF, 0x00, 0x38, 0x30, 0x07, 0x03, 0x00, 0x70, - 0x3F, 0xFE, 0x03, 0xFF, 0x0E, 0x1E, 0x0F, 0xEF, 0xC7, 0xFF, 0xF9, 0xC3, 0x87, 0x00, 0xC0, 0xC7, - 0xFF, 0xF7, 0xFF, 0xFD, 0xC3, 0x00, 0x60, 0xC0, 0x18, 0x78, 0x77, 0xFF, 0xFC, 0xFC, 0xFE, 0x0C, - 0x0C, 0x00, 0x31, 0x87, 0xF0, 0x7C, 0x02, 0x07, 0xFD, 0xFF, 0xFF, 0xF7, 0x00, 0xE0, 0x1C, 0x03, - 0x80, 0x7F, 0xEF, 0xFD, 0xFF, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, 0xF0, 0x19, 0x83, - 0xF0, 0x3C, 0x00, 0x00, 0xF0, 0x3F, 0x8F, 0xF3, 0x87, 0x60, 0x6F, 0xFD, 0xFF, 0xB0, 0x06, 0x00, - 0xE1, 0xCF, 0xF8, 0xFE, 0x06, 0x00, 0x07, 0x00, 0x7F, 0x87, 0xFF, 0x1C, 0x1C, 0xE0, 0x3B, 0x80, - 0x60, 0x01, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0xB8, 0x0E, 0xE0, 0x39, 0xC1, 0xC3, 0xFE, - 0x07, 0xF0, 0x07, 0x00, 0x0F, 0x07, 0xF1, 0xFF, 0x38, 0x70, 0x06, 0xFF, 0xDF, 0xFB, 0xFF, 0x60, - 0xEE, 0x1C, 0xFF, 0x0F, 0xC0, 0x40, 0x18, 0xC0, 0x77, 0x01, 0xDC, 0x00, 0x00, 0x07, 0x00, 0x7F, - 0x87, 0xFF, 0x1C, 0x1C, 0xE0, 0x3B, 0x80, 0x60, 0x01, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x01, - 0xB8, 0x0E, 0xE0, 0x39, 0xC1, 0xC3, 0xFE, 0x07, 0xF0, 0x07, 0x00, 0x19, 0x87, 0x38, 0x67, 0x00, - 0x00, 0xF0, 0x7F, 0x1F, 0xF3, 0x87, 0x00, 0x6F, 0xFD, 0xFF, 0xBF, 0xF6, 0x0E, 0xE1, 0xCF, 0xF0, - 0xFC, 0x04, 0x00, 0x01, 0x98, 0x00, 0x39, 0xC0, 0x03, 0x98, 0x00, 0x00, 0x00, 0x60, 0x60, 0x67, - 0x06, 0x0E, 0x38, 0x61, 0xC3, 0x86, 0x1C, 0x1C, 0x63, 0x81, 0xC6, 0x30, 0x0E, 0x67, 0x00, 0x7F, - 0xE0, 0x07, 0xFE, 0x00, 0xFF, 0xF0, 0x0C, 0x67, 0x01, 0xC6, 0x38, 0x38, 0x61, 0xC3, 0x86, 0x1C, - 0x70, 0x60, 0xEE, 0x06, 0x0E, 0x06, 0x70, 0x03, 0xB8, 0x01, 0x9C, 0x00, 0x00, 0x06, 0x18, 0x33, - 0x8E, 0x38, 0xE7, 0x38, 0x73, 0xB8, 0x1D, 0xD8, 0x07, 0xFC, 0x01, 0xFC, 0x01, 0xFF, 0x01, 0xDD, - 0xC1, 0xCE, 0xF1, 0xC7, 0x38, 0xE3, 0x8E, 0x31, 0x87, 0x70, 0xEE, 0x00, 0x00, 0xE0, 0x7F, 0x1F, - 0xF7, 0x0E, 0xE0, 0xC0, 0x18, 0x07, 0x07, 0xC0, 0xF8, 0x07, 0x80, 0x38, 0x07, 0xE0, 0xFC, 0x1D, - 0xFF, 0x1F, 0xC0, 0xE0, 0x33, 0x1C, 0xE3, 0x38, 0x00, 0x0E, 0x0F, 0xC7, 0xF9, 0x86, 0x03, 0x83, - 0xE0, 0xF0, 0x0E, 0x01, 0xD8, 0x77, 0xF8, 0xFC, 0x00, 0x00, 0x7F, 0xCF, 0xF9, 0xFF, 0x01, 0xC0, - 0x70, 0x1E, 0x03, 0x80, 0xF8, 0x1F, 0xC0, 0x38, 0x03, 0x80, 0x7E, 0x0F, 0xC1, 0xDF, 0xF1, 0xFC, - 0x0E, 0x00, 0x7F, 0xCF, 0xF9, 0xFF, 0x01, 0xC0, 0x70, 0x1E, 0x03, 0x80, 0xF8, 0x1F, 0xC0, 0x38, - 0x03, 0x80, 0x7E, 0x0F, 0xC1, 0xDF, 0xF1, 0xFC, 0x0E, 0x00, 0x0F, 0xC0, 0x7E, 0x00, 0x00, 0x80, - 0x2E, 0x03, 0xF0, 0x3F, 0x83, 0xFC, 0x1F, 0xE1, 0xFF, 0x1D, 0xF8, 0xEF, 0xCE, 0x7E, 0x73, 0xF7, - 0x1F, 0xF0, 0xFF, 0x87, 0xF8, 0x3F, 0x81, 0xFC, 0x0E, 0x3F, 0x0F, 0xC0, 0x01, 0x03, 0xE1, 0xF8, - 0xFE, 0x3F, 0x9F, 0xEF, 0xFB, 0x7F, 0xDF, 0xE7, 0xF1, 0xFC, 0x7E, 0x1C, 0x0C, 0xC0, 0xEE, 0x07, - 0x30, 0x00, 0x04, 0x01, 0x70, 0x1F, 0x81, 0xFC, 0x1F, 0xE0, 0xFF, 0x0F, 0xF8, 0xEF, 0xC7, 0x7E, - 0x73, 0xF3, 0x9F, 0xB8, 0xFF, 0x87, 0xFC, 0x3F, 0xC1, 0xFC, 0x0F, 0xE0, 0x70, 0x33, 0x1C, 0xE3, - 0x30, 0x00, 0x40, 0xF8, 0x7E, 0x3F, 0x8F, 0xE7, 0xFB, 0xFE, 0xDF, 0xF7, 0xF9, 0xFC, 0x7F, 0x1F, - 0x87, 0x0C, 0xC0, 0x73, 0x80, 0xCC, 0x00, 0x00, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, - 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7E, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, - 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x19, 0x83, 0xDC, 0x19, 0xC0, 0x00, 0x0F, 0x01, 0xFC, 0x3F, - 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, - 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFE, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x0F, 0x01, - 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x67, 0xE7, 0x7F, 0xF6, 0x3F, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, - 0x06, 0x00, 0x0C, 0xC0, 0x73, 0x80, 0xCC, 0x00, 0x00, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, - 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0xF8, 0x07, 0x70, 0x39, - 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x19, 0x83, 0xDC, 0x19, 0xC0, 0x00, 0x0F, 0x01, 0xFC, - 0x3F, 0xE7, 0x0E, 0x70, 0x67, 0xE7, 0x7F, 0xF6, 0x3F, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, - 0x00, 0x19, 0x80, 0xEE, 0x06, 0x70, 0x00, 0x00, 0xF0, 0x1F, 0xE1, 0xFF, 0x9E, 0x0E, 0xE0, 0x3E, - 0x01, 0xC0, 0x06, 0x1F, 0xF0, 0xFF, 0x80, 0x0C, 0x00, 0x78, 0x07, 0xE0, 0x3B, 0x87, 0x9F, 0xF8, - 0x7F, 0x80, 0x60, 0x00, 0x31, 0x87, 0x70, 0xEE, 0x00, 0x00, 0xE0, 0x7F, 0x1F, 0xF3, 0x86, 0x20, - 0xE1, 0xFC, 0x3F, 0x87, 0xF6, 0x0E, 0xE3, 0x9F, 0xF1, 0xFC, 0x0E, 0x00, 0x1F, 0xC0, 0xFC, 0x00, - 0x00, 0x80, 0x2E, 0x03, 0xB8, 0x39, 0xC1, 0xC7, 0x0C, 0x38, 0xE0, 0xC7, 0x07, 0x70, 0x1B, 0x80, - 0xF8, 0x07, 0xC0, 0x1C, 0x00, 0xE0, 0x07, 0x01, 0xF0, 0x0F, 0x00, 0x1F, 0x83, 0xF0, 0x00, 0x20, - 0x3E, 0x0E, 0xC1, 0xDC, 0x31, 0x8E, 0x39, 0xC7, 0x30, 0x6E, 0x0F, 0x81, 0xF0, 0x1E, 0x03, 0x80, - 0x70, 0x0C, 0x07, 0x81, 0xE0, 0x38, 0x00, 0x18, 0xC0, 0xEE, 0x07, 0x70, 0x00, 0x04, 0x01, 0x70, - 0x1D, 0xC1, 0xCE, 0x0E, 0x38, 0x61, 0xC7, 0x06, 0x38, 0x3B, 0x80, 0xDC, 0x07, 0xC0, 0x3E, 0x00, - 0xE0, 0x07, 0x00, 0x38, 0x0F, 0x80, 0x78, 0x00, 0x19, 0x87, 0x70, 0xE6, 0x00, 0x04, 0x07, 0xC1, - 0xD8, 0x3B, 0x86, 0x31, 0xC7, 0x38, 0xE6, 0x0D, 0xC1, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0E, 0x01, - 0x80, 0xF0, 0x3C, 0x07, 0x00, 0x0E, 0xC0, 0x6E, 0x07, 0x60, 0x32, 0x04, 0x01, 0x70, 0x1D, 0xC1, + 0xE0, 0x00, 0x00, 0x36, 0x7E, 0x6C, 0xCC, 0xCE, 0xF9, 0xE0, 0x00, 0xEE, 0x6E, 0x36, 0x32, 0x37, + 0x6E, 0xEC, 0x66, 0x66, 0xEE, 0xC0, 0x66, 0x66, 0xEE, 0xC0, 0x37, 0x76, 0x60, 0xDF, 0x80, 0x33, + 0x76, 0x60, 0x43, 0x1C, 0x6F, 0x30, 0x13, 0x98, 0xF7, 0x9C, 0xCE, 0xF9, 0xE0, 0x00, 0x01, 0x80, + 0xC0, 0xC0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0C, 0x0C, 0x06, 0x06, 0x03, 0x01, 0x00, 0x00, + 0x30, 0x06, 0x00, 0x60, 0x0C, 0x01, 0xC0, 0x18, 0x03, 0x00, 0x30, 0x06, 0x00, 0xC0, 0x0C, 0x01, + 0x80, 0x38, 0x03, 0x00, 0x60, 0x06, 0x00, 0xC0, 0x00, 0x65, 0xFB, 0x70, 0x66, 0xEC, 0x00, 0xFF, + 0xFF, 0xC3, 0xC3, 0xC3, 0xFF, 0xFD, 0xFF, 0xF7, 0xC0, 0xDE, 0xF7, 0xBD, 0x80, 0xFF, 0xCE, 0x71, + 0x00, 0x00, 0x6F, 0xFF, 0xFE, 0xB0, 0x1C, 0x0E, 0x0E, 0xCF, 0xE6, 0xF2, 0xE0, 0x70, 0x10, 0x7F, + 0xFF, 0x7B, 0xFF, 0xC4, 0x33, 0x1F, 0xE7, 0xF9, 0xCE, 0x33, 0x00, 0x00, 0xC7, 0xBF, 0x30, 0xC0, + 0xC0, 0x78, 0x3F, 0x83, 0xE0, 0xF3, 0xFB, 0xE1, 0xC0, 0x3F, 0x76, 0x18, 0x7E, 0x7E, 0x66, 0x18, + 0x18, 0x00, 0x4B, 0xE7, 0x9E, 0xF8, 0x00, 0x00, 0xFF, 0xBC, 0x3C, 0x30, 0x03, 0xC7, 0xCF, 0xF3, + 0x00, 0x00, 0x8F, 0x1C, 0x7D, 0xC3, 0xF6, 0xFB, 0x6C, 0x32, 0x0E, 0x63, 0x6E, 0x00, 0xDF, 0x80, + 0x20, 0xCF, 0x8C, 0x78, 0x00, 0x66, 0x7F, 0xB6, 0x5B, 0xE7, 0xF0, 0x67, 0x3C, 0xE3, 0x00, 0x40, + 0x02, 0x70, 0x0E, 0x3F, 0xFC, 0x0F, 0xF0, 0x40, 0x02, 0x70, 0x0E, 0x3F, 0xFC, 0x0F, 0xF0, 0x7F, + 0xF9, 0xFF, 0xE7, 0xFF, 0x80, 0x7F, 0xF9, 0xFF, 0xE7, 0xFF, 0x80, 0x3E, 0x00, 0x37, 0xFF, 0xFF, + 0xEF, 0xFF, 0xE4, 0x00, 0x08, 0x00, 0x00, 0x1F, 0xF8, 0x3F, 0xFC, 0x70, 0x0E, 0x00, 0x0E, 0x00, + 0x03, 0x9F, 0xFF, 0xE0, 0x00, 0xE0, 0x00, 0xC0, 0x79, 0xF7, 0xFF, 0xFD, 0xF0, 0x79, 0xFF, 0xFF, + 0x7D, 0xE0, 0xFF, 0xFF, 0x7B, 0xFC, 0xF3, 0xFD, 0xE0, 0xCF, 0x3C, 0xF3, 0x7D, 0xF0, 0x79, 0xFC, + 0x30, 0xFD, 0xE0, 0x0C, 0x30, 0xDF, 0xFF, 0x3C, 0xFF, 0x7C, 0x43, 0x0C, 0x3E, 0xFF, 0x3C, 0xF3, + 0xCC, 0xFF, 0x7F, 0xF6, 0x7B, 0x3D, 0x9E, 0xCC, 0xFF, 0xEE, 0xEC, 0x66, 0xF7, 0x66, 0x77, 0xED, + 0xB7, 0x9E, 0x38, 0xC0, 0xD9, 0xE7, 0x1C, 0xFB, 0x60, 0x18, 0x03, 0x80, 0x30, 0x02, 0x07, 0xFD, + 0xFF, 0xFF, 0xF7, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x7F, 0xEF, 0xFD, 0xFF, 0x38, 0x07, 0x00, 0xE0, + 0x1C, 0x03, 0xFF, 0xFF, 0xF0, 0x31, 0x87, 0x70, 0xEE, 0x00, 0x07, 0xFD, 0xFF, 0xFF, 0xF7, 0x00, + 0xE0, 0x1C, 0x03, 0x80, 0x7F, 0xEF, 0xFD, 0xFF, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, + 0xF0, 0x7F, 0xF0, 0x3F, 0xFC, 0x1F, 0xFE, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1D, 0xE0, + 0x0F, 0xFC, 0x07, 0xFF, 0x03, 0xC3, 0x81, 0xC0, 0xE0, 0xE0, 0x70, 0x70, 0x38, 0x38, 0x1C, 0x1C, + 0x3C, 0x0E, 0x7C, 0x00, 0x3C, 0x00, 0x03, 0x01, 0xC0, 0x60, 0x10, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, + 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0x07, + 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x60, 0x3B, 0x80, 0x6E, 0x00, 0x3F, 0xE0, 0xFF, 0x83, 0x80, + 0x0E, 0x00, 0x38, 0x06, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x0E, 0x03, 0xFC, + 0x7F, 0xCE, 0x0E, 0xE0, 0x6E, 0x00, 0xF0, 0x07, 0xE0, 0x3F, 0xC0, 0x7E, 0x00, 0xEC, 0x06, 0xE0, + 0x6E, 0x0E, 0x7F, 0xE3, 0xFC, 0x0E, 0x00, 0x5F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x66, 0xEE, 0xE6, + 0x00, 0x10, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, + 0x38, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x36, 0x0D, 0x83, + 0x60, 0xDC, 0x77, 0xFC, 0xFE, 0x04, 0x00, 0x07, 0xF8, 0x00, 0x1F, 0xF8, 0x00, 0x3F, 0xF0, 0x00, + 0x60, 0xE0, 0x00, 0xC1, 0xC0, 0x01, 0x83, 0x80, 0x07, 0x07, 0xF0, 0x0E, 0x0F, 0xFC, 0x1C, 0x1F, + 0xFC, 0x38, 0x38, 0x38, 0x70, 0x70, 0x30, 0xE0, 0xE0, 0x61, 0x81, 0xC0, 0xC7, 0x03, 0x83, 0x9E, + 0x07, 0xFE, 0x38, 0x0F, 0xF8, 0x40, 0x10, 0x03, 0x80, 0xE0, 0x0E, 0x03, 0x80, 0x38, 0x0E, 0x00, + 0xE0, 0x38, 0x03, 0x80, 0xE0, 0x0E, 0x03, 0xF8, 0x3F, 0xFF, 0xFC, 0xFF, 0xFF, 0xFB, 0x80, 0xE0, + 0xEE, 0x03, 0x81, 0xF8, 0x0E, 0x07, 0xE0, 0x38, 0x1F, 0x80, 0xE0, 0xEE, 0x03, 0xFF, 0xB8, 0x0F, + 0xFC, 0x7F, 0xF8, 0x3F, 0xFC, 0x1F, 0xFE, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0xE0, + 0x0F, 0xFC, 0x07, 0xFF, 0x03, 0xC1, 0xC1, 0xC0, 0xE0, 0xE0, 0x70, 0x70, 0x38, 0x38, 0x1C, 0x1C, + 0x0E, 0x0E, 0x07, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x0C, 0x04, 0x03, 0x70, 0x3B, 0x83, 0x9C, 0x38, + 0xE1, 0xC7, 0x1C, 0x39, 0xC1, 0xFC, 0x0F, 0xE0, 0x7F, 0x83, 0x9E, 0x1C, 0x70, 0xE1, 0xC7, 0x07, + 0x38, 0x3D, 0xC0, 0xE0, 0x0E, 0x00, 0x70, 0x01, 0xC0, 0x06, 0x04, 0x01, 0x70, 0x1F, 0x81, 0xFC, + 0x1F, 0xE0, 0xFF, 0x0F, 0xF8, 0xEF, 0xC7, 0x7E, 0x73, 0xF3, 0x9F, 0xB8, 0xFF, 0x87, 0xFC, 0x3F, + 0xC1, 0xFC, 0x0F, 0xE0, 0x70, 0x18, 0xC0, 0xFE, 0x03, 0xE0, 0x04, 0x04, 0x01, 0x70, 0x1D, 0xC1, 0xCE, 0x0E, 0x38, 0x61, 0xC7, 0x06, 0x38, 0x3B, 0x80, 0xDC, 0x07, 0xC0, 0x3E, 0x00, 0xE0, 0x07, - 0x00, 0x38, 0x0F, 0x80, 0x78, 0x00, 0x1D, 0xC3, 0xF0, 0x6C, 0x09, 0x84, 0x07, 0xC1, 0xD8, 0x3B, - 0x86, 0x31, 0xC7, 0x38, 0xE6, 0x0D, 0xC1, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0E, 0x01, 0x80, 0xF0, - 0x3C, 0x07, 0x00, 0x19, 0x83, 0x9C, 0x19, 0xC0, 0x00, 0x40, 0x2E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, - 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x7F, 0xF3, 0xFF, 0x0F, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, - 0x07, 0x33, 0x1C, 0xE3, 0x30, 0x00, 0x40, 0xB8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x77, 0xFD, 0xFF, - 0x1F, 0xC0, 0x70, 0x1C, 0x07, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, - 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0F, 0x03, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xC0, 0x7F, 0xFF, - 0xFF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xF0, 0xF0, 0x30, 0x30, 0x30, 0x30, 0x06, 0x60, - 0x0E, 0xE0, 0x0E, 0x60, 0x00, 0x00, 0x40, 0x02, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, - 0xE0, 0x07, 0xFE, 0x07, 0xFF, 0xC7, 0xFF, 0xE7, 0xE0, 0xE7, 0xE0, 0x77, 0xE0, 0x77, 0xE0, 0x77, - 0xE0, 0xE7, 0xFF, 0xE7, 0xFF, 0xC7, 0x18, 0xC0, 0xEE, 0x07, 0x70, 0x00, 0x04, 0x01, 0xF0, 0x1F, - 0x80, 0xFC, 0x07, 0xFC, 0x3F, 0xFD, 0xFF, 0xEF, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0xFE, 0xFF, 0xE7, - 0x3F, 0xE3, 0xFF, 0x3F, 0xF3, 0x00, 0x30, 0x03, 0x00, 0x30, 0x0F, 0xF8, 0xFF, 0x8F, 0xF0, 0x30, - 0x03, 0x00, 0x30, 0x03, 0x00, 0x3C, 0x03, 0xC0, 0x0C, 0x00, 0xC0, 0x7C, 0x07, 0x80, 0x70, 0x00, - 0x3F, 0xBF, 0xDF, 0xEE, 0x07, 0x07, 0xF3, 0xF9, 0xF8, 0x70, 0x38, 0x1E, 0x0F, 0x01, 0x80, 0xC1, - 0xE1, 0xF0, 0x60, 0x00, 0x60, 0x18, 0xE0, 0x70, 0xE1, 0xE1, 0xC3, 0x81, 0xCE, 0x01, 0xF8, 0x03, - 0xF0, 0x03, 0xC0, 0x07, 0x80, 0x0F, 0x80, 0x3F, 0x00, 0xE7, 0x01, 0xCF, 0x07, 0x0E, 0x1C, 0x0F, - 0xF0, 0x0F, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0xF8, 0x01, 0xE0, 0x03, 0x80, 0x60, 0xCE, 0x39, 0xC6, - 0x1D, 0xC1, 0xF0, 0x3C, 0x03, 0x80, 0xF8, 0x3F, 0x07, 0x71, 0xC6, 0x70, 0xE0, 0x0C, 0x01, 0x81, - 0xF0, 0x3E, 0x07, 0x00, 0x60, 0x19, 0xC0, 0xE3, 0x87, 0x8E, 0x1C, 0x1C, 0xE0, 0x3F, 0x00, 0xFC, - 0x1F, 0xFC, 0x7F, 0xF8, 0x1F, 0x00, 0xFC, 0x07, 0x38, 0x1C, 0xF0, 0xE1, 0xC7, 0x03, 0xB8, 0x07, - 0x60, 0xCE, 0x38, 0xC7, 0x1D, 0xC1, 0xF0, 0xFF, 0x9F, 0xF0, 0xF8, 0x1B, 0x87, 0x71, 0xC7, 0x70, - 0x70, 0xE0, 0xEE, 0x1C, 0xC3, 0x9C, 0x71, 0xCE, 0x79, 0xDF, 0xF3, 0x3C, 0x63, 0x1C, 0x73, 0x87, - 0x70, 0xEE, 0x0E, 0x7F, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, - 0x30, 0x06, 0x00, 0xCF, 0xFE, 0x7C, 0x0C, 0x0E, 0x0E, 0x0E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x0E, - 0x3F, 0xE7, 0xFF, 0xE0, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, - 0x06, 0x00, 0xC0, 0x18, 0xFF, 0x80, 0x38, 0x03, 0x80, 0x70, 0x06, 0x00, 0xC0, 0x1F, 0x03, 0xE0, - 0x7C, 0x0F, 0x81, 0xF0, 0x3E, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0x18, 0x18, 0x38, 0x38, - 0x38, 0x38, 0x38, 0x38, 0x18, 0x18, 0x18, 0x1C, 0xFF, 0xDC, 0x3F, 0x83, 0xF0, 0x3E, 0x07, 0xC0, - 0xF8, 0x1F, 0x03, 0xE0, 0x7C, 0x0F, 0x81, 0xF0, 0x3E, 0x06, 0xE7, 0xCE, 0x0E, 0xE0, 0x6E, 0x06, - 0xE0, 0x6E, 0x06, 0xE0, 0x6E, 0x06, 0xE0, 0x6E, 0x06, 0x60, 0xE7, 0x9C, 0x3F, 0x80, 0xFF, 0xFF, - 0xF8, 0xFE, 0x03, 0xC0, 0x38, 0x06, 0x01, 0x80, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, - 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0xFF, 0x00, 0x70, 0x07, 0x00, 0x60, 0x0C, 0x01, 0x80, - 0x30, 0x06, 0x00, 0xC0, 0x18, 0x07, 0x01, 0xCF, 0xF0, 0xE0, 0x38, 0x0E, 0x03, 0xFF, 0x01, 0xC0, - 0x60, 0x38, 0x0E, 0x03, 0x01, 0xC0, 0x70, 0x18, 0x0E, 0x03, 0x80, 0xC0, 0x70, 0xFF, 0xDC, 0x3F, - 0x83, 0xF0, 0x3E, 0x07, 0xC0, 0xF8, 0x1F, 0x03, 0xE0, 0x7C, 0x0F, 0x81, 0xF0, 0x3F, 0xFE, 0x6F, - 0xCF, 0xBD, 0xC3, 0xB8, 0x37, 0x06, 0xE0, 0xD8, 0x1B, 0x03, 0x60, 0x7C, 0x0F, 0x81, 0xF0, 0x3C, - 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x78, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, - 0xC7, 0xFC, 0xFF, 0x83, 0x1C, 0x60, 0xEE, 0x06, 0xC0, 0x7C, 0x07, 0xC0, 0x7C, 0x07, 0xE0, 0x7E, - 0x06, 0x60, 0xE3, 0x9C, 0x1F, 0x80, 0x60, 0x6E, 0x0D, 0xC1, 0x98, 0x33, 0x06, 0x70, 0xC6, 0x18, - 0xC3, 0x1C, 0x61, 0x9C, 0x33, 0x07, 0xC3, 0xF1, 0xF8, 0x00, 0xFF, 0x1C, 0xFB, 0x83, 0x70, 0x7E, - 0x0F, 0xC1, 0xF8, 0x3B, 0xC7, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, - 0x00, 0xE0, 0xFF, 0x9C, 0x7B, 0x83, 0xF0, 0x7E, 0x06, 0xC0, 0xDF, 0x18, 0x03, 0x00, 0x60, 0x1C, - 0x03, 0x81, 0xEF, 0xF8, 0xE1, 0xF8, 0x76, 0x1D, 0xC7, 0x71, 0xCC, 0x73, 0x9C, 0xFE, 0x1F, 0x07, - 0x01, 0xC0, 0x30, 0x0C, 0x03, 0x80, 0x60, 0x18, 0x07, 0x00, 0x60, 0xDC, 0x33, 0x8C, 0xE3, 0x1C, - 0xC7, 0xF0, 0xF8, 0x1C, 0x07, 0x00, 0xE0, 0x38, 0x07, 0x7F, 0xC0, 0xFF, 0xF0, 0x07, 0x00, 0x60, - 0x0E, 0x00, 0xE0, 0x0C, 0x01, 0xCE, 0x18, 0xE3, 0x8E, 0x38, 0xE3, 0x0E, 0x70, 0xE7, 0x0E, 0x00, - 0xE0, 0x0E, 0x00, 0xE0, 0x00, 0xFF, 0x00, 0xE0, 0x18, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, - 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0xE3, 0x1F, 0x8C, 0x7E, 0x31, 0xF8, 0xC7, 0xE3, 0x1F, 0x8C, - 0x7F, 0xF1, 0xF8, 0x06, 0xE0, 0x19, 0x80, 0x67, 0x03, 0x8F, 0x3C, 0x1F, 0xE0, 0xFF, 0xC1, 0x8E, - 0x30, 0x63, 0x07, 0x30, 0x77, 0x07, 0x70, 0x77, 0x07, 0x30, 0x73, 0x07, 0x30, 0x73, 0x07, 0xF0, - 0x70, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x07, 0x38, 0x18, - 0x60, 0xE1, 0xC3, 0xFF, 0x0F, 0xFC, 0x70, 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, 0x00, 0x00, 0x0C, - 0x00, 0x38, 0x00, 0xC0, 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, 0xD8, - 0x77, 0xFC, 0xFF, 0x0C, 0x01, 0x80, 0xE0, 0x38, 0x07, 0x80, 0x1E, 0x00, 0x38, 0x00, 0xC0, 0x03, - 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x07, 0x38, 0x18, 0x60, 0xE1, - 0xC3, 0xFF, 0x0F, 0xFC, 0x70, 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, 0x0E, 0x03, 0x80, 0x60, 0x10, - 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, 0xD8, 0x77, 0xFC, 0xFF, 0x0C, - 0x00, 0x00, 0x70, 0x01, 0x80, 0x0E, 0x00, 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x00, 0xC0, 0x07, - 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, 0x06, 0x18, 0x38, 0x70, 0xFF, - 0xC3, 0xFF, 0x1C, 0x0E, 0x70, 0x39, 0x80, 0x7E, 0x01, 0xC0, 0x00, 0xE0, 0x38, 0x06, 0x07, 0x00, - 0xF0, 0x37, 0x0E, 0x60, 0x70, 0x3F, 0x8F, 0xF9, 0xC3, 0x00, 0x61, 0xFC, 0xFF, 0x9E, 0x33, 0x06, - 0x61, 0xCF, 0xF8, 0xFF, 0x06, 0x00, 0x01, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x07, 0x80, 0x3F, + 0x00, 0x38, 0x0F, 0x80, 0x78, 0x00, 0x40, 0x37, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, + 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0xFF, 0xFF, 0xFF, + 0x07, 0x00, 0x38, 0x01, 0xC0, 0x04, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, + 0x3B, 0x00, 0xCE, 0x07, 0x38, 0x18, 0x60, 0xE1, 0xC3, 0xFF, 0x0F, 0xFC, 0x70, 0x39, 0xC0, 0xE6, + 0x01, 0xF8, 0x07, 0x7F, 0xCF, 0xFC, 0xFF, 0xCE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xF8, 0xFF, + 0xCE, 0x0E, 0xE0, 0x6E, 0x07, 0xE0, 0x6E, 0x0E, 0xFF, 0xEF, 0xF8, 0x7E, 0x0F, 0xFC, 0xFF, 0xEE, + 0x0E, 0xE0, 0x6E, 0x06, 0xE0, 0xEF, 0xFC, 0xFF, 0xCF, 0xFE, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x0F, + 0xFF, 0xEF, 0xFC, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, + 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0x07, 0xFC, 0x0F, 0xFE, 0x0F, 0xFE, 0x0E, 0x0E, 0x0E, + 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0C, 0x0E, 0x0C, 0x0E, 0x0C, 0x0E, 0x1C, 0x0E, 0x1C, 0x0E, 0x1C, + 0x0E, 0x38, 0x0E, 0x7F, 0xFF, 0x7F, 0xFF, 0x60, 0x03, 0x60, 0x03, 0x60, 0x03, 0x60, 0x03, 0x7F, + 0xDF, 0xFF, 0xFF, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0xFE, 0xFF, 0xDF, 0xF3, 0x80, 0x70, 0x0E, + 0x01, 0xC0, 0x3F, 0xFF, 0xFF, 0x60, 0x60, 0x67, 0x06, 0x0E, 0x38, 0x61, 0xC3, 0x86, 0x1C, 0x1C, + 0x63, 0x81, 0xC6, 0x30, 0x0E, 0x67, 0x00, 0x7F, 0xE0, 0x07, 0xFE, 0x00, 0xFF, 0xF0, 0x0C, 0x67, + 0x01, 0xC6, 0x38, 0x38, 0x61, 0xC3, 0x86, 0x1C, 0x70, 0x60, 0xEE, 0x06, 0x0E, 0x0E, 0x07, 0xF1, + 0xFF, 0x70, 0xEE, 0x0C, 0x01, 0x80, 0x70, 0x7C, 0x0F, 0x80, 0x78, 0x03, 0x80, 0x7E, 0x0F, 0xC1, + 0xDF, 0xF1, 0xFC, 0x0E, 0x00, 0x40, 0x17, 0x01, 0xF8, 0x1F, 0xC1, 0xFE, 0x0F, 0xF0, 0xFF, 0x8E, + 0xFC, 0x77, 0xE7, 0x3F, 0x39, 0xFB, 0x8F, 0xF8, 0x7F, 0xC3, 0xFC, 0x1F, 0xC0, 0xFE, 0x07, 0x0C, + 0xC0, 0x7E, 0x03, 0xE0, 0x04, 0x04, 0x01, 0x70, 0x1F, 0x81, 0xFC, 0x1F, 0xE0, 0xFF, 0x0F, 0xF8, + 0xEF, 0xC7, 0x7E, 0x73, 0xF3, 0x9F, 0xB8, 0xFF, 0x87, 0xFC, 0x3F, 0xC1, 0xFC, 0x0F, 0xE0, 0x70, + 0x40, 0x37, 0x03, 0xB8, 0x39, 0xC3, 0x8E, 0x1C, 0x71, 0xC3, 0x9C, 0x1F, 0xC0, 0xFE, 0x07, 0xF8, + 0x39, 0xE1, 0xC7, 0x0E, 0x1C, 0x70, 0x73, 0x83, 0xDC, 0x0E, 0x07, 0xF8, 0x3F, 0xF0, 0xFF, 0xC3, + 0x07, 0x0C, 0x1C, 0x30, 0x71, 0xC1, 0xC7, 0x07, 0x1C, 0x1C, 0x70, 0x71, 0xC1, 0xC7, 0x07, 0x1C, + 0x1C, 0xE0, 0x77, 0x81, 0xDC, 0x07, 0x60, 0x07, 0xF0, 0x07, 0xF0, 0x0F, 0xF8, 0x0F, 0xF8, 0x1F, + 0xFC, 0x1F, 0xFC, 0x1B, 0xEC, 0x3B, 0xEE, 0x33, 0xE6, 0x73, 0xE7, 0x73, 0xE7, 0x63, 0xE3, 0xE3, + 0xE3, 0xC3, 0xE3, 0xC3, 0xE1, 0xC3, 0x40, 0x17, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, + 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, + 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, + 0x00, 0x7E, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x7F, 0xF7, + 0xFF, 0xFF, 0xFF, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, + 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0x7E, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E, 0xE0, 0x6E, + 0x07, 0xE0, 0x6E, 0x0E, 0xFF, 0xEF, 0xFC, 0xFF, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, + 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x60, 0x3B, 0x80, 0x6E, 0x00, 0x30, 0x00, 0xC0, 0x03, + 0x00, 0x0E, 0x00, 0x38, 0x06, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x7F, 0xF3, + 0xFF, 0xDF, 0xFE, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, + 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x40, 0x17, 0x01, 0xDC, 0x1C, 0xE0, 0xE3, 0x86, + 0x1C, 0x70, 0x63, 0x83, 0xB8, 0x0D, 0xC0, 0x7C, 0x03, 0xE0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0xF8, + 0x07, 0x80, 0x03, 0x80, 0x07, 0x00, 0x7F, 0xC1, 0xFF, 0xC7, 0xB9, 0xDC, 0x71, 0xF8, 0xE3, 0xE1, + 0xC3, 0xC3, 0x87, 0x87, 0x0F, 0x8E, 0x3F, 0x1C, 0x77, 0xBB, 0xC7, 0xFF, 0x07, 0xFC, 0x01, 0xC0, + 0x03, 0x80, 0x60, 0x19, 0xC0, 0xE3, 0x87, 0x8E, 0x1C, 0x1C, 0xE0, 0x3F, 0x00, 0xFC, 0x01, 0xE0, + 0x07, 0x80, 0x1F, 0x00, 0xFC, 0x07, 0x38, 0x1C, 0xF0, 0xE1, 0xC7, 0x03, 0xB8, 0x07, 0x40, 0x33, + 0x80, 0xEE, 0x03, 0xB8, 0x0E, 0xE0, 0x3B, 0x80, 0xEE, 0x03, 0xB8, 0x0E, 0xE0, 0x3B, 0x80, 0xEE, + 0x03, 0xB8, 0x0E, 0xE0, 0x3B, 0x80, 0xEF, 0xFF, 0xFF, 0xFF, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, + 0x03, 0x40, 0x2E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x7F, 0xF3, 0xFF, + 0x0F, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x40, 0xC1, 0xB8, 0x30, 0x6E, 0x0C, 0x1B, + 0x83, 0x06, 0xE0, 0xC1, 0xB8, 0x30, 0x6E, 0x0C, 0x1B, 0x83, 0x06, 0xE0, 0xC1, 0xB8, 0x30, 0x6E, + 0x0C, 0x1B, 0x83, 0x06, 0xE0, 0xC1, 0xB8, 0x30, 0x6F, 0xFF, 0xFB, 0xFF, 0xFE, 0x40, 0xC1, 0x9C, + 0x18, 0x33, 0x83, 0x06, 0x70, 0x60, 0xCE, 0x0C, 0x19, 0xC1, 0x83, 0x38, 0x30, 0x67, 0x06, 0x0C, + 0xE0, 0xC1, 0x9C, 0x18, 0x33, 0x83, 0x06, 0x70, 0x60, 0xCE, 0x0C, 0x19, 0xC1, 0x83, 0x3F, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xE0, 0x00, 0x1C, 0x00, 0x03, 0x80, 0x00, 0x20, 0x7E, 0x00, 0x7F, + 0x00, 0x7F, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0xE0, 0x07, 0xFC, 0x07, 0xFE, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x03, 0x07, 0x07, 0x07, 0x07, 0x07, 0xFE, 0x07, 0xFC, 0x40, 0x02, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xFE, 0x07, 0xFF, 0xC7, 0xFF, 0xE7, 0xE0, + 0xE7, 0xE0, 0x77, 0xE0, 0x77, 0xE0, 0x77, 0xE0, 0xE7, 0xFF, 0xE7, 0xFF, 0xC7, 0x40, 0x0E, 0x00, + 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFE, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E, 0xE0, 0x7E, 0x07, 0xE0, + 0x7E, 0x0E, 0xFF, 0xEF, 0xFC, 0x0F, 0x01, 0xFE, 0x1F, 0xF9, 0xE0, 0xEE, 0x03, 0xE0, 0x1C, 0x00, + 0x61, 0xFF, 0x0F, 0xF8, 0x00, 0xC0, 0x07, 0x80, 0x7E, 0x03, 0xB8, 0x79, 0xFF, 0x87, 0xF8, 0x06, + 0x00, 0x40, 0x3C, 0x1C, 0x1F, 0xE3, 0x87, 0xFE, 0x71, 0xE0, 0xEE, 0x38, 0x0D, 0xCE, 0x01, 0xF9, + 0xC0, 0x3F, 0xF8, 0x03, 0xFF, 0x00, 0x7F, 0xE0, 0x0F, 0x9C, 0x03, 0xF3, 0x80, 0x7E, 0x38, 0x1D, + 0xC7, 0x87, 0xB8, 0x7F, 0xE7, 0x07, 0xF8, 0x00, 0x18, 0x00, 0x03, 0xE1, 0xFF, 0x3F, 0xF7, 0x07, + 0x70, 0x76, 0x07, 0x70, 0x77, 0x07, 0x3F, 0xF1, 0xFF, 0x1C, 0x71, 0xC7, 0x38, 0x73, 0x87, 0x70, + 0x77, 0x07, 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, 0xD8, 0x77, 0xFC, + 0xFF, 0x0C, 0x00, 0x00, 0x40, 0x0E, 0x0F, 0xC3, 0xF8, 0x38, 0x07, 0xF8, 0x7F, 0xC7, 0x8E, 0x70, + 0x66, 0x06, 0x60, 0x76, 0x06, 0x60, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x7C, 0x3F, 0xEF, + 0xFB, 0x87, 0xE1, 0xBF, 0xEF, 0xFB, 0x87, 0xE1, 0xF8, 0x7F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xE0, + 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x31, 0xC1, 0x8E, + 0x1C, 0x70, 0xE3, 0x87, 0x1C, 0x38, 0xE3, 0x87, 0x3F, 0xFF, 0xFF, 0xFE, 0x03, 0xF0, 0x1F, 0x80, + 0xF8, 0x06, 0x0F, 0x03, 0xF8, 0xFF, 0x38, 0x76, 0x06, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0E, 0x1C, + 0xFF, 0x8F, 0xE0, 0x60, 0x61, 0x83, 0x38, 0xE3, 0x8E, 0x73, 0x87, 0x3B, 0x81, 0xDD, 0x80, 0x7F, + 0xC0, 0x1F, 0xC0, 0x1F, 0xF0, 0x1D, 0xDC, 0x1C, 0xEF, 0x1C, 0x73, 0x8E, 0x38, 0xE0, 0x0E, 0x0F, + 0xC7, 0xF9, 0x86, 0x03, 0x83, 0xE0, 0xF0, 0x0E, 0x01, 0xD8, 0x77, 0xF8, 0xFC, 0x00, 0x00, 0x40, + 0xF8, 0x7E, 0x3F, 0x8F, 0xE7, 0xFB, 0xFE, 0xDF, 0xF7, 0xF9, 0xFC, 0x7F, 0x1F, 0x87, 0x33, 0x0F, + 0xC1, 0xE0, 0x10, 0x40, 0xF8, 0x7E, 0x3F, 0x8F, 0xE7, 0xFB, 0xFE, 0xDF, 0xF7, 0xF9, 0xFC, 0x7F, + 0x1F, 0x87, 0x41, 0xF8, 0xFE, 0x3B, 0x9C, 0xEE, 0x3F, 0x0F, 0xC3, 0xF8, 0xEF, 0x39, 0xCE, 0x3B, + 0x87, 0x1F, 0xC7, 0xFC, 0xFF, 0x9C, 0x73, 0x8E, 0x71, 0xCE, 0x39, 0xC7, 0x30, 0xE6, 0x1F, 0xC3, + 0xF0, 0x70, 0x60, 0x1B, 0xC0, 0xFF, 0x07, 0xFE, 0x1F, 0xF8, 0x7F, 0xF3, 0xFF, 0xCD, 0xFB, 0x77, + 0xEF, 0xDF, 0x9E, 0x7E, 0x79, 0xF8, 0xC7, 0x40, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xFF, 0xFF, 0xFF, + 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, + 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x87, 0xE1, + 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0x4E, 0x1F, 0xF3, 0xFF, 0x70, 0x6E, 0x0F, + 0xC1, 0xF8, 0x3F, 0x07, 0xE0, 0xFE, 0x3B, 0xFF, 0x7F, 0xCE, 0x61, 0xC0, 0x38, 0x07, 0x00, 0x40, + 0x00, 0x0F, 0x03, 0xF8, 0xFF, 0x38, 0x77, 0x04, 0xC0, 0x18, 0x03, 0x00, 0x70, 0x6E, 0x1C, 0xFF, + 0x0F, 0xC0, 0x60, 0xFF, 0xBF, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, + 0xC0, 0x30, 0x40, 0x7C, 0x1D, 0x83, 0xB8, 0x63, 0x1C, 0x73, 0x8E, 0x60, 0xDC, 0x1F, 0x03, 0xE0, + 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x0F, 0x03, 0xC0, 0x70, 0x00, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, + 0xC0, 0x0F, 0xC0, 0x7F, 0x83, 0xFF, 0x1C, 0xCE, 0x63, 0x19, 0x8C, 0x66, 0x31, 0xD8, 0xC6, 0x73, + 0x19, 0xCC, 0xE3, 0xFF, 0x07, 0xF8, 0x07, 0x80, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x60, + 0xCE, 0x38, 0xC7, 0x1D, 0xC1, 0xF0, 0x1E, 0x03, 0x80, 0xF8, 0x1B, 0x87, 0x71, 0xC7, 0x70, 0x70, + 0x40, 0xDC, 0x3B, 0x87, 0x70, 0xEE, 0x1D, 0xC3, 0xB8, 0x77, 0x0E, 0xE1, 0xDC, 0x3B, 0xFF, 0xFF, + 0xF0, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x40, 0xB8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x77, 0xFD, 0xFF, + 0x1F, 0xC0, 0x70, 0x1C, 0x07, 0x41, 0x05, 0xC7, 0x1F, 0x8E, 0x3F, 0x1C, 0x7E, 0x38, 0xFC, 0x71, + 0xF8, 0xE3, 0xF1, 0xC7, 0xE3, 0x8F, 0xC7, 0x1F, 0xFF, 0xFF, 0xFF, 0xF0, 0x41, 0x04, 0xE3, 0x8E, + 0xE3, 0x8E, 0xE3, 0x8E, 0xE3, 0x8E, 0xE3, 0x8E, 0xE3, 0x8E, 0xE3, 0x8E, 0xE3, 0x8E, 0xE3, 0x8E, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0xF8, 0x07, 0xE0, 0x3F, + 0x00, 0x38, 0x01, 0xF8, 0x0F, 0xF0, 0x7F, 0xC3, 0x87, 0x1C, 0x38, 0xE1, 0xC7, 0xFC, 0x3F, 0xC0, + 0x40, 0x1F, 0x01, 0xF8, 0x0F, 0xC0, 0x7F, 0xC3, 0xFF, 0xDF, 0xFE, 0xFC, 0x3F, 0xE1, 0xFF, 0x0F, + 0xFF, 0xEF, 0xFE, 0x70, 0x40, 0x38, 0x0E, 0x03, 0x80, 0xFC, 0x3F, 0xEF, 0xFB, 0x87, 0xE1, 0xF8, + 0x7F, 0xFB, 0xFC, 0x0E, 0x07, 0xF1, 0xFF, 0x38, 0x62, 0x0E, 0x1F, 0xC3, 0xF8, 0x7F, 0x60, 0xEE, + 0x39, 0xFF, 0x1F, 0xC0, 0xE0, 0x40, 0xF0, 0xE1, 0xFC, 0xE3, 0xFE, 0xE7, 0x0E, 0xE7, 0x06, 0xFE, + 0x07, 0xFE, 0x07, 0xFE, 0x07, 0xE7, 0x06, 0xE7, 0x0E, 0xE3, 0xFC, 0xE1, 0xF8, 0x00, 0x60, 0x0F, + 0xCF, 0xF7, 0xFD, 0x83, 0x60, 0xDC, 0x37, 0xFC, 0xFF, 0x38, 0xCC, 0x37, 0x0F, 0x83, 0x1C, 0x01, + 0x80, 0x38, 0x03, 0x00, 0xF0, 0x3F, 0x8F, 0xF3, 0x87, 0x60, 0x6F, 0xFD, 0xFF, 0xB0, 0x06, 0x00, + 0xE1, 0xCF, 0xF8, 0xFE, 0x06, 0x00, 0x19, 0x87, 0x38, 0x66, 0x00, 0x00, 0xF0, 0x3F, 0x8F, 0xF3, + 0x87, 0x60, 0x6F, 0xFD, 0xFF, 0xB0, 0x06, 0x00, 0xE1, 0xCF, 0xF8, 0xFE, 0x06, 0x00, 0x20, 0x0E, + 0x03, 0xF8, 0x7F, 0x07, 0x70, 0xFF, 0x9F, 0xF3, 0x87, 0x70, 0xEE, 0x1D, 0xC3, 0xB8, 0x77, 0x0E, + 0xE1, 0xDC, 0x3B, 0x87, 0x00, 0xE0, 0x1C, 0x07, 0x81, 0xE0, 0x18, 0x0E, 0x0C, 0x1C, 0x18, 0x7F, + 0xFF, 0xFF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x0F, 0x03, 0xF8, 0xFF, 0x38, + 0x77, 0x04, 0xFE, 0x1F, 0xC3, 0xF8, 0x70, 0x6E, 0x1C, 0xFF, 0x0F, 0xC0, 0x60, 0x1C, 0x3F, 0xBF, + 0xFC, 0x7E, 0x07, 0xE1, 0xFC, 0x1F, 0x03, 0xE1, 0xFF, 0xEF, 0xE1, 0xC0, 0xDF, 0x85, 0xFF, 0xFF, + 0xFF, 0xFF, 0xC7, 0xDF, 0xB8, 0x01, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, + 0x31, 0xCE, 0x01, 0x1C, 0xE7, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xFD, 0xE4, 0x00, 0x1F, 0xE0, + 0x03, 0xFC, 0x00, 0xFF, 0x80, 0x1C, 0x70, 0x03, 0x8F, 0xE0, 0x71, 0xFF, 0x0E, 0x3F, 0xF1, 0xC7, + 0x06, 0x38, 0xE0, 0xC6, 0x1C, 0x3B, 0xC3, 0xFF, 0x70, 0x7F, 0xC0, 0x40, 0xC0, 0x70, 0xE0, 0x38, + 0x70, 0x1C, 0x38, 0x0E, 0x1C, 0x07, 0xFF, 0xF3, 0xFF, 0xFD, 0xC3, 0x8F, 0xE1, 0xC3, 0xF0, 0xE1, + 0xF8, 0x7F, 0xDC, 0x3F, 0xE0, 0x20, 0x1F, 0xC3, 0xF8, 0x7E, 0x07, 0x70, 0xFF, 0x9F, 0xF3, 0x87, + 0x70, 0xEE, 0x1D, 0xC3, 0xB8, 0x77, 0x0E, 0xE1, 0xDC, 0x3B, 0x87, 0x06, 0x03, 0x80, 0xC0, 0x20, + 0x41, 0xF8, 0xFE, 0x3B, 0x9C, 0xEE, 0x3F, 0x0F, 0xC3, 0xF8, 0xEF, 0x39, 0xCE, 0x3B, 0x87, 0x38, + 0x06, 0x01, 0xC0, 0x30, 0x40, 0xF8, 0x7E, 0x3F, 0x8F, 0xE7, 0xFB, 0xFE, 0xDF, 0xF7, 0xF9, 0xFC, + 0x7F, 0x1F, 0x87, 0x19, 0x83, 0xF0, 0x7C, 0x02, 0x04, 0x07, 0xC1, 0xD8, 0x3B, 0x86, 0x31, 0xC7, + 0x38, 0xE6, 0x0D, 0xC1, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0E, 0x01, 0x80, 0xF0, 0x3C, 0x07, 0x00, + 0x40, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7F, 0xFF, 0xFF, 0x0C, + 0x03, 0x00, 0xC0, 0x30, 0x40, 0x82, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, + 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0x73, 0xC7, + 0x7F, 0xFE, 0x3F, 0xFE, 0x0C, 0x10, 0x43, 0x0D, 0xC7, 0x1B, 0x8E, 0x37, 0x1C, 0x6E, 0x38, 0xDC, + 0x71, 0xB8, 0xE3, 0x71, 0xC6, 0xE3, 0x8D, 0xC7, 0x19, 0xFF, 0xF3, 0xF7, 0xC1, 0x82, 0x00, 0x10, + 0x00, 0xE0, 0x03, 0x80, 0x3F, 0xE0, 0xFF, 0x83, 0xFC, 0x03, 0xF8, 0x0F, 0xFC, 0x3F, 0xF8, 0xE0, + 0xE3, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0xE3, 0xFF, 0x8F, 0xF8, 0x10, 0x03, 0x80, 0x38, 0x03, + 0x80, 0xFF, 0x0F, 0xF0, 0xFF, 0x03, 0x80, 0x3F, 0x83, 0xFE, 0x3F, 0xF3, 0x87, 0x38, 0x73, 0x87, + 0x3F, 0xE3, 0xFC, 0x40, 0x38, 0x38, 0x3F, 0xCE, 0x3F, 0xFB, 0x8E, 0x0E, 0xE7, 0x01, 0xF9, 0xC0, + 0x7E, 0x60, 0x03, 0xFF, 0xE0, 0xFF, 0xF8, 0x3F, 0xFC, 0x0E, 0x60, 0x03, 0x9C, 0x07, 0xE7, 0x01, + 0xF8, 0xE0, 0xEE, 0x1F, 0xF3, 0x83, 0xF8, 0x00, 0x18, 0x00, 0x40, 0xE1, 0xC7, 0xF3, 0x9F, 0xF7, + 0x38, 0x6E, 0xE0, 0xDF, 0xFC, 0x3F, 0xF8, 0x7F, 0xF0, 0xEE, 0x0D, 0xCE, 0x3B, 0x9F, 0xF7, 0x1F, + 0xC0, 0x0E, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x07, + 0x38, 0x18, 0x60, 0xFF, 0xC3, 0xFF, 0x0F, 0xFC, 0x73, 0x39, 0xCC, 0xE6, 0x31, 0xF8, 0xC7, 0x06, + 0x00, 0xE0, 0x0F, 0x01, 0xF0, 0x1F, 0x81, 0xB8, 0x39, 0x83, 0xFC, 0x3F, 0xC7, 0x6E, 0x66, 0xEE, + 0x66, 0x40, 0x30, 0x38, 0x0E, 0x0E, 0x07, 0x83, 0x81, 0xF0, 0xE0, 0xEC, 0x38, 0x3B, 0x0E, 0x0C, + 0xE3, 0x87, 0x18, 0xE1, 0xC7, 0x3F, 0xFF, 0xCF, 0xFF, 0xF3, 0xFF, 0xFE, 0xE3, 0x3B, 0xB9, 0xCE, + 0x6E, 0x73, 0x9F, 0xB8, 0xE7, 0x40, 0xC1, 0xC1, 0xC3, 0x87, 0x87, 0x0F, 0x8E, 0x3F, 0x1C, 0x76, + 0x3F, 0xCE, 0x7F, 0xFC, 0xFF, 0xFD, 0xDD, 0xBB, 0xBB, 0x37, 0x66, 0x70, 0x3F, 0xFC, 0x3F, 0xFC, + 0x3F, 0xF8, 0x1C, 0x38, 0x1C, 0x70, 0x0E, 0x70, 0x07, 0xE0, 0x0F, 0xF0, 0x3F, 0xFC, 0x7F, 0xFE, + 0xE3, 0x8E, 0xE3, 0x86, 0xE3, 0x86, 0xE3, 0x86, 0xE3, 0x86, 0xE3, 0x86, 0x3F, 0xF0, 0xFF, 0xC1, + 0xFF, 0x07, 0x38, 0x0E, 0xE0, 0x3F, 0x03, 0xFF, 0x1F, 0xFE, 0x73, 0x9D, 0x8E, 0x76, 0x39, 0xD8, + 0xE7, 0x40, 0xFF, 0xF3, 0x83, 0xFF, 0xCE, 0x0F, 0xFE, 0x38, 0x1C, 0x38, 0xE0, 0x31, 0xC3, 0x80, + 0xE7, 0x0E, 0x01, 0xF8, 0x3F, 0xFF, 0xF0, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xEE, 0x18, 0x63, 0xB8, + 0xE1, 0x86, 0xE3, 0x86, 0x1F, 0x8E, 0x18, 0x7E, 0x38, 0x61, 0xF8, 0xE1, 0x87, 0x41, 0xFF, 0xB8, + 0x7F, 0xEE, 0x1F, 0xF3, 0x83, 0x9C, 0xE0, 0xEE, 0x3F, 0xFF, 0x0F, 0xFF, 0xFB, 0xFF, 0xFF, 0xE7, + 0x39, 0xF9, 0xCE, 0x3E, 0x73, 0x8F, 0x9C, 0xE3, 0x33, 0x1F, 0x87, 0x81, 0x87, 0x07, 0xF1, 0xFC, + 0x07, 0x03, 0x81, 0xC0, 0xE7, 0xE3, 0xF0, 0x7C, 0x0E, 0x07, 0x03, 0x81, 0xCF, 0xFF, 0xEF, 0x87, + 0x03, 0x80, 0xE0, 0x20, 0x00, 0x33, 0x0F, 0xC1, 0xE0, 0x00, 0x7C, 0x1F, 0xC7, 0xF8, 0x06, 0x01, + 0x8F, 0xE3, 0xF0, 0x7E, 0x01, 0x80, 0x63, 0xF9, 0xFC, 0x70, 0x38, 0x07, 0x81, 0xE0, 0x18, 0x00, + 0x41, 0x82, 0xE1, 0x87, 0xE1, 0x87, 0xE1, 0x87, 0xE1, 0x87, 0xE1, 0x87, 0x61, 0x87, 0x61, 0x86, + 0x71, 0x8E, 0x3D, 0xBC, 0x1F, 0xFC, 0x07, 0xF0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, + 0x03, 0x00, 0x0C, 0x04, 0x30, 0xB8, 0xC7, 0xE3, 0x1F, 0x8C, 0x7E, 0x31, 0xF8, 0xC7, 0xE3, 0x1F, + 0x8C, 0x7E, 0x31, 0xDC, 0xCE, 0x7F, 0xF8, 0xFF, 0xC0, 0x78, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, + 0x30, 0x00, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, + 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x67, 0xE7, 0x7F, 0xF6, 0x3F, 0x70, 0x67, 0x0E, 0x3F, + 0xC1, 0xF8, 0x06, 0x00, 0x40, 0x05, 0xC0, 0x39, 0x80, 0x73, 0x81, 0xC7, 0x03, 0x87, 0x06, 0x0E, + 0x1C, 0x0C, 0x38, 0x1C, 0x60, 0x39, 0xC0, 0x33, 0x00, 0x7E, 0x00, 0xFC, 0x00, 0xF0, 0x01, 0xE0, + 0x03, 0xC0, 0x40, 0x3E, 0x0F, 0x60, 0xF7, 0x1C, 0x71, 0xC3, 0x18, 0x3B, 0x81, 0xB8, 0x1F, 0x01, + 0xF0, 0x0E, 0x00, 0xE0, 0x1B, 0x80, 0x3B, 0x00, 0x37, 0x00, 0x26, 0x04, 0x00, 0x5C, 0x03, 0x98, + 0x07, 0x38, 0x1C, 0x70, 0x38, 0x70, 0x60, 0xE1, 0xC0, 0xC3, 0x81, 0xC6, 0x03, 0x9C, 0x03, 0x30, + 0x07, 0xE0, 0x0F, 0xC0, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x76, 0x07, 0x70, 0x3B, 0x01, 0xB0, + 0x40, 0x3E, 0x0F, 0x60, 0xF7, 0x1C, 0x71, 0xC3, 0x18, 0x3B, 0x81, 0xB8, 0x1F, 0x01, 0xF0, 0x0E, + 0x00, 0xE0, 0x07, 0x80, 0x00, 0x07, 0xF8, 0x00, 0x03, 0xFF, 0x00, 0x01, 0xE1, 0xE0, 0x00, 0x70, + 0x39, 0x80, 0xF8, 0x07, 0x70, 0x7E, 0x01, 0xCC, 0x1F, 0x00, 0x73, 0x86, 0xC0, 0x1C, 0xE3, 0xB0, + 0x07, 0x18, 0xCE, 0x01, 0xC7, 0x73, 0x80, 0x70, 0xDC, 0x70, 0x38, 0x36, 0x1E, 0x1E, 0x0F, 0x83, + 0xFF, 0x01, 0xC0, 0x7F, 0x80, 0x70, 0x03, 0x00, 0x1C, 0x00, 0x00, 0x06, 0x00, 0x00, 0x0F, 0x80, + 0x00, 0x03, 0xC0, 0x00, 0x00, 0xE0, 0x00, 0x0F, 0x0C, 0x06, 0x3F, 0x9C, 0x1C, 0xFF, 0x98, 0x3B, + 0x87, 0x38, 0xE7, 0x06, 0x71, 0xCC, 0x0E, 0x63, 0x18, 0x1C, 0xEE, 0x30, 0x38, 0xDC, 0x70, 0x61, + 0xB0, 0xE1, 0xC3, 0xE0, 0xFF, 0x03, 0x80, 0xFC, 0x07, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x18, 0x00, + 0x01, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x07, 0x00, 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0xE0, 0xEE, 0x03, + 0xF0, 0x0B, 0x80, 0x18, 0x00, 0xC0, 0x07, 0x00, 0x38, 0x0E, 0xE0, 0xF7, 0xFF, 0x9F, 0xFC, 0x3E, + 0xE0, 0x07, 0x0F, 0x03, 0xF8, 0xFF, 0x38, 0x76, 0x06, 0xC0, 0x18, 0x03, 0x00, 0x70, 0x0E, 0x00, + 0xF8, 0x0F, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x00, 0xC0, 0x38, 0x66, 0x0F, 0xC0, 0xF8, 0x0F, + 0x81, 0xBF, 0x73, 0xFC, 0x07, 0x80, 0xFC, 0x1B, 0x87, 0x10, 0xC0, 0x00, 0x03, 0xFF, 0xFF, 0xE0, + 0x1F, 0x7F, 0xFC, 0x60, 0xDB, 0xB0, 0x77, 0x64, 0x78, 0x7F, 0xF1, 0xE0, 0x00, 0x70, 0x00, 0x07, + 0xC0, 0x00, 0x00, 0x00, 0x78, 0x07, 0x83, 0x60, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x78, 0x00, 0xF6, 0xC0, 0x06, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, + 0xE0, 0xF8, 0x0F, 0x86, 0xC0, 0x48, 0x00, 0x70, 0x00, 0x07, 0xC0, 0x00, 0x24, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x60, 0x00, 0x86, 0x00, 0x0C, 0x61, 0xC0, 0xC0, 0x38, 0x0C, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x08, 0xF8, 0x00, 0xF1, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0xE0, 0x30, 0x1C, 0x23, 0x81, 0x06, 0x18, 0x00, 0x60, 0x00, 0x06, 0x00, + 0x00, 0x40, 0x00, 0x0C, 0xC0, 0x1F, 0x80, 0x3E, 0x00, 0x10, 0x04, 0x01, 0x1C, 0x07, 0x38, 0x1E, + 0x70, 0x7C, 0xE0, 0xF9, 0xC3, 0xF3, 0x8E, 0xE7, 0x1D, 0xCE, 0x73, 0x9C, 0xE7, 0x3B, 0x8E, 0x7E, + 0x1C, 0xFC, 0x39, 0xF0, 0x73, 0xC0, 0xFF, 0x81, 0xF0, 0x00, 0xC0, 0x01, 0x80, 0x07, 0x00, 0x04, + 0x33, 0x03, 0xF0, 0x1E, 0x00, 0x40, 0x40, 0xCE, 0x1C, 0xE3, 0xCE, 0x3C, 0xE7, 0xCE, 0xFC, 0xED, + 0xCF, 0xDC, 0xF9, 0xCF, 0x1C, 0xF1, 0xFE, 0x1F, 0x00, 0x60, 0x0E, 0x00, 0xE0, 0x0C, 0x10, 0x00, + 0xE0, 0x03, 0x80, 0x3F, 0xE0, 0xFF, 0x83, 0xFC, 0x03, 0xF8, 0x0F, 0xFC, 0x3F, 0xF8, 0xE0, 0xE3, + 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0xE3, 0xFF, 0x8F, 0xF8, 0x38, 0x03, 0x80, 0xFF, 0x0F, 0xF0, + 0xFF, 0x03, 0x80, 0x3F, 0x83, 0xFE, 0x3F, 0xF3, 0x87, 0x38, 0x73, 0x87, 0x3F, 0xE3, 0xFC, 0x7E, + 0x0F, 0xFC, 0xFF, 0xEE, 0x0E, 0xE0, 0x6E, 0x07, 0xE3, 0x6E, 0x3E, 0xFF, 0xEF, 0xFC, 0xFF, 0xEE, + 0x0C, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x4E, 0x1F, 0xF3, 0xFF, 0x70, 0x6E, 0x0F, 0xC1, 0xF8, + 0x3F, 0x07, 0xE3, 0xFE, 0x7B, 0xFF, 0x7F, 0xFE, 0x6D, 0xC0, 0x38, 0x07, 0x00, 0x40, 0x00, 0x00, + 0xC0, 0x30, 0x0D, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, + 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x00, 0x00, 0x07, 0x07, 0x7F, 0xFF, 0xFF, 0xE0, 0xE0, 0xE0, + 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x1F, 0xF3, 0xFF, 0x3F, 0xF3, 0x80, 0x38, 0x03, 0x80, 0x38, + 0x07, 0xF8, 0xFF, 0x87, 0xF8, 0x38, 0x03, 0x80, 0x38, 0x03, 0x80, 0x38, 0x03, 0x80, 0x3F, 0xBF, + 0xDF, 0xEE, 0x07, 0x07, 0xF3, 0xF9, 0xF8, 0x70, 0x38, 0x1C, 0x0E, 0x00, 0x7F, 0xCF, 0xFC, 0xFF, + 0xCE, 0x00, 0xE0, 0x0E, 0x00, 0xE7, 0x0F, 0xFC, 0xFF, 0xEF, 0x0E, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, + 0x07, 0xE1, 0xEE, 0x7C, 0x07, 0x80, 0x7F, 0x3F, 0xCF, 0xF3, 0x80, 0xE0, 0x3B, 0x0F, 0xF3, 0xFE, + 0xE1, 0xB8, 0x6E, 0x1F, 0x86, 0x03, 0x87, 0xE1, 0xF0, 0x30, 0x60, 0x60, 0x63, 0x83, 0x07, 0x0E, + 0x18, 0x70, 0x70, 0xC3, 0x81, 0xC6, 0x38, 0x0E, 0x31, 0x80, 0x39, 0x9C, 0x00, 0xFF, 0xC0, 0x07, + 0xFE, 0x00, 0x7F, 0xF8, 0x03, 0x19, 0xC0, 0x38, 0xC7, 0x03, 0x86, 0x1C, 0x1C, 0x30, 0xE1, 0xC1, + 0x83, 0xDC, 0x0C, 0x1E, 0x00, 0x00, 0x30, 0x00, 0x01, 0x80, 0x00, 0x0C, 0x00, 0x00, 0x60, 0x61, + 0x83, 0x38, 0xE3, 0x8E, 0x73, 0x87, 0x3B, 0x81, 0xDD, 0x80, 0x7F, 0xC0, 0x1F, 0xC0, 0x1F, 0xF0, + 0x1D, 0xDC, 0x1C, 0xEF, 0x1C, 0x73, 0xEE, 0x38, 0xF0, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, + 0x03, 0x0E, 0x07, 0xF1, 0xFF, 0x70, 0xEE, 0x0C, 0x01, 0x80, 0x70, 0x7C, 0x0F, 0x80, 0x78, 0x03, + 0x80, 0x7E, 0x0F, 0xC1, 0xDF, 0xF1, 0xFC, 0x0E, 0x00, 0xE0, 0x0C, 0x07, 0x80, 0xE0, 0x0E, 0x0F, + 0xC7, 0xF9, 0x86, 0x03, 0x83, 0xE0, 0xF0, 0x0E, 0x01, 0xD8, 0x77, 0xF8, 0xFC, 0x0C, 0x03, 0xC0, + 0x30, 0x3C, 0x0C, 0x00, 0x40, 0x33, 0x81, 0xCE, 0x0E, 0x38, 0x70, 0xE1, 0xC3, 0x8E, 0x0E, 0x70, + 0x3F, 0x80, 0xFE, 0x03, 0xFC, 0x0E, 0x78, 0x38, 0xE0, 0xE1, 0xC3, 0x83, 0x8E, 0x0F, 0xB8, 0x1F, + 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, 0x06, 0x41, 0xDC, 0x7B, 0x8E, 0x73, 0x8E, 0xE1, 0xF8, 0x3F, + 0x07, 0xF0, 0xEF, 0x1C, 0xE3, 0x8F, 0xF0, 0xF0, 0x0E, 0x01, 0xC0, 0x38, 0x03, 0x40, 0x37, 0x03, + 0xB8, 0x39, 0xC1, 0xCE, 0x1C, 0x76, 0xE3, 0xFE, 0x1F, 0xF0, 0xFF, 0x07, 0xFC, 0x3F, 0x71, 0xDB, + 0x8E, 0x0E, 0x70, 0x73, 0x81, 0xDC, 0x0E, 0x40, 0x7C, 0x1F, 0x87, 0x7D, 0xCF, 0xB9, 0xFE, 0x3F, + 0xC7, 0xF8, 0xFB, 0x9F, 0x3B, 0x87, 0x70, 0x70, 0x7E, 0x03, 0x3F, 0x83, 0x9F, 0xC1, 0xC0, 0xE1, + 0xC0, 0x71, 0xC0, 0x39, 0xC0, 0x1C, 0xE0, 0x0F, 0xE0, 0x07, 0xF0, 0x03, 0xF8, 0x01, 0xCE, 0x00, + 0xE3, 0x80, 0x71, 0xE0, 0x38, 0x70, 0x1C, 0x1C, 0x0E, 0x07, 0xF8, 0x3F, 0xE3, 0xFF, 0x3C, 0x39, + 0xC1, 0xDC, 0x0F, 0xC0, 0x7E, 0x03, 0xF8, 0x1D, 0xC0, 0xE7, 0x07, 0x1C, 0x38, 0x70, 0x40, 0x11, + 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0E, 0x7F, 0xFC, 0xFF, 0xF9, 0xC0, + 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0F, 0x70, 0x1E, 0x00, 0x0C, 0x00, 0x18, + 0x00, 0x30, 0x00, 0x60, 0x40, 0xDC, 0x3B, 0x87, 0x70, 0xEE, 0x1D, 0xFF, 0xBF, 0xF7, 0x0E, 0xE1, + 0xDC, 0x3B, 0x87, 0xF0, 0xF0, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x40, 0x1F, 0xDC, 0x07, 0xFF, 0x80, + 0xFF, 0xF0, 0x1C, 0x0E, 0x03, 0x81, 0xC0, 0x70, 0x38, 0x0E, 0x07, 0xFF, 0xC0, 0xFF, 0xF8, 0x1C, + 0x07, 0x03, 0x80, 0xE0, 0x70, 0x1C, 0x0E, 0x03, 0x81, 0xC0, 0x70, 0x38, 0x0E, 0x07, 0x01, 0xC0, + 0x40, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0x8E, 0x1C, 0x7F, 0xE3, 0xFF, 0x1C, 0x38, 0xE1, 0xC7, 0x0E, + 0x38, 0x71, 0xC3, 0x80, 0x7F, 0xF0, 0x07, 0xFF, 0xC0, 0x3F, 0xFE, 0x01, 0xC0, 0x70, 0x0E, 0x03, + 0x80, 0x70, 0x1C, 0x03, 0x80, 0xEE, 0x1C, 0x07, 0xFC, 0xE0, 0x3F, 0xF7, 0x01, 0xC1, 0xF8, 0x0E, + 0x0F, 0xC0, 0x70, 0x3E, 0x03, 0x81, 0xF0, 0x1C, 0x1F, 0x80, 0xE1, 0xFC, 0x07, 0x7E, 0x00, 0x03, + 0xE0, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xFC, 0x03, 0x87, 0x00, 0xE1, 0xC0, 0x38, 0x7F, 0x0E, 0x1F, + 0xF3, 0x87, 0xFE, 0xE1, 0xC3, 0xB8, 0x70, 0x6E, 0x1C, 0x1B, 0x87, 0x06, 0x00, 0x03, 0x80, 0x03, + 0xC0, 0x00, 0xE0, 0x06, 0x00, 0x03, 0xC0, 0x00, 0xF8, 0x70, 0x3C, 0x3F, 0x87, 0x0F, 0xF9, 0xC3, + 0x87, 0x38, 0x70, 0x76, 0x0E, 0x0E, 0xC1, 0xC1, 0xD8, 0x38, 0x3B, 0x87, 0x0E, 0x70, 0x71, 0xC7, + 0x0F, 0x70, 0xF0, 0xFC, 0x0F, 0xFF, 0xF0, 0xFF, 0xFF, 0x03, 0x83, 0x80, 0x0C, 0x00, 0x1E, 0x00, + 0x3C, 0xF8, 0x71, 0xFC, 0x73, 0x8E, 0x63, 0x8E, 0x63, 0x0E, 0x63, 0x8E, 0x73, 0x8C, 0x71, 0xFC, + 0x3F, 0xFF, 0x1F, 0xFF, 0x03, 0x8C, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x60, 0x3B, 0x80, + 0x6E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x38, 0x06, 0x70, 0x39, 0xE1, 0xE3, 0xFF, + 0x07, 0xF8, 0x03, 0x00, 0x0E, 0x00, 0x0C, 0x01, 0xE0, 0x03, 0x00, 0x0F, 0x03, 0xF8, 0xFF, 0x38, + 0x77, 0x04, 0xC0, 0x18, 0x03, 0x00, 0x70, 0x6E, 0x1C, 0xFF, 0x0F, 0xC0, 0x60, 0x0F, 0x00, 0x60, + 0x3C, 0x06, 0x00, 0x7F, 0xF3, 0xFF, 0xDF, 0xFE, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, + 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1E, 0x00, 0xF0, 0x01, 0x80, 0x0C, + 0x00, 0x60, 0x03, 0x00, 0xFF, 0xBF, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, + 0x00, 0xF0, 0x3C, 0x07, 0x01, 0xC0, 0x70, 0x18, 0x40, 0x0B, 0x80, 0xF7, 0x03, 0x8E, 0x1C, 0x38, + 0x70, 0x73, 0x80, 0xFC, 0x03, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, + 0x00, 0x30, 0x00, 0xC0, 0x40, 0x7C, 0x1D, 0x83, 0xB8, 0x63, 0x1C, 0x73, 0x8E, 0x60, 0xDC, 0x1F, + 0x03, 0xE0, 0x3C, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x06, 0x00, 0x40, 0x0B, 0x80, 0xF7, + 0x03, 0x8E, 0x1C, 0x38, 0x70, 0x73, 0x80, 0xFC, 0x03, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0x30, 0x07, + 0xF8, 0x1F, 0xE0, 0x7F, 0x80, 0x30, 0x00, 0xC0, 0x40, 0x7C, 0x1D, 0x83, 0xB8, 0x63, 0x1C, 0x73, + 0x8E, 0x60, 0xDC, 0x1F, 0x03, 0xE0, 0x3C, 0x1F, 0xC3, 0xF8, 0x3F, 0x03, 0x80, 0x70, 0x06, 0x00, + 0x60, 0x18, 0xE0, 0x70, 0xE1, 0xE1, 0xC3, 0x81, 0xCE, 0x01, 0xF8, 0x03, 0xF0, 0x03, 0xC0, 0x07, + 0x80, 0x0F, 0x80, 0x3F, 0x00, 0xE7, 0x01, 0xCF, 0x07, 0x0E, 0x1C, 0x0F, 0xF0, 0x0F, 0x00, 0x0E, + 0x00, 0x1C, 0x00, 0x38, 0x00, 0x60, 0x60, 0xC7, 0x1C, 0x31, 0xC3, 0xB8, 0x1F, 0x00, 0xF0, 0x0E, + 0x01, 0xF0, 0x1B, 0x83, 0xB8, 0x71, 0xFE, 0x0F, 0x00, 0x70, 0x07, 0x00, 0x70, 0x03, 0x7F, 0xF1, + 0x8F, 0xFF, 0x31, 0xFF, 0xC6, 0x03, 0x80, 0xC0, 0x70, 0x18, 0x0E, 0x03, 0x01, 0xC0, 0x60, 0x38, + 0x0C, 0x07, 0x01, 0x80, 0xE0, 0x30, 0x1C, 0x06, 0x03, 0x80, 0xC0, 0x70, 0x18, 0x0E, 0x03, 0x01, + 0xFF, 0xF8, 0x3F, 0xFF, 0x00, 0x00, 0xE0, 0x00, 0x1C, 0x00, 0x03, 0x80, 0x00, 0x20, 0xFE, 0x33, + 0xFC, 0xCF, 0xF3, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, + 0xFF, 0xC7, 0xFF, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, 0x02, 0x40, 0x23, 0x81, 0xCE, 0x07, 0x38, + 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x3C, 0x1C, 0x7F, 0xF0, 0xFF, 0xC0, 0xF7, 0x00, 0x1C, 0x00, + 0x70, 0x01, 0xC0, 0x07, 0x80, 0x1F, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, 0x06, 0x40, 0x9C, 0x3B, + 0x87, 0x70, 0xEE, 0x1D, 0xC3, 0x9F, 0xF3, 0xFE, 0x1F, 0xC0, 0x38, 0x07, 0x80, 0xF0, 0x06, 0x00, + 0xC0, 0x18, 0x03, 0x40, 0x2E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE6, 0x7F, 0x67, 0x7F, + 0xF3, 0xFF, 0x0F, 0x70, 0x67, 0x06, 0x70, 0x07, 0x00, 0x70, 0x07, 0x40, 0xB8, 0x7E, 0x1F, 0x87, + 0xED, 0xFB, 0x77, 0xFD, 0xFF, 0x1F, 0xC3, 0x70, 0xDC, 0x07, 0x40, 0x0E, 0x00, 0xE0, 0x0E, 0x00, + 0xE0, 0x0E, 0x00, 0xEF, 0x8F, 0xFE, 0xFF, 0xFE, 0x07, 0xE0, 0x7E, 0x03, 0xE0, 0x3E, 0x03, 0xE0, + 0x3E, 0x03, 0x40, 0x38, 0x0E, 0x03, 0x80, 0xEE, 0x3F, 0xEF, 0xFB, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, + 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0x00, 0xF0, 0x01, 0xFE, 0x01, 0xFF, 0x81, 0xE1, 0xE0, 0xC0, + 0x76, 0xE0, 0x1B, 0x70, 0x0D, 0xFF, 0xFF, 0x7F, 0xFF, 0x9F, 0xFF, 0x87, 0x00, 0x03, 0x80, 0x60, + 0xE0, 0x70, 0x78, 0x78, 0x1F, 0xF8, 0x07, 0xF8, 0x00, 0x60, 0x00, 0x01, 0xC0, 0x1F, 0xC0, 0x7F, + 0xB3, 0x87, 0xCC, 0x1F, 0xFF, 0xF7, 0xFF, 0xC7, 0x00, 0x0C, 0x00, 0x38, 0x70, 0x7F, 0x80, 0xFC, + 0x00, 0xC0, 0x00, 0xF0, 0x01, 0xFE, 0x01, 0xFF, 0x81, 0xE1, 0xE0, 0xC0, 0x76, 0xE0, 0x1B, 0x70, + 0x0D, 0xFF, 0xFF, 0x7F, 0xFF, 0x9F, 0xFF, 0x87, 0x00, 0x03, 0x80, 0x60, 0xE0, 0x70, 0x78, 0x78, + 0x1F, 0xF8, 0x07, 0xF8, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x01, 0xC0, 0x1F, + 0xC0, 0x7F, 0xB3, 0x87, 0xCC, 0x1F, 0xFF, 0xF7, 0xFF, 0xC7, 0x00, 0x0C, 0x00, 0x38, 0x70, 0x7F, + 0x80, 0xFC, 0x00, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x78, 0x00, 0xC0, 0x5F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x01, 0x98, 0x00, 0x1F, 0x80, 0x01, 0xF0, 0x00, 0x04, 0x00, 0x60, 0x60, 0x67, 0x06, 0x0E, + 0x38, 0x61, 0xC3, 0x86, 0x1C, 0x1C, 0x63, 0x81, 0xC6, 0x30, 0x0E, 0x67, 0x00, 0x7F, 0xE0, 0x07, + 0xFE, 0x00, 0xFF, 0xF0, 0x0C, 0x67, 0x01, 0xC6, 0x38, 0x38, 0x61, 0xC3, 0x86, 0x1C, 0x70, 0x60, + 0xEE, 0x06, 0x0E, 0x06, 0x30, 0x03, 0xF0, 0x00, 0xF8, 0x00, 0x10, 0x06, 0x18, 0x33, 0x8E, 0x38, + 0xE7, 0x38, 0x73, 0xB8, 0x1D, 0xD8, 0x07, 0xFC, 0x01, 0xFC, 0x01, 0xFF, 0x01, 0xDD, 0xC1, 0xCE, + 0xF1, 0xC7, 0x38, 0xE3, 0x8E, 0x40, 0x3E, 0x07, 0xE0, 0xEE, 0x1C, 0xE3, 0x8E, 0x78, 0xE7, 0x0F, + 0xF0, 0xFF, 0xCF, 0xFE, 0xE0, 0xFE, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x00, 0xF0, 0x3E, + 0x03, 0xC0, 0x00, 0x41, 0xDC, 0x7B, 0x8E, 0x73, 0x8E, 0xE1, 0xF8, 0x3F, 0xE7, 0xFE, 0xE1, 0xDC, + 0x1B, 0x83, 0x70, 0x60, 0x3C, 0x0F, 0x00, 0xC0, 0x07, 0xF8, 0x0F, 0xFC, 0x0F, 0xFC, 0x0C, 0x1C, + 0x0C, 0x1C, 0x0C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x38, 0x1C, 0x78, 0x1F, 0x70, 0x1F, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0C, + 0x1F, 0xC1, 0xFF, 0x0F, 0xF8, 0x71, 0xC3, 0x8E, 0x1C, 0x70, 0xE3, 0x87, 0x1C, 0x30, 0xE1, 0x87, + 0x3C, 0x3F, 0xC1, 0xE0, 0x07, 0x00, 0x38, 0x01, 0x80, 0x0C, 0x40, 0x17, 0x01, 0xF8, 0x0F, 0xC0, + 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, + 0x1F, 0x80, 0xFC, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x3E, 0x01, 0xE0, 0x0E, 0x00, 0x40, 0xF8, 0x7E, + 0x1F, 0x87, 0xE1, 0xFF, 0xFF, 0xFF, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0x01, 0xC0, 0x70, 0x3C, + 0x1E, 0x07, 0x00, 0x40, 0x11, 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0E, + 0x7F, 0xFC, 0xFF, 0xF9, 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0F, 0xF0, + 0x1F, 0x00, 0x0C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xC0, 0x40, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, + 0xCF, 0xFC, 0xFF, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xFE, 0x1F, 0x00, 0x60, 0x0E, 0x00, 0xE0, + 0x0C, 0x40, 0x23, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x3C, 0x1C, 0x7F, + 0xF0, 0xFF, 0xC0, 0xF7, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, 0x07, 0x80, 0x1F, 0x00, 0x1C, 0x00, + 0x70, 0x01, 0xC0, 0x06, 0x40, 0x9C, 0x3B, 0x87, 0x70, 0xEE, 0x1D, 0xC3, 0x9F, 0xF3, 0xFE, 0x1F, + 0xC0, 0x38, 0x07, 0x80, 0xF0, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x60, 0x07, 0x3C, 0x01, 0xCF, 0x00, + 0xF3, 0xE0, 0x3C, 0xF8, 0x1F, 0x3F, 0x07, 0xCF, 0xC1, 0xB3, 0xB0, 0xEC, 0xEE, 0x33, 0x39, 0x9C, + 0xCE, 0x77, 0x33, 0x9D, 0x8C, 0xE3, 0xE3, 0x38, 0xF0, 0xCE, 0x3C, 0x3F, 0x87, 0x0F, 0x00, 0x01, + 0xC0, 0x00, 0x60, 0x00, 0x38, 0x00, 0x04, 0x60, 0x18, 0xF0, 0x3C, 0xF0, 0x7C, 0xF8, 0x7C, 0xF8, + 0x7C, 0xFC, 0xFC, 0xFC, 0xDC, 0xED, 0xDC, 0xEF, 0xDC, 0xE7, 0x9C, 0xE7, 0x9E, 0xE3, 0x1E, 0x00, + 0x0E, 0x00, 0x0E, 0x00, 0x0C, 0x00, 0x0C, 0x5F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0C, 0xC0, 0x3F, + 0x00, 0x7C, 0x00, 0x40, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, + 0x07, 0x38, 0x18, 0x60, 0xE1, 0xC3, 0xFF, 0x0F, 0xFC, 0x70, 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, + 0x31, 0x87, 0xE1, 0xF0, 0x10, 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, + 0xD8, 0x77, 0xFC, 0xFF, 0x0C, 0x00, 0x0C, 0xC0, 0x73, 0x80, 0xCE, 0x00, 0x00, 0x03, 0x00, 0x1E, + 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x07, 0x38, 0x18, 0x60, 0xE1, 0xC3, 0xFF, + 0x0F, 0xFC, 0x70, 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, 0x31, 0x8E, 0xE3, 0xB8, 0x00, 0x0E, 0x0F, + 0xE7, 0xFD, 0xC3, 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, 0xD8, 0x77, 0xFC, 0xFF, 0x0C, 0x00, 0x00, + 0xFF, 0xE0, 0x1F, 0xFF, 0x03, 0xFF, 0xF0, 0x3B, 0x00, 0x03, 0x30, 0x00, 0x73, 0x00, 0x06, 0x30, + 0x00, 0xE3, 0xFE, 0x0C, 0x3F, 0xE1, 0xC3, 0xFE, 0x1F, 0xF0, 0x03, 0xFF, 0x00, 0x38, 0x30, 0x07, + 0x03, 0x00, 0x70, 0x3F, 0xFE, 0x03, 0xFF, 0x0E, 0x1E, 0x0F, 0xEF, 0xC7, 0xFF, 0xF9, 0xC3, 0x87, + 0x00, 0xC0, 0xC7, 0xFF, 0xF7, 0xFF, 0xFD, 0xC3, 0x00, 0x60, 0xC0, 0x18, 0x78, 0x77, 0xFF, 0xFC, + 0xFC, 0xFE, 0x0C, 0x0C, 0x00, 0x31, 0x87, 0xF0, 0x7C, 0x02, 0x07, 0xFD, 0xFF, 0xFF, 0xF7, 0x00, + 0xE0, 0x1C, 0x03, 0x80, 0x7F, 0xEF, 0xFD, 0xFF, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, + 0xF0, 0x19, 0x83, 0xF0, 0x3C, 0x00, 0x00, 0xF0, 0x3F, 0x8F, 0xF3, 0x87, 0x60, 0x6F, 0xFD, 0xFF, + 0xB0, 0x06, 0x00, 0xE1, 0xCF, 0xF8, 0xFE, 0x06, 0x00, 0x07, 0x00, 0x7F, 0x87, 0xFF, 0x1C, 0x1C, + 0xE0, 0x3B, 0x80, 0x60, 0x01, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0xB8, 0x0E, 0xE0, 0x39, + 0xC1, 0xC3, 0xFE, 0x07, 0xF0, 0x07, 0x00, 0x0F, 0x07, 0xF1, 0xFF, 0x38, 0x70, 0x06, 0xFF, 0xDF, + 0xFB, 0xFF, 0x60, 0xEE, 0x1C, 0xFF, 0x0F, 0xC0, 0x40, 0x18, 0xC0, 0x77, 0x01, 0xDC, 0x00, 0x00, + 0x07, 0x00, 0x7F, 0x87, 0xFF, 0x1C, 0x1C, 0xE0, 0x3B, 0x80, 0x60, 0x01, 0xBF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFE, 0x01, 0xB8, 0x0E, 0xE0, 0x39, 0xC1, 0xC3, 0xFE, 0x07, 0xF0, 0x07, 0x00, 0x19, 0x87, + 0x38, 0x67, 0x00, 0x00, 0xF0, 0x7F, 0x1F, 0xF3, 0x87, 0x00, 0x6F, 0xFD, 0xFF, 0xBF, 0xF6, 0x0E, + 0xE1, 0xCF, 0xF0, 0xFC, 0x04, 0x00, 0x01, 0x98, 0x00, 0x39, 0xC0, 0x03, 0x98, 0x00, 0x00, 0x00, + 0x60, 0x60, 0x67, 0x06, 0x0E, 0x38, 0x61, 0xC3, 0x86, 0x1C, 0x1C, 0x63, 0x81, 0xC6, 0x30, 0x0E, + 0x67, 0x00, 0x7F, 0xE0, 0x07, 0xFE, 0x00, 0xFF, 0xF0, 0x0C, 0x67, 0x01, 0xC6, 0x38, 0x38, 0x61, + 0xC3, 0x86, 0x1C, 0x70, 0x60, 0xEE, 0x06, 0x0E, 0x06, 0x70, 0x03, 0xB8, 0x01, 0x9C, 0x00, 0x00, + 0x06, 0x18, 0x33, 0x8E, 0x38, 0xE7, 0x38, 0x73, 0xB8, 0x1D, 0xD8, 0x07, 0xFC, 0x01, 0xFC, 0x01, + 0xFF, 0x01, 0xDD, 0xC1, 0xCE, 0xF1, 0xC7, 0x38, 0xE3, 0x8E, 0x31, 0x87, 0x70, 0xEE, 0x00, 0x00, + 0xE0, 0x7F, 0x1F, 0xF7, 0x0E, 0xE0, 0xC0, 0x18, 0x07, 0x07, 0xC0, 0xF8, 0x07, 0x80, 0x38, 0x07, + 0xE0, 0xFC, 0x1D, 0xFF, 0x1F, 0xC0, 0xE0, 0x33, 0x1C, 0xE3, 0x38, 0x00, 0x0E, 0x0F, 0xC7, 0xF9, + 0x86, 0x03, 0x83, 0xE0, 0xF0, 0x0E, 0x01, 0xD8, 0x77, 0xF8, 0xFC, 0x00, 0x00, 0x7F, 0xCF, 0xF9, + 0xFF, 0x01, 0xC0, 0x70, 0x1E, 0x03, 0x80, 0xF8, 0x1F, 0xC0, 0x38, 0x03, 0x80, 0x7E, 0x0F, 0xC1, + 0xDF, 0xF1, 0xFC, 0x0E, 0x00, 0x7F, 0xCF, 0xF9, 0xFF, 0x01, 0xC0, 0x70, 0x1E, 0x03, 0x80, 0xF8, + 0x1F, 0xC0, 0x38, 0x03, 0x80, 0x7E, 0x0F, 0xC1, 0xDF, 0xF1, 0xFC, 0x0E, 0x00, 0x0F, 0xC0, 0x7E, + 0x00, 0x00, 0x80, 0x2E, 0x03, 0xF0, 0x3F, 0x83, 0xFC, 0x1F, 0xE1, 0xFF, 0x1D, 0xF8, 0xEF, 0xCE, + 0x7E, 0x73, 0xF7, 0x1F, 0xF0, 0xFF, 0x87, 0xF8, 0x3F, 0x81, 0xFC, 0x0E, 0x3F, 0x0F, 0xC0, 0x01, + 0x03, 0xE1, 0xF8, 0xFE, 0x3F, 0x9F, 0xEF, 0xFB, 0x7F, 0xDF, 0xE7, 0xF1, 0xFC, 0x7E, 0x1C, 0x0C, + 0xC0, 0xEE, 0x07, 0x30, 0x00, 0x04, 0x01, 0x70, 0x1F, 0x81, 0xFC, 0x1F, 0xE0, 0xFF, 0x0F, 0xF8, + 0xEF, 0xC7, 0x7E, 0x73, 0xF3, 0x9F, 0xB8, 0xFF, 0x87, 0xFC, 0x3F, 0xC1, 0xFC, 0x0F, 0xE0, 0x70, + 0x33, 0x1C, 0xE3, 0x30, 0x00, 0x40, 0xF8, 0x7E, 0x3F, 0x8F, 0xE7, 0xFB, 0xFE, 0xDF, 0xF7, 0xF9, + 0xFC, 0x7F, 0x1F, 0x87, 0x0C, 0xC0, 0x73, 0x80, 0xCC, 0x00, 0x00, 0x07, 0x80, 0x7F, 0x83, 0xFF, + 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7E, 0x01, 0xF8, 0x07, + 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x19, 0x83, 0xDC, 0x19, 0xC0, 0x00, 0x0F, + 0x01, 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, + 0xF8, 0x06, 0x00, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, + 0x00, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x67, 0xE7, 0x7F, 0xF6, 0x3F, 0x70, 0x67, 0x0E, + 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x0C, 0xC0, 0x73, 0x80, 0xCC, 0x00, 0x00, 0x07, 0x80, 0x7F, 0x83, + 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0xF8, + 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x19, 0x83, 0xDC, 0x19, 0xC0, 0x00, + 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x67, 0xE7, 0x7F, 0xF6, 0x3F, 0x70, 0x67, 0x0E, 0x3F, + 0xC1, 0xF8, 0x06, 0x00, 0x19, 0x80, 0xEE, 0x06, 0x70, 0x00, 0x00, 0xF0, 0x1F, 0xE1, 0xFF, 0x9E, + 0x0E, 0xE0, 0x3E, 0x01, 0xC0, 0x06, 0x1F, 0xF0, 0xFF, 0x80, 0x0C, 0x00, 0x78, 0x07, 0xE0, 0x3B, + 0x87, 0x9F, 0xF8, 0x7F, 0x80, 0x60, 0x00, 0x31, 0x87, 0x70, 0xEE, 0x00, 0x00, 0xE0, 0x7F, 0x1F, + 0xF3, 0x86, 0x20, 0xE1, 0xFC, 0x3F, 0x87, 0xF6, 0x0E, 0xE3, 0x9F, 0xF1, 0xFC, 0x0E, 0x00, 0x1F, + 0xC0, 0xFC, 0x00, 0x00, 0x80, 0x2E, 0x03, 0xB8, 0x39, 0xC1, 0xC7, 0x0C, 0x38, 0xE0, 0xC7, 0x07, + 0x70, 0x1B, 0x80, 0xF8, 0x07, 0xC0, 0x1C, 0x00, 0xE0, 0x07, 0x01, 0xF0, 0x0F, 0x00, 0x1F, 0x83, + 0xF0, 0x00, 0x20, 0x3E, 0x0E, 0xC1, 0xDC, 0x31, 0x8E, 0x39, 0xC7, 0x30, 0x6E, 0x0F, 0x81, 0xF0, + 0x1E, 0x03, 0x80, 0x70, 0x0C, 0x07, 0x81, 0xE0, 0x38, 0x00, 0x18, 0xC0, 0xEE, 0x07, 0x70, 0x00, + 0x04, 0x01, 0x70, 0x1D, 0xC1, 0xCE, 0x0E, 0x38, 0x61, 0xC7, 0x06, 0x38, 0x3B, 0x80, 0xDC, 0x07, + 0xC0, 0x3E, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x0F, 0x80, 0x78, 0x00, 0x19, 0x87, 0x70, 0xE6, 0x00, + 0x04, 0x07, 0xC1, 0xD8, 0x3B, 0x86, 0x31, 0xC7, 0x38, 0xE6, 0x0D, 0xC1, 0xF0, 0x3E, 0x03, 0xC0, + 0x70, 0x0E, 0x01, 0x80, 0xF0, 0x3C, 0x07, 0x00, 0x0E, 0xC0, 0x6E, 0x07, 0x60, 0x32, 0x04, 0x01, + 0x70, 0x1D, 0xC1, 0xCE, 0x0E, 0x38, 0x61, 0xC7, 0x06, 0x38, 0x3B, 0x80, 0xDC, 0x07, 0xC0, 0x3E, + 0x00, 0xE0, 0x07, 0x00, 0x38, 0x0F, 0x80, 0x78, 0x00, 0x1D, 0xC3, 0xF0, 0x6C, 0x09, 0x84, 0x07, + 0xC1, 0xD8, 0x3B, 0x86, 0x31, 0xC7, 0x38, 0xE6, 0x0D, 0xC1, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0E, + 0x01, 0x80, 0xF0, 0x3C, 0x07, 0x00, 0x19, 0x83, 0x9C, 0x19, 0xC0, 0x00, 0x40, 0x2E, 0x07, 0xE0, + 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x7F, 0xF3, 0xFF, 0x0F, 0x70, 0x07, 0x00, 0x70, + 0x07, 0x00, 0x70, 0x07, 0x33, 0x1C, 0xE3, 0x30, 0x00, 0x40, 0xB8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, + 0x77, 0xFD, 0xFF, 0x1F, 0xC0, 0x70, 0x1C, 0x07, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x38, 0x0E, + 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0F, 0x03, 0xE0, 0x38, 0x0E, 0x03, 0x80, + 0xC0, 0x7F, 0xFF, 0xFF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xF0, 0xF0, 0x30, 0x30, 0x30, + 0x30, 0x06, 0x60, 0x0E, 0xE0, 0x0E, 0x60, 0x00, 0x00, 0x40, 0x02, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xFE, 0x07, 0xFF, 0xC7, 0xFF, 0xE7, 0xE0, 0xE7, 0xE0, 0x77, 0xE0, + 0x77, 0xE0, 0x77, 0xE0, 0xE7, 0xFF, 0xE7, 0xFF, 0xC7, 0x18, 0xC0, 0xEE, 0x07, 0x70, 0x00, 0x04, + 0x01, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xFC, 0x3F, 0xFD, 0xFF, 0xEF, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, + 0xFE, 0xFF, 0xE7, 0x3F, 0xE3, 0xFF, 0x3F, 0xF3, 0x00, 0x30, 0x03, 0x00, 0x30, 0x0F, 0xF8, 0xFF, + 0x8F, 0xF0, 0x30, 0x03, 0x00, 0x30, 0x03, 0x00, 0x3C, 0x03, 0xC0, 0x0C, 0x00, 0xC0, 0x7C, 0x07, + 0x80, 0x70, 0x00, 0x3F, 0xBF, 0xDF, 0xEE, 0x07, 0x07, 0xF3, 0xF9, 0xF8, 0x70, 0x38, 0x1E, 0x0F, + 0x01, 0x80, 0xC1, 0xE1, 0xF0, 0x60, 0x00, 0x60, 0x18, 0xE0, 0x70, 0xE1, 0xE1, 0xC3, 0x81, 0xCE, + 0x01, 0xF8, 0x03, 0xF0, 0x03, 0xC0, 0x07, 0x80, 0x0F, 0x80, 0x3F, 0x00, 0xE7, 0x01, 0xCF, 0x07, + 0x0E, 0x1C, 0x0F, 0xF0, 0x0F, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0xF8, 0x01, 0xE0, 0x03, 0x80, 0x60, + 0xCE, 0x39, 0xC6, 0x1D, 0xC1, 0xF0, 0x3C, 0x03, 0x80, 0xF8, 0x3F, 0x07, 0x71, 0xC6, 0x70, 0xE0, + 0x0C, 0x01, 0x81, 0xF0, 0x3E, 0x07, 0x00, 0x60, 0x19, 0xC0, 0xE3, 0x87, 0x8E, 0x1C, 0x1C, 0xE0, + 0x3F, 0x00, 0xFC, 0x1F, 0xFC, 0x7F, 0xF8, 0x1F, 0x00, 0xFC, 0x07, 0x38, 0x1C, 0xF0, 0xE1, 0xC7, + 0x03, 0xB8, 0x07, 0x60, 0xCE, 0x38, 0xC7, 0x1D, 0xC1, 0xF0, 0xFF, 0x9F, 0xF0, 0xF8, 0x1B, 0x87, + 0x71, 0xC7, 0x70, 0x70, 0xE0, 0xEE, 0x1C, 0xC3, 0x9C, 0x71, 0xCE, 0x79, 0xDF, 0xF3, 0x3C, 0x63, + 0x1C, 0x73, 0x87, 0x70, 0xEE, 0x0E, 0x7F, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, + 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xCF, 0xFE, 0x7C, 0x0C, 0x0E, 0x0E, 0x0E, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x0E, 0x3F, 0xE7, 0xFF, 0xE0, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, + 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0xFF, 0x80, 0x38, 0x03, 0x80, 0x70, 0x06, 0x00, 0xC0, + 0x1F, 0x03, 0xE0, 0x7C, 0x0F, 0x81, 0xF0, 0x3E, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0x18, + 0x18, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x18, 0x18, 0x18, 0x1C, 0xFF, 0xDC, 0x3F, 0x83, 0xF0, + 0x3E, 0x07, 0xC0, 0xF8, 0x1F, 0x03, 0xE0, 0x7C, 0x0F, 0x81, 0xF0, 0x3E, 0x06, 0xE7, 0xCE, 0x0E, + 0xE0, 0x6E, 0x06, 0xE0, 0x6E, 0x06, 0xE0, 0x6E, 0x06, 0xE0, 0x6E, 0x06, 0x60, 0xE7, 0x9C, 0x3F, + 0x80, 0xFF, 0xFF, 0xF8, 0xFE, 0x03, 0xC0, 0x38, 0x06, 0x01, 0x80, 0x70, 0x1C, 0x07, 0x01, 0xC0, + 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0xFF, 0x00, 0x70, 0x07, 0x00, 0x60, + 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x07, 0x01, 0xCF, 0xF0, 0xE0, 0x38, 0x0E, 0x03, + 0xFF, 0x01, 0xC0, 0x60, 0x38, 0x0E, 0x03, 0x01, 0xC0, 0x70, 0x18, 0x0E, 0x03, 0x80, 0xC0, 0x70, + 0xFF, 0xDC, 0x3F, 0x83, 0xF0, 0x3E, 0x07, 0xC0, 0xF8, 0x1F, 0x03, 0xE0, 0x7C, 0x0F, 0x81, 0xF0, + 0x3F, 0xFE, 0x6F, 0xCF, 0xBD, 0xC3, 0xB8, 0x37, 0x06, 0xE0, 0xD8, 0x1B, 0x03, 0x60, 0x7C, 0x0F, + 0x81, 0xF0, 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x78, 0x71, 0xC7, 0x1C, 0x71, + 0xC7, 0x1C, 0x71, 0xC7, 0xFC, 0xFF, 0x83, 0x1C, 0x60, 0xEE, 0x06, 0xC0, 0x7C, 0x07, 0xC0, 0x7C, + 0x07, 0xE0, 0x7E, 0x06, 0x60, 0xE3, 0x9C, 0x1F, 0x80, 0x60, 0x6E, 0x0D, 0xC1, 0x98, 0x33, 0x06, + 0x70, 0xC6, 0x18, 0xC3, 0x1C, 0x61, 0x9C, 0x33, 0x07, 0xC3, 0xF1, 0xF8, 0x00, 0xFF, 0x1C, 0xFB, + 0x83, 0x70, 0x7E, 0x0F, 0xC1, 0xF8, 0x3B, 0xC7, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, + 0xC0, 0x38, 0x07, 0x00, 0xE0, 0xFF, 0x9C, 0x7B, 0x83, 0xF0, 0x7E, 0x06, 0xC0, 0xDF, 0x18, 0x03, + 0x00, 0x60, 0x1C, 0x03, 0x81, 0xEF, 0xF8, 0xE1, 0xF8, 0x76, 0x1D, 0xC7, 0x71, 0xCC, 0x73, 0x9C, + 0xFE, 0x1F, 0x07, 0x01, 0xC0, 0x30, 0x0C, 0x03, 0x80, 0x60, 0x18, 0x07, 0x00, 0x60, 0xDC, 0x33, + 0x8C, 0xE3, 0x1C, 0xC7, 0xF0, 0xF8, 0x1C, 0x07, 0x00, 0xE0, 0x38, 0x07, 0x7F, 0xC0, 0xFF, 0xF0, + 0x07, 0x00, 0x60, 0x0E, 0x00, 0xE0, 0x0C, 0x01, 0xCE, 0x18, 0xE3, 0x8E, 0x38, 0xE3, 0x0E, 0x70, + 0xE7, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x00, 0xFF, 0x00, 0xE0, 0x18, 0x07, 0x01, 0xC0, 0x70, + 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0xE3, 0x1F, 0x8C, 0x7E, 0x31, 0xF8, 0xC7, + 0xE3, 0x1F, 0x8C, 0x7F, 0xF1, 0xF8, 0x06, 0xE0, 0x19, 0x80, 0x67, 0x03, 0x8F, 0x3C, 0x1F, 0xE0, + 0xFF, 0xC1, 0x8E, 0x30, 0x63, 0x07, 0x30, 0x77, 0x07, 0x70, 0x77, 0x07, 0x30, 0x73, 0x07, 0x30, + 0x73, 0x07, 0xF0, 0x70, 0x27, 0x67, 0x70, 0x33, 0x77, 0x6E, 0x00, 0x6E, 0xE4, 0x00, 0x3F, 0xBF, + 0xFC, 0x2C, 0x07, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x01, 0x80, 0x00, 0x60, 0x78, 0x3C, 0x0C, + 0x00, 0x1C, 0x3E, 0x7C, 0x60, 0x60, 0x73, 0x3F, 0xFE, 0xF8, 0x00, 0xFF, 0xFC, 0x00, 0x6F, 0xF2, + 0x0C, 0xEE, 0x66, 0x67, 0x77, 0x77, 0x73, 0x30, 0x00, 0x18, 0x76, 0x19, 0xFE, 0x7F, 0x1F, 0x07, + 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x0E, 0x03, 0x80, 0x00, 0x00, 0x06, 0x36, 0x67, + 0x67, 0x77, 0x7F, 0xE7, 0xFE, 0x70, 0x03, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, 0x00, 0x38, 0x03, + 0x80, 0x38, 0x00, 0x00, 0x00, 0x07, 0x03, 0xE1, 0xE0, 0x60, 0x38, 0x0F, 0xC1, 0xF0, 0x3C, 0x1C, + 0x06, 0x03, 0x80, 0xE0, 0x1F, 0xE3, 0xF8, 0x00, 0x1E, 0x0F, 0xC7, 0x39, 0x86, 0x61, 0xB8, 0x7E, + 0x1D, 0x86, 0x61, 0x9F, 0xE3, 0xF0, 0x00, 0x00, 0x3F, 0xCF, 0xF0, 0x2C, 0x03, 0x00, 0xE0, 0x38, + 0x0E, 0x03, 0x80, 0xE0, 0x18, 0x06, 0x01, 0x80, 0x70, 0x1C, 0x00, 0x40, 0x0E, 0x07, 0xE0, 0x67, + 0x0E, 0x70, 0xE3, 0x0C, 0x39, 0xC3, 0x98, 0x19, 0x81, 0xF8, 0x1F, 0x00, 0xF0, 0x0F, 0x00, 0xE0, + 0x06, 0x00, 0x00, 0x02, 0x00, 0x60, 0x07, 0x00, 0xF0, 0x0F, 0x00, 0xF8, 0x1F, 0x81, 0x98, 0x39, + 0xC3, 0x9C, 0x30, 0xC7, 0x0E, 0x70, 0xEE, 0x06, 0xE0, 0x70, 0x00, 0x08, 0x0F, 0x87, 0xF1, 0x9C, + 0xE3, 0x38, 0xC7, 0xF1, 0xFC, 0x1F, 0x80, 0xE0, 0x38, 0x06, 0x01, 0x80, 0x60, 0x18, 0x00, 0x00, + 0x10, 0x00, 0xE0, 0x03, 0x98, 0x06, 0x60, 0x1B, 0x80, 0x7E, 0x01, 0xF8, 0x06, 0xE0, 0x39, 0xE3, + 0xC3, 0xFF, 0x07, 0xF0, 0x7D, 0xF0, 0x6F, 0xF2, 0x0C, 0xEE, 0x66, 0x67, 0x77, 0x77, 0x73, 0x30, + 0x00, 0x18, 0x76, 0x19, 0xFE, 0x7F, 0x1F, 0x07, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, + 0x0E, 0x03, 0x80, 0x00, 0x00, 0x06, 0x36, 0x67, 0x67, 0x77, 0x7F, 0xE7, 0xFE, 0x70, 0x03, 0x00, + 0x30, 0x03, 0x00, 0x30, 0x03, 0x00, 0x38, 0x03, 0x80, 0x38, 0x00, 0x00, 0x00, 0x39, 0xE6, 0xF1, + 0xB0, 0x7F, 0x9F, 0xE7, 0xE1, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0x00, + 0x07, 0x01, 0xE0, 0x78, 0x1F, 0x0E, 0xC3, 0x39, 0xC6, 0x61, 0xB8, 0x6E, 0x1F, 0x87, 0xED, 0xDF, + 0xE7, 0xF8, 0x00, 0x3E, 0x7E, 0x60, 0x60, 0x60, 0x73, 0x3F, 0x1E, 0x1C, 0x38, 0x70, 0x60, 0xE0, + 0xC0, 0x00, 0x40, 0x0E, 0x07, 0xE0, 0x67, 0x0E, 0x70, 0xE3, 0x0C, 0x39, 0xC3, 0x98, 0x19, 0x81, + 0xF8, 0x1F, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x06, 0x00, 0x00, 0x02, 0x00, 0x60, 0x07, 0x00, 0xF0, + 0x0F, 0x00, 0xF8, 0x1F, 0x81, 0x98, 0x39, 0xC3, 0x9C, 0x30, 0xC7, 0x0E, 0x70, 0xEE, 0x06, 0xE0, + 0x70, 0x00, 0x08, 0x0F, 0x87, 0xF1, 0x9C, 0xE3, 0x38, 0xC7, 0xF1, 0xFC, 0x1F, 0x80, 0xE0, 0x38, + 0x06, 0x01, 0x80, 0x60, 0x18, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, + 0x00, 0xCE, 0x07, 0x38, 0x18, 0x60, 0xE1, 0xC3, 0xFF, 0x0F, 0xFC, 0x70, 0x39, 0xC0, 0xE6, 0x01, + 0xF8, 0x07, 0x00, 0x00, 0x0C, 0x00, 0x38, 0x00, 0xC0, 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, 0x00, 0xC7, + 0xF7, 0xFD, 0xE3, 0x60, 0xD8, 0x77, 0xFC, 0xFF, 0x0C, 0x01, 0x80, 0xE0, 0x38, 0x07, 0x80, 0x1E, + 0x00, 0x38, 0x00, 0xC0, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, + 0x07, 0x38, 0x18, 0x60, 0xE1, 0xC3, 0xFF, 0x0F, 0xFC, 0x70, 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, + 0x0E, 0x03, 0x80, 0x60, 0x10, 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, + 0xD8, 0x77, 0xFC, 0xFF, 0x0C, 0x00, 0x00, 0x70, 0x01, 0x80, 0x0E, 0x00, 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x00, 0xC0, 0x07, 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, - 0x06, 0x18, 0x38, 0x70, 0xFF, 0xC3, 0xFF, 0x1C, 0x0E, 0x70, 0x39, 0x80, 0x7E, 0x01, 0xC0, 0x07, - 0x00, 0xE0, 0x18, 0x38, 0x0F, 0x06, 0xE3, 0x98, 0x38, 0x3F, 0x9F, 0xF7, 0x0C, 0x03, 0x1F, 0xDF, - 0xF7, 0x8D, 0x83, 0x61, 0xDF, 0xF3, 0xFC, 0x30, 0x01, 0xE0, 0x07, 0x80, 0x0E, 0x00, 0xC0, 0x07, - 0x80, 0x3F, 0x00, 0xCC, 0x00, 0xC0, 0x07, 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, + 0x06, 0x18, 0x38, 0x70, 0xFF, 0xC3, 0xFF, 0x1C, 0x0E, 0x70, 0x39, 0x80, 0x7E, 0x01, 0xC0, 0x00, + 0xE0, 0x38, 0x06, 0x07, 0x00, 0xF0, 0x37, 0x0E, 0x60, 0x70, 0x3F, 0x8F, 0xF9, 0xC3, 0x00, 0x61, + 0xFC, 0xFF, 0x9E, 0x33, 0x06, 0x61, 0xCF, 0xF8, 0xFF, 0x06, 0x00, 0x01, 0xC0, 0x03, 0x00, 0x0E, + 0x00, 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x00, 0xC0, 0x07, 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, + 0x0E, 0xC0, 0x33, 0x81, 0xCE, 0x06, 0x18, 0x38, 0x70, 0xFF, 0xC3, 0xFF, 0x1C, 0x0E, 0x70, 0x39, + 0x80, 0x7E, 0x01, 0xC0, 0x07, 0x00, 0xE0, 0x18, 0x38, 0x0F, 0x06, 0xE3, 0x98, 0x38, 0x3F, 0x9F, + 0xF7, 0x0C, 0x03, 0x1F, 0xDF, 0xF7, 0x8D, 0x83, 0x61, 0xDF, 0xF3, 0xFC, 0x30, 0x01, 0xE0, 0x07, + 0x80, 0x0E, 0x00, 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x00, 0xC0, 0x07, 0x80, 0x1E, 0x00, 0x7C, + 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, 0x06, 0x18, 0x38, 0x70, 0xFF, 0xC3, 0xFF, 0x1C, 0x0E, + 0x70, 0x39, 0x80, 0x7E, 0x01, 0xC0, 0x03, 0x80, 0xE0, 0x18, 0x38, 0x0F, 0x06, 0xE3, 0x98, 0x38, + 0x3F, 0x9F, 0xF7, 0x0C, 0x03, 0x1F, 0xDF, 0xF7, 0x8D, 0x83, 0x61, 0xDF, 0xF3, 0xFC, 0x30, 0x06, + 0x40, 0x3F, 0x00, 0xDC, 0x00, 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x00, 0xC0, 0x07, 0x80, 0x1E, + 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, 0x06, 0x18, 0x38, 0x70, 0xFF, 0xC3, 0xFF, + 0x1C, 0x0E, 0x70, 0x39, 0x80, 0x7E, 0x01, 0xC0, 0x1D, 0x87, 0xE3, 0x30, 0x38, 0x0F, 0x06, 0xE3, + 0x98, 0x38, 0x3F, 0x9F, 0xF7, 0x0C, 0x03, 0x1F, 0xDF, 0xF7, 0x8D, 0x83, 0x61, 0xDF, 0xF3, 0xFC, + 0x30, 0x03, 0x80, 0x1E, 0x00, 0xEC, 0x00, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, + 0xC0, 0x3B, 0x00, 0xCE, 0x07, 0x38, 0x18, 0x60, 0xE1, 0xC3, 0xFF, 0x0F, 0xFC, 0x70, 0x39, 0xC0, + 0xE6, 0x01, 0xF8, 0x07, 0x00, 0x00, 0x0C, 0x00, 0x38, 0x00, 0xC0, 0x0E, 0x07, 0xC3, 0xB8, 0x00, + 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, 0xD8, 0x77, 0xFC, 0xFF, 0x0C, + 0x01, 0x80, 0xE0, 0x38, 0x01, 0xC0, 0x06, 0x00, 0x38, 0x03, 0x10, 0x0C, 0xC0, 0x3F, 0x00, 0x38, + 0x00, 0xC0, 0x07, 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, 0x06, 0x18, + 0x38, 0x70, 0xFF, 0xC3, 0xFF, 0x1C, 0x0E, 0x70, 0x39, 0x80, 0x7E, 0x01, 0xC0, 0x03, 0x01, 0xC0, + 0x60, 0x46, 0x3B, 0x87, 0xC0, 0xE0, 0x38, 0x3F, 0x9F, 0xF7, 0x0C, 0x03, 0x1F, 0xDF, 0xF7, 0x8D, + 0x83, 0x61, 0xDF, 0xF3, 0xFC, 0x30, 0x0E, 0x00, 0x1C, 0x00, 0x30, 0x03, 0x30, 0x0C, 0xC0, 0x3F, + 0x00, 0x30, 0x00, 0xC0, 0x07, 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, + 0x06, 0x18, 0x38, 0x70, 0xFF, 0xC3, 0xFF, 0x1C, 0x0E, 0x70, 0x39, 0x80, 0x7E, 0x01, 0xC0, 0x1C, + 0x07, 0x00, 0xC0, 0xC6, 0x3B, 0x87, 0xC0, 0xE0, 0x38, 0x3F, 0x9F, 0xF7, 0x0C, 0x03, 0x1F, 0xDF, + 0xF7, 0x8D, 0x83, 0x61, 0xDF, 0xF3, 0xFC, 0x30, 0x07, 0x80, 0x1E, 0x00, 0x38, 0x03, 0x10, 0x0C, + 0xC0, 0x3F, 0x00, 0x38, 0x00, 0xC0, 0x07, 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, 0x06, 0x18, 0x38, 0x70, 0xFF, 0xC3, 0xFF, 0x1C, 0x0E, 0x70, 0x39, 0x80, 0x7E, 0x01, - 0xC0, 0x03, 0x80, 0xE0, 0x18, 0x38, 0x0F, 0x06, 0xE3, 0x98, 0x38, 0x3F, 0x9F, 0xF7, 0x0C, 0x03, - 0x1F, 0xDF, 0xF7, 0x8D, 0x83, 0x61, 0xDF, 0xF3, 0xFC, 0x30, 0x06, 0x40, 0x3F, 0x00, 0xDC, 0x00, - 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x00, 0xC0, 0x07, 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, 0x0E, + 0xC0, 0x0E, 0x03, 0x80, 0x60, 0x46, 0x3B, 0x87, 0xC0, 0xE0, 0x38, 0x3F, 0x9F, 0xF7, 0x0C, 0x03, + 0x1F, 0xDF, 0xF7, 0x8D, 0x83, 0x61, 0xDF, 0xF3, 0xFC, 0x30, 0x06, 0x40, 0x3F, 0x00, 0xDC, 0x03, + 0x30, 0x0C, 0xC0, 0x3F, 0x00, 0x30, 0x00, 0xC0, 0x07, 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, 0x06, 0x18, 0x38, 0x70, 0xFF, 0xC3, 0xFF, 0x1C, 0x0E, 0x70, 0x39, 0x80, - 0x7E, 0x01, 0xC0, 0x1D, 0x87, 0xE3, 0x30, 0x38, 0x0F, 0x06, 0xE3, 0x98, 0x38, 0x3F, 0x9F, 0xF7, - 0x0C, 0x03, 0x1F, 0xDF, 0xF7, 0x8D, 0x83, 0x61, 0xDF, 0xF3, 0xFC, 0x30, 0x03, 0x80, 0x1E, 0x00, - 0xEC, 0x00, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x07, + 0x7E, 0x01, 0xC0, 0x1D, 0x87, 0xE3, 0x30, 0x46, 0x3B, 0x87, 0xC0, 0xE0, 0x38, 0x3F, 0x9F, 0xF7, + 0x0C, 0x03, 0x1F, 0xDF, 0xF7, 0x8D, 0x83, 0x61, 0xDF, 0xF3, 0xFC, 0x30, 0x0C, 0xC0, 0x3F, 0x00, + 0x7C, 0x00, 0x40, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x07, 0x38, 0x18, 0x60, 0xE1, 0xC3, 0xFF, 0x0F, 0xFC, 0x70, 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, 0x00, - 0x00, 0x0C, 0x00, 0x38, 0x00, 0xC0, 0x0E, 0x07, 0xC3, 0xB8, 0x00, 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, - 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, 0xD8, 0x77, 0xFC, 0xFF, 0x0C, 0x01, 0x80, 0xE0, 0x38, 0x01, - 0xC0, 0x06, 0x00, 0x38, 0x03, 0x10, 0x0C, 0xC0, 0x3F, 0x00, 0x38, 0x00, 0xC0, 0x07, 0x80, 0x1E, - 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, 0x06, 0x18, 0x38, 0x70, 0xFF, 0xC3, 0xFF, - 0x1C, 0x0E, 0x70, 0x39, 0x80, 0x7E, 0x01, 0xC0, 0x03, 0x01, 0xC0, 0x60, 0x46, 0x3B, 0x87, 0xC0, - 0xE0, 0x38, 0x3F, 0x9F, 0xF7, 0x0C, 0x03, 0x1F, 0xDF, 0xF7, 0x8D, 0x83, 0x61, 0xDF, 0xF3, 0xFC, - 0x30, 0x0E, 0x00, 0x1C, 0x00, 0x30, 0x03, 0x30, 0x0C, 0xC0, 0x3F, 0x00, 0x30, 0x00, 0xC0, 0x07, - 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, 0x06, 0x18, 0x38, 0x70, 0xFF, - 0xC3, 0xFF, 0x1C, 0x0E, 0x70, 0x39, 0x80, 0x7E, 0x01, 0xC0, 0x1C, 0x07, 0x00, 0xC0, 0xC6, 0x3B, - 0x87, 0xC0, 0xE0, 0x38, 0x3F, 0x9F, 0xF7, 0x0C, 0x03, 0x1F, 0xDF, 0xF7, 0x8D, 0x83, 0x61, 0xDF, - 0xF3, 0xFC, 0x30, 0x07, 0x80, 0x1E, 0x00, 0x38, 0x03, 0x10, 0x0C, 0xC0, 0x3F, 0x00, 0x38, 0x00, - 0xC0, 0x07, 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, 0x06, 0x18, 0x38, - 0x70, 0xFF, 0xC3, 0xFF, 0x1C, 0x0E, 0x70, 0x39, 0x80, 0x7E, 0x01, 0xC0, 0x0E, 0x03, 0x80, 0x60, - 0x46, 0x3B, 0x87, 0xC0, 0xE0, 0x38, 0x3F, 0x9F, 0xF7, 0x0C, 0x03, 0x1F, 0xDF, 0xF7, 0x8D, 0x83, - 0x61, 0xDF, 0xF3, 0xFC, 0x30, 0x06, 0x40, 0x3F, 0x00, 0xDC, 0x03, 0x30, 0x0C, 0xC0, 0x3F, 0x00, - 0x30, 0x00, 0xC0, 0x07, 0x80, 0x1E, 0x00, 0x7C, 0x03, 0xF0, 0x0E, 0xC0, 0x33, 0x81, 0xCE, 0x06, - 0x18, 0x38, 0x70, 0xFF, 0xC3, 0xFF, 0x1C, 0x0E, 0x70, 0x39, 0x80, 0x7E, 0x01, 0xC0, 0x1D, 0x87, - 0xE3, 0x30, 0x46, 0x3B, 0x87, 0xC0, 0xE0, 0x38, 0x3F, 0x9F, 0xF7, 0x0C, 0x03, 0x1F, 0xDF, 0xF7, - 0x8D, 0x83, 0x61, 0xDF, 0xF3, 0xFC, 0x30, 0x0C, 0xC0, 0x3F, 0x00, 0x7C, 0x00, 0x40, 0x03, 0x00, - 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x07, 0x38, 0x18, 0x60, 0xE1, 0xC3, - 0xFF, 0x0F, 0xFC, 0x70, 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, 0x00, 0x00, 0x0C, 0x00, 0x38, 0x00, - 0xC0, 0x31, 0x87, 0xE1, 0xF0, 0x10, 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, 0x00, 0xC7, 0xF7, 0xFD, 0xE3, - 0x60, 0xD8, 0x77, 0xFC, 0xFF, 0x0C, 0x01, 0x80, 0xE0, 0x38, 0x7F, 0xDF, 0xFF, 0xFF, 0x70, 0x0E, - 0x01, 0xC0, 0x38, 0x07, 0xFE, 0xFF, 0xDF, 0xF3, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xFF, 0xFF, - 0x00, 0x01, 0xC0, 0x38, 0x07, 0x00, 0x0F, 0x03, 0xF8, 0xFF, 0x38, 0x76, 0x06, 0xFF, 0xDF, 0xFB, - 0x00, 0x60, 0x0E, 0x1C, 0xFF, 0x8F, 0xE0, 0x60, 0x0C, 0x03, 0xC0, 0x30, 0x1E, 0x01, 0xC0, 0x18, - 0x02, 0x07, 0xFD, 0xFF, 0xFF, 0xF7, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x7F, 0xEF, 0xFD, 0xFF, 0x38, - 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, 0xF0, 0x0E, 0x01, 0xE0, 0x1C, 0x03, 0x00, 0xF0, 0x3F, - 0x8F, 0xF3, 0x87, 0x60, 0x6F, 0xFD, 0xFF, 0xB0, 0x06, 0x00, 0xE1, 0xCF, 0xF8, 0xFE, 0x06, 0x00, - 0x19, 0x87, 0xF0, 0xDC, 0x00, 0x07, 0xFD, 0xFF, 0xFF, 0xF7, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x7F, - 0xEF, 0xFD, 0xFF, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, 0xF0, 0x0C, 0x83, 0xF0, 0x7E, - 0x00, 0x00, 0xF0, 0x3F, 0x8F, 0xF3, 0x87, 0x60, 0x6F, 0xFD, 0xFF, 0xB0, 0x06, 0x00, 0xE1, 0xCF, - 0xF8, 0xFE, 0x06, 0x00, 0x00, 0xC0, 0x38, 0x06, 0x07, 0x01, 0xF0, 0x37, 0x0C, 0x63, 0xFE, 0xFF, - 0xFF, 0xFB, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xF7, 0xFE, 0xFF, 0x9C, 0x03, 0x80, 0x70, 0x0E, - 0x01, 0xFF, 0xFF, 0xF8, 0x00, 0xE0, 0x18, 0x07, 0x03, 0x00, 0xF0, 0x3F, 0x06, 0x60, 0x78, 0x1F, - 0xC7, 0xF9, 0xC3, 0xB0, 0x37, 0xFE, 0xFF, 0xD8, 0x03, 0x00, 0x70, 0xE7, 0xFC, 0x7F, 0x03, 0x00, - 0x07, 0x00, 0x70, 0x06, 0x07, 0x01, 0xF0, 0x37, 0x0C, 0x63, 0xFE, 0xFF, 0xFF, 0xFB, 0x80, 0x70, - 0x0E, 0x01, 0xC0, 0x3F, 0xF7, 0xFE, 0xFF, 0x9C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xFF, 0xFF, 0xF8, - 0x03, 0x80, 0x30, 0x07, 0x03, 0x00, 0xF0, 0x3F, 0x06, 0x60, 0x78, 0x1F, 0xC7, 0xF9, 0xC3, 0xB0, - 0x37, 0xFE, 0xFF, 0xD8, 0x03, 0x00, 0x70, 0xE7, 0xFC, 0x7F, 0x03, 0x00, 0x07, 0x80, 0x70, 0x06, - 0x07, 0x01, 0xF0, 0x37, 0x0C, 0x63, 0xFE, 0xFF, 0xFF, 0xFB, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, - 0xF7, 0xFE, 0xFF, 0x9C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xFF, 0xFF, 0xF8, 0x03, 0xC0, 0x78, 0x06, - 0x03, 0x00, 0xF0, 0x3F, 0x06, 0x60, 0x78, 0x1F, 0xC7, 0xF9, 0xC3, 0xB0, 0x37, 0xFE, 0xFF, 0xD8, - 0x03, 0x00, 0x70, 0xE7, 0xFC, 0x7F, 0x03, 0x00, 0x1D, 0x87, 0xF0, 0xDC, 0x07, 0x01, 0xF0, 0x37, - 0x0C, 0x63, 0xFE, 0xFF, 0xFF, 0xFB, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xF7, 0xFE, 0xFF, 0x9C, - 0x03, 0x80, 0x70, 0x0E, 0x01, 0xFF, 0xFF, 0xF8, 0x0D, 0x83, 0xF0, 0x6E, 0x03, 0x00, 0xF0, 0x3F, - 0x06, 0x60, 0x78, 0x1F, 0xC7, 0xF9, 0xC3, 0xB0, 0x37, 0xFE, 0xFF, 0xD8, 0x03, 0x00, 0x70, 0xE7, - 0xFC, 0x7F, 0x03, 0x00, 0x0E, 0x03, 0xE0, 0xEE, 0x00, 0x07, 0xFD, 0xFF, 0xFF, 0xF7, 0x00, 0xE0, - 0x1C, 0x03, 0x80, 0x7F, 0xEF, 0xFD, 0xFF, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, 0xF0, - 0x00, 0x1C, 0x03, 0x80, 0x70, 0x07, 0x01, 0xE0, 0x76, 0x00, 0x00, 0xF0, 0x3F, 0x8F, 0xF3, 0x87, - 0x60, 0x6F, 0xFD, 0xFF, 0xB0, 0x06, 0x00, 0xE1, 0xCF, 0xF8, 0xFE, 0x06, 0x00, 0xC0, 0x3C, 0x03, - 0x00, 0xEF, 0x66, 0x4E, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xF7, 0x32, 0x27, 0x77, 0x77, - 0x77, 0x77, 0x77, 0x5F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xF0, 0xDF, 0x85, 0xFF, 0xFF, 0xFF, - 0xFF, 0x1F, 0xF0, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, - 0x07, 0xC0, 0x1F, 0x00, 0x7E, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, - 0x00, 0x0C, 0x00, 0x78, 0x00, 0xC0, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, - 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x60, 0x07, 0x00, 0x60, 0x07, 0x00, - 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, - 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7E, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, - 0xF8, 0x03, 0x00, 0x0F, 0x00, 0xF0, 0x07, 0x00, 0x60, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x0E, 0x70, - 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x00, 0x70, 0x01, - 0x80, 0x0E, 0x00, 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x01, 0xE0, 0x1F, 0xE0, 0xFF, 0xC7, 0x87, - 0x9C, 0x0E, 0xE0, 0x1F, 0x80, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x80, 0x7E, 0x01, 0xDC, 0x0E, - 0x78, 0x78, 0xFF, 0xC1, 0xFE, 0x00, 0xC0, 0x00, 0xE0, 0x0C, 0x01, 0xC0, 0x60, 0x0F, 0x01, 0xF8, - 0x19, 0x80, 0xF0, 0x1F, 0xC3, 0xFE, 0x70, 0xE7, 0x06, 0x60, 0x76, 0x07, 0x60, 0x77, 0x06, 0x70, - 0xE3, 0xFC, 0x1F, 0x80, 0x60, 0x01, 0xC0, 0x07, 0x00, 0x0E, 0x00, 0xC0, 0x07, 0x80, 0x3F, 0x00, - 0xCC, 0x01, 0xE0, 0x1F, 0xE0, 0xFF, 0xC7, 0x87, 0x9C, 0x0E, 0xE0, 0x1F, 0x80, 0x7C, 0x01, 0xF0, - 0x07, 0xC0, 0x1F, 0x80, 0x7E, 0x01, 0xDC, 0x0E, 0x78, 0x78, 0xFF, 0xC1, 0xFE, 0x00, 0xC0, 0x03, - 0x80, 0x18, 0x01, 0xC0, 0x60, 0x0F, 0x01, 0xF8, 0x19, 0x80, 0xF0, 0x1F, 0xC3, 0xFE, 0x70, 0xE7, - 0x06, 0x60, 0x76, 0x07, 0x60, 0x77, 0x06, 0x70, 0xE3, 0xFC, 0x1F, 0x80, 0x60, 0x01, 0xE0, 0x07, - 0x80, 0x0C, 0x00, 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x01, 0xE0, 0x1F, 0xE0, 0xFF, 0xC7, 0x87, - 0x9C, 0x0E, 0xE0, 0x1F, 0x80, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x80, 0x7E, 0x01, 0xDC, 0x0E, - 0x78, 0x78, 0xFF, 0xC1, 0xFE, 0x00, 0xC0, 0x03, 0xC0, 0x3C, 0x01, 0xC0, 0x60, 0x0F, 0x01, 0xF8, - 0x19, 0x80, 0xF0, 0x1F, 0xC3, 0xFE, 0x70, 0xE7, 0x06, 0x60, 0x76, 0x07, 0x60, 0x77, 0x06, 0x70, - 0xE3, 0xFC, 0x1F, 0x80, 0x60, 0x0E, 0xC0, 0x3F, 0x00, 0xD8, 0x00, 0xC0, 0x07, 0x80, 0x3F, 0x00, - 0xCC, 0x01, 0xE0, 0x1F, 0xE0, 0xFF, 0xC7, 0x87, 0x9C, 0x0E, 0xE0, 0x1F, 0x80, 0x7C, 0x01, 0xF0, - 0x07, 0xC0, 0x1F, 0x80, 0x7E, 0x01, 0xDC, 0x0E, 0x78, 0x78, 0xFF, 0xC1, 0xFE, 0x00, 0xC0, 0x0C, - 0x81, 0xF8, 0x1B, 0x80, 0x60, 0x0F, 0x01, 0xF8, 0x19, 0x80, 0xF0, 0x1F, 0xC3, 0xFE, 0x70, 0xE7, - 0x06, 0x60, 0x76, 0x07, 0x60, 0x77, 0x06, 0x70, 0xE3, 0xFC, 0x1F, 0x80, 0x60, 0x07, 0x00, 0x1E, - 0x00, 0xDC, 0x00, 0x00, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, - 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7E, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, - 0x03, 0x00, 0x0C, 0x00, 0x78, 0x00, 0xC0, 0x07, 0x00, 0xF8, 0x1D, 0x80, 0x00, 0x0F, 0x01, 0xFC, - 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, - 0x00, 0x60, 0x07, 0x00, 0x60, 0x01, 0xC0, 0x06, 0x00, 0x38, 0xC0, 0xC3, 0x07, 0xFC, 0x7F, 0xF3, - 0xFF, 0x9E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7E, 0x01, 0xF8, - 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x03, 0x80, 0x30, 0x07, 0x30, 0x67, - 0x0F, 0xF1, 0xFF, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, - 0xC1, 0xF8, 0x06, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x70, 0xC0, 0xC3, 0x07, 0xFC, 0x7F, 0xF3, 0xFF, - 0x9E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7E, 0x01, 0xF8, 0x07, - 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x1C, 0x00, 0xC0, 0x0E, 0x30, 0x67, 0x0F, - 0xF1, 0xFF, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, - 0xF8, 0x06, 0x00, 0x07, 0x00, 0x1E, 0x00, 0x30, 0xC0, 0xC3, 0x07, 0xFC, 0x7F, 0xF3, 0xFF, 0x9E, + 0x00, 0x0C, 0x00, 0x38, 0x00, 0xC0, 0x31, 0x87, 0xE1, 0xF0, 0x10, 0x0E, 0x0F, 0xE7, 0xFD, 0xC3, + 0x00, 0xC7, 0xF7, 0xFD, 0xE3, 0x60, 0xD8, 0x77, 0xFC, 0xFF, 0x0C, 0x01, 0x80, 0xE0, 0x38, 0x7F, + 0xDF, 0xFF, 0xFF, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0xFE, 0xFF, 0xDF, 0xF3, 0x80, 0x70, 0x0E, + 0x01, 0xC0, 0x3F, 0xFF, 0xFF, 0x00, 0x01, 0xC0, 0x38, 0x07, 0x00, 0x0F, 0x03, 0xF8, 0xFF, 0x38, + 0x76, 0x06, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0E, 0x1C, 0xFF, 0x8F, 0xE0, 0x60, 0x0C, 0x03, 0xC0, + 0x30, 0x1E, 0x01, 0xC0, 0x18, 0x02, 0x07, 0xFD, 0xFF, 0xFF, 0xF7, 0x00, 0xE0, 0x1C, 0x03, 0x80, + 0x7F, 0xEF, 0xFD, 0xFF, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, 0xF0, 0x0E, 0x01, 0xE0, + 0x1C, 0x03, 0x00, 0xF0, 0x3F, 0x8F, 0xF3, 0x87, 0x60, 0x6F, 0xFD, 0xFF, 0xB0, 0x06, 0x00, 0xE1, + 0xCF, 0xF8, 0xFE, 0x06, 0x00, 0x19, 0x87, 0xF0, 0xDC, 0x00, 0x07, 0xFD, 0xFF, 0xFF, 0xF7, 0x00, + 0xE0, 0x1C, 0x03, 0x80, 0x7F, 0xEF, 0xFD, 0xFF, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, + 0xF0, 0x0C, 0x83, 0xF0, 0x7E, 0x00, 0x00, 0xF0, 0x3F, 0x8F, 0xF3, 0x87, 0x60, 0x6F, 0xFD, 0xFF, + 0xB0, 0x06, 0x00, 0xE1, 0xCF, 0xF8, 0xFE, 0x06, 0x00, 0x00, 0xC0, 0x38, 0x06, 0x07, 0x01, 0xF0, + 0x37, 0x0C, 0x63, 0xFE, 0xFF, 0xFF, 0xFB, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xF7, 0xFE, 0xFF, + 0x9C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xFF, 0xFF, 0xF8, 0x00, 0xE0, 0x18, 0x07, 0x03, 0x00, 0xF0, + 0x3F, 0x06, 0x60, 0x78, 0x1F, 0xC7, 0xF9, 0xC3, 0xB0, 0x37, 0xFE, 0xFF, 0xD8, 0x03, 0x00, 0x70, + 0xE7, 0xFC, 0x7F, 0x03, 0x00, 0x07, 0x00, 0x70, 0x06, 0x07, 0x01, 0xF0, 0x37, 0x0C, 0x63, 0xFE, + 0xFF, 0xFF, 0xFB, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xF7, 0xFE, 0xFF, 0x9C, 0x03, 0x80, 0x70, + 0x0E, 0x01, 0xFF, 0xFF, 0xF8, 0x03, 0x80, 0x30, 0x07, 0x03, 0x00, 0xF0, 0x3F, 0x06, 0x60, 0x78, + 0x1F, 0xC7, 0xF9, 0xC3, 0xB0, 0x37, 0xFE, 0xFF, 0xD8, 0x03, 0x00, 0x70, 0xE7, 0xFC, 0x7F, 0x03, + 0x00, 0x07, 0x80, 0x70, 0x06, 0x07, 0x01, 0xF0, 0x37, 0x0C, 0x63, 0xFE, 0xFF, 0xFF, 0xFB, 0x80, + 0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xF7, 0xFE, 0xFF, 0x9C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xFF, 0xFF, + 0xF8, 0x03, 0xC0, 0x78, 0x06, 0x03, 0x00, 0xF0, 0x3F, 0x06, 0x60, 0x78, 0x1F, 0xC7, 0xF9, 0xC3, + 0xB0, 0x37, 0xFE, 0xFF, 0xD8, 0x03, 0x00, 0x70, 0xE7, 0xFC, 0x7F, 0x03, 0x00, 0x1D, 0x87, 0xF0, + 0xDC, 0x07, 0x01, 0xF0, 0x37, 0x0C, 0x63, 0xFE, 0xFF, 0xFF, 0xFB, 0x80, 0x70, 0x0E, 0x01, 0xC0, + 0x3F, 0xF7, 0xFE, 0xFF, 0x9C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xFF, 0xFF, 0xF8, 0x0D, 0x83, 0xF0, + 0x6E, 0x03, 0x00, 0xF0, 0x3F, 0x06, 0x60, 0x78, 0x1F, 0xC7, 0xF9, 0xC3, 0xB0, 0x37, 0xFE, 0xFF, + 0xD8, 0x03, 0x00, 0x70, 0xE7, 0xFC, 0x7F, 0x03, 0x00, 0x0E, 0x03, 0xE0, 0xEE, 0x00, 0x07, 0xFD, + 0xFF, 0xFF, 0xF7, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x7F, 0xEF, 0xFD, 0xFF, 0x38, 0x07, 0x00, 0xE0, + 0x1C, 0x03, 0xFF, 0xFF, 0xF0, 0x00, 0x1C, 0x03, 0x80, 0x70, 0x07, 0x01, 0xE0, 0x76, 0x00, 0x00, + 0xF0, 0x3F, 0x8F, 0xF3, 0x87, 0x60, 0x6F, 0xFD, 0xFF, 0xB0, 0x06, 0x00, 0xE1, 0xCF, 0xF8, 0xFE, + 0x06, 0x00, 0xC0, 0x3C, 0x03, 0x00, 0xEF, 0x66, 0x4E, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, + 0xF7, 0x32, 0x27, 0x77, 0x77, 0x77, 0x77, 0x77, 0x5F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xF0, + 0xDF, 0x85, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, + 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7E, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, + 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x0C, 0x00, 0x78, 0x00, 0xC0, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, + 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x60, + 0x07, 0x00, 0x60, 0x07, 0x00, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7E, 0x01, 0xF8, 0x07, 0x70, - 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x0F, 0x00, 0xF0, 0x07, 0x30, 0x67, 0x0F, 0xF1, - 0xFF, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, - 0x06, 0x00, 0x06, 0xC0, 0x3F, 0x00, 0xFC, 0xC0, 0x03, 0x07, 0xFC, 0x7F, 0xF3, 0xFF, 0x9E, 0x1E, + 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x0F, 0x00, 0xF0, 0x07, 0x00, 0x60, 0x0F, 0x01, + 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, + 0x06, 0x00, 0x00, 0x70, 0x01, 0x80, 0x0E, 0x00, 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x01, 0xE0, + 0x1F, 0xE0, 0xFF, 0xC7, 0x87, 0x9C, 0x0E, 0xE0, 0x1F, 0x80, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, + 0x80, 0x7E, 0x01, 0xDC, 0x0E, 0x78, 0x78, 0xFF, 0xC1, 0xFE, 0x00, 0xC0, 0x00, 0xE0, 0x0C, 0x01, + 0xC0, 0x60, 0x0F, 0x01, 0xF8, 0x19, 0x80, 0xF0, 0x1F, 0xC3, 0xFE, 0x70, 0xE7, 0x06, 0x60, 0x76, + 0x07, 0x60, 0x77, 0x06, 0x70, 0xE3, 0xFC, 0x1F, 0x80, 0x60, 0x01, 0xC0, 0x07, 0x00, 0x0E, 0x00, + 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x01, 0xE0, 0x1F, 0xE0, 0xFF, 0xC7, 0x87, 0x9C, 0x0E, 0xE0, + 0x1F, 0x80, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x80, 0x7E, 0x01, 0xDC, 0x0E, 0x78, 0x78, 0xFF, + 0xC1, 0xFE, 0x00, 0xC0, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x60, 0x0F, 0x01, 0xF8, 0x19, 0x80, 0xF0, + 0x1F, 0xC3, 0xFE, 0x70, 0xE7, 0x06, 0x60, 0x76, 0x07, 0x60, 0x77, 0x06, 0x70, 0xE3, 0xFC, 0x1F, + 0x80, 0x60, 0x01, 0xE0, 0x07, 0x80, 0x0C, 0x00, 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x01, 0xE0, + 0x1F, 0xE0, 0xFF, 0xC7, 0x87, 0x9C, 0x0E, 0xE0, 0x1F, 0x80, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, + 0x80, 0x7E, 0x01, 0xDC, 0x0E, 0x78, 0x78, 0xFF, 0xC1, 0xFE, 0x00, 0xC0, 0x03, 0xC0, 0x3C, 0x01, + 0xC0, 0x60, 0x0F, 0x01, 0xF8, 0x19, 0x80, 0xF0, 0x1F, 0xC3, 0xFE, 0x70, 0xE7, 0x06, 0x60, 0x76, + 0x07, 0x60, 0x77, 0x06, 0x70, 0xE3, 0xFC, 0x1F, 0x80, 0x60, 0x0E, 0xC0, 0x3F, 0x00, 0xD8, 0x00, + 0xC0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x01, 0xE0, 0x1F, 0xE0, 0xFF, 0xC7, 0x87, 0x9C, 0x0E, 0xE0, + 0x1F, 0x80, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x80, 0x7E, 0x01, 0xDC, 0x0E, 0x78, 0x78, 0xFF, + 0xC1, 0xFE, 0x00, 0xC0, 0x0C, 0x81, 0xF8, 0x1B, 0x80, 0x60, 0x0F, 0x01, 0xF8, 0x19, 0x80, 0xF0, + 0x1F, 0xC3, 0xFE, 0x70, 0xE7, 0x06, 0x60, 0x76, 0x07, 0x60, 0x77, 0x06, 0x70, 0xE3, 0xFC, 0x1F, + 0x80, 0x60, 0x07, 0x00, 0x1E, 0x00, 0xDC, 0x00, 0x00, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7E, 0x01, 0xF8, 0x07, 0x70, 0x39, - 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x0C, 0x81, 0xF8, 0x1B, 0xB0, 0x07, 0x0F, 0xF1, 0xFF, - 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, - 0x00, 0x00, 0x0C, 0x00, 0x30, 0x7F, 0xC7, 0xFF, 0x3F, 0xF9, 0xE1, 0xE7, 0x03, 0xB8, 0x07, 0xE0, - 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xE0, 0x1F, 0x80, 0x77, 0x03, 0x9E, 0x1E, 0x3F, 0xF0, 0x7F, - 0x80, 0x30, 0x00, 0xC0, 0x07, 0x80, 0x0C, 0x00, 0x00, 0x30, 0x07, 0x0F, 0xF1, 0xFF, 0x3F, 0xE7, - 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x60, - 0x07, 0x00, 0x60, 0x40, 0x17, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, - 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, 0x76, 0x03, 0xB8, 0x38, 0xFF, 0xC3, 0xFC, 0x07, 0x00, 0x18, - 0x01, 0xC0, 0x0E, 0x00, 0x40, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, - 0x77, 0xFC, 0xFF, 0x08, 0x03, 0x00, 0xE0, 0x30, 0x07, 0x00, 0x38, 0x00, 0xC0, 0x04, 0x04, 0x01, - 0x70, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, - 0xFC, 0x07, 0x60, 0x3B, 0x83, 0x8F, 0xFC, 0x3F, 0xC0, 0x70, 0x00, 0x1C, 0x07, 0x80, 0xE0, 0x30, - 0x40, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x77, 0xFC, 0xFF, 0x08, - 0x00, 0x01, 0x80, 0x07, 0x00, 0x0C, 0x18, 0x18, 0x74, 0x01, 0xFC, 0x07, 0xF8, 0x0F, 0x70, 0x1C, - 0xE0, 0x39, 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0E, 0x70, 0x1C, 0x60, - 0x38, 0xE0, 0xE0, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, 0x07, 0x00, 0x60, 0x0E, 0x70, 0xC7, 0x40, - 0xFE, 0x1E, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0x7F, 0xC3, - 0xFC, 0x08, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x18, 0x18, 0x10, 0x74, 0x01, 0xFC, 0x07, 0xF8, 0x0F, - 0x70, 0x1C, 0xE0, 0x39, 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0E, 0x70, - 0x1C, 0x60, 0x38, 0xE0, 0xE0, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, 0x38, 0x01, 0x80, 0x1C, 0x70, - 0xC7, 0x40, 0xFE, 0x1E, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, - 0x7F, 0xC3, 0xFC, 0x08, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x0C, 0x18, 0x10, 0x74, 0x01, 0xFC, 0x07, - 0xF8, 0x0F, 0x70, 0x1C, 0xE0, 0x39, 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, - 0x0E, 0x70, 0x1C, 0x60, 0x38, 0xE0, 0xE0, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, 0x1C, 0x01, 0xE0, - 0x0E, 0x70, 0xC7, 0x40, 0xFE, 0x1E, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, - 0xCE, 0x1C, 0x7F, 0xC3, 0xFC, 0x08, 0x00, 0x0C, 0xC0, 0x1F, 0x80, 0x6E, 0x18, 0x00, 0x74, 0x01, - 0xFC, 0x07, 0xF8, 0x0F, 0x70, 0x1C, 0xE0, 0x39, 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, - 0x07, 0x38, 0x0E, 0x70, 0x1C, 0x60, 0x38, 0xE0, 0xE0, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, 0x19, - 0x03, 0xF0, 0x3F, 0x70, 0x07, 0x40, 0xFE, 0x1E, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, - 0x1C, 0xE1, 0xCE, 0x1C, 0x7F, 0xC3, 0xFC, 0x08, 0x00, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x7F, 0x01, - 0xFE, 0x03, 0xDC, 0x07, 0x38, 0x0E, 0x70, 0x1C, 0xE0, 0x39, 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, - 0x03, 0x9C, 0x07, 0x18, 0x0E, 0x38, 0x38, 0x3F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x18, 0x00, 0x70, - 0x00, 0xE0, 0x00, 0x00, 0x70, 0x07, 0x40, 0xFE, 0x1E, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, - 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0x7F, 0xC3, 0xFC, 0x08, 0x00, 0xC0, 0x0E, 0x00, 0xC0, 0x0E, 0x00, - 0x18, 0x00, 0x70, 0x00, 0xC0, 0x40, 0x0B, 0x80, 0xF7, 0x03, 0x8E, 0x1C, 0x38, 0x70, 0x73, 0x80, + 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x0C, 0x00, 0x78, 0x00, 0xC0, 0x07, 0x00, 0xF8, 0x1D, + 0x80, 0x00, 0x0F, 0x01, 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, + 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x60, 0x07, 0x00, 0x60, 0x01, 0xC0, 0x06, 0x00, 0x38, 0xC0, + 0xC3, 0x07, 0xFC, 0x7F, 0xF3, 0xFF, 0x9E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, + 0x1F, 0x00, 0x7E, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x03, + 0x80, 0x30, 0x07, 0x30, 0x67, 0x0F, 0xF1, 0xFF, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, + 0x07, 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x70, 0xC0, 0xC3, + 0x07, 0xFC, 0x7F, 0xF3, 0xFF, 0x9E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, + 0x00, 0x7E, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x1C, 0x00, + 0xC0, 0x0E, 0x30, 0x67, 0x0F, 0xF1, 0xFF, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, + 0x70, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x07, 0x00, 0x1E, 0x00, 0x30, 0xC0, 0xC3, 0x07, + 0xFC, 0x7F, 0xF3, 0xFF, 0x9E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, + 0x7E, 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x0F, 0x00, 0xF0, + 0x07, 0x30, 0x67, 0x0F, 0xF1, 0xFF, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, + 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x06, 0xC0, 0x3F, 0x00, 0xFC, 0xC0, 0x03, 0x07, 0xFC, + 0x7F, 0xF3, 0xFF, 0x9E, 0x1E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7E, + 0x01, 0xF8, 0x07, 0x70, 0x39, 0xE1, 0xE3, 0xFF, 0x07, 0xF8, 0x03, 0x00, 0x0C, 0x81, 0xF8, 0x1B, + 0xB0, 0x07, 0x0F, 0xF1, 0xFF, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, + 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x30, 0x7F, 0xC7, 0xFF, 0x3F, 0xF9, 0xE1, + 0xE7, 0x03, 0xB8, 0x07, 0xE0, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xE0, 0x1F, 0x80, 0x77, 0x03, + 0x9E, 0x1E, 0x3F, 0xF0, 0x7F, 0x80, 0x30, 0x00, 0xC0, 0x07, 0x80, 0x0C, 0x00, 0x00, 0x30, 0x07, + 0x0F, 0xF1, 0xFF, 0x3F, 0xE7, 0x0E, 0x70, 0x66, 0x07, 0x60, 0x76, 0x07, 0x70, 0x67, 0x0E, 0x3F, + 0xC1, 0xF8, 0x06, 0x00, 0x60, 0x07, 0x00, 0x60, 0x40, 0x17, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, + 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, 0x76, 0x03, 0xB8, 0x38, 0xFF, + 0xC3, 0xFC, 0x07, 0x00, 0x18, 0x01, 0xC0, 0x0E, 0x00, 0x40, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, + 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x77, 0xFC, 0xFF, 0x08, 0x03, 0x00, 0xE0, 0x30, 0x07, 0x00, 0x38, + 0x00, 0xC0, 0x04, 0x04, 0x01, 0x70, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, + 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0x60, 0x3B, 0x83, 0x8F, 0xFC, 0x3F, 0xC0, 0x70, 0x00, + 0x1C, 0x07, 0x80, 0xE0, 0x30, 0x40, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, + 0xF8, 0x77, 0xFC, 0xFF, 0x08, 0x00, 0x01, 0x80, 0x07, 0x00, 0x0C, 0x18, 0x18, 0x74, 0x01, 0xFC, + 0x07, 0xF8, 0x0F, 0x70, 0x1C, 0xE0, 0x39, 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, + 0x38, 0x0E, 0x70, 0x1C, 0x60, 0x38, 0xE0, 0xE0, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, 0x07, 0x00, + 0x60, 0x0E, 0x70, 0xC7, 0x40, 0xFE, 0x1E, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, + 0xE1, 0xCE, 0x1C, 0x7F, 0xC3, 0xFC, 0x08, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x18, 0x18, 0x10, 0x74, + 0x01, 0xFC, 0x07, 0xF8, 0x0F, 0x70, 0x1C, 0xE0, 0x39, 0xC0, 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, + 0x9C, 0x07, 0x38, 0x0E, 0x70, 0x1C, 0x60, 0x38, 0xE0, 0xE0, 0xFF, 0xC0, 0xFF, 0x00, 0x70, 0x00, + 0x38, 0x01, 0x80, 0x1C, 0x70, 0xC7, 0x40, 0xFE, 0x1E, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, + 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0x7F, 0xC3, 0xFC, 0x08, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x0C, 0x18, + 0x10, 0x74, 0x01, 0xFC, 0x07, 0xF8, 0x0F, 0x70, 0x1C, 0xE0, 0x39, 0xC0, 0x73, 0x80, 0xE7, 0x01, + 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0E, 0x70, 0x1C, 0x60, 0x38, 0xE0, 0xE0, 0xFF, 0xC0, 0xFF, 0x00, + 0x70, 0x00, 0x1C, 0x01, 0xE0, 0x0E, 0x70, 0xC7, 0x40, 0xFE, 0x1E, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, + 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0x7F, 0xC3, 0xFC, 0x08, 0x00, 0x0C, 0xC0, 0x1F, 0x80, + 0x6E, 0x18, 0x00, 0x74, 0x01, 0xFC, 0x07, 0xF8, 0x0F, 0x70, 0x1C, 0xE0, 0x39, 0xC0, 0x73, 0x80, + 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0E, 0x70, 0x1C, 0x60, 0x38, 0xE0, 0xE0, 0xFF, 0xC0, + 0xFF, 0x00, 0x70, 0x00, 0x19, 0x03, 0xF0, 0x3F, 0x70, 0x07, 0x40, 0xFE, 0x1E, 0xE1, 0xCE, 0x1C, + 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0x7F, 0xC3, 0xFC, 0x08, 0x00, 0x00, 0x06, + 0x00, 0x1D, 0x00, 0x7F, 0x01, 0xFE, 0x03, 0xDC, 0x07, 0x38, 0x0E, 0x70, 0x1C, 0xE0, 0x39, 0xC0, + 0x73, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x18, 0x0E, 0x38, 0x38, 0x3F, 0xF0, 0x3F, 0xC0, + 0x1C, 0x00, 0x18, 0x00, 0x70, 0x00, 0xE0, 0x00, 0x00, 0x70, 0x07, 0x40, 0xFE, 0x1E, 0xE1, 0xCE, + 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0x7F, 0xC3, 0xFC, 0x08, 0x00, 0xC0, + 0x0E, 0x00, 0xC0, 0x0E, 0x00, 0x18, 0x00, 0x70, 0x00, 0xC0, 0x40, 0x0B, 0x80, 0xF7, 0x03, 0x8E, + 0x1C, 0x38, 0x70, 0x73, 0x80, 0xFC, 0x03, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, + 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x1C, 0x03, 0x80, 0x38, 0x03, 0x04, 0x07, 0xC1, 0xD8, 0x3B, + 0x86, 0x31, 0xC7, 0x38, 0xE6, 0x0D, 0xC1, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0E, 0x01, 0x80, 0xF0, + 0x3C, 0x07, 0x00, 0x40, 0x0B, 0x80, 0xF7, 0x03, 0x8E, 0x1C, 0x38, 0x70, 0x73, 0x80, 0xFC, 0x03, + 0xF0, 0x07, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x00, + 0x00, 0x0C, 0x00, 0x78, 0x00, 0xC0, 0x40, 0x7C, 0x1D, 0x83, 0xB8, 0x63, 0x1C, 0x73, 0x8E, 0x60, + 0xDC, 0x1F, 0x03, 0xE0, 0x3C, 0x07, 0x00, 0xE0, 0x19, 0x8F, 0x73, 0xCE, 0x70, 0x00, 0x07, 0x00, + 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x40, 0x0B, 0x80, 0xF7, 0x03, 0x8E, 0x1C, 0x38, 0x70, 0x73, 0x80, + 0xFC, 0x03, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, + 0xC0, 0x0E, 0x01, 0xE0, 0x18, 0x03, 0x04, 0x07, 0xC1, 0xD8, 0x3B, 0x86, 0x31, 0xC7, 0x38, 0xE6, + 0x0D, 0xC1, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0E, 0x01, 0x80, 0xF0, 0x3C, 0x07, 0x00, 0x06, 0x40, + 0x3F, 0x00, 0xFC, 0x00, 0x00, 0x40, 0x0B, 0x80, 0xF7, 0x03, 0x8E, 0x1C, 0x38, 0x70, 0x73, 0x80, 0xFC, 0x03, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, - 0xC0, 0x1C, 0x03, 0x80, 0x38, 0x03, 0x04, 0x07, 0xC1, 0xD8, 0x3B, 0x86, 0x31, 0xC7, 0x38, 0xE6, - 0x0D, 0xC1, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0E, 0x01, 0x80, 0xF0, 0x3C, 0x07, 0x00, 0x40, 0x0B, - 0x80, 0xF7, 0x03, 0x8E, 0x1C, 0x38, 0x70, 0x73, 0x80, 0xFC, 0x03, 0xF0, 0x07, 0x80, 0x1E, 0x00, - 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x00, 0x0C, 0x00, 0x78, 0x00, - 0xC0, 0x40, 0x7C, 0x1D, 0x83, 0xB8, 0x63, 0x1C, 0x73, 0x8E, 0x60, 0xDC, 0x1F, 0x03, 0xE0, 0x3C, - 0x07, 0x00, 0xE0, 0x19, 0x8F, 0x73, 0xCE, 0x70, 0x00, 0x07, 0x00, 0x1E, 0x00, 0x30, 0x00, 0xC0, - 0x40, 0x0B, 0x80, 0xF7, 0x03, 0x8E, 0x1C, 0x38, 0x70, 0x73, 0x80, 0xFC, 0x03, 0xF0, 0x07, 0x80, - 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x0E, 0x01, 0xE0, 0x18, - 0x03, 0x04, 0x07, 0xC1, 0xD8, 0x3B, 0x86, 0x31, 0xC7, 0x38, 0xE6, 0x0D, 0xC1, 0xF0, 0x3E, 0x03, - 0xC0, 0x70, 0x0E, 0x01, 0x80, 0xF0, 0x3C, 0x07, 0x00, 0x06, 0x40, 0x3F, 0x00, 0xFC, 0x00, 0x00, - 0x40, 0x0B, 0x80, 0xF7, 0x03, 0x8E, 0x1C, 0x38, 0x70, 0x73, 0x80, 0xFC, 0x03, 0xF0, 0x07, 0x80, - 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x09, 0x83, 0xF0, 0xDC, - 0x00, 0x04, 0x07, 0xC1, 0xD8, 0x3B, 0x86, 0x31, 0xC7, 0x38, 0xE6, 0x0D, 0xC1, 0xF0, 0x3E, 0x03, - 0xC0, 0x70, 0x0E, 0x01, 0x80, 0xF0, 0x3C, 0x07, 0x00, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF8, 0x7F, - 0xFB, 0xFF, 0xC0, 0x00, 0xFF, 0xDF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x7F, 0xEF, 0xFD, - 0xFF, 0xBF, 0xF7, 0xFC, 0x27, 0x76, 0x6E, 0xE0, 0x27, 0x66, 0xEC, 0xC0, 0x7F, 0xFD, 0xB0, 0x46, - 0x67, 0x73, 0x30, 0x22, 0xED, 0xDB, 0x76, 0xFD, 0xFB, 0x00, 0x26, 0xFD, 0xBB, 0x7E, 0xD9, 0xB3, - 0x00, 0x65, 0xDF, 0xBF, 0x6C, 0xDB, 0xB6, 0x00, 0x44, 0x6E, 0x6E, 0x76, 0x76, 0x36, 0x32, 0x18, - 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, - 0xFF, 0xFF, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFB, 0xFF, 0xFF, - 0xFF, 0xDF, 0x0C, 0x03, 0x8F, 0x3F, 0xFF, 0xCC, 0x00, 0x6E, 0xE0, 0x63, 0xF1, 0xF8, 0xE0, 0x00, - 0x63, 0x8D, 0xC7, 0x1F, 0x8E, 0x38, 0x00, 0x00, 0x6E, 0xE0, 0x30, 0x0C, 0x01, 0xF8, 0x38, 0x03, - 0xB0, 0x60, 0x06, 0x61, 0xC0, 0x0C, 0xE7, 0x00, 0x19, 0x8C, 0x00, 0x3F, 0x30, 0x00, 0x3C, 0xE0, - 0x00, 0x03, 0x80, 0x00, 0x06, 0x3C, 0x78, 0x1C, 0xFD, 0xF8, 0x73, 0x9B, 0x30, 0xC7, 0x36, 0x63, - 0x8E, 0x6C, 0xCE, 0x0D, 0xDF, 0x98, 0x1F, 0x3F, 0x00, 0x0C, 0x18, 0x30, 0x0C, 0x00, 0x01, 0xF8, - 0x38, 0x00, 0x03, 0xB0, 0x60, 0x00, 0x06, 0x61, 0xC0, 0x00, 0x0C, 0xE7, 0x00, 0x00, 0x19, 0x8C, - 0x00, 0x00, 0x3F, 0x30, 0x00, 0x00, 0x3C, 0xE0, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x06, 0x3C, - 0x78, 0xF0, 0x1C, 0xFD, 0xFB, 0xF0, 0x73, 0x9B, 0x36, 0x60, 0xC7, 0x36, 0x6C, 0xE3, 0x8E, 0x6C, - 0xD9, 0xCE, 0x0D, 0xDF, 0xBB, 0x18, 0x1F, 0x3F, 0x3E, 0x00, 0x0C, 0x18, 0x30, 0x27, 0x76, 0x66, - 0x40, 0x23, 0x77, 0x77, 0x67, 0x67, 0x66, 0x46, 0x23, 0x13, 0xB9, 0xDD, 0xCC, 0xCE, 0x66, 0x73, - 0x33, 0x19, 0x18, 0x80, 0x66, 0x77, 0x73, 0x30, 0x62, 0x67, 0x77, 0x73, 0x73, 0x33, 0x33, 0x62, - 0x33, 0x39, 0x9D, 0xCC, 0xE6, 0x77, 0x33, 0x99, 0x8C, 0xCC, 0x60, 0x00, 0x0E, 0x0F, 0x86, 0xC7, - 0x33, 0x1C, 0x0E, 0x38, 0xE1, 0xC7, 0x0E, 0x0E, 0x0E, 0x0E, 0x1C, 0x70, 0xE0, 0xE0, 0xE0, 0xE1, - 0xC7, 0x1C, 0x70, 0xC0, 0x07, 0x00, 0xBB, 0x0F, 0xF8, 0x7B, 0xC3, 0xFC, 0x3F, 0xF9, 0xFE, 0xC3, - 0xF8, 0x3D, 0xE1, 0xFF, 0x05, 0xD8, 0x0E, 0x00, 0x61, 0x71, 0xF8, 0xFC, 0x7E, 0x3F, 0x1D, 0x8E, - 0xC7, 0x63, 0xB1, 0xD8, 0xEC, 0x20, 0x03, 0x1F, 0x8F, 0xC7, 0x00, 0x00, 0x0E, 0x0F, 0xC7, 0xF9, - 0x87, 0x61, 0xC3, 0x70, 0xD8, 0x3E, 0x0F, 0x03, 0x80, 0xC0, 0x30, 0x00, 0x03, 0x01, 0xC0, 0x70, - 0x00, 0x00, 0xFF, 0xEF, 0xFC, 0x60, 0x03, 0x3F, 0xFF, 0x8F, 0xFF, 0x81, 0xFF, 0x00, 0x0F, 0xF8, - 0x1F, 0xFF, 0x1F, 0x0F, 0xCC, 0x00, 0x60, 0x01, 0x81, 0x81, 0xC0, 0xC0, 0xE0, 0xE0, 0x70, 0x78, - 0x3E, 0x33, 0x98, 0xC0, 0x00, 0x02, 0x00, 0x7C, 0x03, 0xE0, 0x3F, 0x80, 0xF8, 0x07, 0xC0, 0x08, - 0x0F, 0x3E, 0x7D, 0xF7, 0xFF, 0xFF, 0x7E, 0xFB, 0xE3, 0x06, 0x00, 0x7E, 0xFC, 0x00, 0x18, 0x01, - 0xC0, 0x0C, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x06, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, - 0x01, 0x80, 0x1C, 0x01, 0xC0, 0x0C, 0x00, 0x07, 0xFD, 0x8C, 0x63, 0x18, 0xC7, 0xBF, 0xEC, 0x63, - 0x18, 0xC6, 0x3D, 0xF0, 0x07, 0xFE, 0x73, 0x9C, 0xE7, 0x3F, 0xFF, 0xF3, 0x9C, 0xE7, 0x39, 0xFF, - 0xF0, 0x0E, 0x03, 0xC1, 0xFC, 0x3F, 0x1F, 0xF3, 0xFC, 0xC3, 0xB8, 0x7E, 0x1D, 0xC3, 0x80, 0xE0, - 0x1C, 0x07, 0x01, 0xC0, 0xF0, 0x1E, 0x0F, 0x01, 0xE0, 0x70, 0x1C, 0x03, 0x00, 0xE0, 0x18, 0x07, - 0x00, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x78, 0x0E, 0x03, 0x80, 0x70, 0x00, 0x00, 0x00, 0x0E, 0x04, - 0x7F, 0x1D, 0xFF, 0x3B, 0x0E, 0x7E, 0x1C, 0xE0, 0x39, 0xC0, 0x73, 0x83, 0xC7, 0x0F, 0x0E, 0x1C, - 0x1C, 0x30, 0x38, 0x60, 0x30, 0x00, 0x01, 0x81, 0xC7, 0x83, 0x8E, 0x07, 0x00, 0x00, 0x60, 0xF1, - 0xC3, 0xF3, 0x8F, 0xF7, 0x38, 0x7E, 0x70, 0xFC, 0x01, 0xD8, 0x07, 0x30, 0x1E, 0x60, 0x78, 0xC1, - 0xC1, 0x83, 0x83, 0x07, 0x00, 0x00, 0x0C, 0x0C, 0x38, 0x38, 0x70, 0x70, 0x00, 0x00, 0x7F, 0x3F, - 0x9F, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xC0, 0x60, 0x30, 0x18, 0x0C, - 0x06, 0x00, 0x7C, 0x1F, 0xF3, 0xFF, 0x77, 0xEE, 0xFF, 0xDF, 0xFB, 0xFF, 0x7E, 0xEF, 0xDD, 0xF3, - 0xB8, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0x1F, 0xDF, 0xF7, 0xFF, 0xF7, 0xFD, 0xFF, 0x7F, - 0xDD, 0xFF, 0x3F, 0xC3, 0xF0, 0xFE, 0x3F, 0xEF, 0xFF, 0xBF, 0xEF, 0xFB, 0xFE, 0xFF, 0xFE, 0xFF, - 0xBF, 0x80, 0x08, 0x0C, 0x36, 0x5F, 0xE7, 0xE3, 0xF3, 0xFD, 0xB6, 0x18, 0x0C, 0x00, 0x6F, 0xE0, - 0x00, 0x00, 0x06, 0x77, 0x73, 0x30, 0x0F, 0xF8, 0x1F, 0xFF, 0x1F, 0xFF, 0xCC, 0x00, 0xE0, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x01, 0x9F, 0xFF, 0xC7, 0xFF, - 0xC0, 0xFF, 0x80, 0x08, 0x0C, 0x36, 0x5F, 0xE7, 0xE1, 0xF3, 0xFD, 0xB6, 0x18, 0x0C, 0x02, 0x03, - 0x0D, 0x97, 0xF9, 0xF8, 0x7C, 0xFF, 0x6D, 0x86, 0x03, 0x00, 0x00, 0x10, 0x01, 0x90, 0x1D, 0xE0, - 0xCF, 0x0C, 0x70, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x03, 0x0C, 0x38, 0xF3, 0x87, 0x98, - 0x1D, 0xC0, 0x1C, 0x00, 0x10, 0x03, 0xF0, 0x3F, 0xFE, 0x87, 0xF0, 0x0E, 0x00, 0x0F, 0xF8, 0x1F, - 0xFF, 0x1F, 0xFF, 0xCC, 0x00, 0x60, 0x0C, 0x37, 0x9F, 0xDF, 0xFF, 0xFF, 0xFD, 0xFC, 0xFE, 0x0C, - 0x00, 0x23, 0x11, 0xBB, 0x9D, 0xDD, 0xCC, 0xEC, 0xE6, 0x76, 0x73, 0x33, 0x31, 0x99, 0x18, 0x8C, - 0x1C, 0x1F, 0x9D, 0xCC, 0x76, 0x3B, 0x1D, 0x8E, 0xFE, 0x3E, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x06, - 0x07, 0x87, 0xC7, 0xE3, 0x73, 0xB9, 0xFE, 0xFF, 0x07, 0x00, 0x7E, 0xFE, 0xE0, 0xFC, 0xFE, 0x46, - 0xC6, 0xFE, 0x7C, 0x00, 0x1C, 0x1F, 0x9D, 0xCD, 0xC7, 0xF3, 0x99, 0x8C, 0xFE, 0x3E, 0x00, 0x00, - 0x7F, 0xFF, 0x07, 0x0E, 0x0C, 0x1C, 0x18, 0x38, 0x70, 0x1C, 0x3F, 0x9C, 0xCE, 0xE3, 0xE3, 0xF9, - 0x8E, 0xFE, 0x3F, 0x00, 0x00, 0x1C, 0x3F, 0x9D, 0xCC, 0x67, 0xFB, 0xF9, 0x8C, 0xFE, 0x3E, 0x00, - 0x00, 0x30, 0x73, 0xFF, 0xF3, 0x87, 0x00, 0xFF, 0xFC, 0xFF, 0xFD, 0xF7, 0xF7, 0xC0, 0x76, 0x6E, - 0xEE, 0xEE, 0x67, 0x30, 0x67, 0x73, 0x33, 0x33, 0x76, 0x60, 0xD9, 0xFF, 0xBE, 0x3C, 0x78, 0xF1, - 0x80, 0x1C, 0x1F, 0x9D, 0xCC, 0x76, 0x3B, 0x1D, 0x8E, 0xFE, 0x3E, 0x00, 0x00, 0x1B, 0xDE, 0xB1, - 0x8C, 0x63, 0x18, 0x39, 0xFF, 0xBE, 0x30, 0xE7, 0x9C, 0x7F, 0xFE, 0x38, 0xFE, 0xEE, 0x0E, 0x3C, - 0x3E, 0xC7, 0xFE, 0x7E, 0x00, 0x06, 0x07, 0x87, 0xC7, 0xE3, 0x73, 0xB9, 0xFE, 0xFF, 0x07, 0x00, - 0x7E, 0xFE, 0xE0, 0xFC, 0xFE, 0x46, 0xC6, 0xFE, 0x7C, 0x00, 0x1C, 0x1F, 0x9D, 0xCD, 0xC7, 0xF3, - 0x99, 0x8C, 0xFE, 0x3E, 0x00, 0x00, 0x7F, 0xFF, 0x07, 0x0E, 0x0C, 0x1C, 0x18, 0x38, 0x70, 0x1C, - 0x3F, 0x9C, 0xCE, 0xE3, 0xE3, 0xF9, 0x8E, 0xFE, 0x3F, 0x00, 0x00, 0x1C, 0x3F, 0x9D, 0xCC, 0x67, - 0xFB, 0xF9, 0x8C, 0xFE, 0x3E, 0x00, 0x00, 0x30, 0x73, 0xFF, 0xF3, 0x87, 0x00, 0xFF, 0xFC, 0xFF, - 0xFD, 0xF7, 0xF7, 0xC0, 0x76, 0x6E, 0xEE, 0xEE, 0x67, 0x30, 0x67, 0x73, 0x33, 0x33, 0x76, 0x60, - 0x18, 0x7E, 0x66, 0x7E, 0xFE, 0xFE, 0x7E, 0x00, 0x1C, 0x7E, 0x67, 0xFF, 0x7F, 0x7F, 0x3E, 0x00, - 0x1C, 0x7E, 0x77, 0xE3, 0x63, 0x7F, 0x3E, 0x00, 0x66, 0x76, 0x3E, 0x3C, 0x3C, 0x7E, 0xE7, 0x7C, - 0xFE, 0x47, 0xFF, 0xFF, 0xC7, 0xFE, 0x7E, 0x38, 0xC1, 0x83, 0x67, 0xFE, 0xF8, 0xF1, 0xE3, 0xC6, - 0xC1, 0x83, 0x3E, 0xEF, 0x9E, 0x3E, 0x6E, 0xCE, 0xFF, 0xFF, 0xC0, 0xD9, 0x9F, 0xFF, 0xBB, 0xE7, - 0x7C, 0xEF, 0x9D, 0xF3, 0xB8, 0xD9, 0xFF, 0xBE, 0x3C, 0x78, 0xF1, 0x80, 0xD8, 0xFE, 0xEE, 0xC6, - 0xC6, 0xFE, 0xFC, 0xC0, 0xC0, 0xC0, 0x38, 0x7E, 0x66, 0x7E, 0x3E, 0x7E, 0x7E, 0x00, 0x67, 0xFE, - 0xC6, 0x31, 0xEF, 0x00, 0x1E, 0x01, 0xFC, 0x1C, 0xE1, 0xC3, 0x8E, 0x08, 0x73, 0xFF, 0x9F, 0xFC, - 0xC0, 0x66, 0x03, 0xFF, 0x8F, 0xFC, 0x1F, 0xE0, 0x60, 0x03, 0x00, 0x1F, 0xE0, 0xFF, 0x00, 0x00, - 0x00, 0x00, 0x06, 0xC0, 0x7A, 0x07, 0xF8, 0x3F, 0xF1, 0xED, 0xE6, 0x2F, 0xB9, 0xB6, 0xE6, 0xC3, - 0x1E, 0x0C, 0x58, 0x33, 0x60, 0xED, 0x03, 0xAC, 0x67, 0xB3, 0x9E, 0x9E, 0x3F, 0xF0, 0xFF, 0x83, - 0x70, 0x0B, 0x00, 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0xE0, 0xF6, 0x03, 0xF0, 0x0F, 0x80, 0x18, 0xEE, - 0xC7, 0xF6, 0x3F, 0xB9, 0xE1, 0xCE, 0x37, 0x73, 0xBF, 0xBC, 0xFF, 0xC3, 0xFC, 0x03, 0x00, 0x1F, - 0xF0, 0xFF, 0xC7, 0xFE, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0xFE, 0x1F, 0xF0, 0xFF, 0x86, - 0x00, 0xFC, 0x0F, 0xF0, 0x0E, 0x00, 0x60, 0x03, 0x00, 0x07, 0x03, 0xF0, 0xFF, 0x38, 0x77, 0x0E, - 0xE0, 0x3F, 0xC7, 0xFC, 0x70, 0x1F, 0xE3, 0xFE, 0x38, 0x07, 0x04, 0xE1, 0xFF, 0xFF, 0xFE, 0x00, - 0x00, 0x00, 0xE0, 0xEE, 0xF8, 0xFF, 0xFE, 0xFF, 0xFE, 0xE3, 0x86, 0xE3, 0x87, 0xE3, 0x87, 0xE3, - 0x87, 0xE7, 0x87, 0xE7, 0x87, 0xEF, 0x87, 0xEF, 0x87, 0xED, 0x87, 0x1C, 0x00, 0x18, 0x00, 0x30, - 0x0C, 0x38, 0x0C, 0x3C, 0x0C, 0x3C, 0x0C, 0x3E, 0x0C, 0x37, 0x0C, 0xFF, 0xFF, 0xFF, 0xFF, 0x33, - 0x8C, 0xFF, 0xFF, 0xFF, 0xFF, 0x70, 0xEC, 0x30, 0x7C, 0x30, 0x3C, 0x30, 0x3C, 0x30, 0x1C, 0x7E, - 0x00, 0x1F, 0xF8, 0x03, 0xFF, 0x9C, 0x70, 0x73, 0x8E, 0x06, 0xFD, 0xC0, 0xFF, 0xB8, 0x1B, 0xF7, - 0x07, 0x38, 0xFF, 0xE7, 0x1F, 0xF8, 0xE3, 0xFC, 0x1C, 0x70, 0x03, 0x8E, 0x00, 0x71, 0xC0, 0x0E, - 0x38, 0x00, 0xF7, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x03, 0xFF, 0x00, 0x0F, 0xFE, 0x00, - 0x38, 0x38, 0x00, 0xE0, 0x61, 0xE3, 0x81, 0xDF, 0xCE, 0x06, 0x7F, 0xB8, 0x3B, 0x86, 0xFF, 0xEE, - 0x03, 0xFF, 0x1F, 0x0E, 0x38, 0x3F, 0x38, 0x70, 0x3E, 0xE1, 0xC0, 0x1B, 0x83, 0xB8, 0x6E, 0x0E, - 0xFF, 0xB8, 0x1D, 0xFC, 0x00, 0x01, 0xC0, 0x40, 0x60, 0x2E, 0x0E, 0x07, 0xE0, 0xF0, 0x66, 0x0F, - 0x0E, 0x71, 0xF0, 0xE7, 0x1F, 0x8E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3B, 0x99, 0xCF, 0xFF, 0xFE, - 0xFF, 0xFF, 0xF1, 0xF1, 0xF8, 0x1F, 0x0F, 0x81, 0xF0, 0xF0, 0x0E, 0x0F, 0x00, 0xE0, 0x70, 0x7F, - 0x0D, 0xFF, 0x9B, 0xFF, 0x37, 0x07, 0x6E, 0xEE, 0xDD, 0xDD, 0xBB, 0xBB, 0x77, 0x76, 0xEE, 0xCD, - 0xDC, 0x1B, 0xBF, 0xF7, 0x7F, 0xC0, 0x00, 0x60, 0x1F, 0xC0, 0xFE, 0x07, 0xE0, 0xE6, 0x1F, 0xF0, - 0xFF, 0x8E, 0x1C, 0x60, 0x63, 0x03, 0x18, 0x18, 0xC0, 0xC7, 0x06, 0x38, 0x70, 0xFF, 0x83, 0xFC, - 0x06, 0x00, 0x00, 0x1F, 0xF8, 0xFF, 0xC0, 0x01, 0xC0, 0x3F, 0x83, 0xFE, 0x38, 0x21, 0x80, 0x1C, - 0x01, 0xFF, 0x9F, 0xFC, 0x38, 0x07, 0xFE, 0x3F, 0xE0, 0x70, 0x01, 0xC0, 0x0F, 0x08, 0x3F, 0xE0, - 0xFE, 0x00, 0x80, 0x10, 0x0C, 0x70, 0x38, 0xE0, 0xE1, 0xC3, 0x83, 0x8E, 0x07, 0x38, 0x0E, 0xE0, - 0x7F, 0xFF, 0xFF, 0xFE, 0x7F, 0x80, 0xF7, 0x01, 0xC7, 0x03, 0x87, 0x07, 0x07, 0x0E, 0x0E, 0x1C, - 0x0E, 0x7F, 0xF3, 0xFF, 0xDF, 0xFE, 0x0E, 0x00, 0x70, 0x03, 0xF0, 0x1F, 0x83, 0xF0, 0x1F, 0xE0, - 0xBF, 0x07, 0xE0, 0x3E, 0x01, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, - 0x00, 0x00, 0x07, 0xE0, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x70, 0xE0, 0x00, - 0x07, 0x07, 0x07, 0x00, 0x70, 0x71, 0xFC, 0x07, 0x03, 0x3F, 0xE0, 0x70, 0x33, 0x86, 0x07, 0x03, - 0x70, 0x70, 0x70, 0x37, 0x07, 0x3F, 0x03, 0x70, 0x77, 0xF0, 0x77, 0x07, 0x7F, 0x07, 0x70, 0x76, - 0x78, 0xE7, 0x8E, 0x7F, 0xFE, 0x7F, 0xE7, 0xDF, 0xC7, 0xFC, 0x18, 0x20, 0x73, 0x00, 0x00, 0x07, - 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x20, 0x00, 0x3E, 0x00, 0xFF, 0x03, - 0xFE, 0x0C, 0x1C, 0xFF, 0xFF, 0xFF, 0xF3, 0x03, 0x3F, 0xFF, 0xFF, 0xFC, 0xC3, 0xC3, 0xFE, 0x0F, - 0xF0, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x03, 0x00, 0x0C, 0x00, 0x78, 0x07, 0xF8, 0x3F, - 0xF1, 0xCD, 0xE6, 0x33, 0xB8, 0xC6, 0xE3, 0x03, 0x0C, 0x0C, 0x3F, 0xF0, 0xFF, 0xE3, 0x1F, 0x8C, - 0x67, 0x33, 0x9E, 0xDE, 0x3F, 0xF0, 0x7F, 0x80, 0x30, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x00, 0x1E, - 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x3F, 0xFF, 0x7F, 0xFC, 0xE1, 0xC7, 0xFF, - 0xFF, 0xFF, 0x70, 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, 0x0E, 0x07, 0xF1, 0xFF, 0x38, 0xE6, 0x0C, - 0x03, 0xBF, 0xFF, 0xFF, 0x00, 0x1F, 0xFF, 0xFF, 0xB8, 0x06, 0x0E, 0xE3, 0x9F, 0xF1, 0xFC, 0x0E, - 0x00, 0x03, 0x00, 0x0C, 0x00, 0x78, 0x07, 0xF8, 0x3F, 0xF1, 0xED, 0xE6, 0x33, 0xB8, 0xC6, 0xE3, - 0x03, 0x0C, 0x0C, 0x30, 0x30, 0xC0, 0xE3, 0x03, 0x8C, 0x67, 0x33, 0x9E, 0xDE, 0x3F, 0xF0, 0x7F, - 0x80, 0x30, 0x00, 0xC0, 0x02, 0x00, 0x7F, 0xF3, 0xFF, 0xDF, 0xFE, 0xFF, 0xE7, 0xFF, 0xBF, 0xFC, - 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, - 0x7F, 0xDF, 0xFF, 0xFF, 0x03, 0xC7, 0xFF, 0xFF, 0xDF, 0xF0, 0x3C, 0xFF, 0x1F, 0xC1, 0xE0, 0x1C, - 0x01, 0xC0, 0x1C, 0x03, 0xC0, 0x3C, 0x18, 0x01, 0x80, 0x1B, 0x81, 0xF8, 0x3F, 0x07, 0x90, 0x7F, - 0x81, 0xF0, 0x7C, 0x07, 0x86, 0x58, 0x61, 0x8E, 0x18, 0xE1, 0x9E, 0x1F, 0xC1, 0xF8, 0x1C, 0x00, - 0x07, 0x00, 0x38, 0x01, 0xC0, 0x3F, 0x83, 0xFE, 0x3F, 0xF9, 0xDD, 0xDC, 0xE7, 0xE7, 0x3F, 0x39, - 0xF9, 0xCF, 0xCE, 0x70, 0x1F, 0x80, 0x7F, 0xC1, 0xFF, 0x86, 0x0E, 0x18, 0x1C, 0x60, 0x71, 0x81, - 0xC6, 0x0E, 0xFF, 0xFB, 0xFF, 0xC1, 0x80, 0x3F, 0xE0, 0xFF, 0x80, 0x60, 0x01, 0x80, 0x06, 0x00, - 0x08, 0x80, 0x6C, 0x03, 0xE0, 0x7F, 0x87, 0xFE, 0x3E, 0xFB, 0xB6, 0xDD, 0xB7, 0xED, 0xBF, 0x6C, - 0xB8, 0x00, 0xC0, 0x07, 0x00, 0x1C, 0x03, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0xB0, 0x7F, 0x0F, 0xFC, - 0xFF, 0xEE, 0x0E, 0xE0, 0x6E, 0x06, 0xE0, 0xEF, 0xFC, 0xFF, 0xCF, 0xFE, 0xE0, 0x7E, 0x07, 0xE0, - 0x7E, 0x0F, 0xFF, 0xEF, 0xFC, 0x1B, 0x01, 0xB0, 0x00, 0x00, 0x01, 0xC0, 0x01, 0xE0, 0x01, 0xE0, - 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xFF, 0xFC, 0xFF, 0xFF, 0x3F, 0xFF, 0x0E, 0x00, 0x03, 0x80, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0x0C, 0x00, 0x01, 0x00, 0x07, 0x00, 0x1F, 0x00, 0x7F, 0x01, 0xFF, 0x07, - 0x77, 0x1E, 0xE7, 0x39, 0xC6, 0x23, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, - 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, - 0xFF, 0xFE, 0xFF, 0xFF, 0x7F, 0xFE, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x00, 0xC0, - 0x01, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x23, + 0xC0, 0x09, 0x83, 0xF0, 0xDC, 0x00, 0x04, 0x07, 0xC1, 0xD8, 0x3B, 0x86, 0x31, 0xC7, 0x38, 0xE6, + 0x0D, 0xC1, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0E, 0x01, 0x80, 0xF0, 0x3C, 0x07, 0x00, 0xFF, 0xFF, + 0xF8, 0xFF, 0xFF, 0xF8, 0x7F, 0xFB, 0xFF, 0xC0, 0x00, 0xFF, 0xDF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF0, 0x7F, 0xEF, 0xFD, 0xFF, 0xBF, 0xF7, 0xFC, 0x27, 0x76, 0x6E, 0xE0, 0x27, 0x66, 0xEC, + 0xC0, 0x7F, 0xFD, 0xB0, 0x46, 0x67, 0x73, 0x30, 0x22, 0xED, 0xDB, 0x76, 0xFD, 0xFB, 0x00, 0x26, + 0xFD, 0xBB, 0x7E, 0xD9, 0xB3, 0x00, 0x65, 0xDF, 0xBF, 0x6C, 0xDB, 0xB6, 0x00, 0x44, 0x6E, 0x6E, + 0x76, 0x76, 0x36, 0x32, 0x18, 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x00, 0x18, 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0xFB, 0xFF, 0xFF, 0xFF, 0xDF, 0x0C, 0x03, 0x8F, 0x3F, 0xFF, 0xCC, 0x00, 0x6E, 0xE0, + 0x63, 0xF1, 0xF8, 0xE0, 0x00, 0x63, 0x8D, 0xC7, 0x1F, 0x8E, 0x38, 0x00, 0x00, 0x6E, 0xE0, 0x30, + 0x0C, 0x01, 0xF8, 0x38, 0x03, 0xB0, 0x60, 0x06, 0x61, 0xC0, 0x0C, 0xE7, 0x00, 0x19, 0x8C, 0x00, + 0x3F, 0x30, 0x00, 0x3C, 0xE0, 0x00, 0x03, 0x80, 0x00, 0x06, 0x3C, 0x78, 0x1C, 0xFD, 0xF8, 0x73, + 0x9B, 0x30, 0xC7, 0x36, 0x63, 0x8E, 0x6C, 0xCE, 0x0D, 0xDF, 0x98, 0x1F, 0x3F, 0x00, 0x0C, 0x18, + 0x30, 0x0C, 0x00, 0x01, 0xF8, 0x38, 0x00, 0x03, 0xB0, 0x60, 0x00, 0x06, 0x61, 0xC0, 0x00, 0x0C, + 0xE7, 0x00, 0x00, 0x19, 0x8C, 0x00, 0x00, 0x3F, 0x30, 0x00, 0x00, 0x3C, 0xE0, 0x00, 0x00, 0x03, + 0x80, 0x00, 0x00, 0x06, 0x3C, 0x78, 0xF0, 0x1C, 0xFD, 0xFB, 0xF0, 0x73, 0x9B, 0x36, 0x60, 0xC7, + 0x36, 0x6C, 0xE3, 0x8E, 0x6C, 0xD9, 0xCE, 0x0D, 0xDF, 0xBB, 0x18, 0x1F, 0x3F, 0x3E, 0x00, 0x0C, + 0x18, 0x30, 0x27, 0x76, 0x66, 0x40, 0x23, 0x77, 0x77, 0x67, 0x67, 0x66, 0x46, 0x23, 0x13, 0xB9, + 0xDD, 0xCC, 0xCE, 0x66, 0x73, 0x33, 0x19, 0x18, 0x80, 0x66, 0x77, 0x73, 0x30, 0x62, 0x67, 0x77, + 0x73, 0x73, 0x33, 0x33, 0x62, 0x33, 0x39, 0x9D, 0xCC, 0xE6, 0x77, 0x33, 0x99, 0x8C, 0xCC, 0x60, + 0x00, 0x0E, 0x0F, 0x86, 0xC7, 0x33, 0x1C, 0x0E, 0x38, 0xE1, 0xC7, 0x0E, 0x0E, 0x0E, 0x0E, 0x1C, + 0x70, 0xE0, 0xE0, 0xE0, 0xE1, 0xC7, 0x1C, 0x70, 0xC0, 0x07, 0x00, 0xBB, 0x0F, 0xF8, 0x7B, 0xC3, + 0xFC, 0x3F, 0xF9, 0xFE, 0xC3, 0xF8, 0x3D, 0xE1, 0xFF, 0x05, 0xD8, 0x0E, 0x00, 0x61, 0x71, 0xF8, + 0xFC, 0x7E, 0x3F, 0x1D, 0x8E, 0xC7, 0x63, 0xB1, 0xD8, 0xEC, 0x20, 0x03, 0x1F, 0x8F, 0xC7, 0x00, + 0x00, 0x0E, 0x0F, 0xC7, 0xF9, 0x87, 0x61, 0xC3, 0x70, 0xD8, 0x3E, 0x0F, 0x03, 0x80, 0xC0, 0x30, + 0x00, 0x03, 0x01, 0xC0, 0x70, 0x00, 0x00, 0xFF, 0xEF, 0xFC, 0x60, 0x03, 0x3F, 0xFF, 0x8F, 0xFF, + 0x81, 0xFF, 0x00, 0x0F, 0xF8, 0x1F, 0xFF, 0x1F, 0x0F, 0xCC, 0x00, 0x60, 0x01, 0x81, 0x81, 0xC0, + 0xC0, 0xE0, 0xE0, 0x70, 0x78, 0x3E, 0x33, 0x98, 0xC0, 0x00, 0x02, 0x00, 0x7C, 0x03, 0xE0, 0x3F, + 0x80, 0xF8, 0x07, 0xC0, 0x08, 0x0F, 0x3E, 0x7D, 0xF7, 0xFF, 0xFF, 0x7E, 0xFB, 0xE3, 0x06, 0x00, + 0x7E, 0xFC, 0x00, 0x18, 0x01, 0xC0, 0x0C, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x06, 0x00, 0x70, 0x07, + 0x00, 0x30, 0x03, 0x80, 0x38, 0x01, 0x80, 0x1C, 0x01, 0xC0, 0x0C, 0x00, 0x07, 0xFD, 0x8C, 0x63, + 0x18, 0xC7, 0xBF, 0xEC, 0x63, 0x18, 0xC6, 0x3D, 0xF0, 0x07, 0xFE, 0x73, 0x9C, 0xE7, 0x3F, 0xFF, + 0xF3, 0x9C, 0xE7, 0x39, 0xFF, 0xF0, 0x0E, 0x03, 0xC1, 0xFC, 0x3F, 0x1F, 0xF3, 0xFC, 0xC3, 0xB8, + 0x7E, 0x1D, 0xC3, 0x80, 0xE0, 0x1C, 0x07, 0x01, 0xC0, 0xF0, 0x1E, 0x0F, 0x01, 0xE0, 0x70, 0x1C, + 0x03, 0x00, 0xE0, 0x18, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x78, 0x0E, 0x03, 0x80, 0x70, + 0x00, 0x00, 0x00, 0x0E, 0x04, 0x7F, 0x1D, 0xFF, 0x3B, 0x0E, 0x7E, 0x1C, 0xE0, 0x39, 0xC0, 0x73, + 0x83, 0xC7, 0x0F, 0x0E, 0x1C, 0x1C, 0x30, 0x38, 0x60, 0x30, 0x00, 0x01, 0x81, 0xC7, 0x83, 0x8E, + 0x07, 0x00, 0x00, 0x60, 0xF1, 0xC3, 0xF3, 0x8F, 0xF7, 0x38, 0x7E, 0x70, 0xFC, 0x01, 0xD8, 0x07, + 0x30, 0x1E, 0x60, 0x78, 0xC1, 0xC1, 0x83, 0x83, 0x07, 0x00, 0x00, 0x0C, 0x0C, 0x38, 0x38, 0x70, + 0x70, 0x00, 0x00, 0x7F, 0x3F, 0x9F, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x0E, 0x07, 0x03, 0x81, 0xC0, + 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x00, 0x7C, 0x1F, 0xF3, 0xFF, 0x77, 0xEE, 0xFF, 0xDF, 0xFB, + 0xFF, 0x7E, 0xEF, 0xDD, 0xF3, 0xB8, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0x1F, 0xDF, 0xF7, + 0xFF, 0xF7, 0xFD, 0xFF, 0x7F, 0xDD, 0xFF, 0x3F, 0xC3, 0xF0, 0xFE, 0x3F, 0xEF, 0xFF, 0xBF, 0xEF, + 0xFB, 0xFE, 0xFF, 0xFE, 0xFF, 0xBF, 0x80, 0x08, 0x0C, 0x36, 0x5F, 0xE7, 0xE3, 0xF3, 0xFD, 0xB6, + 0x18, 0x0C, 0x00, 0x6F, 0xE0, 0x00, 0x00, 0x06, 0x77, 0x73, 0x30, 0x0F, 0xF8, 0x1F, 0xFF, 0x1F, + 0xFF, 0xCC, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, + 0x01, 0x9F, 0xFF, 0xC7, 0xFF, 0xC0, 0xFF, 0x80, 0x08, 0x0C, 0x36, 0x5F, 0xE7, 0xE1, 0xF3, 0xFD, + 0xB6, 0x18, 0x0C, 0x02, 0x03, 0x0D, 0x97, 0xF9, 0xF8, 0x7C, 0xFF, 0x6D, 0x86, 0x03, 0x00, 0x00, + 0x10, 0x01, 0x90, 0x1D, 0xE0, 0xCF, 0x0C, 0x70, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x03, + 0x0C, 0x38, 0xF3, 0x87, 0x98, 0x1D, 0xC0, 0x1C, 0x00, 0x10, 0x03, 0xF0, 0x3F, 0xFE, 0x87, 0xF0, + 0x0E, 0x00, 0x0F, 0xF8, 0x1F, 0xFF, 0x1F, 0xFF, 0xCC, 0x00, 0x60, 0x0C, 0x37, 0x9F, 0xDF, 0xFF, + 0xFF, 0xFD, 0xFC, 0xFE, 0x0C, 0x00, 0x23, 0x11, 0xBB, 0x9D, 0xDD, 0xCC, 0xEC, 0xE6, 0x76, 0x73, + 0x33, 0x31, 0x99, 0x18, 0x8C, 0x1C, 0x1F, 0x9D, 0xCC, 0x76, 0x3B, 0x1D, 0x8E, 0xFE, 0x3E, 0x00, + 0x00, 0xFF, 0xFF, 0xC0, 0x06, 0x07, 0x87, 0xC7, 0xE3, 0x73, 0xB9, 0xFE, 0xFF, 0x07, 0x00, 0x7E, + 0xFE, 0xE0, 0xFC, 0xFE, 0x46, 0xC6, 0xFE, 0x7C, 0x00, 0x1C, 0x1F, 0x9D, 0xCD, 0xC7, 0xF3, 0x99, + 0x8C, 0xFE, 0x3E, 0x00, 0x00, 0x7F, 0xFF, 0x07, 0x0E, 0x0C, 0x1C, 0x18, 0x38, 0x70, 0x1C, 0x3F, + 0x9C, 0xCE, 0xE3, 0xE3, 0xF9, 0x8E, 0xFE, 0x3F, 0x00, 0x00, 0x1C, 0x3F, 0x9D, 0xCC, 0x67, 0xFB, + 0xF9, 0x8C, 0xFE, 0x3E, 0x00, 0x00, 0x30, 0x73, 0xFF, 0xF3, 0x87, 0x00, 0xFF, 0xFC, 0xFF, 0xFD, + 0xF7, 0xF7, 0xC0, 0x76, 0x6E, 0xEE, 0xEE, 0x67, 0x30, 0x67, 0x73, 0x33, 0x33, 0x76, 0x60, 0xD9, + 0xFF, 0xBE, 0x3C, 0x78, 0xF1, 0x80, 0x1C, 0x1F, 0x9D, 0xCC, 0x76, 0x3B, 0x1D, 0x8E, 0xFE, 0x3E, + 0x00, 0x00, 0x1B, 0xDE, 0xB1, 0x8C, 0x63, 0x18, 0x39, 0xFF, 0xBE, 0x30, 0xE7, 0x9C, 0x7F, 0xFE, + 0x38, 0xFE, 0xEE, 0x0E, 0x3C, 0x3E, 0xC7, 0xFE, 0x7E, 0x00, 0x06, 0x07, 0x87, 0xC7, 0xE3, 0x73, + 0xB9, 0xFE, 0xFF, 0x07, 0x00, 0x7E, 0xFE, 0xE0, 0xFC, 0xFE, 0x46, 0xC6, 0xFE, 0x7C, 0x00, 0x1C, + 0x1F, 0x9D, 0xCD, 0xC7, 0xF3, 0x99, 0x8C, 0xFE, 0x3E, 0x00, 0x00, 0x7F, 0xFF, 0x07, 0x0E, 0x0C, + 0x1C, 0x18, 0x38, 0x70, 0x1C, 0x3F, 0x9C, 0xCE, 0xE3, 0xE3, 0xF9, 0x8E, 0xFE, 0x3F, 0x00, 0x00, + 0x1C, 0x3F, 0x9D, 0xCC, 0x67, 0xFB, 0xF9, 0x8C, 0xFE, 0x3E, 0x00, 0x00, 0x30, 0x73, 0xFF, 0xF3, + 0x87, 0x00, 0xFF, 0xFC, 0xFF, 0xFD, 0xF7, 0xF7, 0xC0, 0x76, 0x6E, 0xEE, 0xEE, 0x67, 0x30, 0x67, + 0x73, 0x33, 0x33, 0x76, 0x60, 0x18, 0x7E, 0x66, 0x7E, 0xFE, 0xFE, 0x7E, 0x00, 0x1C, 0x7E, 0x67, + 0xFF, 0x7F, 0x7F, 0x3E, 0x00, 0x1C, 0x7E, 0x77, 0xE3, 0x63, 0x7F, 0x3E, 0x00, 0x66, 0x76, 0x3E, + 0x3C, 0x3C, 0x7E, 0xE7, 0x7C, 0xFE, 0x47, 0xFF, 0xFF, 0xC7, 0xFE, 0x7E, 0x38, 0xC1, 0x83, 0x67, + 0xFE, 0xF8, 0xF1, 0xE3, 0xC6, 0xC1, 0x83, 0x3E, 0xEF, 0x9E, 0x3E, 0x6E, 0xCE, 0xFF, 0xFF, 0xC0, + 0xD9, 0x9F, 0xFF, 0xBB, 0xE7, 0x7C, 0xEF, 0x9D, 0xF3, 0xB8, 0xD9, 0xFF, 0xBE, 0x3C, 0x78, 0xF1, + 0x80, 0xD8, 0xFE, 0xEE, 0xC6, 0xC6, 0xFE, 0xFC, 0xC0, 0xC0, 0xC0, 0x38, 0x7E, 0x66, 0x7E, 0x3E, + 0x7E, 0x7E, 0x00, 0x67, 0xFE, 0xC6, 0x31, 0xEF, 0x00, 0x1E, 0x01, 0xFC, 0x1C, 0xE1, 0xC3, 0x8E, + 0x08, 0x73, 0xFF, 0x9F, 0xFC, 0xC0, 0x66, 0x03, 0xFF, 0x8F, 0xFC, 0x1F, 0xE0, 0x60, 0x03, 0x00, + 0x1F, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x06, 0xC0, 0x7A, 0x07, 0xF8, 0x3F, 0xF1, 0xED, 0xE6, + 0x2F, 0xB9, 0xB6, 0xE6, 0xC3, 0x1E, 0x0C, 0x58, 0x33, 0x60, 0xED, 0x03, 0xAC, 0x67, 0xB3, 0x9E, + 0x9E, 0x3F, 0xF0, 0xFF, 0x83, 0x70, 0x0B, 0x00, 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0xE0, 0xF6, 0x03, + 0xF0, 0x0F, 0x80, 0x18, 0xEE, 0xC7, 0xF6, 0x3F, 0xB9, 0xE1, 0xCE, 0x37, 0x73, 0xBF, 0xBC, 0xFF, + 0xC3, 0xFC, 0x03, 0x00, 0x1F, 0xF0, 0xFF, 0xC7, 0xFE, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, + 0xFE, 0x1F, 0xF0, 0xFF, 0x86, 0x00, 0xFC, 0x0F, 0xF0, 0x0E, 0x00, 0x60, 0x03, 0x00, 0x07, 0x03, + 0xF0, 0xFF, 0x38, 0x77, 0x0E, 0xE0, 0x3F, 0xC7, 0xFC, 0x70, 0x1F, 0xE3, 0xFE, 0x38, 0x07, 0x04, + 0xE1, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0xEE, 0xF8, 0xFF, 0xFE, 0xFF, 0xFE, 0xE3, 0x86, + 0xE3, 0x87, 0xE3, 0x87, 0xE3, 0x87, 0xE7, 0x87, 0xE7, 0x87, 0xEF, 0x87, 0xEF, 0x87, 0xED, 0x87, + 0x1C, 0x00, 0x18, 0x00, 0x30, 0x0C, 0x38, 0x0C, 0x3C, 0x0C, 0x3C, 0x0C, 0x3E, 0x0C, 0x37, 0x0C, + 0xFF, 0xFF, 0xFF, 0xFF, 0x33, 0x8C, 0xFF, 0xFF, 0xFF, 0xFF, 0x70, 0xEC, 0x30, 0x7C, 0x30, 0x3C, + 0x30, 0x3C, 0x30, 0x1C, 0x7E, 0x00, 0x1F, 0xF8, 0x03, 0xFF, 0x9C, 0x70, 0x73, 0x8E, 0x06, 0xFD, + 0xC0, 0xFF, 0xB8, 0x1B, 0xF7, 0x07, 0x38, 0xFF, 0xE7, 0x1F, 0xF8, 0xE3, 0xFC, 0x1C, 0x70, 0x03, + 0x8E, 0x00, 0x71, 0xC0, 0x0E, 0x38, 0x00, 0xF7, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x03, + 0xFF, 0x00, 0x0F, 0xFE, 0x00, 0x38, 0x38, 0x00, 0xE0, 0x61, 0xE3, 0x81, 0xDF, 0xCE, 0x06, 0x7F, + 0xB8, 0x3B, 0x86, 0xFF, 0xEE, 0x03, 0xFF, 0x1F, 0x0E, 0x38, 0x3F, 0x38, 0x70, 0x3E, 0xE1, 0xC0, + 0x1B, 0x83, 0xB8, 0x6E, 0x0E, 0xFF, 0xB8, 0x1D, 0xFC, 0x00, 0x01, 0xC0, 0x40, 0x60, 0x2E, 0x0E, + 0x07, 0xE0, 0xF0, 0x66, 0x0F, 0x0E, 0x71, 0xF0, 0xE7, 0x1F, 0x8E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3B, 0x99, 0xCF, 0xFF, 0xFE, 0xFF, 0xFF, 0xF1, 0xF1, 0xF8, 0x1F, 0x0F, 0x81, 0xF0, 0xF0, 0x0E, + 0x0F, 0x00, 0xE0, 0x70, 0x7F, 0x0D, 0xFF, 0x9B, 0xFF, 0x37, 0x07, 0x6E, 0xEE, 0xDD, 0xDD, 0xBB, + 0xBB, 0x77, 0x76, 0xEE, 0xCD, 0xDC, 0x1B, 0xBF, 0xF7, 0x7F, 0xC0, 0x00, 0x60, 0x1F, 0xC0, 0xFE, + 0x07, 0xE0, 0xE6, 0x1F, 0xF0, 0xFF, 0x8E, 0x1C, 0x60, 0x63, 0x03, 0x18, 0x18, 0xC0, 0xC7, 0x06, + 0x38, 0x70, 0xFF, 0x83, 0xFC, 0x06, 0x00, 0x00, 0x1F, 0xF8, 0xFF, 0xC0, 0x01, 0xC0, 0x3F, 0x83, + 0xFE, 0x38, 0x21, 0x80, 0x1C, 0x01, 0xFF, 0x9F, 0xFC, 0x38, 0x07, 0xFE, 0x3F, 0xE0, 0x70, 0x01, + 0xC0, 0x0F, 0x08, 0x3F, 0xE0, 0xFE, 0x00, 0x80, 0x10, 0x0C, 0x70, 0x38, 0xE0, 0xE1, 0xC3, 0x83, + 0x8E, 0x07, 0x38, 0x0E, 0xE0, 0x7F, 0xFF, 0xFF, 0xFE, 0x7F, 0x80, 0xF7, 0x01, 0xC7, 0x03, 0x87, + 0x07, 0x07, 0x0E, 0x0E, 0x1C, 0x0E, 0x7F, 0xF3, 0xFF, 0xDF, 0xFE, 0x0E, 0x00, 0x70, 0x03, 0xF0, + 0x1F, 0x83, 0xF0, 0x1F, 0xE0, 0xBF, 0x07, 0xE0, 0x3E, 0x01, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x07, 0xE0, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x3F, 0xFC, + 0x00, 0x00, 0x70, 0xE0, 0x00, 0x07, 0x07, 0x07, 0x00, 0x70, 0x71, 0xFC, 0x07, 0x03, 0x3F, 0xE0, + 0x70, 0x33, 0x86, 0x07, 0x03, 0x70, 0x70, 0x70, 0x37, 0x07, 0x3F, 0x03, 0x70, 0x77, 0xF0, 0x77, + 0x07, 0x7F, 0x07, 0x70, 0x76, 0x78, 0xE7, 0x8E, 0x7F, 0xFE, 0x7F, 0xE7, 0xDF, 0xC7, 0xFC, 0x18, + 0x20, 0x73, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x20, + 0x00, 0x3E, 0x00, 0xFF, 0x03, 0xFE, 0x0C, 0x1C, 0xFF, 0xFF, 0xFF, 0xF3, 0x03, 0x3F, 0xFF, 0xFF, + 0xFC, 0xC3, 0xC3, 0xFE, 0x0F, 0xF0, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x03, 0x00, 0x0C, + 0x00, 0x78, 0x07, 0xF8, 0x3F, 0xF1, 0xCD, 0xE6, 0x33, 0xB8, 0xC6, 0xE3, 0x03, 0x0C, 0x0C, 0x3F, + 0xF0, 0xFF, 0xE3, 0x1F, 0x8C, 0x67, 0x33, 0x9E, 0xDE, 0x3F, 0xF0, 0x7F, 0x80, 0x30, 0x00, 0xC0, + 0x00, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xF0, 0x0F, 0xC0, 0x3B, 0x00, 0xCE, 0x3F, 0xFF, + 0x7F, 0xFC, 0xE1, 0xC7, 0xFF, 0xFF, 0xFF, 0x70, 0x39, 0xC0, 0xE6, 0x01, 0xF8, 0x07, 0x0E, 0x07, + 0xF1, 0xFF, 0x38, 0xE6, 0x0C, 0x03, 0xBF, 0xFF, 0xFF, 0x00, 0x1F, 0xFF, 0xFF, 0xB8, 0x06, 0x0E, + 0xE3, 0x9F, 0xF1, 0xFC, 0x0E, 0x00, 0x03, 0x00, 0x0C, 0x00, 0x78, 0x07, 0xF8, 0x3F, 0xF1, 0xED, + 0xE6, 0x33, 0xB8, 0xC6, 0xE3, 0x03, 0x0C, 0x0C, 0x30, 0x30, 0xC0, 0xE3, 0x03, 0x8C, 0x67, 0x33, + 0x9E, 0xDE, 0x3F, 0xF0, 0x7F, 0x80, 0x30, 0x00, 0xC0, 0x02, 0x00, 0x7F, 0xF3, 0xFF, 0xDF, 0xFE, + 0xFF, 0xE7, 0xFF, 0xBF, 0xFC, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, + 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x7F, 0xDF, 0xFF, 0xFF, 0x03, 0xC7, 0xFF, 0xFF, 0xDF, 0xF0, 0x3C, + 0xFF, 0x1F, 0xC1, 0xE0, 0x1C, 0x01, 0xC0, 0x1C, 0x03, 0xC0, 0x3C, 0x18, 0x01, 0x80, 0x1B, 0x81, + 0xF8, 0x3F, 0x07, 0x90, 0x7F, 0x81, 0xF0, 0x7C, 0x07, 0x86, 0x58, 0x61, 0x8E, 0x18, 0xE1, 0x9E, + 0x1F, 0xC1, 0xF8, 0x1C, 0x00, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x3F, 0x83, 0xFE, 0x3F, 0xF9, 0xDD, + 0xDC, 0xE7, 0xE7, 0x3F, 0x39, 0xF9, 0xCF, 0xCE, 0x70, 0x1F, 0x80, 0x7F, 0xC1, 0xFF, 0x86, 0x0E, + 0x18, 0x1C, 0x60, 0x71, 0x81, 0xC6, 0x0E, 0xFF, 0xFB, 0xFF, 0xC1, 0x80, 0x3F, 0xE0, 0xFF, 0x80, + 0x60, 0x01, 0x80, 0x06, 0x00, 0x08, 0x80, 0x6C, 0x03, 0xE0, 0x7F, 0x87, 0xFE, 0x3E, 0xFB, 0xB6, + 0xDD, 0xB7, 0xED, 0xBF, 0x6C, 0xB8, 0x00, 0xC0, 0x07, 0x00, 0x1C, 0x03, 0xFF, 0xFF, 0xFF, 0x00, + 0x01, 0xB0, 0x7F, 0x0F, 0xFC, 0xFF, 0xEE, 0x0E, 0xE0, 0x6E, 0x06, 0xE0, 0xEF, 0xFC, 0xFF, 0xCF, + 0xFE, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x0F, 0xFF, 0xEF, 0xFC, 0x1B, 0x01, 0xB0, 0x00, 0x00, 0x01, + 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xFF, 0xFC, 0xFF, 0xFF, 0x3F, 0xFF, + 0x0E, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x0C, 0x00, 0x01, 0x00, 0x07, 0x00, 0x1F, + 0x00, 0x7F, 0x01, 0xFF, 0x07, 0x77, 0x1E, 0xE7, 0x39, 0xC6, 0x23, 0x80, 0x07, 0x00, 0x0E, 0x00, + 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xE0, 0x00, + 0x70, 0x00, 0x38, 0x00, 0x1C, 0xFF, 0xFE, 0xFF, 0xFF, 0x7F, 0xFE, 0x00, 0x1C, 0x00, 0x38, 0x00, + 0x70, 0x00, 0xE0, 0x00, 0xC0, 0x01, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, + 0x00, 0xE0, 0x01, 0xC0, 0x23, 0x84, 0xE7, 0x19, 0xEE, 0x71, 0xFD, 0xC1, 0xFF, 0x01, 0xFC, 0x01, + 0xF0, 0x01, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x30, 0x01, 0xE0, 0x1C, 0x01, 0xE0, + 0x07, 0x01, 0xE0, 0x01, 0xC1, 0xE0, 0x00, 0x71, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, + 0xFF, 0x0E, 0x00, 0x07, 0x03, 0x80, 0x07, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x07, 0x00, 0x0C, 0x03, + 0x00, 0x01, 0x00, 0x07, 0x00, 0x1F, 0x00, 0x7F, 0x01, 0xFF, 0x07, 0x77, 0x1C, 0xE7, 0x31, 0xC6, + 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x23, 0x84, 0xE7, 0x19, 0xEE, 0x71, 0xFD, 0xC1, 0xFF, 0x01, 0xFC, 0x01, 0xF0, 0x01, 0xC0, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0xC0, 0x30, 0x01, 0xE0, 0x1C, 0x01, 0xE0, 0x07, 0x01, 0xE0, 0x01, 0xC1, - 0xE0, 0x00, 0x71, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0x0E, 0x00, 0x07, 0x03, - 0x80, 0x07, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x07, 0x00, 0x0C, 0x03, 0x00, 0x01, 0x00, 0x07, 0x00, - 0x1F, 0x00, 0x7F, 0x01, 0xFF, 0x07, 0x77, 0x1C, 0xE7, 0x31, 0xC6, 0x03, 0x80, 0x07, 0x00, 0x0E, - 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x23, 0x84, 0xE7, 0x19, 0xEE, 0x71, - 0xFD, 0xC1, 0xFF, 0x01, 0xFC, 0x01, 0xF0, 0x01, 0xC0, 0x00, 0x00, 0xFF, 0xCF, 0xFC, 0xFF, 0xCF, - 0x80, 0xDC, 0x0D, 0xE0, 0xCF, 0x0C, 0x78, 0xC3, 0xCC, 0x1E, 0x40, 0xF0, 0x07, 0x00, 0x00, 0x3F, - 0xE3, 0xFF, 0x3F, 0xF0, 0x1F, 0x03, 0xF0, 0x77, 0x0E, 0x71, 0xC7, 0x38, 0x77, 0x07, 0xE0, 0x2C, - 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0xF0, 0x37, 0x87, 0x3C, 0x71, 0xE7, 0x0F, 0x70, 0x7B, 0x03, - 0xF0, 0x1F, 0x3F, 0xF3, 0xFF, 0x00, 0x00, 0x07, 0xC0, 0xFC, 0x1E, 0xC3, 0xCC, 0x78, 0xCF, 0x0D, - 0xE0, 0xFC, 0x0F, 0x80, 0xFF, 0xCF, 0xFC, 0x00, 0x01, 0xC0, 0x00, 0x0F, 0x80, 0x00, 0x3C, 0x07, - 0x00, 0x70, 0x78, 0x01, 0x87, 0x80, 0x0C, 0x78, 0x00, 0xE7, 0x80, 0x07, 0x7F, 0xFF, 0xF3, 0xFF, - 0xFF, 0x0F, 0xFF, 0xE0, 0x38, 0x00, 0x00, 0xE0, 0x00, 0x03, 0x80, 0x00, 0x0E, 0x00, 0x00, 0x30, - 0x00, 0x0E, 0x00, 0x01, 0xF0, 0x00, 0x1E, 0x00, 0x00, 0xE0, 0x0C, 0x06, 0x00, 0x70, 0x70, 0x01, - 0xC1, 0x80, 0x07, 0x0E, 0x00, 0x1C, 0x3F, 0xFF, 0xF0, 0xFF, 0xFF, 0xC1, 0xFF, 0xFC, 0x00, 0x01, - 0xC0, 0x00, 0x1C, 0x00, 0x01, 0xC0, 0x00, 0x1C, 0x00, 0x00, 0xC0, 0x04, 0x00, 0x03, 0x00, 0x01, - 0xC0, 0x00, 0xF0, 0x00, 0x7F, 0xFF, 0xDF, 0xFF, 0xF3, 0xC0, 0x1C, 0x70, 0x07, 0x0C, 0x01, 0xC0, - 0x00, 0x70, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x07, 0x00, - 0x08, 0x00, 0x06, 0x00, 0x03, 0x80, 0x01, 0xEF, 0xFF, 0xFF, 0xFF, 0xFB, 0x00, 0x39, 0x80, 0x18, - 0xC0, 0x08, 0x60, 0x00, 0x30, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x03, 0x00, 0x01, 0x80, - 0x00, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x0C, 0x71, 0xC7, 0x38, 0x77, 0x07, - 0x7F, 0xF7, 0x00, 0x38, 0x01, 0xC0, 0x0C, 0x00, 0x80, 0x00, 0x60, 0x00, 0x30, 0x00, 0x18, 0x00, - 0x0C, 0x00, 0x06, 0x00, 0x43, 0x00, 0x31, 0x80, 0x1C, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xC0, - 0x01, 0xC0, 0x00, 0xC0, 0x00, 0x40, 0xFF, 0xC3, 0xFF, 0x0F, 0xFC, 0x00, 0x30, 0x00, 0xC0, 0x03, - 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x01, 0xFE, 0x07, 0xF8, 0x0F, 0xC0, 0x1E, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x01, 0xC3, - 0x00, 0x71, 0xC0, 0x1C, 0xF0, 0x07, 0x7F, 0xFF, 0xDF, 0xFF, 0xF3, 0xFF, 0xF8, 0x70, 0x00, 0x0C, - 0x00, 0x01, 0x00, 0x00, 0x01, 0xFC, 0x03, 0xC7, 0x01, 0x81, 0xC1, 0xC0, 0x70, 0xC0, 0x18, 0x60, - 0x0C, 0x30, 0x07, 0x9B, 0x00, 0xEF, 0x80, 0x3F, 0x80, 0x0F, 0x80, 0x03, 0x80, 0x00, 0x80, 0x00, - 0x1F, 0xE0, 0x1E, 0x78, 0x1C, 0x0E, 0x0C, 0x03, 0x0C, 0x01, 0x86, 0x00, 0xE3, 0x00, 0x70, 0x01, - 0x9B, 0x00, 0xEF, 0x80, 0x3F, 0x80, 0x0F, 0x80, 0x03, 0x80, 0x00, 0x80, 0x00, 0x80, 0x01, 0x80, - 0x03, 0x80, 0x07, 0x80, 0x0F, 0xF0, 0x0F, 0xF8, 0x07, 0xBC, 0x03, 0x8E, 0x01, 0x86, 0x00, 0x07, - 0x00, 0x07, 0xC0, 0x07, 0xC0, 0x07, 0xE0, 0x07, 0x60, 0x0E, 0x70, 0x0E, 0x3C, 0x3C, 0x1F, 0xF8, - 0x0F, 0xE0, 0x01, 0x00, 0x01, 0x80, 0x01, 0xC0, 0x01, 0xE0, 0x0F, 0xF0, 0x1F, 0xF0, 0x39, 0xE0, - 0x71, 0xC0, 0xE1, 0x80, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x07, 0xC0, 0x07, 0xE0, 0x07, 0x60, 0x0E, - 0x70, 0x0E, 0x3C, 0x3C, 0x1F, 0xF8, 0x0F, 0xE0, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x1E, - 0xFF, 0xFE, 0x00, 0x1E, 0x08, 0x38, 0x1C, 0x30, 0x38, 0x20, 0x70, 0x00, 0xFF, 0xFE, 0x70, 0x00, - 0x38, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xFF, 0xFE, - 0x70, 0x00, 0x38, 0x20, 0x1C, 0x30, 0x08, 0x38, 0x00, 0x1E, 0xFF, 0xFE, 0x00, 0x1E, 0x00, 0x38, - 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x01, 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xFF, 0xF1, 0xFF, - 0xFD, 0xFF, 0xFC, 0xE0, 0x00, 0x3F, 0xFF, 0x8F, 0xFF, 0xC3, 0x80, 0x00, 0xE0, 0x00, 0x38, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x7F, 0xF8, 0xFF, 0xFC, - 0xFF, 0xFE, 0x00, 0x07, 0xFF, 0xFE, 0xFF, 0xFC, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x00, 0xC0, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x30, 0x01, 0xE0, 0x1C, 0x01, 0xE0, 0x07, 0x01, 0xFF, - 0xFF, 0xC1, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xFC, 0xE0, 0x00, 0x07, 0x3F, 0xFF, 0xFF, 0x0F, 0xFF, - 0xFF, 0x03, 0x80, 0x07, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x07, 0x00, 0x0C, 0x03, 0x00, 0x00, 0x00, - 0x00, 0x0C, 0x07, 0x83, 0xF1, 0xFE, 0x7F, 0xC3, 0x00, 0xC0, 0xFE, 0x3F, 0x83, 0x03, 0xF8, 0xFE, - 0x3F, 0x83, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x3F, 0x8F, 0xE0, 0xC0, 0xFE, 0x3F, - 0x8F, 0xE0, 0xC0, 0x30, 0x7F, 0xCF, 0xE1, 0xF0, 0x38, 0x04, 0x00, 0xE1, 0x80, 0x1C, 0x70, 0x03, - 0x9E, 0x00, 0x77, 0xFF, 0xEE, 0xFF, 0xFF, 0xCF, 0xFF, 0xB8, 0xE0, 0x07, 0x0C, 0x00, 0xE0, 0x80, - 0x00, 0x00, 0x30, 0xE0, 0x07, 0x1C, 0x00, 0xF3, 0xBF, 0xFF, 0x7F, 0xFF, 0xEE, 0xFF, 0xF9, 0xC0, - 0x0E, 0x38, 0x01, 0x87, 0x00, 0x20, 0xE0, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x07, 0xC0, 0x01, 0xDC, - 0x00, 0x71, 0xC0, 0x1C, 0x1C, 0x07, 0x01, 0xC1, 0xC0, 0x1C, 0x7E, 0x0F, 0xCF, 0xC1, 0xF8, 0x18, - 0x30, 0x03, 0x06, 0x00, 0x60, 0xC0, 0x0C, 0x18, 0x01, 0x83, 0x00, 0x3F, 0xE0, 0x07, 0xFC, 0x00, - 0x00, 0x00, 0x00, 0x1C, 0x00, 0x07, 0xC0, 0x01, 0xDC, 0x00, 0x71, 0xC0, 0x1C, 0x1C, 0x07, 0x01, - 0xC1, 0xC0, 0x1C, 0x7E, 0x0F, 0xCF, 0xC1, 0xF8, 0x18, 0x30, 0x03, 0x06, 0x00, 0x7F, 0xC0, 0x0F, - 0xF8, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x07, 0xFC, 0x00, 0xC1, 0x80, 0x1F, 0xF0, 0x03, 0xFE, 0x00, - 0x7F, 0xC0, 0x1C, 0x0F, 0xE1, 0xFE, 0x01, 0xE0, 0x1C, 0x1D, 0x8F, 0xFB, 0xFF, 0x70, 0xFC, 0x1F, - 0x83, 0xF0, 0x7E, 0x0C, 0xE3, 0x9F, 0xE1, 0xFC, 0x04, 0x00, 0x07, 0x88, 0x7F, 0xE3, 0xFF, 0x0E, - 0x1E, 0x70, 0xF9, 0xC6, 0x66, 0x31, 0x9D, 0x86, 0x7C, 0x38, 0xE1, 0xC3, 0xFF, 0x1B, 0xF0, 0x43, - 0x00, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x0F, 0xC0, 0x3F, 0x00, 0xCC, 0x07, 0x38, 0x1C, - 0x60, 0xE1, 0xC3, 0x87, 0x0C, 0x0C, 0x70, 0x39, 0xC0, 0xE7, 0xFF, 0xBF, 0xFF, 0x7F, 0xF7, 0xFF, - 0xBF, 0xFD, 0xC0, 0x6E, 0x03, 0x70, 0x1B, 0x80, 0xDC, 0x06, 0xE0, 0x37, 0x01, 0xB8, 0x0D, 0xC0, - 0x6E, 0x03, 0x70, 0x1B, 0x80, 0xDC, 0x06, 0xE0, 0x37, 0x01, 0x98, 0x0C, 0x7F, 0xE7, 0xFF, 0xBF, - 0xFC, 0xF0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x07, 0x80, 0x1C, 0x00, 0xE0, 0x0E, 0x00, - 0xF0, 0x0F, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xF0, 0x00, 0x30, - 0x03, 0x80, 0x18, 0x01, 0x80, 0x1C, 0x00, 0xC0, 0x0C, 0x00, 0xE0, 0x06, 0x00, 0x60, 0x07, 0x00, - 0x30, 0x03, 0x00, 0x38, 0x01, 0x80, 0x00, 0x00, 0x10, 0x01, 0xC0, 0x1C, 0x00, 0xE0, 0x06, 0x00, - 0x70, 0x03, 0x80, 0x38, 0xF9, 0xC7, 0xCE, 0x06, 0xE0, 0x3F, 0x01, 0xF0, 0x07, 0x80, 0x3C, 0x00, - 0xC0, 0x1E, 0x0F, 0x07, 0xF3, 0xF1, 0xFF, 0xFF, 0x30, 0xF8, 0x76, 0x0E, 0x0E, 0xC3, 0xE1, 0xDE, - 0xFF, 0x71, 0xFC, 0xFE, 0x1E, 0x0F, 0x00, 0x00, 0x07, 0x87, 0xC3, 0x81, 0x80, 0xC0, 0x60, 0x30, - 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x0F, 0x87, 0x80, - 0x6E, 0xE0, 0x00, 0x00, 0x6E, 0xE0, 0x00, 0x07, 0xC6, 0x7E, 0x6E, 0x7E, 0xC3, 0xC3, 0x86, 0x7E, - 0x6E, 0xFE, 0xE7, 0xE0, 0x18, 0x03, 0x01, 0xC0, 0x63, 0xFF, 0xFF, 0xC3, 0x01, 0xC3, 0xFF, 0xFF, - 0xCE, 0x03, 0x00, 0xC0, 0x00, 0xC1, 0xF1, 0xFF, 0xF8, 0xF0, 0x3F, 0x81, 0xFC, 0x0F, 0x00, 0x7F, - 0xFF, 0xFF, 0xFF, 0x80, 0x3C, 0x0F, 0xE0, 0x7F, 0x07, 0xCF, 0xEF, 0xE3, 0xC0, 0x80, 0x3F, 0xFF, - 0xFF, 0xFF, 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xFC, 0x1F, 0x80, 0x0F, 0x00, 0x1E, - 0x00, 0xF0, 0x00, 0x78, 0x0E, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x03, 0x87, 0x00, 0xC0, 0x1C, 0x70, - 0x07, 0x00, 0x73, 0x00, 0x38, 0x01, 0xB8, 0x01, 0xC0, 0x0F, 0xC0, 0x0E, 0x00, 0x7E, 0x00, 0x70, - 0x03, 0xE0, 0xFF, 0xFC, 0x0F, 0x07, 0xFF, 0xF0, 0x78, 0x1F, 0xFF, 0x03, 0xE0, 0x07, 0x00, 0x3F, - 0x00, 0x38, 0x01, 0xF8, 0x01, 0xC0, 0x0C, 0xC0, 0x0E, 0x00, 0xE7, 0x00, 0x70, 0x07, 0x1C, 0x01, - 0x00, 0x70, 0xE0, 0x00, 0x03, 0x83, 0x80, 0x00, 0x38, 0x0F, 0x00, 0x07, 0x80, 0x3E, 0x00, 0xF8, - 0x00, 0xFF, 0x7F, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x00, - 0x07, 0xFF, 0xC0, 0x00, 0xFC, 0x1F, 0x80, 0x0F, 0x00, 0x1E, 0x00, 0xF0, 0x00, 0x78, 0x0E, 0x00, - 0x00, 0xE0, 0xE0, 0x00, 0x03, 0x87, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x73, 0x00, 0x00, 0x01, - 0xB8, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x03, 0xE0, 0x3F, 0xF8, 0x0F, 0x01, - 0xFF, 0xC0, 0x78, 0x00, 0x00, 0x03, 0xE0, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x01, 0xF8, 0x00, 0x00, - 0x0C, 0xC0, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x07, 0x1C, 0x00, 0x00, 0x70, 0xE0, 0x00, 0x03, 0x83, - 0x80, 0x00, 0x38, 0x0F, 0x00, 0x07, 0x80, 0x3E, 0x00, 0xF8, 0x00, 0xFF, 0x7F, 0x80, 0x01, 0xFF, - 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xFC, 0x1F, - 0x80, 0x0F, 0x00, 0x1E, 0x00, 0xF0, 0x00, 0x78, 0x0E, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x03, 0x87, - 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x73, 0x01, 0x83, 0x01, 0xB8, 0x0E, 0x38, 0x0F, 0xC0, 0x7B, - 0xC0, 0x7E, 0x01, 0xFC, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0x78, 0x03, 0xF8, 0x03, - 0xE0, 0x3D, 0xE0, 0x3F, 0x01, 0xC7, 0x01, 0xF8, 0x0C, 0x18, 0x0C, 0xC0, 0x00, 0x00, 0xE7, 0x00, - 0x00, 0x07, 0x1C, 0x00, 0x00, 0x70, 0xE0, 0x00, 0x03, 0x83, 0x80, 0x00, 0x38, 0x0F, 0x00, 0x07, - 0x80, 0x3E, 0x00, 0xF8, 0x00, 0xFF, 0x7F, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, - 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xFC, 0x1F, 0x80, 0x0F, 0x00, 0x1E, 0x00, 0xF0, - 0x00, 0x78, 0x0E, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x03, 0x87, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x20, - 0x73, 0x00, 0x03, 0x81, 0xB8, 0x00, 0x38, 0x0F, 0xC0, 0x03, 0xC0, 0x7E, 0x00, 0x1C, 0x03, 0xE0, - 0x01, 0xC0, 0x0F, 0x00, 0x1C, 0x00, 0x78, 0x01, 0xC0, 0x03, 0xE0, 0x1C, 0x00, 0x3F, 0x01, 0xE0, - 0x01, 0xF8, 0x1E, 0x00, 0x0C, 0xC0, 0xE0, 0x00, 0xE7, 0x02, 0x00, 0x07, 0x1C, 0x00, 0x00, 0x70, - 0xE0, 0x00, 0x03, 0x83, 0x80, 0x00, 0x38, 0x0F, 0x00, 0x07, 0x80, 0x3E, 0x00, 0xF8, 0x00, 0xFF, - 0x7F, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x1E, 0xE6, 0x1C, 0xC0, 0x18, 0x03, - 0x01, 0xFF, 0xCC, 0x39, 0x87, 0x30, 0xE6, 0x1C, 0xC3, 0x98, 0x73, 0x0E, 0x61, 0xCC, 0x39, 0x87, - 0x1E, 0xE3, 0x0E, 0x30, 0xE3, 0x0E, 0x30, 0xEF, 0xEE, 0x30, 0xE3, 0x0E, 0x30, 0xE3, 0x0E, 0x30, - 0xE3, 0x0E, 0x30, 0xE3, 0x0E, 0x30, 0xE3, 0x07, + 0xFF, 0xCF, 0xFC, 0xFF, 0xCF, 0x80, 0xDC, 0x0D, 0xE0, 0xCF, 0x0C, 0x78, 0xC3, 0xCC, 0x1E, 0x40, + 0xF0, 0x07, 0x00, 0x00, 0x3F, 0xE3, 0xFF, 0x3F, 0xF0, 0x1F, 0x03, 0xF0, 0x77, 0x0E, 0x71, 0xC7, + 0x38, 0x77, 0x07, 0xE0, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0xF0, 0x37, 0x87, 0x3C, 0x71, + 0xE7, 0x0F, 0x70, 0x7B, 0x03, 0xF0, 0x1F, 0x3F, 0xF3, 0xFF, 0x00, 0x00, 0x07, 0xC0, 0xFC, 0x1E, + 0xC3, 0xCC, 0x78, 0xCF, 0x0D, 0xE0, 0xFC, 0x0F, 0x80, 0xFF, 0xCF, 0xFC, 0x00, 0x01, 0xC0, 0x00, + 0x0F, 0x80, 0x00, 0x3C, 0x07, 0x00, 0x70, 0x78, 0x01, 0x87, 0x80, 0x0C, 0x78, 0x00, 0xE7, 0x80, + 0x07, 0x7F, 0xFF, 0xF3, 0xFF, 0xFF, 0x0F, 0xFF, 0xE0, 0x38, 0x00, 0x00, 0xE0, 0x00, 0x03, 0x80, + 0x00, 0x0E, 0x00, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x01, 0xF0, 0x00, 0x1E, 0x00, 0x00, 0xE0, 0x0C, + 0x06, 0x00, 0x70, 0x70, 0x01, 0xC1, 0x80, 0x07, 0x0E, 0x00, 0x1C, 0x3F, 0xFF, 0xF0, 0xFF, 0xFF, + 0xC1, 0xFF, 0xFC, 0x00, 0x01, 0xC0, 0x00, 0x1C, 0x00, 0x01, 0xC0, 0x00, 0x1C, 0x00, 0x00, 0xC0, + 0x04, 0x00, 0x03, 0x00, 0x01, 0xC0, 0x00, 0xF0, 0x00, 0x7F, 0xFF, 0xDF, 0xFF, 0xF3, 0xC0, 0x1C, + 0x70, 0x07, 0x0C, 0x01, 0xC0, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x70, + 0x00, 0x1C, 0x00, 0x07, 0x00, 0x08, 0x00, 0x06, 0x00, 0x03, 0x80, 0x01, 0xEF, 0xFF, 0xFF, 0xFF, + 0xFB, 0x00, 0x39, 0x80, 0x18, 0xC0, 0x08, 0x60, 0x00, 0x30, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, + 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x0C, + 0x71, 0xC7, 0x38, 0x77, 0x07, 0x7F, 0xF7, 0x00, 0x38, 0x01, 0xC0, 0x0C, 0x00, 0x80, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x43, 0x00, 0x31, 0x80, 0x1C, 0xFF, 0xFF, + 0x7F, 0xFF, 0xFF, 0xFF, 0xC0, 0x01, 0xC0, 0x00, 0xC0, 0x00, 0x40, 0xFF, 0xC3, 0xFF, 0x0F, 0xFC, + 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x01, 0xFE, + 0x07, 0xF8, 0x0F, 0xC0, 0x1E, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x70, 0x00, 0x1C, + 0x00, 0x07, 0x00, 0x01, 0xC3, 0x00, 0x71, 0xC0, 0x1C, 0xF0, 0x07, 0x7F, 0xFF, 0xDF, 0xFF, 0xF3, + 0xFF, 0xF8, 0x70, 0x00, 0x0C, 0x00, 0x01, 0x00, 0x00, 0x01, 0xFC, 0x03, 0xC7, 0x01, 0x81, 0xC1, + 0xC0, 0x70, 0xC0, 0x18, 0x60, 0x0C, 0x30, 0x07, 0x9B, 0x00, 0xEF, 0x80, 0x3F, 0x80, 0x0F, 0x80, + 0x03, 0x80, 0x00, 0x80, 0x00, 0x1F, 0xE0, 0x1E, 0x78, 0x1C, 0x0E, 0x0C, 0x03, 0x0C, 0x01, 0x86, + 0x00, 0xE3, 0x00, 0x70, 0x01, 0x9B, 0x00, 0xEF, 0x80, 0x3F, 0x80, 0x0F, 0x80, 0x03, 0x80, 0x00, + 0x80, 0x00, 0x80, 0x01, 0x80, 0x03, 0x80, 0x07, 0x80, 0x0F, 0xF0, 0x0F, 0xF8, 0x07, 0xBC, 0x03, + 0x8E, 0x01, 0x86, 0x00, 0x07, 0x00, 0x07, 0xC0, 0x07, 0xC0, 0x07, 0xE0, 0x07, 0x60, 0x0E, 0x70, + 0x0E, 0x3C, 0x3C, 0x1F, 0xF8, 0x0F, 0xE0, 0x01, 0x00, 0x01, 0x80, 0x01, 0xC0, 0x01, 0xE0, 0x0F, + 0xF0, 0x1F, 0xF0, 0x39, 0xE0, 0x71, 0xC0, 0xE1, 0x80, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x07, 0xC0, + 0x07, 0xE0, 0x07, 0x60, 0x0E, 0x70, 0x0E, 0x3C, 0x3C, 0x1F, 0xF8, 0x0F, 0xE0, 0x00, 0x20, 0x00, + 0x30, 0x00, 0x38, 0x00, 0x1E, 0xFF, 0xFE, 0x00, 0x1E, 0x08, 0x38, 0x1C, 0x30, 0x38, 0x20, 0x70, + 0x00, 0xFF, 0xFE, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x38, + 0x00, 0x70, 0x00, 0xFF, 0xFE, 0x70, 0x00, 0x38, 0x20, 0x1C, 0x30, 0x08, 0x38, 0x00, 0x1E, 0xFF, + 0xFE, 0x00, 0x1E, 0x00, 0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x01, 0xC0, 0x01, 0xE0, 0x01, + 0xE0, 0x01, 0xFF, 0xF1, 0xFF, 0xFD, 0xFF, 0xFC, 0xE0, 0x00, 0x3F, 0xFF, 0x8F, 0xFF, 0xC3, 0x80, + 0x00, 0xE0, 0x00, 0x38, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xE0, 0x00, + 0x70, 0x7F, 0xF8, 0xFF, 0xFC, 0xFF, 0xFE, 0x00, 0x07, 0xFF, 0xFE, 0xFF, 0xFC, 0x00, 0x38, 0x00, + 0x70, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x30, 0x01, 0xE0, 0x1C, + 0x01, 0xE0, 0x07, 0x01, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xFC, 0xE0, 0x00, 0x07, + 0x3F, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0x03, 0x80, 0x07, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x07, 0x00, + 0x0C, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x07, 0x83, 0xF1, 0xFE, 0x7F, 0xC3, 0x00, 0xC0, 0xFE, + 0x3F, 0x83, 0x03, 0xF8, 0xFE, 0x3F, 0x83, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x3F, + 0x8F, 0xE0, 0xC0, 0xFE, 0x3F, 0x8F, 0xE0, 0xC0, 0x30, 0x7F, 0xCF, 0xE1, 0xF0, 0x38, 0x04, 0x00, + 0xE1, 0x80, 0x1C, 0x70, 0x03, 0x9E, 0x00, 0x77, 0xFF, 0xEE, 0xFF, 0xFF, 0xCF, 0xFF, 0xB8, 0xE0, + 0x07, 0x0C, 0x00, 0xE0, 0x80, 0x00, 0x00, 0x30, 0xE0, 0x07, 0x1C, 0x00, 0xF3, 0xBF, 0xFF, 0x7F, + 0xFF, 0xEE, 0xFF, 0xF9, 0xC0, 0x0E, 0x38, 0x01, 0x87, 0x00, 0x20, 0xE0, 0x00, 0x00, 0x00, 0x1C, + 0x00, 0x07, 0xC0, 0x01, 0xDC, 0x00, 0x71, 0xC0, 0x1C, 0x1C, 0x07, 0x01, 0xC1, 0xC0, 0x1C, 0x7E, + 0x0F, 0xCF, 0xC1, 0xF8, 0x18, 0x30, 0x03, 0x06, 0x00, 0x60, 0xC0, 0x0C, 0x18, 0x01, 0x83, 0x00, + 0x3F, 0xE0, 0x07, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x07, 0xC0, 0x01, 0xDC, 0x00, 0x71, + 0xC0, 0x1C, 0x1C, 0x07, 0x01, 0xC1, 0xC0, 0x1C, 0x7E, 0x0F, 0xCF, 0xC1, 0xF8, 0x18, 0x30, 0x03, + 0x06, 0x00, 0x7F, 0xC0, 0x0F, 0xF8, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x07, 0xFC, 0x00, 0xC1, 0x80, + 0x1F, 0xF0, 0x03, 0xFE, 0x00, 0x7F, 0xC0, 0x1C, 0x0F, 0xE1, 0xFE, 0x01, 0xE0, 0x1C, 0x1D, 0x8F, + 0xFB, 0xFF, 0x70, 0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0C, 0xE3, 0x9F, 0xE1, 0xFC, 0x04, 0x00, 0x07, + 0x88, 0x7F, 0xE3, 0xFF, 0x0E, 0x1E, 0x70, 0xF9, 0xC6, 0x66, 0x31, 0x9D, 0x86, 0x7C, 0x38, 0xE1, + 0xC3, 0xFF, 0x1B, 0xF0, 0x43, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x0F, 0xC0, 0x3F, + 0x00, 0xCC, 0x07, 0x38, 0x1C, 0x60, 0xE1, 0xC3, 0x87, 0x0C, 0x0C, 0x70, 0x39, 0xC0, 0xE7, 0xFF, + 0xBF, 0xFF, 0x7F, 0xF7, 0xFF, 0xBF, 0xFD, 0xC0, 0x6E, 0x03, 0x70, 0x1B, 0x80, 0xDC, 0x06, 0xE0, + 0x37, 0x01, 0xB8, 0x0D, 0xC0, 0x6E, 0x03, 0x70, 0x1B, 0x80, 0xDC, 0x06, 0xE0, 0x37, 0x01, 0x98, + 0x0C, 0x7F, 0xE7, 0xFF, 0xBF, 0xFC, 0xF0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x07, 0x80, + 0x1C, 0x00, 0xE0, 0x0E, 0x00, 0xF0, 0x0F, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0xFF, 0xFF, 0xFE, + 0xFF, 0xFF, 0xF0, 0x00, 0x30, 0x03, 0x80, 0x18, 0x01, 0x80, 0x1C, 0x00, 0xC0, 0x0C, 0x00, 0xE0, + 0x06, 0x00, 0x60, 0x07, 0x00, 0x30, 0x03, 0x00, 0x38, 0x01, 0x80, 0x00, 0x00, 0x10, 0x01, 0xC0, + 0x1C, 0x00, 0xE0, 0x06, 0x00, 0x70, 0x03, 0x80, 0x38, 0xF9, 0xC7, 0xCE, 0x06, 0xE0, 0x3F, 0x01, + 0xF0, 0x07, 0x80, 0x3C, 0x00, 0xC0, 0x1E, 0x0F, 0x07, 0xF3, 0xF1, 0xFF, 0xFF, 0x30, 0xF8, 0x76, + 0x0E, 0x0E, 0xC3, 0xE1, 0xDE, 0xFF, 0x71, 0xFC, 0xFE, 0x1E, 0x0F, 0x00, 0x00, 0x07, 0x87, 0xC3, + 0x81, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, + 0x06, 0x03, 0x0F, 0x87, 0x80, 0x6E, 0xE0, 0x00, 0x00, 0x6E, 0xE0, 0x00, 0x07, 0xC6, 0x7E, 0x6E, + 0x7E, 0xC3, 0xC3, 0x86, 0x7E, 0x6E, 0xFE, 0xE7, 0xE0, 0x18, 0x03, 0x01, 0xC0, 0x63, 0xFF, 0xFF, + 0xC3, 0x01, 0xC3, 0xFF, 0xFF, 0xCE, 0x03, 0x00, 0xC0, 0x00, 0xC1, 0xF1, 0xFF, 0xF8, 0xF0, 0x3F, + 0x81, 0xFC, 0x0F, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0x3C, 0x0F, 0xE0, 0x7F, 0x07, 0xCF, 0xEF, + 0xE3, 0xC0, 0x80, 0x3F, 0xFF, 0xFF, 0xFF, 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xFC, + 0x1F, 0x80, 0x0F, 0x00, 0x1E, 0x00, 0xF0, 0x00, 0x78, 0x0E, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x03, + 0x87, 0x00, 0xC0, 0x1C, 0x70, 0x07, 0x00, 0x73, 0x00, 0x38, 0x01, 0xB8, 0x01, 0xC0, 0x0F, 0xC0, + 0x0E, 0x00, 0x7E, 0x00, 0x70, 0x03, 0xE0, 0xFF, 0xFC, 0x0F, 0x07, 0xFF, 0xF0, 0x78, 0x1F, 0xFF, + 0x03, 0xE0, 0x07, 0x00, 0x3F, 0x00, 0x38, 0x01, 0xF8, 0x01, 0xC0, 0x0C, 0xC0, 0x0E, 0x00, 0xE7, + 0x00, 0x70, 0x07, 0x1C, 0x01, 0x00, 0x70, 0xE0, 0x00, 0x03, 0x83, 0x80, 0x00, 0x38, 0x0F, 0x00, + 0x07, 0x80, 0x3E, 0x00, 0xF8, 0x00, 0xFF, 0x7F, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, 0x00, + 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xFC, 0x1F, 0x80, 0x0F, 0x00, 0x1E, 0x00, + 0xF0, 0x00, 0x78, 0x0E, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x03, 0x87, 0x00, 0x00, 0x1C, 0x70, 0x00, + 0x00, 0x73, 0x00, 0x00, 0x01, 0xB8, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x03, + 0xE0, 0x3F, 0xF8, 0x0F, 0x01, 0xFF, 0xC0, 0x78, 0x00, 0x00, 0x03, 0xE0, 0x00, 0x00, 0x3F, 0x00, + 0x00, 0x01, 0xF8, 0x00, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x07, 0x1C, 0x00, 0x00, + 0x70, 0xE0, 0x00, 0x03, 0x83, 0x80, 0x00, 0x38, 0x0F, 0x00, 0x07, 0x80, 0x3E, 0x00, 0xF8, 0x00, + 0xFF, 0x7F, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x07, + 0xFF, 0xC0, 0x00, 0xFC, 0x1F, 0x80, 0x0F, 0x00, 0x1E, 0x00, 0xF0, 0x00, 0x78, 0x0E, 0x00, 0x00, + 0xE0, 0xE0, 0x00, 0x03, 0x87, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x73, 0x01, 0x83, 0x01, 0xB8, + 0x0E, 0x38, 0x0F, 0xC0, 0x7B, 0xC0, 0x7E, 0x01, 0xFC, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x00, 0x3E, + 0x00, 0x78, 0x03, 0xF8, 0x03, 0xE0, 0x3D, 0xE0, 0x3F, 0x01, 0xC7, 0x01, 0xF8, 0x0C, 0x18, 0x0C, + 0xC0, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x07, 0x1C, 0x00, 0x00, 0x70, 0xE0, 0x00, 0x03, 0x83, 0x80, + 0x00, 0x38, 0x0F, 0x00, 0x07, 0x80, 0x3E, 0x00, 0xF8, 0x00, 0xFF, 0x7F, 0x80, 0x01, 0xFF, 0xF0, + 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xFC, 0x1F, 0x80, + 0x0F, 0x00, 0x1E, 0x00, 0xF0, 0x00, 0x78, 0x0E, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x03, 0x87, 0x00, + 0x00, 0x1C, 0x70, 0x00, 0x20, 0x73, 0x00, 0x03, 0x81, 0xB8, 0x00, 0x38, 0x0F, 0xC0, 0x03, 0xC0, + 0x7E, 0x00, 0x1C, 0x03, 0xE0, 0x01, 0xC0, 0x0F, 0x00, 0x1C, 0x00, 0x78, 0x01, 0xC0, 0x03, 0xE0, + 0x1C, 0x00, 0x3F, 0x01, 0xE0, 0x01, 0xF8, 0x1E, 0x00, 0x0C, 0xC0, 0xE0, 0x00, 0xE7, 0x02, 0x00, + 0x07, 0x1C, 0x00, 0x00, 0x70, 0xE0, 0x00, 0x03, 0x83, 0x80, 0x00, 0x38, 0x0F, 0x00, 0x07, 0x80, + 0x3E, 0x00, 0xF8, 0x00, 0xFF, 0x7F, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x1E, + 0xE6, 0x1C, 0xC0, 0x18, 0x03, 0x01, 0xFF, 0xCC, 0x39, 0x87, 0x30, 0xE6, 0x1C, 0xC3, 0x98, 0x73, + 0x0E, 0x61, 0xCC, 0x39, 0x87, 0x1E, 0xE3, 0x0E, 0x30, 0xE3, 0x0E, 0x30, 0xEF, 0xEE, 0x30, 0xE3, + 0x0E, 0x30, 0xE3, 0x0E, 0x30, 0xE3, 0x0E, 0x30, 0xE3, 0x0E, 0x30, 0xE3, 0x07, 0x00, 0x00, 0x20, + 0x00, 0x06, 0x60, 0x00, 0x7E, 0x00, 0x07, 0xE0, 0x00, 0x3E, 0x00, 0x03, 0xE0, 0x00, 0x77, 0xFF, + 0xFE, 0x3F, 0xFF, 0xC0, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0xF8, 0x00, 0x0F, 0x80, + 0x00, 0x60, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0xE0, 0x00, 0x30, 0xE0, 0x00, 0x78, 0xE0, 0x01, 0xF8, 0x7F, 0xFF, 0xFF, 0x3F, 0xFF, 0x1F, + 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x01, 0xB0, 0x00, 0x00, + 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x40, 0x00, 0x08, 0x71, 0xC3, 0x0C, 0x30, 0xDF, 0x78, 0x00, + 0x1F, 0x6C, 0xE3, 0x84, 0x1C, 0x0C, 0x06, 0x07, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x7C, 0x3E, 0x06, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xF8, 0x00, 0x0F, 0x80, 0x00, 0xF8, 0x00, + 0x1F, 0x80, 0x00, 0xF0, 0x20, 0x00, 0x06, 0x60, 0x00, 0x7E, 0x00, 0x07, 0xE0, 0x00, 0x3E, 0x00, + 0x03, 0xE0, 0x00, 0x77, 0xFF, 0xFE, 0x3F, 0xFF, 0xC0, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xFC, 0x00, 0x01, 0xF8, 0x00, 0x00, 0xF0, 0x00, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x30, 0xE0, 0x00, 0x78, 0xE0, + 0x01, 0xF8, 0x7F, 0xFF, 0xFF, 0x3F, 0xFF, 0x1F, 0x0F, 0xF0, 0x00, 0x01, 0x87, 0x9F, 0x6F, 0xFF, + 0x00, 0x10, 0xC3, 0x0E, 0x38, 0x63, 0xBC, 0xF0, 0x00, 0x00, 0x18, 0x0F, 0x07, 0xC3, 0xE3, 0xF0, + 0xF0, 0x00, 0x00, 0x0E, 0x06, 0x03, 0x03, 0x87, 0xFF, 0xFE, 0x00, 0x7E, 0x07, 0xFE, 0x13, 0xFE, + 0x07, 0xF0, 0xFC, 0x0F, 0x00, 0xE0, 0x0E, 0x00, 0x67, 0xC7, 0x36, 0x30, 0xC1, 0x87, 0x0C, 0x00, + 0x70, 0x01, 0xE0, 0x07, 0xFF, 0x1F, 0xF8, 0x0C, 0x00, 0x7E, 0x00, 0x7F, 0xE0, 0x13, 0xFE, 0x00, + 0x7F, 0x00, 0xFF, 0x00, 0xF3, 0x80, 0xE0, 0xE0, 0xE0, 0x7E, 0x67, 0xDF, 0x73, 0x60, 0x30, 0xC0, + 0x18, 0x70, 0x0C, 0x00, 0x07, 0x00, 0x01, 0xE0, 0x00, 0x7F, 0xF0, 0x1F, 0xF8, 0x00, 0xC0, 0x00, + 0x38, 0x07, 0xF0, 0x7F, 0x80, 0x1F, 0x03, 0xF0, 0xF8, 0xFE, 0x0F, 0x80, 0x00, 0x00, 0x80, 0x1F, + 0x81, 0xF0, 0x06, 0x00, 0xE0, 0x00, 0x00, 0x38, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x0F, 0x80, 0x3F, + 0x01, 0xFC, 0x3F, 0x9F, 0x7C, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x6C, 0x00, 0xD8, 0x00, 0xE0, 0x01, + 0xC0, 0x01, 0x00, 0x00, 0x00, 0x0C, 0x07, 0x83, 0xE1, 0xB1, 0xF0, 0xF0, 0x00, 0x00, 0x03, 0x03, + 0x80, 0xE0, 0x30, 0x1C, 0x06, 0x03, 0x41, 0xBF, 0xDF, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x80, + 0x3E, 0x01, 0xB0, 0x1F, 0x80, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x80, 0x0E, 0x00, 0x30, + 0x01, 0xC0, 0x06, 0x00, 0x38, 0x41, 0xC3, 0xFF, 0x9F, 0xDC, 0x00, 0x00, 0x00, 0x1C, 0x1C, 0x36, + 0x3E, 0x12, 0x00, 0x00, 0x0C, 0x0E, 0x06, 0x07, 0x07, 0x07, 0x06, 0x0E, 0x1C, 0x7C, 0xF0, 0x60, + 0x00, 0x03, 0x80, 0x70, 0x1B, 0x03, 0xE0, 0x08, 0x00, 0x00, 0x00, 0x0C, 0x01, 0xC0, 0x18, 0x03, + 0x80, 0x70, 0x0F, 0x81, 0xF8, 0x70, 0x1C, 0x0F, 0x83, 0xE0, 0x30, 0x00, 0x00, 0x18, 0x1E, 0x1F, + 0x1B, 0x3F, 0x3C, 0x00, 0x00, 0x0C, 0x0E, 0x06, 0x07, 0x07, 0x07, 0x06, 0x0E, 0x1C, 0x7C, 0xF0, + 0x60, 0x00, 0x06, 0x00, 0x78, 0x0F, 0x83, 0xB0, 0x7C, 0x0F, 0x00, 0x00, 0x00, 0x01, 0x80, 0x38, + 0x03, 0x00, 0x70, 0x0E, 0x01, 0xF0, 0x3F, 0x0E, 0x03, 0x81, 0xF0, 0x7C, 0x06, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x3C, 0x00, 0x1F, 0x00, 0x0F, 0x80, 0x03, 0xC0, 0x00, 0x70, 0x00, 0x0F, 0x00, 0x00, + 0xF0, 0x60, 0x07, 0x1C, 0x00, 0x73, 0x80, 0x06, 0x70, 0x00, 0xCE, 0x00, 0x18, 0xFF, 0xFF, 0x0F, + 0xFF, 0xC0, 0x7F, 0xC0, 0x00, 0x00, 0x40, 0x00, 0x07, 0x80, 0x00, 0x7C, 0x00, 0x07, 0xC0, 0x00, + 0x3C, 0x00, 0x00, 0xE0, 0x00, 0x03, 0xC0, 0x00, 0x07, 0x80, 0x60, 0x0F, 0x03, 0x80, 0x1E, 0x0E, + 0x00, 0x38, 0x38, 0x00, 0x70, 0xE0, 0x01, 0xE1, 0xFF, 0xFF, 0xE3, 0xFF, 0xF7, 0xC3, 0xFE, 0x00, + 0x01, 0x83, 0xC7, 0xCF, 0x87, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0x80, 0xE0, 0x77, 0xF3, + 0xF0, 0x00, 0x01, 0x80, 0x78, 0x1F, 0x07, 0xC0, 0x70, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, + 0xE0, 0x06, 0x00, 0x70, 0x07, 0x8F, 0xFF, 0xFC, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x1E, 0x00, 0x0F, 0xA0, 0x03, 0xDE, 0x00, 0x6F, 0x80, 0x07, 0xC0, 0x01, 0xE0, 0x00, 0x38, 0x00, + 0x07, 0x80, 0x00, 0x78, 0x30, 0x03, 0x8E, 0x00, 0x39, 0xC0, 0x03, 0x38, 0x00, 0x67, 0x00, 0x0C, + 0x7F, 0xFF, 0x87, 0xFF, 0xE0, 0x3F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x78, + 0x00, 0x07, 0xD0, 0x00, 0x3D, 0xE0, 0x00, 0xDF, 0x00, 0x01, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x38, + 0x00, 0x00, 0xF0, 0x00, 0x01, 0xE0, 0x18, 0x03, 0xC0, 0xE0, 0x07, 0x83, 0x80, 0x0E, 0x0E, 0x00, + 0x1C, 0x38, 0x00, 0x78, 0x7F, 0xFF, 0xF8, 0xFF, 0xFD, 0xF0, 0xFF, 0x80, 0x00, 0x00, 0x03, 0x87, + 0xC7, 0xBF, 0x7E, 0xF9, 0xF0, 0xE0, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x30, 0x1C, 0x0E, 0xFE, + 0x7E, 0x00, 0x00, 0x00, 0x00, 0x70, 0x1F, 0x03, 0xD8, 0xF7, 0x8D, 0xF0, 0x7C, 0x07, 0x00, 0xE0, + 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x78, 0xFF, 0xFF, 0xCF, 0x00, 0x00, + 0x00, 0x10, 0x00, 0xE0, 0x03, 0x98, 0x06, 0x60, 0x1B, 0x80, 0x7E, 0x01, 0xF8, 0x06, 0xE0, 0x39, + 0xE3, 0xC3, 0xFF, 0x07, 0xF0, 0x00, 0x38, 0x00, 0x1C, 0x18, 0x06, 0x0C, 0x03, 0x8E, 0x01, 0xF7, + 0x00, 0xFF, 0x80, 0x61, 0xC0, 0x70, 0x78, 0x78, 0x1F, 0xF8, 0x07, 0xF0, 0x00, 0x18, 0x1C, 0x3E, + 0x7E, 0x67, 0xE3, 0xE3, 0xE3, 0x7F, 0x7E, 0x3E, 0x07, 0xE0, 0xE6, 0x0E, 0x70, 0xE3, 0x0E, 0x3E, + 0x61, 0xE0, 0x00, 0x11, 0x8C, 0x73, 0x8C, 0xFE, 0xF1, 0x08, 0xE7, 0x00, 0x00, 0x06, 0x03, 0xC7, + 0x79, 0xE3, 0x78, 0x7C, 0x07, 0x80, 0xF0, 0x0E, 0x00, 0x80, 0x00, 0x00, 0x1C, 0x00, 0x78, 0x03, + 0xF0, 0x0D, 0xE0, 0x73, 0xC1, 0xCF, 0x83, 0x76, 0x0F, 0x9B, 0xFF, 0xEF, 0xFF, 0x8C, 0x18, 0x01, + 0xC0, 0x0F, 0x80, 0x7E, 0x03, 0x98, 0x0E, 0x60, 0x33, 0x80, 0xDC, 0x3F, 0xFF, 0xFF, 0xFC, 0x30, + 0xC0, 0xE3, 0x03, 0xFC, 0x07, 0xF0, 0x07, 0x00, 0x00, 0x01, 0xC0, 0x1E, 0x03, 0xF0, 0x37, 0x87, + 0x3C, 0x73, 0xE3, 0x76, 0x3E, 0x6F, 0xFE, 0xFF, 0xE0, 0x18, 0x07, 0x01, 0xF0, 0x1F, 0x83, 0xB8, + 0x33, 0x83, 0x38, 0x77, 0x0F, 0xFF, 0xFF, 0xF7, 0x1C, 0x30, 0xC3, 0xFC, 0x1F, 0x80, 0xF0, 0x06, + 0x00, 0x01, 0xE0, 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x03, 0x98, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x07, + 0x00, 0x00, 0x7F, 0xFF, 0xE7, 0xFF, 0xFC, 0x1F, 0xFC, 0x00, 0x07, 0xF9, 0xFF, 0xFF, 0xF9, 0xC0, + 0x0C, 0x00, 0x7C, 0x01, 0xFF, 0xE1, 0xFF, 0x38, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x00, 0x1E, 0x00, + 0x03, 0x80, 0x00, 0x0C, 0x00, 0x03, 0xC0, 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x07, 0x30, 0x01, 0xC0, + 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xFF, 0xFF, 0xCF, 0xFF, 0xF8, 0x3F, 0xF8, 0x38, 0x01, 0xC0, + 0x0C, 0x00, 0x78, 0x03, 0x80, 0x03, 0xFC, 0xFF, 0xFF, 0xFC, 0xE0, 0x06, 0x00, 0x3E, 0x00, 0xFF, + 0xF0, 0xFF, 0x80, 0x00, 0x1E, 0x00, 0x7F, 0x00, 0x72, 0x00, 0xE0, 0x00, 0xC0, 0x60, 0xE0, 0x60, + 0xF8, 0xE0, 0x7E, 0xE0, 0x0E, 0xE0, 0x0E, 0xE0, 0x1E, 0x78, 0x7C, 0x3F, 0xF8, 0x1F, 0xE0, 0x60, + 0x00, 0x30, 0x1C, 0x38, 0x0F, 0xDC, 0x03, 0xFE, 0x00, 0x67, 0x00, 0x71, 0xE0, 0xF0, 0x7F, 0xF0, + 0x1F, 0xE0, 0x00, 0x08, 0x71, 0xC3, 0x0C, 0x30, 0xDF, 0x78, 0x00, 0x1B, 0x6C, 0x1C, 0x0C, 0x06, + 0x07, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x7E, 0x3E, 0x00, 0x1C, 0x3E, 0x7C, 0x60, 0x60, 0x73, 0x3F, + 0xFE, 0xF8, 0x00, 0x78, 0x7F, 0x0E, 0x00, 0x18, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x78, 0x1F, 0xC0, 0xE0, 0x00, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, + 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0xE0, 0x78, 0x00, 0xF7, 0x31, + 0xFE, 0x01, 0x8E, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x9C, 0xE0, 0xF0, 0xE0, 0xC0, 0xF8, 0xE0, + 0x00, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x30, 0x30, 0x30, 0x3E, 0x1E, + 0x00, 0x1C, 0x1E, 0x0E, 0x03, 0xC3, 0xC0, 0x00, 0x78, 0x7E, 0x77, 0x31, 0x98, 0xEF, 0xF3, 0xF0, + 0x78, 0x1C, 0x7C, 0x7C, 0x38, 0x00, 0x1C, 0x07, 0x80, 0xE0, 0x0F, 0x03, 0xC0, 0x00, 0x07, 0x81, + 0xF8, 0x77, 0x0C, 0x61, 0x8E, 0x3F, 0xF3, 0xFE, 0x0E, 0x01, 0xC1, 0xF0, 0x7C, 0x0E, 0x00, 0xE7, + 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x31, 0x80, 0xF7, 0x31, 0xEE, 0x00, 0xE1, 0xC3, 0x87, + 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC1, 0x83, 0x06, 0x0F, 0x8F, 0x00, 0xF1, 0xC3, 0x07, 0x8E, 0x00, + 0x1C, 0x1E, 0x38, 0x7F, 0x30, 0x72, 0x3E, 0xE0, 0x3C, 0xC0, 0x60, 0xE0, 0x60, 0xF8, 0xE0, 0x7E, + 0xE0, 0x0E, 0xE0, 0x0E, 0xE0, 0x1E, 0x78, 0x7C, 0x3F, 0xF8, 0x1F, 0xE0, 0x1C, 0x00, 0x1C, 0x00, + 0x0C, 0x00, 0x07, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x01, 0x80, 0xE1, 0xC0, 0x7E, 0xE0, 0x1F, 0xF0, + 0x03, 0x38, 0x03, 0x8F, 0x07, 0x83, 0xFF, 0x80, 0xFF, 0x00, 0x77, 0x30, 0xFF, 0x00, 0x46, 0x31, + 0xCE, 0x33, 0xFB, 0xC0, 0x78, 0x38, 0x18, 0x0F, 0x07, 0x00, 0x00, 0x60, 0x30, 0x38, 0x1C, 0x3F, + 0xDD, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xD8, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, + 0xE1, 0xC1, 0x83, 0x06, 0x0F, 0x8F, 0x00, 0x00, 0x00, 0x20, 0x00, 0x06, 0x60, 0x00, 0x7E, 0x00, + 0x07, 0xE0, 0x00, 0x3E, 0x00, 0x03, 0xE0, 0x00, 0x77, 0xFF, 0xFE, 0x3F, 0xFF, 0xC0, 0xFF, 0xC0, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0xE0, 0x00, 0x30, 0xE0, 0x00, 0x78, 0xE0, 0x01, 0xF8, 0x7F, 0xFF, 0xFF, 0x3F, 0xFF, 0x1F, + 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x11, 0x8C, 0x73, 0x8C, + 0xFE, 0xF0, 0x00, 0xE6, 0x00, 0x18, 0x0C, 0x0E, 0x07, 0x0F, 0xF7, 0x78, 0x00, 0x00, 0x38, 0x1C, + 0x00, 0x36, 0x3E, 0x04, 0x00, 0x18, 0x1C, 0x3E, 0x7E, 0x67, 0xE3, 0xE3, 0xE3, 0x7F, 0x7E, 0x1F, + 0x03, 0xF0, 0x12, 0x00, 0x00, 0x03, 0x00, 0x70, 0x1F, 0x03, 0xF0, 0x73, 0x0E, 0x30, 0xE3, 0x07, + 0xF0, 0x7F, 0x80, 0xFE, 0x01, 0xE0, 0x00, 0x00, 0xF8, 0x00, 0x1F, 0x80, 0x00, 0x90, 0x20, 0x00, + 0x06, 0x60, 0x00, 0x7E, 0x00, 0x07, 0xE0, 0x00, 0x3E, 0x00, 0x03, 0xE0, 0x00, 0x77, 0xFF, 0xFE, + 0x3F, 0xFF, 0xC0, 0xFF, 0xC0, 0x00, 0xF8, 0x00, 0x01, 0xF8, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x60, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x30, 0xE0, 0x00, 0x78, 0xE0, 0x01, 0xF8, + 0x7F, 0xFF, 0xFF, 0x3F, 0xFF, 0x1F, 0x0F, 0xF0, 0x00, 0xDB, 0xE4, 0x00, 0x10, 0xC3, 0x0E, 0x38, + 0x63, 0xBC, 0xF0, 0x00, 0x3E, 0x3F, 0x09, 0x00, 0x01, 0xC0, 0xC0, 0x60, 0x70, 0xFF, 0xFF, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x60, 0x00, 0x0F, 0x80, 0x01, 0xF8, 0x00, 0x09, 0x02, + 0x00, 0x00, 0x66, 0x00, 0x07, 0xE0, 0x00, 0x7E, 0x00, 0x03, 0xE0, 0x00, 0x3E, 0x00, 0x07, 0x7F, + 0xFF, 0xE3, 0xFF, 0xFC, 0x0F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x60, 0x00, + 0x00, 0xF8, 0x00, 0x01, 0xF8, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0xE0, 0x00, 0x30, 0xE0, 0x00, 0x78, 0xE0, 0x01, 0xF8, 0x7F, 0xFF, 0xFF, 0x3F, 0xFF, + 0x1F, 0x0F, 0xF0, 0x00, 0x01, 0xC7, 0x36, 0xF9, 0x00, 0x04, 0x30, 0xC3, 0x8E, 0x18, 0xEF, 0x3C, + 0x00, 0x00, 0x0C, 0x06, 0x07, 0xC7, 0xE1, 0x20, 0x00, 0x38, 0x18, 0x0C, 0x0E, 0x1F, 0xFF, 0xF8, + 0x00, 0x7C, 0x07, 0xFC, 0x13, 0xFC, 0x07, 0xE0, 0xF8, 0x0F, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x07, + 0x1C, 0x30, 0xE1, 0x80, 0x0C, 0x00, 0x70, 0x01, 0xC0, 0x4F, 0xFF, 0x1F, 0xF0, 0x1C, 0x00, 0x7C, + 0x01, 0xFF, 0x01, 0x3F, 0xC0, 0x1F, 0x80, 0xFC, 0x03, 0xD8, 0x0E, 0x38, 0x38, 0x3F, 0xE0, 0x3F, + 0xC7, 0x03, 0x0E, 0x06, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x1C, 0x04, 0x3F, 0xFC, 0x1F, 0xF0, 0x07, + 0x00, 0x38, 0x07, 0xF0, 0x7F, 0x80, 0x1F, 0x03, 0xF0, 0xF8, 0xFE, 0x0F, 0x80, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x60, 0x38, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x0F, 0x80, 0x3F, 0x01, 0xFC, 0x3F, 0x9F, + 0x7C, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x70, 0x00, 0x7C, 0x07, 0xFC, 0x13, 0xFC, 0x07, + 0xE0, 0xF8, 0x0F, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x07, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x70, + 0x01, 0xC0, 0x4F, 0xFF, 0x1F, 0xF0, 0x1C, 0x00, 0x7C, 0x01, 0xFF, 0x01, 0x3F, 0xC0, 0x1F, 0x80, + 0xFC, 0x03, 0xD8, 0x0E, 0x38, 0x38, 0x3F, 0xE0, 0x3F, 0xC0, 0x03, 0x00, 0x06, 0x00, 0x0C, 0x00, + 0x1C, 0x00, 0x1C, 0x04, 0x3F, 0xFC, 0x1F, 0xF0, 0x07, 0x00, 0x38, 0x07, 0xF0, 0x7F, 0x80, 0x1F, + 0x03, 0xF0, 0xF8, 0xFE, 0x0F, 0x80, 0x00, 0x00, 0x38, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x0F, 0x80, + 0x3F, 0x01, 0xFC, 0x3F, 0x9F, 0x7C, 0x1F, 0x00, 0x00, 0x07, 0x00, 0x38, 0x00, 0x80, 0x00, 0x07, + 0xC0, 0x7F, 0xC1, 0x3F, 0xC0, 0x7E, 0x0F, 0x80, 0xF0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x70, 0x03, + 0x00, 0x18, 0x00, 0xC0, 0x07, 0x00, 0x1C, 0x04, 0xFF, 0xF1, 0xFF, 0x01, 0xC0, 0x07, 0x00, 0x0E, + 0x00, 0x08, 0x00, 0x00, 0x07, 0xC0, 0x1F, 0xF0, 0x13, 0xFC, 0x01, 0xF8, 0x0F, 0xC0, 0x3D, 0x80, + 0xE3, 0x83, 0x83, 0xFE, 0x03, 0xFC, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0xC0, 0x01, 0xC0, + 0x43, 0xFF, 0xC1, 0xFF, 0x00, 0x70, 0x00, 0x38, 0x03, 0x80, 0x10, 0x00, 0x00, 0x38, 0x07, 0xF0, + 0x7F, 0x80, 0x1F, 0x03, 0xF0, 0xF8, 0xFE, 0x0F, 0x80, 0x00, 0x00, 0x30, 0x00, 0x70, 0x00, 0x40, + 0x00, 0x00, 0x03, 0x80, 0x0F, 0xE0, 0x1F, 0xE0, 0x00, 0xF8, 0x03, 0xF0, 0x1F, 0xC3, 0xF9, 0xF7, + 0xC1, 0xF0, 0x00, 0x00, 0x06, 0x07, 0x01, 0xC0, 0x60, 0x38, 0x0C, 0x06, 0x83, 0x7F, 0xBF, 0x80, + 0x00, 0x06, 0x00, 0x70, 0x01, 0xC0, 0x06, 0x00, 0x38, 0x00, 0xC0, 0x07, 0x08, 0x38, 0x7F, 0xF3, + 0xFB, 0x80, 0x00, 0x1C, 0x0E, 0x02, 0x00, 0x00, 0x60, 0x70, 0x1C, 0x06, 0x03, 0x80, 0xC0, 0x68, + 0x37, 0xFB, 0xF8, 0x00, 0x1C, 0x00, 0xE0, 0x02, 0x00, 0x00, 0x00, 0x60, 0x07, 0x00, 0x1C, 0x00, + 0x60, 0x03, 0x80, 0x0C, 0x00, 0x70, 0x83, 0x87, 0xFF, 0x3F, 0xB8, 0x00, 0x00, 0x00, 0x0C, 0x0E, + 0x06, 0x07, 0x07, 0x07, 0x06, 0x0E, 0x1C, 0x7C, 0xF0, 0x60, 0x00, 0x01, 0x80, 0x38, 0x03, 0x00, + 0x70, 0x0E, 0x01, 0xF0, 0x3F, 0x0E, 0x03, 0x81, 0xF0, 0x7C, 0x06, 0x00, 0x0C, 0x1C, 0x08, 0x00, + 0x00, 0x0C, 0x0E, 0x06, 0x07, 0x07, 0x07, 0x06, 0x0E, 0x1C, 0x7C, 0xF0, 0x60, 0x1C, 0x03, 0x80, + 0x20, 0x00, 0x00, 0x00, 0x18, 0x03, 0x80, 0x30, 0x07, 0x00, 0xE0, 0x1F, 0x03, 0xF0, 0xE0, 0x38, + 0x1F, 0x07, 0xC0, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0x80, 0x00, 0x01, 0xC0, 0x07, + 0x0C, 0x60, 0x03, 0x86, 0x33, 0x00, 0xC3, 0x1D, 0x80, 0x73, 0x8D, 0xC0, 0x3F, 0xFE, 0xE0, 0x1F, + 0xFE, 0x70, 0x0C, 0x00, 0x38, 0x0E, 0x00, 0x0F, 0x0F, 0x00, 0x03, 0xFF, 0x00, 0x00, 0xFE, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x38, 0x63, 0x00, 0x03, 0x86, 0x30, 0x60, + 0x18, 0x63, 0x86, 0x01, 0xCE, 0x38, 0xE0, 0x1F, 0xFF, 0xFE, 0x01, 0xFF, 0xEF, 0xE0, 0x18, 0x00, + 0x0E, 0x03, 0x80, 0x00, 0x78, 0x78, 0x00, 0x03, 0xFF, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x0E, 0x00, 0x06, 0x18, 0xC6, 0x38, 0xC7, 0x38, 0xC7, 0x39, 0xC7, 0xFF, 0xFE, 0xFF, + 0xFC, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xC0, 0x63, 0x18, 0x1C, 0x63, 0x83, 0x8C, 0x70, 0x73, + 0x8E, 0x3F, 0xFF, 0xFF, 0xFF, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x09, 0x10, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x0E, 0x00, 0x38, 0x63, 0x00, 0x1C, 0x31, 0x98, 0x06, 0x18, 0xEC, 0x03, 0x9C, 0x6E, 0x01, + 0xFF, 0xF7, 0x00, 0xFF, 0xF3, 0x80, 0x60, 0x01, 0xC0, 0x70, 0x00, 0x78, 0x78, 0x00, 0x1F, 0xF8, + 0x00, 0x07, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x1F, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x38, 0x63, 0x00, 0x03, 0x86, 0x30, 0x60, 0x18, 0x63, 0x86, 0x01, 0xCE, 0x38, 0xE0, + 0x1F, 0xFF, 0xFE, 0x01, 0xFF, 0xEF, 0xE0, 0x18, 0x00, 0x0E, 0x03, 0x80, 0x00, 0x78, 0x78, 0x00, + 0x03, 0xFF, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x03, 0xF0, + 0x03, 0xF0, 0x01, 0x24, 0x00, 0x0E, 0x00, 0x06, 0x18, 0xC6, 0x38, 0xC7, 0x38, 0xC7, 0x39, 0xC7, + 0xFF, 0xFE, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x03, 0x00, 0x01, 0xF8, 0x00, + 0x3F, 0x00, 0x02, 0x40, 0x00, 0x00, 0x80, 0x00, 0x30, 0x18, 0xC6, 0x07, 0x18, 0xE0, 0xE3, 0x1C, + 0x1C, 0xE3, 0x8F, 0xFF, 0xFF, 0xFF, 0xFB, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x07, + 0xF8, 0x00, 0xE1, 0xE7, 0x00, 0x1C, 0x70, 0x76, 0x01, 0x9C, 0x0E, 0xC0, 0x3F, 0x03, 0xB8, 0x07, + 0xFF, 0xF7, 0x00, 0xFF, 0xF8, 0xE0, 0x18, 0xF8, 0x1C, 0x07, 0x00, 0x01, 0xE1, 0xE0, 0x00, 0x1F, + 0xF8, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x00, 0xFF, 0x00, 0x03, 0x87, + 0x9C, 0x00, 0x0E, 0x38, 0x38, 0x60, 0x19, 0xC0, 0xE1, 0x80, 0x7E, 0x07, 0x0E, 0x01, 0xFF, 0xFF, + 0xB8, 0x07, 0xFF, 0xFE, 0xE0, 0x18, 0xF8, 0x03, 0x80, 0xE0, 0x00, 0x07, 0x87, 0x80, 0x00, 0x0F, + 0xFC, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x0F, 0xE1, 0x87, 0x9C, 0x63, 0x83, + 0x39, 0xC0, 0xCE, 0xE0, 0x7F, 0xFF, 0xFB, 0xBF, 0xFC, 0x03, 0xF8, 0x00, 0x00, 0x0F, 0x00, 0x01, + 0xFC, 0x06, 0x1E, 0x70, 0x31, 0xC1, 0x83, 0x9C, 0x0C, 0x1D, 0xC0, 0xE3, 0xFF, 0xFF, 0xFD, 0xFF, + 0xFF, 0x03, 0xF8, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x7F, 0x80, 0x0E, 0x1E, 0x70, 0x01, 0xC7, 0x07, 0x60, + 0x19, 0xC0, 0xEC, 0x03, 0xF0, 0x3B, 0x80, 0x7F, 0xFF, 0x70, 0x0F, 0xFF, 0x8E, 0x01, 0x8F, 0x81, + 0xC0, 0x70, 0x00, 0x1E, 0x1E, 0x00, 0x01, 0xFF, 0x80, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, + 0x80, 0x00, 0x00, 0xFF, 0x00, 0x03, 0x87, 0x9C, 0x00, 0x0E, 0x38, 0x38, 0x60, 0x19, 0xC0, 0xE1, + 0x80, 0x7E, 0x07, 0x0E, 0x01, 0xFF, 0xFF, 0xB8, 0x07, 0xFF, 0xFE, 0xE0, 0x18, 0xF8, 0x03, 0x80, + 0xE0, 0x00, 0x07, 0x87, 0x80, 0x00, 0x0F, 0xFC, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x03, 0x80, 0x00, 0x40, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x0F, 0xE1, 0x87, 0x9C, 0x63, 0x83, + 0x39, 0xC0, 0xCE, 0xE0, 0x7F, 0xFF, 0xFB, 0xBF, 0xFC, 0x03, 0xF8, 0x00, 0x00, 0x0E, 0x00, 0x00, + 0x70, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x1F, 0xC0, 0x61, 0xE7, 0x03, 0x1C, + 0x18, 0x39, 0xC0, 0xC1, 0xDC, 0x0E, 0x3F, 0xFF, 0xFF, 0xDF, 0xFF, 0xF0, 0x3F, 0x80, 0x00, 0x0C, + 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x7C, 0x0D, + 0xFE, 0x0F, 0xC7, 0x0F, 0x87, 0x0F, 0x07, 0x0E, 0x0E, 0xFF, 0xFE, 0xFF, 0xF8, 0x1F, 0xC0, 0x0C, + 0x00, 0x01, 0x80, 0x00, 0x30, 0x00, 0x06, 0x00, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x03, 0x00, 0x00, + 0x63, 0xE0, 0x0D, 0xFE, 0x01, 0xF8, 0xE0, 0x3E, 0x1C, 0x07, 0x83, 0x80, 0xE0, 0xE1, 0xFF, 0xFF, + 0xBF, 0xFF, 0xF8, 0xFE, 0x00, 0x70, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, 0x07, + 0x00, 0x1C, 0x7C, 0x77, 0xF9, 0xFE, 0xE3, 0xE1, 0xCE, 0x06, 0x70, 0x3B, 0xFF, 0xCF, 0xFE, 0x00, + 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x71, + 0xF0, 0x77, 0xF8, 0x7F, 0xB8, 0x3E, 0x1C, 0x38, 0x18, 0x70, 0x38, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x18, 0x0C, 0x38, 0x0C, 0x10, 0x0C, 0x00, 0x0C, + 0x7C, 0x0D, 0xFE, 0x0F, 0xC7, 0x0F, 0x87, 0x0F, 0x07, 0x0E, 0x0E, 0xFF, 0xFE, 0xFF, 0xF8, 0x1F, + 0xC0, 0x0C, 0x00, 0x01, 0x80, 0x00, 0x30, 0x00, 0x06, 0x18, 0x00, 0xC7, 0x00, 0x18, 0x40, 0x03, + 0x00, 0x00, 0x63, 0xE0, 0x0D, 0xFE, 0x01, 0xF8, 0xE0, 0x3E, 0x1C, 0x07, 0x83, 0x80, 0xE0, 0xE1, + 0xFF, 0xFF, 0xBF, 0xFF, 0xF8, 0xFE, 0x00, 0x70, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x70, 0x71, 0xC1, + 0xC2, 0x07, 0x00, 0x1C, 0x7C, 0x77, 0xF9, 0xFE, 0xE3, 0xE1, 0xCE, 0x06, 0x70, 0x3B, 0xFF, 0xCF, + 0xFE, 0x00, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x71, 0xC0, 0x71, 0xC0, 0x70, 0x80, 0x70, + 0x00, 0x71, 0xF0, 0x77, 0xF8, 0x7F, 0xB8, 0x3E, 0x1C, 0x38, 0x18, 0x70, 0x38, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0x00, 0x1E, 0x01, 0xF0, 0x1F, 0x80, 0xC0, 0x0E, 0x00, 0x30, 0xC1, 0xFE, 0x07, 0xF0, + 0x3C, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x1C, 0x00, 0x70, 0x03, 0xFF, + 0x87, 0xFC, 0x06, 0x00, 0x0F, 0x01, 0xFC, 0x1F, 0xF0, 0xE3, 0x83, 0x98, 0x0F, 0xC0, 0x3C, 0x03, + 0xFE, 0x3B, 0xF3, 0x80, 0x38, 0x01, 0x80, 0x0C, 0x00, 0x70, 0x03, 0x80, 0x8F, 0xFE, 0x3F, 0xE0, + 0x30, 0x00, 0x00, 0x07, 0xC3, 0xF1, 0xC0, 0x70, 0x1C, 0x03, 0x80, 0xFF, 0xFF, 0xFF, 0x80, 0x00, + 0x0F, 0x03, 0xF8, 0x7F, 0xC7, 0x1C, 0x79, 0x81, 0xF8, 0x1F, 0x0F, 0xFF, 0xFB, 0xF0, 0x00, 0x0C, + 0x00, 0x60, 0x00, 0x00, 0x00, 0x01, 0xE0, 0x1F, 0x01, 0xF8, 0x0C, 0x00, 0xE0, 0x03, 0x0C, 0x1F, + 0xE0, 0x7F, 0x03, 0xC0, 0x38, 0x01, 0x80, 0x1C, 0x00, 0xC0, 0x06, 0x00, 0x30, 0x01, 0xC0, 0x07, + 0x00, 0x3F, 0xF8, 0x7F, 0xC0, 0x60, 0x06, 0x00, 0x30, 0x00, 0x80, 0x00, 0x00, 0xF0, 0x1F, 0xC1, + 0xFF, 0x0E, 0x38, 0x39, 0x80, 0xFC, 0x03, 0xC0, 0x3F, 0xE3, 0xBF, 0x38, 0x03, 0x80, 0x18, 0x00, + 0xC0, 0x07, 0x00, 0x38, 0x08, 0xFF, 0xE3, 0xFE, 0x03, 0x00, 0x0E, 0x03, 0x80, 0x40, 0x00, 0x00, + 0x07, 0xC3, 0xF1, 0xC0, 0x70, 0x1C, 0x03, 0x80, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x06, 0x00, 0xE0, + 0x04, 0x00, 0x00, 0x0F, 0x03, 0xF8, 0x7F, 0xC7, 0x1C, 0x79, 0x81, 0xF8, 0x1F, 0x0F, 0xFF, 0xFB, + 0xF0, 0x00, 0x00, 0x01, 0x80, 0x00, 0x38, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, + 0x3E, 0x00, 0x07, 0xE0, 0x00, 0x67, 0x60, 0x0E, 0x7E, 0x00, 0xE7, 0xE0, 0x07, 0xFE, 0x00, 0x3F, + 0xE0, 0x00, 0x77, 0xFF, 0xFE, 0x3F, 0xFF, 0xE0, 0xFF, 0xF0, 0x00, 0x00, 0x60, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x01, 0xF8, 0x60, 0x03, 0xF8, 0xE0, + 0x03, 0x9C, 0xE0, 0x03, 0x1C, 0xE0, 0x03, 0x98, 0xE0, 0x01, 0xF8, 0x7F, 0xFF, 0xFF, 0x3F, 0xFF, + 0xFF, 0x0F, 0xFC, 0x00, 0x1C, 0x0E, 0x02, 0x00, 0x00, 0xC1, 0xF8, 0xFC, 0xE6, 0x73, 0xB9, 0xCF, + 0xE3, 0xF0, 0x77, 0xF3, 0xF0, 0x00, 0x0C, 0x00, 0xC0, 0x04, 0x00, 0x00, 0x0E, 0x03, 0xF0, 0x3B, + 0x87, 0x38, 0x71, 0x83, 0x38, 0x3F, 0x0F, 0xFE, 0xFF, 0xF0, 0x00, 0x00, 0x6C, 0x00, 0x6C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x7E, 0x00, 0x6E, 0x00, 0xE6, 0x60, 0xC7, 0x60, 0xFF, 0xE0, + 0xFF, 0xE0, 0x3E, 0xE0, 0x06, 0xE0, 0x0E, 0x78, 0x3C, 0x3F, 0xF8, 0x1F, 0xE0, 0x00, 0x7C, 0x00, + 0x1F, 0x80, 0x02, 0x40, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x1F, 0x80, 0x0E, 0xE0, 0x03, 0x18, 0x70, + 0xC7, 0x18, 0x3F, 0xFE, 0x07, 0xFF, 0x80, 0x18, 0xE0, 0x0E, 0x1C, 0x07, 0x87, 0xCF, 0xC0, 0xFF, + 0xE0, 0x0F, 0xE0, 0x00, 0x36, 0x1F, 0x04, 0x00, 0x00, 0xC1, 0xF8, 0xFC, 0xE6, 0x73, 0xB9, 0xCF, + 0xE3, 0xF0, 0x77, 0xF3, 0xF0, 0x00, 0x1F, 0x03, 0xF0, 0x12, 0x00, 0x00, 0x0E, 0x03, 0xF0, 0x3B, + 0x87, 0x38, 0x71, 0x83, 0x38, 0x3F, 0x0F, 0xFE, 0xFF, 0xF0, 0x00, 0x00, 0x01, 0x80, 0x00, 0x60, + 0x00, 0x18, 0x00, 0x06, 0x00, 0x61, 0x80, 0x38, 0x60, 0x0C, 0x18, 0x03, 0x86, 0x60, 0x61, 0xB8, + 0x78, 0x6E, 0x18, 0x1B, 0x80, 0x06, 0xE0, 0x03, 0x9F, 0xFF, 0xE3, 0xFF, 0xF0, 0x3F, 0xE0, 0x00, + 0x01, 0x80, 0x00, 0x0C, 0x00, 0x00, 0x60, 0x00, 0x03, 0x00, 0x06, 0x18, 0x00, 0x70, 0xC0, 0x03, + 0x06, 0x00, 0x1C, 0x30, 0x60, 0x61, 0x87, 0x0F, 0x0C, 0x38, 0x60, 0x61, 0xC0, 0x03, 0x8E, 0x00, + 0x3C, 0x3F, 0xFF, 0xF8, 0xFF, 0xFD, 0xE1, 0xFF, 0x00, 0x01, 0x83, 0xC7, 0xCF, 0x87, 0x07, 0x01, + 0xC0, 0x70, 0x1C, 0x07, 0x01, 0x80, 0xE0, 0x77, 0xF3, 0xF0, 0x00, 0x01, 0x80, 0x78, 0x1F, 0x07, + 0xC0, 0x70, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x06, 0x00, 0x70, 0x07, 0x8F, 0xFF, + 0xFC, 0xF0, 0x00, 0x00, 0x18, 0x00, 0x60, 0x01, 0x80, 0x06, 0x00, 0x18, 0x00, 0x60, 0x01, 0x80, + 0x06, 0x00, 0x18, 0x00, 0x60, 0x01, 0x98, 0x07, 0x60, 0x1B, 0x80, 0x6E, 0x01, 0xB8, 0x06, 0xE0, + 0x39, 0xE3, 0xC3, 0xFE, 0x07, 0xF0, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x03, 0x00, 0x01, + 0x80, 0x00, 0xC0, 0x00, 0x60, 0x00, 0x30, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x0C, 0x03, 0x86, + 0x01, 0xC7, 0x00, 0xFB, 0x80, 0x7F, 0xC0, 0x30, 0xE0, 0x38, 0x3C, 0x78, 0x0F, 0xF8, 0x03, 0xF8, + 0x00, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8E, 0x73, 0x7B, 0x80, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x38, 0x38, 0x38, 0xFE, 0xEF, 0x00, 0x1F, 0x0F, 0xE7, 0x39, 0x86, + 0x01, 0xCF, 0xF7, 0xFB, 0x86, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x18, 0x06, 0x01, 0x80, 0x00, + 0x00, 0x07, 0x00, 0x7C, 0x0F, 0xE0, 0x73, 0x87, 0x8C, 0x7E, 0x73, 0xFF, 0xF9, 0xFF, 0xC3, 0x06, + 0x00, 0x30, 0x01, 0xC0, 0x0E, 0x00, 0x30, 0x01, 0x80, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x03, 0xE1, + 0xFC, 0xE7, 0x30, 0xDE, 0x3F, 0xFF, 0xBE, 0x00, 0x00, 0x07, 0x00, 0x3E, 0x01, 0xF8, 0x0E, 0x70, + 0x38, 0xC1, 0xE3, 0x8F, 0xFF, 0xBB, 0xFE, 0x03, 0x00, 0x03, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x00, 0xE0, 0x03, 0x98, 0x06, 0x60, 0x1B, 0x80, 0x7E, 0x01, 0xF8, 0x06, 0xE0, 0x39, + 0xE3, 0xC3, 0xFF, 0x07, 0xF0, 0x03, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xC0, 0x00, 0xE0, 0xC0, 0x30, 0x60, 0x1C, 0x70, 0x0F, 0xB8, 0x07, 0xFC, 0x03, 0x0E, 0x03, + 0x83, 0xC3, 0xC0, 0xFF, 0xC0, 0x3F, 0x80, 0x73, 0x88, 0x01, 0x18, 0xC7, 0x38, 0xCF, 0xEF, 0x00, + 0x30, 0x1C, 0x04, 0x00, 0x01, 0x80, 0xC0, 0xE0, 0x70, 0xFF, 0x77, 0x80, 0x00, 0x18, 0x1C, 0x3E, + 0x7E, 0x67, 0xE3, 0xE3, 0xE3, 0x7F, 0x7E, 0x03, 0x00, 0x70, 0x1F, 0x03, 0xF0, 0x73, 0x0E, 0x30, + 0xE3, 0x07, 0xF0, 0x7F, 0x80, 0xFE, 0x01, 0xE0, 0x00, 0x00, 0x01, 0xC0, 0x1E, 0x03, 0xF0, 0x37, + 0x87, 0x3C, 0x73, 0xE3, 0x76, 0x3E, 0x6F, 0xFE, 0xFF, 0xE0, 0x18, 0x07, 0x01, 0xF0, 0x1F, 0x83, + 0xB8, 0x33, 0x83, 0x38, 0x77, 0x0F, 0xFF, 0xFF, 0xF7, 0x1C, 0x30, 0xC3, 0xFC, 0x1F, 0x80, 0xF0, + 0x00, 0x0F, 0x0F, 0xCE, 0xE6, 0x33, 0x1D, 0xFE, 0x7E, 0x0F, 0x03, 0x8F, 0x8F, 0x87, 0x00, 0x00, + 0x03, 0xC0, 0xFC, 0x3B, 0x86, 0x30, 0xC7, 0x1F, 0xF9, 0xFF, 0x07, 0x00, 0xE0, 0xF8, 0x3E, 0x07, + 0x00, 0x00, 0x1E, 0x00, 0x7F, 0x00, 0x72, 0x00, 0xE0, 0x00, 0xC0, 0x60, 0xE0, 0x60, 0xF8, 0xE0, + 0x7E, 0xE0, 0x0E, 0xE0, 0x0E, 0xE0, 0x1E, 0x78, 0x7C, 0x3F, 0xF8, 0x1F, 0xE0, 0x60, 0x00, 0x30, + 0x1C, 0x38, 0x0F, 0xDC, 0x03, 0xFE, 0x00, 0x67, 0x00, 0x71, 0xE0, 0xF0, 0x7F, 0xF0, 0x1F, 0xE0, + 0x00, 0x00, 0x1E, 0x00, 0x7F, 0x00, 0x72, 0x00, 0xE0, 0x00, 0xC0, 0x60, 0xE0, 0x60, 0xF8, 0xE0, + 0x7E, 0xE0, 0x0E, 0xE0, 0x0E, 0xE0, 0x1E, 0x78, 0x7C, 0x3F, 0xF8, 0x1F, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x06, 0xC0, 0x06, 0xC0, 0x60, 0x00, 0x30, 0x1C, 0x38, 0x0F, 0xDC, 0x03, 0xFE, 0x00, 0x67, + 0x00, 0x71, 0xE0, 0xF0, 0x7F, 0xF0, 0x1F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xF8, 0x00, 0xFC, + 0x00, 0x08, 0x71, 0xC3, 0x0C, 0x30, 0xDF, 0x78, 0x00, 0x1B, 0x6C, 0x1C, 0x0C, 0x06, 0x07, 0x0F, + 0xFF, 0xFC, 0x00, 0x00, 0x7E, 0x3E, 0x00, 0x72, 0x07, 0xF9, 0x83, 0x8C, 0x00, 0x61, 0x83, 0x0F, + 0x18, 0x3C, 0xC0, 0xFE, 0x01, 0xF0, 0x07, 0x00, 0x3C, 0x03, 0xF0, 0x39, 0x83, 0x8C, 0x3F, 0xE1, + 0xFE, 0x38, 0x00, 0x7F, 0x30, 0x06, 0x30, 0x00, 0x30, 0x18, 0x30, 0x1C, 0x30, 0x1E, 0x30, 0x0F, + 0x30, 0x07, 0x30, 0x03, 0xB0, 0x01, 0xF0, 0x00, 0xF0, 0x00, 0x70, 0x00, 0xF8, 0x1F, 0xFF, 0x1F, + 0x9F, 0x00, 0x00, 0x70, 0x07, 0x00, 0x60, 0x07, 0x86, 0x70, 0x60, 0x06, 0x30, 0x63, 0xC6, 0x1E, + 0x60, 0xFE, 0x03, 0xE0, 0x1C, 0x01, 0xE0, 0x3F, 0x07, 0x30, 0xE3, 0x1F, 0xF1, 0xFE, 0x70, 0x03, + 0x80, 0x0C, 0x00, 0x3E, 0x30, 0xF0, 0xC0, 0x03, 0x06, 0x0C, 0x1C, 0x30, 0x78, 0xC0, 0xF3, 0x01, + 0xCC, 0x03, 0xB0, 0x07, 0xC0, 0x0F, 0x00, 0x1C, 0x00, 0xF8, 0x7F, 0xFD, 0xF9, 0xF0, 0x00, 0x00, + 0x00, 0xC0, 0x18, 0x03, 0x30, 0x67, 0x8C, 0x79, 0x87, 0xF0, 0x3E, 0x03, 0x80, 0x78, 0x1F, 0x87, + 0x31, 0xC6, 0x7F, 0xCF, 0xF0, 0x00, 0x38, 0x0F, 0x01, 0x80, 0x1E, 0x07, 0x80, 0x00, 0xC0, 0x03, + 0x00, 0x0C, 0x18, 0x30, 0x70, 0xC1, 0xE3, 0x03, 0xCC, 0x07, 0x30, 0x0E, 0xC0, 0x1F, 0x00, 0x3C, + 0x00, 0x70, 0x03, 0xE1, 0xFF, 0xF7, 0xE7, 0xC0, 0x00, 0x1C, 0x00, 0x70, 0x01, 0x80, 0x07, 0x80, + 0x1C, 0x00, 0x00, 0xC0, 0x18, 0x03, 0x30, 0x67, 0x8C, 0x79, 0x87, 0xF0, 0x3E, 0x03, 0x80, 0x78, + 0x1F, 0x87, 0x31, 0xC6, 0x7F, 0xCF, 0xF0, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x18, 0x30, 0x70, 0xC1, + 0xE3, 0x03, 0xCC, 0x07, 0x30, 0x0E, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0x70, 0x03, 0xE1, 0xFF, 0xF7, + 0xE7, 0xC0, 0x00, }; static const EpdGlyph inter_10_regularGlyphs[] = { @@ -2032,656 +2334,851 @@ static const EpdGlyph inter_10_regularGlyphs[] = { { 8, 5, 0, -3, 17, 5, 10338 }, // U+030B { 7, 4, 0, 1, 16, 4, 10343 }, // U+030C { 8, 4, 0, 1, 16, 4, 10347 }, // U+030F - { 4, 7, 0, 4, 11, 4, 10351 }, // U+0313 - { 4, 7, 0, 4, 11, 4, 10355 }, // U+0315 - { 4, 5, 0, -5, 15, 3, 10359 }, // U+031B - { 3, 3, 78, 1, -1, 2, 10362 }, // U+0323 - { 4, 5, 0, -9, -1, 3, 10364 }, // U+0326 - { 5, 6, 0, 1, 1, 4, 10367 }, // U+0327 - { 5, 6, 0, 0, 1, 4, 10371 }, // U+0328 - { 7, 4, 0, 1, -1, 4, 10375 }, // U+032C - { 9, 15, 0, -11, 13, 17, 10379 }, // U+0337 - { 12, 17, 0, -12, 16, 26, 10396 }, // U+0338 - { 7, 3, 0, -9, 16, 3, 10422 }, // U+0342 - { 4, 5, 0, 0, 13, 3, 10425 }, // U+0343 - { 8, 5, 0, -10, 17, 5, 10428 }, // U+0346 - { 7, 5, 0, -10, -3, 5, 10433 }, // U+0347 - { 5, 5, 0, -8, -1, 4, 10438 }, // U+0348 - { 5, 5, 0, -8, -1, 4, 10442 }, // U+0349 - { 8, 5, 0, -10, 17, 5, 10446 }, // U+034A - { 9, 8, 0, -11, 19, 9, 10451 }, // U+034B - { 8, 5, 0, -10, 17, 5, 10460 }, // U+034C - { 10, 5, 0, -11, -1, 7, 10465 }, // U+034D - { 6, 6, 0, -9, 0, 5, 10472 }, // U+034E - { 0, 0, 0, 0, 0, 0, 10477 }, // U+034F - { 9, 8, 0, -10, 21, 9, 10477 }, // U+0350 - { 3, 5, 0, -7, 16, 2, 10486 }, // U+0351 - { 8, 7, 0, -10, 18, 7, 10488 }, // U+0352 - { 6, 6, 0, -8, 0, 5, 10495 }, // U+0353 - { 6, 6, 0, -8, 0, 5, 10500 }, // U+0354 - { 6, 6, 0, -8, 0, 5, 10505 }, // U+0355 - { 12, 6, 0, -10, 0, 9, 10510 }, // U+0356 - { 4, 7, 0, 0, 18, 4, 10519 }, // U+0357 - { 3, 3, 0, 1, 16, 2, 10523 }, // U+0358 - { 6, 6, 0, -9, 0, 5, 10525 }, // U+0359 - { 9, 5, 0, -10, 0, 6, 10530 }, // U+035A - { 5, 5, 0, -8, 17, 4, 10536 }, // U+035B - { 16, 4, 0, -8, 0, 8, 10540 }, // U+035C - { 16, 4, 0, -8, 16, 8, 10548 }, // U+035D - { 14, 3, 0, -13, 16, 6, 10556 }, // U+035E - { 14, 3, 0, -13, -1, 6, 10562 }, // U+035F - { 20, 4, 0, -10, 16, 10, 10568 }, // U+0360 - { 16, 4, 0, -8, 16, 8, 10578 }, // U+0361 - { 17, 5, 0, -8, -1, 11, 10586 }, // U+0362 - { 6, 6, 0, -9, 18, 5, 10597 }, // U+0363 - { 6, 6, 0, -9, 18, 5, 10602 }, // U+0364 - { 2, 8, 0, -7, 20, 2, 10607 }, // U+0365 - { 6, 6, 0, -9, 18, 5, 10609 }, // U+0366 - { 6, 6, 0, -9, 18, 5, 10614 }, // U+0367 - { 6, 6, 0, -9, 18, 5, 10619 }, // U+0368 - { 6, 9, 0, -9, 21, 7, 10624 }, // U+0369 - { 6, 9, 0, -9, 21, 7, 10631 }, // U+036A - { 9, 6, 0, -10, 18, 7, 10638 }, // U+036B - { 4, 6, 0, -8, 18, 3, 10645 }, // U+036C - { 4, 8, 0, 0, 20, 4, 10648 }, // U+036D - { 6, 6, 0, -9, 18, 5, 10652 }, // U+036E - { 6, 6, 0, -8, 18, 5, 10657 }, // U+036F - { 11, 20, 199, 1, 20, 28, 10662 }, // U+0400 - { 11, 20, 199, 1, 20, 28, 10690 }, // U+0401 - { 17, 17, 278, 0, 16, 37, 10718 }, // U+0402 - { 10, 20, 190, 1, 20, 25, 10755 }, // U+0403 - { 14, 17, 243, 1, 16, 30, 10780 }, // U+0404 - { 12, 17, 212, 1, 16, 26, 10810 }, // U+0405 - { 3, 16, 87, 1, 16, 6, 10836 }, // U+0406 - { 8, 20, 87, -1, 20, 20, 10842 }, // U+0407 - { 10, 17, 188, 0, 16, 22, 10862 }, // U+0408 - { 23, 16, 368, 0, 16, 46, 10884 }, // U+0409 - { 22, 16, 373, 1, 16, 44, 10930 }, // U+040A - { 17, 16, 292, 0, 16, 34, 10974 }, // U+040B - { 13, 20, 221, 1, 20, 33, 11008 }, // U+040C - { 13, 20, 248, 1, 20, 33, 11041 }, // U+040D - { 13, 20, 207, 0, 20, 33, 11074 }, // U+040E - { 13, 20, 238, 1, 16, 33, 11107 }, // U+040F - { 14, 16, 227, 0, 16, 28, 11140 }, // U+0410 - { 12, 16, 213, 1, 16, 24, 11168 }, // U+0411 - { 12, 16, 217, 1, 16, 24, 11192 }, // U+0412 - { 10, 16, 190, 1, 16, 20, 11216 }, // U+0413 - { 16, 20, 269, 0, 16, 40, 11236 }, // U+0414 - { 11, 16, 199, 1, 16, 22, 11276 }, // U+0415 - { 20, 16, 317, 0, 16, 40, 11298 }, // U+0416 - { 11, 17, 204, 1, 16, 24, 11338 }, // U+0417 - { 13, 16, 248, 1, 16, 26, 11362 }, // U+0418 - { 13, 20, 248, 1, 20, 33, 11388 }, // U+0419 - { 13, 16, 221, 1, 16, 26, 11421 }, // U+041A - { 14, 16, 246, 0, 16, 28, 11447 }, // U+041B - { 16, 16, 298, 1, 16, 32, 11475 }, // U+041C - { 13, 16, 245, 1, 16, 26, 11507 }, // U+041D - { 14, 17, 254, 1, 16, 30, 11533 }, // U+041E - { 13, 16, 245, 1, 16, 26, 11563 }, // U+041F - { 12, 16, 211, 1, 16, 24, 11589 }, // U+0420 - { 14, 17, 243, 1, 16, 30, 11613 }, // U+0421 - { 13, 16, 213, 0, 16, 26, 11643 }, // U+0422 - { 13, 16, 207, 0, 16, 26, 11669 }, // U+0423 - { 15, 17, 275, 1, 16, 32, 11695 }, // U+0424 - { 14, 16, 225, 0, 16, 28, 11727 }, // U+0425 - { 14, 20, 238, 1, 16, 35, 11755 }, // U+0426 - { 12, 16, 231, 1, 16, 24, 11790 }, // U+0427 - { 18, 16, 316, 1, 16, 36, 11814 }, // U+0428 - { 19, 20, 321, 1, 16, 48, 11850 }, // U+0429 - { 16, 16, 269, 0, 16, 32, 11898 }, // U+042A - { 16, 16, 289, 1, 16, 32, 11930 }, // U+042B - { 12, 16, 214, 1, 16, 24, 11962 }, // U+042C - { 13, 17, 243, 1, 16, 28, 11986 }, // U+042D - { 19, 17, 337, 1, 16, 41, 12014 }, // U+042E - { 12, 16, 214, 0, 16, 24, 12055 }, // U+042F - { 10, 13, 184, 0, 12, 17, 12079 }, // U+0430 - { 12, 17, 194, 0, 16, 26, 12096 }, // U+0431 - { 10, 12, 185, 1, 12, 15, 12122 }, // U+0432 - { 8, 12, 144, 1, 12, 12, 12137 }, // U+0433 - { 13, 16, 203, 0, 12, 26, 12149 }, // U+0434 - { 11, 13, 191, 0, 12, 18, 12175 }, // U+0435 - { 17, 12, 267, 0, 12, 26, 12193 }, // U+0436 - { 10, 13, 163, 0, 12, 17, 12219 }, // U+0437 - { 10, 12, 194, 1, 12, 15, 12236 }, // U+0438 - { 10, 16, 194, 1, 16, 20, 12251 }, // U+0439 - { 10, 12, 178, 1, 12, 15, 12271 }, // U+043A - { 11, 12, 190, 0, 12, 17, 12286 }, // U+043B - { 14, 12, 254, 1, 12, 21, 12303 }, // U+043C - { 10, 12, 193, 1, 12, 15, 12324 }, // U+043D - { 12, 13, 196, 0, 12, 20, 12339 }, // U+043E - { 10, 12, 193, 1, 12, 15, 12359 }, // U+043F - { 11, 17, 201, 1, 12, 24, 12374 }, // U+0440 - { 11, 13, 187, 0, 12, 18, 12398 }, // U+0441 - { 10, 12, 155, 0, 12, 15, 12416 }, // U+0442 - { 11, 17, 184, 0, 12, 24, 12431 }, // U+0443 - { 14, 21, 226, 0, 16, 37, 12455 }, // U+0444 - { 11, 12, 179, 0, 12, 17, 12492 }, // U+0445 - { 11, 16, 197, 1, 12, 22, 12509 }, // U+0446 - { 10, 12, 190, 1, 12, 15, 12531 }, // U+0447 - { 15, 12, 271, 1, 12, 23, 12546 }, // U+0448 - { 16, 16, 276, 1, 12, 32, 12569 }, // U+0449 - { 13, 12, 213, 0, 12, 20, 12601 }, // U+044A - { 13, 12, 241, 1, 12, 20, 12621 }, // U+044B - { 10, 12, 184, 1, 12, 15, 12641 }, // U+044C - { 11, 13, 187, 0, 12, 18, 12656 }, // U+044D - { 16, 13, 276, 1, 12, 26, 12674 }, // U+044E - { 10, 12, 182, 0, 12, 15, 12700 }, // U+044F - { 11, 17, 191, 0, 16, 24, 12715 }, // U+0450 - { 11, 17, 191, 0, 16, 24, 12739 }, // U+0451 - { 11, 21, 194, 0, 16, 29, 12763 }, // U+0452 - { 8, 16, 144, 1, 16, 16, 12792 }, // U+0453 - { 11, 13, 187, 0, 12, 18, 12808 }, // U+0454 - { 9, 13, 172, 1, 12, 15, 12826 }, // U+0455 - { 3, 16, 78, 1, 16, 6, 12841 }, // U+0456 - { 7, 16, 78, -1, 16, 14, 12847 }, // U+0457 - { 5, 21, 78, -1, 16, 14, 12861 }, // U+0458 - { 19, 12, 306, 0, 12, 29, 12875 }, // U+0459 - { 17, 12, 297, 1, 12, 26, 12904 }, // U+045A - { 11, 16, 194, 0, 16, 22, 12930 }, // U+045B - { 10, 16, 178, 1, 16, 20, 12952 }, // U+045C - { 10, 16, 194, 1, 16, 20, 12972 }, // U+045D - { 11, 21, 184, 0, 16, 29, 12992 }, // U+045E - { 10, 16, 193, 1, 12, 20, 13021 }, // U+045F - { 16, 17, 296, 1, 16, 34, 13041 }, // U+0460 - { 15, 13, 266, 1, 12, 25, 13075 }, // U+0461 - { 14, 16, 228, 0, 16, 28, 13100 }, // U+0462 - { 12, 16, 203, 0, 16, 24, 13128 }, // U+0463 - { 18, 17, 316, 1, 16, 39, 13152 }, // U+0464 - { 15, 13, 258, 1, 12, 25, 13191 }, // U+0465 - { 14, 16, 227, 0, 16, 28, 13216 }, // U+0466 - { 12, 12, 188, 0, 12, 18, 13244 }, // U+0467 - { 18, 16, 310, 1, 16, 36, 13262 }, // U+0468 - { 15, 12, 257, 1, 12, 23, 13298 }, // U+0469 - { 16, 16, 282, 1, 16, 32, 13321 }, // U+046A - { 14, 12, 231, 0, 12, 21, 13353 }, // U+046B - { 22, 16, 379, 1, 16, 44, 13374 }, // U+046C - { 18, 12, 317, 1, 12, 27, 13418 }, // U+046D - { 9, 25, 179, 1, 20, 29, 13445 }, // U+046E - { 10, 21, 171, 0, 16, 27, 13474 }, // U+046F - { 16, 16, 291, 1, 16, 32, 13501 }, // U+0470 - { 14, 19, 256, 1, 14, 34, 13533 }, // U+0471 - { 14, 17, 254, 1, 16, 30, 13567 }, // U+0472 - { 12, 13, 196, 0, 12, 20, 13597 }, // U+0473 - { 15, 16, 232, 0, 16, 30, 13617 }, // U+0474 - { 12, 12, 195, 0, 12, 18, 13647 }, // U+0475 - { 15, 20, 232, 0, 20, 38, 13665 }, // U+0476 - { 12, 16, 195, 0, 16, 24, 13703 }, // U+0477 - { 26, 21, 437, 1, 16, 69, 13727 }, // U+0478 - { 23, 17, 373, 0, 12, 49, 13796 }, // U+0479 - { 13, 16, 236, 1, 16, 26, 13845 }, // U+0480 - { 11, 16, 185, 0, 12, 22, 13871 }, // U+0481 - { 11, 14, 208, 1, 14, 20, 13893 }, // U+0482 - { 8, 4, 0, -10, 16, 4, 13913 }, // U+0483 - { 9, 3, 0, -10, 16, 4, 13917 }, // U+0484 - { 3, 4, 0, -7, 17, 2, 13921 }, // U+0485 - { 4, 4, 0, -7, 17, 2, 13923 }, // U+0486 - { 9, 3, 0, -10, 16, 4, 13925 }, // U+0487 - { 21, 19, 0, -16, 15, 50, 13929 }, // U+0488 - { 20, 21, 0, -16, 16, 53, 13979 }, // U+0489 - { 15, 24, 248, 1, 20, 45, 14032 }, // U+048A - { 12, 20, 194, 1, 16, 30, 14077 }, // U+048B - { 14, 16, 228, 0, 16, 28, 14107 }, // U+048C - { 12, 14, 203, 0, 14, 21, 14135 }, // U+048D - { 12, 16, 209, 1, 16, 24, 14156 }, // U+048E - { 11, 17, 201, 1, 12, 24, 14180 }, // U+048F - { 10, 19, 190, 1, 19, 24, 14204 }, // U+0490 - { 8, 15, 144, 1, 15, 15, 14228 }, // U+0491 - { 12, 16, 190, -1, 16, 24, 14243 }, // U+0492 - { 9, 12, 144, 0, 12, 14, 14267 }, // U+0493 - { 12, 17, 215, 1, 16, 26, 14281 }, // U+0494 - { 10, 16, 178, 1, 12, 20, 14307 }, // U+0495 - { 21, 20, 323, 0, 16, 53, 14327 }, // U+0496 - { 17, 16, 274, 0, 12, 34, 14380 }, // U+0497 - { 11, 21, 204, 1, 16, 29, 14414 }, // U+0498 - { 10, 17, 163, 0, 12, 22, 14443 }, // U+0499 - { 14, 20, 233, 1, 16, 35, 14465 }, // U+049A - { 11, 16, 185, 1, 12, 22, 14500 }, // U+049B - { 13, 16, 219, 1, 16, 26, 14522 }, // U+049C - { 11, 12, 192, 1, 12, 17, 14548 }, // U+049D - { 17, 16, 276, 0, 16, 34, 14565 }, // U+04A0 - { 13, 12, 207, 0, 12, 20, 14599 }, // U+04A1 - { 15, 20, 250, 1, 16, 38, 14619 }, // U+04A2 - { 11, 16, 198, 1, 12, 22, 14657 }, // U+04A3 - { 19, 16, 325, 1, 16, 38, 14679 }, // U+04A4 - { 13, 12, 225, 1, 12, 20, 14717 }, // U+04A5 - { 21, 17, 367, 1, 16, 45, 14737 }, // U+04A6 - { 18, 15, 305, 1, 12, 34, 14782 }, // U+04A7 - { 19, 17, 326, 1, 16, 41, 14816 }, // U+04A8 - { 16, 13, 257, 0, 12, 26, 14857 }, // U+04A9 - { 14, 21, 243, 1, 16, 37, 14883 }, // U+04AA - { 11, 17, 187, 0, 12, 24, 14920 }, // U+04AB - { 13, 20, 213, 0, 16, 33, 14944 }, // U+04AC - { 10, 16, 155, 0, 12, 20, 14977 }, // U+04AD - { 14, 16, 224, 0, 16, 28, 14997 }, // U+04AE - { 11, 17, 184, 0, 12, 24, 15025 }, // U+04AF - { 14, 16, 224, 0, 16, 28, 15049 }, // U+04B0 - { 11, 17, 184, 0, 12, 24, 15077 }, // U+04B1 - { 15, 20, 231, 0, 16, 38, 15101 }, // U+04B2 - { 12, 16, 186, 0, 12, 24, 15139 }, // U+04B3 - { 19, 20, 303, 0, 16, 48, 15163 }, // U+04B4 - { 14, 16, 218, 0, 12, 28, 15211 }, // U+04B5 - { 14, 20, 235, 1, 16, 35, 15239 }, // U+04B6 - { 11, 16, 194, 1, 12, 22, 15274 }, // U+04B7 - { 12, 16, 231, 1, 16, 24, 15296 }, // U+04B8 - { 10, 12, 190, 1, 12, 15, 15320 }, // U+04B9 - { 12, 16, 231, 1, 16, 24, 15335 }, // U+04BA - { 10, 16, 194, 1, 16, 20, 15359 }, // U+04BB - { 17, 17, 279, 0, 16, 37, 15379 }, // U+04BC - { 14, 13, 235, 0, 12, 23, 15416 }, // U+04BD - { 17, 20, 279, 0, 16, 43, 15439 }, // U+04BE - { 14, 17, 235, 0, 12, 30, 15482 }, // U+04BF - { 3, 16, 87, 1, 16, 6, 15512 }, // U+04C0 - { 20, 20, 317, 0, 20, 50, 15518 }, // U+04C1 - { 17, 16, 267, 0, 16, 34, 15568 }, // U+04C2 - { 12, 20, 225, 1, 16, 30, 15602 }, // U+04C3 - { 11, 15, 193, 1, 12, 21, 15632 }, // U+04C4 - { 16, 20, 246, 0, 16, 40, 15653 }, // U+04C5 - { 13, 16, 190, 0, 12, 26, 15693 }, // U+04C6 - { 13, 21, 245, 1, 16, 35, 15719 }, // U+04C7 - { 10, 17, 193, 1, 12, 22, 15754 }, // U+04C8 - { 15, 20, 245, 1, 16, 38, 15776 }, // U+04C9 - { 12, 16, 193, 1, 12, 24, 15814 }, // U+04CA - { 14, 20, 235, 1, 16, 35, 15838 }, // U+04CB - { 11, 16, 194, 1, 12, 22, 15873 }, // U+04CC - { 18, 20, 298, 1, 16, 45, 15895 }, // U+04CD - { 16, 16, 254, 1, 12, 32, 15940 }, // U+04CE - { 3, 16, 78, 1, 16, 6, 15972 }, // U+04CF - { 14, 20, 227, 0, 20, 35, 15978 }, // U+04D0 - { 10, 17, 184, 0, 16, 22, 16013 }, // U+04D1 - { 14, 20, 227, 0, 20, 35, 16035 }, // U+04D2 - { 10, 17, 184, 0, 16, 22, 16070 }, // U+04D3 - { 20, 16, 329, 0, 16, 40, 16092 }, // U+04D4 - { 18, 13, 302, 0, 12, 30, 16132 }, // U+04D5 - { 11, 20, 199, 1, 20, 28, 16162 }, // U+04D6 - { 11, 17, 191, 0, 16, 24, 16190 }, // U+04D7 - { 14, 17, 247, 1, 16, 30, 16214 }, // U+04D8 - { 11, 13, 191, 0, 12, 18, 16244 }, // U+04D9 - { 14, 21, 247, 1, 20, 37, 16262 }, // U+04DA - { 11, 17, 191, 0, 16, 24, 16299 }, // U+04DB - { 20, 20, 317, 0, 20, 50, 16323 }, // U+04DC - { 17, 16, 267, 0, 16, 34, 16373 }, // U+04DD - { 11, 21, 204, 1, 20, 29, 16407 }, // U+04DE - { 10, 17, 163, 0, 16, 22, 16436 }, // U+04DF - { 11, 17, 205, 1, 16, 24, 16458 }, // U+04E0 - { 11, 17, 205, 1, 12, 24, 16482 }, // U+04E1 - { 13, 19, 248, 1, 19, 31, 16506 }, // U+04E2 - { 10, 15, 194, 1, 15, 19, 16537 }, // U+04E3 - { 13, 20, 248, 1, 20, 33, 16556 }, // U+04E4 - { 10, 16, 194, 1, 16, 20, 16589 }, // U+04E5 - { 14, 21, 254, 1, 20, 37, 16609 }, // U+04E6 - { 12, 17, 196, 0, 16, 26, 16646 }, // U+04E7 - { 14, 17, 254, 1, 16, 30, 16672 }, // U+04E8 - { 12, 13, 196, 0, 12, 20, 16702 }, // U+04E9 - { 14, 21, 254, 1, 20, 37, 16722 }, // U+04EA - { 12, 17, 196, 0, 16, 26, 16759 }, // U+04EB - { 13, 21, 243, 1, 20, 35, 16785 }, // U+04EC - { 11, 17, 187, 0, 16, 24, 16820 }, // U+04ED - { 13, 19, 207, 0, 19, 31, 16844 }, // U+04EE - { 11, 20, 184, 0, 15, 28, 16875 }, // U+04EF - { 13, 20, 207, 0, 20, 33, 16903 }, // U+04F0 - { 11, 21, 184, 0, 16, 29, 16936 }, // U+04F1 - { 13, 20, 207, 0, 20, 33, 16965 }, // U+04F2 - { 11, 21, 184, 0, 16, 29, 16998 }, // U+04F3 - { 12, 20, 231, 1, 20, 30, 17027 }, // U+04F4 - { 10, 16, 190, 1, 16, 20, 17057 }, // U+04F5 - { 10, 20, 190, 1, 16, 25, 17077 }, // U+04F6 - { 8, 16, 144, 1, 12, 16, 17102 }, // U+04F7 - { 16, 20, 289, 1, 20, 40, 17118 }, // U+04F8 - { 13, 16, 241, 1, 16, 26, 17158 }, // U+04F9 - { 12, 21, 196, 0, 16, 32, 17184 }, // U+04FA - { 9, 17, 144, 0, 12, 20, 17216 }, // U+04FB - { 15, 21, 233, 0, 16, 40, 17236 }, // U+04FC - { 11, 17, 171, 0, 12, 24, 17276 }, // U+04FD - { 14, 16, 225, 0, 16, 28, 17300 }, // U+04FE - { 11, 12, 179, 0, 12, 17, 17328 }, // U+04FF - { 11, 13, 209, 1, 13, 18, 17345 }, // U+05D0 - { 11, 13, 184, 0, 13, 18, 17363 }, // U+05D1 - { 8, 13, 140, 0, 13, 13, 17381 }, // U+05D2 - { 11, 13, 176, 0, 13, 18, 17394 }, // U+05D3 - { 11, 13, 212, 1, 13, 18, 17412 }, // U+05D4 - { 3, 13, 87, 1, 13, 5, 17430 }, // U+05D5 - { 8, 13, 126, 0, 13, 13, 17435 }, // U+05D6 - { 11, 13, 216, 1, 13, 18, 17448 }, // U+05D7 - { 12, 13, 215, 1, 13, 20, 17466 }, // U+05D8 - { 3, 7, 80, 1, 13, 3, 17486 }, // U+05D9 - { 10, 17, 172, 0, 13, 22, 17489 }, // U+05DA - { 11, 13, 177, 0, 13, 18, 17511 }, // U+05DB - { 10, 16, 158, 0, 16, 20, 17529 }, // U+05DC - { 11, 13, 216, 1, 13, 18, 17549 }, // U+05DD - { 11, 13, 218, 1, 13, 18, 17567 }, // U+05DE - { 3, 17, 87, 1, 13, 7, 17585 }, // U+05DF - { 6, 13, 116, 0, 13, 10, 17592 }, // U+05E0 - { 12, 13, 214, 1, 13, 20, 17602 }, // U+05E1 - { 11, 14, 198, 0, 13, 20, 17622 }, // U+05E2 - { 11, 17, 210, 1, 13, 24, 17642 }, // U+05E3 - { 11, 13, 208, 1, 13, 18, 17666 }, // U+05E4 - { 10, 17, 172, 0, 13, 22, 17684 }, // U+05E5 - { 10, 13, 181, 0, 13, 17, 17706 }, // U+05E6 - { 12, 17, 203, 1, 13, 26, 17723 }, // U+05E7 - { 10, 13, 175, 0, 13, 17, 17749 }, // U+05E8 - { 14, 13, 251, 1, 13, 23, 17766 }, // U+05E9 - { 12, 13, 222, 1, 13, 20, 17789 }, // U+05EA - { 14, 20, 227, 0, 16, 35, 17809 }, // U+1EA0 - { 10, 16, 184, 0, 12, 20, 17844 }, // U+1EA1 - { 14, 20, 227, 0, 20, 35, 17864 }, // U+1EA2 - { 10, 17, 184, 0, 16, 22, 17899 }, // U+1EA3 - { 14, 23, 227, 0, 23, 41, 17921 }, // U+1EA4 - { 11, 20, 184, 0, 19, 28, 17962 }, // U+1EA5 - { 14, 23, 227, 0, 23, 41, 17990 }, // U+1EA6 - { 10, 20, 184, 0, 19, 25, 18031 }, // U+1EA7 - { 14, 23, 227, 0, 23, 41, 18056 }, // U+1EA8 - { 10, 20, 184, 0, 19, 25, 18097 }, // U+1EA9 - { 14, 23, 227, 0, 23, 41, 18122 }, // U+1EAA - { 10, 20, 184, 0, 19, 25, 18163 }, // U+1EAB - { 14, 24, 227, 0, 20, 42, 18188 }, // U+1EAC - { 10, 20, 184, 0, 16, 25, 18230 }, // U+1EAD - { 14, 23, 227, 0, 23, 41, 18255 }, // U+1EAE - { 10, 20, 184, 0, 19, 25, 18296 }, // U+1EAF - { 14, 23, 227, 0, 23, 41, 18321 }, // U+1EB0 - { 10, 20, 184, 0, 19, 25, 18362 }, // U+1EB1 - { 14, 23, 227, 0, 23, 41, 18387 }, // U+1EB2 - { 10, 20, 184, 0, 19, 25, 18428 }, // U+1EB3 - { 14, 23, 227, 0, 23, 41, 18453 }, // U+1EB4 - { 10, 20, 184, 0, 19, 25, 18494 }, // U+1EB5 - { 14, 24, 227, 0, 20, 42, 18519 }, // U+1EB6 - { 10, 20, 184, 0, 16, 25, 18561 }, // U+1EB7 - { 11, 20, 199, 1, 16, 28, 18586 }, // U+1EB8 - { 11, 16, 191, 0, 12, 22, 18614 }, // U+1EB9 - { 11, 20, 199, 1, 20, 28, 18636 }, // U+1EBA - { 11, 17, 191, 0, 16, 24, 18664 }, // U+1EBB - { 11, 20, 199, 1, 20, 28, 18688 }, // U+1EBC - { 11, 17, 191, 0, 16, 24, 18716 }, // U+1EBD - { 11, 23, 199, 1, 23, 32, 18740 }, // U+1EBE - { 11, 20, 191, 0, 19, 28, 18772 }, // U+1EBF - { 11, 23, 199, 1, 23, 32, 18800 }, // U+1EC0 - { 11, 20, 191, 0, 19, 28, 18832 }, // U+1EC1 - { 11, 23, 199, 1, 23, 32, 18860 }, // U+1EC2 - { 11, 20, 191, 0, 19, 28, 18892 }, // U+1EC3 - { 11, 23, 199, 1, 23, 32, 18920 }, // U+1EC4 - { 11, 20, 191, 0, 19, 28, 18952 }, // U+1EC5 - { 11, 24, 199, 1, 20, 33, 18980 }, // U+1EC6 - { 11, 20, 191, 0, 16, 28, 19013 }, // U+1EC7 - { 4, 20, 87, 1, 20, 10, 19041 }, // U+1EC8 - { 4, 16, 78, 0, 16, 8, 19051 }, // U+1EC9 - { 3, 20, 87, 1, 16, 8, 19059 }, // U+1ECA - { 3, 20, 78, 1, 16, 8, 19067 }, // U+1ECB - { 14, 20, 254, 1, 16, 35, 19075 }, // U+1ECC - { 12, 16, 196, 0, 12, 24, 19110 }, // U+1ECD - { 14, 21, 254, 1, 20, 37, 19134 }, // U+1ECE - { 12, 17, 196, 0, 16, 26, 19171 }, // U+1ECF - { 14, 24, 254, 1, 23, 42, 19197 }, // U+1ED0 - { 12, 20, 196, 0, 19, 30, 19239 }, // U+1ED1 - { 14, 24, 254, 1, 23, 42, 19269 }, // U+1ED2 - { 12, 20, 196, 0, 19, 30, 19311 }, // U+1ED3 - { 14, 24, 254, 1, 23, 42, 19341 }, // U+1ED4 - { 12, 20, 196, 0, 19, 30, 19383 }, // U+1ED5 - { 14, 24, 254, 1, 23, 42, 19413 }, // U+1ED6 - { 12, 20, 196, 0, 19, 30, 19455 }, // U+1ED7 - { 14, 24, 254, 1, 20, 42, 19485 }, // U+1ED8 - { 12, 20, 196, 0, 16, 30, 19527 }, // U+1ED9 - { 14, 21, 253, 1, 20, 37, 19557 }, // U+1EDA - { 12, 17, 195, 0, 16, 26, 19594 }, // U+1EDB - { 14, 21, 253, 1, 20, 37, 19620 }, // U+1EDC - { 12, 17, 195, 0, 16, 26, 19657 }, // U+1EDD - { 14, 21, 253, 1, 20, 37, 19683 }, // U+1EDE - { 12, 17, 195, 0, 16, 26, 19720 }, // U+1EDF - { 14, 21, 253, 1, 20, 37, 19746 }, // U+1EE0 - { 12, 17, 195, 0, 16, 26, 19783 }, // U+1EE1 - { 14, 22, 253, 1, 18, 39, 19809 }, // U+1EE2 - { 12, 18, 195, 0, 14, 27, 19848 }, // U+1EE3 - { 13, 20, 245, 1, 16, 33, 19875 }, // U+1EE4 - { 10, 16, 194, 1, 12, 20, 19908 }, // U+1EE5 - { 13, 21, 245, 1, 20, 35, 19928 }, // U+1EE6 - { 10, 17, 194, 1, 16, 22, 19963 }, // U+1EE7 - { 15, 21, 244, 1, 20, 40, 19985 }, // U+1EE8 - { 12, 17, 205, 1, 16, 26, 20025 }, // U+1EE9 - { 15, 21, 244, 1, 20, 40, 20051 }, // U+1EEA - { 12, 17, 205, 1, 16, 26, 20091 }, // U+1EEB - { 15, 21, 244, 1, 20, 40, 20117 }, // U+1EEC - { 12, 17, 205, 1, 16, 26, 20157 }, // U+1EED - { 15, 21, 244, 1, 20, 40, 20183 }, // U+1EEE - { 12, 17, 205, 1, 16, 26, 20223 }, // U+1EEF - { 15, 22, 244, 1, 18, 42, 20249 }, // U+1EF0 - { 12, 18, 205, 1, 14, 27, 20291 }, // U+1EF1 - { 14, 20, 224, 0, 20, 35, 20318 }, // U+1EF2 - { 11, 21, 184, 0, 16, 29, 20353 }, // U+1EF3 - { 14, 20, 224, 0, 16, 35, 20382 }, // U+1EF4 - { 11, 17, 184, 0, 12, 24, 20417 }, // U+1EF5 - { 14, 20, 224, 0, 20, 35, 20441 }, // U+1EF6 - { 11, 21, 184, 0, 16, 29, 20476 }, // U+1EF7 - { 14, 20, 224, 0, 20, 35, 20505 }, // U+1EF8 - { 11, 21, 184, 0, 16, 29, 20540 }, // U+1EF9 - { 0, 0, 182, 0, 0, 0, 20569 }, // U+2000 - { 0, 0, 364, 0, 0, 0, 20569 }, // U+2001 - { 0, 0, 167, 0, 0, 0, 20569 }, // U+2002 - { 0, 0, 333, 0, 0, 0, 20569 }, // U+2003 - { 0, 0, 111, 0, 0, 0, 20569 }, // U+2004 - { 0, 0, 83, 0, 0, 0, 20569 }, // U+2005 - { 0, 0, 55, 0, 0, 0, 20569 }, // U+2006 - { 0, 0, 216, 0, 0, 0, 20569 }, // U+2007 - { 0, 0, 91, 0, 0, 0, 20569 }, // U+2008 - { 0, 0, 58, 0, 0, 0, 20569 }, // U+2009 - { 0, 0, 28, 0, 0, 0, 20569 }, // U+200A - { 0, 0, 0, 0, 0, 0, 20569 }, // U+200B - { 0, 0, 0, 0, 0, 0, 20569 }, // U+200C - { 0, 0, 0, 0, 0, 0, 20569 }, // U+200D - { 0, 0, 0, 0, 0, 0, 20569 }, // U+200E - { 0, 0, 0, 0, 0, 0, 20569 }, // U+200F - { 7, 3, 117, 0, 8, 3, 20569 }, // U+2010 - { 7, 3, 117, 0, 8, 3, 20572 }, // U+2011 - { 13, 3, 216, 0, 8, 5, 20575 }, // U+2012 - { 11, 3, 167, 0, 8, 5, 20580 }, // U+2013 - { 21, 3, 333, 0, 8, 8, 20585 }, // U+2014 - { 21, 3, 333, 0, 8, 8, 20593 }, // U+2015 - { 6, 26, 157, 2, 21, 20, 20601 }, // U+2016 - { 11, 5, 149, -1, 0, 7, 20621 }, // U+2017 - { 4, 7, 82, 1, 16, 4, 20628 }, // U+2018 - { 4, 7, 82, 1, 16, 4, 20632 }, // U+2019 - { 3, 7, 76, 1, 3, 3, 20636 }, // U+201A - { 4, 7, 82, 0, 16, 4, 20639 }, // U+201B - { 7, 7, 139, 1, 16, 7, 20643 }, // U+201C - { 7, 7, 139, 1, 16, 7, 20650 }, // U+201D - { 7, 7, 133, 1, 3, 7, 20657 }, // U+201E - { 8, 7, 139, 0, 16, 7, 20664 }, // U+201F - { 8, 13, 161, 1, 16, 13, 20671 }, // U+2020 - { 8, 16, 161, 1, 16, 16, 20684 }, // U+2021 - { 7, 8, 183, 2, 10, 7, 20700 }, // U+2022 - { 6, 8, 183, 3, 10, 6, 20707 }, // U+2023 - { 4, 4, 91, 1, 3, 2, 20713 }, // U+2024 - { 9, 4, 182, 1, 3, 5, 20715 }, // U+2025 - { 15, 4, 273, 1, 3, 8, 20720 }, // U+2026 - { 4, 4, 91, 1, 9, 2, 20728 }, // U+2027 - { 0, 0, 0, 0, 0, 0, 20730 }, // U+202A - { 0, 0, 0, 0, 0, 0, 20730 }, // U+202B - { 0, 0, 0, 0, 0, 0, 20730 }, // U+202C - { 0, 0, 0, 0, 0, 0, 20730 }, // U+202D - { 0, 0, 0, 0, 0, 0, 20730 }, // U+202E - { 0, 0, 58, 0, 0, 0, 20730 }, // U+202F - { 23, 17, 433, 2, 16, 49, 20730 }, // U+2030 - { 31, 17, 550, 2, 16, 66, 20779 }, // U+2031 - { 4, 7, 72, 0, 16, 4, 20845 }, // U+2032 - { 8, 7, 143, 0, 16, 7, 20849 }, // U+2033 - { 13, 7, 213, 0, 16, 12, 20856 }, // U+2034 - { 4, 7, 72, 0, 16, 4, 20868 }, // U+2035 - { 8, 7, 143, 0, 16, 7, 20872 }, // U+2036 - { 13, 7, 213, 0, 16, 12, 20879 }, // U+2037 - { 9, 6, 149, 0, 0, 7, 20891 }, // U+2038 - { 7, 10, 124, 0, 11, 9, 20898 }, // U+2039 - { 7, 10, 124, 0, 11, 9, 20907 }, // U+203A - { 13, 12, 210, 0, 13, 20, 20916 }, // U+203B - { 9, 17, 182, 1, 16, 20, 20936 }, // U+203C - { 10, 17, 168, 0, 16, 22, 20956 }, // U+203D - { 11, 2, 178, 0, 18, 3, 20978 }, // U+203E - { 17, 4, 270, 0, -1, 9, 20981 }, // U+203F - { 17, 4, 270, 0, 16, 9, 20990 }, // U+2040 - { 9, 12, 148, 0, 6, 14, 20999 }, // U+2041 - { 13, 13, 239, 1, 14, 22, 21013 }, // U+2042 - { 7, 2, 183, 2, 7, 2, 21035 }, // U+2043 - { 13, 16, 59, -5, 16, 26, 21037 }, // U+2044 - { 5, 20, 117, 2, 17, 13, 21063 }, // U+2045 - { 5, 20, 117, 1, 17, 13, 21076 }, // U+2046 - { 21, 17, 343, 0, 16, 45, 21089 }, // U+2047 - { 15, 17, 262, 0, 16, 32, 21134 }, // U+2048 - { 15, 17, 262, 1, 16, 32, 21166 }, // U+2049 - { 9, 17, 144, 0, 12, 20, 21198 }, // U+204A - { 11, 16, 198, 1, 16, 22, 21218 }, // U+204B - { 10, 10, 195, 1, 11, 13, 21240 }, // U+204C - { 10, 10, 195, 1, 11, 13, 21253 }, // U+204D - { 9, 10, 165, 1, 8, 12, 21266 }, // U+204E - { 4, 15, 95, 1, 11, 8, 21278 }, // U+204F - { 17, 21, 268, 0, 16, 45, 21286 }, // U+2050 - { 9, 20, 178, 2, 16, 23, 21331 }, // U+2051 - { 13, 16, 238, 1, 16, 26, 21354 }, // U+2052 - { 13, 5, 239, 1, 10, 9, 21380 }, // U+2053 - { 17, 4, 270, 0, -1, 9, 21389 }, // U+2054 - { 9, 9, 180, 1, 10, 11, 21398 }, // U+2055 - { 17, 7, 284, 0, 16, 15, 21409 }, // U+2057 - { 0, 0, 91, 0, 0, 0, 21424 }, // U+205F - { 0, 0, 0, 0, 0, 0, 21424 }, // U+2060 - { 0, 0, 0, 0, 0, 0, 21424 }, // U+2061 - { 0, 0, 0, 0, 0, 0, 21424 }, // U+2062 - { 0, 0, 0, 0, 0, 0, 21424 }, // U+2063 - { 0, 0, 0, 0, 0, 0, 21424 }, // U+2064 - { 0, 0, 0, 0, 0, 0, 21424 }, // U+2066 - { 0, 0, 0, 0, 0, 0, 21424 }, // U+2067 - { 0, 0, 0, 0, 0, 0, 21424 }, // U+2068 - { 0, 0, 0, 0, 0, 0, 21424 }, // U+2069 - { 0, 0, 0, 0, 0, 0, 21424 }, // U+206A - { 0, 0, 0, 0, 0, 0, 21424 }, // U+206B - { 0, 0, 0, 0, 0, 0, 21424 }, // U+206C - { 0, 0, 0, 0, 0, 0, 21424 }, // U+206D - { 0, 0, 0, 0, 0, 0, 21424 }, // U+206E - { 0, 0, 0, 0, 0, 0, 21424 }, // U+206F - { 9, 10, 150, 0, 18, 12, 21424 }, // U+2070 - { 2, 9, 64, 1, 18, 3, 21436 }, // U+2071 - { 9, 9, 149, 0, 18, 11, 21439 }, // U+2074 - { 8, 10, 143, 1, 18, 10, 21450 }, // U+2075 - { 9, 10, 144, 0, 18, 12, 21460 }, // U+2076 - { 8, 9, 132, 0, 18, 9, 21472 }, // U+2077 - { 9, 10, 146, 0, 18, 12, 21481 }, // U+2078 - { 9, 10, 144, 0, 18, 12, 21493 }, // U+2079 - { 7, 6, 142, 1, 16, 6, 21505 }, // U+207A - { 7, 2, 142, 1, 14, 2, 21511 }, // U+207B - { 7, 5, 142, 1, 15, 5, 21513 }, // U+207C - { 4, 11, 86, 1, 18, 6, 21518 }, // U+207D - { 4, 11, 86, 0, 18, 6, 21524 }, // U+207E - { 7, 7, 141, 1, 16, 7, 21530 }, // U+207F - { 9, 10, 150, 0, 6, 12, 21537 }, // U+2080 - { 5, 9, 98, 0, 6, 6, 21549 }, // U+2081 - { 7, 9, 143, 1, 6, 8, 21555 }, // U+2082 - { 8, 10, 145, 1, 6, 10, 21563 }, // U+2083 - { 9, 9, 149, 0, 6, 11, 21573 }, // U+2084 - { 8, 10, 143, 1, 6, 10, 21584 }, // U+2085 - { 9, 10, 144, 0, 6, 12, 21594 }, // U+2086 - { 8, 9, 132, 0, 6, 9, 21606 }, // U+2087 - { 9, 10, 146, 0, 6, 12, 21615 }, // U+2088 - { 9, 10, 144, 0, 6, 12, 21627 }, // U+2089 - { 7, 6, 142, 1, 4, 6, 21639 }, // U+208A - { 7, 2, 142, 1, 2, 2, 21645 }, // U+208B - { 7, 5, 142, 1, 3, 5, 21647 }, // U+208C - { 4, 11, 86, 1, 6, 6, 21652 }, // U+208D - { 4, 11, 86, 0, 6, 6, 21658 }, // U+208E - { 8, 8, 131, 0, 4, 8, 21664 }, // U+2090 - { 8, 8, 135, 0, 4, 8, 21672 }, // U+2091 - { 8, 8, 138, 0, 4, 8, 21680 }, // U+2092 - { 8, 7, 130, 0, 4, 7, 21688 }, // U+2093 - { 8, 9, 148, 1, 4, 9, 21695 }, // U+2094 - { 7, 9, 141, 1, 6, 8, 21704 }, // U+2095 - { 7, 9, 129, 1, 6, 8, 21712 }, // U+2096 - { 2, 9, 64, 1, 6, 3, 21720 }, // U+2097 - { 11, 7, 203, 1, 4, 10, 21723 }, // U+2098 - { 7, 7, 141, 1, 4, 7, 21733 }, // U+2099 - { 8, 10, 142, 1, 4, 10, 21740 }, // U+209A - { 8, 8, 126, 0, 4, 8, 21750 }, // U+209B - { 5, 9, 82, 0, 5, 6, 21758 }, // U+209C - { 13, 17, 240, 1, 16, 28, 21764 }, // U+20A0 - { 14, 20, 243, 1, 18, 35, 21792 }, // U+20A1 - { 13, 17, 241, 1, 16, 28, 21827 }, // U+20A2 - { 13, 16, 200, -1, 16, 26, 21855 }, // U+20A3 - { 11, 16, 201, 1, 16, 22, 21881 }, // U+20A4 - { 16, 16, 283, 1, 14, 32, 21903 }, // U+20A5 - { 16, 16, 254, 0, 16, 32, 21935 }, // U+20A6 - { 19, 17, 314, 1, 16, 41, 21967 }, // U+20A7 - { 22, 17, 370, 1, 16, 47, 22008 }, // U+20A8 - { 20, 16, 315, 0, 16, 40, 22055 }, // U+20A9 - { 15, 12, 269, 1, 12, 23, 22095 }, // U+20AA - { 13, 20, 201, 0, 16, 33, 22118 }, // U+20AB - { 13, 17, 220, 0, 16, 28, 22151 }, // U+20AC - { 15, 16, 234, 0, 16, 30, 22179 }, // U+20AD - { 13, 16, 213, 0, 16, 26, 22209 }, // U+20AE - { 28, 23, 458, 0, 18, 81, 22235 }, // U+20AF - { 14, 16, 219, 0, 16, 28, 22316 }, // U+20B1 - { 14, 21, 247, 1, 18, 37, 22344 }, // U+20B2 - { 14, 16, 227, 0, 16, 28, 22381 }, // U+20B3 - { 11, 17, 208, 1, 16, 24, 22409 }, // U+20B4 - { 14, 21, 243, 1, 18, 37, 22433 }, // U+20B5 - { 13, 16, 213, 0, 16, 26, 22470 }, // U+20B8 - { 11, 16, 178, 0, 16, 22, 22496 }, // U+20B9 - { 12, 17, 195, 0, 16, 26, 22518 }, // U+20BA - { 13, 12, 240, 1, 12, 20, 22544 }, // U+20BC - { 14, 16, 231, 0, 16, 28, 22564 }, // U+20BD - { 13, 16, 239, 1, 16, 26, 22592 }, // U+20BE - { 12, 20, 217, 1, 18, 30, 22618 }, // U+20BF - { 17, 14, 316, 1, 14, 30, 22648 }, // U+2190 - { 15, 16, 276, 1, 16, 30, 22678 }, // U+2191 - { 16, 14, 316, 2, 14, 28, 22708 }, // U+2192 - { 15, 17, 276, 1, 16, 32, 22736 }, // U+2193 - { 25, 14, 445, 1, 14, 44, 22768 }, // U+2194 - { 15, 25, 276, 1, 19, 47, 22812 }, // U+2195 - { 12, 13, 255, 2, 12, 20, 22859 }, // U+2196 - { 12, 13, 255, 2, 12, 20, 22879 }, // U+2197 - { 12, 12, 255, 2, 12, 18, 22899 }, // U+2198 - { 12, 12, 255, 2, 12, 18, 22917 }, // U+2199 - { 21, 16, 379, 1, 16, 42, 22935 }, // U+21A9 - { 21, 16, 379, 1, 16, 42, 22977 }, // U+21AA - { 18, 16, 327, 1, 16, 36, 23019 }, // U+21B0 - { 17, 16, 327, 2, 16, 34, 23055 }, // U+21B1 - { 12, 15, 273, 2, 15, 23, 23089 }, // U+21B2 - { 17, 14, 327, 2, 16, 30, 23112 }, // U+21B3 - { 14, 17, 275, 2, 16, 30, 23142 }, // U+21B4 - { 18, 14, 327, 1, 16, 32, 23172 }, // U+21B5 - { 17, 13, 273, 0, 14, 28, 23204 }, // U+21B6 - { 17, 13, 273, 0, 14, 28, 23232 }, // U+21B7 - { 16, 19, 316, 2, 19, 38, 23260 }, // U+21BA - { 16, 19, 316, 2, 19, 38, 23298 }, // U+21BB - { 16, 15, 273, 1, 15, 30, 23336 }, // U+21C4 - { 16, 15, 273, 1, 15, 30, 23366 }, // U+21C6 - { 17, 15, 316, 1, 14, 32, 23396 }, // U+21D0 - { 16, 15, 316, 2, 14, 30, 23428 }, // U+21D2 - { 25, 15, 445, 1, 14, 47, 23458 }, // U+21D4 - { 10, 16, 200, 1, 16, 20, 23505 }, // U+21DE - { 10, 17, 200, 1, 16, 22, 23525 }, // U+21DF - { 19, 9, 336, 1, 12, 22, 23547 }, // U+21E4 - { 19, 9, 336, 1, 12, 22, 23569 }, // U+21E5 - { 19, 17, 341, 1, 17, 41, 23591 }, // U+21E7 - { 19, 21, 341, 1, 17, 50, 23632 }, // U+21EA - { 11, 17, 209, 1, 16, 24, 23682 }, // U+2202 - { 14, 13, 226, 0, 12, 23, 23706 }, // U+2205 - { 14, 16, 224, 0, 16, 28, 23729 }, // U+2206 - { 13, 19, 238, 1, 16, 31, 23757 }, // U+220F - { 13, 19, 209, 0, 16, 31, 23788 }, // U+2211 - { 10, 2, 218, 2, 7, 3, 23819 }, // U+2212 - { 13, 15, 49, -5, 15, 25, 23822 }, // U+2215 - { 13, 16, 207, 0, 16, 26, 23847 }, // U+221A - { 19, 9, 309, 0, 11, 22, 23873 }, // U+221E - { 9, 22, 120, -1, 17, 25, 23895 }, // U+222B - { 4, 12, 91, 1, 13, 6, 23920 }, // U+2236 - { 12, 10, 218, 1, 11, 15, 23926 }, // U+2248 - { 10, 12, 218, 2, 12, 15, 23941 }, // U+2260 - { 10, 12, 218, 2, 12, 15, 23956 }, // U+2264 - { 10, 12, 218, 2, 12, 15, 23971 }, // U+2265 - { 29, 29, 463, 0, 22, 106, 23986 }, // U+2295 - { 29, 29, 463, 0, 22, 106, 24092 }, // U+2296 - { 29, 29, 463, 0, 22, 106, 24198 }, // U+2297 - { 29, 29, 463, 0, 22, 106, 24304 }, // U+2298 - { 11, 16, 189, 0, 16, 22, 24410 }, // U+FB01 - { 12, 16, 199, 0, 16, 24, 24432 }, // U+FB02 + { 4, 6, 0, -2, 15, 3, 10351 }, // U+0312 + { 4, 7, 0, 4, 11, 4, 10354 }, // U+0313 + { 4, 7, 0, 4, 11, 4, 10358 }, // U+0315 + { 4, 5, 0, -5, 15, 3, 10362 }, // U+031B + { 3, 3, 78, 1, -1, 2, 10365 }, // U+0323 + { 4, 5, 0, -9, -1, 3, 10367 }, // U+0326 + { 5, 6, 0, 1, 1, 4, 10370 }, // U+0327 + { 5, 6, 0, 0, 1, 4, 10374 }, // U+0328 + { 7, 4, 0, 1, -1, 4, 10378 }, // U+032C + { 9, 15, 0, -11, 13, 17, 10382 }, // U+0337 + { 12, 17, 0, -12, 16, 26, 10399 }, // U+0338 + { 7, 3, 0, -9, 16, 3, 10425 }, // U+0342 + { 4, 5, 0, 0, 13, 3, 10428 }, // U+0343 + { 8, 5, 0, -10, 17, 5, 10431 }, // U+0346 + { 7, 5, 0, -10, -3, 5, 10436 }, // U+0347 + { 5, 5, 0, -8, -1, 4, 10441 }, // U+0348 + { 5, 5, 0, -8, -1, 4, 10445 }, // U+0349 + { 8, 5, 0, -10, 17, 5, 10449 }, // U+034A + { 9, 8, 0, -11, 19, 9, 10454 }, // U+034B + { 8, 5, 0, -10, 17, 5, 10463 }, // U+034C + { 10, 5, 0, -11, -1, 7, 10468 }, // U+034D + { 6, 6, 0, -9, 0, 5, 10475 }, // U+034E + { 0, 0, 0, 0, 0, 0, 10480 }, // U+034F + { 9, 8, 0, -10, 21, 9, 10480 }, // U+0350 + { 3, 5, 0, -7, 16, 2, 10489 }, // U+0351 + { 8, 7, 0, -10, 18, 7, 10491 }, // U+0352 + { 6, 6, 0, -8, 0, 5, 10498 }, // U+0353 + { 6, 6, 0, -8, 0, 5, 10503 }, // U+0354 + { 6, 6, 0, -8, 0, 5, 10508 }, // U+0355 + { 12, 6, 0, -10, 0, 9, 10513 }, // U+0356 + { 4, 7, 0, 0, 18, 4, 10522 }, // U+0357 + { 3, 3, 0, 1, 16, 2, 10526 }, // U+0358 + { 6, 6, 0, -9, 0, 5, 10528 }, // U+0359 + { 9, 5, 0, -10, 0, 6, 10533 }, // U+035A + { 5, 5, 0, -8, 17, 4, 10539 }, // U+035B + { 16, 4, 0, -8, 0, 8, 10543 }, // U+035C + { 16, 4, 0, -8, 16, 8, 10551 }, // U+035D + { 14, 3, 0, -13, 16, 6, 10559 }, // U+035E + { 14, 3, 0, -13, -1, 6, 10565 }, // U+035F + { 20, 4, 0, -10, 16, 10, 10571 }, // U+0360 + { 16, 4, 0, -8, 16, 8, 10581 }, // U+0361 + { 17, 5, 0, -8, -1, 11, 10589 }, // U+0362 + { 6, 6, 0, -9, 18, 5, 10600 }, // U+0363 + { 6, 6, 0, -9, 18, 5, 10605 }, // U+0364 + { 2, 8, 0, -7, 20, 2, 10610 }, // U+0365 + { 6, 6, 0, -9, 18, 5, 10612 }, // U+0366 + { 6, 6, 0, -9, 18, 5, 10617 }, // U+0367 + { 6, 6, 0, -9, 18, 5, 10622 }, // U+0368 + { 6, 9, 0, -9, 21, 7, 10627 }, // U+0369 + { 6, 9, 0, -9, 21, 7, 10634 }, // U+036A + { 9, 6, 0, -10, 18, 7, 10641 }, // U+036B + { 4, 6, 0, -8, 18, 3, 10648 }, // U+036C + { 4, 8, 0, 0, 20, 4, 10651 }, // U+036D + { 6, 6, 0, -9, 18, 5, 10655 }, // U+036E + { 6, 6, 0, -8, 18, 5, 10660 }, // U+036F + { 11, 20, 199, 1, 20, 28, 10665 }, // U+0400 + { 11, 20, 199, 1, 20, 28, 10693 }, // U+0401 + { 17, 17, 278, 0, 16, 37, 10721 }, // U+0402 + { 10, 20, 190, 1, 20, 25, 10758 }, // U+0403 + { 14, 17, 243, 1, 16, 30, 10783 }, // U+0404 + { 12, 17, 212, 1, 16, 26, 10813 }, // U+0405 + { 3, 16, 87, 1, 16, 6, 10839 }, // U+0406 + { 8, 20, 87, -1, 20, 20, 10845 }, // U+0407 + { 10, 17, 188, 0, 16, 22, 10865 }, // U+0408 + { 23, 16, 368, 0, 16, 46, 10887 }, // U+0409 + { 22, 16, 373, 1, 16, 44, 10933 }, // U+040A + { 17, 16, 292, 0, 16, 34, 10977 }, // U+040B + { 13, 20, 221, 1, 20, 33, 11011 }, // U+040C + { 13, 20, 248, 1, 20, 33, 11044 }, // U+040D + { 13, 20, 207, 0, 20, 33, 11077 }, // U+040E + { 13, 20, 238, 1, 16, 33, 11110 }, // U+040F + { 14, 16, 227, 0, 16, 28, 11143 }, // U+0410 + { 12, 16, 213, 1, 16, 24, 11171 }, // U+0411 + { 12, 16, 217, 1, 16, 24, 11195 }, // U+0412 + { 10, 16, 190, 1, 16, 20, 11219 }, // U+0413 + { 16, 20, 269, 0, 16, 40, 11239 }, // U+0414 + { 11, 16, 199, 1, 16, 22, 11279 }, // U+0415 + { 20, 16, 317, 0, 16, 40, 11301 }, // U+0416 + { 11, 17, 204, 1, 16, 24, 11341 }, // U+0417 + { 13, 16, 248, 1, 16, 26, 11365 }, // U+0418 + { 13, 20, 248, 1, 20, 33, 11391 }, // U+0419 + { 13, 16, 221, 1, 16, 26, 11424 }, // U+041A + { 14, 16, 246, 0, 16, 28, 11450 }, // U+041B + { 16, 16, 298, 1, 16, 32, 11478 }, // U+041C + { 13, 16, 245, 1, 16, 26, 11510 }, // U+041D + { 14, 17, 254, 1, 16, 30, 11536 }, // U+041E + { 13, 16, 245, 1, 16, 26, 11566 }, // U+041F + { 12, 16, 211, 1, 16, 24, 11592 }, // U+0420 + { 14, 17, 243, 1, 16, 30, 11616 }, // U+0421 + { 13, 16, 213, 0, 16, 26, 11646 }, // U+0422 + { 13, 16, 207, 0, 16, 26, 11672 }, // U+0423 + { 15, 17, 275, 1, 16, 32, 11698 }, // U+0424 + { 14, 16, 225, 0, 16, 28, 11730 }, // U+0425 + { 14, 20, 238, 1, 16, 35, 11758 }, // U+0426 + { 12, 16, 231, 1, 16, 24, 11793 }, // U+0427 + { 18, 16, 316, 1, 16, 36, 11817 }, // U+0428 + { 19, 20, 321, 1, 16, 48, 11853 }, // U+0429 + { 16, 16, 269, 0, 16, 32, 11901 }, // U+042A + { 16, 16, 289, 1, 16, 32, 11933 }, // U+042B + { 12, 16, 214, 1, 16, 24, 11965 }, // U+042C + { 13, 17, 243, 1, 16, 28, 11989 }, // U+042D + { 19, 17, 337, 1, 16, 41, 12017 }, // U+042E + { 12, 16, 214, 0, 16, 24, 12058 }, // U+042F + { 10, 13, 184, 0, 12, 17, 12082 }, // U+0430 + { 12, 17, 194, 0, 16, 26, 12099 }, // U+0431 + { 10, 12, 185, 1, 12, 15, 12125 }, // U+0432 + { 8, 12, 144, 1, 12, 12, 12140 }, // U+0433 + { 13, 16, 203, 0, 12, 26, 12152 }, // U+0434 + { 11, 13, 191, 0, 12, 18, 12178 }, // U+0435 + { 17, 12, 267, 0, 12, 26, 12196 }, // U+0436 + { 10, 13, 163, 0, 12, 17, 12222 }, // U+0437 + { 10, 12, 194, 1, 12, 15, 12239 }, // U+0438 + { 10, 16, 194, 1, 16, 20, 12254 }, // U+0439 + { 10, 12, 178, 1, 12, 15, 12274 }, // U+043A + { 11, 12, 190, 0, 12, 17, 12289 }, // U+043B + { 14, 12, 254, 1, 12, 21, 12306 }, // U+043C + { 10, 12, 193, 1, 12, 15, 12327 }, // U+043D + { 12, 13, 196, 0, 12, 20, 12342 }, // U+043E + { 10, 12, 193, 1, 12, 15, 12362 }, // U+043F + { 11, 17, 201, 1, 12, 24, 12377 }, // U+0440 + { 11, 13, 187, 0, 12, 18, 12401 }, // U+0441 + { 10, 12, 155, 0, 12, 15, 12419 }, // U+0442 + { 11, 17, 184, 0, 12, 24, 12434 }, // U+0443 + { 14, 21, 226, 0, 16, 37, 12458 }, // U+0444 + { 11, 12, 179, 0, 12, 17, 12495 }, // U+0445 + { 11, 16, 197, 1, 12, 22, 12512 }, // U+0446 + { 10, 12, 190, 1, 12, 15, 12534 }, // U+0447 + { 15, 12, 271, 1, 12, 23, 12549 }, // U+0448 + { 16, 16, 276, 1, 12, 32, 12572 }, // U+0449 + { 13, 12, 213, 0, 12, 20, 12604 }, // U+044A + { 13, 12, 241, 1, 12, 20, 12624 }, // U+044B + { 10, 12, 184, 1, 12, 15, 12644 }, // U+044C + { 11, 13, 187, 0, 12, 18, 12659 }, // U+044D + { 16, 13, 276, 1, 12, 26, 12677 }, // U+044E + { 10, 12, 182, 0, 12, 15, 12703 }, // U+044F + { 11, 17, 191, 0, 16, 24, 12718 }, // U+0450 + { 11, 17, 191, 0, 16, 24, 12742 }, // U+0451 + { 11, 21, 194, 0, 16, 29, 12766 }, // U+0452 + { 8, 16, 144, 1, 16, 16, 12795 }, // U+0453 + { 11, 13, 187, 0, 12, 18, 12811 }, // U+0454 + { 9, 13, 172, 1, 12, 15, 12829 }, // U+0455 + { 3, 16, 78, 1, 16, 6, 12844 }, // U+0456 + { 7, 16, 78, -1, 16, 14, 12850 }, // U+0457 + { 5, 21, 78, -1, 16, 14, 12864 }, // U+0458 + { 19, 12, 306, 0, 12, 29, 12878 }, // U+0459 + { 17, 12, 297, 1, 12, 26, 12907 }, // U+045A + { 11, 16, 194, 0, 16, 22, 12933 }, // U+045B + { 10, 16, 178, 1, 16, 20, 12955 }, // U+045C + { 10, 16, 194, 1, 16, 20, 12975 }, // U+045D + { 11, 21, 184, 0, 16, 29, 12995 }, // U+045E + { 10, 16, 193, 1, 12, 20, 13024 }, // U+045F + { 16, 17, 296, 1, 16, 34, 13044 }, // U+0460 + { 15, 13, 266, 1, 12, 25, 13078 }, // U+0461 + { 14, 16, 228, 0, 16, 28, 13103 }, // U+0462 + { 12, 16, 203, 0, 16, 24, 13131 }, // U+0463 + { 18, 17, 316, 1, 16, 39, 13155 }, // U+0464 + { 15, 13, 258, 1, 12, 25, 13194 }, // U+0465 + { 14, 16, 227, 0, 16, 28, 13219 }, // U+0466 + { 12, 12, 188, 0, 12, 18, 13247 }, // U+0467 + { 18, 16, 310, 1, 16, 36, 13265 }, // U+0468 + { 15, 12, 257, 1, 12, 23, 13301 }, // U+0469 + { 16, 16, 282, 1, 16, 32, 13324 }, // U+046A + { 14, 12, 231, 0, 12, 21, 13356 }, // U+046B + { 22, 16, 379, 1, 16, 44, 13377 }, // U+046C + { 18, 12, 317, 1, 12, 27, 13421 }, // U+046D + { 9, 25, 179, 1, 20, 29, 13448 }, // U+046E + { 10, 21, 171, 0, 16, 27, 13477 }, // U+046F + { 16, 16, 291, 1, 16, 32, 13504 }, // U+0470 + { 14, 19, 256, 1, 14, 34, 13536 }, // U+0471 + { 14, 17, 254, 1, 16, 30, 13570 }, // U+0472 + { 12, 13, 196, 0, 12, 20, 13600 }, // U+0473 + { 15, 16, 232, 0, 16, 30, 13620 }, // U+0474 + { 12, 12, 195, 0, 12, 18, 13650 }, // U+0475 + { 15, 20, 232, 0, 20, 38, 13668 }, // U+0476 + { 12, 16, 195, 0, 16, 24, 13706 }, // U+0477 + { 26, 21, 437, 1, 16, 69, 13730 }, // U+0478 + { 23, 17, 373, 0, 12, 49, 13799 }, // U+0479 + { 13, 16, 236, 1, 16, 26, 13848 }, // U+0480 + { 11, 16, 185, 0, 12, 22, 13874 }, // U+0481 + { 11, 14, 208, 1, 14, 20, 13896 }, // U+0482 + { 8, 4, 0, -10, 16, 4, 13916 }, // U+0483 + { 9, 3, 0, -10, 16, 4, 13920 }, // U+0484 + { 3, 4, 0, -7, 17, 2, 13924 }, // U+0485 + { 4, 4, 0, -7, 17, 2, 13926 }, // U+0486 + { 9, 3, 0, -10, 16, 4, 13928 }, // U+0487 + { 21, 19, 0, -16, 15, 50, 13932 }, // U+0488 + { 20, 21, 0, -16, 16, 53, 13982 }, // U+0489 + { 15, 24, 248, 1, 20, 45, 14035 }, // U+048A + { 12, 20, 194, 1, 16, 30, 14080 }, // U+048B + { 14, 16, 228, 0, 16, 28, 14110 }, // U+048C + { 12, 14, 203, 0, 14, 21, 14138 }, // U+048D + { 12, 16, 209, 1, 16, 24, 14159 }, // U+048E + { 11, 17, 201, 1, 12, 24, 14183 }, // U+048F + { 10, 19, 190, 1, 19, 24, 14207 }, // U+0490 + { 8, 15, 144, 1, 15, 15, 14231 }, // U+0491 + { 12, 16, 190, -1, 16, 24, 14246 }, // U+0492 + { 9, 12, 144, 0, 12, 14, 14270 }, // U+0493 + { 12, 17, 215, 1, 16, 26, 14284 }, // U+0494 + { 10, 16, 178, 1, 12, 20, 14310 }, // U+0495 + { 21, 20, 323, 0, 16, 53, 14330 }, // U+0496 + { 17, 16, 274, 0, 12, 34, 14383 }, // U+0497 + { 11, 21, 204, 1, 16, 29, 14417 }, // U+0498 + { 10, 17, 163, 0, 12, 22, 14446 }, // U+0499 + { 14, 20, 233, 1, 16, 35, 14468 }, // U+049A + { 11, 16, 185, 1, 12, 22, 14503 }, // U+049B + { 13, 16, 219, 1, 16, 26, 14525 }, // U+049C + { 11, 12, 192, 1, 12, 17, 14551 }, // U+049D + { 17, 16, 276, 0, 16, 34, 14568 }, // U+04A0 + { 13, 12, 207, 0, 12, 20, 14602 }, // U+04A1 + { 15, 20, 250, 1, 16, 38, 14622 }, // U+04A2 + { 11, 16, 198, 1, 12, 22, 14660 }, // U+04A3 + { 19, 16, 325, 1, 16, 38, 14682 }, // U+04A4 + { 13, 12, 225, 1, 12, 20, 14720 }, // U+04A5 + { 21, 17, 367, 1, 16, 45, 14740 }, // U+04A6 + { 18, 15, 305, 1, 12, 34, 14785 }, // U+04A7 + { 19, 17, 326, 1, 16, 41, 14819 }, // U+04A8 + { 16, 13, 257, 0, 12, 26, 14860 }, // U+04A9 + { 14, 21, 243, 1, 16, 37, 14886 }, // U+04AA + { 11, 17, 187, 0, 12, 24, 14923 }, // U+04AB + { 13, 20, 213, 0, 16, 33, 14947 }, // U+04AC + { 10, 16, 155, 0, 12, 20, 14980 }, // U+04AD + { 14, 16, 224, 0, 16, 28, 15000 }, // U+04AE + { 11, 17, 184, 0, 12, 24, 15028 }, // U+04AF + { 14, 16, 224, 0, 16, 28, 15052 }, // U+04B0 + { 11, 17, 184, 0, 12, 24, 15080 }, // U+04B1 + { 15, 20, 231, 0, 16, 38, 15104 }, // U+04B2 + { 12, 16, 186, 0, 12, 24, 15142 }, // U+04B3 + { 19, 20, 303, 0, 16, 48, 15166 }, // U+04B4 + { 14, 16, 218, 0, 12, 28, 15214 }, // U+04B5 + { 14, 20, 235, 1, 16, 35, 15242 }, // U+04B6 + { 11, 16, 194, 1, 12, 22, 15277 }, // U+04B7 + { 12, 16, 231, 1, 16, 24, 15299 }, // U+04B8 + { 10, 12, 190, 1, 12, 15, 15323 }, // U+04B9 + { 12, 16, 231, 1, 16, 24, 15338 }, // U+04BA + { 10, 16, 194, 1, 16, 20, 15362 }, // U+04BB + { 17, 17, 279, 0, 16, 37, 15382 }, // U+04BC + { 14, 13, 235, 0, 12, 23, 15419 }, // U+04BD + { 17, 20, 279, 0, 16, 43, 15442 }, // U+04BE + { 14, 17, 235, 0, 12, 30, 15485 }, // U+04BF + { 3, 16, 87, 1, 16, 6, 15515 }, // U+04C0 + { 20, 20, 317, 0, 20, 50, 15521 }, // U+04C1 + { 17, 16, 267, 0, 16, 34, 15571 }, // U+04C2 + { 12, 20, 225, 1, 16, 30, 15605 }, // U+04C3 + { 11, 15, 193, 1, 12, 21, 15635 }, // U+04C4 + { 16, 20, 246, 0, 16, 40, 15656 }, // U+04C5 + { 13, 16, 190, 0, 12, 26, 15696 }, // U+04C6 + { 13, 21, 245, 1, 16, 35, 15722 }, // U+04C7 + { 10, 17, 193, 1, 12, 22, 15757 }, // U+04C8 + { 15, 20, 245, 1, 16, 38, 15779 }, // U+04C9 + { 12, 16, 193, 1, 12, 24, 15817 }, // U+04CA + { 14, 20, 235, 1, 16, 35, 15841 }, // U+04CB + { 11, 16, 194, 1, 12, 22, 15876 }, // U+04CC + { 18, 20, 298, 1, 16, 45, 15898 }, // U+04CD + { 16, 16, 254, 1, 12, 32, 15943 }, // U+04CE + { 3, 16, 78, 1, 16, 6, 15975 }, // U+04CF + { 14, 20, 227, 0, 20, 35, 15981 }, // U+04D0 + { 10, 17, 184, 0, 16, 22, 16016 }, // U+04D1 + { 14, 20, 227, 0, 20, 35, 16038 }, // U+04D2 + { 10, 17, 184, 0, 16, 22, 16073 }, // U+04D3 + { 20, 16, 329, 0, 16, 40, 16095 }, // U+04D4 + { 18, 13, 302, 0, 12, 30, 16135 }, // U+04D5 + { 11, 20, 199, 1, 20, 28, 16165 }, // U+04D6 + { 11, 17, 191, 0, 16, 24, 16193 }, // U+04D7 + { 14, 17, 247, 1, 16, 30, 16217 }, // U+04D8 + { 11, 13, 191, 0, 12, 18, 16247 }, // U+04D9 + { 14, 21, 247, 1, 20, 37, 16265 }, // U+04DA + { 11, 17, 191, 0, 16, 24, 16302 }, // U+04DB + { 20, 20, 317, 0, 20, 50, 16326 }, // U+04DC + { 17, 16, 267, 0, 16, 34, 16376 }, // U+04DD + { 11, 21, 204, 1, 20, 29, 16410 }, // U+04DE + { 10, 17, 163, 0, 16, 22, 16439 }, // U+04DF + { 11, 17, 205, 1, 16, 24, 16461 }, // U+04E0 + { 11, 17, 205, 1, 12, 24, 16485 }, // U+04E1 + { 13, 19, 248, 1, 19, 31, 16509 }, // U+04E2 + { 10, 15, 194, 1, 15, 19, 16540 }, // U+04E3 + { 13, 20, 248, 1, 20, 33, 16559 }, // U+04E4 + { 10, 16, 194, 1, 16, 20, 16592 }, // U+04E5 + { 14, 21, 254, 1, 20, 37, 16612 }, // U+04E6 + { 12, 17, 196, 0, 16, 26, 16649 }, // U+04E7 + { 14, 17, 254, 1, 16, 30, 16675 }, // U+04E8 + { 12, 13, 196, 0, 12, 20, 16705 }, // U+04E9 + { 14, 21, 254, 1, 20, 37, 16725 }, // U+04EA + { 12, 17, 196, 0, 16, 26, 16762 }, // U+04EB + { 13, 21, 243, 1, 20, 35, 16788 }, // U+04EC + { 11, 17, 187, 0, 16, 24, 16823 }, // U+04ED + { 13, 19, 207, 0, 19, 31, 16847 }, // U+04EE + { 11, 20, 184, 0, 15, 28, 16878 }, // U+04EF + { 13, 20, 207, 0, 20, 33, 16906 }, // U+04F0 + { 11, 21, 184, 0, 16, 29, 16939 }, // U+04F1 + { 13, 20, 207, 0, 20, 33, 16968 }, // U+04F2 + { 11, 21, 184, 0, 16, 29, 17001 }, // U+04F3 + { 12, 20, 231, 1, 20, 30, 17030 }, // U+04F4 + { 10, 16, 190, 1, 16, 20, 17060 }, // U+04F5 + { 10, 20, 190, 1, 16, 25, 17080 }, // U+04F6 + { 8, 16, 144, 1, 12, 16, 17105 }, // U+04F7 + { 16, 20, 289, 1, 20, 40, 17121 }, // U+04F8 + { 13, 16, 241, 1, 16, 26, 17161 }, // U+04F9 + { 12, 21, 196, 0, 16, 32, 17187 }, // U+04FA + { 9, 17, 144, 0, 12, 20, 17219 }, // U+04FB + { 15, 21, 233, 0, 16, 40, 17239 }, // U+04FC + { 11, 17, 171, 0, 12, 24, 17279 }, // U+04FD + { 14, 16, 225, 0, 16, 28, 17303 }, // U+04FE + { 11, 12, 179, 0, 12, 17, 17331 }, // U+04FF + { 11, 13, 209, 1, 13, 18, 17348 }, // U+05D0 + { 11, 13, 184, 0, 13, 18, 17366 }, // U+05D1 + { 8, 13, 140, 0, 13, 13, 17384 }, // U+05D2 + { 11, 13, 176, 0, 13, 18, 17397 }, // U+05D3 + { 11, 13, 212, 1, 13, 18, 17415 }, // U+05D4 + { 3, 13, 87, 1, 13, 5, 17433 }, // U+05D5 + { 8, 13, 126, 0, 13, 13, 17438 }, // U+05D6 + { 11, 13, 216, 1, 13, 18, 17451 }, // U+05D7 + { 12, 13, 215, 1, 13, 20, 17469 }, // U+05D8 + { 3, 7, 80, 1, 13, 3, 17489 }, // U+05D9 + { 10, 17, 172, 0, 13, 22, 17492 }, // U+05DA + { 11, 13, 177, 0, 13, 18, 17514 }, // U+05DB + { 10, 16, 158, 0, 16, 20, 17532 }, // U+05DC + { 11, 13, 216, 1, 13, 18, 17552 }, // U+05DD + { 11, 13, 218, 1, 13, 18, 17570 }, // U+05DE + { 3, 17, 87, 1, 13, 7, 17588 }, // U+05DF + { 6, 13, 116, 0, 13, 10, 17595 }, // U+05E0 + { 12, 13, 214, 1, 13, 20, 17605 }, // U+05E1 + { 11, 14, 198, 0, 13, 20, 17625 }, // U+05E2 + { 11, 17, 210, 1, 13, 24, 17645 }, // U+05E3 + { 11, 13, 208, 1, 13, 18, 17669 }, // U+05E4 + { 10, 17, 172, 0, 13, 22, 17687 }, // U+05E5 + { 10, 13, 181, 0, 13, 17, 17709 }, // U+05E6 + { 12, 17, 203, 1, 13, 26, 17726 }, // U+05E7 + { 10, 13, 175, 0, 13, 17, 17752 }, // U+05E8 + { 14, 13, 251, 1, 13, 23, 17769 }, // U+05E9 + { 12, 13, 222, 1, 13, 20, 17792 }, // U+05EA + { 4, 6, 109, 1, 5, 3, 17812 }, // U+060C + { 4, 12, 115, 1, 11, 6, 17815 }, // U+061B + { 9, 17, 145, 0, 16, 20, 17821 }, // U+061F + { 8, 10, 137, 0, 8, 10, 17841 }, // U+0621 + { 7, 3, 90, 0, 2, 3, 17851 }, // U+0640 + { 4, 4, 191, 4, 8, 2, 17854 }, // U+0660 + { 4, 16, 191, 4, 15, 8, 17856 }, // U+0661 + { 10, 16, 191, 1, 15, 20, 17864 }, // U+0662 + { 12, 16, 191, 0, 15, 24, 17884 }, // U+0663 + { 10, 16, 191, 1, 15, 20, 17908 }, // U+0664 + { 10, 12, 191, 1, 11, 15, 17928 }, // U+0665 + { 10, 16, 191, 1, 15, 20, 17943 }, // U+0666 + { 12, 16, 191, 0, 15, 24, 17963 }, // U+0667 + { 12, 16, 191, 0, 15, 24, 17987 }, // U+0668 + { 10, 16, 191, 1, 15, 20, 18011 }, // U+0669 + { 14, 12, 228, 0, 7, 21, 18031 }, // U+06BA + { 6, 2, 100, 0, 2, 2, 18052 }, // U+06D4 + { 4, 4, 191, 4, 8, 2, 18054 }, // U+06F0 + { 4, 16, 191, 4, 15, 8, 18056 }, // U+06F1 + { 10, 16, 191, 1, 15, 20, 18064 }, // U+06F2 + { 12, 16, 191, 0, 15, 24, 18084 }, // U+06F3 + { 10, 15, 191, 1, 15, 19, 18108 }, // U+06F4 + { 10, 15, 191, 1, 15, 19, 18127 }, // U+06F5 + { 8, 16, 191, 2, 15, 16, 18146 }, // U+06F6 + { 12, 16, 191, 0, 15, 24, 18162 }, // U+06F7 + { 12, 16, 191, 0, 15, 24, 18186 }, // U+06F8 + { 10, 16, 191, 1, 15, 20, 18210 }, // U+06F9 + { 14, 20, 227, 0, 16, 35, 18230 }, // U+1EA0 + { 10, 16, 184, 0, 12, 20, 18265 }, // U+1EA1 + { 14, 20, 227, 0, 20, 35, 18285 }, // U+1EA2 + { 10, 17, 184, 0, 16, 22, 18320 }, // U+1EA3 + { 14, 23, 227, 0, 23, 41, 18342 }, // U+1EA4 + { 11, 20, 184, 0, 19, 28, 18383 }, // U+1EA5 + { 14, 23, 227, 0, 23, 41, 18411 }, // U+1EA6 + { 10, 20, 184, 0, 19, 25, 18452 }, // U+1EA7 + { 14, 23, 227, 0, 23, 41, 18477 }, // U+1EA8 + { 10, 20, 184, 0, 19, 25, 18518 }, // U+1EA9 + { 14, 23, 227, 0, 23, 41, 18543 }, // U+1EAA + { 10, 20, 184, 0, 19, 25, 18584 }, // U+1EAB + { 14, 24, 227, 0, 20, 42, 18609 }, // U+1EAC + { 10, 20, 184, 0, 16, 25, 18651 }, // U+1EAD + { 14, 23, 227, 0, 23, 41, 18676 }, // U+1EAE + { 10, 20, 184, 0, 19, 25, 18717 }, // U+1EAF + { 14, 23, 227, 0, 23, 41, 18742 }, // U+1EB0 + { 10, 20, 184, 0, 19, 25, 18783 }, // U+1EB1 + { 14, 23, 227, 0, 23, 41, 18808 }, // U+1EB2 + { 10, 20, 184, 0, 19, 25, 18849 }, // U+1EB3 + { 14, 23, 227, 0, 23, 41, 18874 }, // U+1EB4 + { 10, 20, 184, 0, 19, 25, 18915 }, // U+1EB5 + { 14, 24, 227, 0, 20, 42, 18940 }, // U+1EB6 + { 10, 20, 184, 0, 16, 25, 18982 }, // U+1EB7 + { 11, 20, 199, 1, 16, 28, 19007 }, // U+1EB8 + { 11, 16, 191, 0, 12, 22, 19035 }, // U+1EB9 + { 11, 20, 199, 1, 20, 28, 19057 }, // U+1EBA + { 11, 17, 191, 0, 16, 24, 19085 }, // U+1EBB + { 11, 20, 199, 1, 20, 28, 19109 }, // U+1EBC + { 11, 17, 191, 0, 16, 24, 19137 }, // U+1EBD + { 11, 23, 199, 1, 23, 32, 19161 }, // U+1EBE + { 11, 20, 191, 0, 19, 28, 19193 }, // U+1EBF + { 11, 23, 199, 1, 23, 32, 19221 }, // U+1EC0 + { 11, 20, 191, 0, 19, 28, 19253 }, // U+1EC1 + { 11, 23, 199, 1, 23, 32, 19281 }, // U+1EC2 + { 11, 20, 191, 0, 19, 28, 19313 }, // U+1EC3 + { 11, 23, 199, 1, 23, 32, 19341 }, // U+1EC4 + { 11, 20, 191, 0, 19, 28, 19373 }, // U+1EC5 + { 11, 24, 199, 1, 20, 33, 19401 }, // U+1EC6 + { 11, 20, 191, 0, 16, 28, 19434 }, // U+1EC7 + { 4, 20, 87, 1, 20, 10, 19462 }, // U+1EC8 + { 4, 16, 78, 0, 16, 8, 19472 }, // U+1EC9 + { 3, 20, 87, 1, 16, 8, 19480 }, // U+1ECA + { 3, 20, 78, 1, 16, 8, 19488 }, // U+1ECB + { 14, 20, 254, 1, 16, 35, 19496 }, // U+1ECC + { 12, 16, 196, 0, 12, 24, 19531 }, // U+1ECD + { 14, 21, 254, 1, 20, 37, 19555 }, // U+1ECE + { 12, 17, 196, 0, 16, 26, 19592 }, // U+1ECF + { 14, 24, 254, 1, 23, 42, 19618 }, // U+1ED0 + { 12, 20, 196, 0, 19, 30, 19660 }, // U+1ED1 + { 14, 24, 254, 1, 23, 42, 19690 }, // U+1ED2 + { 12, 20, 196, 0, 19, 30, 19732 }, // U+1ED3 + { 14, 24, 254, 1, 23, 42, 19762 }, // U+1ED4 + { 12, 20, 196, 0, 19, 30, 19804 }, // U+1ED5 + { 14, 24, 254, 1, 23, 42, 19834 }, // U+1ED6 + { 12, 20, 196, 0, 19, 30, 19876 }, // U+1ED7 + { 14, 24, 254, 1, 20, 42, 19906 }, // U+1ED8 + { 12, 20, 196, 0, 16, 30, 19948 }, // U+1ED9 + { 14, 21, 253, 1, 20, 37, 19978 }, // U+1EDA + { 12, 17, 195, 0, 16, 26, 20015 }, // U+1EDB + { 14, 21, 253, 1, 20, 37, 20041 }, // U+1EDC + { 12, 17, 195, 0, 16, 26, 20078 }, // U+1EDD + { 14, 21, 253, 1, 20, 37, 20104 }, // U+1EDE + { 12, 17, 195, 0, 16, 26, 20141 }, // U+1EDF + { 14, 21, 253, 1, 20, 37, 20167 }, // U+1EE0 + { 12, 17, 195, 0, 16, 26, 20204 }, // U+1EE1 + { 14, 22, 253, 1, 18, 39, 20230 }, // U+1EE2 + { 12, 18, 195, 0, 14, 27, 20269 }, // U+1EE3 + { 13, 20, 245, 1, 16, 33, 20296 }, // U+1EE4 + { 10, 16, 194, 1, 12, 20, 20329 }, // U+1EE5 + { 13, 21, 245, 1, 20, 35, 20349 }, // U+1EE6 + { 10, 17, 194, 1, 16, 22, 20384 }, // U+1EE7 + { 15, 21, 244, 1, 20, 40, 20406 }, // U+1EE8 + { 12, 17, 205, 1, 16, 26, 20446 }, // U+1EE9 + { 15, 21, 244, 1, 20, 40, 20472 }, // U+1EEA + { 12, 17, 205, 1, 16, 26, 20512 }, // U+1EEB + { 15, 21, 244, 1, 20, 40, 20538 }, // U+1EEC + { 12, 17, 205, 1, 16, 26, 20578 }, // U+1EED + { 15, 21, 244, 1, 20, 40, 20604 }, // U+1EEE + { 12, 17, 205, 1, 16, 26, 20644 }, // U+1EEF + { 15, 22, 244, 1, 18, 42, 20670 }, // U+1EF0 + { 12, 18, 205, 1, 14, 27, 20712 }, // U+1EF1 + { 14, 20, 224, 0, 20, 35, 20739 }, // U+1EF2 + { 11, 21, 184, 0, 16, 29, 20774 }, // U+1EF3 + { 14, 20, 224, 0, 16, 35, 20803 }, // U+1EF4 + { 11, 17, 184, 0, 12, 24, 20838 }, // U+1EF5 + { 14, 20, 224, 0, 20, 35, 20862 }, // U+1EF6 + { 11, 21, 184, 0, 16, 29, 20897 }, // U+1EF7 + { 14, 20, 224, 0, 20, 35, 20926 }, // U+1EF8 + { 11, 21, 184, 0, 16, 29, 20961 }, // U+1EF9 + { 0, 0, 182, 0, 0, 0, 20990 }, // U+2000 + { 0, 0, 364, 0, 0, 0, 20990 }, // U+2001 + { 0, 0, 167, 0, 0, 0, 20990 }, // U+2002 + { 0, 0, 333, 0, 0, 0, 20990 }, // U+2003 + { 0, 0, 111, 0, 0, 0, 20990 }, // U+2004 + { 0, 0, 83, 0, 0, 0, 20990 }, // U+2005 + { 0, 0, 55, 0, 0, 0, 20990 }, // U+2006 + { 0, 0, 216, 0, 0, 0, 20990 }, // U+2007 + { 0, 0, 91, 0, 0, 0, 20990 }, // U+2008 + { 0, 0, 58, 0, 0, 0, 20990 }, // U+2009 + { 0, 0, 28, 0, 0, 0, 20990 }, // U+200A + { 0, 0, 0, 0, 0, 0, 20990 }, // U+200B + { 0, 0, 0, 0, 0, 0, 20990 }, // U+200C + { 0, 0, 0, 0, 0, 0, 20990 }, // U+200D + { 0, 0, 0, 0, 0, 0, 20990 }, // U+200E + { 0, 0, 0, 0, 0, 0, 20990 }, // U+200F + { 7, 3, 117, 0, 8, 3, 20990 }, // U+2010 + { 7, 3, 117, 0, 8, 3, 20993 }, // U+2011 + { 13, 3, 216, 0, 8, 5, 20996 }, // U+2012 + { 11, 3, 167, 0, 8, 5, 21001 }, // U+2013 + { 21, 3, 333, 0, 8, 8, 21006 }, // U+2014 + { 21, 3, 333, 0, 8, 8, 21014 }, // U+2015 + { 6, 26, 157, 2, 21, 20, 21022 }, // U+2016 + { 11, 5, 149, -1, 0, 7, 21042 }, // U+2017 + { 4, 7, 82, 1, 16, 4, 21049 }, // U+2018 + { 4, 7, 82, 1, 16, 4, 21053 }, // U+2019 + { 3, 7, 76, 1, 3, 3, 21057 }, // U+201A + { 4, 7, 82, 0, 16, 4, 21060 }, // U+201B + { 7, 7, 139, 1, 16, 7, 21064 }, // U+201C + { 7, 7, 139, 1, 16, 7, 21071 }, // U+201D + { 7, 7, 133, 1, 3, 7, 21078 }, // U+201E + { 8, 7, 139, 0, 16, 7, 21085 }, // U+201F + { 8, 13, 161, 1, 16, 13, 21092 }, // U+2020 + { 8, 16, 161, 1, 16, 16, 21105 }, // U+2021 + { 7, 8, 183, 2, 10, 7, 21121 }, // U+2022 + { 6, 8, 183, 3, 10, 6, 21128 }, // U+2023 + { 4, 4, 91, 1, 3, 2, 21134 }, // U+2024 + { 9, 4, 182, 1, 3, 5, 21136 }, // U+2025 + { 15, 4, 273, 1, 3, 8, 21141 }, // U+2026 + { 4, 4, 91, 1, 9, 2, 21149 }, // U+2027 + { 0, 0, 0, 0, 0, 0, 21151 }, // U+202A + { 0, 0, 0, 0, 0, 0, 21151 }, // U+202B + { 0, 0, 0, 0, 0, 0, 21151 }, // U+202C + { 0, 0, 0, 0, 0, 0, 21151 }, // U+202D + { 0, 0, 0, 0, 0, 0, 21151 }, // U+202E + { 0, 0, 58, 0, 0, 0, 21151 }, // U+202F + { 23, 17, 433, 2, 16, 49, 21151 }, // U+2030 + { 31, 17, 550, 2, 16, 66, 21200 }, // U+2031 + { 4, 7, 72, 0, 16, 4, 21266 }, // U+2032 + { 8, 7, 143, 0, 16, 7, 21270 }, // U+2033 + { 13, 7, 213, 0, 16, 12, 21277 }, // U+2034 + { 4, 7, 72, 0, 16, 4, 21289 }, // U+2035 + { 8, 7, 143, 0, 16, 7, 21293 }, // U+2036 + { 13, 7, 213, 0, 16, 12, 21300 }, // U+2037 + { 9, 6, 149, 0, 0, 7, 21312 }, // U+2038 + { 7, 10, 124, 0, 11, 9, 21319 }, // U+2039 + { 7, 10, 124, 0, 11, 9, 21328 }, // U+203A + { 13, 12, 210, 0, 13, 20, 21337 }, // U+203B + { 9, 17, 182, 1, 16, 20, 21357 }, // U+203C + { 10, 17, 168, 0, 16, 22, 21377 }, // U+203D + { 11, 2, 178, 0, 18, 3, 21399 }, // U+203E + { 17, 4, 270, 0, -1, 9, 21402 }, // U+203F + { 17, 4, 270, 0, 16, 9, 21411 }, // U+2040 + { 9, 12, 148, 0, 6, 14, 21420 }, // U+2041 + { 13, 13, 239, 1, 14, 22, 21434 }, // U+2042 + { 7, 2, 183, 2, 7, 2, 21456 }, // U+2043 + { 13, 16, 59, -5, 16, 26, 21458 }, // U+2044 + { 5, 20, 117, 2, 17, 13, 21484 }, // U+2045 + { 5, 20, 117, 1, 17, 13, 21497 }, // U+2046 + { 21, 17, 343, 0, 16, 45, 21510 }, // U+2047 + { 15, 17, 262, 0, 16, 32, 21555 }, // U+2048 + { 15, 17, 262, 1, 16, 32, 21587 }, // U+2049 + { 9, 17, 144, 0, 12, 20, 21619 }, // U+204A + { 11, 16, 198, 1, 16, 22, 21639 }, // U+204B + { 10, 10, 195, 1, 11, 13, 21661 }, // U+204C + { 10, 10, 195, 1, 11, 13, 21674 }, // U+204D + { 9, 10, 165, 1, 8, 12, 21687 }, // U+204E + { 4, 15, 95, 1, 11, 8, 21699 }, // U+204F + { 17, 21, 268, 0, 16, 45, 21707 }, // U+2050 + { 9, 20, 178, 2, 16, 23, 21752 }, // U+2051 + { 13, 16, 238, 1, 16, 26, 21775 }, // U+2052 + { 13, 5, 239, 1, 10, 9, 21801 }, // U+2053 + { 17, 4, 270, 0, -1, 9, 21810 }, // U+2054 + { 9, 9, 180, 1, 10, 11, 21819 }, // U+2055 + { 17, 7, 284, 0, 16, 15, 21830 }, // U+2057 + { 0, 0, 91, 0, 0, 0, 21845 }, // U+205F + { 0, 0, 0, 0, 0, 0, 21845 }, // U+2060 + { 0, 0, 0, 0, 0, 0, 21845 }, // U+2061 + { 0, 0, 0, 0, 0, 0, 21845 }, // U+2062 + { 0, 0, 0, 0, 0, 0, 21845 }, // U+2063 + { 0, 0, 0, 0, 0, 0, 21845 }, // U+2064 + { 0, 0, 0, 0, 0, 0, 21845 }, // U+2066 + { 0, 0, 0, 0, 0, 0, 21845 }, // U+2067 + { 0, 0, 0, 0, 0, 0, 21845 }, // U+2068 + { 0, 0, 0, 0, 0, 0, 21845 }, // U+2069 + { 0, 0, 0, 0, 0, 0, 21845 }, // U+206A + { 0, 0, 0, 0, 0, 0, 21845 }, // U+206B + { 0, 0, 0, 0, 0, 0, 21845 }, // U+206C + { 0, 0, 0, 0, 0, 0, 21845 }, // U+206D + { 0, 0, 0, 0, 0, 0, 21845 }, // U+206E + { 0, 0, 0, 0, 0, 0, 21845 }, // U+206F + { 9, 10, 150, 0, 18, 12, 21845 }, // U+2070 + { 2, 9, 64, 1, 18, 3, 21857 }, // U+2071 + { 9, 9, 149, 0, 18, 11, 21860 }, // U+2074 + { 8, 10, 143, 1, 18, 10, 21871 }, // U+2075 + { 9, 10, 144, 0, 18, 12, 21881 }, // U+2076 + { 8, 9, 132, 0, 18, 9, 21893 }, // U+2077 + { 9, 10, 146, 0, 18, 12, 21902 }, // U+2078 + { 9, 10, 144, 0, 18, 12, 21914 }, // U+2079 + { 7, 6, 142, 1, 16, 6, 21926 }, // U+207A + { 7, 2, 142, 1, 14, 2, 21932 }, // U+207B + { 7, 5, 142, 1, 15, 5, 21934 }, // U+207C + { 4, 11, 86, 1, 18, 6, 21939 }, // U+207D + { 4, 11, 86, 0, 18, 6, 21945 }, // U+207E + { 7, 7, 141, 1, 16, 7, 21951 }, // U+207F + { 9, 10, 150, 0, 6, 12, 21958 }, // U+2080 + { 5, 9, 98, 0, 6, 6, 21970 }, // U+2081 + { 7, 9, 143, 1, 6, 8, 21976 }, // U+2082 + { 8, 10, 145, 1, 6, 10, 21984 }, // U+2083 + { 9, 9, 149, 0, 6, 11, 21994 }, // U+2084 + { 8, 10, 143, 1, 6, 10, 22005 }, // U+2085 + { 9, 10, 144, 0, 6, 12, 22015 }, // U+2086 + { 8, 9, 132, 0, 6, 9, 22027 }, // U+2087 + { 9, 10, 146, 0, 6, 12, 22036 }, // U+2088 + { 9, 10, 144, 0, 6, 12, 22048 }, // U+2089 + { 7, 6, 142, 1, 4, 6, 22060 }, // U+208A + { 7, 2, 142, 1, 2, 2, 22066 }, // U+208B + { 7, 5, 142, 1, 3, 5, 22068 }, // U+208C + { 4, 11, 86, 1, 6, 6, 22073 }, // U+208D + { 4, 11, 86, 0, 6, 6, 22079 }, // U+208E + { 8, 8, 131, 0, 4, 8, 22085 }, // U+2090 + { 8, 8, 135, 0, 4, 8, 22093 }, // U+2091 + { 8, 8, 138, 0, 4, 8, 22101 }, // U+2092 + { 8, 7, 130, 0, 4, 7, 22109 }, // U+2093 + { 8, 9, 148, 1, 4, 9, 22116 }, // U+2094 + { 7, 9, 141, 1, 6, 8, 22125 }, // U+2095 + { 7, 9, 129, 1, 6, 8, 22133 }, // U+2096 + { 2, 9, 64, 1, 6, 3, 22141 }, // U+2097 + { 11, 7, 203, 1, 4, 10, 22144 }, // U+2098 + { 7, 7, 141, 1, 4, 7, 22154 }, // U+2099 + { 8, 10, 142, 1, 4, 10, 22161 }, // U+209A + { 8, 8, 126, 0, 4, 8, 22171 }, // U+209B + { 5, 9, 82, 0, 5, 6, 22179 }, // U+209C + { 13, 17, 240, 1, 16, 28, 22185 }, // U+20A0 + { 14, 20, 243, 1, 18, 35, 22213 }, // U+20A1 + { 13, 17, 241, 1, 16, 28, 22248 }, // U+20A2 + { 13, 16, 200, -1, 16, 26, 22276 }, // U+20A3 + { 11, 16, 201, 1, 16, 22, 22302 }, // U+20A4 + { 16, 16, 283, 1, 14, 32, 22324 }, // U+20A5 + { 16, 16, 254, 0, 16, 32, 22356 }, // U+20A6 + { 19, 17, 314, 1, 16, 41, 22388 }, // U+20A7 + { 22, 17, 370, 1, 16, 47, 22429 }, // U+20A8 + { 20, 16, 315, 0, 16, 40, 22476 }, // U+20A9 + { 15, 12, 269, 1, 12, 23, 22516 }, // U+20AA + { 13, 20, 201, 0, 16, 33, 22539 }, // U+20AB + { 13, 17, 220, 0, 16, 28, 22572 }, // U+20AC + { 15, 16, 234, 0, 16, 30, 22600 }, // U+20AD + { 13, 16, 213, 0, 16, 26, 22630 }, // U+20AE + { 28, 23, 458, 0, 18, 81, 22656 }, // U+20AF + { 14, 16, 219, 0, 16, 28, 22737 }, // U+20B1 + { 14, 21, 247, 1, 18, 37, 22765 }, // U+20B2 + { 14, 16, 227, 0, 16, 28, 22802 }, // U+20B3 + { 11, 17, 208, 1, 16, 24, 22830 }, // U+20B4 + { 14, 21, 243, 1, 18, 37, 22854 }, // U+20B5 + { 13, 16, 213, 0, 16, 26, 22891 }, // U+20B8 + { 11, 16, 178, 0, 16, 22, 22917 }, // U+20B9 + { 12, 17, 195, 0, 16, 26, 22939 }, // U+20BA + { 13, 12, 240, 1, 12, 20, 22965 }, // U+20BC + { 14, 16, 231, 0, 16, 28, 22985 }, // U+20BD + { 13, 16, 239, 1, 16, 26, 23013 }, // U+20BE + { 12, 20, 217, 1, 18, 30, 23039 }, // U+20BF + { 17, 14, 316, 1, 14, 30, 23069 }, // U+2190 + { 15, 16, 276, 1, 16, 30, 23099 }, // U+2191 + { 16, 14, 316, 2, 14, 28, 23129 }, // U+2192 + { 15, 17, 276, 1, 16, 32, 23157 }, // U+2193 + { 25, 14, 445, 1, 14, 44, 23189 }, // U+2194 + { 15, 25, 276, 1, 19, 47, 23233 }, // U+2195 + { 12, 13, 255, 2, 12, 20, 23280 }, // U+2196 + { 12, 13, 255, 2, 12, 20, 23300 }, // U+2197 + { 12, 12, 255, 2, 12, 18, 23320 }, // U+2198 + { 12, 12, 255, 2, 12, 18, 23338 }, // U+2199 + { 21, 16, 379, 1, 16, 42, 23356 }, // U+21A9 + { 21, 16, 379, 1, 16, 42, 23398 }, // U+21AA + { 18, 16, 327, 1, 16, 36, 23440 }, // U+21B0 + { 17, 16, 327, 2, 16, 34, 23476 }, // U+21B1 + { 12, 15, 273, 2, 15, 23, 23510 }, // U+21B2 + { 17, 14, 327, 2, 16, 30, 23533 }, // U+21B3 + { 14, 17, 275, 2, 16, 30, 23563 }, // U+21B4 + { 18, 14, 327, 1, 16, 32, 23593 }, // U+21B5 + { 17, 13, 273, 0, 14, 28, 23625 }, // U+21B6 + { 17, 13, 273, 0, 14, 28, 23653 }, // U+21B7 + { 16, 19, 316, 2, 19, 38, 23681 }, // U+21BA + { 16, 19, 316, 2, 19, 38, 23719 }, // U+21BB + { 16, 15, 273, 1, 15, 30, 23757 }, // U+21C4 + { 16, 15, 273, 1, 15, 30, 23787 }, // U+21C6 + { 17, 15, 316, 1, 14, 32, 23817 }, // U+21D0 + { 16, 15, 316, 2, 14, 30, 23849 }, // U+21D2 + { 25, 15, 445, 1, 14, 47, 23879 }, // U+21D4 + { 10, 16, 200, 1, 16, 20, 23926 }, // U+21DE + { 10, 17, 200, 1, 16, 22, 23946 }, // U+21DF + { 19, 9, 336, 1, 12, 22, 23968 }, // U+21E4 + { 19, 9, 336, 1, 12, 22, 23990 }, // U+21E5 + { 19, 17, 341, 1, 17, 41, 24012 }, // U+21E7 + { 19, 21, 341, 1, 17, 50, 24053 }, // U+21EA + { 11, 17, 209, 1, 16, 24, 24103 }, // U+2202 + { 14, 13, 226, 0, 12, 23, 24127 }, // U+2205 + { 14, 16, 224, 0, 16, 28, 24150 }, // U+2206 + { 13, 19, 238, 1, 16, 31, 24178 }, // U+220F + { 13, 19, 209, 0, 16, 31, 24209 }, // U+2211 + { 10, 2, 218, 2, 7, 3, 24240 }, // U+2212 + { 13, 15, 49, -5, 15, 25, 24243 }, // U+2215 + { 13, 16, 207, 0, 16, 26, 24268 }, // U+221A + { 19, 9, 309, 0, 11, 22, 24294 }, // U+221E + { 9, 22, 120, -1, 17, 25, 24316 }, // U+222B + { 4, 12, 91, 1, 13, 6, 24341 }, // U+2236 + { 12, 10, 218, 1, 11, 15, 24347 }, // U+2248 + { 10, 12, 218, 2, 12, 15, 24362 }, // U+2260 + { 10, 12, 218, 2, 12, 15, 24377 }, // U+2264 + { 10, 12, 218, 2, 12, 15, 24392 }, // U+2265 + { 29, 29, 463, 0, 22, 106, 24407 }, // U+2295 + { 29, 29, 463, 0, 22, 106, 24513 }, // U+2296 + { 29, 29, 463, 0, 22, 106, 24619 }, // U+2297 + { 29, 29, 463, 0, 22, 106, 24725 }, // U+2298 + { 11, 16, 189, 0, 16, 22, 24831 }, // U+FB01 + { 12, 16, 199, 0, 16, 24, 24853 }, // U+FB02 + { 20, 17, 331, 0, 9, 43, 24877 }, // U+FB56 + { 24, 16, 364, 0, 8, 48, 24920 }, // U+FB57 + { 6, 16, 100, -1, 9, 12, 24968 }, // U+FB58 + { 9, 13, 124, 0, 6, 15, 24980 }, // U+FB59 + { 20, 16, 331, 0, 15, 40, 24995 }, // U+FB66 + { 24, 16, 364, 0, 15, 48, 25035 }, // U+FB67 + { 6, 18, 90, 0, 17, 14, 25083 }, // U+FB68 + { 9, 16, 124, 0, 15, 18, 25097 }, // U+FB69 + { 13, 18, 220, 0, 9, 30, 25115 }, // U+FB7A + { 17, 18, 240, 0, 9, 39, 25145 }, // U+FB7B + { 12, 15, 196, 0, 8, 23, 25184 }, // U+FB7C + { 15, 15, 212, 0, 8, 29, 25207 }, // U+FB7D + { 9, 20, 159, 0, 19, 23, 25236 }, // U+FB88 + { 13, 20, 177, 0, 19, 33, 25259 }, // U+FB89 + { 8, 20, 122, -1, 15, 20, 25292 }, // U+FB8A + { 11, 20, 132, -1, 15, 28, 25312 }, // U+FB8B + { 8, 21, 122, -1, 16, 21, 25340 }, // U+FB8C + { 11, 21, 132, -1, 16, 29, 25361 }, // U+FB8D + { 19, 16, 292, 0, 15, 38, 25390 }, // U+FB8E + { 22, 16, 325, 0, 15, 44, 25428 }, // U+FB8F + { 9, 16, 137, 0, 15, 18, 25472 }, // U+FB90 + { 12, 16, 169, 0, 15, 24, 25490 }, // U+FB91 + { 19, 19, 292, 0, 18, 46, 25514 }, // U+FB92 + { 22, 19, 325, 0, 18, 53, 25560 }, // U+FB93 + { 9, 19, 137, 0, 18, 22, 25613 }, // U+FB94 + { 12, 19, 169, 0, 18, 29, 25635 }, // U+FB95 + { 14, 12, 228, 0, 7, 21, 25664 }, // U+FB9E + { 17, 11, 243, 0, 6, 24, 25685 }, // U+FB9F + { 8, 10, 135, 0, 10, 10, 25709 }, // U+FBA6 + { 12, 8, 160, 0, 7, 12, 25719 }, // U+FBA7 + { 5, 13, 90, 0, 9, 9, 25731 }, // U+FBA8 + { 11, 10, 146, 0, 4, 14, 25740 }, // U+FBA9 + { 14, 12, 224, 0, 11, 21, 25754 }, // U+FBAA + { 14, 14, 205, 0, 9, 25, 25775 }, // U+FBAB + { 12, 12, 191, 0, 11, 18, 25800 }, // U+FBAC + { 12, 14, 169, 0, 9, 21, 25818 }, // U+FBAD + { 19, 11, 314, 0, 10, 27, 25839 }, // U+FBAE + { 13, 8, 183, 0, 2, 13, 25866 }, // U+FBAF + { 19, 16, 314, 0, 15, 38, 25879 }, // U+FBB0 + { 13, 13, 183, 0, 7, 22, 25917 }, // U+FBB1 + { 16, 14, 253, 0, 9, 28, 25939 }, // U+FBFC + { 17, 9, 245, 0, 4, 20, 25967 }, // U+FBFD + { 6, 13, 100, -1, 9, 10, 25987 }, // U+FBFE + { 9, 10, 124, 0, 6, 12, 25997 }, // U+FBFF + { 8, 10, 137, 0, 8, 10, 26009 }, // U+FE80 + { 8, 18, 78, -2, 18, 18, 26019 }, // U+FE81 + { 10, 20, 97, -2, 19, 25, 26037 }, // U+FE82 + { 5, 20, 78, 0, 20, 13, 26062 }, // U+FE83 + { 8, 22, 97, 0, 21, 22, 26075 }, // U+FE84 + { 9, 18, 149, 0, 13, 21, 26097 }, // U+FE85 + { 11, 18, 157, 0, 13, 25, 26118 }, // U+FE86 + { 5, 21, 79, 1, 15, 14, 26143 }, // U+FE87 + { 7, 21, 97, 1, 15, 19, 26157 }, // U+FE88 + { 16, 14, 253, 0, 9, 28, 26176 }, // U+FE89 + { 17, 14, 245, 0, 9, 30, 26204 }, // U+FE8A + { 5, 16, 90, 0, 15, 10, 26234 }, // U+FE8B + { 9, 13, 114, 0, 12, 15, 26244 }, // U+FE8C + { 3, 15, 79, 1, 15, 6, 26259 }, // U+FE8D + { 7, 16, 97, 1, 15, 14, 26265 }, // U+FE8E + { 20, 13, 331, 0, 9, 33, 26279 }, // U+FE8F + { 24, 12, 364, 0, 8, 36, 26312 }, // U+FE90 + { 5, 13, 90, 0, 9, 9, 26348 }, // U+FE91 + { 9, 10, 114, 0, 6, 12, 26357 }, // U+FE92 + { 8, 14, 135, 0, 14, 14, 26369 }, // U+FE93 + { 12, 16, 162, 0, 15, 24, 26383 }, // U+FE94 + { 20, 12, 331, 0, 11, 30, 26407 }, // U+FE95 + { 24, 12, 364, 0, 11, 36, 26437 }, // U+FE96 + { 6, 14, 90, 0, 13, 11, 26473 }, // U+FE97 + { 9, 11, 124, 0, 10, 13, 26484 }, // U+FE98 + { 20, 15, 331, 0, 14, 38, 26497 }, // U+FE99 + { 24, 15, 364, 0, 14, 45, 26535 }, // U+FE9A + { 6, 17, 90, 0, 16, 13, 26580 }, // U+FE9B + { 9, 14, 124, 0, 13, 16, 26593 }, // U+FE9C + { 13, 18, 213, 0, 9, 30, 26609 }, // U+FE9D + { 15, 18, 216, 0, 9, 34, 26639 }, // U+FE9E + { 12, 12, 196, 0, 8, 18, 26673 }, // U+FE9F + { 15, 12, 212, 0, 8, 23, 26691 }, // U+FEA0 + { 13, 18, 213, 0, 9, 30, 26714 }, // U+FEA1 + { 15, 18, 216, 0, 9, 34, 26744 }, // U+FEA2 + { 12, 9, 196, 0, 8, 14, 26778 }, // U+FEA3 + { 15, 9, 212, 0, 8, 17, 26792 }, // U+FEA4 + { 13, 22, 213, 0, 13, 36, 26809 }, // U+FEA5 + { 15, 22, 216, 0, 13, 42, 26845 }, // U+FEA6 + { 12, 13, 196, 0, 12, 20, 26887 }, // U+FEA7 + { 15, 13, 212, 0, 12, 25, 26907 }, // U+FEA8 + { 9, 11, 159, 0, 10, 13, 26932 }, // U+FEA9 + { 13, 11, 177, 0, 10, 18, 26945 }, // U+FEAA + { 9, 15, 159, 0, 14, 17, 26963 }, // U+FEAB + { 13, 15, 177, 0, 14, 25, 26980 }, // U+FEAC + { 8, 13, 122, -1, 8, 13, 27005 }, // U+FEAD + { 11, 13, 132, -1, 8, 18, 27018 }, // U+FEAE + { 8, 17, 122, -1, 12, 17, 27036 }, // U+FEAF + { 11, 17, 132, -1, 12, 24, 27053 }, // U+FEB0 + { 25, 14, 403, 0, 9, 44, 27077 }, // U+FEB1 + { 28, 13, 423, 0, 8, 46, 27121 }, // U+FEB2 + { 16, 10, 261, 0, 9, 20, 27167 }, // U+FEB3 + { 19, 9, 283, 0, 8, 22, 27187 }, // U+FEB4 + { 25, 19, 403, 0, 14, 60, 27209 }, // U+FEB5 + { 28, 19, 423, 0, 14, 67, 27269 }, // U+FEB6 + { 16, 15, 261, 0, 14, 30, 27336 }, // U+FEB7 + { 19, 15, 283, 0, 14, 36, 27366 }, // U+FEB8 + { 27, 13, 435, 0, 8, 44, 27402 }, // U+FEB9 + { 30, 13, 451, 0, 8, 49, 27446 }, // U+FEBA + { 18, 9, 301, 0, 8, 21, 27495 }, // U+FEBB + { 21, 9, 316, 0, 8, 24, 27516 }, // U+FEBC + { 27, 17, 435, 0, 12, 58, 27540 }, // U+FEBD + { 30, 17, 451, 0, 12, 64, 27598 }, // U+FEBE + { 18, 13, 301, 0, 12, 30, 27662 }, // U+FEBF + { 21, 13, 316, 0, 12, 35, 27692 }, // U+FEC0 + { 16, 16, 263, 0, 15, 32, 27727 }, // U+FEC1 + { 19, 16, 277, 0, 15, 38, 27759 }, // U+FEC2 + { 14, 16, 226, 0, 15, 28, 27797 }, // U+FEC3 + { 16, 16, 237, 0, 15, 32, 27825 }, // U+FEC4 + { 16, 16, 263, 0, 15, 32, 27857 }, // U+FEC5 + { 19, 16, 277, 0, 15, 38, 27889 }, // U+FEC6 + { 14, 16, 226, 0, 15, 28, 27927 }, // U+FEC7 + { 16, 16, 237, 0, 15, 32, 27955 }, // U+FEC8 + { 13, 20, 169, 0, 11, 33, 27987 }, // U+FEC9 + { 13, 18, 177, 0, 9, 30, 28020 }, // U+FECA + { 10, 11, 168, 0, 10, 14, 28050 }, // U+FECB + { 12, 10, 174, 0, 9, 15, 28064 }, // U+FECC + { 13, 24, 169, 0, 15, 39, 28079 }, // U+FECD + { 13, 22, 177, 0, 13, 36, 28118 }, // U+FECE + { 10, 15, 168, 0, 14, 19, 28154 }, // U+FECF + { 12, 14, 174, 0, 13, 21, 28173 }, // U+FED0 + { 20, 16, 328, 0, 15, 40, 28194 }, // U+FED1 + { 24, 14, 366, 0, 13, 42, 28234 }, // U+FED2 + { 9, 16, 149, 0, 15, 18, 28276 }, // U+FED3 + { 12, 14, 165, 0, 13, 21, 28294 }, // U+FED4 + { 16, 17, 258, 0, 13, 34, 28315 }, // U+FED5 + { 18, 17, 269, 0, 11, 39, 28349 }, // U+FED6 + { 9, 16, 149, 0, 15, 18, 28388 }, // U+FED7 + { 12, 14, 165, 0, 13, 21, 28406 }, // U+FED8 + { 18, 16, 294, 0, 15, 36, 28427 }, // U+FED9 + { 21, 16, 307, 0, 15, 42, 28463 }, // U+FEDA + { 9, 16, 137, 0, 15, 18, 28505 }, // U+FEDB + { 12, 16, 169, 0, 15, 24, 28523 }, // U+FEDC + { 14, 20, 232, 0, 15, 35, 28547 }, // U+FEDD + { 17, 20, 243, 0, 15, 43, 28582 }, // U+FEDE + { 5, 16, 87, 0, 15, 10, 28625 }, // U+FEDF + { 8, 16, 100, 0, 15, 16, 28635 }, // U+FEE0 + { 10, 17, 161, 0, 8, 22, 28651 }, // U+FEE1 + { 13, 17, 187, 0, 8, 28, 28673 }, // U+FEE2 + { 10, 9, 175, 0, 8, 12, 28701 }, // U+FEE3 + { 14, 9, 193, 0, 8, 16, 28713 }, // U+FEE4 + { 14, 16, 228, 0, 11, 28, 28729 }, // U+FEE5 + { 17, 16, 243, 0, 11, 34, 28757 }, // U+FEE6 + { 5, 14, 90, 0, 13, 9, 28791 }, // U+FEE7 + { 9, 11, 114, 0, 10, 13, 28800 }, // U+FEE8 + { 8, 10, 135, 0, 10, 10, 28813 }, // U+FEE9 + { 12, 12, 162, 0, 11, 18, 28823 }, // U+FEEA + { 12, 12, 191, 0, 11, 18, 28841 }, // U+FEEB + { 12, 14, 169, 0, 9, 21, 28859 }, // U+FEEC + { 9, 13, 149, 0, 8, 15, 28880 }, // U+FEED + { 11, 13, 157, 0, 8, 18, 28895 }, // U+FEEE + { 16, 14, 253, 0, 9, 28, 28913 }, // U+FEEF + { 17, 9, 245, 0, 4, 20, 28941 }, // U+FEF0 + { 16, 18, 253, 0, 9, 36, 28961 }, // U+FEF1 + { 17, 13, 245, 0, 4, 28, 28997 }, // U+FEF2 + { 6, 13, 100, -1, 9, 10, 29025 }, // U+FEF3 + { 9, 10, 124, 0, 6, 12, 29035 }, // U+FEF4 + { 13, 16, 194, -2, 16, 26, 29047 }, // U+FEF5 + { 16, 17, 200, -2, 16, 34, 29073 }, // U+FEF6 + { 12, 18, 194, -1, 18, 27, 29107 }, // U+FEF7 + { 14, 19, 200, 0, 18, 34, 29134 }, // U+FEF8 + { 11, 21, 194, 0, 15, 29, 29168 }, // U+FEF9 + { 14, 21, 200, 0, 15, 37, 29197 }, // U+FEFA + { 11, 15, 194, 0, 15, 21, 29234 }, // U+FEFB + { 14, 16, 200, 0, 15, 28, 29255 }, // U+FEFC }; static const EpdUnicodeInterval inter_10_regularIntervals[] = { - { 0x0, 0x7F, 0x0 }, - { 0x80, 0xFF, 0x80 }, - { 0x100, 0x148, 0x100 }, + { 0x0, 0x148, 0x0 }, { 0x14A, 0x17F, 0x149 }, { 0x1A0, 0x1A1, 0x17F }, { 0x1AF, 0x1B0, 0x181 }, @@ -2689,59 +3186,75 @@ static const EpdUnicodeInterval inter_10_regularIntervals[] = { { 0x300, 0x304, 0x1DE }, { 0x306, 0x30C, 0x1E3 }, { 0x30F, 0x30F, 0x1EA }, - { 0x313, 0x313, 0x1EB }, - { 0x315, 0x315, 0x1EC }, - { 0x31B, 0x31B, 0x1ED }, - { 0x323, 0x323, 0x1EE }, - { 0x326, 0x328, 0x1EF }, - { 0x32C, 0x32C, 0x1F2 }, - { 0x337, 0x338, 0x1F3 }, - { 0x342, 0x343, 0x1F5 }, - { 0x346, 0x36F, 0x1F7 }, - { 0x400, 0x479, 0x221 }, - { 0x480, 0x49D, 0x29B }, - { 0x4A0, 0x4FF, 0x2B9 }, - { 0x5D0, 0x5EA, 0x319 }, - { 0x1EA0, 0x1EF9, 0x334 }, - { 0x2000, 0x2027, 0x38E }, - { 0x202A, 0x2055, 0x3B6 }, - { 0x2057, 0x2057, 0x3E2 }, - { 0x205F, 0x2064, 0x3E3 }, - { 0x2066, 0x206F, 0x3E9 }, - { 0x2070, 0x2071, 0x3F3 }, - { 0x2074, 0x208E, 0x3F5 }, - { 0x2090, 0x209C, 0x410 }, - { 0x20A0, 0x20AF, 0x41D }, - { 0x20B1, 0x20B5, 0x42D }, - { 0x20B8, 0x20BA, 0x432 }, - { 0x20BC, 0x20BF, 0x435 }, - { 0x2190, 0x2199, 0x439 }, - { 0x21A9, 0x21AA, 0x443 }, - { 0x21B0, 0x21B7, 0x445 }, - { 0x21BA, 0x21BB, 0x44D }, - { 0x21C4, 0x21C4, 0x44F }, - { 0x21C6, 0x21C6, 0x450 }, - { 0x21D0, 0x21D0, 0x451 }, - { 0x21D2, 0x21D2, 0x452 }, - { 0x21D4, 0x21D4, 0x453 }, - { 0x21DE, 0x21DF, 0x454 }, - { 0x21E4, 0x21E5, 0x456 }, - { 0x21E7, 0x21E7, 0x458 }, - { 0x21EA, 0x21EA, 0x459 }, - { 0x2202, 0x2202, 0x45A }, - { 0x2205, 0x2206, 0x45B }, - { 0x220F, 0x220F, 0x45D }, - { 0x2211, 0x2212, 0x45E }, - { 0x2215, 0x2215, 0x460 }, - { 0x221A, 0x221A, 0x461 }, - { 0x221E, 0x221E, 0x462 }, - { 0x222B, 0x222B, 0x463 }, - { 0x2236, 0x2236, 0x464 }, - { 0x2248, 0x2248, 0x465 }, - { 0x2260, 0x2260, 0x466 }, - { 0x2264, 0x2265, 0x467 }, - { 0x2295, 0x2298, 0x469 }, - { 0xFB01, 0xFB02, 0x46D }, + { 0x312, 0x313, 0x1EB }, + { 0x315, 0x315, 0x1ED }, + { 0x31B, 0x31B, 0x1EE }, + { 0x323, 0x323, 0x1EF }, + { 0x326, 0x328, 0x1F0 }, + { 0x32C, 0x32C, 0x1F3 }, + { 0x337, 0x338, 0x1F4 }, + { 0x342, 0x343, 0x1F6 }, + { 0x346, 0x36F, 0x1F8 }, + { 0x400, 0x479, 0x222 }, + { 0x480, 0x49D, 0x29C }, + { 0x4A0, 0x4FF, 0x2BA }, + { 0x5D0, 0x5EA, 0x31A }, + { 0x60C, 0x60C, 0x335 }, + { 0x61B, 0x61B, 0x336 }, + { 0x61F, 0x61F, 0x337 }, + { 0x621, 0x621, 0x338 }, + { 0x640, 0x640, 0x339 }, + { 0x660, 0x669, 0x33A }, + { 0x6BA, 0x6BA, 0x344 }, + { 0x6D4, 0x6D4, 0x345 }, + { 0x6F0, 0x6F9, 0x346 }, + { 0x1EA0, 0x1EF9, 0x350 }, + { 0x2000, 0x2027, 0x3AA }, + { 0x202A, 0x2055, 0x3D2 }, + { 0x2057, 0x2057, 0x3FE }, + { 0x205F, 0x2064, 0x3FF }, + { 0x2066, 0x2071, 0x405 }, + { 0x2074, 0x208E, 0x411 }, + { 0x2090, 0x209C, 0x42C }, + { 0x20A0, 0x20AF, 0x439 }, + { 0x20B1, 0x20B5, 0x449 }, + { 0x20B8, 0x20BA, 0x44E }, + { 0x20BC, 0x20BF, 0x451 }, + { 0x2190, 0x2199, 0x455 }, + { 0x21A9, 0x21AA, 0x45F }, + { 0x21B0, 0x21B7, 0x461 }, + { 0x21BA, 0x21BB, 0x469 }, + { 0x21C4, 0x21C4, 0x46B }, + { 0x21C6, 0x21C6, 0x46C }, + { 0x21D0, 0x21D0, 0x46D }, + { 0x21D2, 0x21D2, 0x46E }, + { 0x21D4, 0x21D4, 0x46F }, + { 0x21DE, 0x21DF, 0x470 }, + { 0x21E4, 0x21E5, 0x472 }, + { 0x21E7, 0x21E7, 0x474 }, + { 0x21EA, 0x21EA, 0x475 }, + { 0x2202, 0x2202, 0x476 }, + { 0x2205, 0x2206, 0x477 }, + { 0x220F, 0x220F, 0x479 }, + { 0x2211, 0x2212, 0x47A }, + { 0x2215, 0x2215, 0x47C }, + { 0x221A, 0x221A, 0x47D }, + { 0x221E, 0x221E, 0x47E }, + { 0x222B, 0x222B, 0x47F }, + { 0x2236, 0x2236, 0x480 }, + { 0x2248, 0x2248, 0x481 }, + { 0x2260, 0x2260, 0x482 }, + { 0x2264, 0x2265, 0x483 }, + { 0x2295, 0x2298, 0x485 }, + { 0xFB01, 0xFB02, 0x489 }, + { 0xFB56, 0xFB59, 0x48B }, + { 0xFB66, 0xFB69, 0x48F }, + { 0xFB7A, 0xFB7D, 0x493 }, + { 0xFB88, 0xFB95, 0x497 }, + { 0xFB9E, 0xFB9F, 0x4A5 }, + { 0xFBA6, 0xFBB1, 0x4A7 }, + { 0xFBFC, 0xFBFF, 0x4B3 }, + { 0xFE80, 0xFEFC, 0x4B7 }, }; static const EpdKernClassEntry inter_10_regularKernLeftClasses[] = { @@ -3509,6 +4022,10 @@ static const EpdKernClassEntry inter_10_regularKernLeftClasses[] = { { 0x2264, 20 }, // U+2264 { 0x2265, 20 }, // U+2265 { 0xFB02, 145 }, // U+FB02 + { 0xFB8C, 146 }, // U+FB8C + { 0xFEAD, 146 }, // U+FEAD + { 0xFEAE, 146 }, // U+FEAE + { 0xFEAF, 146 }, // U+FEAF }; static const EpdKernClassEntry inter_10_regularKernRightClasses[] = { @@ -4281,161 +4798,181 @@ static const EpdKernClassEntry inter_10_regularKernRightClasses[] = { { 0x2260, 24 }, // U+2260 { 0x2264, 24 }, // U+2264 { 0x2265, 24 }, // U+2265 + { 0xFB8E, 141 }, // U+FB8E + { 0xFBA6, 141 }, // U+FBA6 + { 0xFBAA, 141 }, // U+FBAA + { 0xFE8D, 142 }, // U+FE8D + { 0xFEA3, 141 }, // U+FEA3 + { 0xFEA9, 143 }, // U+FEA9 + { 0xFEB1, 143 }, // U+FEB1 + { 0xFEB3, 143 }, // U+FEB3 + { 0xFEB9, 143 }, // U+FEB9 + { 0xFEBB, 143 }, // U+FEBB + { 0xFEC1, 143 }, // U+FEC1 + { 0xFEC3, 143 }, // U+FEC3 + { 0xFECB, 142 }, // U+FECB + { 0xFEDB, 141 }, // U+FEDB + { 0xFEDF, 142 }, // U+FEDF + { 0xFEE1, 141 }, // U+FEE1 + { 0xFEE3, 141 }, // U+FEE3 + { 0xFEE9, 141 }, // U+FEE9 + { 0xFEEB, 141 }, // U+FEEB }; static const int8_t inter_10_regularKernMatrix[] = { - 0, 0, 0, 0, -11, 0, 0, 0, -27, 0, -27, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, -55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 15, 15, 0, 0, -2, -2, -2, 0, 0, 0, 0, 15, 0, 0, -4, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -23, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, -21, -13, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, 0, 0, -13, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 9, -4, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -11, 0, 0, 0, -45, 0, -45, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, -3, -26, 0, -3, -32, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -4, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -25, 0, -17, -17, -13, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -36, -36, -25, 0, 0, 0, 0, 0, 0, 0, -49, -21, 0, 0, -47, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -30, 0, -8, 0, -5, -5, -7, -2, 0, 0, 0, -33, -15, 0, 0, -15, 0, -11, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, -8, -15, 0, -11, 0, 0, 0, -15, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, 0, -19, 0, -45, -45, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -4, 0, -11, -11, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -15, 0, -17, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 2, 0, 11, 0, 21, 21, 0, 0, 0, 0, 0, 0, 0, 0, 16, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -6, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -8, 0, -11, 0, -11, 0, 0, -8, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -8, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -1, 0, -9, 0, -9, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, -19, 0, 1, -15, 0, 1, 0, -42, 0, -42, 0, -5, 0, 0, -6, -19, -3, 7, -5, -3, -11, -30, 0, 0, 0, -36, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 1, -53, -15, -17, 0, 0, 0, -17, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -15, -17, 0, 0, 0, 30, -17, 0, 0, -15, 0, 0, 30, 30, 0, -17, 0, 0, 0, 0, 0, 0, 0, 30, -35, -35, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -15, 0, -23, -21, -17, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -15, -15, -15, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, -25, -25, -19, -30, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, -11, -11, -17, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 0, -11, -1, -13, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -11, 0, -13, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, -13, -33, 0, 0, 0, 0, - -26, 0, 0, 0, 0, 0, -26, -11, 0, -11, 0, 7, -2, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -21, -11, 5, 0, -11, 0, -29, -8, -25, -19, 0, -27, 0, -26, 0, 0, -1, 0, 1, 0, 0, 0, -7, 0, -23, -23, 0, 0, -11, 0, -25, -11, -11, 0, -24, -26, 0, 0, -16, -11, 0, -11, 0, 0, 0, -11, 0, 0, -1, 1, 1, 1, 0, -1, 0, -23, 0, 0, 0, 0, 0, 0, -1, -29, -29, -48, 0, 0, 1, 0, 0, 0, 0, 0, 0, -11, 0, -29, 0, 0, -7, -23, -7, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -43, -34, 0, 0, 0, -11, 0, -34, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -18, -18, -18, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 19, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -24, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -30, 0, -1, -23, 0, 0, 0, 0, 0, 0, 0, 0, -11, -7, -11, 0, 0, 0, -9, 0, 0, -11, -11, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -7, -7, -11, 0, 0, 0, 0, -11, -11, -11, -7, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 0, 0, 39, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, -10, 0, -7, 0, -2, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -10, -10, -10, 0, 0, 0, 7, 11, 0, 0, -2, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -4, 0, 0, 0, 0, -36, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, -12, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 4, 0, 0, 0, 0, -11, -13, -21, 0, 0, -36, 0, 0, -17, -53, 0, -5, 0, 0, -11, 0, -51, 0, 0, 0, 0, 0, -11, 0, 0, -13, 4, 4, 4, 28, -13, -11, -13, 0, 0, 0, 28, 28, 0, -13, 0, 0, 0, -11, 0, 4, 28, 28, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, - -30, 0, 0, 0, 0, 0, -57, -28, 0, -45, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -9, 0, 0, -9, 0, -32, 0, -23, 0, 0, -19, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -42, 0, -32, -21, -72, 0, -27, -59, 0, 0, -29, -49, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -32, 0, 0, -7, -50, -7, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -28, -13, 0, -15, -15, -11, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -11, 0, 0, -15, -13, -15, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -32, -1, 0, 0, 0, 0, 0, 0, 0, 0, -2, -4, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, -2, -2, -4, 0, 0, 0, 0, -4, 0, 0, -2, 0, 0, 0, 0, 0, -4, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -6, 0, 0, -8, 0, 0, 0, -2, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -5, 0, 0, 0, 15, -5, 0, 0, -2, 0, 0, 15, 15, 0, -5, -1, -1, -1, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -11, 0, 0, -25, -11, -25, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -29, 0, -11, -30, 0, 0, 0, 0, 0, 5, 0, 0, -19, -27, -29, -1, 0, 0, -21, -28, -27, -26, -25, -24, -23, -19, -25, 0, 0, -43, -25, 0, 0, 0, 0, -60, 0, -51, 0, -17, 0, 0, 0, -11, -41, -41, -46, 0, 0, 0, 30, -46, -46, -36, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -39, 0, 0, 42, 30, -44, -44, 0, 0, -11, 0, 0, 0, 0, -27, -21, -27, -28, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, 0, 0, 0, - 0, 0, 0, 0, -17, 0, 0, -17, -32, -17, -32, -16, -4, 0, 0, 0, -11, 0, 0, 0, 0, -21, -25, -21, 0, -22, -25, 0, -11, -32, 0, 0, 6, 0, 0, 0, 0, 0, -26, -18, -18, 0, 0, 0, -3, -15, 0, 0, 0, 0, 0, 0, -17, 0, 0, -19, -38, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, -13, -18, -31, -18, 0, 0, 0, 28, -33, 0, 0, -18, 0, 0, 28, 28, 0, -18, 0, 0, 0, 0, 0, 0, 13, 28, -41, -41, 0, 0, -11, 0, 0, 0, 0, 0, -5, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, -13, -32, 0, 0, 0, 0, 0, 0, 0, -44, -51, 0, 0, 0, 0, - 0, 0, 0, 0, -17, 0, 0, -17, -32, -17, -32, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -17, -25, -21, 0, -1, -19, 0, -1, -21, 0, 0, 0, 7, 0, 0, 0, 0, 0, -17, -17, 0, 0, 0, -10, -11, 0, 0, 0, 0, 0, 0, -17, 0, 0, -19, -17, 0, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, -17, -17, -17, 0, 0, 0, 0, -17, 0, 0, -17, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -10, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, -29, -35, 0, 0, 0, 0, - 0, 0, -4, 0, 0, 0, 0, -13, 0, -13, 0, 0, -11, 0, 0, -7, -13, 0, 0, -7, 0, 0, -19, -17, 0, -13, 0, 0, -13, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, -19, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, -13, -29, 0, -2, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, -15, -19, -19, -9, 0, 0, 0, 0, -9, 0, 0, -19, 0, 0, 0, 0, 0, -9, -10, -10, -10, 0, 0, 0, 28, 0, 0, 0, 0, -7, -30, -11, -10, 0, 0, -21, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, - 0, 0, 0, 0, -21, 0, 0, -26, -11, -26, -11, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -13, -27, 0, -13, -11, 5, 0, 0, 0, 0, 0, 0, 0, 0, -26, -27, 0, -9, 0, -15, -23, 0, -13, 0, 0, 0, 0, -26, 0, 0, -30, -43, 0, -3, 0, 0, -13, 0, -43, 0, -21, 0, 0, 0, -28, -49, -41, -27, -9, -19, -9, 0, -27, -13, 0, -42, 0, 0, 0, 0, 0, -46, 5, 5, 5, -13, 0, -9, 28, 0, 0, 0, 0, 0, -13, 0, 5, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -43, 0, 0, 0, 0, 0, 0, 0, -50, -59, 0, 0, 0, -11, - 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, -11, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -11, -13, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 28, 28, 0, 0, 0, 0, 0, 0, 0, 0, 11, 28, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 9, -4, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -26, 0, 0, 0, 0, 0, -26, -11, 0, -11, 0, 7, -2, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -21, -11, 5, 0, -11, 0, -42, -8, -41, -38, 0, -27, 0, -26, 0, 0, -1, 0, 1, 0, 0, 0, -7, 0, -40, -23, 0, 0, -11, 0, -25, -11, -11, 0, -24, -26, 0, 0, -16, -11, 0, -11, 0, 0, 0, -11, 0, 0, -1, 1, 1, 1, 0, -1, 0, -40, 0, 0, 0, 0, 0, 0, -1, -42, -42, -42, 0, 0, 1, 0, 0, 0, 0, 0, 0, -11, 0, -42, 0, 0, -7, -23, -7, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -58, -53, 0, 0, 0, -11, 0, -53, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, - 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, -15, -36, 0, -15, -19, -15, 0, -15, -15, 0, 0, 0, 0, -13, 0, 9, -13, 0, -19, -13, -26, 0, 0, 0, 0, -23, 0, 0, 0, 0, 9, 32, 9, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9, 0, 0, 0, -26, 0, 0, 9, 0, 0, 32, 0, -19, -19, -19, 0, 9, 9, 0, 0, 19, 19, 0, -15, -13, -36, -17, 15, 15, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, - -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, -17, -17, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -25, -25, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -15, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -7, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, -30, 0, -18, -17, -9, -29, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -7, -6, -8, -5, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -7, 3, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 12, 0, 0, 0, -9, 0, 0, 0, -30, 0, 0, 0, -4, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -21, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -9, -19, -9, -19, -9, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -21, 0, 0, 0, 0, 0, 3, 0, 0, -5, -5, -8, -12, 0, 0, 0, -7, -11, 0, 0, 6, 0, 0, -9, 0, 0, -13, -9, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, -7, -5, -5, -8, 0, 0, 0, 34, -8, 0, 0, -5, 0, 0, 34, 47, 0, -8, 0, 0, 0, 0, 0, 0, 15, 45, -23, -23, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, -7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -30, 0, 0, -7, -19, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 35, 23, 0, 0, 0, 0, 0, 0, 0, 0, 28, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, -17, -38, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -7, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -13, -21, -13, -21, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 3, 0, -19, -19, 0, 0, 0, 0, 0, -11, 0, 0, -1, -5, 5, 3, 4, 4, 0, 5, 0, 5, 3, 4, 0, -13, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -1, -1, -5, 3, 3, 3, 9, -5, 0, 5, -1, 0, 3, 9, 9, 4, -5, -19, -19, -19, 0, 3, 3, 0, 9, 0, 0, 0, 0, 0, 0, -19, -8, -8, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 5, -26, 0, 0, 0, -21, 5, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, - -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, -15, -11, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -25, -25, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 1, -1, 1, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -17, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -11, 0, -3, -3, 0, -5, 0, 0, 0, 2, -3, -7, 0, 3, 0, -3, -3, 0, 7, 6, 0, 0, -7, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, -4, 2, 2, -3, 0, 0, 0, 0, -3, 0, 7, 2, 0, 0, 0, 0, 3, -3, -11, -11, -11, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -3, 0, -3, -3, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -13, 2, 0, 0, 0, -4, 0, 2, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -17, -42, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -13, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -11, -16, -11, -16, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -13, 0, 0, -23, 2, 0, -21, -11, 0, 0, 0, 0, -11, 0, 0, -26, -2, -7, 5, 0, 0, 0, -1, 5, 0, 0, 0, 0, 0, -11, 0, 0, -13, -11, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -2, -2, -7, 0, 0, 0, 0, -7, 0, 0, -2, 0, 2, 0, 0, 0, -7, -11, -32, -11, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, -11, -15, -15, 5, 0, 5, -1, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 3, -26, 0, 0, 0, -41, 3, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, - 0, 0, 0, 0, -11, 0, 0, 0, -25, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, -19, -17, 0, 0, 0, 0, 0, -11, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -6, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, -26, 0, 0, 0, -25, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, -1, -8, 6, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, -7, 0, 0, -11, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -1, -1, -8, 0, 0, 0, 0, -8, 0, 0, -1, 0, 0, 0, 0, 0, -8, -21, -21, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 6, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, -5, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -4, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -25, 0, -17, -17, -24, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -36, -36, -25, 0, 0, 0, 0, 0, 0, 0, -24, -21, 0, 0, -47, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -17, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -2, -24, 0, -2, -24, 0, 0, 0, 0, -2, -3, 0, 0, 0, -4, -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, -9, 0, 0, 0, 0, -9, 0, 0, -4, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -2, 0, 0, 0, 0, 0, 0, 0, -4, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -17, -42, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -30, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -4, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -51, 0, -30, -17, -28, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -51, -51, -51, 0, 0, 0, 0, 0, 0, 0, -28, -21, 0, 0, -51, 0, 0, 0, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -17, -15, 0, 0, -17, -11, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -43, 0, -21, -19, -13, -30, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -43, -43, -43, 0, 0, 0, 0, 0, -11, -11, -30, -15, 0, 0, -66, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -28, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -11, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 13, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -30, 0, -13, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -34, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -13, -19, 0, -17, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, -42, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -18, -18, -18, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 36, 35, 0, 0, 0, 0, 0, 0, 0, 0, 24, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 23, 23, 9, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 35, 23, 35, 57, 0, 0, 0, 0, 0, 21, 57, 57, 34, 0, 30, 30, 30, 0, 46, 35, 17, 57, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, -17, -17, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -42, -42, -42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -15, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -7, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, -47, 0, -18, -17, -9, -29, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -7, -6, -8, -5, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -7, 3, 0, 0, 0, 0, 0, 0, -47, -47, -47, 0, 0, 0, 12, 0, 0, 0, -9, 0, 0, 0, -47, 0, 0, 0, -4, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -21, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -49, 0, -17, -17, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -49, -49, -49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -49, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -15, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 11, 0, 0, 0, 0, 33, 19, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 13, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 13, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 6, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 29, 17, 0, 0, 0, 0, 0, 11, 17, 17, 9, 0, 13, 13, 13, 0, 11, 30, 15, 17, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 13, 13, 0, 0, 0, 0, 0, 13, 13, 13, 13, 0, 11, 11, 11, 0, 11, 11, 13, 13, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 34, 9, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 36, 36, 36, 57, 0, 0, 0, 0, 0, 21, 57, 57, 34, 0, 30, 30, 30, 0, 46, 36, 36, 57, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 36, 9, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 36, 36, 36, 57, 0, 0, 0, 0, 0, 21, 57, 57, 36, 0, 30, 30, 30, 0, 46, 36, 17, 57, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 23, 9, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 36, 36, 36, 57, 0, 0, 0, 0, 0, 21, 57, 57, 34, 0, 30, 30, 30, 0, 46, 36, 17, 57, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 36, 35, 0, 0, 0, 0, 0, 0, 0, 0, 24, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -30, 0, 0, 0, 0, 0, -59, -45, 0, -45, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -9, 0, 0, -9, 0, -32, 0, -23, 0, 0, -19, 0, -59, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -43, 0, -32, -21, -45, 0, -27, -59, 0, 0, -29, -45, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -32, 0, 0, -7, -50, -7, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -28, -13, 0, -15, -15, -11, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 2, 0, 11, 0, 21, 21, 0, 0, 0, 0, 0, 0, 0, 0, 29, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -30, 0, 0, 0, 0, 0, -57, -42, 0, -42, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -9, 0, 0, -9, 0, -32, 0, -23, 0, 0, -19, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -40, 0, -32, -21, -42, 0, -27, -59, 0, 0, -29, -42, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -32, 0, 0, -7, -50, -7, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -28, -13, 0, -15, -15, -11, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 2, 0, 11, 0, 21, 21, 0, 0, 0, 0, 0, 0, 0, 0, 16, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -30, 0, 0, 0, 0, 0, -55, -28, 0, -45, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -9, 0, 0, -9, 0, -32, 0, -23, 0, 0, -38, 0, -55, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -42, 0, -32, -21, -72, 0, -27, -55, 0, 0, -29, -49, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -32, 0, 0, -7, -50, -7, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -28, -24, 0, -15, -15, -11, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 2, 0, 11, 0, 21, 21, 0, 0, 0, 0, 0, 0, 0, 0, 16, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -11, 0, 0, -36, -11, -36, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -29, 0, -11, -30, 0, 0, 0, 0, 0, 5, 0, 0, -19, -27, -29, -1, 0, 0, -21, -28, -27, -37, -25, -24, -47, -31, -36, 0, 0, -43, -36, 0, 0, 0, 0, -37, 0, -36, 0, -17, 0, 0, 0, -11, -41, -41, -46, 0, 0, 0, 30, -46, -37, -36, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -37, 0, 0, 42, 30, -44, -44, 0, 0, -11, 0, 0, 0, 0, -27, -21, -27, -28, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, 0, 0, 0, - 0, 0, 0, 0, -11, 0, 0, -36, -11, -36, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -29, 0, -11, -30, 0, 0, 0, 0, 0, 5, 0, 0, -19, -27, -29, -1, 0, 0, -21, -28, -27, -26, -25, -24, -23, -19, -36, 0, 0, -43, -36, 0, 0, 0, 0, -60, 0, -36, 0, -17, 0, 0, 0, -11, -41, -41, -46, 0, 0, 0, 30, -46, -46, -36, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -39, 0, 0, 42, 30, -44, -44, 0, 0, -11, 0, 0, 0, 0, -27, -21, -27, -28, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, 0, 0, 0, - 0, 0, 0, 0, -11, 0, 0, -25, -11, -25, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -48, 0, -11, -70, 0, 0, 0, 0, 0, 5, 0, 0, -19, -27, -29, -1, 0, 0, -21, -28, -27, -26, -25, -24, -23, -19, -25, 0, 0, -43, -25, 0, 0, 0, 0, -60, 0, -51, 0, -17, 0, 0, 0, -11, -41, -41, -46, 0, 0, 0, 30, -46, -46, -36, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -39, 0, 0, 42, 30, -44, -44, 0, 0, -11, 0, 0, 0, 0, -27, -21, -27, -28, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -66, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -21, -10, 0, 0, -18, -18, -9, -18, -18, -18, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, -21, 0, 0, 0, 0, -21, -18, -18, -18, 0, 0, 0, 0, 0, -21, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 14, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 10, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, -12, 0, 5, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, -5, -24, -24, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 35, 35, 0, 0, 0, 0, 0, 0, 0, 0, 39, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -7, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, -56, 0, -18, -17, -9, -29, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -7, -6, -8, -5, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -7, 3, 0, 0, 0, 0, 0, 0, -56, -56, -56, 0, 0, 0, 12, 0, 0, 0, -9, 0, 0, 0, -56, 0, 0, 0, -4, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -21, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -13, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 24, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 35, 35, 0, 0, 0, 0, 0, 0, 0, 0, 28, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 35, 36, 9, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 35, 35, 35, 57, 0, 0, 0, 0, 0, 21, 57, 57, 36, 0, 30, 30, 30, 0, 46, 35, 32, 71, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -4, 0, 0, 0, 0, -49, 0, -49, 0, 0, -11, 0, 0, -7, -13, 0, 0, -7, 0, 0, -17, -17, 0, -13, 0, 0, -13, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, -19, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -49, 0, 0, -30, -49, 0, -2, 0, 0, 0, 0, -49, 0, 0, 0, 0, 0, -15, -19, -19, -9, 0, 0, 0, 0, -9, 0, 0, -19, 0, 0, 0, 0, 0, -9, -10, -10, -10, 0, 0, 0, 28, 0, 0, 0, 0, -7, -30, -11, -10, 0, 0, -21, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 0, -11, -1, -13, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -11, 0, -30, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, -13, -33, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -9, 0, 0, 0, 0, 0, -9, -9, 5, -9, 5, 0, -4, 0, 0, 0, -8, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -5, -8, 0, 0, 0, 0, -8, 0, -7, -8, 0, 0, -9, 0, 0, 0, -9, 0, 0, -9, 0, 0, -8, -9, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, -5, 0, -7, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -5, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 5, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -13, -42, -13, -42, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 3, 0, -19, -19, 0, 0, 0, 0, 0, -11, 0, 0, -1, -5, 5, 3, 4, 4, 0, 5, 0, 5, 3, 4, 0, -13, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -1, -1, -5, 3, 3, 3, 9, -5, 0, 5, -1, 0, 3, 9, 9, 4, -5, -19, -19, -19, 0, 3, 3, 0, 9, 0, 0, 0, 0, 0, 0, -19, -8, -8, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 5, -51, 0, 0, 0, -42, 5, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, - 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, -33, 0, -9, -57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, -9, -7, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -33, -33, -33, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -33, 0, 0, -9, 0, -39, 0, -57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -21, 0, 0, -26, -11, -26, -11, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -13, -27, 0, -13, -11, 5, 0, 0, 0, 0, 0, 0, 0, 0, -26, -54, 0, -9, 0, -15, -23, 0, -13, 0, 0, 0, 0, -26, 0, 0, -30, -43, 0, -3, 0, 0, -13, 0, -43, 0, -21, 0, 0, 0, -28, -49, -41, -54, -9, -19, -9, 0, -54, -13, 0, -42, 0, 0, 0, 0, 0, -54, 5, 5, 5, -13, 0, -9, 28, 0, 0, 0, 0, 0, -13, 0, 5, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -43, 0, 0, 0, 0, 0, 0, 0, -50, -59, 0, 0, 0, -11, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -10, -10, 5, -10, 0, -10, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 2, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 35, 23, 0, 0, 0, 0, 0, 0, 0, 0, 24, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -7, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, -30, 0, -18, -17, -9, -29, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -7, -6, -8, -5, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -7, 3, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 23, 0, 0, 0, -9, 0, 0, 0, -30, 0, 0, 0, -4, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -21, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -21, 0, 0, -26, -22, -26, -22, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -13, -27, 0, -13, -11, 5, 0, 0, 0, 0, 0, 0, 0, 0, -26, -27, 0, -9, 0, -15, -23, 0, -13, 0, 0, 0, 0, -26, 0, 0, -30, -43, 0, -3, 0, 0, -13, 0, -43, 0, -21, 0, 0, 0, -28, -49, -41, -27, -9, -19, -9, 0, -27, -13, 0, -42, 0, 0, 0, 0, 0, -46, 5, 5, 5, -13, 0, -9, 28, 0, 0, 0, 0, 0, -13, 0, 5, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -22, 0, 0, 0, 0, 0, 0, 0, -50, -59, 0, 0, 0, -11, - 0, 0, 0, 0, -21, 0, 0, 0, -28, 0, -28, -32, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, -49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, -21, -38, 0, 0, 0, 0, 0, -47, -28, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, -42, 0, 0, -23, 0, 0, -21, -33, -21, -33, -47, -15, 0, 0, 0, -32, 0, -21, -13, 0, -19, -32, -19, 0, -19, -46, 0, -19, -42, 0, 0, 0, 0, 0, 0, 0, 0, -13, -19, -21, 0, 0, 0, -7, -26, 5, -13, 5, 0, 0, 0, -36, 0, 0, -28, -21, 0, 0, 0, 0, -28, -4, -21, 0, 0, -23, 0, 0, 0, -19, -19, -21, 0, 0, 0, 0, -21, -13, 5, -19, 0, 0, 0, 0, 0, -21, 0, 0, 0, -13, 0, 0, 0, 0, -17, -30, 0, 0, -19, 0, 0, -40, -21, 5, 0, 5, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, -30, -21, 0, -36, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, 0, 0, 0, 0, 0, -17, -11, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 17, 17, 0, 0, 0, -21, -49, 21, 21, -17, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -42, -32, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 0, -13, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, - -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -30, 0, -8, 0, -5, -5, -7, -2, 0, 0, 0, -33, -15, 0, 0, -15, 0, -11, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, -8, -15, 0, -11, 0, 0, 0, -15, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, 0, -19, 0, -45, -61, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 4, -2, 0, -11, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -13, 0, -42, 0, -44, -29, 0, -50, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 5, 0, -42, -42, -66, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -53, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -5, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -23, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -11, 0, 0, 0, -27, 0, -27, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, -55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 15, 15, 0, 0, -2, -2, -2, 0, 0, 0, 0, 15, 0, 0, -4, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -23, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, -21, -13, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, 0, 0, -13, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 9, -4, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -11, 0, 0, 0, -45, 0, -45, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, -3, -26, 0, -3, -32, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -4, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -25, 0, -17, -17, -13, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -36, -36, -25, 0, 0, 0, 0, 0, 0, 0, -49, -21, 0, 0, -47, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -30, 0, -8, 0, -5, -5, -7, -2, 0, 0, 0, -33, -15, 0, 0, -15, 0, -11, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, -8, -15, 0, -11, 0, 0, 0, -15, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, 0, -19, 0, -45, -45, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -4, 0, -11, -11, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -15, 0, -17, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 2, 0, 11, 0, 21, 21, 0, 0, 0, 0, 0, 0, 0, 0, 16, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -6, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -8, 0, -11, 0, -11, 0, 0, -8, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -8, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -1, 0, -9, 0, -9, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -19, 0, 1, -15, 0, 1, 0, -42, 0, -42, 0, -5, 0, 0, -6, -19, -3, 7, -5, -3, -11, -30, 0, 0, 0, -36, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 1, -53, -15, -17, 0, 0, 0, -17, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -15, -17, 0, 0, 0, 30, -17, 0, 0, -15, 0, 0, 30, 30, 0, -17, 0, 0, 0, 0, 0, 0, 0, 30, -35, -35, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -15, 0, -23, -21, -17, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -15, -15, -15, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, -25, -25, -19, -30, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, -11, -11, -17, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 0, -11, -1, -13, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -11, 0, -13, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, -13, -33, 0, 0, 0, 0, 0, 0, 0, + -26, 0, 0, 0, 0, 0, -26, -11, 0, -11, 0, 7, -2, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -21, -11, 5, 0, -11, 0, -29, -8, -25, -19, 0, -27, 0, -26, 0, 0, -1, 0, 1, 0, 0, 0, -7, 0, -23, -23, 0, 0, -11, 0, -25, -11, -11, 0, -24, -26, 0, 0, -16, -11, 0, -11, 0, 0, 0, -11, 0, 0, -1, 1, 1, 1, 0, -1, 0, -23, 0, 0, 0, 0, 0, 0, -1, -29, -29, -48, 0, 0, 1, 0, 0, 0, 0, 0, 0, -11, 0, -29, 0, 0, -7, -23, -7, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -43, -34, 0, 0, 0, -11, 0, -34, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -18, -18, -18, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 19, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -24, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -30, 0, -1, -23, 0, 0, 0, 0, 0, 0, 0, 0, -11, -7, -11, 0, 0, 0, -9, 0, 0, -11, -11, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -7, -7, -11, 0, 0, 0, 0, -11, -11, -11, -7, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 0, 0, 39, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, -10, 0, -7, 0, -2, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -10, -10, -10, 0, 0, 0, 7, 11, 0, 0, -2, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -4, 0, 0, 0, 0, -36, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, -12, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 4, 0, 0, 0, 0, -11, -13, -21, 0, 0, -36, 0, 0, -17, -53, 0, -5, 0, 0, -11, 0, -51, 0, 0, 0, 0, 0, -11, 0, 0, -13, 4, 4, 4, 28, -13, -11, -13, 0, 0, 0, 28, 28, 0, -13, 0, 0, 0, -11, 0, 4, 28, 28, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, + -30, 0, 0, 0, 0, 0, -57, -28, 0, -45, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -9, 0, 0, -9, 0, -32, 0, -23, 0, 0, -19, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -42, 0, -32, -21, -72, 0, -27, -59, 0, 0, -29, -49, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -32, 0, 0, -7, -50, -7, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -28, -13, 0, -15, -15, -11, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -11, 0, 0, -15, -13, -15, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -32, -1, 0, 0, 0, 0, 0, 0, 0, 0, -2, -4, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, -2, -2, -4, 0, 0, 0, 0, -4, 0, 0, -2, 0, 0, 0, 0, 0, -4, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -6, 0, 0, -8, 0, 0, 0, -2, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -5, 0, 0, 0, 15, -5, 0, 0, -2, 0, 0, 15, 15, 0, -5, -1, -1, -1, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -11, 0, 0, -25, -11, -25, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -29, 0, -11, -30, 0, 0, 0, 0, 0, 5, 0, 0, -19, -27, -29, -1, 0, 0, -21, -28, -27, -26, -25, -24, -23, -19, -25, 0, 0, -43, -25, 0, 0, 0, 0, -60, 0, -51, 0, -17, 0, 0, 0, -11, -41, -41, -46, 0, 0, 0, 30, -46, -46, -36, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -39, 0, 0, 42, 30, -44, -44, 0, 0, -11, 0, 0, 0, 0, -27, -21, -27, -28, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -17, 0, 0, -17, -32, -17, -32, -16, -4, 0, 0, 0, -11, 0, 0, 0, 0, -21, -25, -21, 0, -22, -25, 0, -11, -32, 0, 0, 6, 0, 0, 0, 0, 0, -26, -18, -18, 0, 0, 0, -3, -15, 0, 0, 0, 0, 0, 0, -17, 0, 0, -19, -38, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, -13, -18, -31, -18, 0, 0, 0, 28, -33, 0, 0, -18, 0, 0, 28, 28, 0, -18, 0, 0, 0, 0, 0, 0, 13, 28, -41, -41, 0, 0, -11, 0, 0, 0, 0, 0, -5, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, -13, -32, 0, 0, 0, 0, 0, 0, 0, -44, -51, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -17, 0, 0, -17, -32, -17, -32, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -17, -25, -21, 0, -1, -19, 0, -1, -21, 0, 0, 0, 7, 0, 0, 0, 0, 0, -17, -17, 0, 0, 0, -10, -11, 0, 0, 0, 0, 0, 0, -17, 0, 0, -19, -17, 0, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, -17, -17, -17, 0, 0, 0, 0, -17, 0, 0, -17, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -10, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, -29, -35, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -4, 0, 0, 0, 0, -13, 0, -13, 0, 0, -11, 0, 0, -7, -13, 0, 0, -7, 0, 0, -19, -17, 0, -13, 0, 0, -13, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, -19, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, -13, -29, 0, -2, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, -15, -19, -19, -9, 0, 0, 0, 0, -9, 0, 0, -19, 0, 0, 0, 0, 0, -9, -10, -10, -10, 0, 0, 0, 28, 0, 0, 0, 0, -7, -30, -11, -10, 0, 0, -21, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -21, 0, 0, -26, -11, -26, -11, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -13, -27, 0, -13, -11, 5, 0, 0, 0, 0, 0, 0, 0, 0, -26, -27, 0, -9, 0, -15, -23, 0, -13, 0, 0, 0, 0, -26, 0, 0, -30, -43, 0, -3, 0, 0, -13, 0, -43, 0, -21, 0, 0, 0, -28, -49, -41, -27, -9, -19, -9, 0, -27, -13, 0, -42, 0, 0, 0, 0, 0, -46, 5, 5, 5, -13, 0, -9, 28, 0, 0, 0, 0, 0, -13, 0, 5, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -43, 0, 0, 0, 0, 0, 0, 0, -50, -59, 0, 0, 0, -11, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, -11, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -11, -13, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 28, 28, 0, 0, 0, 0, 0, 0, 0, 0, 11, 28, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 9, -4, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -26, 0, 0, 0, 0, 0, -26, -11, 0, -11, 0, 7, -2, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -21, -11, 5, 0, -11, 0, -42, -8, -41, -38, 0, -27, 0, -26, 0, 0, -1, 0, 1, 0, 0, 0, -7, 0, -40, -23, 0, 0, -11, 0, -25, -11, -11, 0, -24, -26, 0, 0, -16, -11, 0, -11, 0, 0, 0, -11, 0, 0, -1, 1, 1, 1, 0, -1, 0, -40, 0, 0, 0, 0, 0, 0, -1, -42, -42, -42, 0, 0, 1, 0, 0, 0, 0, 0, 0, -11, 0, -42, 0, 0, -7, -23, -7, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -58, -53, 0, 0, 0, -11, 0, -53, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, -15, -36, 0, -15, -19, -15, 0, -15, -15, 0, 0, 0, 0, -13, 0, 9, -13, 0, -19, -13, -26, 0, 0, 0, 0, -23, 0, 0, 0, 0, 9, 32, 9, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9, 0, 0, 0, -26, 0, 0, 9, 0, 0, 32, 0, -19, -19, -19, 0, 9, 9, 0, 0, 19, 19, 0, -15, -13, -36, -17, 15, 15, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, + -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, -17, -17, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -25, -25, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -15, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -7, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, -30, 0, -18, -17, -9, -29, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -7, -6, -8, -5, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -7, 3, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 12, 0, 0, 0, -9, 0, 0, 0, -30, 0, 0, 0, -4, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -21, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -9, -19, -9, -19, -9, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -21, 0, 0, 0, 0, 0, 3, 0, 0, -5, -5, -8, -12, 0, 0, 0, -7, -11, 0, 0, 6, 0, 0, -9, 0, 0, -13, -9, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, -7, -5, -5, -8, 0, 0, 0, 34, -8, 0, 0, -5, 0, 0, 34, 47, 0, -8, 0, 0, 0, 0, 0, 0, 15, 45, -23, -23, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, -7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -30, 0, 0, -7, -19, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 35, 23, 0, 0, 0, 0, 0, 0, 0, 0, 28, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, -17, -38, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -7, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -13, -21, -13, -21, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 3, 0, -19, -19, 0, 0, 0, 0, 0, -11, 0, 0, -1, -5, 5, 3, 4, 4, 0, 5, 0, 5, 3, 4, 0, -13, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -1, -1, -5, 3, 3, 3, 9, -5, 0, 5, -1, 0, 3, 9, 9, 4, -5, -19, -19, -19, 0, 3, 3, 0, 9, 0, 0, 0, 0, 0, 0, -19, -8, -8, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 5, -26, 0, 0, 0, -21, 5, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, + -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, -15, -11, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -25, -25, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 1, -1, 1, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -17, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -11, 0, -3, -3, 0, -5, 0, 0, 0, 2, -3, -7, 0, 3, 0, -3, -3, 0, 7, 6, 0, 0, -7, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, -4, 2, 2, -3, 0, 0, 0, 0, -3, 0, 7, 2, 0, 0, 0, 0, 3, -3, -11, -11, -11, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -3, 0, -3, -3, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -13, 2, 0, 0, 0, -4, 0, 2, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -17, -42, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -13, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -11, -16, -11, -16, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -13, 0, 0, -23, 2, 0, -21, -11, 0, 0, 0, 0, -11, 0, 0, -26, -2, -7, 5, 0, 0, 0, -1, 5, 0, 0, 0, 0, 0, -11, 0, 0, -13, -11, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -2, -2, -7, 0, 0, 0, 0, -7, 0, 0, -2, 0, 2, 0, 0, 0, -7, -11, -32, -11, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, -11, -15, -15, 5, 0, 5, -1, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 3, -26, 0, 0, 0, -41, 3, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -11, 0, 0, 0, -25, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, -19, -17, 0, 0, 0, 0, 0, -11, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -6, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, -26, 0, 0, 0, -25, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, -1, -8, 6, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, -7, 0, 0, -11, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -1, -1, -8, 0, 0, 0, 0, -8, 0, 0, -1, 0, 0, 0, 0, 0, -8, -21, -21, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 6, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, -5, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -4, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -25, 0, -17, -17, -24, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -36, -36, -25, 0, 0, 0, 0, 0, 0, 0, -24, -21, 0, 0, -47, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -17, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -2, -24, 0, -2, -24, 0, 0, 0, 0, -2, -3, 0, 0, 0, -4, -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, -9, 0, 0, 0, 0, -9, 0, 0, -4, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -2, 0, 0, 0, 0, 0, 0, 0, -4, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -17, -42, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -30, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -4, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -15, -51, 0, -30, -17, -28, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -51, -51, -51, 0, 0, 0, 0, 0, 0, 0, -28, -21, 0, 0, -51, 0, 0, 0, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -17, -15, 0, 0, -17, -11, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -43, 0, -21, -19, -13, -30, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -43, -43, -43, 0, 0, 0, 0, 0, -11, -11, -30, -15, 0, 0, -66, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -28, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -11, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 13, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -30, 0, -13, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -34, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -13, -19, 0, -17, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -19, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, -42, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, -18, -18, -18, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 36, 35, 0, 0, 0, 0, 0, 0, 0, 0, 24, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 23, 23, 9, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 35, 23, 35, 57, 0, 0, 0, 0, 0, 21, 57, 57, 34, 0, 30, 30, 30, 0, 46, 35, 17, 57, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, -17, -17, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -42, -42, -42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -15, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -7, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, -47, 0, -18, -17, -9, -29, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -7, -6, -8, -5, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -7, 3, 0, 0, 0, 0, 0, 0, -47, -47, -47, 0, 0, 0, 12, 0, 0, 0, -9, 0, 0, 0, -47, 0, 0, 0, -4, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -21, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -49, 0, -17, -17, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -49, -49, -49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -49, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -15, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 11, 0, 0, 0, 0, 33, 19, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 13, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 13, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 6, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 29, 17, 0, 0, 0, 0, 0, 11, 17, 17, 9, 0, 13, 13, 13, 0, 11, 30, 15, 17, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 13, 13, 0, 0, 0, 0, 0, 13, 13, 13, 13, 0, 11, 11, 11, 0, 11, 11, 13, 13, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 34, 9, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 36, 36, 36, 57, 0, 0, 0, 0, 0, 21, 57, 57, 34, 0, 30, 30, 30, 0, 46, 36, 36, 57, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 36, 9, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 36, 36, 36, 57, 0, 0, 0, 0, 0, 21, 57, 57, 36, 0, 30, 30, 30, 0, 46, 36, 17, 57, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 36, 23, 9, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 36, 36, 36, 57, 0, 0, 0, 0, 0, 21, 57, 57, 34, 0, 30, 30, 30, 0, 46, 36, 17, 57, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 36, 35, 0, 0, 0, 0, 0, 0, 0, 0, 24, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -30, 0, 0, 0, 0, 0, -59, -45, 0, -45, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -9, 0, 0, -9, 0, -32, 0, -23, 0, 0, -19, 0, -59, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -43, 0, -32, -21, -45, 0, -27, -59, 0, 0, -29, -45, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -32, 0, 0, -7, -50, -7, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -28, -13, 0, -15, -15, -11, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 2, 0, 11, 0, 21, 21, 0, 0, 0, 0, 0, 0, 0, 0, 29, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -30, 0, 0, 0, 0, 0, -57, -42, 0, -42, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -9, 0, 0, -9, 0, -32, 0, -23, 0, 0, -19, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -40, 0, -32, -21, -42, 0, -27, -59, 0, 0, -29, -42, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -32, 0, 0, -7, -50, -7, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -28, -13, 0, -15, -15, -11, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 2, 0, 11, 0, 21, 21, 0, 0, 0, 0, 0, 0, 0, 0, 16, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -30, 0, 0, 0, 0, 0, -55, -28, 0, -45, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -9, 0, 0, -9, 0, -32, 0, -23, 0, 0, -38, 0, -55, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -23, 0, 0, 0, -42, 0, -32, -21, -72, 0, -27, -55, 0, 0, -29, -49, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -32, 0, 0, -7, -50, -7, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -28, -24, 0, -15, -15, -11, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 2, 0, 11, 0, 21, 21, 0, 0, 0, 0, 0, 0, 0, 0, 16, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -11, 0, 0, -36, -11, -36, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -29, 0, -11, -30, 0, 0, 0, 0, 0, 5, 0, 0, -19, -27, -29, -1, 0, 0, -21, -28, -27, -37, -25, -24, -47, -31, -36, 0, 0, -43, -36, 0, 0, 0, 0, -37, 0, -36, 0, -17, 0, 0, 0, -11, -41, -41, -46, 0, 0, 0, 30, -46, -37, -36, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -37, 0, 0, 42, 30, -44, -44, 0, 0, -11, 0, 0, 0, 0, -27, -21, -27, -28, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -11, 0, 0, -36, -11, -36, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -29, 0, -11, -30, 0, 0, 0, 0, 0, 5, 0, 0, -19, -27, -29, -1, 0, 0, -21, -28, -27, -26, -25, -24, -23, -19, -36, 0, 0, -43, -36, 0, 0, 0, 0, -60, 0, -36, 0, -17, 0, 0, 0, -11, -41, -41, -46, 0, 0, 0, 30, -46, -46, -36, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -39, 0, 0, 42, 30, -44, -44, 0, 0, -11, 0, 0, 0, 0, -27, -21, -27, -28, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -42, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -11, 0, 0, -25, -11, -25, -11, -43, -11, 0, 0, 0, -23, 0, -26, 0, 0, -11, -26, -11, 0, -11, -48, 0, -11, -70, 0, 0, 0, 0, 0, 5, 0, 0, -19, -27, -29, -1, 0, 0, -21, -28, -27, -26, -25, -24, -23, -19, -25, 0, 0, -43, -25, 0, 0, 0, 0, -60, 0, -51, 0, -17, 0, 0, 0, -11, -41, -41, -46, 0, 0, 0, 30, -46, -46, -36, -27, 0, 0, 30, 30, 0, -29, 0, 0, 0, -39, 0, 0, 42, 30, -44, -44, 0, 0, -11, 0, 0, 0, 0, -27, -21, -27, -28, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, -40, 0, 0, -11, -40, 0, 0, 0, 0, 0, 0, 0, 0, -66, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -21, -10, 0, 0, -18, -18, -9, -18, -18, -18, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, -21, 0, 0, 0, 0, -21, -18, -18, -18, 0, 0, 0, 0, 0, -21, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 14, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 10, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, -12, 0, 5, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, -5, -24, -24, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 35, 35, 0, 0, 0, 0, 0, 0, 0, 0, 39, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -7, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, -56, 0, -18, -17, -9, -29, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -7, -6, -8, -5, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -7, 3, 0, 0, 0, 0, 0, 0, -56, -56, -56, 0, 0, 0, 12, 0, 0, 0, -9, 0, 0, 0, -56, 0, 0, 0, -4, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -21, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, -19, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -13, 5, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 24, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 35, 35, 0, 0, 0, 0, 0, 0, 0, 0, 28, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 30, 0, 28, 0, 0, 0, 30, 0, 0, 0, 0, 13, 35, 36, 9, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 35, 35, 35, 57, 0, 0, 0, 0, 0, 21, 57, 57, 36, 0, 30, 30, 30, 0, 46, 35, 32, 71, 0, 0, 0, 0, 0, 0, 30, 0, 0, 17, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -4, 0, 0, 0, 0, -49, 0, -49, 0, 0, -11, 0, 0, -7, -13, 0, 0, -7, 0, 0, -17, -17, 0, -13, 0, 0, -13, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, -19, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -49, 0, 0, -30, -49, 0, -2, 0, 0, 0, 0, -49, 0, 0, 0, 0, 0, -15, -19, -19, -9, 0, 0, 0, 0, -9, 0, 0, -19, 0, 0, 0, 0, 0, -9, -10, -10, -10, 0, 0, 0, 28, 0, 0, 0, 0, -7, -30, -11, -10, 0, 0, -21, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 0, -11, -1, -13, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -11, 0, -30, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, -13, -33, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -9, 0, 0, 0, 0, 0, -9, -9, 5, -9, 5, 0, -4, 0, 0, 0, -8, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -5, -8, 0, 0, 0, 0, -8, 0, -7, -8, 0, 0, -9, 0, 0, 0, -9, 0, 0, -9, 0, 0, -8, -9, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, -5, 0, -7, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -5, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 5, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -13, -42, -13, -42, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 3, 0, -19, -19, 0, 0, 0, 0, 0, -11, 0, 0, -1, -5, 5, 3, 4, 4, 0, 5, 0, 5, 3, 4, 0, -13, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -1, -1, -5, 3, 3, 3, 9, -5, 0, 5, -1, 0, 3, 9, 9, 4, -5, -19, -19, -19, 0, 3, 3, 0, 9, 0, 0, 0, 0, 0, 0, -19, -8, -8, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 5, -51, 0, 0, 0, -42, 5, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, -33, 0, -9, -57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, -9, -7, 0, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -33, -33, -33, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -33, 0, 0, -9, 0, -39, 0, -57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -21, 0, 0, -26, -11, -26, -11, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -13, -27, 0, -13, -11, 5, 0, 0, 0, 0, 0, 0, 0, 0, -26, -54, 0, -9, 0, -15, -23, 0, -13, 0, 0, 0, 0, -26, 0, 0, -30, -43, 0, -3, 0, 0, -13, 0, -43, 0, -21, 0, 0, 0, -28, -49, -41, -54, -9, -19, -9, 0, -54, -13, 0, -42, 0, 0, 0, 0, 0, -54, 5, 5, 5, -13, 0, -9, 28, 0, 0, 0, 0, 0, -13, 0, 5, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -43, 0, 0, 0, 0, 0, 0, 0, -50, -59, 0, 0, 0, -11, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -10, -10, 5, -10, 0, -10, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 2, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 35, 23, 0, 0, 0, 0, 0, 0, 0, 0, 24, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -7, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, -30, 0, -18, -17, -9, -29, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, -7, -6, -8, -5, 0, 0, -11, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, -7, 3, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 23, 0, 0, 0, -9, 0, 0, 0, -30, 0, 0, 0, -4, 0, 0, -58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -21, -8, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -21, 0, 0, -26, -22, -26, -22, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, -21, -34, -32, 0, -13, -27, 0, -13, -11, 5, 0, 0, 0, 0, 0, 0, 0, 0, -26, -27, 0, -9, 0, -15, -23, 0, -13, 0, 0, 0, 0, -26, 0, 0, -30, -43, 0, -3, 0, 0, -13, 0, -43, 0, -21, 0, 0, 0, -28, -49, -41, -27, -9, -19, -9, 0, -27, -13, 0, -42, 0, 0, 0, 0, 0, -46, 5, 5, 5, -13, 0, -9, 28, 0, 0, 0, 0, 0, -13, 0, 5, 0, 0, 0, -19, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -13, -22, 0, 0, 0, 0, 0, 0, 0, -50, -59, 0, 0, 0, -11, 0, 0, 0, + 0, 0, 0, 0, -21, 0, 0, 0, -28, 0, -28, -32, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, -49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, -21, -38, 0, 0, 0, 0, 0, -47, -28, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -42, 0, 0, -23, 0, 0, -21, -33, -21, -33, -47, -15, 0, 0, 0, -32, 0, -21, -13, 0, -19, -32, -19, 0, -19, -46, 0, -19, -42, 0, 0, 0, 0, 0, 0, 0, 0, -13, -19, -21, 0, 0, 0, -7, -26, 5, -13, 5, 0, 0, 0, -36, 0, 0, -28, -21, 0, 0, 0, 0, -28, -4, -21, 0, 0, -23, 0, 0, 0, -19, -19, -21, 0, 0, 0, 0, -21, -13, 5, -19, 0, 0, 0, 0, 0, -21, 0, 0, 0, -13, 0, 0, 0, 0, -17, -30, 0, 0, -19, 0, 0, -40, -21, 5, 0, 5, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, -30, -21, 0, -36, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, 0, 0, 0, 0, 0, 0, 0, -17, -11, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 17, 17, 0, 0, 0, -21, -49, 21, 21, -17, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -42, -32, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 0, -13, 0, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, + -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -30, 0, -8, 0, -5, -5, -7, -2, 0, 0, 0, -33, -15, 0, 0, -15, 0, -11, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -11, -11, 0, 0, 0, 0, 0, 0, 0, 0, -8, -15, 0, -11, 0, 0, 0, -15, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, 0, -19, 0, -45, -61, 0, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 4, -2, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -13, 0, -42, 0, -44, -29, 0, -50, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 5, 0, -42, -42, -66, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -53, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -5, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -23, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -8, -10, }; static constexpr EpdFontData inter_10_regular = { inter_10_regularBitmaps, inter_10_regularGlyphs, inter_10_regularIntervals, - 63, + 77, 25, 21, -6, @@ -4446,10 +4983,10 @@ static constexpr EpdFontData inter_10_regular = { inter_10_regularKernLeftClasses, inter_10_regularKernRightClasses, inter_10_regularKernMatrix, - 764, - 769, - 145, - 140, + 768, + 788, + 146, + 143, nullptr, 0, }; diff --git a/lib/EpdFont/builtinFonts/inter_12_bold.h b/lib/EpdFont/builtinFonts/inter_12_bold.h index 8412b6ac95..19f1308f79 100644 --- a/lib/EpdFont/builtinFonts/inter_12_bold.h +++ b/lib/EpdFont/builtinFonts/inter_12_bold.h @@ -3,12 +3,12 @@ * name: inter_12_bold * size: 12 * mode: 1-bit - * Command used: fontconvert.py inter_12_bold 12 ../builtinFonts/source/Inter/Inter-Bold.ttf ../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-Bold.ttf --additional-intervals 0x05D0,0x05EA + * Command used: fontconvert.py inter_12_bold 12 ../builtinFonts/source/Inter/Inter-Bold.ttf ../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-Bold.ttf ../builtinFonts/source/NotoSansArabic/NotoSansArabic-Bold.ttf --additional-intervals 0x05D0,0x05EA --additional-intervals 0x060C,0x060C --additional-intervals 0x061B,0x061B --additional-intervals 0x061F,0x061F --additional-intervals 0x0621,0x0621 --additional-intervals 0x0640,0x0640 --additional-intervals 0x0660,0x0669 --additional-intervals 0x06BA,0x06BA --additional-intervals 0x06D4,0x06D4 --additional-intervals 0x06F0,0x06F9 --additional-intervals 0xFB56,0xFB59 --additional-intervals 0xFB66,0xFB69 --additional-intervals 0xFB7A,0xFB7D --additional-intervals 0xFB88,0xFB95 --additional-intervals 0xFB9E,0xFB9F --additional-intervals 0xFBA6,0xFBB1 --additional-intervals 0xFBFC,0xFBFF --additional-intervals 0xFE80,0xFEFC */ #pragma once #include "EpdFontData.h" -static const uint8_t inter_12_boldBitmaps[37113] = { +static const uint8_t inter_12_boldBitmaps[45320] = { 0x79, 0xE7, 0x9E, 0x79, 0xE7, 0x9E, 0x79, 0xE7, 0x9E, 0x79, 0xE0, 0x1E, 0x7B, 0xE7, 0x8C, 0xF3, 0xBD, 0xEF, 0x7B, 0xDE, 0xF7, 0xBD, 0xEF, 0x7B, 0xCE, 0x63, 0x80, 0x07, 0x1C, 0x0F, 0x1C, 0x0F, 0x1C, 0x0F, 0x1C, 0x0E, 0x3C, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x3F, 0xFE, 0x1E, 0x38, 0x1E, @@ -993,1342 +993,1855 @@ static const uint8_t inter_12_boldBitmaps[37113] = { 0x3C, 0x71, 0xE3, 0x80, 0x1F, 0x07, 0xF1, 0xEF, 0x3C, 0xF0, 0x39, 0xDF, 0xF7, 0xF9, 0x9C, 0x7F, 0xFF, 0xFF, 0x00, 0xE3, 0xFF, 0xDF, 0xC7, 0xC0, 0x77, 0xFE, 0xE0, 0xF3, 0xBD, 0xFF, 0x7D, 0x8E, 0xFF, 0xCE, 0xE0, 0x38, 0xFF, 0xBF, 0x3F, 0xEF, 0x8E, 0x00, 0x18, 0xC3, 0xDE, 0x3D, 0xE7, 0xBC, - 0x73, 0x87, 0x38, 0x7B, 0xE7, 0xF8, 0x7E, 0x0F, 0x80, 0x7B, 0xCF, 0x78, 0xF7, 0x0E, 0xF0, 0x03, - 0xDE, 0xF7, 0xBD, 0xDC, 0x00, 0x03, 0xDE, 0xF7, 0xBD, 0xDC, 0x00, 0x7B, 0xDC, 0xEF, 0x71, 0x80, - 0x77, 0xFE, 0xE0, 0x77, 0x7F, 0xE6, 0x61, 0x87, 0x8F, 0x7F, 0xE0, 0x00, 0x38, 0xF9, 0xE7, 0xC7, - 0xCF, 0xC6, 0x00, 0x7B, 0xE7, 0xF8, 0x7E, 0x0F, 0x80, 0x00, 0x60, 0x0E, 0x00, 0xC0, 0x1C, 0x01, - 0x80, 0x38, 0x03, 0x00, 0x70, 0x06, 0x00, 0xE0, 0x0C, 0x01, 0xC0, 0x18, 0x03, 0x80, 0x30, 0x07, - 0x00, 0x60, 0x00, 0x00, 0x0C, 0x00, 0x38, 0x00, 0x60, 0x01, 0xC0, 0x07, 0x00, 0x0C, 0x00, 0x38, - 0x00, 0xE0, 0x01, 0xC0, 0x07, 0x00, 0x0C, 0x00, 0x38, 0x00, 0xE0, 0x01, 0x80, 0x07, 0x00, 0x0C, - 0x00, 0x38, 0x00, 0xE0, 0x01, 0x80, 0x07, 0x00, 0x00, 0x39, 0xDF, 0xF7, 0xF9, 0x9C, 0x7B, 0xDD, - 0xEF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0xE1, 0xF8, 0x70, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, - 0xFF, 0xFE, 0xE7, 0xDF, 0xBF, 0x7E, 0xFD, 0xC0, 0x00, 0xFF, 0xFF, 0xCF, 0x3C, 0x60, 0x02, 0x07, - 0xFD, 0xFF, 0xBF, 0xE6, 0xF8, 0x18, 0x00, 0x1E, 0x0F, 0x81, 0xE0, 0x76, 0x7F, 0xFF, 0xFE, 0xFB, - 0x78, 0x1E, 0x0F, 0x81, 0xE0, 0x00, 0x1F, 0xFF, 0xFF, 0x9E, 0x7F, 0xFF, 0xFE, 0x78, 0x19, 0x87, - 0x1C, 0xFF, 0xEF, 0xFF, 0x70, 0xE3, 0x9C, 0x00, 0xC7, 0xBF, 0xF4, 0xC3, 0x00, 0x80, 0x3C, 0x0F, - 0xC3, 0xFE, 0x1F, 0xC3, 0xF7, 0xFF, 0xFC, 0xF8, 0x30, 0x00, 0x37, 0x66, 0x70, 0x1F, 0x1F, 0xE7, - 0xFB, 0xCF, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x01, 0xDB, 0xF3, 0xC7, 0xDF, 0x90, 0x00, 0x00, 0x1D, - 0xFF, 0xCF, 0xC7, 0xC1, 0x80, 0x01, 0xE3, 0xF9, 0xF7, 0xFF, 0x30, 0x00, 0x00, 0x31, 0xE0, 0x71, - 0xF9, 0xE0, 0xFB, 0xC7, 0xFF, 0xDF, 0x99, 0x98, 0x33, 0x00, 0xEF, 0x73, 0x7F, 0xC0, 0x77, 0xFE, - 0xE0, 0x00, 0xCF, 0xFF, 0x39, 0xE4, 0x80, 0x00, 0x0F, 0xFB, 0xFF, 0x66, 0x6F, 0xFC, 0xFF, 0x80, - 0x31, 0xC7, 0xFF, 0xFC, 0x63, 0x80, 0x70, 0x00, 0xE7, 0x80, 0x1E, 0x3F, 0xFF, 0xC1, 0xFF, 0xF8, - 0x07, 0xFE, 0x00, 0xE0, 0x01, 0xFF, 0x00, 0x79, 0xFF, 0xFF, 0x1F, 0xFF, 0x80, 0x7F, 0xC0, 0x7F, - 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xC0, 0x01, 0xDF, 0xFF, - 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0x80, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x01, 0xFF, - 0xC0, 0xFF, 0xFE, 0x3F, 0x07, 0xEF, 0x00, 0x1F, 0xC0, 0x01, 0xC0, 0x00, 0x00, 0xE0, 0x00, 0x03, - 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x78, 0x00, 0x03, 0x80, 0x79, 0xFB, 0xB3, 0xEF, - 0xDB, 0xBF, 0x3E, 0x39, 0xFB, 0xBF, 0xFF, 0xFD, 0xBF, 0x1C, 0x1F, 0xEF, 0xFF, 0xFF, 0x00, 0x1C, - 0x7E, 0x7F, 0x67, 0x67, 0x77, 0x7E, 0x3C, 0xCD, 0x9F, 0x3E, 0x7C, 0xFF, 0xFF, 0xBE, 0x39, 0xFB, - 0xB6, 0x2C, 0x1D, 0xBF, 0x1C, 0x06, 0x07, 0x07, 0x3F, 0x7F, 0x77, 0x67, 0x67, 0x7F, 0x7F, 0x3E, - 0xC1, 0x83, 0x07, 0xCF, 0xDF, 0xB3, 0xE7, 0xCF, 0x9F, 0x30, 0x7D, 0xC7, 0xFE, 0x7F, 0xE6, 0x67, - 0x66, 0x76, 0x67, 0x66, 0x76, 0x66, 0x7F, 0xFF, 0xCE, 0x73, 0x8C, 0x23, 0xBF, 0xF7, 0x39, 0xCE, - 0x79, 0xC0, 0x67, 0x76, 0x7E, 0x3E, 0x3C, 0x3C, 0x1C, 0x18, 0xEF, 0xF9, 0xF3, 0xC7, 0x8F, 0xBB, - 0xE7, 0x1E, 0x00, 0x70, 0x03, 0xC0, 0x0E, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x3E, 0x01, - 0xF0, 0x0F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x3C, 0xE1, 0xEF, 0x8F, 0x7C, 0x39, 0xC0, 0x00, - 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7F, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, - 0xFF, 0x07, 0xFF, 0xF8, 0x3F, 0xFF, 0xC1, 0xFF, 0xFE, 0x00, 0x3E, 0x00, 0x01, 0xF0, 0x00, 0x0F, - 0x80, 0x00, 0x7C, 0xC0, 0x03, 0xFF, 0xC0, 0x1F, 0xFF, 0x00, 0xFF, 0xFC, 0x07, 0xFF, 0xE0, 0x3E, - 0x0F, 0x81, 0xF0, 0x7C, 0x0F, 0x83, 0xE0, 0x7C, 0x1F, 0x03, 0xE1, 0xF0, 0x1F, 0x7F, 0x80, 0xFB, - 0xF8, 0x00, 0x1F, 0x80, 0x00, 0xF0, 0x00, 0x01, 0xE0, 0x1E, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x3F, - 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, - 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0x03, 0xE0, - 0x07, 0xFC, 0x07, 0xFF, 0x87, 0xFF, 0xC7, 0xE3, 0xF7, 0xE0, 0x7B, 0xE0, 0x3F, 0xF0, 0x00, 0xFF, - 0xE0, 0x7F, 0xF0, 0x3F, 0xF8, 0x1F, 0xF8, 0x0F, 0x80, 0x77, 0xC0, 0x7D, 0xF0, 0x7C, 0xFE, 0xFE, - 0x3F, 0xFE, 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, 0x00, 0x07, 0xC0, 0x3F, 0xE1, 0xFF, 0xE3, 0xFF, - 0xEF, 0x87, 0xDF, 0x07, 0xBE, 0x00, 0x7E, 0x00, 0x7F, 0xC0, 0x7F, 0xE0, 0x7F, 0xF0, 0x1F, 0xE0, - 0x07, 0xFE, 0x07, 0xFC, 0x0F, 0xFE, 0x3E, 0xFF, 0xFC, 0xFF, 0xF0, 0x7F, 0xC0, 0x3C, 0x00, 0x77, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x73, 0xCF, 0x79, 0xEF, 0x38, - 0xE0, 0x00, 0x1C, 0x07, 0xC0, 0xF8, 0x1F, 0x03, 0xE0, 0x7C, 0x0F, 0x81, 0xF0, 0x3E, 0x07, 0xC0, - 0xF8, 0x1F, 0x03, 0xE0, 0x7C, 0x0F, 0x81, 0xF0, 0x3E, 0x07, 0xC0, 0xF8, 0x00, 0x78, 0x03, 0xC0, - 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF7, - 0x87, 0xFC, 0x3F, 0xE1, 0xEF, 0x9F, 0x7F, 0xF9, 0xFF, 0x87, 0xF8, 0x0E, 0x00, 0x07, 0xFF, 0x80, - 0x01, 0xFF, 0xF0, 0x00, 0x3F, 0xFE, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0xF0, 0x78, 0x00, 0x1E, 0x0F, - 0x00, 0x03, 0xC1, 0xE0, 0x00, 0x78, 0x3F, 0xF0, 0x0F, 0x07, 0xFF, 0x81, 0xE0, 0xFF, 0xF8, 0x3C, - 0x1F, 0xFF, 0x8F, 0x83, 0xC1, 0xF1, 0xF0, 0x78, 0x1E, 0x3E, 0x0F, 0x03, 0xC7, 0x81, 0xE0, 0xF9, - 0xF0, 0x3C, 0x3F, 0xFE, 0x07, 0xFF, 0xDF, 0x80, 0xFF, 0xF3, 0xE0, 0x1F, 0xFC, 0x00, 0x70, 0x0F, - 0x00, 0x1F, 0x01, 0xE0, 0x03, 0xE0, 0x3C, 0x00, 0x7C, 0x07, 0x80, 0x0F, 0x80, 0xF0, 0x01, 0xF0, - 0x1E, 0x00, 0x3E, 0x03, 0xC0, 0x07, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xF3, - 0xFF, 0xFF, 0xFF, 0x7C, 0x07, 0x81, 0xEF, 0x80, 0xF0, 0x3D, 0xF0, 0x1E, 0x07, 0xBE, 0x03, 0xC0, - 0xF7, 0xC0, 0x78, 0x7E, 0xF8, 0x0F, 0xFF, 0x9F, 0x01, 0xFF, 0xF3, 0xE0, 0x3F, 0xF8, 0x00, 0x7F, - 0xFF, 0x07, 0xFF, 0xFC, 0x3F, 0xFF, 0xE1, 0xFF, 0xFF, 0x00, 0x3E, 0x00, 0x01, 0xF0, 0x00, 0x0F, - 0x80, 0x00, 0x7C, 0xE0, 0x03, 0xFF, 0xE0, 0x1F, 0xFF, 0x80, 0xFF, 0xFC, 0x07, 0xC3, 0xF0, 0x3E, - 0x0F, 0x81, 0xF0, 0x3C, 0x0F, 0x81, 0xE0, 0x7C, 0x0F, 0x03, 0xE0, 0x78, 0x1F, 0x03, 0xC0, 0xF8, - 0x1E, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x00, 0x03, 0x80, 0x7B, 0xE0, 0x7D, - 0xF0, 0x7C, 0xF8, 0x7C, 0x7C, 0x3E, 0x3E, 0x3E, 0x1F, 0x3E, 0x0F, 0xBE, 0x07, 0xFF, 0x03, 0xFF, - 0x01, 0xFF, 0x80, 0xFF, 0xE0, 0x7C, 0xF8, 0x3E, 0x3E, 0x1F, 0x1F, 0x0F, 0x87, 0xC7, 0xC1, 0xF3, - 0xE0, 0xFD, 0xF0, 0x3E, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xE0, 0x00, 0x78, 0x00, 0x00, 0x03, 0x80, - 0x7B, 0xE0, 0x7F, 0xF0, 0x3F, 0xF8, 0x3F, 0xFC, 0x3F, 0xFE, 0x1F, 0xFF, 0x1F, 0xFF, 0x8F, 0xFF, - 0xCF, 0xFF, 0xEF, 0xBF, 0xF7, 0x9F, 0xFF, 0xCF, 0xFF, 0xC7, 0xFF, 0xE3, 0xFF, 0xE1, 0xFF, 0xE0, - 0xFF, 0xF0, 0x7F, 0xF0, 0x3F, 0xF0, 0x1F, 0x0E, 0x38, 0x07, 0xFC, 0x01, 0xFC, 0x00, 0x7C, 0x00, - 0x00, 0x03, 0xC0, 0x3B, 0xF0, 0x3E, 0xF8, 0x3E, 0x7C, 0x1F, 0x1F, 0x0F, 0x0F, 0x8F, 0x83, 0xE7, - 0xC1, 0xF7, 0xC0, 0x7B, 0xE0, 0x3F, 0xE0, 0x0F, 0xF0, 0x07, 0xF0, 0x01, 0xF8, 0x00, 0xF8, 0x00, - 0x7C, 0x01, 0xFE, 0x00, 0xFE, 0x00, 0x7E, 0x00, 0x3E, 0x00, 0x70, 0x0E, 0xF8, 0x1F, 0xF8, 0x1F, + 0x73, 0x87, 0x38, 0x7B, 0xE7, 0xF8, 0x7E, 0x0F, 0x80, 0x7B, 0xCF, 0x78, 0xF7, 0x0E, 0xF0, 0x3C, + 0xE7, 0x9E, 0x7B, 0xE7, 0x00, 0x03, 0xDE, 0xF7, 0xBD, 0xDC, 0x00, 0x03, 0xDE, 0xF7, 0xBD, 0xDC, + 0x00, 0x7B, 0xDC, 0xEF, 0x71, 0x80, 0x77, 0xFE, 0xE0, 0x77, 0x7F, 0xE6, 0x61, 0x87, 0x8F, 0x7F, + 0xE0, 0x00, 0x38, 0xF9, 0xE7, 0xC7, 0xCF, 0xC6, 0x00, 0x7B, 0xE7, 0xF8, 0x7E, 0x0F, 0x80, 0x00, + 0x60, 0x0E, 0x00, 0xC0, 0x1C, 0x01, 0x80, 0x38, 0x03, 0x00, 0x70, 0x06, 0x00, 0xE0, 0x0C, 0x01, + 0xC0, 0x18, 0x03, 0x80, 0x30, 0x07, 0x00, 0x60, 0x00, 0x00, 0x0C, 0x00, 0x38, 0x00, 0x60, 0x01, + 0xC0, 0x07, 0x00, 0x0C, 0x00, 0x38, 0x00, 0xE0, 0x01, 0xC0, 0x07, 0x00, 0x0C, 0x00, 0x38, 0x00, + 0xE0, 0x01, 0x80, 0x07, 0x00, 0x0C, 0x00, 0x38, 0x00, 0xE0, 0x01, 0x80, 0x07, 0x00, 0x00, 0x39, + 0xDF, 0xF7, 0xF9, 0x9C, 0x7B, 0xDD, 0xEF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0xE1, 0xF8, 0x70, + 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xFF, 0xFE, 0xE7, 0xDF, 0xBF, 0x7E, 0xFD, 0xC0, 0x00, 0xFF, + 0xFF, 0xCF, 0x3C, 0x60, 0x02, 0x07, 0xFD, 0xFF, 0xBF, 0xE6, 0xF8, 0x18, 0x00, 0x1E, 0x0F, 0x81, + 0xE0, 0x76, 0x7F, 0xFF, 0xFE, 0xFB, 0x78, 0x1E, 0x0F, 0x81, 0xE0, 0x00, 0x1F, 0xFF, 0xFF, 0x9E, + 0x7F, 0xFF, 0xFE, 0x78, 0x19, 0x87, 0x1C, 0xFF, 0xEF, 0xFF, 0x70, 0xE3, 0x9C, 0x00, 0xC7, 0xBF, + 0xF4, 0xC3, 0x00, 0x80, 0x3C, 0x0F, 0xC3, 0xFE, 0x1F, 0xC3, 0xF7, 0xFF, 0xFC, 0xF8, 0x30, 0x00, + 0x37, 0x66, 0x70, 0x1F, 0x1F, 0xE7, 0xFB, 0xCF, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x01, 0xDB, 0xF3, + 0xC7, 0xDF, 0x90, 0x00, 0x00, 0x1D, 0xFF, 0xCF, 0xC7, 0xC1, 0x80, 0x01, 0xE3, 0xF9, 0xF7, 0xFF, + 0x30, 0x00, 0x00, 0x31, 0xE0, 0x71, 0xF9, 0xE0, 0xFB, 0xC7, 0xFF, 0xDF, 0x99, 0x98, 0x33, 0x00, + 0xEF, 0x73, 0x7F, 0xC0, 0x77, 0xFE, 0xE0, 0x00, 0xCF, 0xFF, 0x39, 0xE4, 0x80, 0x00, 0x0F, 0xFB, + 0xFF, 0x66, 0x6F, 0xFC, 0xFF, 0x80, 0x31, 0xC7, 0xFF, 0xFC, 0x63, 0x80, 0x70, 0x00, 0xE7, 0x80, + 0x1E, 0x3F, 0xFF, 0xC1, 0xFF, 0xF8, 0x07, 0xFE, 0x00, 0xE0, 0x01, 0xFF, 0x00, 0x79, 0xFF, 0xFF, + 0x1F, 0xFF, 0x80, 0x7F, 0xC0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF8, 0x1F, 0xC0, 0x01, 0xDF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0x80, 0x3F, 0xFC, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x40, 0x01, 0xFF, 0xC0, 0xFF, 0xFE, 0x3F, 0x07, 0xEF, 0x00, 0x1F, 0xC0, 0x01, + 0xC0, 0x00, 0x00, 0xE0, 0x00, 0x03, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x78, 0x00, + 0x03, 0x80, 0x79, 0xFB, 0xB3, 0xEF, 0xDB, 0xBF, 0x3E, 0x39, 0xFB, 0xBF, 0xFF, 0xFD, 0xBF, 0x1C, + 0x1F, 0xEF, 0xFF, 0xFF, 0x00, 0x1C, 0x7E, 0x7F, 0x67, 0x67, 0x77, 0x7E, 0x3C, 0xCD, 0x9F, 0x3E, + 0x7C, 0xFF, 0xFF, 0xBE, 0x39, 0xFB, 0xB6, 0x2C, 0x1D, 0xBF, 0x1C, 0x06, 0x07, 0x07, 0x3F, 0x7F, + 0x77, 0x67, 0x67, 0x7F, 0x7F, 0x3E, 0xC1, 0x83, 0x07, 0xCF, 0xDF, 0xB3, 0xE7, 0xCF, 0x9F, 0x30, + 0x7D, 0xC7, 0xFE, 0x7F, 0xE6, 0x67, 0x66, 0x76, 0x67, 0x66, 0x76, 0x66, 0x7F, 0xFF, 0xCE, 0x73, + 0x8C, 0x23, 0xBF, 0xF7, 0x39, 0xCE, 0x79, 0xC0, 0x67, 0x76, 0x7E, 0x3E, 0x3C, 0x3C, 0x1C, 0x18, + 0xEF, 0xF9, 0xF3, 0xC7, 0x8F, 0xBB, 0xE7, 0x1E, 0x00, 0x70, 0x03, 0xC0, 0x0E, 0x00, 0x00, 0x3F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF7, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x3C, 0xE1, + 0xEF, 0x8F, 0x7C, 0x39, 0xC0, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, 0x01, + 0xF0, 0x0F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xFF, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x07, 0xFF, 0xF8, 0x3F, 0xFF, 0xC1, 0xFF, 0xFE, 0x00, + 0x3E, 0x00, 0x01, 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x7C, 0xC0, 0x03, 0xFF, 0xC0, 0x1F, 0xFF, 0x00, + 0xFF, 0xFC, 0x07, 0xFF, 0xE0, 0x3E, 0x0F, 0x81, 0xF0, 0x7C, 0x0F, 0x83, 0xE0, 0x7C, 0x1F, 0x03, + 0xE1, 0xF0, 0x1F, 0x7F, 0x80, 0xFB, 0xF8, 0x00, 0x1F, 0x80, 0x00, 0xF0, 0x00, 0x01, 0xE0, 0x1E, + 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x3F, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, 0x01, 0xF0, + 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, + 0x03, 0xE0, 0x1F, 0x00, 0x03, 0xE0, 0x07, 0xFC, 0x07, 0xFF, 0x87, 0xFF, 0xC7, 0xE3, 0xF7, 0xE0, + 0x7B, 0xE0, 0x3F, 0xF0, 0x00, 0xFF, 0xE0, 0x7F, 0xF0, 0x3F, 0xF8, 0x1F, 0xF8, 0x0F, 0x80, 0x77, + 0xC0, 0x7D, 0xF0, 0x7C, 0xFE, 0xFE, 0x3F, 0xFE, 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, 0x00, 0x07, + 0xC0, 0x3F, 0xE1, 0xFF, 0xE3, 0xFF, 0xEF, 0x87, 0xDF, 0x07, 0xBE, 0x00, 0x7E, 0x00, 0x7F, 0xC0, + 0x7F, 0xE0, 0x7F, 0xF0, 0x1F, 0xE0, 0x07, 0xFE, 0x07, 0xFC, 0x0F, 0xFE, 0x3E, 0xFF, 0xFC, 0xFF, + 0xF0, 0x7F, 0xC0, 0x3C, 0x00, 0x77, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0x73, 0xCF, 0x79, 0xEF, 0x38, 0xE0, 0x00, 0x1C, 0x07, 0xC0, 0xF8, 0x1F, 0x03, 0xE0, 0x7C, + 0x0F, 0x81, 0xF0, 0x3E, 0x07, 0xC0, 0xF8, 0x1F, 0x03, 0xE0, 0x7C, 0x0F, 0x81, 0xF0, 0x3E, 0x07, + 0xC0, 0xF8, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, + 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF7, 0x87, 0xFC, 0x3F, 0xE1, 0xEF, 0x9F, 0x7F, 0xF9, 0xFF, 0x87, + 0xF8, 0x0E, 0x00, 0x07, 0xFF, 0x80, 0x01, 0xFF, 0xF0, 0x00, 0x3F, 0xFE, 0x00, 0x07, 0xFF, 0xC0, + 0x00, 0xF0, 0x78, 0x00, 0x1E, 0x0F, 0x00, 0x03, 0xC1, 0xE0, 0x00, 0x78, 0x3F, 0xF0, 0x0F, 0x07, + 0xFF, 0x81, 0xE0, 0xFF, 0xF8, 0x3C, 0x1F, 0xFF, 0x8F, 0x83, 0xC1, 0xF1, 0xF0, 0x78, 0x1E, 0x3E, + 0x0F, 0x03, 0xC7, 0x81, 0xE0, 0xF9, 0xF0, 0x3C, 0x3F, 0xFE, 0x07, 0xFF, 0xDF, 0x80, 0xFF, 0xF3, + 0xE0, 0x1F, 0xFC, 0x00, 0x70, 0x0F, 0x00, 0x1F, 0x01, 0xE0, 0x03, 0xE0, 0x3C, 0x00, 0x7C, 0x07, + 0x80, 0x0F, 0x80, 0xF0, 0x01, 0xF0, 0x1E, 0x00, 0x3E, 0x03, 0xC0, 0x07, 0xFF, 0xFF, 0xF0, 0xFF, + 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF, 0x7C, 0x07, 0x81, 0xEF, 0x80, 0xF0, 0x3D, + 0xF0, 0x1E, 0x07, 0xBE, 0x03, 0xC0, 0xF7, 0xC0, 0x78, 0x7E, 0xF8, 0x0F, 0xFF, 0x9F, 0x01, 0xFF, + 0xF3, 0xE0, 0x3F, 0xF8, 0x00, 0x7F, 0xFF, 0x07, 0xFF, 0xFC, 0x3F, 0xFF, 0xE1, 0xFF, 0xFF, 0x00, + 0x3E, 0x00, 0x01, 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x7C, 0xE0, 0x03, 0xFF, 0xE0, 0x1F, 0xFF, 0x80, + 0xFF, 0xFC, 0x07, 0xC3, 0xF0, 0x3E, 0x0F, 0x81, 0xF0, 0x3C, 0x0F, 0x81, 0xE0, 0x7C, 0x0F, 0x03, + 0xE0, 0x78, 0x1F, 0x03, 0xC0, 0xF8, 0x1E, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0x70, 0x00, + 0x00, 0x03, 0x80, 0x7B, 0xE0, 0x7D, 0xF0, 0x7C, 0xF8, 0x7C, 0x7C, 0x3E, 0x3E, 0x3E, 0x1F, 0x3E, + 0x0F, 0xBE, 0x07, 0xFF, 0x03, 0xFF, 0x01, 0xFF, 0x80, 0xFF, 0xE0, 0x7C, 0xF8, 0x3E, 0x3E, 0x1F, + 0x1F, 0x0F, 0x87, 0xC7, 0xC1, 0xF3, 0xE0, 0xFD, 0xF0, 0x3E, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xE0, + 0x00, 0x78, 0x00, 0x00, 0x03, 0x80, 0x7B, 0xE0, 0x7F, 0xF0, 0x3F, 0xF8, 0x3F, 0xFC, 0x3F, 0xFE, + 0x1F, 0xFF, 0x1F, 0xFF, 0x8F, 0xFF, 0xCF, 0xFF, 0xEF, 0xBF, 0xF7, 0x9F, 0xFF, 0xCF, 0xFF, 0xC7, + 0xFF, 0xE3, 0xFF, 0xE1, 0xFF, 0xE0, 0xFF, 0xF0, 0x7F, 0xF0, 0x3F, 0xF0, 0x1F, 0x0E, 0x38, 0x07, + 0xFC, 0x01, 0xFC, 0x00, 0x7C, 0x00, 0x00, 0x03, 0xC0, 0x3B, 0xF0, 0x3E, 0xF8, 0x3E, 0x7C, 0x1F, + 0x1F, 0x0F, 0x0F, 0x8F, 0x83, 0xE7, 0xC1, 0xF7, 0xC0, 0x7B, 0xE0, 0x3F, 0xE0, 0x0F, 0xF0, 0x07, + 0xF0, 0x01, 0xF8, 0x00, 0xF8, 0x00, 0x7C, 0x01, 0xFE, 0x00, 0xFE, 0x00, 0x7E, 0x00, 0x3E, 0x00, + 0x70, 0x0E, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, - 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x3F, 0x80, 0x1F, - 0xE0, 0x07, 0xF8, 0x01, 0xFF, 0x00, 0xFF, 0xC0, 0x3E, 0xF0, 0x0F, 0x3E, 0x07, 0xCF, 0x81, 0xF1, - 0xE0, 0x7C, 0x7C, 0x3F, 0xFF, 0x0F, 0xFF, 0xC3, 0xFF, 0xF9, 0xF0, 0x3E, 0x7C, 0x07, 0xDE, 0x01, - 0xFF, 0x80, 0x7C, 0x7F, 0xF1, 0xFF, 0xF3, 0xFF, 0xE7, 0xFF, 0xCF, 0x80, 0x1F, 0x00, 0x3E, 0x00, - 0x7C, 0x00, 0xFF, 0xE1, 0xFF, 0xF3, 0xFF, 0xF7, 0xC3, 0xEF, 0x83, 0xDF, 0x07, 0xFE, 0x0F, 0x7C, - 0x3E, 0xFF, 0xFD, 0xFF, 0xF3, 0xFF, 0xC0, 0x7F, 0x81, 0xFF, 0xE3, 0xFF, 0xE7, 0xFF, 0xEF, 0x87, - 0xDF, 0x07, 0xBE, 0x0F, 0x7C, 0x3E, 0xFF, 0xF9, 0xFF, 0xE3, 0xFF, 0xF7, 0xC7, 0xEF, 0x83, 0xFF, - 0x07, 0xFE, 0x0F, 0xFC, 0x7F, 0xFF, 0xFD, 0xFF, 0xFB, 0xFF, 0xC0, 0x7F, 0xF7, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, - 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x00, 0x03, 0xFF, 0xC0, 0x3F, 0xFF, 0x01, - 0xFF, 0xF8, 0x0F, 0xFF, 0xC0, 0x78, 0x3E, 0x03, 0xC1, 0xF0, 0x1E, 0x0F, 0x80, 0xF0, 0x7C, 0x07, - 0x83, 0xE0, 0x3C, 0x1F, 0x03, 0xE0, 0xF8, 0x1F, 0x07, 0xC0, 0xF0, 0x3E, 0x07, 0x81, 0xF0, 0x7C, - 0x0F, 0x87, 0xE0, 0x7C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0xFF, 0x80, - 0x07, 0xFC, 0x00, 0x3F, 0xE0, 0x01, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x7C, - 0x03, 0xE0, 0x1F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x80, 0x7C, 0x03, 0xE0, 0x1F, - 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x78, 0x1E, 0x07, 0x9F, 0x07, 0x83, 0xF7, 0xE1, 0xE0, 0xF8, - 0xF8, 0x78, 0x7C, 0x1F, 0x1E, 0x1F, 0x07, 0xC7, 0x8F, 0x80, 0xF9, 0xE3, 0xE0, 0x1E, 0x79, 0xF0, - 0x07, 0xFF, 0xF8, 0x00, 0xFF, 0xFE, 0x00, 0x7F, 0xFF, 0x80, 0x1F, 0xFF, 0xF0, 0x0F, 0x9E, 0x3C, - 0x07, 0xC7, 0x8F, 0x81, 0xF1, 0xE1, 0xF0, 0xF8, 0x78, 0x7C, 0x3E, 0x1E, 0x0F, 0x9F, 0x07, 0x83, - 0xEF, 0xC1, 0xE0, 0x7C, 0x07, 0x80, 0x7F, 0x87, 0xFF, 0x9F, 0xFE, 0xF8, 0x7F, 0xE1, 0xF0, 0x07, - 0xC0, 0x3E, 0x07, 0xF8, 0x1F, 0x80, 0x7F, 0x80, 0x7F, 0x00, 0x7F, 0xC0, 0xFF, 0x03, 0xFE, 0x1F, - 0x7F, 0xFD, 0xFF, 0xE1, 0xFE, 0x01, 0xE0, 0x70, 0x0F, 0x7C, 0x0F, 0xFE, 0x07, 0xFF, 0x07, 0xFF, - 0x87, 0xFF, 0xC3, 0xFF, 0xE3, 0xFF, 0xF1, 0xFF, 0xF9, 0xFF, 0xFD, 0xF7, 0xFE, 0xF3, 0xFF, 0xF9, - 0xFF, 0xF8, 0xFF, 0xFC, 0x7F, 0xFC, 0x3F, 0xFC, 0x1F, 0xFE, 0x0F, 0xFE, 0x07, 0xFE, 0x03, 0xE0, - 0x0E, 0x38, 0x07, 0xF8, 0x03, 0xFC, 0x00, 0x7C, 0x00, 0x00, 0x03, 0x80, 0x7B, 0xE0, 0x7F, 0xF0, - 0x3F, 0xF8, 0x3F, 0xFC, 0x3F, 0xFE, 0x1F, 0xFF, 0x1F, 0xFF, 0x8F, 0xFF, 0xCF, 0xFF, 0xEF, 0xBF, - 0xF7, 0x9F, 0xFF, 0xCF, 0xFF, 0xC7, 0xFF, 0xE3, 0xFF, 0xE1, 0xFF, 0xE0, 0xFF, 0xF0, 0x7F, 0xF0, - 0x3F, 0xF0, 0x1F, 0x70, 0x0F, 0x7C, 0x0F, 0xBE, 0x0F, 0x9F, 0x0F, 0x8F, 0x87, 0xC7, 0xC7, 0xC3, - 0xE7, 0xC1, 0xF7, 0xC0, 0xFF, 0xE0, 0x7F, 0xE0, 0x3F, 0xF0, 0x1F, 0xFC, 0x0F, 0x9F, 0x07, 0xC7, - 0xC3, 0xE3, 0xE1, 0xF0, 0xF8, 0xF8, 0x3E, 0x7C, 0x1F, 0xBE, 0x07, 0xC0, 0x07, 0xFF, 0x83, 0xFF, - 0xE0, 0xFF, 0xF8, 0x3F, 0xFE, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, - 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x7C, 0x1E, 0x1F, 0x07, 0x87, 0xC1, 0xE1, 0xE0, 0x79, 0xF8, 0x1E, - 0xFE, 0x07, 0xBF, 0x01, 0xEF, 0x80, 0x78, 0x7C, 0x01, 0xF7, 0xF0, 0x1F, 0xFF, 0x80, 0xFF, 0xFE, - 0x07, 0xFF, 0xF0, 0x7F, 0xFF, 0x83, 0xFF, 0xFE, 0x3F, 0xFF, 0xF1, 0xFF, 0xFF, 0x8F, 0xFF, 0xFE, - 0xFF, 0xFE, 0xF7, 0xBF, 0xF7, 0xBD, 0xFF, 0xBD, 0xEF, 0xFC, 0xFE, 0x7F, 0xE7, 0xF3, 0xFF, 0x3F, - 0x9F, 0xF8, 0xF8, 0xFF, 0xC7, 0xC7, 0xFE, 0x3E, 0x3E, 0x70, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, - 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0x03, - 0xE0, 0x03, 0xFE, 0x01, 0xFF, 0xE0, 0xFF, 0xF8, 0x7E, 0x3F, 0x3F, 0x03, 0xEF, 0x80, 0xFB, 0xE0, - 0x1E, 0xF0, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF8, 0x07, 0xBE, 0x03, 0xE7, 0xC1, - 0xF9, 0xFD, 0xFC, 0x3F, 0xFE, 0x07, 0xFF, 0x00, 0xFF, 0x80, 0x07, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0xE0, + 0x00, 0xFC, 0x00, 0x3F, 0x80, 0x1F, 0xE0, 0x07, 0xF8, 0x01, 0xFF, 0x00, 0xFF, 0xC0, 0x3E, 0xF0, + 0x0F, 0x3E, 0x07, 0xCF, 0x81, 0xF1, 0xE0, 0x7C, 0x7C, 0x3F, 0xFF, 0x0F, 0xFF, 0xC3, 0xFF, 0xF9, + 0xF0, 0x3E, 0x7C, 0x07, 0xDE, 0x01, 0xFF, 0x80, 0x7C, 0x7F, 0xF1, 0xFF, 0xF3, 0xFF, 0xE7, 0xFF, + 0xCF, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x00, 0xFF, 0xE1, 0xFF, 0xF3, 0xFF, 0xF7, 0xC3, 0xEF, + 0x83, 0xDF, 0x07, 0xFE, 0x0F, 0x7C, 0x3E, 0xFF, 0xFD, 0xFF, 0xF3, 0xFF, 0xC0, 0x7F, 0x81, 0xFF, + 0xE3, 0xFF, 0xE7, 0xFF, 0xEF, 0x87, 0xDF, 0x07, 0xBE, 0x0F, 0x7C, 0x3E, 0xFF, 0xF9, 0xFF, 0xE3, + 0xFF, 0xF7, 0xC7, 0xEF, 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0xFC, 0x7F, 0xFF, 0xFD, 0xFF, 0xFB, 0xFF, + 0xC0, 0x7F, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, + 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x00, + 0x03, 0xFF, 0xC0, 0x3F, 0xFF, 0x01, 0xFF, 0xF8, 0x0F, 0xFF, 0xC0, 0x78, 0x3E, 0x03, 0xC1, 0xF0, + 0x1E, 0x0F, 0x80, 0xF0, 0x7C, 0x07, 0x83, 0xE0, 0x3C, 0x1F, 0x03, 0xE0, 0xF8, 0x1F, 0x07, 0xC0, + 0xF0, 0x3E, 0x07, 0x81, 0xF0, 0x7C, 0x0F, 0x87, 0xE0, 0x7C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF0, 0x00, 0xFF, 0x80, 0x07, 0xFC, 0x00, 0x3F, 0xE0, 0x01, 0xE0, 0x7F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xEF, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x78, 0x1E, 0x07, 0x9F, + 0x07, 0x83, 0xF7, 0xE1, 0xE0, 0xF8, 0xF8, 0x78, 0x7C, 0x1F, 0x1E, 0x1F, 0x07, 0xC7, 0x8F, 0x80, + 0xF9, 0xE3, 0xE0, 0x1E, 0x79, 0xF0, 0x07, 0xFF, 0xF8, 0x00, 0xFF, 0xFE, 0x00, 0x7F, 0xFF, 0x80, + 0x1F, 0xFF, 0xF0, 0x0F, 0x9E, 0x3C, 0x07, 0xC7, 0x8F, 0x81, 0xF1, 0xE1, 0xF0, 0xF8, 0x78, 0x7C, + 0x3E, 0x1E, 0x0F, 0x9F, 0x07, 0x83, 0xEF, 0xC1, 0xE0, 0x7C, 0x07, 0x80, 0x7F, 0x87, 0xFF, 0x9F, + 0xFE, 0xF8, 0x7F, 0xE1, 0xF0, 0x07, 0xC0, 0x3E, 0x07, 0xF8, 0x1F, 0x80, 0x7F, 0x80, 0x7F, 0x00, + 0x7F, 0xC0, 0xFF, 0x03, 0xFE, 0x1F, 0x7F, 0xFD, 0xFF, 0xE1, 0xFE, 0x01, 0xE0, 0x70, 0x0F, 0x7C, + 0x0F, 0xFE, 0x07, 0xFF, 0x07, 0xFF, 0x87, 0xFF, 0xC3, 0xFF, 0xE3, 0xFF, 0xF1, 0xFF, 0xF9, 0xFF, + 0xFD, 0xF7, 0xFE, 0xF3, 0xFF, 0xF9, 0xFF, 0xF8, 0xFF, 0xFC, 0x7F, 0xFC, 0x3F, 0xFC, 0x1F, 0xFE, + 0x0F, 0xFE, 0x07, 0xFE, 0x03, 0xE0, 0x0E, 0x38, 0x07, 0xF8, 0x03, 0xFC, 0x00, 0x7C, 0x00, 0x00, + 0x03, 0x80, 0x7B, 0xE0, 0x7F, 0xF0, 0x3F, 0xF8, 0x3F, 0xFC, 0x3F, 0xFE, 0x1F, 0xFF, 0x1F, 0xFF, + 0x8F, 0xFF, 0xCF, 0xFF, 0xEF, 0xBF, 0xF7, 0x9F, 0xFF, 0xCF, 0xFF, 0xC7, 0xFF, 0xE3, 0xFF, 0xE1, + 0xFF, 0xE0, 0xFF, 0xF0, 0x7F, 0xF0, 0x3F, 0xF0, 0x1F, 0x70, 0x0F, 0x7C, 0x0F, 0xBE, 0x0F, 0x9F, + 0x0F, 0x8F, 0x87, 0xC7, 0xC7, 0xC3, 0xE7, 0xC1, 0xF7, 0xC0, 0xFF, 0xE0, 0x7F, 0xE0, 0x3F, 0xF0, + 0x1F, 0xFC, 0x0F, 0x9F, 0x07, 0xC7, 0xC3, 0xE3, 0xE1, 0xF0, 0xF8, 0xF8, 0x3E, 0x7C, 0x1F, 0xBE, + 0x07, 0xC0, 0x07, 0xFF, 0x83, 0xFF, 0xE0, 0xFF, 0xF8, 0x3F, 0xFE, 0x0F, 0x07, 0x83, 0xC1, 0xE0, + 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x7C, 0x1E, 0x1F, 0x07, 0x87, + 0xC1, 0xE1, 0xE0, 0x79, 0xF8, 0x1E, 0xFE, 0x07, 0xBF, 0x01, 0xEF, 0x80, 0x78, 0x7C, 0x01, 0xF7, + 0xF0, 0x1F, 0xFF, 0x80, 0xFF, 0xFE, 0x07, 0xFF, 0xF0, 0x7F, 0xFF, 0x83, 0xFF, 0xFE, 0x3F, 0xFF, + 0xF1, 0xFF, 0xFF, 0x8F, 0xFF, 0xFE, 0xFF, 0xFE, 0xF7, 0xBF, 0xF7, 0xBD, 0xFF, 0xBD, 0xEF, 0xFC, + 0xFE, 0x7F, 0xE7, 0xF3, 0xFF, 0x3F, 0x9F, 0xF8, 0xF8, 0xFF, 0xC7, 0xC7, 0xFE, 0x3E, 0x3E, 0x70, + 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, + 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0x03, 0xE0, 0x03, 0xFE, 0x01, 0xFF, 0xE0, 0xFF, 0xF8, 0x7E, 0x3F, + 0x3F, 0x03, 0xEF, 0x80, 0xFB, 0xE0, 0x1E, 0xF0, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, + 0xF8, 0x07, 0xBE, 0x03, 0xE7, 0xC1, 0xF9, 0xFD, 0xFC, 0x3F, 0xFE, 0x07, 0xFF, 0x00, 0xFF, 0x80, + 0x07, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, - 0xF8, 0x0F, 0x7F, 0x81, 0xFF, 0xE3, 0xFF, 0xE7, 0xFF, 0xEF, 0x87, 0xDF, 0x07, 0xBE, 0x0F, 0xFC, - 0x1F, 0xF8, 0x3D, 0xF1, 0xFB, 0xFF, 0xF7, 0xFF, 0xCF, 0xFE, 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x00, - 0xF8, 0x01, 0xF0, 0x03, 0xE0, 0x00, 0x03, 0xE0, 0x07, 0xFC, 0x07, 0xFF, 0x87, 0xFF, 0xC7, 0xE3, - 0xF7, 0xE0, 0x7B, 0xE0, 0x3F, 0xF0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, - 0x80, 0x77, 0xC0, 0x7D, 0xF0, 0x7C, 0xFE, 0xFE, 0x3F, 0xFE, 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, - 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, + 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0x7F, 0x81, 0xFF, 0xE3, 0xFF, 0xE7, 0xFF, 0xEF, + 0x87, 0xDF, 0x07, 0xBE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3D, 0xF1, 0xFB, 0xFF, 0xF7, 0xFF, 0xCF, 0xFE, + 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xE0, 0x00, 0x03, 0xE0, 0x07, 0xFC, + 0x07, 0xFF, 0x87, 0xFF, 0xC7, 0xE3, 0xF7, 0xE0, 0x7B, 0xE0, 0x3F, 0xF0, 0x00, 0xF0, 0x00, 0x78, + 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x80, 0x77, 0xC0, 0x7D, 0xF0, 0x7C, 0xFE, 0xFE, 0x3F, 0xFE, + 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, - 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x78, 0x07, 0x7E, 0x07, 0xDF, 0x07, 0xCF, 0x83, 0xE3, - 0xE1, 0xE1, 0xF1, 0xF0, 0x7C, 0xF8, 0x3E, 0xF8, 0x0F, 0x7C, 0x07, 0xFC, 0x01, 0xFE, 0x00, 0xFE, - 0x00, 0x3F, 0x00, 0x1F, 0x00, 0x0F, 0x80, 0x3F, 0xC0, 0x1F, 0xC0, 0x0F, 0xC0, 0x07, 0xC0, 0x00, - 0x00, 0xE0, 0x00, 0x3E, 0x00, 0x07, 0xC0, 0x07, 0xFF, 0x01, 0xFF, 0xF0, 0x7F, 0xFF, 0x1F, 0xFF, - 0xF7, 0xCF, 0x9F, 0xF9, 0xF3, 0xFE, 0x3E, 0x3F, 0xC7, 0xC7, 0xF8, 0xF8, 0xFF, 0x1F, 0x1F, 0xF3, - 0xE7, 0xFF, 0x7D, 0xF3, 0xFF, 0xFE, 0x3F, 0xFF, 0x83, 0xFF, 0xE0, 0x1F, 0xF0, 0x00, 0xF8, 0x00, - 0x1F, 0x00, 0x00, 0x00, 0x00, 0x78, 0x07, 0x9F, 0x03, 0xE3, 0xE1, 0xF0, 0xFC, 0x7C, 0x1F, 0x3E, - 0x03, 0xEF, 0x00, 0xFF, 0xC0, 0x1F, 0xE0, 0x03, 0xF0, 0x00, 0xFC, 0x00, 0x3F, 0x00, 0x1F, 0xE0, - 0x07, 0xFC, 0x03, 0xFF, 0x01, 0xF3, 0xE0, 0x7C, 0x7C, 0x3E, 0x1F, 0x1F, 0x03, 0xEF, 0xC0, 0x7C, - 0x70, 0x0E, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, - 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, - 0x81, 0xF3, 0xE0, 0x7C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x1F, 0x00, 0x07, 0xC0, - 0x01, 0xF0, 0x00, 0x7C, 0x70, 0x1F, 0xF0, 0x3F, 0xE0, 0x7F, 0xC0, 0xFF, 0x81, 0xFF, 0x03, 0xFE, - 0x07, 0xFC, 0x0F, 0xF8, 0x1E, 0xFF, 0xFD, 0xFF, 0xF9, 0xFF, 0xF0, 0xFD, 0xE0, 0x03, 0xC0, 0x07, - 0x80, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x70, 0x38, 0x3F, 0xE1, 0xF0, 0xFF, 0x87, 0xC3, - 0xFE, 0x1F, 0x0F, 0xF8, 0x7C, 0x3F, 0xE1, 0xF0, 0xFF, 0x87, 0xC3, 0xFE, 0x1F, 0x0F, 0xF8, 0x7C, + 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x78, 0x07, 0x7E, + 0x07, 0xDF, 0x07, 0xCF, 0x83, 0xE3, 0xE1, 0xE1, 0xF1, 0xF0, 0x7C, 0xF8, 0x3E, 0xF8, 0x0F, 0x7C, + 0x07, 0xFC, 0x01, 0xFE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0x00, 0x0F, 0x80, 0x3F, 0xC0, 0x1F, + 0xC0, 0x0F, 0xC0, 0x07, 0xC0, 0x00, 0x00, 0xE0, 0x00, 0x3E, 0x00, 0x07, 0xC0, 0x07, 0xFF, 0x01, + 0xFF, 0xF0, 0x7F, 0xFF, 0x1F, 0xFF, 0xF7, 0xCF, 0x9F, 0xF9, 0xF3, 0xFE, 0x3E, 0x3F, 0xC7, 0xC7, + 0xF8, 0xF8, 0xFF, 0x1F, 0x1F, 0xF3, 0xE7, 0xFF, 0x7D, 0xF3, 0xFF, 0xFE, 0x3F, 0xFF, 0x83, 0xFF, + 0xE0, 0x1F, 0xF0, 0x00, 0xF8, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x78, 0x07, 0x9F, 0x03, 0xE3, + 0xE1, 0xF0, 0xFC, 0x7C, 0x1F, 0x3E, 0x03, 0xEF, 0x00, 0xFF, 0xC0, 0x1F, 0xE0, 0x03, 0xF0, 0x00, + 0xFC, 0x00, 0x3F, 0x00, 0x1F, 0xE0, 0x07, 0xFC, 0x03, 0xFF, 0x01, 0xF3, 0xE0, 0x7C, 0x7C, 0x3E, + 0x1F, 0x1F, 0x03, 0xEF, 0xC0, 0x7C, 0x70, 0x0E, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, + 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, + 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFC, 0x00, 0x1F, 0x00, 0x07, 0xC0, 0x01, 0xF0, 0x00, 0x7C, 0x70, 0x1F, 0xF0, 0x3F, 0xE0, 0x7F, + 0xC0, 0xFF, 0x81, 0xFF, 0x03, 0xFE, 0x07, 0xFC, 0x0F, 0xF8, 0x1E, 0xFF, 0xFD, 0xFF, 0xF9, 0xFF, + 0xF0, 0xFD, 0xE0, 0x03, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x70, 0x38, 0x3F, 0xE1, 0xF0, 0xFF, 0x87, 0xC3, 0xFE, 0x1F, 0x0F, 0xF8, 0x7C, 0x3F, 0xE1, 0xF0, 0xFF, 0x87, - 0xC3, 0xFE, 0x1F, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x70, 0x38, 0x3C, - 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, + 0xC3, 0xFE, 0x1F, 0x0F, 0xF8, 0x7C, 0x3F, 0xE1, 0xF0, 0xFF, 0x87, 0xC3, 0xFE, 0x1F, 0x0F, 0xF8, + 0x7C, 0x3F, 0xE1, 0xF0, 0xFF, 0x87, 0xC3, 0xFE, 0x1F, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xC0, 0x70, 0x38, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, - 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x0F, 0x00, - 0x00, 0x0F, 0x7F, 0x80, 0x0F, 0xFC, 0x00, 0xFF, 0xC0, 0x0F, 0xFC, 0x00, 0x07, 0xC0, 0x00, 0x7C, - 0x00, 0x07, 0xC0, 0x00, 0x7F, 0xF0, 0x07, 0xFF, 0xC0, 0x7F, 0xFE, 0x07, 0xFF, 0xF0, 0x7C, 0x1F, - 0x07, 0xC0, 0xF0, 0x7C, 0x0F, 0x07, 0xC1, 0xF0, 0x7C, 0x3F, 0x07, 0xFF, 0xE0, 0x7F, 0xFC, 0x07, - 0xFF, 0x80, 0x70, 0x00, 0x77, 0xC0, 0x07, 0xFE, 0x00, 0x3F, 0xF0, 0x01, 0xFF, 0x80, 0x0F, 0xFC, - 0x00, 0x7F, 0xE0, 0x03, 0xFF, 0xFC, 0x1F, 0xFF, 0xF8, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0x3F, 0xF0, - 0x7D, 0xFF, 0x83, 0xEF, 0xFC, 0x1F, 0x7F, 0xE0, 0xFB, 0xFF, 0x1F, 0x9F, 0xFF, 0xFC, 0xFF, 0xFF, - 0xC7, 0xFF, 0xFC, 0x3E, 0x70, 0x01, 0xF0, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x3E, - 0x00, 0x7F, 0xF0, 0xFF, 0xF9, 0xFF, 0xFB, 0xFF, 0xF7, 0xC1, 0xFF, 0x83, 0xFF, 0x07, 0xFE, 0x0F, - 0xFC, 0x7E, 0xFF, 0xFD, 0xFF, 0xF3, 0xFF, 0xC0, 0x03, 0xE0, 0x03, 0xFF, 0x01, 0xFF, 0xE0, 0xFF, - 0xFC, 0x7E, 0x1F, 0x1F, 0x03, 0xE7, 0x80, 0x78, 0x00, 0x1F, 0x01, 0xFF, 0xC0, 0x7F, 0xF0, 0x1F, - 0xFC, 0x07, 0xFF, 0x78, 0x07, 0x9E, 0x03, 0xE7, 0xC0, 0xF8, 0xFC, 0xFC, 0x3F, 0xFF, 0x07, 0xFF, - 0x80, 0xFF, 0x80, 0x07, 0x00, 0x70, 0x07, 0xC0, 0xF8, 0x1F, 0xF0, 0xF8, 0x7F, 0xF8, 0xF8, 0xFF, - 0xFC, 0xF8, 0xFC, 0x7E, 0xF9, 0xF0, 0x3E, 0xF9, 0xF0, 0x1F, 0xF9, 0xE0, 0x1F, 0xFF, 0xE0, 0x0F, - 0xFF, 0xE0, 0x0F, 0xFF, 0xE0, 0x0F, 0xFF, 0xE0, 0x0F, 0xF9, 0xE0, 0x1F, 0xF9, 0xF0, 0x1F, 0xF9, - 0xF8, 0x3E, 0xF8, 0xFE, 0xFE, 0xF8, 0x7F, 0xFC, 0xF8, 0x3F, 0xF8, 0xF8, 0x1F, 0xF0, 0x00, 0x03, - 0x80, 0x01, 0xFE, 0x1F, 0xFC, 0xFF, 0xF9, 0xFF, 0xF7, 0xE1, 0xEF, 0x83, 0xDE, 0x07, 0xBC, 0x0F, - 0x7C, 0x1E, 0xFF, 0xFC, 0xFF, 0xF8, 0xFF, 0xF0, 0xFF, 0xE3, 0xE3, 0xC7, 0x87, 0x9F, 0x0F, 0x7C, - 0x1E, 0xF8, 0x3F, 0xE0, 0x78, 0x0F, 0xC1, 0xFF, 0x8F, 0xFE, 0xF9, 0xF7, 0x87, 0x80, 0x7C, 0x7F, - 0xEF, 0xFF, 0x7F, 0x7F, 0xC3, 0xFE, 0x3E, 0xFF, 0xF7, 0xFF, 0x9F, 0xFC, 0x38, 0x00, 0x00, 0x18, - 0x00, 0x78, 0x1F, 0xF0, 0x7F, 0xC3, 0xFF, 0x07, 0xC0, 0x1E, 0x78, 0x3F, 0xF8, 0x7F, 0xF8, 0xFC, - 0xF9, 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xEF, 0x07, 0x9E, 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, - 0x3F, 0xC0, 0x1C, 0x00, 0xFF, 0x87, 0xFF, 0x3F, 0xFD, 0xFF, 0xEF, 0x0F, 0x78, 0xFB, 0xFF, 0x9F, - 0xFC, 0xFF, 0xF7, 0x87, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0x7F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xF8, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x3E, 0x00, 0x1F, 0xFC, 0x1F, - 0xFC, 0x1F, 0xFC, 0x1F, 0xFC, 0x1E, 0x3C, 0x1E, 0x3C, 0x1E, 0x3C, 0x1E, 0x3C, 0x3C, 0x3C, 0x3C, - 0x3C, 0x3C, 0x3C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, - 0x0F, 0x07, 0xC0, 0x7F, 0xC3, 0xFF, 0x9F, 0xBE, 0x78, 0x3D, 0xE0, 0xF7, 0xFF, 0xDF, 0xFF, 0x7F, - 0xFD, 0xE0, 0x07, 0xC3, 0xCF, 0xFF, 0x3F, 0xF8, 0x3F, 0xC0, 0x38, 0x00, 0xF8, 0x78, 0xFB, 0xE3, - 0xC7, 0xDF, 0x1E, 0x7C, 0x7C, 0xF7, 0xC1, 0xF7, 0xBC, 0x07, 0xFF, 0xE0, 0x3F, 0xFE, 0x00, 0xFF, - 0xF0, 0x0F, 0xFF, 0xC0, 0xFB, 0xFE, 0x0F, 0xDE, 0xF8, 0x7C, 0xF3, 0xE7, 0xC7, 0x9F, 0x7C, 0x3C, - 0x7C, 0x0F, 0x81, 0xFF, 0x1F, 0xF8, 0xFB, 0xE7, 0x8F, 0x00, 0xF8, 0x1F, 0x80, 0xFC, 0x07, 0xF3, - 0x87, 0x9E, 0x3C, 0xFF, 0xE7, 0xFF, 0x0F, 0xE0, 0x18, 0x00, 0xF0, 0x7F, 0xC3, 0xFE, 0x3F, 0xF3, - 0xFF, 0x9F, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF3, 0xFF, 0x9F, 0xF8, 0xFF, 0x87, 0xF8, - 0x3C, 0x38, 0xE1, 0xFF, 0x07, 0xF0, 0x1F, 0x00, 0x00, 0x78, 0x3F, 0xE1, 0xFF, 0x1F, 0xF9, 0xFF, - 0xCF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0xF9, 0xFF, 0xCF, 0xFC, 0x7F, 0xC3, 0xFC, 0x1E, - 0xF0, 0x7B, 0xE3, 0xEF, 0x9F, 0x3E, 0xFC, 0xFB, 0xE3, 0xFF, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0xC3, - 0xEF, 0x8F, 0xBE, 0x3E, 0x7C, 0xF8, 0xFB, 0xE3, 0xE0, 0x1F, 0xFC, 0xFF, 0xF3, 0xFF, 0xCF, 0xFF, - 0x3E, 0x7C, 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x3C, 0x7C, 0xF1, 0xF7, 0xC7, 0xFF, 0x1F, 0xF8, 0x7F, - 0xC1, 0xF0, 0xFC, 0x0F, 0xFF, 0x87, 0xFF, 0xE1, 0xFF, 0xF8, 0x7F, 0xFF, 0x3F, 0xFF, 0xCF, 0xFF, - 0xFB, 0xFF, 0xDF, 0xEF, 0xF7, 0xFB, 0xFD, 0xFE, 0xFF, 0x3F, 0x3F, 0xCF, 0xCF, 0xF1, 0xF3, 0xFC, - 0x78, 0xF0, 0xF0, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3C, 0x07, 0xC0, 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, - 0xE7, 0xC7, 0xCF, 0x07, 0x9E, 0x0F, 0xBC, 0x1F, 0x78, 0x3E, 0xF0, 0x79, 0xF1, 0xF1, 0xFF, 0xC3, - 0xFF, 0x81, 0xFE, 0x00, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0xFC, 0x3F, - 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3C, 0xF3, 0xC3, 0xFF, - 0xCF, 0xFF, 0xBF, 0xFE, 0xF8, 0x7F, 0xE1, 0xFF, 0x07, 0xFC, 0x0F, 0xF8, 0x7F, 0xE1, 0xFF, 0xCF, - 0xFF, 0xFE, 0xFF, 0xF3, 0xFF, 0x8F, 0x98, 0x3E, 0x00, 0xF8, 0x03, 0xE0, 0x0F, 0x80, 0x00, 0x00, - 0x07, 0xC0, 0x7F, 0xC3, 0xFF, 0x9F, 0xFF, 0x7C, 0x7D, 0xE0, 0xE7, 0x80, 0x1E, 0x00, 0x78, 0x01, - 0xE0, 0xF7, 0xC7, 0xCF, 0xFE, 0x3F, 0xF8, 0x3F, 0xC0, 0x38, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xF0, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, - 0x07, 0xC0, 0xF8, 0x3F, 0xF0, 0x79, 0xE0, 0xF3, 0xE3, 0xE7, 0xC7, 0x87, 0x8F, 0x0F, 0xBE, 0x0F, - 0x78, 0x1E, 0xF0, 0x3F, 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xFC, 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xC0, - 0x3F, 0x80, 0xFE, 0x01, 0xF8, 0x00, 0xC0, 0x00, 0x00, 0xE0, 0x00, 0x3E, 0x00, 0x07, 0xC0, 0x00, - 0xF8, 0x00, 0x1F, 0x00, 0x0F, 0xF8, 0x07, 0xFF, 0xC1, 0xFF, 0xFC, 0x7F, 0xFF, 0xCF, 0xBE, 0x79, - 0xE7, 0xCF, 0x3C, 0xF9, 0xF7, 0x9F, 0x3E, 0xF3, 0xE7, 0xDE, 0x7C, 0xF3, 0xEF, 0xBE, 0x3F, 0xFF, - 0xC3, 0xFF, 0xF0, 0x3F, 0xFC, 0x01, 0xFC, 0x00, 0x1F, 0x00, 0x03, 0xE0, 0x00, 0x7C, 0x00, 0x0F, - 0x80, 0x00, 0x00, 0x00, 0x78, 0x7D, 0xF1, 0xE3, 0xCF, 0x8F, 0xBC, 0x1F, 0xE0, 0x7F, 0x80, 0xFC, - 0x03, 0xF0, 0x0F, 0xE0, 0x7F, 0x83, 0xFF, 0x0F, 0x3E, 0x7C, 0x7B, 0xE1, 0xF0, 0xF0, 0x79, 0xF0, - 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, 0x3E, 0x1E, 0x7C, 0x3C, 0xF8, 0x79, 0xF0, 0xF3, - 0xE1, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0xF0, - 0xFF, 0xC7, 0xFE, 0x3F, 0xF1, 0xFF, 0x8F, 0xFC, 0x7F, 0xE3, 0xEF, 0xFF, 0x7F, 0xF9, 0xFF, 0xC0, - 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0xF0, 0xF1, 0xFF, 0x1E, 0x3F, 0xE3, 0xC7, 0xFC, 0x78, 0xFF, - 0x8F, 0x1F, 0xF1, 0xE3, 0xFE, 0x3C, 0x7F, 0xC7, 0x8F, 0xF8, 0xF1, 0xFF, 0x1E, 0x3F, 0xE3, 0xC7, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xF0, 0xF1, 0xE7, 0xC7, 0x8F, 0x3E, 0x3C, 0x79, - 0xF1, 0xE3, 0xCF, 0x8F, 0x1E, 0x7C, 0x78, 0xF3, 0xE3, 0xC7, 0x9F, 0x1E, 0x3C, 0xF8, 0xF1, 0xE7, - 0xC7, 0x8F, 0x3E, 0x3C, 0x79, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x01, 0xE0, - 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x03, 0xC0, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, - 0x1F, 0xF0, 0x1F, 0xFC, 0x1F, 0xFE, 0x1F, 0xFF, 0x1F, 0x0F, 0x1F, 0x0F, 0x1F, 0x1F, 0x1F, 0xFF, - 0x1F, 0xFE, 0x1F, 0xFC, 0xF0, 0x07, 0xFE, 0x01, 0xFF, 0x80, 0x7F, 0xE0, 0x1F, 0xFF, 0x07, 0xFF, - 0xF9, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xF8, 0xFF, 0xFE, 0x1F, 0xFF, 0x8F, 0xFF, 0xFF, 0xDF, 0xFF, - 0xF7, 0xFF, 0xF9, 0xF0, 0xF0, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xF0, 0x7F, 0xF3, 0xFF, 0xDF, - 0xFF, 0xF8, 0xFF, 0xC3, 0xFE, 0x3F, 0xFF, 0xEF, 0xFF, 0x7F, 0xF0, 0x0F, 0xC0, 0xFF, 0xC3, 0xFF, - 0x1F, 0xFE, 0x78, 0x7C, 0xC1, 0xF0, 0x7F, 0xC1, 0xFF, 0x03, 0xFD, 0xE1, 0xF7, 0x87, 0x9F, 0xFE, - 0x3F, 0xF0, 0x7F, 0x80, 0x70, 0x00, 0xF0, 0x3F, 0x0F, 0x0F, 0xF8, 0xF1, 0xFF, 0xCF, 0x1F, 0xFE, - 0xF3, 0xE1, 0xEF, 0xFE, 0x1F, 0xFF, 0xC1, 0xFF, 0xFC, 0x1F, 0xFF, 0xC1, 0xFF, 0x3E, 0x1F, 0xF3, - 0xE3, 0xEF, 0x1F, 0xFE, 0xF0, 0xFF, 0xCF, 0x07, 0xF8, 0x00, 0x1C, 0x00, 0x0F, 0xF9, 0xFF, 0xDF, - 0xFE, 0xFF, 0xF7, 0x87, 0xBC, 0x3D, 0xFF, 0xEF, 0xFF, 0x3F, 0xF8, 0xFF, 0xCF, 0x9E, 0x78, 0xF7, - 0xC7, 0xFC, 0x3C, 0x1F, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xE0, 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, - 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, - 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x1C, 0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0x1C, 0x00, 0x00, 0x1F, - 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, - 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x1C, 0x00, 0x78, 0x03, 0xFF, 0x07, 0xFE, - 0x07, 0xFC, 0x07, 0x9F, 0x0F, 0xFF, 0x1F, 0xFF, 0x3F, 0xFE, 0x7C, 0x7C, 0xF8, 0x79, 0xF0, 0xF3, - 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, 0x3E, 0x1E, 0x7C, 0x3C, 0xF8, 0x78, 0x00, 0xF0, 0x01, - 0xE0, 0x1F, 0xC0, 0x3F, 0x80, 0x7E, 0x00, 0x70, 0x07, 0x81, 0xC0, 0xF0, 0x38, 0x00, 0x3F, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x80, - 0x07, 0xC0, 0x7F, 0xC3, 0xFF, 0x9F, 0xFF, 0x7C, 0x3D, 0xE0, 0xE7, 0xF8, 0x1F, 0xE0, 0x7F, 0x81, - 0xE0, 0xF7, 0xC7, 0xCF, 0xFE, 0x3F, 0xF8, 0x3F, 0xC0, 0x38, 0x00, 0x0F, 0xC1, 0xFF, 0x9F, 0xFC, - 0xF9, 0xF7, 0x87, 0x3F, 0x01, 0xFF, 0x87, 0xFE, 0x0F, 0xF8, 0x87, 0xDE, 0x1E, 0xFF, 0xF3, 0xFF, - 0x0F, 0xF0, 0x1E, 0x00, 0xF7, 0xFE, 0xE0, 0x7B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, - 0x73, 0xFE, 0xF7, 0xBD, 0xC6, 0x00, 0x07, 0x81, 0xF0, 0x7C, 0x1F, 0x07, 0xC1, 0xF0, 0x7C, 0x1F, - 0x07, 0xC1, 0xF0, 0x7C, 0x1F, 0x07, 0xC1, 0xF0, 0x3C, 0x7C, 0xF8, 0xE0, 0x07, 0x8F, 0x9F, 0x3E, - 0x7C, 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x3E, 0x7C, 0xF9, 0xF3, 0xDF, 0xBF, 0x7C, 0x00, 0x1F, 0xFC, - 0x00, 0x3F, 0xF8, 0x00, 0x7F, 0xF0, 0x00, 0xFF, 0xE0, 0x01, 0xE3, 0xFC, 0x03, 0xC7, 0xFF, 0x07, - 0x8F, 0xFF, 0x1F, 0x1F, 0xFE, 0x3E, 0x3C, 0x3E, 0x7C, 0x78, 0x7C, 0xF0, 0xF0, 0xFF, 0xE1, 0xFF, - 0xEF, 0x83, 0xFF, 0xDF, 0x07, 0xFE, 0x00, 0xF0, 0x78, 0x07, 0xC3, 0xC0, 0x3E, 0x1E, 0x01, 0xF0, - 0xF0, 0x0F, 0x87, 0x80, 0x7F, 0xFF, 0xE3, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, - 0xC3, 0xFE, 0x1E, 0x1F, 0xF0, 0xFF, 0xFF, 0x87, 0xFF, 0x7C, 0x3F, 0xF0, 0x1C, 0x00, 0xFF, 0x83, - 0xFF, 0x07, 0xFE, 0x03, 0xC0, 0x07, 0x9F, 0x0F, 0xFF, 0x1F, 0xFF, 0x3F, 0xFE, 0x7C, 0x7C, 0xF8, - 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, 0x3E, 0x1E, 0x7C, 0x3C, 0xF8, 0x78, - 0x03, 0xC0, 0x0E, 0x00, 0x78, 0x01, 0xC0, 0x00, 0x03, 0xC1, 0xEF, 0x8F, 0xBE, 0x7C, 0xFB, 0xF3, - 0xEF, 0x8F, 0xFC, 0x3F, 0xE0, 0xFF, 0x83, 0xFF, 0x0F, 0xBE, 0x3E, 0xF8, 0xF9, 0xF3, 0xE3, 0xEF, - 0x8F, 0x80, 0x1E, 0x00, 0xF0, 0x03, 0xC0, 0x0E, 0x00, 0x00, 0x78, 0x3F, 0xE1, 0xFF, 0x1F, 0xF9, - 0xFF, 0xCF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0xF9, 0xFF, 0xCF, 0xFC, 0x7F, 0xC3, 0xFC, - 0x1E, 0x1C, 0x70, 0x3F, 0xE0, 0x3F, 0x80, 0x3E, 0x00, 0x00, 0x1F, 0x07, 0xFE, 0x0F, 0x3C, 0x1E, - 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, 0xC1, 0xEF, 0x03, 0xDE, 0x07, 0xF8, 0x07, 0xF0, 0x0F, - 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x78, 0x07, 0xF0, 0x1F, 0xC0, 0x3F, 0x00, 0x18, 0x00, - 0xF0, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, - 0xFE, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x00, 0x70, 0x78, - 0x3F, 0xE1, 0xE0, 0xFF, 0x87, 0x83, 0xFE, 0x1E, 0x0F, 0xF8, 0x78, 0x3F, 0xE1, 0xE0, 0xFF, 0x87, - 0x83, 0xFE, 0x1E, 0x0F, 0xF8, 0x78, 0x3F, 0xE1, 0xE0, 0xFF, 0x87, 0x83, 0xFE, 0x1E, 0x0F, 0xF8, - 0x78, 0x3F, 0xE1, 0xE0, 0xFF, 0x87, 0xC7, 0xDF, 0x3F, 0x1F, 0x7F, 0xFF, 0xF8, 0xFF, 0xFF, 0xE1, - 0xFC, 0xFE, 0x01, 0xC0, 0xE0, 0xF0, 0xF1, 0xFF, 0x8F, 0x1F, 0xF8, 0xF1, 0xFF, 0x8F, 0x1F, 0xF8, - 0xF1, 0xFF, 0x8F, 0x1F, 0xF8, 0xF1, 0xFF, 0x8F, 0x1F, 0xF8, 0xF1, 0xFF, 0x8F, 0x1F, 0xF9, 0xF9, - 0xF7, 0xFF, 0xFE, 0x7F, 0xFF, 0xE3, 0xF9, 0xFC, 0x0E, 0x07, 0x00, 0x1C, 0x00, 0x1F, 0x00, 0x3F, - 0xF8, 0x1F, 0xFC, 0x0F, 0xFE, 0x03, 0xFF, 0x00, 0xF8, 0x00, 0x7F, 0xF0, 0x3F, 0xFE, 0x1F, 0xFF, - 0x8F, 0xFF, 0xE7, 0xC1, 0xF3, 0xE0, 0x79, 0xF0, 0x3C, 0xF8, 0x3E, 0x7C, 0x3F, 0x3F, 0xFF, 0x1F, - 0xFF, 0x0F, 0xFF, 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xF0, 0x01, 0xE0, 0x1F, 0xFC, 0x3F, - 0xF8, 0x7F, 0xF0, 0x1E, 0x00, 0x3F, 0xC0, 0x7F, 0xE0, 0xFF, 0xE1, 0xFF, 0xE3, 0xC3, 0xC7, 0x87, - 0x8F, 0x1F, 0x1F, 0xFE, 0x3F, 0xF8, 0x7F, 0xE0, 0x70, 0x07, 0xC1, 0xF0, 0x3F, 0xE3, 0xE1, 0xFF, - 0xE7, 0xC7, 0xFF, 0xEF, 0x8F, 0xC7, 0xFF, 0x3E, 0x07, 0xFE, 0x7C, 0x07, 0xFC, 0xF0, 0x00, 0xFF, - 0xFF, 0x01, 0xFF, 0xFF, 0x03, 0xFF, 0xFE, 0x07, 0xFF, 0xF8, 0x0F, 0x9E, 0x00, 0x1F, 0x3E, 0x03, - 0xFE, 0x7C, 0x0F, 0xFC, 0x7F, 0x7E, 0xF8, 0x7F, 0xFD, 0xF0, 0x7F, 0xF3, 0xE0, 0x7F, 0xC0, 0x00, - 0x1C, 0x00, 0xF0, 0x3F, 0x1F, 0x1F, 0xF3, 0xE7, 0xFF, 0x7C, 0xFF, 0xFF, 0xBE, 0x3F, 0xF7, 0x83, - 0xFF, 0xFF, 0x07, 0xFF, 0xE0, 0xFF, 0xFC, 0x1F, 0x78, 0x3F, 0xEF, 0x8F, 0xFC, 0xFF, 0xEF, 0x9F, - 0xFD, 0xF0, 0xFF, 0x00, 0x07, 0x00, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x3F, 0x80, 0x1F, 0xE0, 0x07, - 0xF8, 0x01, 0xFF, 0x00, 0xFF, 0xC0, 0x3E, 0xF0, 0x0F, 0x3E, 0x07, 0xCF, 0x81, 0xF1, 0xE0, 0x7F, - 0xFC, 0x3F, 0xFF, 0x0F, 0xFF, 0xC3, 0xDE, 0xF9, 0xF7, 0xBE, 0x7D, 0xE7, 0xDE, 0x79, 0xFF, 0x9E, - 0x7C, 0x07, 0xC0, 0x0F, 0x80, 0x3F, 0x00, 0x7F, 0x00, 0xFE, 0x03, 0xFC, 0x07, 0xBC, 0x1F, 0x78, - 0x3F, 0xF8, 0x7F, 0xF1, 0xFF, 0xE3, 0xDF, 0xE7, 0xBB, 0xDE, 0x77, 0xC0, 0x70, 0x0F, 0x80, 0xF8, - 0x1F, 0x80, 0xF8, 0x1F, 0xC0, 0xF8, 0x1F, 0xC0, 0xF8, 0x3F, 0xE0, 0xF8, 0x3F, 0xE0, 0xF8, 0x3D, - 0xE0, 0xF8, 0x7D, 0xF0, 0xF8, 0x7D, 0xF0, 0xF8, 0x78, 0xF0, 0xF8, 0xF8, 0xF8, 0xFF, 0xFF, 0xF8, - 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFC, 0xF9, 0xF7, 0x7C, 0xF9, 0xE7, 0x3C, 0xFB, 0xE7, 0x3E, 0xFB, - 0xE7, 0x3E, 0xFB, 0xC7, 0x1E, 0xF0, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x7E, 0x0F, 0x87, 0xF0, 0xF8, - 0x7F, 0x0F, 0x8F, 0xF8, 0xF8, 0xF7, 0x8F, 0xFF, 0x78, 0xFF, 0xFF, 0xCF, 0xFF, 0xFC, 0xFB, 0xFF, - 0xCF, 0xBD, 0xDE, 0xFB, 0xDD, 0xEF, 0xFD, 0xDF, 0x3F, 0xFF, 0xC3, 0xFF, 0xFC, 0x3F, 0xFF, 0xC1, - 0xFF, 0xF8, 0x0F, 0x0F, 0x00, 0xF0, 0xF0, 0x07, 0x9E, 0x00, 0x7F, 0xE0, 0x03, 0xFC, 0x01, 0xFF, - 0xF8, 0x3F, 0xFF, 0xC7, 0xFF, 0xFE, 0x7C, 0xF3, 0xEF, 0x8F, 0x1F, 0xF8, 0xF1, 0xFF, 0x8F, 0x1F, - 0xF8, 0xF1, 0xFF, 0x8F, 0x1F, 0xF8, 0xF1, 0xF0, 0x7F, 0xFE, 0x1F, 0xFF, 0x0F, 0xFF, 0x83, 0xC7, - 0x81, 0xF7, 0x80, 0x7B, 0xC0, 0x3F, 0xE0, 0x7F, 0xFC, 0x7F, 0xFF, 0x7F, 0xFF, 0xBC, 0xF3, 0xFE, - 0x79, 0xFF, 0x3C, 0xFF, 0x9E, 0x7C, 0x70, 0x7F, 0xFF, 0x9F, 0x0F, 0xFF, 0xFB, 0xE0, 0xFF, 0xFE, - 0x7C, 0x1F, 0xFF, 0x8F, 0x81, 0xE0, 0xF1, 0xF0, 0x1E, 0x3C, 0x3E, 0x03, 0xEF, 0x87, 0xC0, 0x3D, - 0xE0, 0xF8, 0x07, 0xF8, 0x1F, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xEF, 0x8F, - 0x9F, 0x3F, 0xF1, 0xE3, 0xE3, 0xFE, 0x3C, 0x7C, 0x7F, 0xC7, 0x8F, 0x8F, 0xF8, 0xF1, 0xF1, 0xFF, - 0x1E, 0x3E, 0x3F, 0xE3, 0xC7, 0xC7, 0x80, 0xF0, 0xFF, 0xFC, 0xF8, 0xFF, 0xFC, 0xF8, 0x7F, 0xF8, - 0xF8, 0x3C, 0xF8, 0xF8, 0x3E, 0xF0, 0xF8, 0x1F, 0xE0, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFC, 0xFF, - 0xFF, 0xFE, 0xFF, 0xFF, 0xFE, 0xF9, 0xE7, 0x9E, 0xF9, 0xE7, 0x9E, 0xF9, 0xE7, 0x9E, 0xF9, 0xE7, - 0x9E, 0x38, 0x73, 0xFF, 0x1F, 0xC0, 0x78, 0x03, 0x03, 0xC0, 0x7F, 0xC7, 0xFE, 0x7F, 0xE0, 0x1F, - 0x01, 0xF0, 0x1F, 0x01, 0xE1, 0xFE, 0x1F, 0xC1, 0xFE, 0x1F, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x01, - 0xF3, 0xFF, 0x7F, 0xEF, 0xFC, 0xFE, 0x0F, 0x00, 0xF8, 0x07, 0xC0, 0x78, 0x01, 0x80, 0x3D, 0xE1, - 0xFE, 0x1F, 0xC0, 0xF8, 0x00, 0x07, 0xF8, 0x7F, 0xE7, 0xFE, 0x3F, 0xF0, 0x0F, 0x01, 0xF1, 0xFE, - 0x1F, 0xE1, 0xFF, 0x00, 0xF0, 0x0F, 0x1F, 0xF3, 0xFF, 0x7F, 0xEF, 0xF0, 0xF8, 0x07, 0xE0, 0x7E, - 0x03, 0xE0, 0x00, 0x00, 0x70, 0xF0, 0xEF, 0x8F, 0x0F, 0xF8, 0xF0, 0xFF, 0x8F, 0x0F, 0xF8, 0xF0, - 0xFF, 0x8F, 0x0F, 0xF8, 0xF0, 0xFF, 0x8F, 0x0F, 0xF8, 0xF1, 0xF7, 0x8F, 0x1E, 0x7C, 0xF3, 0xE3, - 0xFF, 0xFE, 0x1F, 0xFF, 0xC0, 0xFF, 0xF0, 0x03, 0xFC, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, - 0x00, 0x00, 0xF0, 0x00, 0x01, 0xE0, 0x00, 0x3E, 0x00, 0x07, 0xC0, 0x78, 0xF9, 0xEF, 0x1F, 0x3F, - 0xE3, 0xE7, 0xFC, 0x7C, 0xFF, 0x8F, 0x9F, 0xF1, 0xF3, 0xFE, 0x3E, 0x7F, 0xC7, 0xCF, 0xFC, 0xF9, - 0xEF, 0x9F, 0x3C, 0xFB, 0xEF, 0x9F, 0xFF, 0xF1, 0xFF, 0xFC, 0x1F, 0xFF, 0x00, 0x7F, 0x00, 0x07, - 0xC0, 0x00, 0xF8, 0x00, 0x1F, 0x00, 0x03, 0xE0, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x03, 0xFE, 0x01, - 0xFF, 0xE0, 0xFF, 0xF8, 0x7E, 0x3F, 0x3F, 0x03, 0xEF, 0x80, 0xFB, 0xE0, 0x1E, 0xFF, 0xFF, 0xBF, - 0xFF, 0xEF, 0xFF, 0xFB, 0xFF, 0xFE, 0xF8, 0x07, 0xBE, 0x03, 0xE7, 0xC1, 0xF9, 0xFD, 0xFC, 0x3F, - 0xFE, 0x07, 0xFF, 0x00, 0xFF, 0x80, 0x07, 0x00, 0x07, 0xC0, 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xE7, - 0xC7, 0xCF, 0x07, 0x9F, 0xFF, 0xBF, 0xFF, 0x79, 0xFE, 0xF0, 0x79, 0xF1, 0xF1, 0xFF, 0xC3, 0xFF, - 0x81, 0xFE, 0x00, 0xE0, 0x00, 0x70, 0x00, 0xDF, 0x00, 0x7D, 0xF0, 0x1F, 0xBE, 0x03, 0xF7, 0xC0, - 0xF8, 0x7C, 0x1F, 0x0F, 0x83, 0xC1, 0xF0, 0xF8, 0x1E, 0x1F, 0x03, 0xE3, 0xC0, 0x7C, 0xF8, 0x07, - 0x9F, 0x00, 0xFB, 0xC0, 0x0F, 0xF8, 0x01, 0xFF, 0x00, 0x3F, 0xC0, 0x03, 0xF8, 0x00, 0x7E, 0x00, - 0x0F, 0xC0, 0x00, 0xF8, 0x1F, 0xF0, 0x7D, 0xE1, 0xFB, 0xC3, 0xF7, 0xC7, 0x87, 0x9F, 0x0F, 0x3C, - 0x1F, 0x78, 0x1F, 0xF0, 0x3F, 0xC0, 0x7F, 0x80, 0x7E, 0x00, 0xFC, 0x00, 0xF8, 0x00, 0x1E, 0xF0, - 0x01, 0xEE, 0x00, 0x1D, 0xE0, 0x03, 0xDC, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xBE, 0x00, 0xFB, 0xE0, - 0x3F, 0x7C, 0x07, 0xEF, 0x81, 0xF0, 0xF8, 0x3E, 0x1F, 0x07, 0x83, 0xE1, 0xF0, 0x3C, 0x3E, 0x07, - 0xC7, 0x80, 0xF9, 0xF0, 0x0F, 0x3E, 0x01, 0xF7, 0x80, 0x1F, 0xF0, 0x03, 0xFE, 0x00, 0x7F, 0x80, - 0x07, 0xF0, 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x7B, 0xC0, 0x7F, 0x80, 0xF7, 0x80, 0xFF, 0x00, 0x00, - 0x1F, 0x03, 0xFE, 0x0F, 0xBC, 0x3F, 0x78, 0x7E, 0xF8, 0xF0, 0xF3, 0xE1, 0xE7, 0x83, 0xEF, 0x03, - 0xFE, 0x07, 0xF8, 0x0F, 0xF0, 0x0F, 0xC0, 0x1F, 0x80, 0x1F, 0x00, 0x03, 0xE0, 0x00, 0x00, 0x07, - 0xFC, 0x00, 0x00, 0x07, 0xFF, 0x80, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0x07, 0xE3, 0xF0, 0x00, 0x07, - 0xE0, 0x7D, 0xF0, 0x7F, 0xE0, 0x3E, 0xF8, 0x3F, 0xF0, 0x0F, 0x3C, 0x1E, 0xF0, 0x07, 0x9F, 0x1F, - 0x78, 0x03, 0xC7, 0x8F, 0x3C, 0x01, 0xE3, 0xC7, 0x9E, 0x00, 0xF1, 0xF7, 0xCF, 0x80, 0x78, 0x7B, - 0xC7, 0xC0, 0x7C, 0x3D, 0xE1, 0xF0, 0x7E, 0x1F, 0xF0, 0xFE, 0xFE, 0x07, 0xF0, 0x3F, 0xFE, 0x03, - 0xF8, 0x0F, 0xFE, 0x00, 0xF8, 0x03, 0xFE, 0x00, 0x7C, 0x00, 0x38, 0x00, 0x3E, 0x00, 0x00, 0x00, - 0x1E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x07, 0xC1, 0xE0, 0x78, 0x7F, 0xC7, 0xC1, 0xE3, 0xFF, 0x9F, 0x0F, 0x9F, 0xFF, - 0x3C, 0x3C, 0x7C, 0x7C, 0xF8, 0xF1, 0xE0, 0xF1, 0xE7, 0xC7, 0x83, 0xE7, 0x9E, 0x1E, 0x0F, 0x9F, - 0x78, 0x78, 0x3E, 0x3F, 0xE1, 0xE0, 0xF0, 0xFF, 0x07, 0xC7, 0xC3, 0xFC, 0x0F, 0xFE, 0x07, 0xE0, - 0x3F, 0xF8, 0x1F, 0x80, 0x3F, 0xC0, 0x3E, 0x00, 0x38, 0x00, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x00, - 0x00, 0xFE, 0x00, 0x00, 0x03, 0xF8, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x00, 0x08, 0x00, 0x03, 0xE0, - 0x1F, 0xF8, 0x3F, 0xFC, 0x7F, 0xFE, 0x7E, 0x3F, 0xF8, 0x1F, 0xF8, 0x0F, 0xF0, 0x00, 0xF0, 0x00, - 0xF0, 0x00, 0xF0, 0x00, 0xF8, 0x0F, 0xF8, 0x1F, 0x7E, 0x3F, 0x7F, 0xFF, 0x3F, 0xFF, 0x1F, 0xEF, - 0x03, 0x8F, 0x00, 0x0F, 0x07, 0xC0, 0x7F, 0xC3, 0xFF, 0x9F, 0xFF, 0x7C, 0x7D, 0xE0, 0xF7, 0x80, - 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0xC0, 0x0F, 0xF0, 0x3F, 0xC0, 0x3F, 0x00, 0x3C, 0x00, 0xF0, - 0x03, 0xC0, 0x0F, 0x00, 0x38, 0x00, 0x00, 0x18, 0x00, 0xE0, 0x47, 0x03, 0x9C, 0x0F, 0xE0, 0x0F, - 0x80, 0x1F, 0x80, 0x7F, 0x43, 0x9D, 0xDC, 0x07, 0xF0, 0x0F, 0x80, 0x0F, 0x80, 0x7F, 0x83, 0xCC, - 0x0E, 0x00, 0x70, 0x00, 0x01, 0xDF, 0xF7, 0xFD, 0xC0, 0x60, 0x00, 0x0F, 0xCF, 0xFD, 0xF3, 0x80, - 0x00, 0x0F, 0xF7, 0x30, 0x03, 0x9C, 0xE6, 0x00, 0x3F, 0x07, 0xFE, 0x63, 0xF0, 0x00, 0x00, 0x3C, - 0x00, 0x00, 0x3F, 0x00, 0x00, 0x19, 0x80, 0x01, 0x80, 0x07, 0x01, 0xF0, 0x07, 0xC1, 0x98, 0x03, - 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x03, 0x3E, 0x00, 0x03, - 0xF9, 0x80, 0x03, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x1E, 0x00, 0x7C, 0x1F, 0x80, 0x3E, 0x0C, 0xC0, 0x19, 0x00, 0x03, 0x80, 0x00, 0x03, 0xE0, 0x00, - 0x03, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x18, - 0x38, 0x00, 0x0C, 0x30, 0x38, 0x0E, 0x00, 0xF0, 0x06, 0x00, 0xE0, 0x04, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x18, 0xF8, 0x00, 0x1F, 0x38, 0x00, 0x0F, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x60, 0x0F, 0x00, 0xE0, 0x1E, 0x18, 0x70, 0x18, - 0x18, 0x30, 0x00, 0x18, 0x30, 0x00, 0x38, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, - 0x01, 0xFE, 0x00, 0x3F, 0xC0, 0x01, 0xF0, 0x00, 0x00, 0x00, 0xE0, 0x1E, 0x3E, 0x07, 0xE7, 0xC0, - 0xFC, 0xF8, 0x3F, 0x9F, 0x0F, 0xF3, 0xE1, 0xFE, 0x7C, 0x7F, 0xCF, 0x8F, 0xF9, 0xF3, 0xFF, 0x3E, - 0xFB, 0xE7, 0xDE, 0x7C, 0xFF, 0xCF, 0x9F, 0xF1, 0xF3, 0xFE, 0x3E, 0x7F, 0x87, 0xCF, 0xE0, 0xF9, - 0xFC, 0x1F, 0xBF, 0x03, 0xF7, 0xC0, 0x7E, 0x00, 0x07, 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x07, - 0x80, 0x38, 0xE0, 0x3F, 0xE0, 0x1F, 0xC0, 0x0F, 0x80, 0x00, 0x00, 0xF0, 0x78, 0xF8, 0x78, 0xF8, - 0xF8, 0xF9, 0xF8, 0xF9, 0xF8, 0xFB, 0xF8, 0xFF, 0xF8, 0xFF, 0xF8, 0xFF, 0x78, 0xFE, 0x78, 0xFE, - 0x78, 0xFC, 0x7E, 0xF8, 0x7E, 0xF0, 0x7E, 0x00, 0x3E, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x1C, - 0x00, 0x1F, 0x00, 0x3F, 0xF8, 0x1F, 0xFC, 0x0F, 0xFE, 0x03, 0xFF, 0x00, 0xF8, 0x00, 0x7F, 0xF0, - 0x3F, 0xFE, 0x1F, 0xFF, 0x8F, 0xFF, 0xE7, 0xC1, 0xF3, 0xE0, 0x79, 0xF0, 0x3C, 0xF8, 0x3E, 0x7C, - 0x3F, 0x3F, 0xFF, 0x1F, 0xFF, 0x0F, 0xFF, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x07, 0xFF, 0x0F, - 0xFE, 0x1F, 0xFC, 0x07, 0x80, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xF8, 0x7F, 0xF8, 0xF0, 0xF1, 0xE1, - 0xE3, 0xC7, 0xC7, 0xFF, 0x8F, 0xFE, 0x1F, 0xF8, 0x7F, 0x81, 0xFF, 0xE3, 0xFF, 0xE7, 0xFF, 0xEF, - 0x87, 0xDF, 0x07, 0xFE, 0x0F, 0xFC, 0xDF, 0xF9, 0xFF, 0xF3, 0xFB, 0xFF, 0xF7, 0xFF, 0xCF, 0xFF, - 0x1F, 0x07, 0x3E, 0x0C, 0x7C, 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xE0, 0x00, 0xF3, 0xE3, 0xFF, 0xCF, - 0xFF, 0xBF, 0xFE, 0xF8, 0x7F, 0xE1, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0xBF, 0xE7, 0xFF, 0xDF, 0xFF, - 0xFE, 0xFF, 0xFB, 0xFF, 0xEF, 0x9B, 0xBE, 0x00, 0xF8, 0x03, 0xE0, 0x0F, 0x80, 0x00, 0x00, 0x00, - 0x78, 0x03, 0xC0, 0x1E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF8, 0x07, 0xC0, 0x3E, - 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, - 0x80, 0x7C, 0x00, 0x03, 0xC0, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xF8, 0x3E, 0x0F, - 0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x00, 0x1F, 0xFC, 0x7F, 0xFC, 0xFF, 0xF9, 0xFF, - 0xF3, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0xFF, 0xE1, 0xFF, 0xC3, 0xFF, 0x83, 0xFF, 0x03, - 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x00, 0xF8, 0x00, 0x3F, 0xF3, 0xFF, - 0x3F, 0xF3, 0xFF, 0x3E, 0x07, 0xFC, 0xFF, 0xCF, 0xFC, 0x3E, 0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x3E, - 0x03, 0xE0, 0x7F, 0xF1, 0xFF, 0xF3, 0xFF, 0xE7, 0xFF, 0xCF, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, - 0xC0, 0xFF, 0xF1, 0xFF, 0xF3, 0xFF, 0xF7, 0xFF, 0xEF, 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, - 0xF8, 0x7D, 0xF7, 0xFB, 0xEF, 0xE0, 0x1F, 0x80, 0x3C, 0x00, 0xFF, 0xC7, 0xFE, 0x3F, 0xF1, 0xFF, - 0x8F, 0x80, 0x7C, 0x03, 0xFE, 0x1F, 0xFC, 0xFF, 0xF7, 0xFF, 0xBE, 0x3D, 0xF0, 0xFF, 0x87, 0xFC, - 0x78, 0x03, 0xC0, 0xFE, 0x07, 0xE0, 0x3E, 0x01, 0x80, 0x78, 0x1E, 0x07, 0x87, 0xC1, 0xE0, 0xFC, - 0x7E, 0x1E, 0x0F, 0x83, 0xE1, 0xE1, 0xF0, 0x1F, 0x1E, 0x1F, 0x01, 0xF1, 0xE3, 0xE0, 0x0F, 0x9E, - 0x3E, 0x00, 0x79, 0xE7, 0xC0, 0x07, 0xFF, 0xF8, 0x00, 0x3F, 0xFF, 0x80, 0x07, 0xFF, 0xF8, 0x00, - 0x7F, 0xFF, 0xC0, 0x0F, 0x9E, 0x3C, 0x01, 0xF1, 0xE3, 0xE0, 0x1F, 0x1E, 0x1F, 0x03, 0xE1, 0xE1, - 0xF0, 0x3E, 0x1E, 0x0F, 0xE7, 0xC1, 0xE0, 0xFE, 0xFC, 0x1E, 0x07, 0xE0, 0x00, 0x00, 0x1E, 0x00, - 0x00, 0x01, 0xE0, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x01, 0xE0, 0xF8, 0x78, 0xF8, 0xF8, 0xF1, 0xF1, - 0xF1, 0xE7, 0xC1, 0xF3, 0xDF, 0x01, 0xF7, 0xBC, 0x01, 0xFF, 0xF8, 0x03, 0xFF, 0xE0, 0x03, 0xFF, - 0xC0, 0x0F, 0xFF, 0xC0, 0x3E, 0xFF, 0x80, 0xFD, 0xEF, 0x81, 0xF3, 0xCF, 0xF7, 0xC7, 0x9F, 0xDF, - 0x0F, 0x1F, 0x80, 0x00, 0x0F, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x78, 0x07, 0x80, - 0x7F, 0x87, 0xFF, 0x9F, 0xFE, 0xF8, 0x7F, 0xE1, 0xF0, 0x07, 0xC0, 0x3E, 0x07, 0xF8, 0x1F, 0x80, - 0x7F, 0x80, 0x7F, 0x00, 0x7F, 0xC0, 0xFF, 0x03, 0xFE, 0x1F, 0x7F, 0xFD, 0xFF, 0xE1, 0xFE, 0x01, - 0xE0, 0x03, 0xC0, 0x0F, 0x80, 0x3E, 0x01, 0xF0, 0x00, 0x00, 0x0F, 0x81, 0xFF, 0x1F, 0xF8, 0xFB, - 0xE7, 0x8F, 0x00, 0xF8, 0x1F, 0x80, 0xFC, 0x07, 0xF3, 0x87, 0x9E, 0x3C, 0xFF, 0xE7, 0xFF, 0x0F, - 0xE0, 0x1C, 0x00, 0xF0, 0x03, 0xC0, 0x3E, 0x01, 0xE0, 0x00, 0x00, 0x70, 0x0F, 0x3E, 0x07, 0xCF, - 0x83, 0xE3, 0xE1, 0xF0, 0xF8, 0x7C, 0x3E, 0x3E, 0x0F, 0x9F, 0x03, 0xEF, 0x80, 0xFF, 0xE0, 0x3F, - 0xF0, 0x0F, 0xFC, 0x03, 0xFF, 0x80, 0xF9, 0xF0, 0x3E, 0x3E, 0x0F, 0x8F, 0x83, 0xE1, 0xF0, 0xF8, - 0x3F, 0xFE, 0x0F, 0xFF, 0x81, 0xFC, 0x00, 0x1F, 0x00, 0x07, 0xC0, 0x01, 0xF0, 0x00, 0x7C, 0xF0, - 0x79, 0xF1, 0xF3, 0xE7, 0xC7, 0xDF, 0x8F, 0xBE, 0x1F, 0xF8, 0x3F, 0xE0, 0x7F, 0xC0, 0xFF, 0xC1, - 0xF7, 0xC3, 0xEF, 0x87, 0xCF, 0xFF, 0x8F, 0xFF, 0x1F, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, - 0x7C, 0x70, 0x0F, 0x7C, 0x0F, 0xBE, 0x0F, 0x9F, 0x07, 0xCF, 0x87, 0xC7, 0xFB, 0xE3, 0xFF, 0xE1, - 0xFF, 0xF0, 0xFF, 0xF0, 0x7F, 0xF8, 0x3F, 0xFC, 0x1F, 0xFF, 0x0F, 0xFF, 0x87, 0xFB, 0xE3, 0xFD, - 0xF1, 0xF0, 0x7C, 0xF8, 0x3E, 0x7C, 0x0F, 0xBE, 0x07, 0xC0, 0xF0, 0x3F, 0xF0, 0xFB, 0xE1, 0xF7, - 0xF7, 0xCF, 0xEF, 0x9F, 0xFE, 0x3F, 0xFC, 0x7F, 0xF0, 0xFF, 0xF1, 0xFD, 0xF3, 0xFB, 0xE7, 0xF3, - 0xEF, 0x87, 0xDF, 0x07, 0xC0, 0x7F, 0x80, 0x7B, 0xFF, 0x03, 0xEF, 0xFC, 0x1F, 0x3F, 0xF0, 0xFC, - 0x07, 0xC3, 0xE0, 0x1F, 0x1F, 0x00, 0x7C, 0xF8, 0x01, 0xF3, 0xE0, 0x07, 0xFF, 0x00, 0x1F, 0xF8, - 0x00, 0x7F, 0xF0, 0x01, 0xFF, 0xC0, 0x07, 0xCF, 0x80, 0x1F, 0x1F, 0x00, 0x7C, 0x7E, 0x01, 0xF0, - 0xF8, 0x07, 0xC1, 0xF0, 0x1F, 0x03, 0xE0, 0x7C, 0x0F, 0xC0, 0xFF, 0x0F, 0xFF, 0x8F, 0xBF, 0xCF, - 0xDF, 0xE7, 0xC1, 0xF7, 0xC0, 0xFF, 0xC0, 0x7F, 0xE0, 0x3F, 0xE0, 0x1F, 0xF8, 0x0F, 0xBE, 0x07, - 0xDF, 0x83, 0xE7, 0xC1, 0xF1, 0xF0, 0xF8, 0x7C, 0x70, 0x0F, 0x3E, 0x03, 0xCF, 0x80, 0xF3, 0xE0, - 0x3C, 0xF8, 0x0F, 0x3E, 0x03, 0xCF, 0x80, 0xF3, 0xE0, 0x3C, 0xFF, 0xFF, 0x3F, 0xFF, 0xCF, 0xFF, - 0xF3, 0xFF, 0xFC, 0xF8, 0x0F, 0x3E, 0x03, 0xCF, 0x80, 0xF3, 0xE0, 0x3C, 0xF8, 0x0F, 0xFE, 0x03, - 0xFF, 0x80, 0xFC, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xF0, 0x00, 0x3C, 0xF0, 0x79, 0xF0, 0xF3, - 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0xFF, 0x3F, 0xFE, 0x7F, 0xFC, 0xFF, 0xF9, 0xF0, 0xF3, 0xE1, - 0xE7, 0xC3, 0xFF, 0x87, 0xFF, 0x0F, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x70, 0x0F, - 0xFE, 0xF8, 0x0F, 0xFF, 0xF8, 0x0F, 0xFF, 0xF8, 0x0F, 0xFE, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, - 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, - 0xFF, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, - 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xF0, 0x7F, 0xFC, 0x3F, 0xFE, 0x1F, 0xFF, 0x0F, 0xEF, - 0x87, 0x87, 0xFF, 0xC3, 0xFF, 0xE1, 0xFF, 0xF0, 0xFF, 0xF8, 0x7C, 0x3C, 0x3E, 0x1E, 0x1F, 0x0F, - 0x0F, 0x87, 0x87, 0xC3, 0xC0, 0x7F, 0xFF, 0x00, 0x1F, 0xFF, 0xE0, 0x03, 0xFF, 0xFC, 0x00, 0x7F, - 0xFF, 0x80, 0x0F, 0x80, 0xF0, 0x01, 0xF0, 0x1E, 0x00, 0x3E, 0x03, 0xC0, 0x07, 0xC0, 0x79, 0xC0, - 0xF8, 0x0F, 0xFE, 0x1F, 0x01, 0xFF, 0xF3, 0xE0, 0x3F, 0xFE, 0x7C, 0x07, 0xFF, 0xEF, 0x80, 0xF8, - 0x7D, 0xF0, 0x1E, 0x07, 0xBE, 0x03, 0xC0, 0xF7, 0xC0, 0x78, 0x1E, 0xF8, 0x0F, 0x0F, 0xDF, 0x01, - 0xEF, 0xF3, 0xE0, 0x3D, 0xFE, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x03, 0x80, 0xFF, 0xF8, 0x03, 0xFF, - 0xE0, 0x0F, 0xFF, 0x80, 0x3F, 0xFE, 0x00, 0xF8, 0x78, 0x03, 0xE1, 0xF8, 0x0F, 0x87, 0xFE, 0x3E, - 0x1F, 0xFC, 0xF8, 0x7F, 0xFB, 0xE1, 0xE3, 0xEF, 0x87, 0x87, 0xFE, 0x1E, 0x0F, 0xF8, 0x78, 0x3F, - 0xE1, 0xE1, 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x7E, 0x00, 0x01, 0xE0, 0x00, 0x03, 0x00, 0x03, 0x80, - 0x00, 0x07, 0xC0, 0x00, 0x07, 0xE1, 0xF0, 0x07, 0xF3, 0xFE, 0x07, 0xE3, 0xFF, 0x87, 0xE1, 0xFF, - 0xE3, 0xE1, 0xF9, 0xF9, 0xF0, 0xF8, 0x7C, 0xF0, 0x78, 0x1E, 0x78, 0x3C, 0x0F, 0x3C, 0x1E, 0x0F, - 0x9E, 0x0F, 0x87, 0xCF, 0x87, 0xC7, 0xC7, 0xC1, 0xF7, 0xE1, 0xF0, 0xFF, 0xE0, 0xFF, 0x3F, 0xE4, - 0x3F, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xC0, 0x1E, 0x07, 0x80, 0x07, 0x00, 0x00, - 0xF8, 0x00, 0x0F, 0xCF, 0xC0, 0xFE, 0xFF, 0x87, 0xCF, 0xFC, 0x3C, 0x7F, 0xF1, 0xE3, 0xCF, 0x8F, - 0x1E, 0x3C, 0x78, 0xF3, 0xE3, 0xE7, 0xDE, 0x1F, 0xBF, 0xF0, 0x7F, 0xFF, 0xE1, 0xFF, 0xFF, 0x07, - 0xFF, 0xFC, 0x07, 0x87, 0x00, 0x03, 0xE0, 0x07, 0xFC, 0x07, 0xFF, 0x87, 0xFF, 0xC7, 0xE3, 0xF7, - 0xE0, 0x7B, 0xE0, 0x3F, 0xF0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x80, - 0x77, 0xC0, 0x7D, 0xF0, 0x7C, 0xFE, 0xFE, 0x3F, 0xFE, 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, 0x00, - 0x1E, 0x00, 0x07, 0x80, 0x07, 0xC0, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x7F, 0xC3, 0xFF, - 0x9F, 0xFF, 0x7C, 0x7D, 0xE0, 0xE7, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0xF7, 0xC7, 0xCF, 0xFE, - 0x3F, 0xF8, 0x3F, 0xC0, 0x38, 0x00, 0xF0, 0x01, 0xE0, 0x0F, 0x80, 0x3C, 0x00, 0x00, 0x7F, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, - 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xF0, - 0x03, 0xF0, 0x03, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xF0, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0xC0, - 0xFC, 0x07, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0x78, 0x07, 0xBF, 0x03, 0xF7, 0xC0, - 0xF8, 0xF8, 0x7C, 0x3E, 0x1F, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x3F, 0xF0, 0x07, 0xF8, 0x01, 0xFE, - 0x00, 0x3F, 0x00, 0x0F, 0xC0, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, - 0x00, 0x78, 0x00, 0x1E, 0x00, 0xF8, 0x3F, 0xF0, 0x79, 0xE0, 0xF3, 0xE3, 0xE7, 0xC7, 0x87, 0x8F, - 0x0F, 0xBE, 0x0F, 0x78, 0x1E, 0xF0, 0x3F, 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xFC, 0x00, 0xF8, 0x01, - 0xF0, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x78, 0x07, 0xBF, 0x03, 0xF7, - 0xC0, 0xF8, 0xF8, 0x7C, 0x3E, 0x1F, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x3F, 0xF0, 0x07, 0xF8, 0x01, - 0xFE, 0x00, 0x3F, 0x00, 0x0F, 0xC0, 0x01, 0xE0, 0x03, 0xFF, 0x81, 0xFF, 0xE0, 0x7F, 0xF8, 0x0F, - 0xFC, 0x00, 0x78, 0x00, 0x1E, 0x00, 0xF8, 0x3F, 0xF0, 0x79, 0xE0, 0xF3, 0xE3, 0xE7, 0xC7, 0x87, - 0x8F, 0x0F, 0xBE, 0x0F, 0x78, 0x1E, 0xF0, 0x3F, 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xFC, 0x03, 0xFE, - 0x07, 0xFC, 0x0F, 0xF8, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x78, 0x07, 0x8F, 0x81, - 0xF0, 0xF8, 0x7C, 0x1F, 0x8F, 0x81, 0xF3, 0xE0, 0x1F, 0x78, 0x03, 0xFF, 0x00, 0x3F, 0xC0, 0x03, - 0xF0, 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x03, 0xFC, 0x00, 0x7F, 0xC0, 0x1F, 0xF8, 0x07, 0xCF, 0x80, - 0xF8, 0xF8, 0x3E, 0x1F, 0xEF, 0x81, 0xFF, 0xF0, 0x1F, 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x03, - 0xC0, 0x00, 0x78, 0x78, 0x7C, 0x7C, 0x78, 0x3C, 0xF8, 0x3E, 0xF0, 0x1F, 0xE0, 0x1F, 0xE0, 0x0F, - 0xC0, 0x0F, 0xC0, 0x0F, 0xE0, 0x1F, 0xE0, 0x3F, 0xF0, 0x3C, 0xFE, 0x7C, 0x7E, 0xF8, 0x7E, 0x00, - 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x7F, 0xFE, 0x38, 0xFF, 0xFF, 0x7C, 0xFF, 0xFF, 0x7C, - 0xFF, 0xFF, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, - 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, - 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, - 0x00, 0x00, 0x1F, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1E, 0xFF, 0x9E, 0x3F, 0xE7, - 0x8F, 0xF9, 0xE3, 0xFE, 0x78, 0x1E, 0x1E, 0x07, 0x87, 0x81, 0xE1, 0xE0, 0x78, 0x78, 0x1E, 0x1E, - 0x07, 0x87, 0x81, 0xE1, 0xE0, 0x7F, 0xFE, 0x1F, 0xFF, 0x87, 0xFF, 0xE0, 0x00, 0x78, 0x00, 0x1E, - 0x00, 0x07, 0x80, 0x01, 0xE0, 0x70, 0x1E, 0x7C, 0x0F, 0x3E, 0x07, 0x9F, 0x03, 0xCF, 0x81, 0xE7, - 0xC0, 0xF3, 0xE0, 0x79, 0xF0, 0x3C, 0xF8, 0x1E, 0x3F, 0xFF, 0x1F, 0xFF, 0x87, 0xFF, 0xC0, 0xFD, - 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x00, 0xF0, - 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0xF0, 0xF9, 0xF1, 0xF3, 0xE3, 0xE7, 0xC7, 0xCF, 0x8F, 0x9F, - 0x1F, 0x3E, 0x3E, 0x3F, 0xFC, 0x7F, 0xF8, 0x7F, 0xF0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, - 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x70, 0x1F, 0xF0, 0x3F, 0xE0, 0x7F, 0xC0, 0xFF, - 0x81, 0xFF, 0x03, 0xFE, 0xE7, 0xFD, 0xCF, 0xFB, 0x9E, 0xFF, 0xFD, 0xFF, 0xF9, 0xFF, 0xF0, 0xFD, - 0xE0, 0x73, 0xC0, 0xE7, 0x80, 0xCF, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0xF0, 0xFF, 0xC7, 0xFE, - 0x3F, 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0x7F, 0xF9, 0xFF, 0xC1, 0xFE, 0x0F, 0xF0, - 0x7F, 0x83, 0xFC, 0x70, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0xF8, - 0x00, 0xFF, 0xF0, 0xFF, 0xFC, 0xFF, 0xFC, 0xFF, 0xFE, 0xF8, 0x3E, 0xF8, 0x1E, 0xF8, 0x1E, 0xF8, - 0x1E, 0xF8, 0x1E, 0xF8, 0x1E, 0xF8, 0x1E, 0xF8, 0x1E, 0x70, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, - 0x00, 0x79, 0xF3, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xC7, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3F, - 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0xFE, 0x1E, 0x00, 0x1F, 0x00, 0x03, 0xFF, 0x00, 0x1F, 0xFE, - 0x00, 0xFF, 0xFC, 0x03, 0xF1, 0xF0, 0x1F, 0x03, 0xEF, 0x78, 0x07, 0xBF, 0xE0, 0x1E, 0xFF, 0xFF, - 0xFB, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0x87, 0xFF, 0xFE, 0x0F, 0x80, 0x00, 0x1F, 0x01, 0xE0, 0x7C, - 0x0F, 0x80, 0xFC, 0xFC, 0x03, 0xFF, 0xF0, 0x07, 0xFF, 0x80, 0x07, 0xFC, 0x00, 0x03, 0x80, 0x00, - 0x7C, 0x00, 0x7F, 0xC0, 0x3F, 0xFB, 0x9F, 0xBE, 0xE7, 0x83, 0xFF, 0xE0, 0xF7, 0xFF, 0xFC, 0xFF, - 0xFF, 0x0F, 0xFF, 0xC1, 0xE0, 0x00, 0x7C, 0x3C, 0x0F, 0xFF, 0x03, 0xFF, 0x80, 0x3F, 0xC0, 0x03, - 0x80, 0x00, 0x1F, 0x00, 0x03, 0xFF, 0x00, 0x1F, 0xFE, 0x00, 0xFF, 0xFC, 0x03, 0xF1, 0xF0, 0x1F, - 0x03, 0xEF, 0x78, 0x07, 0xBF, 0xE0, 0x1E, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0x87, - 0xFF, 0xFE, 0x0F, 0x80, 0x00, 0x1F, 0x01, 0xE0, 0x7C, 0x0F, 0x80, 0xFC, 0xFC, 0x03, 0xFF, 0xF0, - 0x07, 0xFF, 0x80, 0x07, 0xFC, 0x00, 0x07, 0x80, 0x00, 0x1E, 0x00, 0x00, 0x78, 0x00, 0x01, 0xE0, - 0x00, 0x00, 0x7C, 0x00, 0x7F, 0xC0, 0x3F, 0xFB, 0x9F, 0xBE, 0xE7, 0x83, 0xFF, 0xE0, 0xF7, 0xFF, - 0xFC, 0xFF, 0xFF, 0x0F, 0xFF, 0xC1, 0xE0, 0x00, 0x7C, 0x3C, 0x0F, 0xFF, 0x03, 0xFF, 0x80, 0x3F, - 0xC0, 0x03, 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x0F, 0x80, 0x03, 0xC0, 0x00, 0x00, 0x77, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x71, 0xC0, 0x00, 0x1F, 0xE0, - 0x00, 0x07, 0xF8, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x07, 0x81, 0xE7, 0xC1, 0xE0, - 0xFD, 0xF8, 0x78, 0x3E, 0x3E, 0x1E, 0x1F, 0x07, 0xC7, 0x87, 0xC1, 0xF1, 0xE3, 0xE0, 0x3E, 0x78, - 0xF8, 0x07, 0x9E, 0x7C, 0x01, 0xFF, 0xFE, 0x00, 0x3F, 0xFF, 0x80, 0x1F, 0xFF, 0xE0, 0x07, 0xFF, - 0xFC, 0x03, 0xE7, 0x8F, 0x01, 0xF1, 0xE3, 0xE0, 0x7C, 0x78, 0x7C, 0x3E, 0x1E, 0x1F, 0x0F, 0x87, - 0x83, 0xE7, 0xC1, 0xE0, 0xFB, 0xF0, 0x78, 0x1F, 0x03, 0x8E, 0x00, 0x1F, 0xF0, 0x00, 0x7F, 0x00, - 0x01, 0xF0, 0x00, 0x00, 0x00, 0x7C, 0x3C, 0x7D, 0xF1, 0xE3, 0xEF, 0x8F, 0x3E, 0x3E, 0x7B, 0xE0, - 0xFB, 0xDE, 0x03, 0xFF, 0xF0, 0x1F, 0xFF, 0x00, 0x7F, 0xF8, 0x07, 0xFF, 0xE0, 0x7D, 0xFF, 0x07, - 0xEF, 0x7C, 0x3E, 0x79, 0xF3, 0xE3, 0xCF, 0xBE, 0x1E, 0x3E, 0x70, 0x1E, 0xF8, 0x3E, 0xF8, 0x7C, - 0xF8, 0xFC, 0xF8, 0xF8, 0xF9, 0xF0, 0xFB, 0xE0, 0xFB, 0xE0, 0xFF, 0xC0, 0xFF, 0xF0, 0xFF, 0xF8, - 0xFF, 0xFC, 0xF8, 0x7E, 0xF8, 0x1E, 0xF8, 0x1E, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1E, 0xF8, 0x1E, - 0x00, 0x3E, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xF8, 0x00, 0xC0, 0xF0, 0x7B, 0xE3, 0xEF, 0x9F, 0x3E, - 0xFC, 0xFB, 0xE3, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0xFF, 0xFB, 0xE3, 0xEF, 0x87, 0xFE, 0x0F, 0xF8, - 0x3F, 0xE1, 0xF0, 0x0F, 0x80, 0x7E, 0x00, 0xF0, 0x03, 0x00, 0x07, 0xFF, 0x80, 0xFF, 0xF8, 0x0F, - 0xFF, 0x80, 0xFF, 0xF8, 0x0F, 0x07, 0x80, 0xF0, 0x78, 0x0F, 0x07, 0x80, 0xF0, 0x78, 0x0F, 0x07, - 0x80, 0xF0, 0x78, 0x0F, 0x07, 0x81, 0xF0, 0x78, 0x1F, 0x07, 0x81, 0xF0, 0x78, 0x1E, 0x07, 0x87, - 0xE0, 0x78, 0xFE, 0x07, 0xFF, 0xC0, 0x7E, 0xF8, 0x07, 0xE0, 0x00, 0x3E, 0x00, 0x03, 0xC0, 0x00, - 0x3C, 0x00, 0x03, 0xC0, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3E, 0x7C, 0x3E, 0x7C, - 0x3E, 0x7C, 0x3E, 0x7C, 0x3C, 0x7C, 0x3C, 0x7C, 0x7C, 0x7C, 0xFC, 0x7F, 0xF8, 0x7F, 0xF0, 0x7E, - 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x3E, 0x00, 0x3C, 0x70, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, - 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0x00, 0x0F, - 0x00, 0x0F, 0x00, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x38, 0xF0, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, - 0xFF, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, - 0x3C, 0x01, 0xE0, 0x0F, 0x03, 0xF8, 0x1F, 0xC0, 0xFC, 0x03, 0x80, 0x70, 0x0F, 0x1F, 0x01, 0xE3, - 0xE0, 0x3C, 0x7C, 0x07, 0x8F, 0x80, 0xF1, 0xF0, 0x1E, 0x3E, 0x03, 0xC7, 0xC0, 0x78, 0xFF, 0xFF, - 0x1F, 0xFF, 0xE3, 0xFF, 0xFC, 0x7F, 0xFF, 0x8F, 0x80, 0xF1, 0xF0, 0x1E, 0x3E, 0x03, 0xC7, 0xC0, - 0x78, 0xF8, 0x0F, 0xDF, 0x01, 0xFB, 0xE0, 0x3F, 0x00, 0x03, 0xE0, 0x00, 0x78, 0x00, 0x0F, 0x00, - 0x01, 0xE0, 0xF0, 0x78, 0xF8, 0x78, 0xF8, 0x78, 0xF8, 0x78, 0xF8, 0x78, 0xFF, 0xF8, 0xFF, 0xF8, - 0xFF, 0xF8, 0xFF, 0xF8, 0xF8, 0x78, 0xF8, 0x78, 0xF8, 0x7E, 0xF8, 0x7E, 0xF8, 0x7E, 0x00, 0x3E, - 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x7C, 0x70, 0x1E, 0x7C, 0x0F, 0x3E, 0x07, 0x9F, 0x03, 0xCF, 0x81, - 0xE7, 0xC0, 0xF3, 0xE0, 0x79, 0xF0, 0x3C, 0xF8, 0x1E, 0x3F, 0xFF, 0x1F, 0xFF, 0x87, 0xFF, 0xC0, - 0xFD, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x00, - 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0xF0, 0xF9, 0xF1, 0xF3, 0xE3, 0xE7, 0xC7, 0xCF, 0x8F, - 0x9F, 0x1F, 0x3E, 0x3E, 0x3F, 0xFC, 0x7F, 0xF8, 0x7F, 0xF0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xE0, - 0x1F, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x7C, 0x01, 0xF1, 0xFC, 0x07, 0xF3, 0xF8, - 0x0F, 0xE7, 0xF8, 0x1F, 0xCF, 0xF0, 0x7F, 0x9F, 0xE0, 0xFF, 0x3F, 0xE3, 0xFE, 0x7F, 0xC7, 0xFC, - 0xFF, 0x8F, 0xF9, 0xFF, 0xBF, 0xF3, 0xEF, 0x7B, 0xE7, 0xDE, 0xF7, 0xCF, 0xBD, 0xEF, 0x9F, 0x3F, - 0x9F, 0x3E, 0x7F, 0x3E, 0x7C, 0xFE, 0x7C, 0xF8, 0xF8, 0xFF, 0xF1, 0xF1, 0xFF, 0xE3, 0xE3, 0xF0, - 0x00, 0x01, 0xE0, 0x00, 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x1E, 0x00, 0xFC, 0x0F, 0xCF, 0xE1, - 0xFC, 0xFE, 0x1F, 0xCF, 0xE1, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0xBF, 0xCF, 0x7F, 0xBC, - 0xF7, 0xFB, 0xCF, 0x7F, 0xBC, 0xF3, 0xF3, 0xCF, 0x3F, 0x3F, 0xF1, 0xF3, 0xFF, 0x1E, 0x3F, 0x00, - 0x01, 0xE0, 0x00, 0x1E, 0x00, 0x03, 0xE0, 0x00, 0x3C, 0x77, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x07, 0x1C, 0x01, 0xFF, 0x00, 0x3F, 0x80, 0x07, 0xC0, 0x00, 0x00, - 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, - 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, - 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x1C, 0x70, 0xFF, 0x07, 0xF8, - 0x0F, 0x80, 0x00, 0x07, 0xE0, 0xFF, 0xC7, 0xFF, 0x7C, 0xFB, 0xC3, 0xC0, 0x3E, 0x3F, 0xF7, 0xFF, - 0xBF, 0xBF, 0xE1, 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, 0xCF, 0xFE, 0x1C, 0x00, 0x07, 0x3C, 0x03, 0xEF, - 0x00, 0xFB, 0xC0, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, - 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, - 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, - 0xE0, 0x1F, 0x1C, 0xF1, 0xF7, 0x8F, 0xBC, 0x39, 0xC0, 0x00, 0x07, 0xE0, 0xFF, 0xC7, 0xFF, 0x7C, - 0xFB, 0xC3, 0xC0, 0x3E, 0x3F, 0xF7, 0xFF, 0xBF, 0xBF, 0xE1, 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, 0xCF, - 0xFE, 0x1C, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x7F, 0xFF, 0xC0, 0x7F, 0xFF, 0xE0, 0x3F, 0xFF, 0xF0, - 0x3E, 0xF0, 0x00, 0x1F, 0x78, 0x00, 0x1F, 0x3C, 0x00, 0x0F, 0x9E, 0x00, 0x07, 0x8F, 0xFF, 0x07, - 0xC7, 0xFF, 0x83, 0xC3, 0xFF, 0xC3, 0xE1, 0xFF, 0xE1, 0xFF, 0xF0, 0x01, 0xFF, 0xF8, 0x00, 0xFF, - 0xFC, 0x00, 0x78, 0x1F, 0xFE, 0x7C, 0x0F, 0xFF, 0xBE, 0x07, 0xFF, 0xFE, 0x03, 0xFF, 0xE0, 0x0F, - 0xC7, 0xC0, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0x9F, 0x3F, 0xBE, 0x78, 0x78, 0x7C, 0x01, 0xE0, 0xF1, - 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFD, 0xE1, 0xE0, 0x0F, 0x8F, 0xC7, 0xDF, 0xFF, 0xFE, - 0x7F, 0xFF, 0xF8, 0xFF, 0x3F, 0xC0, 0xE0, 0x38, 0x00, 0x38, 0xE1, 0xFF, 0x07, 0xF0, 0x1F, 0x00, - 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7F, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, - 0x1C, 0x70, 0x7F, 0xC0, 0xFE, 0x01, 0xF0, 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, - 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, - 0xFF, 0x00, 0xE0, 0x03, 0xC0, 0x0F, 0xFC, 0x0F, 0xFF, 0x0F, 0xFF, 0xC7, 0xC3, 0xF7, 0xC0, 0xFB, - 0xE0, 0x3C, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xC0, - 0x7B, 0xE0, 0x7C, 0xFC, 0xFE, 0x3F, 0xFE, 0x1F, 0xFE, 0x03, 0xFC, 0x00, 0x78, 0x00, 0x0F, 0xC0, - 0xFF, 0xC7, 0xFF, 0x9F, 0x3E, 0x78, 0x7C, 0x00, 0xF7, 0xFF, 0xDF, 0xFF, 0x7F, 0xFD, 0xE0, 0xF7, - 0xC7, 0xCF, 0xFE, 0x3F, 0xF0, 0x7F, 0x80, 0x30, 0x00, 0x0E, 0x70, 0x0F, 0xBC, 0x07, 0x9E, 0x01, - 0xCE, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x7F, 0xE0, 0x7F, 0xF8, 0x7F, 0xFE, 0x3E, 0x1F, 0xBE, 0x07, - 0xDF, 0x01, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x07, 0xFE, - 0x03, 0xDF, 0x03, 0xE7, 0xE7, 0xF1, 0xFF, 0xF0, 0xFF, 0xF0, 0x1F, 0xE0, 0x03, 0xC0, 0x00, 0x1C, - 0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0x1C, 0x00, 0x00, 0x3F, 0x03, 0xFF, 0x1F, 0xFE, 0x7C, 0xF9, 0xE1, - 0xF0, 0x03, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x83, 0xDF, 0x1F, 0x3F, 0xF8, 0xFF, 0xC1, 0xFE, - 0x00, 0xC0, 0x00, 0x73, 0xC0, 0x00, 0x3E, 0xF0, 0x00, 0x0F, 0xBC, 0x00, 0x01, 0xCE, 0x00, 0x00, - 0x00, 0x00, 0x1E, 0x07, 0x81, 0xE7, 0xC1, 0xE0, 0xFD, 0xF8, 0x78, 0x3E, 0x3E, 0x1E, 0x1F, 0x07, - 0xC7, 0x87, 0xC1, 0xF1, 0xE3, 0xE0, 0x3E, 0x78, 0xF8, 0x07, 0x9E, 0x7C, 0x01, 0xFF, 0xFE, 0x00, - 0x3F, 0xFF, 0x80, 0x1F, 0xFF, 0xE0, 0x07, 0xFF, 0xFC, 0x03, 0xE7, 0x8F, 0x01, 0xF1, 0xE3, 0xE0, - 0x7C, 0x78, 0x7C, 0x3E, 0x1E, 0x1F, 0x0F, 0x87, 0x83, 0xE7, 0xC1, 0xE0, 0xFB, 0xF0, 0x78, 0x1F, - 0x03, 0xCE, 0x00, 0x1E, 0xF8, 0x00, 0xF7, 0xC0, 0x03, 0x1C, 0x00, 0x00, 0x00, 0x7C, 0x3C, 0x7D, - 0xF1, 0xE3, 0xEF, 0x8F, 0x3E, 0x3E, 0x7B, 0xE0, 0xFB, 0xDE, 0x03, 0xFF, 0xF0, 0x1F, 0xFF, 0x00, - 0x7F, 0xF8, 0x07, 0xFF, 0xE0, 0x7D, 0xFF, 0x07, 0xEF, 0x7C, 0x3E, 0x79, 0xF3, 0xE3, 0xCF, 0xBE, - 0x1E, 0x3E, 0x1C, 0xE0, 0xF3, 0xC3, 0xCF, 0x07, 0x38, 0x00, 0x00, 0x1E, 0x01, 0xFE, 0x1F, 0xFE, - 0x7F, 0xFB, 0xE1, 0xFF, 0x87, 0xC0, 0x1F, 0x00, 0xF8, 0x1F, 0xE0, 0x7E, 0x01, 0xFE, 0x01, 0xFC, - 0x01, 0xFF, 0x03, 0xFC, 0x0F, 0xF8, 0x7D, 0xFF, 0xF7, 0xFF, 0x87, 0xF8, 0x07, 0x80, 0x39, 0xE1, - 0xEF, 0x0F, 0x78, 0x71, 0xC0, 0x00, 0x07, 0xC0, 0xFF, 0x8F, 0xFC, 0x7D, 0xF3, 0xC7, 0x80, 0x7C, - 0x0F, 0xC0, 0x7E, 0x03, 0xF9, 0xC3, 0xCF, 0x1E, 0x7F, 0xF3, 0xFF, 0x87, 0xF0, 0x0C, 0x00, 0x7F, - 0xF9, 0xFF, 0xF7, 0xFF, 0xDF, 0xFE, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xE0, 0x3F, - 0xC0, 0xFF, 0x80, 0x1F, 0x00, 0x3F, 0xC0, 0xFF, 0x07, 0xFE, 0x3F, 0x7F, 0xF9, 0xFF, 0xE1, 0xFE, - 0x01, 0xE0, 0x7F, 0xF9, 0xFF, 0xF7, 0xFF, 0xDF, 0xFE, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, - 0x0F, 0xE0, 0x3F, 0xC0, 0xFF, 0x80, 0x1F, 0x00, 0x3F, 0xC0, 0xFF, 0x07, 0xFE, 0x3F, 0x7F, 0xF9, - 0xFF, 0xE1, 0xFE, 0x01, 0xE0, 0x07, 0xF0, 0x07, 0xF8, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x03, - 0x80, 0x7B, 0xE0, 0x7F, 0xF0, 0x3F, 0xF8, 0x3F, 0xFC, 0x3F, 0xFE, 0x1F, 0xFF, 0x1F, 0xFF, 0x8F, - 0xFF, 0xCF, 0xFF, 0xEF, 0xBF, 0xF7, 0x9F, 0xFF, 0xCF, 0xFF, 0xC7, 0xFF, 0xE3, 0xFF, 0xE1, 0xFF, - 0xE0, 0xFF, 0xF0, 0x7F, 0xF0, 0x3F, 0xF0, 0x1F, 0x1F, 0xC0, 0xFF, 0x07, 0xF8, 0x00, 0x00, 0x00, + 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, 0xF8, 0x7C, 0x3C, + 0xF8, 0x7C, 0x3C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x0F, 0x7F, 0x80, 0x0F, 0xFC, 0x00, 0xFF, 0xC0, 0x0F, + 0xFC, 0x00, 0x07, 0xC0, 0x00, 0x7C, 0x00, 0x07, 0xC0, 0x00, 0x7F, 0xF0, 0x07, 0xFF, 0xC0, 0x7F, + 0xFE, 0x07, 0xFF, 0xF0, 0x7C, 0x1F, 0x07, 0xC0, 0xF0, 0x7C, 0x0F, 0x07, 0xC1, 0xF0, 0x7C, 0x3F, + 0x07, 0xFF, 0xE0, 0x7F, 0xFC, 0x07, 0xFF, 0x80, 0x70, 0x00, 0x77, 0xC0, 0x07, 0xFE, 0x00, 0x3F, + 0xF0, 0x01, 0xFF, 0x80, 0x0F, 0xFC, 0x00, 0x7F, 0xE0, 0x03, 0xFF, 0xFC, 0x1F, 0xFF, 0xF8, 0xFF, + 0xFF, 0xE7, 0xFF, 0xFF, 0x3F, 0xF0, 0x7D, 0xFF, 0x83, 0xEF, 0xFC, 0x1F, 0x7F, 0xE0, 0xFB, 0xFF, + 0x1F, 0x9F, 0xFF, 0xFC, 0xFF, 0xFF, 0xC7, 0xFF, 0xFC, 0x3E, 0x70, 0x01, 0xF0, 0x03, 0xE0, 0x07, + 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7F, 0xF0, 0xFF, 0xF9, 0xFF, 0xFB, 0xFF, 0xF7, 0xC1, + 0xFF, 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0xFC, 0x7E, 0xFF, 0xFD, 0xFF, 0xF3, 0xFF, 0xC0, 0x03, 0xE0, + 0x03, 0xFF, 0x01, 0xFF, 0xE0, 0xFF, 0xFC, 0x7E, 0x1F, 0x1F, 0x03, 0xE7, 0x80, 0x78, 0x00, 0x1F, + 0x01, 0xFF, 0xC0, 0x7F, 0xF0, 0x1F, 0xFC, 0x07, 0xFF, 0x78, 0x07, 0x9E, 0x03, 0xE7, 0xC0, 0xF8, + 0xFC, 0xFC, 0x3F, 0xFF, 0x07, 0xFF, 0x80, 0xFF, 0x80, 0x07, 0x00, 0x70, 0x07, 0xC0, 0xF8, 0x1F, + 0xF0, 0xF8, 0x7F, 0xF8, 0xF8, 0xFF, 0xFC, 0xF8, 0xFC, 0x7E, 0xF9, 0xF0, 0x3E, 0xF9, 0xF0, 0x1F, + 0xF9, 0xE0, 0x1F, 0xFF, 0xE0, 0x0F, 0xFF, 0xE0, 0x0F, 0xFF, 0xE0, 0x0F, 0xFF, 0xE0, 0x0F, 0xF9, + 0xE0, 0x1F, 0xF9, 0xF0, 0x1F, 0xF9, 0xF8, 0x3E, 0xF8, 0xFE, 0xFE, 0xF8, 0x7F, 0xFC, 0xF8, 0x3F, + 0xF8, 0xF8, 0x1F, 0xF0, 0x00, 0x03, 0x80, 0x01, 0xFE, 0x1F, 0xFC, 0xFF, 0xF9, 0xFF, 0xF7, 0xE1, + 0xEF, 0x83, 0xDE, 0x07, 0xBC, 0x0F, 0x7C, 0x1E, 0xFF, 0xFC, 0xFF, 0xF8, 0xFF, 0xF0, 0xFF, 0xE3, + 0xE3, 0xC7, 0x87, 0x9F, 0x0F, 0x7C, 0x1E, 0xF8, 0x3F, 0xE0, 0x78, 0x0F, 0xC1, 0xFF, 0x8F, 0xFE, + 0xF9, 0xF7, 0x87, 0x80, 0x7C, 0x7F, 0xEF, 0xFF, 0x7F, 0x7F, 0xC3, 0xFE, 0x3E, 0xFF, 0xF7, 0xFF, + 0x9F, 0xFC, 0x38, 0x00, 0x00, 0x18, 0x00, 0x78, 0x1F, 0xF0, 0x7F, 0xC3, 0xFF, 0x07, 0xC0, 0x1E, + 0x78, 0x3F, 0xF8, 0x7F, 0xF8, 0xFC, 0xF9, 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xEF, 0x07, 0x9E, 0x0F, + 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0xFF, 0x87, 0xFF, 0x3F, 0xFD, 0xFF, + 0xEF, 0x0F, 0x78, 0xFB, 0xFF, 0x9F, 0xFC, 0xFF, 0xF7, 0x87, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0x7F, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x83, 0xE0, + 0xF8, 0x3E, 0x00, 0x1F, 0xFC, 0x1F, 0xFC, 0x1F, 0xFC, 0x1F, 0xFC, 0x1E, 0x3C, 0x1E, 0x3C, 0x1E, + 0x3C, 0x1E, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, + 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0x07, 0xC0, 0x7F, 0xC3, 0xFF, 0x9F, 0xBE, 0x78, 0x3D, + 0xE0, 0xF7, 0xFF, 0xDF, 0xFF, 0x7F, 0xFD, 0xE0, 0x07, 0xC3, 0xCF, 0xFF, 0x3F, 0xF8, 0x3F, 0xC0, + 0x38, 0x00, 0xF8, 0x78, 0xFB, 0xE3, 0xC7, 0xDF, 0x1E, 0x7C, 0x7C, 0xF7, 0xC1, 0xF7, 0xBC, 0x07, + 0xFF, 0xE0, 0x3F, 0xFE, 0x00, 0xFF, 0xF0, 0x0F, 0xFF, 0xC0, 0xFB, 0xFE, 0x0F, 0xDE, 0xF8, 0x7C, + 0xF3, 0xE7, 0xC7, 0x9F, 0x7C, 0x3C, 0x7C, 0x0F, 0x81, 0xFF, 0x1F, 0xF8, 0xFB, 0xE7, 0x8F, 0x00, + 0xF8, 0x1F, 0x80, 0xFC, 0x07, 0xF3, 0x87, 0x9E, 0x3C, 0xFF, 0xE7, 0xFF, 0x0F, 0xE0, 0x18, 0x00, + 0xF0, 0x7F, 0xC3, 0xFE, 0x3F, 0xF3, 0xFF, 0x9F, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF3, + 0xFF, 0x9F, 0xF8, 0xFF, 0x87, 0xF8, 0x3C, 0x38, 0xE1, 0xFF, 0x07, 0xF0, 0x1F, 0x00, 0x00, 0x78, + 0x3F, 0xE1, 0xFF, 0x1F, 0xF9, 0xFF, 0xCF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0xF9, 0xFF, + 0xCF, 0xFC, 0x7F, 0xC3, 0xFC, 0x1E, 0xF0, 0x7B, 0xE3, 0xEF, 0x9F, 0x3E, 0xFC, 0xFB, 0xE3, 0xFF, + 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0xC3, 0xEF, 0x8F, 0xBE, 0x3E, 0x7C, 0xF8, 0xFB, 0xE3, 0xE0, 0x1F, + 0xFC, 0xFF, 0xF3, 0xFF, 0xCF, 0xFF, 0x3E, 0x7C, 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x3C, 0x7C, 0xF1, + 0xF7, 0xC7, 0xFF, 0x1F, 0xF8, 0x7F, 0xC1, 0xF0, 0xFC, 0x0F, 0xFF, 0x87, 0xFF, 0xE1, 0xFF, 0xF8, + 0x7F, 0xFF, 0x3F, 0xFF, 0xCF, 0xFF, 0xFB, 0xFF, 0xDF, 0xEF, 0xF7, 0xFB, 0xFD, 0xFE, 0xFF, 0x3F, + 0x3F, 0xCF, 0xCF, 0xF1, 0xF3, 0xFC, 0x78, 0xF0, 0xF0, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3C, 0x07, + 0xC0, 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xE7, 0xC7, 0xCF, 0x07, 0x9E, 0x0F, 0xBC, 0x1F, 0x78, 0x3E, + 0xF0, 0x79, 0xF1, 0xF1, 0xFF, 0xC3, 0xFF, 0x81, 0xFE, 0x00, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x87, 0xFC, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, + 0x87, 0xFC, 0x3C, 0xF3, 0xC3, 0xFF, 0xCF, 0xFF, 0xBF, 0xFE, 0xF8, 0x7F, 0xE1, 0xFF, 0x07, 0xFC, + 0x0F, 0xF8, 0x7F, 0xE1, 0xFF, 0xCF, 0xFF, 0xFE, 0xFF, 0xF3, 0xFF, 0x8F, 0x98, 0x3E, 0x00, 0xF8, + 0x03, 0xE0, 0x0F, 0x80, 0x00, 0x00, 0x07, 0xC0, 0x7F, 0xC3, 0xFF, 0x9F, 0xFF, 0x7C, 0x7D, 0xE0, + 0xE7, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0xF7, 0xC7, 0xCF, 0xFE, 0x3F, 0xF8, 0x3F, 0xC0, 0x38, + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, + 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0xF8, 0x3F, 0xF0, 0x79, 0xE0, 0xF3, 0xE3, 0xE7, + 0xC7, 0x87, 0x8F, 0x0F, 0xBE, 0x0F, 0x78, 0x1E, 0xF0, 0x3F, 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xFC, + 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xC0, 0x3F, 0x80, 0xFE, 0x01, 0xF8, 0x00, 0xC0, 0x00, 0x00, 0xE0, + 0x00, 0x3E, 0x00, 0x07, 0xC0, 0x00, 0xF8, 0x00, 0x1F, 0x00, 0x0F, 0xF8, 0x07, 0xFF, 0xC1, 0xFF, + 0xFC, 0x7F, 0xFF, 0xCF, 0xBE, 0x79, 0xE7, 0xCF, 0x3C, 0xF9, 0xF7, 0x9F, 0x3E, 0xF3, 0xE7, 0xDE, + 0x7C, 0xF3, 0xEF, 0xBE, 0x3F, 0xFF, 0xC3, 0xFF, 0xF0, 0x3F, 0xFC, 0x01, 0xFC, 0x00, 0x1F, 0x00, + 0x03, 0xE0, 0x00, 0x7C, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x00, 0x78, 0x7D, 0xF1, 0xE3, 0xCF, 0x8F, + 0xBC, 0x1F, 0xE0, 0x7F, 0x80, 0xFC, 0x03, 0xF0, 0x0F, 0xE0, 0x7F, 0x83, 0xFF, 0x0F, 0x3E, 0x7C, + 0x7B, 0xE1, 0xF0, 0xF0, 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, 0x3E, 0x1E, + 0x7C, 0x3C, 0xF8, 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x07, 0x80, + 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0xF0, 0xFF, 0xC7, 0xFE, 0x3F, 0xF1, 0xFF, 0x8F, 0xFC, 0x7F, 0xE3, + 0xEF, 0xFF, 0x7F, 0xF9, 0xFF, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0xF0, 0xF1, 0xFF, 0x1E, + 0x3F, 0xE3, 0xC7, 0xFC, 0x78, 0xFF, 0x8F, 0x1F, 0xF1, 0xE3, 0xFE, 0x3C, 0x7F, 0xC7, 0x8F, 0xF8, + 0xF1, 0xFF, 0x1E, 0x3F, 0xE3, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xF0, 0xF1, + 0xE7, 0xC7, 0x8F, 0x3E, 0x3C, 0x79, 0xF1, 0xE3, 0xCF, 0x8F, 0x1E, 0x7C, 0x78, 0xF3, 0xE3, 0xC7, + 0x9F, 0x1E, 0x3C, 0xF8, 0xF1, 0xE7, 0xC7, 0x8F, 0x3E, 0x3C, 0x79, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFC, 0x00, 0x01, 0xE0, 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x03, 0xC0, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x1F, 0xF0, 0x1F, 0xFC, 0x1F, 0xFE, 0x1F, 0xFF, 0x1F, 0x0F, + 0x1F, 0x0F, 0x1F, 0x1F, 0x1F, 0xFF, 0x1F, 0xFE, 0x1F, 0xFC, 0xF0, 0x07, 0xFE, 0x01, 0xFF, 0x80, + 0x7F, 0xE0, 0x1F, 0xFF, 0x07, 0xFF, 0xF9, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xF8, 0xFF, 0xFE, 0x1F, + 0xFF, 0x8F, 0xFF, 0xFF, 0xDF, 0xFF, 0xF7, 0xFF, 0xF9, 0xF0, 0xF0, 0x07, 0xC0, 0x3E, 0x01, 0xF0, + 0x0F, 0xF0, 0x7F, 0xF3, 0xFF, 0xDF, 0xFF, 0xF8, 0xFF, 0xC3, 0xFE, 0x3F, 0xFF, 0xEF, 0xFF, 0x7F, + 0xF0, 0x0F, 0xC0, 0xFF, 0xC3, 0xFF, 0x1F, 0xFE, 0x78, 0x7C, 0xC1, 0xF0, 0x7F, 0xC1, 0xFF, 0x03, + 0xFD, 0xE1, 0xF7, 0x87, 0x9F, 0xFE, 0x3F, 0xF0, 0x7F, 0x80, 0x70, 0x00, 0xF0, 0x3F, 0x0F, 0x0F, + 0xF8, 0xF1, 0xFF, 0xCF, 0x1F, 0xFE, 0xF3, 0xE1, 0xEF, 0xFE, 0x1F, 0xFF, 0xC1, 0xFF, 0xFC, 0x1F, + 0xFF, 0xC1, 0xFF, 0x3E, 0x1F, 0xF3, 0xE3, 0xEF, 0x1F, 0xFE, 0xF0, 0xFF, 0xCF, 0x07, 0xF8, 0x00, + 0x1C, 0x00, 0x0F, 0xF9, 0xFF, 0xDF, 0xFE, 0xFF, 0xF7, 0x87, 0xBC, 0x3D, 0xFF, 0xEF, 0xFF, 0x3F, + 0xF8, 0xFF, 0xCF, 0x9E, 0x78, 0xF7, 0xC7, 0xFC, 0x3C, 0x1F, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xE0, + 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, + 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x1C, 0xF0, 0x7B, 0xC1, + 0xEF, 0x07, 0x1C, 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, + 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x1C, + 0x00, 0x78, 0x03, 0xFF, 0x07, 0xFE, 0x07, 0xFC, 0x07, 0x9F, 0x0F, 0xFF, 0x1F, 0xFF, 0x3F, 0xFE, + 0x7C, 0x7C, 0xF8, 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, 0x3E, 0x1E, 0x7C, + 0x3C, 0xF8, 0x78, 0x00, 0xF0, 0x01, 0xE0, 0x1F, 0xC0, 0x3F, 0x80, 0x7E, 0x00, 0x70, 0x07, 0x81, + 0xC0, 0xF0, 0x38, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0x83, 0xE0, 0xF8, 0x3E, 0x0F, + 0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x80, 0x07, 0xC0, 0x7F, 0xC3, 0xFF, 0x9F, 0xFF, 0x7C, 0x3D, 0xE0, + 0xE7, 0xF8, 0x1F, 0xE0, 0x7F, 0x81, 0xE0, 0xF7, 0xC7, 0xCF, 0xFE, 0x3F, 0xF8, 0x3F, 0xC0, 0x38, + 0x00, 0x0F, 0xC1, 0xFF, 0x9F, 0xFC, 0xF9, 0xF7, 0x87, 0x3F, 0x01, 0xFF, 0x87, 0xFE, 0x0F, 0xF8, + 0x87, 0xDE, 0x1E, 0xFF, 0xF3, 0xFF, 0x0F, 0xF0, 0x1E, 0x00, 0xF7, 0xFE, 0xE0, 0x7B, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x73, 0xFE, 0xF7, 0xBD, 0xC6, 0x00, 0x07, 0x81, 0xF0, 0x7C, + 0x1F, 0x07, 0xC1, 0xF0, 0x7C, 0x1F, 0x07, 0xC1, 0xF0, 0x7C, 0x1F, 0x07, 0xC1, 0xF0, 0x3C, 0x7C, + 0xF8, 0xE0, 0x07, 0x8F, 0x9F, 0x3E, 0x7C, 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x3E, 0x7C, 0xF9, 0xF3, + 0xDF, 0xBF, 0x7C, 0x00, 0x1F, 0xFC, 0x00, 0x3F, 0xF8, 0x00, 0x7F, 0xF0, 0x00, 0xFF, 0xE0, 0x01, + 0xE3, 0xFC, 0x03, 0xC7, 0xFF, 0x07, 0x8F, 0xFF, 0x1F, 0x1F, 0xFE, 0x3E, 0x3C, 0x3E, 0x7C, 0x78, + 0x7C, 0xF0, 0xF0, 0xFF, 0xE1, 0xFF, 0xEF, 0x83, 0xFF, 0xDF, 0x07, 0xFE, 0x00, 0xF0, 0x78, 0x07, + 0xC3, 0xC0, 0x3E, 0x1E, 0x01, 0xF0, 0xF0, 0x0F, 0x87, 0x80, 0x7F, 0xFF, 0xE3, 0xFF, 0xFF, 0xDF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xC3, 0xFE, 0x1E, 0x1F, 0xF0, 0xFF, 0xFF, 0x87, 0xFF, 0x7C, + 0x3F, 0xF0, 0x1C, 0x00, 0xFF, 0x83, 0xFF, 0x07, 0xFE, 0x03, 0xC0, 0x07, 0x9F, 0x0F, 0xFF, 0x1F, + 0xFF, 0x3F, 0xFE, 0x7C, 0x7C, 0xF8, 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, + 0x3E, 0x1E, 0x7C, 0x3C, 0xF8, 0x78, 0x03, 0xC0, 0x0E, 0x00, 0x78, 0x01, 0xC0, 0x00, 0x03, 0xC1, + 0xEF, 0x8F, 0xBE, 0x7C, 0xFB, 0xF3, 0xEF, 0x8F, 0xFC, 0x3F, 0xE0, 0xFF, 0x83, 0xFF, 0x0F, 0xBE, + 0x3E, 0xF8, 0xF9, 0xF3, 0xE3, 0xEF, 0x8F, 0x80, 0x1E, 0x00, 0xF0, 0x03, 0xC0, 0x0E, 0x00, 0x00, 0x78, 0x3F, 0xE1, 0xFF, 0x1F, 0xF9, 0xFF, 0xCF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0xF9, - 0xFF, 0xCF, 0xFC, 0x7F, 0xC3, 0xFC, 0x1E, 0x0E, 0x78, 0x0F, 0xBC, 0x07, 0xDE, 0x01, 0xCE, 0x00, - 0x00, 0x03, 0x80, 0x7B, 0xE0, 0x7F, 0xF0, 0x3F, 0xF8, 0x3F, 0xFC, 0x3F, 0xFE, 0x1F, 0xFF, 0x1F, - 0xFF, 0x8F, 0xFF, 0xCF, 0xFF, 0xEF, 0xBF, 0xF7, 0x9F, 0xFF, 0xCF, 0xFF, 0xC7, 0xFF, 0xE3, 0xFF, - 0xE1, 0xFF, 0xE0, 0xFF, 0xF0, 0x7F, 0xF0, 0x3F, 0xF0, 0x1F, 0x3C, 0xE1, 0xEF, 0x8F, 0x7C, 0x31, - 0xC0, 0x00, 0x78, 0x3F, 0xE1, 0xFF, 0x1F, 0xF9, 0xFF, 0xCF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xBF, 0xF9, 0xFF, 0xCF, 0xFC, 0x7F, 0xC3, 0xFC, 0x1E, 0x0F, 0x38, 0x03, 0xDE, 0x00, 0xF7, 0x80, - 0x38, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x7F, 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, - 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, - 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, - 0xC0, 0x00, 0x1E, 0x70, 0x3D, 0xE0, 0x7B, 0xC0, 0xE3, 0x80, 0x00, 0x00, 0xF8, 0x07, 0xFC, 0x1F, - 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, 0xC1, 0xF7, 0x83, 0xEF, 0x07, 0xDE, 0x0F, 0x3E, 0x3E, - 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x03, 0xE0, 0x03, 0xFE, 0x01, 0xFF, 0xE0, 0xFF, - 0xF8, 0x7E, 0x3F, 0x3F, 0x03, 0xEF, 0x80, 0xFB, 0xE0, 0x1E, 0xFF, 0xFF, 0xBF, 0xFF, 0xEF, 0xFF, - 0xFB, 0xFF, 0xFE, 0xF8, 0x07, 0xBE, 0x03, 0xE7, 0xC1, 0xF9, 0xFD, 0xFC, 0x3F, 0xFE, 0x07, 0xFF, - 0x00, 0xFF, 0x80, 0x07, 0x00, 0x07, 0xC0, 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xE7, 0xC7, 0xCF, 0x07, - 0x9F, 0xFF, 0xBF, 0xFF, 0x79, 0xFE, 0xF0, 0x79, 0xF1, 0xF1, 0xFF, 0xC3, 0xFF, 0x81, 0xFE, 0x00, - 0xE0, 0x00, 0x0F, 0x38, 0x03, 0xDE, 0x00, 0xF7, 0x80, 0x38, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, - 0xFF, 0x80, 0x7F, 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBF, - 0xFF, 0xEF, 0xFF, 0xFB, 0xFF, 0xFE, 0xFF, 0xFF, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, - 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x1E, 0x70, 0x3D, 0xE0, 0x7B, - 0xC0, 0xE3, 0x80, 0x00, 0x00, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, - 0xFF, 0xF7, 0xFF, 0xEF, 0x3F, 0xDE, 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, - 0x00, 0x0F, 0x38, 0x03, 0xDF, 0x00, 0xF7, 0xC0, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, - 0xC0, 0x7F, 0xF8, 0x3F, 0xFF, 0x1F, 0x87, 0xC7, 0xC0, 0xF9, 0xE0, 0x1E, 0x00, 0x07, 0xC0, 0x7F, - 0xF0, 0x1F, 0xFC, 0x07, 0xFF, 0x01, 0xFF, 0xDE, 0x01, 0xE7, 0x80, 0xF9, 0xF0, 0x3E, 0x3F, 0x3F, - 0x0F, 0xFF, 0xC1, 0xFF, 0xE0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x1C, 0xE0, 0xF3, 0xC3, 0xCF, 0x07, - 0x38, 0x00, 0x00, 0x3F, 0x03, 0xFF, 0x0F, 0xFC, 0x7F, 0xF9, 0xE1, 0xF3, 0x07, 0xC1, 0xFF, 0x07, - 0xFC, 0x0F, 0xF7, 0x87, 0xDE, 0x1E, 0x7F, 0xF8, 0xFF, 0xC1, 0xFE, 0x01, 0xC0, 0x07, 0xF0, 0x03, - 0xF8, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x3B, 0xF0, 0x3E, 0xF8, 0x3E, 0x7C, 0x1F, - 0x1F, 0x0F, 0x0F, 0x8F, 0x83, 0xE7, 0xC1, 0xF7, 0xC0, 0x7B, 0xE0, 0x3F, 0xE0, 0x0F, 0xF0, 0x07, - 0xF0, 0x01, 0xF8, 0x00, 0xF8, 0x00, 0x7C, 0x01, 0xFE, 0x00, 0xFE, 0x00, 0x7E, 0x00, 0x3E, 0x00, - 0x0F, 0xE0, 0x3F, 0xC0, 0x7F, 0x80, 0x00, 0x00, 0x00, 0x1F, 0x07, 0xFE, 0x0F, 0x3C, 0x1E, 0x7C, - 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, 0xC1, 0xEF, 0x03, 0xDE, 0x07, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, - 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x78, 0x07, 0xF0, 0x1F, 0xC0, 0x3F, 0x00, 0x18, 0x00, 0x0F, - 0x38, 0x07, 0xBC, 0x03, 0xDE, 0x01, 0xC7, 0x00, 0x00, 0x03, 0xC0, 0x3B, 0xF0, 0x3E, 0xF8, 0x3E, - 0x7C, 0x1F, 0x1F, 0x0F, 0x0F, 0x8F, 0x83, 0xE7, 0xC1, 0xF7, 0xC0, 0x7B, 0xE0, 0x3F, 0xE0, 0x0F, - 0xF0, 0x07, 0xF0, 0x01, 0xF8, 0x00, 0xF8, 0x00, 0x7C, 0x01, 0xFE, 0x00, 0xFE, 0x00, 0x7E, 0x00, - 0x3E, 0x00, 0x1C, 0xF0, 0x7D, 0xE0, 0x7B, 0xC0, 0xE3, 0x80, 0x00, 0x1F, 0x07, 0xFE, 0x0F, 0x3C, - 0x1E, 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, 0xC1, 0xEF, 0x03, 0xDE, 0x07, 0xF8, 0x07, 0xF0, - 0x0F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x78, 0x07, 0xF0, 0x1F, 0xC0, 0x3F, 0x00, 0x18, - 0x00, 0x07, 0xBC, 0x03, 0xFC, 0x03, 0xDE, 0x01, 0xDE, 0x00, 0x00, 0x03, 0xC0, 0x3B, 0xF0, 0x3E, - 0xF8, 0x3E, 0x7C, 0x1F, 0x1F, 0x0F, 0x0F, 0x8F, 0x83, 0xE7, 0xC1, 0xF7, 0xC0, 0x7B, 0xE0, 0x3F, - 0xE0, 0x0F, 0xF0, 0x07, 0xF0, 0x01, 0xF8, 0x00, 0xF8, 0x00, 0x7C, 0x01, 0xFE, 0x00, 0xFE, 0x00, - 0x7E, 0x00, 0x3E, 0x00, 0x0F, 0x78, 0x1D, 0xE0, 0x7B, 0x80, 0xEF, 0x00, 0x00, 0x1F, 0x07, 0xFE, - 0x0F, 0x3C, 0x1E, 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, 0xC1, 0xEF, 0x03, 0xDE, 0x07, 0xF8, - 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x78, 0x07, 0xF0, 0x1F, 0xC0, 0x3F, - 0x00, 0x18, 0x00, 0x1E, 0x70, 0x3D, 0xF0, 0x7B, 0xE0, 0x63, 0x80, 0x00, 0x0E, 0x03, 0xFE, 0x07, - 0xFC, 0x0F, 0xF8, 0x1F, 0xF0, 0x3F, 0xE0, 0x7F, 0xC0, 0xFF, 0x81, 0xFF, 0x03, 0xDF, 0xFF, 0xBF, - 0xFF, 0x3F, 0xFE, 0x1F, 0xBC, 0x00, 0x78, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x07, 0x80, 0x0F, - 0x39, 0xE1, 0xEF, 0x0F, 0x78, 0x71, 0xC0, 0x00, 0x78, 0x7F, 0xE3, 0xFF, 0x1F, 0xF8, 0xFF, 0xC7, - 0xFE, 0x3F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x7F, - 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, - 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xFE, 0x07, 0xF0, 0x3F, 0x80, 0x7C, 0x03, - 0xE0, 0x1E, 0x00, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, - 0x3E, 0x0F, 0x83, 0xF0, 0xFC, 0x3F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x00, 0x07, 0x9C, 0x00, 0x3D, - 0xF0, 0x01, 0xEF, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x38, 0x00, 0x3B, 0xE0, 0x03, 0xFF, 0x00, - 0x1F, 0xF8, 0x00, 0xFF, 0xC0, 0x07, 0xFE, 0x00, 0x3F, 0xF0, 0x01, 0xFF, 0xFE, 0x0F, 0xFF, 0xFC, - 0x7F, 0xFF, 0xF3, 0xFF, 0xFF, 0x9F, 0xF8, 0x3E, 0xFF, 0xC1, 0xF7, 0xFE, 0x0F, 0xBF, 0xF0, 0x7D, - 0xFF, 0x8F, 0xCF, 0xFF, 0xFE, 0x7F, 0xFF, 0xE3, 0xFF, 0xFE, 0x1F, 0x0F, 0x38, 0x03, 0xDF, 0x00, - 0xF7, 0xC0, 0x1C, 0xE0, 0x00, 0x00, 0x3C, 0x01, 0xFF, 0x80, 0x7F, 0xE0, 0x1F, 0xF8, 0x07, 0xFF, - 0xC1, 0xFF, 0xFE, 0x7F, 0xFF, 0xDF, 0xFF, 0xFF, 0xFE, 0x3F, 0xFF, 0x87, 0xFF, 0xE3, 0xFF, 0xFF, - 0xF7, 0xFF, 0xFD, 0xFF, 0xFE, 0x7C, 0x1F, 0xFC, 0x7F, 0xFC, 0xFF, 0xF9, 0xFF, 0xF3, 0xE0, 0x07, - 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0xFF, 0xE1, 0xFF, 0xC3, 0xFF, 0x83, 0xFF, 0x03, 0xE0, 0x07, 0xC0, - 0x0F, 0x80, 0x1F, 0x00, 0x3F, 0x80, 0x7F, 0x00, 0xFE, 0x00, 0x3C, 0x00, 0xF8, 0x07, 0xE0, 0x0F, - 0xC0, 0x1F, 0x00, 0x38, 0x00, 0x3F, 0xF3, 0xFF, 0x3F, 0xF3, 0xFF, 0x3E, 0x07, 0xFC, 0xFF, 0xCF, - 0xFC, 0x3E, 0x03, 0xE0, 0x3E, 0x03, 0xF0, 0x3F, 0x83, 0xF8, 0x0F, 0x80, 0xF0, 0x3F, 0x07, 0xF0, - 0x7E, 0x03, 0x80, 0x78, 0x07, 0x8F, 0x81, 0xF0, 0xF8, 0x7C, 0x1F, 0x8F, 0x81, 0xF3, 0xE0, 0x1F, - 0x78, 0x03, 0xFF, 0x00, 0x3F, 0xC0, 0x03, 0xF0, 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x03, 0xFC, 0x00, - 0x7F, 0xC0, 0x1F, 0xF8, 0x07, 0xCF, 0x80, 0xF8, 0xF8, 0x3E, 0x1F, 0xEF, 0x81, 0xFF, 0xF0, 0x1F, - 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x1F, 0xC0, 0x03, 0xF8, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF8, - 0x7B, 0xC7, 0xDF, 0x3C, 0x7B, 0xE3, 0xFE, 0x0F, 0xF0, 0x3F, 0x01, 0xF8, 0x1F, 0xC0, 0xFF, 0x0F, - 0xF8, 0x7B, 0xE7, 0xCF, 0xFC, 0x3C, 0x01, 0xE0, 0x0F, 0x03, 0xF8, 0x1F, 0xC0, 0xFC, 0x03, 0x80, - 0x78, 0x07, 0x9F, 0x03, 0xE3, 0xE1, 0xF0, 0xFC, 0x7C, 0x1F, 0x3E, 0x03, 0xEF, 0x00, 0xFF, 0xC0, - 0x1F, 0xE0, 0x3F, 0xFF, 0x0F, 0xFF, 0xE3, 0xFF, 0xF8, 0x1F, 0xE0, 0x07, 0xFC, 0x03, 0xFF, 0x01, - 0xF3, 0xE0, 0x7C, 0x7C, 0x3E, 0x1F, 0x1F, 0x03, 0xEF, 0xC0, 0x7C, 0x78, 0x7D, 0xF1, 0xE3, 0xCF, - 0x8F, 0xBC, 0x1F, 0xE0, 0x7F, 0x87, 0xFF, 0x9F, 0xFE, 0x0F, 0xE0, 0x7F, 0x83, 0xFF, 0x0F, 0x3E, - 0x7C, 0x7B, 0xE1, 0xF0, 0xFC, 0x3E, 0xF8, 0x7C, 0xF8, 0xF9, 0xF1, 0xF1, 0xF3, 0xE3, 0xF7, 0xCF, - 0xEF, 0x3F, 0xFE, 0x7B, 0xF9, 0xF7, 0xE3, 0xE7, 0xC7, 0xC7, 0xCF, 0x8F, 0x9F, 0x0F, 0xBE, 0x1F, - 0x80, 0xFF, 0xE3, 0xFF, 0x8F, 0xFF, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7C, 0x01, - 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x7E, 0x1F, 0xC7, 0xF8, - 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0xC1, 0xF0, 0x7C, 0x7F, 0xFF, 0xFE, 0xFF, 0x3C, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, - 0xC0, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0xFF, 0xF3, 0xFF, 0xEF, 0xFF, 0xC0, - 0x1F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, - 0x3F, 0xE0, 0xFF, 0x83, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, - 0xFF, 0xFF, 0xFC, 0x38, 0x1C, 0x07, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x83, 0xE0, 0x78, 0x1E, 0x07, - 0x81, 0xF0, 0xFF, 0xF9, 0xFF, 0xFB, 0xFF, 0xF7, 0xC3, 0xFF, 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0xFC, - 0x1F, 0xF8, 0x3F, 0xF0, 0x7F, 0xE0, 0xFF, 0xC1, 0xFF, 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0x80, 0xF9, - 0xF9, 0xF3, 0xFB, 0xE7, 0xF7, 0xC3, 0xEF, 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3D, - 0xF0, 0x7B, 0xE0, 0xF3, 0xE7, 0xE7, 0xFF, 0x87, 0xFF, 0x07, 0xF8, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xF8, 0xFF, 0x8F, 0xFE, 0xFF, 0xE0, 0x3F, 0x01, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, - 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, - 0xFF, 0x87, 0xFF, 0x3F, 0xFC, 0x07, 0xE0, 0x0F, 0x00, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, - 0xC0, 0x3C, 0x07, 0xEF, 0xFF, 0x7F, 0xF3, 0xFE, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, - 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x1F, 0x00, 0xF0, 0x0F, 0x80, 0x7C, 0x07, 0xC0, 0x3E, 0x01, 0xE0, - 0x1F, 0x00, 0xF8, 0x0F, 0x80, 0x7C, 0x03, 0xC0, 0xFF, 0xF9, 0xFF, 0xFB, 0xFF, 0xF7, 0xC3, 0xFF, - 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3F, 0xF0, 0x7F, 0xE0, 0xFF, 0xC1, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x79, 0xF8, 0xF7, 0xF9, 0xFF, 0xF3, 0xFB, 0xF7, 0xC3, 0xEF, 0x87, - 0xDF, 0x0F, 0xBE, 0x1F, 0x78, 0x3E, 0xF0, 0x7D, 0xE0, 0xFF, 0xC1, 0xFF, 0x9F, 0xFF, 0x3F, 0xFC, - 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x7E, - 0x3F, 0x9F, 0xC1, 0xF0, 0xF8, 0x7C, 0x3E, 0x1F, 0x0F, 0x87, 0xC3, 0xE1, 0xFF, 0xFF, 0xFF, 0xFE, - 0xFF, 0xE1, 0xFF, 0xF3, 0xFF, 0xF1, 0xC7, 0xE7, 0x83, 0xCF, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, - 0x3F, 0xF0, 0x7F, 0xE0, 0xF3, 0xF7, 0xE7, 0xFF, 0x87, 0xFE, 0x03, 0xF8, 0x00, 0xF8, 0x3C, 0xF0, - 0x79, 0xF0, 0xF3, 0xE1, 0xE3, 0xC3, 0xC7, 0xC7, 0x8F, 0x8F, 0x0F, 0x1E, 0x1E, 0x3C, 0x3E, 0xF8, - 0x3D, 0xF0, 0x7F, 0xC3, 0xFF, 0x1F, 0xFE, 0x3F, 0xF0, 0x7F, 0x00, 0xFF, 0xE3, 0xFF, 0xCF, 0xFF, - 0xBC, 0x3F, 0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0xFF, 0x9F, 0xFE, 0x7D, 0xF9, 0xF0, 0x07, 0xC0, 0x1F, - 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0xFF, 0xE1, - 0xFF, 0xF3, 0xFF, 0xE7, 0xC7, 0xEF, 0x83, 0xDF, 0x07, 0x9F, 0x8F, 0xBF, 0x1F, 0x3E, 0x3E, 0x00, - 0x78, 0x00, 0xF0, 0x07, 0xEF, 0xFF, 0x9F, 0xFF, 0x3F, 0xF8, 0x00, 0xF8, 0x7F, 0xE1, 0xF7, 0x87, - 0xDF, 0x1F, 0x7C, 0x7C, 0xF1, 0xF3, 0xE7, 0xCF, 0xBE, 0x1F, 0xF8, 0x7F, 0xC1, 0xFE, 0x03, 0xE0, - 0x0F, 0x80, 0x3E, 0x00, 0x78, 0x01, 0xF0, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xF8, 0xFC, 0x3D, - 0xF0, 0xF3, 0xE3, 0xCF, 0x8F, 0x1F, 0x3C, 0x3F, 0xF0, 0xFF, 0xC1, 0xFE, 0x03, 0xE0, 0x0F, 0xC0, - 0x1F, 0x00, 0x3E, 0x7F, 0xF9, 0xFF, 0xE7, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x01, - 0xF0, 0x03, 0xC0, 0x0F, 0x80, 0x1F, 0x7C, 0x7C, 0xF8, 0xF9, 0xF1, 0xE3, 0xE7, 0xC7, 0xCF, 0x8F, - 0x9E, 0x1F, 0x7C, 0x3E, 0xF8, 0x7C, 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xE0, 0x07, 0xC0, 0x00, 0xFF, - 0xCF, 0xFE, 0xFF, 0xF0, 0x3F, 0x01, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, - 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0xF9, 0xE3, 0xFE, 0x78, 0xFF, 0x9E, 0x3F, 0xE7, 0x8F, 0xF9, - 0xE3, 0xFE, 0x78, 0xFF, 0xFE, 0x3F, 0xFF, 0x9F, 0xFF, 0xC7, 0xFE, 0x01, 0xF7, 0xC0, 0xF9, 0xFC, - 0xFE, 0x3F, 0xFF, 0x07, 0xFF, 0x80, 0xFF, 0xC0, 0xFF, 0xF3, 0xFF, 0xEF, 0xFF, 0xC7, 0x1F, 0x3C, - 0x3C, 0xF0, 0xF3, 0xC3, 0xDF, 0x0F, 0x7C, 0x3C, 0xF0, 0xF3, 0xC3, 0xCF, 0x0F, 0xFC, 0x3F, 0xF0, - 0xFF, 0xC3, 0xC0, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x3F, 0x80, 0x1F, 0xE0, 0x07, 0xF8, 0x01, 0xFF, - 0x00, 0xFF, 0xC0, 0x3E, 0xF0, 0x0F, 0x3E, 0x07, 0xCF, 0x81, 0xF1, 0xE0, 0x7C, 0x7C, 0x3F, 0xFF, - 0x0F, 0xFF, 0xC3, 0xFF, 0xF9, 0xF0, 0x3E, 0x7C, 0x07, 0xDE, 0x01, 0xFF, 0x80, 0x7C, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x78, 0x00, 0x1F, 0x00, 0x07, 0xC0, 0x00, 0xE0, 0x00, 0x0F, 0xC1, 0xFF, 0x8F, - 0xFE, 0xF9, 0xF7, 0x87, 0x80, 0x7C, 0x7F, 0xEF, 0xFF, 0x7F, 0x7F, 0xC3, 0xFE, 0x3E, 0xFF, 0xF7, - 0xFF, 0x9F, 0xFC, 0x38, 0x00, 0x00, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x03, 0xE0, 0x00, - 0x7C, 0x00, 0x0F, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, - 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, - 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, - 0x7F, 0xE0, 0x1F, 0x0F, 0x80, 0x7E, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x07, 0xE0, 0xFF, 0xC7, 0xFF, - 0x7C, 0xFB, 0xC3, 0xC0, 0x3E, 0x3F, 0xF7, 0xFF, 0xBF, 0xBF, 0xE1, 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, - 0xCF, 0xFE, 0x1C, 0x00, 0x00, 0x0F, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0x70, 0x01, 0xE0, 0x00, - 0xFC, 0x00, 0x7F, 0x80, 0x3C, 0xF0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, - 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, - 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, - 0x7F, 0xE0, 0x1F, 0x00, 0x3C, 0x01, 0xE0, 0x07, 0x00, 0x1C, 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x0F, - 0x3C, 0x00, 0x00, 0x3F, 0x03, 0xFF, 0x0F, 0xFE, 0x7C, 0xF9, 0xE1, 0xE0, 0x0F, 0x87, 0xFE, 0x7F, - 0xF9, 0xFD, 0xEF, 0x87, 0xBE, 0x3E, 0x7F, 0xF9, 0xFF, 0xE3, 0xFF, 0x83, 0x80, 0x00, 0x38, 0x00, - 0x0F, 0x00, 0x01, 0xC0, 0x00, 0x30, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3C, 0xF0, 0x00, - 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, - 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, - 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x00, 0xE0, 0x07, 0x80, - 0x1C, 0x00, 0x60, 0x78, 0x07, 0xE0, 0x7F, 0x87, 0x9E, 0x00, 0x00, 0x7E, 0x0F, 0xFC, 0x7F, 0xF7, - 0xCF, 0xBC, 0x3C, 0x03, 0xE3, 0xFF, 0x7F, 0xFB, 0xFB, 0xFE, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, - 0xFF, 0xE1, 0xC0, 0x00, 0x3C, 0x00, 0x0F, 0x00, 0x01, 0xC0, 0x00, 0x60, 0x01, 0xE0, 0x00, 0xFC, - 0x00, 0x7F, 0x80, 0x3C, 0xF0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, - 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, - 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, - 0xE0, 0x1F, 0x00, 0xF0, 0x07, 0x80, 0x1C, 0x00, 0xC0, 0x78, 0x07, 0xE0, 0x7F, 0x87, 0x9E, 0x00, - 0x00, 0x7E, 0x0F, 0xFC, 0x7F, 0xF7, 0xCF, 0xBC, 0x3C, 0x03, 0xE3, 0xFF, 0x7F, 0xFB, 0xFB, 0xFE, - 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE1, 0xC0, 0x03, 0x9C, 0x01, 0xFF, 0x00, 0x7F, 0x80, - 0x18, 0xC0, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3C, 0xF0, 0x00, 0x00, 0x00, 0x78, 0x00, + 0xFF, 0xCF, 0xFC, 0x7F, 0xC3, 0xFC, 0x1E, 0x1C, 0x70, 0x3F, 0xE0, 0x3F, 0x80, 0x3E, 0x00, 0x00, + 0x1F, 0x07, 0xFE, 0x0F, 0x3C, 0x1E, 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, 0xC1, 0xEF, 0x03, + 0xDE, 0x07, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x78, 0x07, 0xF0, + 0x1F, 0xC0, 0x3F, 0x00, 0x18, 0x00, 0xF0, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3F, + 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0xFE, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3E, 0x01, 0xF0, + 0x0F, 0x80, 0x7C, 0x00, 0x70, 0x78, 0x3F, 0xE1, 0xE0, 0xFF, 0x87, 0x83, 0xFE, 0x1E, 0x0F, 0xF8, + 0x78, 0x3F, 0xE1, 0xE0, 0xFF, 0x87, 0x83, 0xFE, 0x1E, 0x0F, 0xF8, 0x78, 0x3F, 0xE1, 0xE0, 0xFF, + 0x87, 0x83, 0xFE, 0x1E, 0x0F, 0xF8, 0x78, 0x3F, 0xE1, 0xE0, 0xFF, 0x87, 0xC7, 0xDF, 0x3F, 0x1F, + 0x7F, 0xFF, 0xF8, 0xFF, 0xFF, 0xE1, 0xFC, 0xFE, 0x01, 0xC0, 0xE0, 0xF0, 0xF1, 0xFF, 0x8F, 0x1F, + 0xF8, 0xF1, 0xFF, 0x8F, 0x1F, 0xF8, 0xF1, 0xFF, 0x8F, 0x1F, 0xF8, 0xF1, 0xFF, 0x8F, 0x1F, 0xF8, + 0xF1, 0xFF, 0x8F, 0x1F, 0xF9, 0xF9, 0xF7, 0xFF, 0xFE, 0x7F, 0xFF, 0xE3, 0xF9, 0xFC, 0x0E, 0x07, + 0x00, 0x1C, 0x00, 0x1F, 0x00, 0x3F, 0xF8, 0x1F, 0xFC, 0x0F, 0xFE, 0x03, 0xFF, 0x00, 0xF8, 0x00, + 0x7F, 0xF0, 0x3F, 0xFE, 0x1F, 0xFF, 0x8F, 0xFF, 0xE7, 0xC1, 0xF3, 0xE0, 0x79, 0xF0, 0x3C, 0xF8, + 0x3E, 0x7C, 0x3F, 0x3F, 0xFF, 0x1F, 0xFF, 0x0F, 0xFF, 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x78, 0x00, + 0xF0, 0x01, 0xE0, 0x1F, 0xFC, 0x3F, 0xF8, 0x7F, 0xF0, 0x1E, 0x00, 0x3F, 0xC0, 0x7F, 0xE0, 0xFF, + 0xE1, 0xFF, 0xE3, 0xC3, 0xC7, 0x87, 0x8F, 0x1F, 0x1F, 0xFE, 0x3F, 0xF8, 0x7F, 0xE0, 0x70, 0x07, + 0xC1, 0xF0, 0x3F, 0xE3, 0xE1, 0xFF, 0xE7, 0xC7, 0xFF, 0xEF, 0x8F, 0xC7, 0xFF, 0x3E, 0x07, 0xFE, + 0x7C, 0x07, 0xFC, 0xF0, 0x00, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0x03, 0xFF, 0xFE, 0x07, 0xFF, 0xF8, + 0x0F, 0x9E, 0x00, 0x1F, 0x3E, 0x03, 0xFE, 0x7C, 0x0F, 0xFC, 0x7F, 0x7E, 0xF8, 0x7F, 0xFD, 0xF0, + 0x7F, 0xF3, 0xE0, 0x7F, 0xC0, 0x00, 0x1C, 0x00, 0xF0, 0x3F, 0x1F, 0x1F, 0xF3, 0xE7, 0xFF, 0x7C, + 0xFF, 0xFF, 0xBE, 0x3F, 0xF7, 0x83, 0xFF, 0xFF, 0x07, 0xFF, 0xE0, 0xFF, 0xFC, 0x1F, 0x78, 0x3F, + 0xEF, 0x8F, 0xFC, 0xFF, 0xEF, 0x9F, 0xFD, 0xF0, 0xFF, 0x00, 0x07, 0x00, 0x01, 0xE0, 0x00, 0xFC, + 0x00, 0x3F, 0x80, 0x1F, 0xE0, 0x07, 0xF8, 0x01, 0xFF, 0x00, 0xFF, 0xC0, 0x3E, 0xF0, 0x0F, 0x3E, + 0x07, 0xCF, 0x81, 0xF1, 0xE0, 0x7F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFF, 0xC3, 0xDE, 0xF9, 0xF7, 0xBE, + 0x7D, 0xE7, 0xDE, 0x79, 0xFF, 0x9E, 0x7C, 0x07, 0xC0, 0x0F, 0x80, 0x3F, 0x00, 0x7F, 0x00, 0xFE, + 0x03, 0xFC, 0x07, 0xBC, 0x1F, 0x78, 0x3F, 0xF8, 0x7F, 0xF1, 0xFF, 0xE3, 0xDF, 0xE7, 0xBB, 0xDE, + 0x77, 0xC0, 0x70, 0x0F, 0x80, 0xF8, 0x1F, 0x80, 0xF8, 0x1F, 0xC0, 0xF8, 0x1F, 0xC0, 0xF8, 0x3F, + 0xE0, 0xF8, 0x3F, 0xE0, 0xF8, 0x3D, 0xE0, 0xF8, 0x7D, 0xF0, 0xF8, 0x7D, 0xF0, 0xF8, 0x78, 0xF0, + 0xF8, 0xF8, 0xF8, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFC, 0xF9, 0xF7, 0x7C, 0xF9, + 0xE7, 0x3C, 0xFB, 0xE7, 0x3E, 0xFB, 0xE7, 0x3E, 0xFB, 0xC7, 0x1E, 0xF0, 0x3E, 0x0F, 0x83, 0xE0, + 0xF8, 0x7E, 0x0F, 0x87, 0xF0, 0xF8, 0x7F, 0x0F, 0x8F, 0xF8, 0xF8, 0xF7, 0x8F, 0xFF, 0x78, 0xFF, + 0xFF, 0xCF, 0xFF, 0xFC, 0xFB, 0xFF, 0xCF, 0xBD, 0xDE, 0xFB, 0xDD, 0xEF, 0xFD, 0xDF, 0x3F, 0xFF, + 0xC3, 0xFF, 0xFC, 0x3F, 0xFF, 0xC1, 0xFF, 0xF8, 0x0F, 0x0F, 0x00, 0xF0, 0xF0, 0x07, 0x9E, 0x00, + 0x7F, 0xE0, 0x03, 0xFC, 0x01, 0xFF, 0xF8, 0x3F, 0xFF, 0xC7, 0xFF, 0xFE, 0x7C, 0xF3, 0xEF, 0x8F, + 0x1F, 0xF8, 0xF1, 0xFF, 0x8F, 0x1F, 0xF8, 0xF1, 0xFF, 0x8F, 0x1F, 0xF8, 0xF1, 0xF0, 0x7F, 0xFE, + 0x1F, 0xFF, 0x0F, 0xFF, 0x83, 0xC7, 0x81, 0xF7, 0x80, 0x7B, 0xC0, 0x3F, 0xE0, 0x7F, 0xFC, 0x7F, + 0xFF, 0x7F, 0xFF, 0xBC, 0xF3, 0xFE, 0x79, 0xFF, 0x3C, 0xFF, 0x9E, 0x7C, 0x70, 0x7F, 0xFF, 0x9F, + 0x0F, 0xFF, 0xFB, 0xE0, 0xFF, 0xFE, 0x7C, 0x1F, 0xFF, 0x8F, 0x81, 0xE0, 0xF1, 0xF0, 0x1E, 0x3C, + 0x3E, 0x03, 0xEF, 0x87, 0xC0, 0x3D, 0xE0, 0xF8, 0x07, 0xF8, 0x1F, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, + 0xFF, 0x7F, 0xFF, 0xFF, 0xEF, 0x8F, 0x9F, 0x3F, 0xF1, 0xE3, 0xE3, 0xFE, 0x3C, 0x7C, 0x7F, 0xC7, + 0x8F, 0x8F, 0xF8, 0xF1, 0xF1, 0xFF, 0x1E, 0x3E, 0x3F, 0xE3, 0xC7, 0xC7, 0x80, 0xF0, 0xFF, 0xFC, + 0xF8, 0xFF, 0xFC, 0xF8, 0x7F, 0xF8, 0xF8, 0x3C, 0xF8, 0xF8, 0x3E, 0xF0, 0xF8, 0x1F, 0xE0, 0xFF, + 0xFF, 0xF0, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFE, 0xF9, 0xE7, 0x9E, 0xF9, 0xE7, + 0x9E, 0xF9, 0xE7, 0x9E, 0xF9, 0xE7, 0x9E, 0x38, 0x73, 0xFF, 0x1F, 0xC0, 0x78, 0x03, 0x03, 0xC0, + 0x7F, 0xC7, 0xFE, 0x7F, 0xE0, 0x1F, 0x01, 0xF0, 0x1F, 0x01, 0xE1, 0xFE, 0x1F, 0xC1, 0xFE, 0x1F, + 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x01, 0xF3, 0xFF, 0x7F, 0xEF, 0xFC, 0xFE, 0x0F, 0x00, 0xF8, 0x07, + 0xC0, 0x78, 0x01, 0x80, 0x3D, 0xE1, 0xFE, 0x1F, 0xC0, 0xF8, 0x00, 0x07, 0xF8, 0x7F, 0xE7, 0xFE, + 0x3F, 0xF0, 0x0F, 0x01, 0xF1, 0xFE, 0x1F, 0xE1, 0xFF, 0x00, 0xF0, 0x0F, 0x1F, 0xF3, 0xFF, 0x7F, + 0xEF, 0xF0, 0xF8, 0x07, 0xE0, 0x7E, 0x03, 0xE0, 0x00, 0x00, 0x70, 0xF0, 0xEF, 0x8F, 0x0F, 0xF8, + 0xF0, 0xFF, 0x8F, 0x0F, 0xF8, 0xF0, 0xFF, 0x8F, 0x0F, 0xF8, 0xF0, 0xFF, 0x8F, 0x0F, 0xF8, 0xF1, + 0xF7, 0x8F, 0x1E, 0x7C, 0xF3, 0xE3, 0xFF, 0xFE, 0x1F, 0xFF, 0xC0, 0xFF, 0xF0, 0x03, 0xFC, 0x00, + 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x01, 0xE0, 0x00, 0x3E, 0x00, 0x07, + 0xC0, 0x78, 0xF9, 0xEF, 0x1F, 0x3F, 0xE3, 0xE7, 0xFC, 0x7C, 0xFF, 0x8F, 0x9F, 0xF1, 0xF3, 0xFE, + 0x3E, 0x7F, 0xC7, 0xCF, 0xFC, 0xF9, 0xEF, 0x9F, 0x3C, 0xFB, 0xEF, 0x9F, 0xFF, 0xF1, 0xFF, 0xFC, + 0x1F, 0xFF, 0x00, 0x7F, 0x00, 0x07, 0xC0, 0x00, 0xF8, 0x00, 0x1F, 0x00, 0x03, 0xE0, 0x00, 0x00, + 0x00, 0x03, 0xE0, 0x03, 0xFE, 0x01, 0xFF, 0xE0, 0xFF, 0xF8, 0x7E, 0x3F, 0x3F, 0x03, 0xEF, 0x80, + 0xFB, 0xE0, 0x1E, 0xFF, 0xFF, 0xBF, 0xFF, 0xEF, 0xFF, 0xFB, 0xFF, 0xFE, 0xF8, 0x07, 0xBE, 0x03, + 0xE7, 0xC1, 0xF9, 0xFD, 0xFC, 0x3F, 0xFE, 0x07, 0xFF, 0x00, 0xFF, 0x80, 0x07, 0x00, 0x07, 0xC0, + 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xE7, 0xC7, 0xCF, 0x07, 0x9F, 0xFF, 0xBF, 0xFF, 0x79, 0xFE, 0xF0, + 0x79, 0xF1, 0xF1, 0xFF, 0xC3, 0xFF, 0x81, 0xFE, 0x00, 0xE0, 0x00, 0x70, 0x00, 0xDF, 0x00, 0x7D, + 0xF0, 0x1F, 0xBE, 0x03, 0xF7, 0xC0, 0xF8, 0x7C, 0x1F, 0x0F, 0x83, 0xC1, 0xF0, 0xF8, 0x1E, 0x1F, + 0x03, 0xE3, 0xC0, 0x7C, 0xF8, 0x07, 0x9F, 0x00, 0xFB, 0xC0, 0x0F, 0xF8, 0x01, 0xFF, 0x00, 0x3F, + 0xC0, 0x03, 0xF8, 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x00, 0xF8, 0x1F, 0xF0, 0x7D, 0xE1, 0xFB, 0xC3, + 0xF7, 0xC7, 0x87, 0x9F, 0x0F, 0x3C, 0x1F, 0x78, 0x1F, 0xF0, 0x3F, 0xC0, 0x7F, 0x80, 0x7E, 0x00, + 0xFC, 0x00, 0xF8, 0x00, 0x1E, 0xF0, 0x01, 0xEE, 0x00, 0x1D, 0xE0, 0x03, 0xDC, 0x00, 0x00, 0x00, + 0xE0, 0x01, 0xBE, 0x00, 0xFB, 0xE0, 0x3F, 0x7C, 0x07, 0xEF, 0x81, 0xF0, 0xF8, 0x3E, 0x1F, 0x07, + 0x83, 0xE1, 0xF0, 0x3C, 0x3E, 0x07, 0xC7, 0x80, 0xF9, 0xF0, 0x0F, 0x3E, 0x01, 0xF7, 0x80, 0x1F, + 0xF0, 0x03, 0xFE, 0x00, 0x7F, 0x80, 0x07, 0xF0, 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x7B, 0xC0, 0x7F, + 0x80, 0xF7, 0x80, 0xFF, 0x00, 0x00, 0x1F, 0x03, 0xFE, 0x0F, 0xBC, 0x3F, 0x78, 0x7E, 0xF8, 0xF0, + 0xF3, 0xE1, 0xE7, 0x83, 0xEF, 0x03, 0xFE, 0x07, 0xF8, 0x0F, 0xF0, 0x0F, 0xC0, 0x1F, 0x80, 0x1F, + 0x00, 0x03, 0xE0, 0x00, 0x00, 0x07, 0xFC, 0x00, 0x00, 0x07, 0xFF, 0x80, 0x00, 0x07, 0xFF, 0xC0, + 0x00, 0x07, 0xE3, 0xF0, 0x00, 0x07, 0xE0, 0x7D, 0xF0, 0x7F, 0xE0, 0x3E, 0xF8, 0x3F, 0xF0, 0x0F, + 0x3C, 0x1E, 0xF0, 0x07, 0x9F, 0x1F, 0x78, 0x03, 0xC7, 0x8F, 0x3C, 0x01, 0xE3, 0xC7, 0x9E, 0x00, + 0xF1, 0xF7, 0xCF, 0x80, 0x78, 0x7B, 0xC7, 0xC0, 0x7C, 0x3D, 0xE1, 0xF0, 0x7E, 0x1F, 0xF0, 0xFE, + 0xFE, 0x07, 0xF0, 0x3F, 0xFE, 0x03, 0xF8, 0x0F, 0xFE, 0x00, 0xF8, 0x03, 0xFE, 0x00, 0x7C, 0x00, + 0x38, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3F, 0x00, + 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0xC1, 0xE0, 0x78, 0x7F, 0xC7, 0xC1, + 0xE3, 0xFF, 0x9F, 0x0F, 0x9F, 0xFF, 0x3C, 0x3C, 0x7C, 0x7C, 0xF8, 0xF1, 0xE0, 0xF1, 0xE7, 0xC7, + 0x83, 0xE7, 0x9E, 0x1E, 0x0F, 0x9F, 0x78, 0x78, 0x3E, 0x3F, 0xE1, 0xE0, 0xF0, 0xFF, 0x07, 0xC7, + 0xC3, 0xFC, 0x0F, 0xFE, 0x07, 0xE0, 0x3F, 0xF8, 0x1F, 0x80, 0x3F, 0xC0, 0x3E, 0x00, 0x38, 0x00, + 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x03, 0xF8, 0x00, 0x00, 0x0F, 0xC0, + 0x00, 0x00, 0x08, 0x00, 0x03, 0xE0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7F, 0xFE, 0x7E, 0x3F, 0xF8, 0x1F, + 0xF8, 0x0F, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF8, 0x0F, 0xF8, 0x1F, 0x7E, 0x3F, + 0x7F, 0xFF, 0x3F, 0xFF, 0x1F, 0xEF, 0x03, 0x8F, 0x00, 0x0F, 0x07, 0xC0, 0x7F, 0xC3, 0xFF, 0x9F, + 0xFF, 0x7C, 0x7D, 0xE0, 0xF7, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0xC0, 0x0F, 0xF0, 0x3F, + 0xC0, 0x3F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x38, 0x00, 0x00, 0x18, 0x00, 0xE0, + 0x47, 0x03, 0x9C, 0x0F, 0xE0, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x43, 0x9D, 0xDC, 0x07, 0xF0, 0x0F, + 0x80, 0x0F, 0x80, 0x7F, 0x83, 0xCC, 0x0E, 0x00, 0x70, 0x00, 0x01, 0xDF, 0xF7, 0xFD, 0xC0, 0x60, + 0x00, 0x0F, 0xCF, 0xFD, 0xF3, 0x80, 0x00, 0x0F, 0xF7, 0x30, 0x03, 0x9C, 0xE6, 0x00, 0x3F, 0x07, + 0xFE, 0x63, 0xF0, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x19, 0x80, 0x01, 0x80, 0x07, + 0x01, 0xF0, 0x07, 0xC1, 0x98, 0x03, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x03, 0x3E, 0x00, 0x03, 0xF9, 0x80, 0x03, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x7C, 0x1F, 0x80, 0x3E, 0x0C, 0xC0, 0x19, 0x00, + 0x03, 0x80, 0x00, 0x03, 0xE0, 0x00, 0x03, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x18, 0x00, 0x18, 0x38, 0x00, 0x0C, 0x30, 0x38, 0x0E, 0x00, 0xF0, 0x06, 0x00, + 0xE0, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x18, 0xF8, 0x00, 0x1F, + 0x38, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x60, 0x0F, + 0x00, 0xE0, 0x1E, 0x18, 0x70, 0x18, 0x18, 0x30, 0x00, 0x18, 0x30, 0x00, 0x38, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x01, 0xFE, 0x00, 0x3F, 0xC0, 0x01, 0xF0, 0x00, 0x00, 0x00, + 0xE0, 0x1E, 0x3E, 0x07, 0xE7, 0xC0, 0xFC, 0xF8, 0x3F, 0x9F, 0x0F, 0xF3, 0xE1, 0xFE, 0x7C, 0x7F, + 0xCF, 0x8F, 0xF9, 0xF3, 0xFF, 0x3E, 0xFB, 0xE7, 0xDE, 0x7C, 0xFF, 0xCF, 0x9F, 0xF1, 0xF3, 0xFE, + 0x3E, 0x7F, 0x87, 0xCF, 0xE0, 0xF9, 0xFC, 0x1F, 0xBF, 0x03, 0xF7, 0xC0, 0x7E, 0x00, 0x07, 0x80, + 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x38, 0xE0, 0x3F, 0xE0, 0x1F, 0xC0, 0x0F, 0x80, 0x00, + 0x00, 0xF0, 0x78, 0xF8, 0x78, 0xF8, 0xF8, 0xF9, 0xF8, 0xF9, 0xF8, 0xFB, 0xF8, 0xFF, 0xF8, 0xFF, + 0xF8, 0xFF, 0x78, 0xFE, 0x78, 0xFE, 0x78, 0xFC, 0x7E, 0xF8, 0x7E, 0xF0, 0x7E, 0x00, 0x3E, 0x00, + 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x1C, 0x00, 0x1F, 0x00, 0x3F, 0xF8, 0x1F, 0xFC, 0x0F, 0xFE, 0x03, + 0xFF, 0x00, 0xF8, 0x00, 0x7F, 0xF0, 0x3F, 0xFE, 0x1F, 0xFF, 0x8F, 0xFF, 0xE7, 0xC1, 0xF3, 0xE0, + 0x79, 0xF0, 0x3C, 0xF8, 0x3E, 0x7C, 0x3F, 0x3F, 0xFF, 0x1F, 0xFF, 0x0F, 0xFF, 0x00, 0x1E, 0x00, + 0x3C, 0x00, 0x78, 0x07, 0xFF, 0x0F, 0xFE, 0x1F, 0xFC, 0x07, 0x80, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, + 0xF8, 0x7F, 0xF8, 0xF0, 0xF1, 0xE1, 0xE3, 0xC7, 0xC7, 0xFF, 0x8F, 0xFE, 0x1F, 0xF8, 0x7F, 0x81, + 0xFF, 0xE3, 0xFF, 0xE7, 0xFF, 0xEF, 0x87, 0xDF, 0x07, 0xFE, 0x0F, 0xFC, 0xDF, 0xF9, 0xFF, 0xF3, + 0xFB, 0xFF, 0xF7, 0xFF, 0xCF, 0xFF, 0x1F, 0x07, 0x3E, 0x0C, 0x7C, 0x00, 0xF8, 0x01, 0xF0, 0x03, + 0xE0, 0x00, 0xF3, 0xE3, 0xFF, 0xCF, 0xFF, 0xBF, 0xFE, 0xF8, 0x7F, 0xE1, 0xFF, 0x83, 0xFE, 0x0F, + 0xF8, 0xBF, 0xE7, 0xFF, 0xDF, 0xFF, 0xFE, 0xFF, 0xFB, 0xFF, 0xEF, 0x9B, 0xBE, 0x00, 0xF8, 0x03, + 0xE0, 0x0F, 0x80, 0x00, 0x00, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, + 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x00, 0x03, 0xC0, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xE0, 0xF8, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x00, 0x1F, + 0xFC, 0x7F, 0xFC, 0xFF, 0xF9, 0xFF, 0xF3, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0xFF, 0xE1, + 0xFF, 0xC3, 0xFF, 0x83, 0xFF, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, + 0x00, 0xF8, 0x00, 0x3F, 0xF3, 0xFF, 0x3F, 0xF3, 0xFF, 0x3E, 0x07, 0xFC, 0xFF, 0xCF, 0xFC, 0x3E, + 0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x7F, 0xF1, 0xFF, 0xF3, 0xFF, 0xE7, 0xFF, 0xCF, + 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0xC0, 0xFF, 0xF1, 0xFF, 0xF3, 0xFF, 0xF7, 0xFF, 0xEF, 0x83, + 0xFF, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x7D, 0xF7, 0xFB, 0xEF, 0xE0, 0x1F, 0x80, 0x3C, 0x00, + 0xFF, 0xC7, 0xFE, 0x3F, 0xF1, 0xFF, 0x8F, 0x80, 0x7C, 0x03, 0xFE, 0x1F, 0xFC, 0xFF, 0xF7, 0xFF, + 0xBE, 0x3D, 0xF0, 0xFF, 0x87, 0xFC, 0x78, 0x03, 0xC0, 0xFE, 0x07, 0xE0, 0x3E, 0x01, 0x80, 0x78, + 0x1E, 0x07, 0x87, 0xC1, 0xE0, 0xFC, 0x7E, 0x1E, 0x0F, 0x83, 0xE1, 0xE1, 0xF0, 0x1F, 0x1E, 0x1F, + 0x01, 0xF1, 0xE3, 0xE0, 0x0F, 0x9E, 0x3E, 0x00, 0x79, 0xE7, 0xC0, 0x07, 0xFF, 0xF8, 0x00, 0x3F, + 0xFF, 0x80, 0x07, 0xFF, 0xF8, 0x00, 0x7F, 0xFF, 0xC0, 0x0F, 0x9E, 0x3C, 0x01, 0xF1, 0xE3, 0xE0, + 0x1F, 0x1E, 0x1F, 0x03, 0xE1, 0xE1, 0xF0, 0x3E, 0x1E, 0x0F, 0xE7, 0xC1, 0xE0, 0xFE, 0xFC, 0x1E, + 0x07, 0xE0, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x01, 0xE0, + 0xF8, 0x78, 0xF8, 0xF8, 0xF1, 0xF1, 0xF1, 0xE7, 0xC1, 0xF3, 0xDF, 0x01, 0xF7, 0xBC, 0x01, 0xFF, + 0xF8, 0x03, 0xFF, 0xE0, 0x03, 0xFF, 0xC0, 0x0F, 0xFF, 0xC0, 0x3E, 0xFF, 0x80, 0xFD, 0xEF, 0x81, + 0xF3, 0xCF, 0xF7, 0xC7, 0x9F, 0xDF, 0x0F, 0x1F, 0x80, 0x00, 0x0F, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0x78, 0x07, 0x80, 0x7F, 0x87, 0xFF, 0x9F, 0xFE, 0xF8, 0x7F, 0xE1, 0xF0, 0x07, + 0xC0, 0x3E, 0x07, 0xF8, 0x1F, 0x80, 0x7F, 0x80, 0x7F, 0x00, 0x7F, 0xC0, 0xFF, 0x03, 0xFE, 0x1F, + 0x7F, 0xFD, 0xFF, 0xE1, 0xFE, 0x01, 0xE0, 0x03, 0xC0, 0x0F, 0x80, 0x3E, 0x01, 0xF0, 0x00, 0x00, + 0x0F, 0x81, 0xFF, 0x1F, 0xF8, 0xFB, 0xE7, 0x8F, 0x00, 0xF8, 0x1F, 0x80, 0xFC, 0x07, 0xF3, 0x87, + 0x9E, 0x3C, 0xFF, 0xE7, 0xFF, 0x0F, 0xE0, 0x1C, 0x00, 0xF0, 0x03, 0xC0, 0x3E, 0x01, 0xE0, 0x00, + 0x00, 0x70, 0x0F, 0x3E, 0x07, 0xCF, 0x83, 0xE3, 0xE1, 0xF0, 0xF8, 0x7C, 0x3E, 0x3E, 0x0F, 0x9F, + 0x03, 0xEF, 0x80, 0xFF, 0xE0, 0x3F, 0xF0, 0x0F, 0xFC, 0x03, 0xFF, 0x80, 0xF9, 0xF0, 0x3E, 0x3E, + 0x0F, 0x8F, 0x83, 0xE1, 0xF0, 0xF8, 0x3F, 0xFE, 0x0F, 0xFF, 0x81, 0xFC, 0x00, 0x1F, 0x00, 0x07, + 0xC0, 0x01, 0xF0, 0x00, 0x7C, 0xF0, 0x79, 0xF1, 0xF3, 0xE7, 0xC7, 0xDF, 0x8F, 0xBE, 0x1F, 0xF8, + 0x3F, 0xE0, 0x7F, 0xC0, 0xFF, 0xC1, 0xF7, 0xC3, 0xEF, 0x87, 0xCF, 0xFF, 0x8F, 0xFF, 0x1F, 0xC0, + 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x70, 0x0F, 0x7C, 0x0F, 0xBE, 0x0F, 0x9F, 0x07, 0xCF, + 0x87, 0xC7, 0xFB, 0xE3, 0xFF, 0xE1, 0xFF, 0xF0, 0xFF, 0xF0, 0x7F, 0xF8, 0x3F, 0xFC, 0x1F, 0xFF, + 0x0F, 0xFF, 0x87, 0xFB, 0xE3, 0xFD, 0xF1, 0xF0, 0x7C, 0xF8, 0x3E, 0x7C, 0x0F, 0xBE, 0x07, 0xC0, + 0xF0, 0x3F, 0xF0, 0xFB, 0xE1, 0xF7, 0xF7, 0xCF, 0xEF, 0x9F, 0xFE, 0x3F, 0xFC, 0x7F, 0xF0, 0xFF, + 0xF1, 0xFD, 0xF3, 0xFB, 0xE7, 0xF3, 0xEF, 0x87, 0xDF, 0x07, 0xC0, 0x7F, 0x80, 0x7B, 0xFF, 0x03, + 0xEF, 0xFC, 0x1F, 0x3F, 0xF0, 0xFC, 0x07, 0xC3, 0xE0, 0x1F, 0x1F, 0x00, 0x7C, 0xF8, 0x01, 0xF3, + 0xE0, 0x07, 0xFF, 0x00, 0x1F, 0xF8, 0x00, 0x7F, 0xF0, 0x01, 0xFF, 0xC0, 0x07, 0xCF, 0x80, 0x1F, + 0x1F, 0x00, 0x7C, 0x7E, 0x01, 0xF0, 0xF8, 0x07, 0xC1, 0xF0, 0x1F, 0x03, 0xE0, 0x7C, 0x0F, 0xC0, + 0xFF, 0x0F, 0xFF, 0x8F, 0xBF, 0xCF, 0xDF, 0xE7, 0xC1, 0xF7, 0xC0, 0xFF, 0xC0, 0x7F, 0xE0, 0x3F, + 0xE0, 0x1F, 0xF8, 0x0F, 0xBE, 0x07, 0xDF, 0x83, 0xE7, 0xC1, 0xF1, 0xF0, 0xF8, 0x7C, 0x70, 0x0F, + 0x3E, 0x03, 0xCF, 0x80, 0xF3, 0xE0, 0x3C, 0xF8, 0x0F, 0x3E, 0x03, 0xCF, 0x80, 0xF3, 0xE0, 0x3C, + 0xFF, 0xFF, 0x3F, 0xFF, 0xCF, 0xFF, 0xF3, 0xFF, 0xFC, 0xF8, 0x0F, 0x3E, 0x03, 0xCF, 0x80, 0xF3, + 0xE0, 0x3C, 0xF8, 0x0F, 0xFE, 0x03, 0xFF, 0x80, 0xFC, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xF0, + 0x00, 0x3C, 0xF0, 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0xFF, 0x3F, 0xFE, 0x7F, + 0xFC, 0xFF, 0xF9, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xFF, 0x87, 0xFF, 0x0F, 0xC0, 0x07, 0x80, 0x0F, + 0x00, 0x1E, 0x00, 0x3C, 0x70, 0x0F, 0xFE, 0xF8, 0x0F, 0xFF, 0xF8, 0x0F, 0xFF, 0xF8, 0x0F, 0xFE, + 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xFF, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, + 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xF0, 0x7F, 0xFC, + 0x3F, 0xFE, 0x1F, 0xFF, 0x0F, 0xEF, 0x87, 0x87, 0xFF, 0xC3, 0xFF, 0xE1, 0xFF, 0xF0, 0xFF, 0xF8, + 0x7C, 0x3C, 0x3E, 0x1E, 0x1F, 0x0F, 0x0F, 0x87, 0x87, 0xC3, 0xC0, 0x7F, 0xFF, 0x00, 0x1F, 0xFF, + 0xE0, 0x03, 0xFF, 0xFC, 0x00, 0x7F, 0xFF, 0x80, 0x0F, 0x80, 0xF0, 0x01, 0xF0, 0x1E, 0x00, 0x3E, + 0x03, 0xC0, 0x07, 0xC0, 0x79, 0xC0, 0xF8, 0x0F, 0xFE, 0x1F, 0x01, 0xFF, 0xF3, 0xE0, 0x3F, 0xFE, + 0x7C, 0x07, 0xFF, 0xEF, 0x80, 0xF8, 0x7D, 0xF0, 0x1E, 0x07, 0xBE, 0x03, 0xC0, 0xF7, 0xC0, 0x78, + 0x1E, 0xF8, 0x0F, 0x0F, 0xDF, 0x01, 0xEF, 0xF3, 0xE0, 0x3D, 0xFE, 0x00, 0x00, 0x3F, 0x80, 0x00, + 0x03, 0x80, 0xFF, 0xF8, 0x03, 0xFF, 0xE0, 0x0F, 0xFF, 0x80, 0x3F, 0xFE, 0x00, 0xF8, 0x78, 0x03, + 0xE1, 0xF8, 0x0F, 0x87, 0xFE, 0x3E, 0x1F, 0xFC, 0xF8, 0x7F, 0xFB, 0xE1, 0xE3, 0xEF, 0x87, 0x87, + 0xFE, 0x1E, 0x0F, 0xF8, 0x78, 0x3F, 0xE1, 0xE1, 0xF0, 0x00, 0x0F, 0x80, 0x00, 0x7E, 0x00, 0x01, + 0xE0, 0x00, 0x03, 0x00, 0x03, 0x80, 0x00, 0x07, 0xC0, 0x00, 0x07, 0xE1, 0xF0, 0x07, 0xF3, 0xFE, + 0x07, 0xE3, 0xFF, 0x87, 0xE1, 0xFF, 0xE3, 0xE1, 0xF9, 0xF9, 0xF0, 0xF8, 0x7C, 0xF0, 0x78, 0x1E, + 0x78, 0x3C, 0x0F, 0x3C, 0x1E, 0x0F, 0x9E, 0x0F, 0x87, 0xCF, 0x87, 0xC7, 0xC7, 0xC1, 0xF7, 0xE1, + 0xF0, 0xFF, 0xE0, 0xFF, 0x3F, 0xE4, 0x3F, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xC0, + 0x1E, 0x07, 0x80, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x0F, 0xCF, 0xC0, 0xFE, 0xFF, 0x87, 0xCF, 0xFC, + 0x3C, 0x7F, 0xF1, 0xE3, 0xCF, 0x8F, 0x1E, 0x3C, 0x78, 0xF3, 0xE3, 0xE7, 0xDE, 0x1F, 0xBF, 0xF0, + 0x7F, 0xFF, 0xE1, 0xFF, 0xFF, 0x07, 0xFF, 0xFC, 0x07, 0x87, 0x00, 0x03, 0xE0, 0x07, 0xFC, 0x07, + 0xFF, 0x87, 0xFF, 0xC7, 0xE3, 0xF7, 0xE0, 0x7B, 0xE0, 0x3F, 0xF0, 0x00, 0xF0, 0x00, 0x78, 0x00, + 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x80, 0x77, 0xC0, 0x7D, 0xF0, 0x7C, 0xFE, 0xFE, 0x3F, 0xFE, 0x0F, + 0xFE, 0x03, 0xFE, 0x00, 0x38, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x07, 0xC0, 0x03, 0xC0, 0x00, 0x00, + 0x00, 0x07, 0xC0, 0x7F, 0xC3, 0xFF, 0x9F, 0xFF, 0x7C, 0x7D, 0xE0, 0xE7, 0x80, 0x1E, 0x00, 0x78, + 0x01, 0xE0, 0xF7, 0xC7, 0xCF, 0xFE, 0x3F, 0xF8, 0x3F, 0xC0, 0x38, 0x00, 0xF0, 0x01, 0xE0, 0x0F, + 0x80, 0x3C, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0x03, 0xE0, + 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, + 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xF0, 0x03, 0xF0, 0x03, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, + 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, + 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0xC0, 0xFC, 0x07, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, + 0x78, 0x07, 0xBF, 0x03, 0xF7, 0xC0, 0xF8, 0xF8, 0x7C, 0x3E, 0x1F, 0x07, 0xCF, 0x81, 0xF3, 0xE0, + 0x3F, 0xF0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x3F, 0x00, 0x0F, 0xC0, 0x01, 0xE0, 0x00, 0x78, 0x00, + 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0xF8, 0x3F, 0xF0, 0x79, 0xE0, + 0xF3, 0xE3, 0xE7, 0xC7, 0x87, 0x8F, 0x0F, 0xBE, 0x0F, 0x78, 0x1E, 0xF0, 0x3F, 0xC0, 0x3F, 0x80, + 0x7F, 0x00, 0xFC, 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x00, + 0x00, 0x78, 0x07, 0xBF, 0x03, 0xF7, 0xC0, 0xF8, 0xF8, 0x7C, 0x3E, 0x1F, 0x07, 0xCF, 0x81, 0xF3, + 0xE0, 0x3F, 0xF0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x3F, 0x00, 0x0F, 0xC0, 0x01, 0xE0, 0x03, 0xFF, + 0x81, 0xFF, 0xE0, 0x7F, 0xF8, 0x0F, 0xFC, 0x00, 0x78, 0x00, 0x1E, 0x00, 0xF8, 0x3F, 0xF0, 0x79, + 0xE0, 0xF3, 0xE3, 0xE7, 0xC7, 0x87, 0x8F, 0x0F, 0xBE, 0x0F, 0x78, 0x1E, 0xF0, 0x3F, 0xC0, 0x3F, + 0x80, 0x7F, 0x00, 0xFC, 0x03, 0xFE, 0x07, 0xFC, 0x0F, 0xF8, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, + 0x00, 0x00, 0x78, 0x07, 0x8F, 0x81, 0xF0, 0xF8, 0x7C, 0x1F, 0x8F, 0x81, 0xF3, 0xE0, 0x1F, 0x78, + 0x03, 0xFF, 0x00, 0x3F, 0xC0, 0x03, 0xF0, 0x00, 0x7E, 0x00, 0x0F, 0xC0, 0x03, 0xFC, 0x00, 0x7F, + 0xC0, 0x1F, 0xF8, 0x07, 0xCF, 0x80, 0xF8, 0xF8, 0x3E, 0x1F, 0xEF, 0x81, 0xFF, 0xF0, 0x1F, 0x80, + 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x03, 0xC0, 0x00, 0x78, 0x78, 0x7C, 0x7C, 0x78, 0x3C, 0xF8, 0x3E, + 0xF0, 0x1F, 0xE0, 0x1F, 0xE0, 0x0F, 0xC0, 0x0F, 0xC0, 0x0F, 0xE0, 0x1F, 0xE0, 0x3F, 0xF0, 0x3C, + 0xFE, 0x7C, 0x7E, 0xF8, 0x7E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x7F, 0xFE, 0x38, + 0xFF, 0xFF, 0x7C, 0xFF, 0xFF, 0x7C, 0xFF, 0xFF, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, + 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, + 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xE0, 0x7C, 0x03, 0xFF, 0xFF, + 0x03, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x1E, 0x00, + 0x00, 0x1E, 0xFF, 0x9E, 0x3F, 0xE7, 0x8F, 0xF9, 0xE3, 0xFE, 0x78, 0x1E, 0x1E, 0x07, 0x87, 0x81, + 0xE1, 0xE0, 0x78, 0x78, 0x1E, 0x1E, 0x07, 0x87, 0x81, 0xE1, 0xE0, 0x7F, 0xFE, 0x1F, 0xFF, 0x87, + 0xFF, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x70, 0x1E, 0x7C, 0x0F, 0x3E, + 0x07, 0x9F, 0x03, 0xCF, 0x81, 0xE7, 0xC0, 0xF3, 0xE0, 0x79, 0xF0, 0x3C, 0xF8, 0x1E, 0x3F, 0xFF, + 0x1F, 0xFF, 0x87, 0xFF, 0xC0, 0xFD, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1F, 0x80, + 0x0F, 0xC0, 0x07, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0xF0, 0xF9, 0xF1, 0xF3, + 0xE3, 0xE7, 0xC7, 0xCF, 0x8F, 0x9F, 0x1F, 0x3E, 0x3E, 0x3F, 0xFC, 0x7F, 0xF8, 0x7F, 0xF0, 0x03, + 0xE0, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x70, 0x1F, + 0xF0, 0x3F, 0xE0, 0x7F, 0xC0, 0xFF, 0x81, 0xFF, 0x03, 0xFE, 0xE7, 0xFD, 0xCF, 0xFB, 0x9E, 0xFF, + 0xFD, 0xFF, 0xF9, 0xFF, 0xF0, 0xFD, 0xE0, 0x73, 0xC0, 0xE7, 0x80, 0xCF, 0x00, 0x1E, 0x00, 0x3C, + 0x00, 0x78, 0xF0, 0xFF, 0xC7, 0xFE, 0x3F, 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0x7F, + 0xF9, 0xFF, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x70, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0xF8, + 0x00, 0xF8, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0xFF, 0xF0, 0xFF, 0xFC, 0xFF, 0xFC, 0xFF, 0xFE, 0xF8, + 0x3E, 0xF8, 0x1E, 0xF8, 0x1E, 0xF8, 0x1E, 0xF8, 0x1E, 0xF8, 0x1E, 0xF8, 0x1E, 0xF8, 0x1E, 0x70, + 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x79, 0xF3, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xC7, 0xFE, + 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0xFE, 0x1E, 0x00, 0x1F, + 0x00, 0x03, 0xFF, 0x00, 0x1F, 0xFE, 0x00, 0xFF, 0xFC, 0x03, 0xF1, 0xF0, 0x1F, 0x03, 0xEF, 0x78, + 0x07, 0xBF, 0xE0, 0x1E, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0x87, 0xFF, 0xFE, 0x0F, + 0x80, 0x00, 0x1F, 0x01, 0xE0, 0x7C, 0x0F, 0x80, 0xFC, 0xFC, 0x03, 0xFF, 0xF0, 0x07, 0xFF, 0x80, + 0x07, 0xFC, 0x00, 0x03, 0x80, 0x00, 0x7C, 0x00, 0x7F, 0xC0, 0x3F, 0xFB, 0x9F, 0xBE, 0xE7, 0x83, + 0xFF, 0xE0, 0xF7, 0xFF, 0xFC, 0xFF, 0xFF, 0x0F, 0xFF, 0xC1, 0xE0, 0x00, 0x7C, 0x3C, 0x0F, 0xFF, + 0x03, 0xFF, 0x80, 0x3F, 0xC0, 0x03, 0x80, 0x00, 0x1F, 0x00, 0x03, 0xFF, 0x00, 0x1F, 0xFE, 0x00, + 0xFF, 0xFC, 0x03, 0xF1, 0xF0, 0x1F, 0x03, 0xEF, 0x78, 0x07, 0xBF, 0xE0, 0x1E, 0xFF, 0xFF, 0xFB, + 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0x87, 0xFF, 0xFE, 0x0F, 0x80, 0x00, 0x1F, 0x01, 0xE0, 0x7C, 0x0F, + 0x80, 0xFC, 0xFC, 0x03, 0xFF, 0xF0, 0x07, 0xFF, 0x80, 0x07, 0xFC, 0x00, 0x07, 0x80, 0x00, 0x1E, + 0x00, 0x00, 0x78, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x7C, 0x00, 0x7F, 0xC0, 0x3F, 0xFB, 0x9F, 0xBE, + 0xE7, 0x83, 0xFF, 0xE0, 0xF7, 0xFF, 0xFC, 0xFF, 0xFF, 0x0F, 0xFF, 0xC1, 0xE0, 0x00, 0x7C, 0x3C, + 0x0F, 0xFF, 0x03, 0xFF, 0x80, 0x3F, 0xC0, 0x03, 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x0F, 0x80, + 0x03, 0xC0, 0x00, 0x00, 0x77, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0x00, 0x71, 0xC0, 0x00, 0x1F, 0xE0, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, + 0x1E, 0x07, 0x81, 0xE7, 0xC1, 0xE0, 0xFD, 0xF8, 0x78, 0x3E, 0x3E, 0x1E, 0x1F, 0x07, 0xC7, 0x87, + 0xC1, 0xF1, 0xE3, 0xE0, 0x3E, 0x78, 0xF8, 0x07, 0x9E, 0x7C, 0x01, 0xFF, 0xFE, 0x00, 0x3F, 0xFF, + 0x80, 0x1F, 0xFF, 0xE0, 0x07, 0xFF, 0xFC, 0x03, 0xE7, 0x8F, 0x01, 0xF1, 0xE3, 0xE0, 0x7C, 0x78, + 0x7C, 0x3E, 0x1E, 0x1F, 0x0F, 0x87, 0x83, 0xE7, 0xC1, 0xE0, 0xFB, 0xF0, 0x78, 0x1F, 0x03, 0x8E, + 0x00, 0x1F, 0xF0, 0x00, 0x7F, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x00, 0x7C, 0x3C, 0x7D, 0xF1, 0xE3, + 0xEF, 0x8F, 0x3E, 0x3E, 0x7B, 0xE0, 0xFB, 0xDE, 0x03, 0xFF, 0xF0, 0x1F, 0xFF, 0x00, 0x7F, 0xF8, + 0x07, 0xFF, 0xE0, 0x7D, 0xFF, 0x07, 0xEF, 0x7C, 0x3E, 0x79, 0xF3, 0xE3, 0xCF, 0xBE, 0x1E, 0x3E, + 0x70, 0x1E, 0xF8, 0x3E, 0xF8, 0x7C, 0xF8, 0xFC, 0xF8, 0xF8, 0xF9, 0xF0, 0xFB, 0xE0, 0xFB, 0xE0, + 0xFF, 0xC0, 0xFF, 0xF0, 0xFF, 0xF8, 0xFF, 0xFC, 0xF8, 0x7E, 0xF8, 0x1E, 0xF8, 0x1E, 0xF8, 0x1F, + 0xF8, 0x1F, 0xF8, 0x1E, 0xF8, 0x1E, 0x00, 0x3E, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xF8, 0x00, 0xC0, + 0xF0, 0x7B, 0xE3, 0xEF, 0x9F, 0x3E, 0xFC, 0xFB, 0xE3, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0xFF, 0xFB, + 0xE3, 0xEF, 0x87, 0xFE, 0x0F, 0xF8, 0x3F, 0xE1, 0xF0, 0x0F, 0x80, 0x7E, 0x00, 0xF0, 0x03, 0x00, + 0x07, 0xFF, 0x80, 0xFF, 0xF8, 0x0F, 0xFF, 0x80, 0xFF, 0xF8, 0x0F, 0x07, 0x80, 0xF0, 0x78, 0x0F, + 0x07, 0x80, 0xF0, 0x78, 0x0F, 0x07, 0x80, 0xF0, 0x78, 0x0F, 0x07, 0x81, 0xF0, 0x78, 0x1F, 0x07, + 0x81, 0xF0, 0x78, 0x1E, 0x07, 0x87, 0xE0, 0x78, 0xFE, 0x07, 0xFF, 0xC0, 0x7E, 0xF8, 0x07, 0xE0, + 0x00, 0x3E, 0x00, 0x03, 0xC0, 0x00, 0x3C, 0x00, 0x03, 0xC0, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, + 0x3F, 0xFC, 0x3E, 0x7C, 0x3E, 0x7C, 0x3E, 0x7C, 0x3E, 0x7C, 0x3C, 0x7C, 0x3C, 0x7C, 0x7C, 0x7C, + 0xFC, 0x7F, 0xF8, 0x7F, 0xF0, 0x7E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x3E, 0x00, 0x3C, 0x70, 0x0F, + 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, + 0xF8, 0x0F, 0xF8, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x38, + 0xF0, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, + 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3C, 0x01, 0xE0, 0x0F, 0x03, 0xF8, 0x1F, 0xC0, 0xFC, 0x03, + 0x80, 0x70, 0x0F, 0x1F, 0x01, 0xE3, 0xE0, 0x3C, 0x7C, 0x07, 0x8F, 0x80, 0xF1, 0xF0, 0x1E, 0x3E, + 0x03, 0xC7, 0xC0, 0x78, 0xFF, 0xFF, 0x1F, 0xFF, 0xE3, 0xFF, 0xFC, 0x7F, 0xFF, 0x8F, 0x80, 0xF1, + 0xF0, 0x1E, 0x3E, 0x03, 0xC7, 0xC0, 0x78, 0xF8, 0x0F, 0xDF, 0x01, 0xFB, 0xE0, 0x3F, 0x00, 0x03, + 0xE0, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x01, 0xE0, 0xF0, 0x78, 0xF8, 0x78, 0xF8, 0x78, 0xF8, 0x78, + 0xF8, 0x78, 0xFF, 0xF8, 0xFF, 0xF8, 0xFF, 0xF8, 0xFF, 0xF8, 0xF8, 0x78, 0xF8, 0x78, 0xF8, 0x7E, + 0xF8, 0x7E, 0xF8, 0x7E, 0x00, 0x3E, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x7C, 0x70, 0x1E, 0x7C, 0x0F, + 0x3E, 0x07, 0x9F, 0x03, 0xCF, 0x81, 0xE7, 0xC0, 0xF3, 0xE0, 0x79, 0xF0, 0x3C, 0xF8, 0x1E, 0x3F, + 0xFF, 0x1F, 0xFF, 0x87, 0xFF, 0xC0, 0xFD, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1F, + 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0xF0, 0xF9, 0xF1, + 0xF3, 0xE3, 0xE7, 0xC7, 0xCF, 0x8F, 0x9F, 0x1F, 0x3E, 0x3E, 0x3F, 0xFC, 0x7F, 0xF8, 0x7F, 0xF0, + 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x7C, + 0x01, 0xF1, 0xFC, 0x07, 0xF3, 0xF8, 0x0F, 0xE7, 0xF8, 0x1F, 0xCF, 0xF0, 0x7F, 0x9F, 0xE0, 0xFF, + 0x3F, 0xE3, 0xFE, 0x7F, 0xC7, 0xFC, 0xFF, 0x8F, 0xF9, 0xFF, 0xBF, 0xF3, 0xEF, 0x7B, 0xE7, 0xDE, + 0xF7, 0xCF, 0xBD, 0xEF, 0x9F, 0x3F, 0x9F, 0x3E, 0x7F, 0x3E, 0x7C, 0xFE, 0x7C, 0xF8, 0xF8, 0xFF, + 0xF1, 0xF1, 0xFF, 0xE3, 0xE3, 0xF0, 0x00, 0x01, 0xE0, 0x00, 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, + 0x1E, 0x00, 0xFC, 0x0F, 0xCF, 0xE1, 0xFC, 0xFE, 0x1F, 0xCF, 0xE1, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, + 0xFC, 0xFF, 0xBF, 0xCF, 0x7F, 0xBC, 0xF7, 0xFB, 0xCF, 0x7F, 0xBC, 0xF3, 0xF3, 0xCF, 0x3F, 0x3F, + 0xF1, 0xF3, 0xFF, 0x1E, 0x3F, 0x00, 0x01, 0xE0, 0x00, 0x1E, 0x00, 0x03, 0xE0, 0x00, 0x3C, 0x77, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x07, 0x1C, 0x01, 0xFF, 0x00, + 0x3F, 0x80, 0x07, 0xC0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, + 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, + 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, + 0x1F, 0x1C, 0x70, 0xFF, 0x07, 0xF8, 0x0F, 0x80, 0x00, 0x07, 0xE0, 0xFF, 0xC7, 0xFF, 0x7C, 0xFB, + 0xC3, 0xC0, 0x3E, 0x3F, 0xF7, 0xFF, 0xBF, 0xBF, 0xE1, 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, 0xCF, 0xFE, + 0x1C, 0x00, 0x07, 0x3C, 0x03, 0xEF, 0x00, 0xFB, 0xC0, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, - 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x0E, 0x70, 0xFF, 0x87, 0xF8, 0x31, 0x80, 0x78, - 0x07, 0xE0, 0x7F, 0x87, 0x9E, 0x00, 0x00, 0x7E, 0x0F, 0xFC, 0x7F, 0xF7, 0xCF, 0xBC, 0x3C, 0x03, - 0xE3, 0xFF, 0x7F, 0xFB, 0xFB, 0xFE, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE1, 0xC0, 0x01, - 0xF0, 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3C, 0xF0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, - 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, - 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, - 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x07, 0xC0, 0x01, 0xF0, - 0x00, 0x38, 0x00, 0x07, 0x80, 0x7E, 0x07, 0xF8, 0x79, 0xE0, 0x00, 0x07, 0xE0, 0xFF, 0xC7, 0xFF, - 0x7C, 0xFB, 0xC3, 0xC0, 0x3E, 0x3F, 0xF7, 0xFF, 0xBF, 0xBF, 0xE1, 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, - 0xCF, 0xFE, 0x1C, 0x00, 0x00, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x00, 0x78, 0x00, 0x1C, - 0x00, 0x0E, 0x00, 0x03, 0x80, 0x07, 0x1C, 0x01, 0xCF, 0x00, 0x7F, 0x80, 0x0F, 0xC0, 0x00, 0x40, + 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x1C, 0xF1, 0xF7, 0x8F, 0xBC, 0x39, 0xC0, 0x00, + 0x07, 0xE0, 0xFF, 0xC7, 0xFF, 0x7C, 0xFB, 0xC3, 0xC0, 0x3E, 0x3F, 0xF7, 0xFF, 0xBF, 0xBF, 0xE1, + 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, 0xCF, 0xFE, 0x1C, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x7F, 0xFF, 0xC0, + 0x7F, 0xFF, 0xE0, 0x3F, 0xFF, 0xF0, 0x3E, 0xF0, 0x00, 0x1F, 0x78, 0x00, 0x1F, 0x3C, 0x00, 0x0F, + 0x9E, 0x00, 0x07, 0x8F, 0xFF, 0x07, 0xC7, 0xFF, 0x83, 0xC3, 0xFF, 0xC3, 0xE1, 0xFF, 0xE1, 0xFF, + 0xF0, 0x01, 0xFF, 0xF8, 0x00, 0xFF, 0xFC, 0x00, 0x78, 0x1F, 0xFE, 0x7C, 0x0F, 0xFF, 0xBE, 0x07, + 0xFF, 0xFE, 0x03, 0xFF, 0xE0, 0x0F, 0xC7, 0xC0, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0x9F, 0x3F, 0xBE, + 0x78, 0x78, 0x7C, 0x01, 0xE0, 0xF1, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFD, 0xE1, 0xE0, + 0x0F, 0x8F, 0xC7, 0xDF, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0xFF, 0x3F, 0xC0, 0xE0, 0x38, 0x00, 0x38, + 0xE1, 0xFF, 0x07, 0xF0, 0x1F, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, + 0x01, 0xF0, 0x0F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, + 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x1C, 0x70, 0x7F, 0xC0, 0xFE, 0x01, 0xF0, 0x00, 0x00, 0x1F, + 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, + 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x03, 0xC0, 0x0F, 0xFC, 0x0F, 0xFF, 0x0F, + 0xFF, 0xC7, 0xC3, 0xF7, 0xC0, 0xFB, 0xE0, 0x3C, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xC0, 0x7B, 0xE0, 0x7C, 0xFC, 0xFE, 0x3F, 0xFE, 0x1F, 0xFE, 0x03, + 0xFC, 0x00, 0x78, 0x00, 0x0F, 0xC0, 0xFF, 0xC7, 0xFF, 0x9F, 0x3E, 0x78, 0x7C, 0x00, 0xF7, 0xFF, + 0xDF, 0xFF, 0x7F, 0xFD, 0xE0, 0xF7, 0xC7, 0xCF, 0xFE, 0x3F, 0xF0, 0x7F, 0x80, 0x30, 0x00, 0x0E, + 0x70, 0x0F, 0xBC, 0x07, 0x9E, 0x01, 0xCE, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x7F, 0xE0, 0x7F, 0xF8, + 0x7F, 0xFE, 0x3E, 0x1F, 0xBE, 0x07, 0xDF, 0x01, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xF8, 0x07, 0xFE, 0x03, 0xDF, 0x03, 0xE7, 0xE7, 0xF1, 0xFF, 0xF0, 0xFF, 0xF0, + 0x1F, 0xE0, 0x03, 0xC0, 0x00, 0x1C, 0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0x1C, 0x00, 0x00, 0x3F, 0x03, + 0xFF, 0x1F, 0xFE, 0x7C, 0xF9, 0xE1, 0xF0, 0x03, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x83, 0xDF, + 0x1F, 0x3F, 0xF8, 0xFF, 0xC1, 0xFE, 0x00, 0xC0, 0x00, 0x73, 0xC0, 0x00, 0x3E, 0xF0, 0x00, 0x0F, + 0xBC, 0x00, 0x01, 0xCE, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x07, 0x81, 0xE7, 0xC1, 0xE0, 0xFD, 0xF8, + 0x78, 0x3E, 0x3E, 0x1E, 0x1F, 0x07, 0xC7, 0x87, 0xC1, 0xF1, 0xE3, 0xE0, 0x3E, 0x78, 0xF8, 0x07, + 0x9E, 0x7C, 0x01, 0xFF, 0xFE, 0x00, 0x3F, 0xFF, 0x80, 0x1F, 0xFF, 0xE0, 0x07, 0xFF, 0xFC, 0x03, + 0xE7, 0x8F, 0x01, 0xF1, 0xE3, 0xE0, 0x7C, 0x78, 0x7C, 0x3E, 0x1E, 0x1F, 0x0F, 0x87, 0x83, 0xE7, + 0xC1, 0xE0, 0xFB, 0xF0, 0x78, 0x1F, 0x03, 0xCE, 0x00, 0x1E, 0xF8, 0x00, 0xF7, 0xC0, 0x03, 0x1C, + 0x00, 0x00, 0x00, 0x7C, 0x3C, 0x7D, 0xF1, 0xE3, 0xEF, 0x8F, 0x3E, 0x3E, 0x7B, 0xE0, 0xFB, 0xDE, + 0x03, 0xFF, 0xF0, 0x1F, 0xFF, 0x00, 0x7F, 0xF8, 0x07, 0xFF, 0xE0, 0x7D, 0xFF, 0x07, 0xEF, 0x7C, + 0x3E, 0x79, 0xF3, 0xE3, 0xCF, 0xBE, 0x1E, 0x3E, 0x1C, 0xE0, 0xF3, 0xC3, 0xCF, 0x07, 0x38, 0x00, + 0x00, 0x1E, 0x01, 0xFE, 0x1F, 0xFE, 0x7F, 0xFB, 0xE1, 0xFF, 0x87, 0xC0, 0x1F, 0x00, 0xF8, 0x1F, + 0xE0, 0x7E, 0x01, 0xFE, 0x01, 0xFC, 0x01, 0xFF, 0x03, 0xFC, 0x0F, 0xF8, 0x7D, 0xFF, 0xF7, 0xFF, + 0x87, 0xF8, 0x07, 0x80, 0x39, 0xE1, 0xEF, 0x0F, 0x78, 0x71, 0xC0, 0x00, 0x07, 0xC0, 0xFF, 0x8F, + 0xFC, 0x7D, 0xF3, 0xC7, 0x80, 0x7C, 0x0F, 0xC0, 0x7E, 0x03, 0xF9, 0xC3, 0xCF, 0x1E, 0x7F, 0xF3, + 0xFF, 0x87, 0xF0, 0x0C, 0x00, 0x7F, 0xF9, 0xFF, 0xF7, 0xFF, 0xDF, 0xFE, 0x00, 0xF8, 0x07, 0xC0, + 0x3E, 0x01, 0xF0, 0x0F, 0xE0, 0x3F, 0xC0, 0xFF, 0x80, 0x1F, 0x00, 0x3F, 0xC0, 0xFF, 0x07, 0xFE, + 0x3F, 0x7F, 0xF9, 0xFF, 0xE1, 0xFE, 0x01, 0xE0, 0x7F, 0xF9, 0xFF, 0xF7, 0xFF, 0xDF, 0xFE, 0x00, + 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xE0, 0x3F, 0xC0, 0xFF, 0x80, 0x1F, 0x00, 0x3F, 0xC0, + 0xFF, 0x07, 0xFE, 0x3F, 0x7F, 0xF9, 0xFF, 0xE1, 0xFE, 0x01, 0xE0, 0x07, 0xF0, 0x07, 0xF8, 0x03, + 0xFC, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x7B, 0xE0, 0x7F, 0xF0, 0x3F, 0xF8, 0x3F, 0xFC, 0x3F, + 0xFE, 0x1F, 0xFF, 0x1F, 0xFF, 0x8F, 0xFF, 0xCF, 0xFF, 0xEF, 0xBF, 0xF7, 0x9F, 0xFF, 0xCF, 0xFF, + 0xC7, 0xFF, 0xE3, 0xFF, 0xE1, 0xFF, 0xE0, 0xFF, 0xF0, 0x7F, 0xF0, 0x3F, 0xF0, 0x1F, 0x1F, 0xC0, + 0xFF, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x78, 0x3F, 0xE1, 0xFF, 0x1F, 0xF9, 0xFF, 0xCF, 0xFE, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0xF9, 0xFF, 0xCF, 0xFC, 0x7F, 0xC3, 0xFC, 0x1E, 0x0E, 0x78, 0x0F, + 0xBC, 0x07, 0xDE, 0x01, 0xCE, 0x00, 0x00, 0x03, 0x80, 0x7B, 0xE0, 0x7F, 0xF0, 0x3F, 0xF8, 0x3F, + 0xFC, 0x3F, 0xFE, 0x1F, 0xFF, 0x1F, 0xFF, 0x8F, 0xFF, 0xCF, 0xFF, 0xEF, 0xBF, 0xF7, 0x9F, 0xFF, + 0xCF, 0xFF, 0xC7, 0xFF, 0xE3, 0xFF, 0xE1, 0xFF, 0xE0, 0xFF, 0xF0, 0x7F, 0xF0, 0x3F, 0xF0, 0x1F, + 0x3C, 0xE1, 0xEF, 0x8F, 0x7C, 0x31, 0xC0, 0x00, 0x78, 0x3F, 0xE1, 0xFF, 0x1F, 0xF9, 0xFF, 0xCF, + 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0xF9, 0xFF, 0xCF, 0xFC, 0x7F, 0xC3, 0xFC, 0x1E, 0x0F, + 0x38, 0x03, 0xDE, 0x00, 0xF7, 0x80, 0x38, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x7F, + 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, + 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, + 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x1E, 0x70, 0x3D, 0xE0, 0x7B, 0xC0, 0xE3, 0x80, + 0x00, 0x00, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, 0xC1, 0xF7, 0x83, + 0xEF, 0x07, 0xDE, 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x03, 0xE0, + 0x03, 0xFE, 0x01, 0xFF, 0xE0, 0xFF, 0xF8, 0x7E, 0x3F, 0x3F, 0x03, 0xEF, 0x80, 0xFB, 0xE0, 0x1E, + 0xFF, 0xFF, 0xBF, 0xFF, 0xEF, 0xFF, 0xFB, 0xFF, 0xFE, 0xF8, 0x07, 0xBE, 0x03, 0xE7, 0xC1, 0xF9, + 0xFD, 0xFC, 0x3F, 0xFE, 0x07, 0xFF, 0x00, 0xFF, 0x80, 0x07, 0x00, 0x07, 0xC0, 0x3F, 0xE0, 0xFF, + 0xE3, 0xFF, 0xE7, 0xC7, 0xCF, 0x07, 0x9F, 0xFF, 0xBF, 0xFF, 0x79, 0xFE, 0xF0, 0x79, 0xF1, 0xF1, + 0xFF, 0xC3, 0xFF, 0x81, 0xFE, 0x00, 0xE0, 0x00, 0x0F, 0x38, 0x03, 0xDE, 0x00, 0xF7, 0x80, 0x38, + 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x7F, 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, + 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBF, 0xFF, 0xEF, 0xFF, 0xFB, 0xFF, 0xFE, 0xFF, 0xFF, 0xBE, 0x01, + 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, + 0x00, 0x1E, 0x70, 0x3D, 0xE0, 0x7B, 0xC0, 0xE3, 0x80, 0x00, 0x00, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, + 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, 0xFF, 0xF7, 0xFF, 0xEF, 0x3F, 0xDE, 0x0F, 0x3E, 0x3E, 0x3F, + 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x0F, 0x38, 0x03, 0xDF, 0x00, 0xF7, 0xC0, 0x1C, 0xE0, + 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, 0xC0, 0x7F, 0xF8, 0x3F, 0xFF, 0x1F, 0x87, 0xC7, 0xC0, 0xF9, + 0xE0, 0x1E, 0x00, 0x07, 0xC0, 0x7F, 0xF0, 0x1F, 0xFC, 0x07, 0xFF, 0x01, 0xFF, 0xDE, 0x01, 0xE7, + 0x80, 0xF9, 0xF0, 0x3E, 0x3F, 0x3F, 0x0F, 0xFF, 0xC1, 0xFF, 0xE0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, + 0x1C, 0xE0, 0xF3, 0xC3, 0xCF, 0x07, 0x38, 0x00, 0x00, 0x3F, 0x03, 0xFF, 0x0F, 0xFC, 0x7F, 0xF9, + 0xE1, 0xF3, 0x07, 0xC1, 0xFF, 0x07, 0xFC, 0x0F, 0xF7, 0x87, 0xDE, 0x1E, 0x7F, 0xF8, 0xFF, 0xC1, + 0xFE, 0x01, 0xC0, 0x07, 0xF0, 0x03, 0xF8, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x3B, + 0xF0, 0x3E, 0xF8, 0x3E, 0x7C, 0x1F, 0x1F, 0x0F, 0x0F, 0x8F, 0x83, 0xE7, 0xC1, 0xF7, 0xC0, 0x7B, + 0xE0, 0x3F, 0xE0, 0x0F, 0xF0, 0x07, 0xF0, 0x01, 0xF8, 0x00, 0xF8, 0x00, 0x7C, 0x01, 0xFE, 0x00, + 0xFE, 0x00, 0x7E, 0x00, 0x3E, 0x00, 0x0F, 0xE0, 0x3F, 0xC0, 0x7F, 0x80, 0x00, 0x00, 0x00, 0x1F, + 0x07, 0xFE, 0x0F, 0x3C, 0x1E, 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, 0xC1, 0xEF, 0x03, 0xDE, + 0x07, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x78, 0x07, 0xF0, 0x1F, + 0xC0, 0x3F, 0x00, 0x18, 0x00, 0x0F, 0x38, 0x07, 0xBC, 0x03, 0xDE, 0x01, 0xC7, 0x00, 0x00, 0x03, + 0xC0, 0x3B, 0xF0, 0x3E, 0xF8, 0x3E, 0x7C, 0x1F, 0x1F, 0x0F, 0x0F, 0x8F, 0x83, 0xE7, 0xC1, 0xF7, + 0xC0, 0x7B, 0xE0, 0x3F, 0xE0, 0x0F, 0xF0, 0x07, 0xF0, 0x01, 0xF8, 0x00, 0xF8, 0x00, 0x7C, 0x01, + 0xFE, 0x00, 0xFE, 0x00, 0x7E, 0x00, 0x3E, 0x00, 0x1C, 0xF0, 0x7D, 0xE0, 0x7B, 0xC0, 0xE3, 0x80, + 0x00, 0x1F, 0x07, 0xFE, 0x0F, 0x3C, 0x1E, 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, 0xC1, 0xEF, + 0x03, 0xDE, 0x07, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x78, 0x07, + 0xF0, 0x1F, 0xC0, 0x3F, 0x00, 0x18, 0x00, 0x07, 0xBC, 0x03, 0xFC, 0x03, 0xDE, 0x01, 0xDE, 0x00, + 0x00, 0x03, 0xC0, 0x3B, 0xF0, 0x3E, 0xF8, 0x3E, 0x7C, 0x1F, 0x1F, 0x0F, 0x0F, 0x8F, 0x83, 0xE7, + 0xC1, 0xF7, 0xC0, 0x7B, 0xE0, 0x3F, 0xE0, 0x0F, 0xF0, 0x07, 0xF0, 0x01, 0xF8, 0x00, 0xF8, 0x00, + 0x7C, 0x01, 0xFE, 0x00, 0xFE, 0x00, 0x7E, 0x00, 0x3E, 0x00, 0x0F, 0x78, 0x1D, 0xE0, 0x7B, 0x80, + 0xEF, 0x00, 0x00, 0x1F, 0x07, 0xFE, 0x0F, 0x3C, 0x1E, 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, + 0xC1, 0xEF, 0x03, 0xDE, 0x07, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, + 0x78, 0x07, 0xF0, 0x1F, 0xC0, 0x3F, 0x00, 0x18, 0x00, 0x1E, 0x70, 0x3D, 0xF0, 0x7B, 0xE0, 0x63, + 0x80, 0x00, 0x0E, 0x03, 0xFE, 0x07, 0xFC, 0x0F, 0xF8, 0x1F, 0xF0, 0x3F, 0xE0, 0x7F, 0xC0, 0xFF, + 0x81, 0xFF, 0x03, 0xDF, 0xFF, 0xBF, 0xFF, 0x3F, 0xFE, 0x1F, 0xBC, 0x00, 0x78, 0x00, 0xF0, 0x01, + 0xE0, 0x03, 0xC0, 0x07, 0x80, 0x0F, 0x39, 0xE1, 0xEF, 0x0F, 0x78, 0x71, 0xC0, 0x00, 0x78, 0x7F, + 0xE3, 0xFF, 0x1F, 0xF8, 0xFF, 0xC7, 0xFE, 0x3F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE0, 0x1F, + 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x7F, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x7C, 0x03, 0xE0, + 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xFE, + 0x07, 0xF0, 0x3F, 0x80, 0x7C, 0x03, 0xE0, 0x1E, 0x00, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF8, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x83, 0xF0, 0xFC, 0x3F, 0x03, 0xC0, 0xF0, 0x3C, + 0x0F, 0x00, 0x07, 0x9C, 0x00, 0x3D, 0xF0, 0x01, 0xEF, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x38, + 0x00, 0x3B, 0xE0, 0x03, 0xFF, 0x00, 0x1F, 0xF8, 0x00, 0xFF, 0xC0, 0x07, 0xFE, 0x00, 0x3F, 0xF0, + 0x01, 0xFF, 0xFE, 0x0F, 0xFF, 0xFC, 0x7F, 0xFF, 0xF3, 0xFF, 0xFF, 0x9F, 0xF8, 0x3E, 0xFF, 0xC1, + 0xF7, 0xFE, 0x0F, 0xBF, 0xF0, 0x7D, 0xFF, 0x8F, 0xCF, 0xFF, 0xFE, 0x7F, 0xFF, 0xE3, 0xFF, 0xFE, + 0x1F, 0x0F, 0x38, 0x03, 0xDF, 0x00, 0xF7, 0xC0, 0x1C, 0xE0, 0x00, 0x00, 0x3C, 0x01, 0xFF, 0x80, + 0x7F, 0xE0, 0x1F, 0xF8, 0x07, 0xFF, 0xC1, 0xFF, 0xFE, 0x7F, 0xFF, 0xDF, 0xFF, 0xFF, 0xFE, 0x3F, + 0xFF, 0x87, 0xFF, 0xE3, 0xFF, 0xFF, 0xF7, 0xFF, 0xFD, 0xFF, 0xFE, 0x7C, 0x1F, 0xFC, 0x7F, 0xFC, + 0xFF, 0xF9, 0xFF, 0xF3, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0xFF, 0xE1, 0xFF, 0xC3, 0xFF, + 0x83, 0xFF, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x1F, 0x00, 0x3F, 0x80, 0x7F, 0x00, 0xFE, 0x00, + 0x3C, 0x00, 0xF8, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0x00, 0x38, 0x00, 0x3F, 0xF3, 0xFF, 0x3F, 0xF3, + 0xFF, 0x3E, 0x07, 0xFC, 0xFF, 0xCF, 0xFC, 0x3E, 0x03, 0xE0, 0x3E, 0x03, 0xF0, 0x3F, 0x83, 0xF8, + 0x0F, 0x80, 0xF0, 0x3F, 0x07, 0xF0, 0x7E, 0x03, 0x80, 0x78, 0x07, 0x8F, 0x81, 0xF0, 0xF8, 0x7C, + 0x1F, 0x8F, 0x81, 0xF3, 0xE0, 0x1F, 0x78, 0x03, 0xFF, 0x00, 0x3F, 0xC0, 0x03, 0xF0, 0x00, 0x7E, + 0x00, 0x0F, 0xC0, 0x03, 0xFC, 0x00, 0x7F, 0xC0, 0x1F, 0xF8, 0x07, 0xCF, 0x80, 0xF8, 0xF8, 0x3E, + 0x1F, 0xEF, 0x81, 0xFF, 0xF0, 0x1F, 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x1F, 0xC0, 0x03, 0xF8, + 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF8, 0x7B, 0xC7, 0xDF, 0x3C, 0x7B, 0xE3, 0xFE, 0x0F, 0xF0, 0x3F, + 0x01, 0xF8, 0x1F, 0xC0, 0xFF, 0x0F, 0xF8, 0x7B, 0xE7, 0xCF, 0xFC, 0x3C, 0x01, 0xE0, 0x0F, 0x03, + 0xF8, 0x1F, 0xC0, 0xFC, 0x03, 0x80, 0x78, 0x07, 0x9F, 0x03, 0xE3, 0xE1, 0xF0, 0xFC, 0x7C, 0x1F, + 0x3E, 0x03, 0xEF, 0x00, 0xFF, 0xC0, 0x1F, 0xE0, 0x3F, 0xFF, 0x0F, 0xFF, 0xE3, 0xFF, 0xF8, 0x1F, + 0xE0, 0x07, 0xFC, 0x03, 0xFF, 0x01, 0xF3, 0xE0, 0x7C, 0x7C, 0x3E, 0x1F, 0x1F, 0x03, 0xEF, 0xC0, + 0x7C, 0x78, 0x7D, 0xF1, 0xE3, 0xCF, 0x8F, 0xBC, 0x1F, 0xE0, 0x7F, 0x87, 0xFF, 0x9F, 0xFE, 0x0F, + 0xE0, 0x7F, 0x83, 0xFF, 0x0F, 0x3E, 0x7C, 0x7B, 0xE1, 0xF0, 0xFC, 0x3E, 0xF8, 0x7C, 0xF8, 0xF9, + 0xF1, 0xF1, 0xF3, 0xE3, 0xF7, 0xCF, 0xEF, 0x3F, 0xFE, 0x7B, 0xF9, 0xF7, 0xE3, 0xE7, 0xC7, 0xC7, + 0xCF, 0x8F, 0x9F, 0x0F, 0xBE, 0x1F, 0x80, 0xFF, 0xE3, 0xFF, 0x8F, 0xFF, 0x00, 0x7C, 0x01, 0xF0, + 0x07, 0xC0, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7C, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xC0, 0x7E, 0x1F, 0xC7, 0xF8, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0xC1, 0xF0, 0x7C, + 0x7F, 0xFF, 0xFE, 0xFF, 0x3C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x7C, 0x01, 0xF0, 0x07, 0xC0, + 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, + 0xFF, 0xF3, 0xFF, 0xEF, 0xFF, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xFE, 0x0F, 0xF8, 0x3F, + 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFC, 0x38, 0x1C, 0x07, 0x03, 0xC0, 0xF0, 0x3C, + 0x0F, 0x83, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xF0, 0xFF, 0xF9, 0xFF, 0xFB, 0xFF, 0xF7, 0xC3, 0xFF, + 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3F, 0xF0, 0x7F, 0xE0, 0xFF, 0xC1, 0xFF, 0x83, + 0xFF, 0x07, 0xFE, 0x0F, 0x80, 0xF9, 0xF9, 0xF3, 0xFB, 0xE7, 0xF7, 0xC3, 0xEF, 0x83, 0xFF, 0x07, + 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3D, 0xF0, 0x7B, 0xE0, 0xF3, 0xE7, 0xE7, 0xFF, 0x87, 0xFF, 0x07, + 0xF8, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0x8F, 0xFE, 0xFF, 0xE0, 0x3F, 0x01, 0xF0, + 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, + 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0xFF, 0x87, 0xFF, 0x3F, 0xFC, 0x07, 0xE0, 0x0F, 0x00, 0x7C, + 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3C, 0x07, 0xEF, 0xFF, 0x7F, 0xF3, 0xFE, 0x00, 0xF8, + 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x1F, 0x00, 0xF0, 0x0F, 0x80, + 0x7C, 0x07, 0xC0, 0x3E, 0x01, 0xE0, 0x1F, 0x00, 0xF8, 0x0F, 0x80, 0x7C, 0x03, 0xC0, 0xFF, 0xF9, + 0xFF, 0xFB, 0xFF, 0xF7, 0xC3, 0xFF, 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3F, 0xF0, + 0x7F, 0xE0, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x79, 0xF8, 0xF7, 0xF9, 0xFF, + 0xF3, 0xFB, 0xF7, 0xC3, 0xEF, 0x87, 0xDF, 0x0F, 0xBE, 0x1F, 0x78, 0x3E, 0xF0, 0x7D, 0xE0, 0xFF, + 0xC1, 0xFF, 0x9F, 0xFF, 0x3F, 0xFC, 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x7E, 0x3F, 0x9F, 0xC1, 0xF0, 0xF8, 0x7C, 0x3E, 0x1F, 0x0F, 0x87, + 0xC3, 0xE1, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xE1, 0xFF, 0xF3, 0xFF, 0xF1, 0xC7, 0xE7, 0x83, 0xCF, + 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3F, 0xF0, 0x7F, 0xE0, 0xF3, 0xF7, 0xE7, 0xFF, 0x87, 0xFE, + 0x03, 0xF8, 0x00, 0xF8, 0x3C, 0xF0, 0x79, 0xF0, 0xF3, 0xE1, 0xE3, 0xC3, 0xC7, 0xC7, 0x8F, 0x8F, + 0x0F, 0x1E, 0x1E, 0x3C, 0x3E, 0xF8, 0x3D, 0xF0, 0x7F, 0xC3, 0xFF, 0x1F, 0xFE, 0x3F, 0xF0, 0x7F, + 0x00, 0xFF, 0xE3, 0xFF, 0xCF, 0xFF, 0xBC, 0x3F, 0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0xFF, 0x9F, 0xFE, + 0x7D, 0xF9, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x7C, 0x01, + 0xF0, 0x07, 0xC0, 0x1F, 0xFF, 0xE1, 0xFF, 0xF3, 0xFF, 0xE7, 0xC7, 0xEF, 0x83, 0xDF, 0x07, 0x9F, + 0x8F, 0xBF, 0x1F, 0x3E, 0x3E, 0x00, 0x78, 0x00, 0xF0, 0x07, 0xEF, 0xFF, 0x9F, 0xFF, 0x3F, 0xF8, + 0x00, 0xF8, 0x7F, 0xE1, 0xF7, 0x87, 0xDF, 0x1F, 0x7C, 0x7C, 0xF1, 0xF3, 0xE7, 0xCF, 0xBE, 0x1F, + 0xF8, 0x7F, 0xC1, 0xFE, 0x03, 0xE0, 0x0F, 0x80, 0x3E, 0x00, 0x78, 0x01, 0xF0, 0x07, 0xC0, 0x0F, + 0x00, 0x3E, 0x00, 0xF8, 0xFC, 0x3D, 0xF0, 0xF3, 0xE3, 0xCF, 0x8F, 0x1F, 0x3C, 0x3F, 0xF0, 0xFF, + 0xC1, 0xFE, 0x03, 0xE0, 0x0F, 0xC0, 0x1F, 0x00, 0x3E, 0x7F, 0xF9, 0xFF, 0xE7, 0xFF, 0x80, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x01, 0xF0, 0x03, 0xC0, 0x0F, 0x80, 0x1F, 0x7C, 0x7C, 0xF8, 0xF9, + 0xF1, 0xE3, 0xE7, 0xC7, 0xCF, 0x8F, 0x9E, 0x1F, 0x7C, 0x3E, 0xF8, 0x7C, 0x00, 0xF8, 0x01, 0xF0, + 0x03, 0xE0, 0x07, 0xC0, 0x00, 0xFF, 0xCF, 0xFE, 0xFF, 0xF0, 0x3F, 0x01, 0xF0, 0x0F, 0x00, 0xF0, + 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0xF9, 0xE3, 0xFE, 0x78, + 0xFF, 0x9E, 0x3F, 0xE7, 0x8F, 0xF9, 0xE3, 0xFE, 0x78, 0xFF, 0xFE, 0x3F, 0xFF, 0x9F, 0xFF, 0xC7, + 0xFE, 0x01, 0xF7, 0xC0, 0xF9, 0xFC, 0xFE, 0x3F, 0xFF, 0x07, 0xFF, 0x80, 0xFF, 0xC0, 0xFF, 0xF3, + 0xFF, 0xEF, 0xFF, 0xC7, 0x1F, 0x3C, 0x3C, 0xF0, 0xF3, 0xC3, 0xDF, 0x0F, 0x7C, 0x3C, 0xF0, 0xF3, + 0xC3, 0xCF, 0x0F, 0xFC, 0x3F, 0xF0, 0xFF, 0xC3, 0xC0, 0x23, 0xDF, 0xEF, 0xFD, 0xE0, 0x00, 0x7C, + 0xF1, 0xE7, 0xCF, 0x9F, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x1F, 0x7E, 0xFC, 0xF8, 0xE0, 0x00, + 0x01, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0x7C, 0x3B, 0xE0, 0x1F, 0xC0, 0x7F, 0x01, 0xFC, 0x07, + 0xE0, 0x0F, 0x00, 0x78, 0x00, 0x00, 0x0C, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xE0, 0x06, 0x00, + 0x1F, 0x87, 0xF0, 0xFE, 0x3F, 0xC7, 0x80, 0xF0, 0x1F, 0x71, 0xFF, 0x7F, 0xFF, 0xFF, 0xFE, 0x3E, + 0x04, 0x00, 0xFF, 0xBF, 0xEF, 0xFF, 0xFE, 0xFE, 0x00, 0x00, 0xC7, 0xBF, 0x7C, 0xE0, 0x00, 0x03, + 0xCF, 0xBE, 0x79, 0xE7, 0x9F, 0x7D, 0xF7, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF0, 0x00, 0x00, 0x07, + 0x83, 0xBC, 0x3D, 0xF1, 0xEF, 0xFF, 0x3F, 0xF1, 0xFF, 0x8F, 0xF0, 0x7C, 0x03, 0xE0, 0x1F, 0x00, + 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x03, 0xC0, + 0xFF, 0xBF, 0xFE, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0xFF, 0xDF, 0xFE, 0x7C, 0x01, 0xF0, 0x03, 0xC0, + 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF0, 0x1F, 0x03, 0xF0, 0x7E, 0x07, 0xC0, 0x78, 0x07, 0xF0, 0x7F, 0x03, 0xF0, 0x3F, 0x07, + 0xE0, 0x78, 0x0F, 0x80, 0xFF, 0xE7, 0xFE, 0x7F, 0xF3, 0xFE, 0x03, 0x00, 0x07, 0x01, 0xFC, 0x3F, + 0xE7, 0xFE, 0x7F, 0xF7, 0x9F, 0xF8, 0xFF, 0x0F, 0xF0, 0xFF, 0x8F, 0x7F, 0xF7, 0xFE, 0x3F, 0xE1, + 0xFC, 0x00, 0x07, 0xFF, 0x3F, 0xF9, 0xFF, 0xCF, 0xFE, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, + 0x0F, 0x00, 0x7C, 0x03, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0xC0, 0x10, + 0x10, 0x11, 0xE0, 0x7B, 0xE0, 0xF7, 0xC3, 0xE7, 0x87, 0xCF, 0x8F, 0x0F, 0x3E, 0x1E, 0x78, 0x3E, + 0xF0, 0x7F, 0xE0, 0x7F, 0x80, 0xFF, 0x01, 0xFE, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x07, 0x80, + 0x0F, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x1F, 0x00, 0x3E, 0x00, 0xFC, 0x01, 0xFC, 0x03, + 0xF8, 0x0F, 0xF0, 0x1F, 0xE0, 0x3F, 0xE0, 0xFB, 0xC1, 0xE7, 0x83, 0xCF, 0x8F, 0x8F, 0x1E, 0x1E, + 0x7C, 0x3E, 0xF8, 0x3D, 0xE0, 0x78, 0xC0, 0xC0, 0x00, 0x01, 0xF8, 0x3F, 0xC7, 0xFC, 0xFF, 0xEF, + 0x3E, 0xF1, 0xEF, 0xFE, 0xFF, 0xE7, 0xFE, 0x3F, 0xE0, 0xFF, 0x01, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, + 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0xF8, 0x00, 0x3E, 0x78, 0x07, 0x9E, 0x01, + 0xE7, 0x80, 0x7F, 0xE0, 0x1F, 0xF8, 0x07, 0xFE, 0x01, 0xEF, 0x80, 0x79, 0xF0, 0x7E, 0x7F, 0xFF, + 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x1F, 0xC0, 0xFF, 0xFF, 0xF8, 0x00, 0xC7, 0xBF, 0x7C, 0xE0, 0x00, + 0x03, 0xCF, 0xBE, 0x79, 0xE7, 0x9F, 0x7D, 0xF7, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF0, 0x00, 0x00, + 0x07, 0x83, 0xBC, 0x3D, 0xF1, 0xEF, 0xFF, 0x3F, 0xF1, 0xFF, 0x8F, 0xF0, 0x7C, 0x03, 0xE0, 0x1F, + 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x03, + 0xC0, 0xFF, 0xBF, 0xFE, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0xFF, 0xDF, 0xFE, 0x7C, 0x01, 0xF0, 0x03, + 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x00, 0x00, 0x00, + 0x00, 0x07, 0x9F, 0x3D, 0xF9, 0xEF, 0xCF, 0xF8, 0x7F, 0xF9, 0xFF, 0xCF, 0xFF, 0x7F, 0xF3, 0xFC, + 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x00, 0x00, 0x03, + 0x00, 0x1E, 0x00, 0xF8, 0x01, 0xF0, 0x0F, 0xE0, 0x7F, 0xC1, 0xFF, 0x0F, 0xBE, 0x7C, 0x79, 0xE1, + 0xF7, 0x87, 0xDE, 0xCF, 0xFB, 0xBF, 0xFF, 0xF7, 0xFF, 0x9F, 0xFE, 0x3F, 0xF0, 0x1F, 0xC3, 0xFC, + 0x3F, 0x87, 0xF8, 0x78, 0x07, 0x80, 0x7C, 0x63, 0xFE, 0x3F, 0xF0, 0xFF, 0x0F, 0xC1, 0xF8, 0x3F, + 0x03, 0xE0, 0x7C, 0x07, 0xC0, 0x78, 0x01, 0x80, 0x10, 0x11, 0xE0, 0x7B, 0xE0, 0xF7, 0xC3, 0xE7, + 0x87, 0xCF, 0x8F, 0x0F, 0x3E, 0x1E, 0x78, 0x3E, 0xF0, 0x7F, 0xE0, 0x7F, 0x80, 0xFF, 0x01, 0xFE, + 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x07, 0x80, 0x0F, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, + 0x1F, 0x00, 0x3E, 0x00, 0xFC, 0x01, 0xFC, 0x03, 0xF8, 0x0F, 0xF0, 0x1F, 0xE0, 0x3F, 0xE0, 0xFB, + 0xC1, 0xE7, 0x83, 0xCF, 0x8F, 0x8F, 0x1E, 0x1E, 0x7C, 0x3E, 0xF8, 0x3D, 0xE0, 0x78, 0xC0, 0xC0, + 0x00, 0x01, 0xF8, 0x3F, 0xC7, 0xFC, 0xFF, 0xEF, 0x3E, 0xF1, 0xEF, 0xFE, 0xFF, 0xE7, 0xFE, 0x3F, + 0xE0, 0xFF, 0x01, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x01, 0xE0, 0x00, 0xFC, 0x00, + 0x3F, 0x80, 0x1F, 0xE0, 0x07, 0xF8, 0x01, 0xFF, 0x00, 0xFF, 0xC0, 0x3E, 0xF0, 0x0F, 0x3E, 0x07, + 0xCF, 0x81, 0xF1, 0xE0, 0x7C, 0x7C, 0x3F, 0xFF, 0x0F, 0xFF, 0xC3, 0xFF, 0xF9, 0xF0, 0x3E, 0x7C, + 0x07, 0xDE, 0x01, 0xFF, 0x80, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x1F, 0x00, 0x07, + 0xC0, 0x00, 0xE0, 0x00, 0x0F, 0xC1, 0xFF, 0x8F, 0xFE, 0xF9, 0xF7, 0x87, 0x80, 0x7C, 0x7F, 0xEF, + 0xFF, 0x7F, 0x7F, 0xC3, 0xFE, 0x3E, 0xFF, 0xF7, 0xFF, 0x9F, 0xFC, 0x38, 0x00, 0x00, 0x07, 0x80, + 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x03, 0xE0, 0x00, 0x7C, 0x00, 0x0F, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, - 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x01, 0xE0, 0x0E, 0x00, 0xE0, - 0x07, 0x01, 0x87, 0x0E, 0x78, 0x7F, 0x81, 0xF8, 0x01, 0x00, 0x7E, 0x0F, 0xFC, 0x7F, 0xF7, 0xCF, - 0xBC, 0x3C, 0x03, 0xE3, 0xFF, 0x7F, 0xFB, 0xFB, 0xFE, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, - 0xE1, 0xC0, 0x03, 0x80, 0x00, 0xF0, 0x00, 0x1C, 0x00, 0x03, 0x00, 0x06, 0x1C, 0x01, 0xCF, 0x00, - 0x7F, 0x80, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, - 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, - 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, - 0x1F, 0x0E, 0x00, 0x78, 0x01, 0xC0, 0x06, 0x01, 0x87, 0x0E, 0x70, 0x7F, 0x81, 0xF8, 0x01, 0x00, - 0x7E, 0x0F, 0xFC, 0x7F, 0xF7, 0xCF, 0xBC, 0x3C, 0x03, 0xE3, 0xFF, 0x7F, 0xFB, 0xFB, 0xFE, 0x1F, - 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE1, 0xC0, 0x01, 0xE0, 0x00, 0x7C, 0x00, 0x0E, 0x00, 0x03, - 0x80, 0x07, 0x1C, 0x01, 0xCF, 0x00, 0x7F, 0x80, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x78, 0x00, 0x3F, - 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, - 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, - 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x07, 0x80, 0x3C, 0x00, 0xE0, 0x06, 0x01, 0x87, 0x0E, - 0x78, 0x7F, 0x81, 0xF8, 0x01, 0x00, 0x7E, 0x0F, 0xFC, 0x7F, 0xF7, 0xCF, 0xBC, 0x3C, 0x03, 0xE3, - 0xFF, 0x7F, 0xFB, 0xFB, 0xFE, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE1, 0xC0, 0x03, 0x9C, - 0x01, 0xFF, 0x00, 0x7F, 0x80, 0x18, 0xC0, 0x07, 0x1C, 0x01, 0xCF, 0x00, 0x7F, 0x80, 0x0F, 0xC0, - 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, - 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, - 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x0E, 0x70, 0xFF, - 0x87, 0xF8, 0x31, 0x81, 0x87, 0x0E, 0x70, 0x7F, 0x81, 0xF8, 0x00, 0x00, 0x7E, 0x0F, 0xFC, 0x7F, - 0xF7, 0xCF, 0xBC, 0x3C, 0x03, 0xE3, 0xFF, 0x7F, 0xFB, 0xFB, 0xFE, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, - 0xFC, 0xFF, 0xE1, 0xC0, 0x07, 0x1C, 0x01, 0xFF, 0x00, 0x3F, 0x80, 0x07, 0xC0, 0x00, 0x00, 0x00, + 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x0F, 0x80, 0x7E, 0x00, 0xE0, + 0x07, 0x00, 0x00, 0x07, 0xE0, 0xFF, 0xC7, 0xFF, 0x7C, 0xFB, 0xC3, 0xC0, 0x3E, 0x3F, 0xF7, 0xFF, + 0xBF, 0xBF, 0xE1, 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, 0xCF, 0xFE, 0x1C, 0x00, 0x00, 0x0F, 0x00, 0x03, + 0x80, 0x01, 0xC0, 0x00, 0x70, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3C, 0xF0, 0x00, 0x00, + 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, + 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, + 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x00, 0x3C, 0x01, 0xE0, 0x07, + 0x00, 0x1C, 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x0F, 0x3C, 0x00, 0x00, 0x3F, 0x03, 0xFF, 0x0F, 0xFE, + 0x7C, 0xF9, 0xE1, 0xE0, 0x0F, 0x87, 0xFE, 0x7F, 0xF9, 0xFD, 0xEF, 0x87, 0xBE, 0x3E, 0x7F, 0xF9, + 0xFF, 0xE3, 0xFF, 0x83, 0x80, 0x00, 0x38, 0x00, 0x0F, 0x00, 0x01, 0xC0, 0x00, 0x30, 0x01, 0xE0, + 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3C, 0xF0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, + 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, + 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, + 0x80, 0x7F, 0xE0, 0x1F, 0x00, 0xE0, 0x07, 0x80, 0x1C, 0x00, 0x60, 0x78, 0x07, 0xE0, 0x7F, 0x87, + 0x9E, 0x00, 0x00, 0x7E, 0x0F, 0xFC, 0x7F, 0xF7, 0xCF, 0xBC, 0x3C, 0x03, 0xE3, 0xFF, 0x7F, 0xFB, + 0xFB, 0xFE, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE1, 0xC0, 0x00, 0x3C, 0x00, 0x0F, 0x00, + 0x01, 0xC0, 0x00, 0x60, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3C, 0xF0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, - 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, - 0x00, 0x07, 0xC0, 0x01, 0xF0, 0x00, 0x38, 0x00, 0x1C, 0x70, 0xFF, 0x07, 0xF8, 0x0F, 0x80, 0x00, - 0x07, 0xE0, 0xFF, 0xC7, 0xFF, 0x7C, 0xFB, 0xC3, 0xC0, 0x3E, 0x3F, 0xF7, 0xFF, 0xBF, 0xBF, 0xE1, - 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, 0xCF, 0xFE, 0x1C, 0x00, 0x00, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, - 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, - 0x00, 0x00, 0x00, 0x03, 0xC0, 0x3E, 0x00, 0xF0, 0x07, 0x00, 0x07, 0xC0, 0x7F, 0xC3, 0xFF, 0x9F, - 0xBE, 0x78, 0x3D, 0xE0, 0xF7, 0xFF, 0xDF, 0xFF, 0x7F, 0xFD, 0xE0, 0x07, 0xC3, 0xCF, 0xFF, 0x3F, - 0xF8, 0x3F, 0xC0, 0x38, 0x00, 0x00, 0x03, 0x80, 0x1F, 0x00, 0x7C, 0x00, 0xE0, 0x0F, 0x80, 0x7C, - 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, 0x01, 0xF0, - 0x0F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xFF, 0x7F, - 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x1F, 0x00, 0x1C, 0x00, 0xE0, 0x00, 0x00, 0x1F, 0x01, 0xFF, - 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, 0x1F, 0x0F, - 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x1E, 0x60, 0xFF, 0x0F, 0xF8, 0x73, 0x80, 0x00, 0x3F, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xF7, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x70, - 0x7F, 0xC1, 0xFE, 0x06, 0x30, 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, - 0x83, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, - 0xE0, 0x00, 0x38, 0x01, 0xE0, 0x07, 0x00, 0x38, 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x0F, 0x3C, 0x00, - 0x01, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0xFF, 0xFB, 0xE0, 0x0F, 0x80, 0x3E, 0x00, 0xF8, 0x03, 0xFF, - 0xEF, 0xFF, 0xBF, 0xFE, 0xFF, 0xF3, 0xE0, 0x0F, 0x80, 0x3E, 0x00, 0xFF, 0xF3, 0xFF, 0xEF, 0xFF, - 0xBF, 0xFE, 0x00, 0x3C, 0x00, 0xE0, 0x07, 0x80, 0x1C, 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x07, 0xBC, - 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, - 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x01, 0xE0, 0x07, 0x00, - 0x1C, 0x00, 0xE0, 0x78, 0x07, 0xE0, 0x7F, 0x87, 0x9E, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFC, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7C, 0x03, - 0xE0, 0x1F, 0x00, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0xE0, 0x03, 0xC0, 0x07, 0x00, - 0x0E, 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x07, 0xBC, 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, + 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x00, 0xF0, 0x07, 0x80, 0x1C, 0x00, + 0xC0, 0x78, 0x07, 0xE0, 0x7F, 0x87, 0x9E, 0x00, 0x00, 0x7E, 0x0F, 0xFC, 0x7F, 0xF7, 0xCF, 0xBC, + 0x3C, 0x03, 0xE3, 0xFF, 0x7F, 0xFB, 0xFB, 0xFE, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE1, + 0xC0, 0x03, 0x9C, 0x01, 0xFF, 0x00, 0x7F, 0x80, 0x18, 0xC0, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x7F, + 0x80, 0x3C, 0xF0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, + 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, + 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, + 0x0E, 0x70, 0xFF, 0x87, 0xF8, 0x31, 0x80, 0x78, 0x07, 0xE0, 0x7F, 0x87, 0x9E, 0x00, 0x00, 0x7E, + 0x0F, 0xFC, 0x7F, 0xF7, 0xCF, 0xBC, 0x3C, 0x03, 0xE3, 0xFF, 0x7F, 0xFB, 0xFB, 0xFE, 0x1F, 0xF1, + 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE1, 0xC0, 0x01, 0xF0, 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3C, 0xF0, + 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, + 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, + 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1E, 0x00, 0x07, 0xC0, 0x01, 0xF0, 0x00, 0x38, 0x00, 0x07, 0x80, 0x7E, 0x07, 0xF8, + 0x79, 0xE0, 0x00, 0x07, 0xE0, 0xFF, 0xC7, 0xFF, 0x7C, 0xFB, 0xC3, 0xC0, 0x3E, 0x3F, 0xF7, 0xFF, + 0xBF, 0xBF, 0xE1, 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, 0xCF, 0xFE, 0x1C, 0x00, 0x00, 0x03, 0xC0, 0x1E, + 0x00, 0xF0, 0x07, 0x80, 0x00, 0x78, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x03, 0x80, 0x07, 0x1C, 0x01, + 0xCF, 0x00, 0x7F, 0x80, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, + 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, + 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, + 0x7F, 0xE0, 0x1F, 0x01, 0xE0, 0x0E, 0x00, 0xE0, 0x07, 0x01, 0x87, 0x0E, 0x78, 0x7F, 0x81, 0xF8, + 0x01, 0x00, 0x7E, 0x0F, 0xFC, 0x7F, 0xF7, 0xCF, 0xBC, 0x3C, 0x03, 0xE3, 0xFF, 0x7F, 0xFB, 0xFB, + 0xFE, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE1, 0xC0, 0x03, 0x80, 0x00, 0xF0, 0x00, 0x1C, + 0x00, 0x03, 0x00, 0x06, 0x1C, 0x01, 0xCF, 0x00, 0x7F, 0x80, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x78, + 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, + 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, + 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x0E, 0x00, 0x78, 0x01, 0xC0, 0x06, 0x01, + 0x87, 0x0E, 0x70, 0x7F, 0x81, 0xF8, 0x01, 0x00, 0x7E, 0x0F, 0xFC, 0x7F, 0xF7, 0xCF, 0xBC, 0x3C, + 0x03, 0xE3, 0xFF, 0x7F, 0xFB, 0xFB, 0xFE, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE1, 0xC0, + 0x01, 0xE0, 0x00, 0x7C, 0x00, 0x0E, 0x00, 0x03, 0x80, 0x07, 0x1C, 0x01, 0xCF, 0x00, 0x7F, 0x80, + 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, + 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, + 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x07, + 0x80, 0x3C, 0x00, 0xE0, 0x06, 0x01, 0x87, 0x0E, 0x78, 0x7F, 0x81, 0xF8, 0x01, 0x00, 0x7E, 0x0F, + 0xFC, 0x7F, 0xF7, 0xCF, 0xBC, 0x3C, 0x03, 0xE3, 0xFF, 0x7F, 0xFB, 0xFB, 0xFE, 0x1F, 0xF1, 0xF7, + 0xFF, 0xBF, 0xFC, 0xFF, 0xE1, 0xC0, 0x03, 0x9C, 0x01, 0xFF, 0x00, 0x7F, 0x80, 0x18, 0xC0, 0x07, + 0x1C, 0x01, 0xCF, 0x00, 0x7F, 0x80, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, + 0xE0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, + 0xE0, 0x7C, 0x78, 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, + 0xF7, 0x80, 0x7F, 0xE0, 0x1F, 0x0E, 0x70, 0xFF, 0x87, 0xF8, 0x31, 0x81, 0x87, 0x0E, 0x70, 0x7F, + 0x81, 0xF8, 0x00, 0x00, 0x7E, 0x0F, 0xFC, 0x7F, 0xF7, 0xCF, 0xBC, 0x3C, 0x03, 0xE3, 0xFF, 0x7F, + 0xFB, 0xFB, 0xFE, 0x1F, 0xF1, 0xF7, 0xFF, 0xBF, 0xFC, 0xFF, 0xE1, 0xC0, 0x07, 0x1C, 0x01, 0xFF, + 0x00, 0x3F, 0x80, 0x07, 0xC0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3F, 0x00, 0x0F, 0xE0, 0x07, 0xF8, + 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x3F, 0xF0, 0x0F, 0xBC, 0x03, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x78, + 0x1F, 0x1F, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xFE, 0x7C, 0x0F, 0x9F, 0x01, 0xF7, 0x80, 0x7F, + 0xE0, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x07, 0xC0, 0x01, 0xF0, 0x00, 0x38, 0x00, + 0x1C, 0x70, 0xFF, 0x07, 0xF8, 0x0F, 0x80, 0x00, 0x07, 0xE0, 0xFF, 0xC7, 0xFF, 0x7C, 0xFB, 0xC3, + 0xC0, 0x3E, 0x3F, 0xF7, 0xFF, 0xBF, 0xBF, 0xE1, 0xFF, 0x1F, 0x7F, 0xFB, 0xFF, 0xCF, 0xFE, 0x1C, + 0x00, 0x00, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x80, 0x7C, 0x03, + 0xE0, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x3E, 0x00, 0xF0, + 0x07, 0x00, 0x07, 0xC0, 0x7F, 0xC3, 0xFF, 0x9F, 0xBE, 0x78, 0x3D, 0xE0, 0xF7, 0xFF, 0xDF, 0xFF, + 0x7F, 0xFD, 0xE0, 0x07, 0xC3, 0xCF, 0xFF, 0x3F, 0xF8, 0x3F, 0xC0, 0x38, 0x00, 0x00, 0x03, 0x80, + 0x1F, 0x00, 0x7C, 0x00, 0xE0, 0x0F, 0x80, 0x7C, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x3F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF7, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x1F, 0x00, + 0x1C, 0x00, 0xE0, 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, + 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x1E, + 0x60, 0xFF, 0x0F, 0xF8, 0x73, 0x80, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, + 0x01, 0xF0, 0x0F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, + 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x70, 0x7F, 0xC1, 0xFE, 0x06, 0x30, 0x00, 0x00, 0x1F, + 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, + 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x00, 0x38, 0x01, 0xE0, 0x07, 0x00, 0x38, + 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x0F, 0x3C, 0x00, 0x01, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0xFF, 0xFB, + 0xE0, 0x0F, 0x80, 0x3E, 0x00, 0xF8, 0x03, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0xFF, 0xF3, 0xE0, 0x0F, + 0x80, 0x3E, 0x00, 0xFF, 0xF3, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0x00, 0x3C, 0x00, 0xE0, 0x07, 0x80, + 0x1C, 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x07, 0xBC, 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, - 0xE0, 0xFF, 0x00, 0xE0, 0x01, 0xE0, 0x0F, 0x80, 0x1C, 0x01, 0xC0, 0x78, 0x07, 0xE0, 0x7F, 0x87, + 0xE0, 0xFF, 0x00, 0xE0, 0x01, 0xE0, 0x07, 0x00, 0x1C, 0x00, 0xE0, 0x78, 0x07, 0xE0, 0x7F, 0x87, 0x9E, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, - 0xFF, 0xF0, 0x00, 0xF0, 0x03, 0xC0, 0x07, 0x00, 0x1C, 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x07, 0xBC, + 0xFF, 0xF0, 0x00, 0xE0, 0x03, 0xC0, 0x07, 0x00, 0x0E, 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x07, 0xBC, 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, - 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x1E, 0x60, 0xFF, 0x0F, - 0xF8, 0x31, 0x00, 0x78, 0x07, 0xE0, 0x7F, 0x87, 0x9E, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x01, 0xE0, 0x0F, 0x80, + 0x1C, 0x01, 0xC0, 0x78, 0x07, 0xE0, 0x7F, 0x87, 0x9E, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7C, 0x03, - 0xE0, 0x1F, 0x00, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x0F, 0x70, 0x7F, 0xC1, 0xFE, 0x06, - 0x30, 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x07, 0xBC, 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, + 0xE0, 0x1F, 0x00, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0xF0, 0x03, 0xC0, 0x07, 0x00, + 0x1C, 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x07, 0xBC, 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, - 0xE0, 0xFF, 0x00, 0xE0, 0x0F, 0x80, 0xFE, 0x07, 0xF8, 0x79, 0xE0, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, - 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0xE0, - 0x1F, 0x00, 0x78, 0x03, 0x80, 0x07, 0xC0, 0x3F, 0x81, 0xEF, 0x0F, 0x3C, 0x00, 0x00, 0x1F, 0x01, - 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, 0x1F, - 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x7C, 0x01, 0xF0, 0x03, - 0x80, 0xFF, 0xCE, 0xE0, 0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0x79, 0xF3, 0xCE, 0x01, 0xE7, 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, 0xC0, 0x77, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x3B, 0xFF, 0x70, 0xF7, - 0xFE, 0xE0, 0x7B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x7B, 0xFF, 0x70, 0x03, - 0xE0, 0x03, 0xFE, 0x01, 0xFF, 0xE0, 0xFF, 0xF8, 0x7E, 0x3F, 0x3F, 0x03, 0xEF, 0x80, 0xFB, 0xE0, - 0x1E, 0xF0, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF8, 0x07, 0xBE, 0x03, 0xE7, 0xC1, - 0xF9, 0xFD, 0xFC, 0x3F, 0xFE, 0x07, 0xFF, 0x00, 0xFF, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x70, - 0x00, 0x3E, 0x00, 0x0F, 0x80, 0x01, 0xC0, 0x00, 0x07, 0xC0, 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xE7, - 0xC7, 0xCF, 0x07, 0x9E, 0x0F, 0xBC, 0x1F, 0x78, 0x3E, 0xF0, 0x79, 0xF1, 0xF1, 0xFF, 0xC3, 0xFF, - 0x81, 0xFE, 0x00, 0xE0, 0x00, 0x00, 0x03, 0x80, 0x0F, 0x80, 0x1F, 0x00, 0x1C, 0x00, 0x03, 0xE0, - 0x00, 0xF8, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x7F, 0xF8, - 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, - 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, - 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x07, 0xC0, 0x0F, 0x80, 0x07, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, 0xC1, 0xF7, 0x83, 0xEF, - 0x07, 0xDE, 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x00, 0x1E, 0x00, - 0x07, 0x00, 0x03, 0xC0, 0x00, 0xE0, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3D, 0xE0, 0x00, + 0xE0, 0xFF, 0x00, 0xE0, 0x1E, 0x60, 0xFF, 0x0F, 0xF8, 0x31, 0x00, 0x78, 0x07, 0xE0, 0x7F, 0x87, + 0x9E, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF0, 0x0F, 0x70, 0x7F, 0xC1, 0xFE, 0x06, 0x30, 0x07, 0x80, 0x3F, 0x01, 0xFE, 0x07, 0xBC, + 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, 0xDF, 0xFF, 0x7F, 0xFD, + 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x0F, 0x80, 0xFE, 0x07, + 0xF8, 0x79, 0xE0, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, + 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xFF, 0x7F, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0xE0, 0x1F, 0x00, 0x78, 0x03, 0x80, 0x07, 0xC0, 0x3F, + 0x81, 0xEF, 0x0F, 0x3C, 0x00, 0x00, 0x1F, 0x01, 0xFF, 0x0F, 0xFE, 0x7E, 0xF9, 0xE0, 0xF7, 0x83, + 0xDF, 0xFF, 0x7F, 0xFD, 0xFF, 0xF7, 0x80, 0x1F, 0x0F, 0x3F, 0xFC, 0xFF, 0xE0, 0xFF, 0x00, 0xE0, + 0x00, 0x00, 0x0E, 0x00, 0x7C, 0x01, 0xF0, 0x03, 0x80, 0xFF, 0xCE, 0xE0, 0x3B, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x79, 0xF3, 0xCE, 0x01, 0xE7, 0xDF, 0x7D, 0xF7, + 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, 0xC0, 0x77, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0x00, 0x3B, 0xFF, 0x70, 0xF7, 0xFE, 0xE0, 0x7B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0x00, 0x7B, 0xFF, 0x70, 0x03, 0xE0, 0x03, 0xFE, 0x01, 0xFF, 0xE0, 0xFF, 0xF8, + 0x7E, 0x3F, 0x3F, 0x03, 0xEF, 0x80, 0xFB, 0xE0, 0x1E, 0xF0, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, + 0xC0, 0x1E, 0xF8, 0x07, 0xBE, 0x03, 0xE7, 0xC1, 0xF9, 0xFD, 0xFC, 0x3F, 0xFE, 0x07, 0xFF, 0x00, + 0xFF, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x3E, 0x00, 0x0F, 0x80, 0x01, 0xC0, 0x00, + 0x07, 0xC0, 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xE7, 0xC7, 0xCF, 0x07, 0x9E, 0x0F, 0xBC, 0x1F, 0x78, + 0x3E, 0xF0, 0x79, 0xF1, 0xF1, 0xFF, 0xC3, 0xFF, 0x81, 0xFE, 0x00, 0xE0, 0x00, 0x00, 0x03, 0x80, + 0x0F, 0x80, 0x1F, 0x00, 0x1C, 0x00, 0x03, 0xE0, 0x00, 0xF8, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x7F, 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, - 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x00, - 0x3C, 0x00, 0x70, 0x01, 0xE0, 0x03, 0x80, 0x3C, 0x00, 0xFC, 0x03, 0xFC, 0x0F, 0x78, 0x00, 0x00, - 0x0F, 0x80, 0x7F, 0xC1, 0xFF, 0xC7, 0xFF, 0xCF, 0x8F, 0x9E, 0x0F, 0x3C, 0x1F, 0x78, 0x3E, 0xF0, - 0x7D, 0xE0, 0xF3, 0xE3, 0xE3, 0xFF, 0x87, 0xFF, 0x03, 0xFC, 0x01, 0xC0, 0x00, 0x78, 0x00, 0x0E, - 0x00, 0x03, 0xC0, 0x00, 0x70, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3D, 0xE0, 0x00, 0x00, - 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x7F, 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, - 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, - 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0xF0, - 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xC0, 0x3C, 0x00, 0xFC, 0x03, 0xFC, 0x0F, 0x78, 0x00, 0x00, 0x0F, - 0x80, 0x7F, 0xC1, 0xFF, 0xC7, 0xFF, 0xCF, 0x8F, 0x9E, 0x0F, 0x3C, 0x1F, 0x78, 0x3E, 0xF0, 0x7D, - 0xE0, 0xF3, 0xE3, 0xE3, 0xFF, 0x87, 0xFF, 0x03, 0xFC, 0x01, 0xC0, 0x00, 0x78, 0x00, 0x1F, 0x00, - 0x01, 0x80, 0x00, 0xE0, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3D, 0xE0, 0x00, 0x00, 0x00, - 0xF8, 0x00, 0xFF, 0x80, 0x7F, 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, - 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, - 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0xF0, 0x01, - 0xF0, 0x00, 0xC0, 0x03, 0x80, 0x3C, 0x00, 0xFC, 0x03, 0xFC, 0x0F, 0x78, 0x00, 0x00, 0x0F, 0x80, - 0x7F, 0xC1, 0xFF, 0xC7, 0xFF, 0xCF, 0x8F, 0x9E, 0x0F, 0x3C, 0x1F, 0x78, 0x3E, 0xF0, 0x7D, 0xE0, - 0xF3, 0xE3, 0xE3, 0xFF, 0x87, 0xFF, 0x03, 0xFC, 0x01, 0xC0, 0x07, 0xB8, 0x03, 0xFE, 0x00, 0xFF, - 0x80, 0x31, 0x80, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3D, 0xE0, 0x00, 0x00, 0x00, 0xF8, - 0x00, 0xFF, 0x80, 0x7F, 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, - 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, - 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x0F, 0x30, 0x3F, 0xE0, - 0x7F, 0xC0, 0xC6, 0x00, 0x3C, 0x00, 0xFC, 0x03, 0xFC, 0x0F, 0x78, 0x00, 0x00, 0x0F, 0x80, 0x7F, - 0xC1, 0xFF, 0xC7, 0xFF, 0xCF, 0x8F, 0x9E, 0x0F, 0x3C, 0x1F, 0x78, 0x3E, 0xF0, 0x7D, 0xE0, 0xF3, - 0xE3, 0xE3, 0xFF, 0x87, 0xFF, 0x03, 0xFC, 0x01, 0xC0, 0x03, 0xE0, 0x01, 0xFC, 0x00, 0xF7, 0x80, - 0x38, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x7F, 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, - 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, - 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, - 0xC0, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x0F, 0x80, 0x03, 0xE0, 0x00, 0x70, 0x00, 0x07, 0xC0, 0x1F, - 0xC0, 0x7B, 0xC0, 0xE3, 0x80, 0x00, 0x00, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, - 0xE0, 0xF3, 0xC1, 0xF7, 0x83, 0xEF, 0x07, 0xDE, 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, - 0xC0, 0x1C, 0x00, 0x00, 0x00, 0x70, 0x01, 0xF0, 0x03, 0xE0, 0x03, 0x80, 0x00, 0xF8, 0x00, 0x3C, - 0x00, 0x1E, 0x00, 0x07, 0x0F, 0x00, 0x03, 0xC0, 0xFF, 0xF0, 0xFF, 0xF8, 0x7F, 0xFE, 0x3F, 0xFF, + 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x07, + 0xC0, 0x0F, 0x80, 0x07, 0x00, 0x1C, 0x00, 0x00, 0x00, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, 0x7F, 0xFC, + 0xF8, 0xF9, 0xE0, 0xF3, 0xC1, 0xF7, 0x83, 0xEF, 0x07, 0xDE, 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, + 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x00, 0x1E, 0x00, 0x07, 0x00, 0x03, 0xC0, 0x00, 0xE0, 0x01, 0xE0, + 0x00, 0xFC, 0x00, 0x7F, 0x80, 0x3D, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x7F, 0xF8, + 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, + 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, + 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x3C, 0x00, 0x70, 0x01, 0xE0, 0x03, 0x80, 0x3C, + 0x00, 0xFC, 0x03, 0xFC, 0x0F, 0x78, 0x00, 0x00, 0x0F, 0x80, 0x7F, 0xC1, 0xFF, 0xC7, 0xFF, 0xCF, + 0x8F, 0x9E, 0x0F, 0x3C, 0x1F, 0x78, 0x3E, 0xF0, 0x7D, 0xE0, 0xF3, 0xE3, 0xE3, 0xFF, 0x87, 0xFF, + 0x03, 0xFC, 0x01, 0xC0, 0x00, 0x78, 0x00, 0x0E, 0x00, 0x03, 0xC0, 0x00, 0x70, 0x01, 0xE0, 0x00, + 0xFC, 0x00, 0x7F, 0x80, 0x3D, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x7F, 0xF8, 0x3F, + 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, + 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, + 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0xF0, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xC0, 0x3C, 0x00, + 0xFC, 0x03, 0xFC, 0x0F, 0x78, 0x00, 0x00, 0x0F, 0x80, 0x7F, 0xC1, 0xFF, 0xC7, 0xFF, 0xCF, 0x8F, + 0x9E, 0x0F, 0x3C, 0x1F, 0x78, 0x3E, 0xF0, 0x7D, 0xE0, 0xF3, 0xE3, 0xE3, 0xFF, 0x87, 0xFF, 0x03, + 0xFC, 0x01, 0xC0, 0x00, 0x78, 0x00, 0x1F, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x01, 0xE0, 0x00, 0xFC, + 0x00, 0x7F, 0x80, 0x3D, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x7F, 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, - 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x01, 0xF0, 0x03, 0xC0, 0x0F, 0x38, 0x1C, 0x70, 0x00, 0xE0, 0xFF, - 0xC7, 0xFF, 0x9F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, 0xC1, 0xF7, 0x83, 0xEF, 0x07, 0xDE, - 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, - 0x3C, 0x00, 0x07, 0x0F, 0x00, 0x03, 0xC0, 0xFF, 0xF0, 0xFF, 0xF8, 0x7F, 0xFE, 0x3F, 0xFF, 0x1F, + 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0xF0, 0x01, 0xF0, 0x00, 0xC0, 0x03, 0x80, 0x3C, 0x00, 0xFC, + 0x03, 0xFC, 0x0F, 0x78, 0x00, 0x00, 0x0F, 0x80, 0x7F, 0xC1, 0xFF, 0xC7, 0xFF, 0xCF, 0x8F, 0x9E, + 0x0F, 0x3C, 0x1F, 0x78, 0x3E, 0xF0, 0x7D, 0xE0, 0xF3, 0xE3, 0xE3, 0xFF, 0x87, 0xFF, 0x03, 0xFC, + 0x01, 0xC0, 0x07, 0xB8, 0x03, 0xFE, 0x00, 0xFF, 0x80, 0x31, 0x80, 0x01, 0xE0, 0x00, 0xFC, 0x00, + 0x7F, 0x80, 0x3D, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, 0x80, 0x7F, 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, - 0xE0, 0x01, 0xC0, 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x1E, 0x38, 0x1C, 0x70, 0x00, 0xE0, 0xFF, 0xC7, - 0xFF, 0x9F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, 0xC1, 0xF7, 0x83, 0xEF, 0x07, 0xDE, 0x0F, - 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x03, 0xE0, 0x00, 0xF8, 0x00, 0x0E, - 0x00, 0x07, 0x0F, 0x00, 0x03, 0xC0, 0xFF, 0xF0, 0xFF, 0xF8, 0x7F, 0xFE, 0x3F, 0xFF, 0x1F, 0x8F, - 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, - 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, - 0x01, 0xC0, 0x00, 0x07, 0xC0, 0x0F, 0x80, 0x07, 0x38, 0x1C, 0x70, 0x00, 0xE0, 0xFF, 0xC7, 0xFF, - 0x9F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, 0xC1, 0xF7, 0x83, 0xEF, 0x07, 0xDE, 0x0F, 0x3E, - 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x07, 0xB8, 0x03, 0xFE, 0x00, 0xFF, 0x80, - 0x39, 0xCF, 0x00, 0x03, 0xC0, 0xFF, 0xF0, 0xFF, 0xF8, 0x7F, 0xFE, 0x3F, 0xFF, 0x1F, 0x8F, 0xCF, - 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, - 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, - 0xC0, 0x00, 0x06, 0x60, 0x1F, 0xC0, 0x3F, 0xB8, 0x66, 0x70, 0x00, 0xE0, 0xFF, 0xC7, 0xFF, 0x9F, - 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, 0xC1, 0xF7, 0x83, 0xEF, 0x07, 0xDE, 0x0F, 0x3E, 0x3E, - 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x00, 0x03, 0xC0, 0x00, 0xF0, 0x3F, 0xFC, 0x3F, - 0xFE, 0x1F, 0xFF, 0x8F, 0xFF, 0xC7, 0xE3, 0xF3, 0xF0, 0x3E, 0xF8, 0x0F, 0xBE, 0x01, 0xEF, 0x00, - 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBC, 0x01, 0xEF, 0x80, 0x7B, 0xE0, 0x3E, 0x7C, 0x1F, 0x9F, 0xDF, - 0xC3, 0xFF, 0xE0, 0x7F, 0xF0, 0x0F, 0xF8, 0x00, 0x70, 0x00, 0x00, 0x00, 0x07, 0x00, 0x03, 0xE0, - 0x00, 0xF8, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x3F, 0xF1, 0xFF, 0xE7, 0xFF, - 0x1F, 0xFF, 0x3E, 0x3E, 0x78, 0x3C, 0xF0, 0x7D, 0xE0, 0xFB, 0xC1, 0xF7, 0x83, 0xCF, 0x8F, 0x8F, - 0xFE, 0x1F, 0xFC, 0x0F, 0xF0, 0x07, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x7C, 0x00, 0xF8, 0x00, 0xE0, - 0x00, 0x70, 0x0E, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, - 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0x7E, - 0x7E, 0x7F, 0xFE, 0x3F, 0xFC, 0x0F, 0xF8, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x03, 0xC0, 0x03, - 0xC0, 0x03, 0xC0, 0xF0, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3F, 0xE1, 0xFF, 0x0F, - 0xF8, 0x7F, 0xC3, 0xFF, 0x3E, 0xFF, 0xF7, 0xFF, 0x9F, 0xBC, 0x30, 0x00, 0x00, 0x07, 0x80, 0x7C, - 0x01, 0xE0, 0x0E, 0x00, 0x07, 0xC0, 0x07, 0xE0, 0x01, 0xC0, 0x01, 0xC0, 0x00, 0x00, 0x70, 0x0E, + 0xE0, 0x01, 0xC0, 0x00, 0x0F, 0x30, 0x3F, 0xE0, 0x7F, 0xC0, 0xC6, 0x00, 0x3C, 0x00, 0xFC, 0x03, + 0xFC, 0x0F, 0x78, 0x00, 0x00, 0x0F, 0x80, 0x7F, 0xC1, 0xFF, 0xC7, 0xFF, 0xCF, 0x8F, 0x9E, 0x0F, + 0x3C, 0x1F, 0x78, 0x3E, 0xF0, 0x7D, 0xE0, 0xF3, 0xE3, 0xE3, 0xFF, 0x87, 0xFF, 0x03, 0xFC, 0x01, + 0xC0, 0x03, 0xE0, 0x01, 0xFC, 0x00, 0xF7, 0x80, 0x38, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xFF, + 0x80, 0x7F, 0xF8, 0x3F, 0xFE, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, + 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, + 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x0F, 0x80, + 0x03, 0xE0, 0x00, 0x70, 0x00, 0x07, 0xC0, 0x1F, 0xC0, 0x7B, 0xC0, 0xE3, 0x80, 0x00, 0x00, 0xF8, + 0x07, 0xFC, 0x1F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, 0xC1, 0xF7, 0x83, 0xEF, 0x07, 0xDE, + 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, 0x00, 0x00, 0x70, 0x01, 0xF0, + 0x03, 0xE0, 0x03, 0x80, 0x00, 0xF8, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x07, 0x0F, 0x00, 0x03, 0xC0, + 0xFF, 0xF0, 0xFF, 0xF8, 0x7F, 0xFE, 0x3F, 0xFF, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, + 0x07, 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, + 0x7E, 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x01, 0xF0, 0x03, + 0xC0, 0x0F, 0x38, 0x1C, 0x70, 0x00, 0xE0, 0xFF, 0xC7, 0xFF, 0x9F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, + 0xE0, 0xF3, 0xC1, 0xF7, 0x83, 0xEF, 0x07, 0xDE, 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, + 0xC0, 0x1C, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x3C, 0x00, 0x07, 0x0F, 0x00, 0x03, 0xC0, 0xFF, + 0xF0, 0xFF, 0xF8, 0x7F, 0xFE, 0x3F, 0xFF, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, + 0xBC, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, + 0x7F, 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x0F, 0x00, 0x1E, 0x00, + 0x1E, 0x38, 0x1C, 0x70, 0x00, 0xE0, 0xFF, 0xC7, 0xFF, 0x9F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, + 0xF3, 0xC1, 0xF7, 0x83, 0xEF, 0x07, 0xDE, 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, + 0x1C, 0x00, 0x03, 0xE0, 0x00, 0xF8, 0x00, 0x0E, 0x00, 0x07, 0x0F, 0x00, 0x03, 0xC0, 0xFF, 0xF0, + 0xFF, 0xF8, 0x7F, 0xFE, 0x3F, 0xFF, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, + 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, + 0x7F, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x07, 0xC0, 0x0F, 0x80, 0x07, + 0x38, 0x1C, 0x70, 0x00, 0xE0, 0xFF, 0xC7, 0xFF, 0x9F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, + 0xC1, 0xF7, 0x83, 0xEF, 0x07, 0xDE, 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, + 0x00, 0x07, 0xB8, 0x03, 0xFE, 0x00, 0xFF, 0x80, 0x39, 0xCF, 0x00, 0x03, 0xC0, 0xFF, 0xF0, 0xFF, + 0xF8, 0x7F, 0xFE, 0x3F, 0xFF, 0x1F, 0x8F, 0xCF, 0xC0, 0xFB, 0xE0, 0x3E, 0xF8, 0x07, 0xBC, 0x01, + 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBE, 0x01, 0xEF, 0x80, 0xF9, 0xF0, 0x7E, 0x7F, 0x7F, + 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x3F, 0xE0, 0x01, 0xC0, 0x00, 0x06, 0x60, 0x1F, 0xC0, 0x3F, 0xB8, + 0x66, 0x70, 0x00, 0xE0, 0xFF, 0xC7, 0xFF, 0x9F, 0xFC, 0x7F, 0xFC, 0xF8, 0xF9, 0xE0, 0xF3, 0xC1, + 0xF7, 0x83, 0xEF, 0x07, 0xDE, 0x0F, 0x3E, 0x3E, 0x3F, 0xF8, 0x7F, 0xF0, 0x3F, 0xC0, 0x1C, 0x00, + 0x00, 0x03, 0xC0, 0x00, 0xF0, 0x3F, 0xFC, 0x3F, 0xFE, 0x1F, 0xFF, 0x8F, 0xFF, 0xC7, 0xE3, 0xF3, + 0xF0, 0x3E, 0xF8, 0x0F, 0xBE, 0x01, 0xEF, 0x00, 0x7B, 0xC0, 0x1E, 0xF0, 0x07, 0xBC, 0x01, 0xEF, + 0x80, 0x7B, 0xE0, 0x3E, 0x7C, 0x1F, 0x9F, 0xDF, 0xC3, 0xFF, 0xE0, 0x7F, 0xF0, 0x0F, 0xF8, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x07, 0x00, 0x03, 0xE0, 0x00, 0xF8, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x00, + 0x1C, 0x00, 0x38, 0x3F, 0xF1, 0xFF, 0xE7, 0xFF, 0x1F, 0xFF, 0x3E, 0x3E, 0x78, 0x3C, 0xF0, 0x7D, + 0xE0, 0xFB, 0xC1, 0xF7, 0x83, 0xCF, 0x8F, 0x8F, 0xFE, 0x1F, 0xFC, 0x0F, 0xF0, 0x07, 0x00, 0x00, + 0x00, 0x1C, 0x00, 0x7C, 0x00, 0xF8, 0x00, 0xE0, 0x00, 0x70, 0x0E, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, + 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, + 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0x7E, 0x7E, 0x7F, 0xFE, 0x3F, 0xFC, 0x0F, 0xF8, 0x03, + 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0xF0, 0x7F, 0xC3, 0xFE, 0x1F, + 0xF0, 0xFF, 0x87, 0xFC, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0xFF, 0x3E, 0xFF, 0xF7, 0xFF, + 0x9F, 0xBC, 0x30, 0x00, 0x00, 0x07, 0x80, 0x7C, 0x01, 0xE0, 0x0E, 0x00, 0x07, 0xC0, 0x07, 0xE0, + 0x01, 0xC0, 0x01, 0xC0, 0x00, 0x00, 0x70, 0x0E, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, - 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0x7E, 0x7E, 0x7F, 0xFE, - 0x3F, 0xFC, 0x0F, 0xF8, 0x03, 0xC0, 0x0F, 0x80, 0x7C, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x78, 0x3F, - 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3F, 0xE1, 0xFF, 0x9F, - 0x7F, 0xFB, 0xFF, 0xCF, 0xDE, 0x18, 0x00, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x0F, - 0x00, 0x03, 0xDC, 0x03, 0xFF, 0x81, 0xFF, 0xE0, 0x7E, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, - 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, - 0x81, 0xF3, 0xE0, 0x7C, 0x7E, 0x7E, 0x1F, 0xFF, 0x83, 0xFF, 0xC0, 0x3F, 0xE0, 0x03, 0xC0, 0x00, - 0x03, 0xE0, 0x07, 0x80, 0x0E, 0x38, 0x3C, 0x70, 0x00, 0xFE, 0x0F, 0xFE, 0x1F, 0xFC, 0x3E, 0xF8, - 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, 0x3E, 0x1E, 0x7E, 0x7C, 0x7F, 0xF8, - 0xFF, 0xF0, 0xFD, 0xE0, 0x60, 0x00, 0x0F, 0x00, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x0F, 0x0F, 0x00, - 0x03, 0xDC, 0x03, 0xFF, 0x81, 0xFF, 0xE0, 0x7E, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, - 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, - 0xF3, 0xE0, 0x7C, 0x7E, 0x7E, 0x1F, 0xFF, 0x83, 0xFF, 0xC0, 0x3F, 0xE0, 0x03, 0xC0, 0x00, 0x1E, - 0x00, 0x3C, 0x00, 0x3C, 0x38, 0x38, 0x70, 0x00, 0xFE, 0x0F, 0xFE, 0x1F, 0xFC, 0x3E, 0xF8, 0x79, - 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, 0x3E, 0x1E, 0x7E, 0x7C, 0x7F, 0xF8, 0xFF, - 0xF0, 0xFD, 0xE0, 0x60, 0x00, 0x07, 0xC0, 0x01, 0xF8, 0x00, 0x1C, 0x00, 0x07, 0x0F, 0x00, 0x03, - 0xDC, 0x03, 0xFF, 0x81, 0xFF, 0xE0, 0x7E, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, + 0xF8, 0x1F, 0xF8, 0x1F, 0x7E, 0x7E, 0x7F, 0xFE, 0x3F, 0xFC, 0x0F, 0xF8, 0x03, 0xC0, 0x0F, 0x80, + 0x7C, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x78, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0xFE, 0x1F, + 0xF0, 0xFF, 0x87, 0xFC, 0x3F, 0xE1, 0xFF, 0x9F, 0x7F, 0xFB, 0xFF, 0xCF, 0xDE, 0x18, 0x00, 0x00, + 0xF0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x0F, 0x00, 0x03, 0xDC, 0x03, 0xFF, 0x81, 0xFF, 0xE0, + 0x7E, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, + 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x7E, 0x7E, 0x1F, 0xFF, + 0x83, 0xFF, 0xC0, 0x3F, 0xE0, 0x03, 0xC0, 0x00, 0x03, 0xE0, 0x07, 0x80, 0x0E, 0x38, 0x3C, 0x70, + 0x00, 0xFE, 0x0F, 0xFE, 0x1F, 0xFC, 0x3E, 0xF8, 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, + 0x9F, 0x0F, 0x3E, 0x1E, 0x7E, 0x7C, 0x7F, 0xF8, 0xFF, 0xF0, 0xFD, 0xE0, 0x60, 0x00, 0x0F, 0x00, + 0x01, 0xE0, 0x00, 0x78, 0x00, 0x0F, 0x0F, 0x00, 0x03, 0xDC, 0x03, 0xFF, 0x81, 0xFF, 0xE0, 0x7E, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, - 0xE0, 0x7C, 0x7E, 0x7E, 0x1F, 0xFF, 0x83, 0xFF, 0xC0, 0x3F, 0xE0, 0x03, 0xC0, 0x00, 0x0F, 0x80, - 0x1F, 0x00, 0x0E, 0x38, 0x38, 0x70, 0x00, 0xFE, 0x0F, 0xFE, 0x1F, 0xFC, 0x3E, 0xF8, 0x79, 0xF0, - 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, 0x3E, 0x1E, 0x7E, 0x7C, 0x7F, 0xF8, 0xFF, 0xF0, - 0xFD, 0xE0, 0x60, 0x00, 0x07, 0x38, 0x03, 0xFE, 0x00, 0xFF, 0x00, 0x33, 0x8F, 0x00, 0x03, 0xDC, - 0x03, 0xFF, 0x81, 0xFF, 0xE0, 0x7E, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, + 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x7E, 0x7E, 0x1F, 0xFF, 0x83, + 0xFF, 0xC0, 0x3F, 0xE0, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x3C, 0x38, 0x38, 0x70, 0x00, + 0xFE, 0x0F, 0xFE, 0x1F, 0xFC, 0x3E, 0xF8, 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, + 0x0F, 0x3E, 0x1E, 0x7E, 0x7C, 0x7F, 0xF8, 0xFF, 0xF0, 0xFD, 0xE0, 0x60, 0x00, 0x07, 0xC0, 0x01, + 0xF8, 0x00, 0x1C, 0x00, 0x07, 0x0F, 0x00, 0x03, 0xDC, 0x03, 0xFF, 0x81, 0xFF, 0xE0, 0x7E, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, - 0x7C, 0x7E, 0x7E, 0x1F, 0xFF, 0x83, 0xFF, 0xC0, 0x3F, 0xE0, 0x03, 0xC0, 0x00, 0x1E, 0x60, 0x7F, - 0xC0, 0xFF, 0xB9, 0xCE, 0x70, 0x00, 0xFE, 0x0F, 0xFE, 0x1F, 0xFC, 0x3E, 0xF8, 0x79, 0xF0, 0xF3, - 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, 0x3E, 0x1E, 0x7E, 0x7C, 0x7F, 0xF8, 0xFF, 0xF0, 0xFD, - 0xE0, 0x60, 0x00, 0x00, 0x03, 0xC0, 0x00, 0xF7, 0x00, 0xFF, 0xE0, 0x7F, 0xF8, 0x1F, 0xBE, 0x07, - 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, - 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x1F, 0x9F, 0x87, 0xFF, 0xE0, 0xFF, 0xF0, - 0x0F, 0xF8, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xF0, 0x00, 0x3C, 0x00, - 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x3F, 0x83, 0xFF, 0x87, 0xFF, 0x0F, 0xBE, 0x1E, 0x7C, 0x3C, 0xF8, - 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x9F, 0x1F, 0xFE, 0x3F, 0xFC, 0x3F, 0x78, - 0x18, 0x00, 0x00, 0x00, 0x3C, 0x00, 0xF8, 0x00, 0xF0, 0x01, 0xC0, 0x00, 0x07, 0x80, 0x00, 0xF0, - 0x00, 0x3C, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, 0x01, 0xEF, 0xC0, 0xFD, 0xF0, 0x3E, 0x3E, 0x1F, - 0x0F, 0x87, 0xC1, 0xF3, 0xE0, 0x7C, 0xF8, 0x0F, 0xFC, 0x01, 0xFE, 0x00, 0x7F, 0x80, 0x0F, 0xC0, - 0x03, 0xF0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, - 0x07, 0x80, 0x1F, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x00, 0x1F, 0x07, 0xFE, 0x0F, 0x3C, - 0x1E, 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, 0xC1, 0xEF, 0x03, 0xDE, 0x07, 0xF8, 0x07, 0xF0, - 0x0F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x78, 0x07, 0xF0, 0x1F, 0xC0, 0x3F, 0x00, 0x18, - 0x00, 0x78, 0x07, 0xBF, 0x03, 0xF7, 0xC0, 0xF8, 0xF8, 0x7C, 0x3E, 0x1F, 0x07, 0xCF, 0x81, 0xF3, - 0xE0, 0x3F, 0xF0, 0x07, 0xF8, 0x01, 0xFE, 0x00, 0x3F, 0x00, 0x0F, 0xC0, 0x01, 0xE0, 0x00, 0x78, - 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0xF8, 0x3F, 0xF0, 0x79, 0xE0, 0xF3, - 0xE3, 0xE7, 0xC7, 0x87, 0x8F, 0x0F, 0xBE, 0x0F, 0x78, 0x1E, 0xF0, 0x3F, 0xC0, 0x3F, 0x80, 0x7F, - 0x00, 0xFC, 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xC0, 0x3F, 0xB8, 0xFE, 0xF9, 0xF9, 0xF0, 0xC1, 0xC0, - 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x0E, 0x00, 0x03, 0x80, 0x00, 0x00, 0x1E, 0x01, 0xEF, 0xC0, 0xFD, - 0xF0, 0x3E, 0x3E, 0x1F, 0x0F, 0x87, 0xC1, 0xF3, 0xE0, 0x7C, 0xF8, 0x0F, 0xFC, 0x01, 0xFE, 0x00, - 0x7F, 0x80, 0x0F, 0xC0, 0x03, 0xF0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, - 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x07, 0x80, 0x0F, 0x80, 0x07, 0x00, 0x1C, 0x00, 0x00, 0x1F, - 0x07, 0xFE, 0x0F, 0x3C, 0x1E, 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, 0xC1, 0xEF, 0x03, 0xDE, - 0x07, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x78, 0x07, 0xF0, 0x1F, - 0xC0, 0x3F, 0x00, 0x18, 0x00, 0x03, 0x9C, 0x01, 0xFE, 0x00, 0x7F, 0x80, 0x19, 0xC0, 0x00, 0x00, - 0x1E, 0x01, 0xEF, 0xC0, 0xFD, 0xF0, 0x3E, 0x3E, 0x1F, 0x0F, 0x87, 0xC1, 0xF3, 0xE0, 0x7C, 0xF8, - 0x0F, 0xFC, 0x01, 0xFE, 0x00, 0x7F, 0x80, 0x0F, 0xC0, 0x03, 0xF0, 0x00, 0x78, 0x00, 0x1E, 0x00, - 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x0F, 0x70, 0x3F, 0xE0, 0x7F, - 0x80, 0xC6, 0x00, 0x00, 0x1F, 0x07, 0xFE, 0x0F, 0x3C, 0x1E, 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, - 0xF7, 0xC1, 0xEF, 0x03, 0xDE, 0x07, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, - 0x00, 0x78, 0x07, 0xF0, 0x1F, 0xC0, 0x3F, 0x00, 0x18, 0x00, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0xFF, - 0xFF, 0xFF, 0xE0, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFE, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xEF, - 0xFB, 0xFE, 0xFF, 0xBF, 0xEF, 0xFB, 0xFE, 0xFF, 0xBF, 0xEF, 0xFB, 0xFE, 0xFF, 0xBF, 0xEF, 0xFB, - 0xFE, 0xFF, 0xBF, 0xEF, 0xFB, 0xFE, 0xFF, 0xBF, 0xEF, 0xFB, 0xFE, 0xFF, 0xBF, 0xEF, 0xFB, 0xFE, - 0xFF, 0xBF, 0xEF, 0xF8, 0xFF, 0xFF, 0xFF, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x18, 0xF3, 0xCE, - 0x79, 0xE7, 0x9E, 0x70, 0x39, 0xF7, 0x9E, 0x79, 0xC7, 0x3C, 0x60, 0x7B, 0xDE, 0xEF, 0x7B, 0xDC, - 0xE0, 0x71, 0xE7, 0x9E, 0x38, 0xF3, 0xCF, 0x18, 0x18, 0x63, 0xCE, 0x3C, 0xE3, 0x9E, 0x79, 0xE7, - 0x9E, 0x7B, 0xE7, 0xBC, 0x71, 0xC0, 0x39, 0xEF, 0xBD, 0xE7, 0xBD, 0xE7, 0xBC, 0xE7, 0x9C, 0xF7, - 0x9C, 0x63, 0x80, 0x79, 0xCF, 0x79, 0xEF, 0x39, 0xEF, 0x3D, 0xE7, 0x3C, 0xE7, 0x3C, 0xE3, 0x00, - 0x73, 0xCF, 0x79, 0xEF, 0xBC, 0xF3, 0x9E, 0x7B, 0xCF, 0x39, 0xE7, 0x18, 0x60, 0x07, 0x00, 0xF0, - 0x0F, 0x00, 0xF0, 0x7F, 0xF7, 0xFF, 0x7F, 0xF0, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x07, - 0x00, 0x70, 0x06, 0x00, 0x07, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x7F, 0xF7, 0xFF, 0x7F, 0xF0, 0xF0, - 0x0F, 0x07, 0xFE, 0x7F, 0xF7, 0xFF, 0x7F, 0xF0, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0x70, 0x07, - 0x00, 0x00, 0x1F, 0x9F, 0xCF, 0xF7, 0xFB, 0xFD, 0xFE, 0x7E, 0x1E, 0x00, 0x01, 0x83, 0xC7, 0xCF, - 0xFF, 0xBC, 0x70, 0x80, 0x79, 0xEF, 0x9E, 0x30, 0x78, 0x7B, 0xC7, 0xFE, 0x3E, 0xF0, 0xF3, 0x02, - 0x00, 0x78, 0x78, 0x73, 0xC7, 0xC7, 0xFE, 0x3E, 0x3E, 0xF0, 0xF0, 0xF3, 0x02, 0x02, 0x00, 0x79, - 0xEF, 0x9E, 0x30, 0x3C, 0x01, 0xC0, 0x00, 0xFC, 0x07, 0x80, 0x03, 0xFC, 0x0E, 0x00, 0x07, 0x38, - 0x3C, 0x00, 0x0E, 0x78, 0xF0, 0x00, 0x1C, 0xF1, 0xC0, 0x00, 0x39, 0xC7, 0x00, 0x00, 0x7F, 0x9E, - 0x00, 0x00, 0x7E, 0x38, 0x00, 0x00, 0x78, 0xE0, 0x00, 0x00, 0x03, 0xC7, 0x83, 0xC0, 0x07, 0x3F, - 0x8F, 0xE0, 0x1C, 0x7F, 0xBF, 0xC0, 0x78, 0xE7, 0x73, 0x80, 0xE3, 0xCE, 0xE7, 0x83, 0x87, 0x9D, - 0xCE, 0x0F, 0x07, 0x7B, 0x9C, 0x3C, 0x0F, 0xF7, 0xF8, 0x70, 0x0F, 0xC7, 0xE0, 0x00, 0x06, 0x03, - 0x00, 0x3C, 0x01, 0xC0, 0x00, 0x00, 0x7E, 0x03, 0xC0, 0x00, 0x00, 0xFF, 0x03, 0x80, 0x00, 0x00, - 0xE7, 0x07, 0x80, 0x00, 0x00, 0xE7, 0x8F, 0x00, 0x00, 0x00, 0xE7, 0x8E, 0x00, 0x00, 0x00, 0xE7, - 0x1C, 0x00, 0x00, 0x00, 0xFF, 0x3C, 0x00, 0x00, 0x00, 0x7E, 0x38, 0x00, 0x00, 0x00, 0x3C, 0x70, - 0x00, 0x00, 0x00, 0x00, 0xF1, 0xE0, 0xF0, 0x7C, 0x00, 0xE7, 0xF1, 0xFC, 0xFE, 0x01, 0xC7, 0xFB, - 0xFD, 0xFE, 0x03, 0xC7, 0x3B, 0x9D, 0xCF, 0x03, 0x8F, 0x3B, 0x9F, 0xC7, 0x07, 0x0F, 0x3B, 0x9D, - 0xCF, 0x0F, 0x07, 0x7B, 0x9D, 0xEF, 0x1E, 0x07, 0xFB, 0xFC, 0xFE, 0x1C, 0x03, 0xF1, 0xF8, 0xFC, - 0x00, 0x00, 0xC0, 0x60, 0x38, 0x73, 0xFD, 0xEF, 0x7B, 0xDC, 0x70, 0xE7, 0x9E, 0xF1, 0xEF, 0x1E, - 0xF1, 0xEF, 0x1C, 0xF1, 0xCE, 0x1C, 0x70, 0xE1, 0xDE, 0x78, 0xFF, 0x1E, 0x3F, 0xC7, 0x8F, 0xF1, - 0xE3, 0xBC, 0x70, 0xEF, 0x1C, 0x3B, 0x87, 0x1E, 0x77, 0xBC, 0xE7, 0x3D, 0xEF, 0x71, 0xCF, 0x1E, - 0xF1, 0xE7, 0x1E, 0x71, 0xE7, 0x8E, 0x78, 0xE7, 0x8E, 0x71, 0xC3, 0xBC, 0x78, 0xFF, 0x1E, 0x3D, - 0xC7, 0x8F, 0x71, 0xE3, 0xDE, 0x38, 0xF7, 0x8E, 0x1D, 0xE3, 0x87, 0x0F, 0x01, 0xF0, 0x3F, 0x83, - 0xBC, 0x79, 0xEF, 0x0E, 0x00, 0x07, 0xC7, 0xC7, 0xC7, 0xC7, 0xC3, 0xE1, 0xF0, 0x7C, 0x1F, 0x07, - 0xC1, 0xF0, 0x78, 0x00, 0x1F, 0x03, 0xE0, 0x78, 0x1F, 0x03, 0xE0, 0x7C, 0x3E, 0x1F, 0x07, 0xC3, - 0xE1, 0xF0, 0x78, 0x00, 0x03, 0xC0, 0x01, 0xE0, 0x07, 0xF6, 0x07, 0xFF, 0x83, 0xF7, 0xC0, 0xFF, - 0xC3, 0xFF, 0xFD, 0xEF, 0xDE, 0xF7, 0xFF, 0x3F, 0xFF, 0x07, 0xFF, 0x07, 0xFF, 0x81, 0xFF, 0x80, - 0x5E, 0x80, 0x0F, 0x00, 0x03, 0x00, 0x78, 0x79, 0xE1, 0xE7, 0x87, 0x9E, 0x1E, 0x78, 0x79, 0xE1, - 0xE7, 0x87, 0x9E, 0x1E, 0x78, 0x79, 0xE1, 0xE7, 0x87, 0x9E, 0x1E, 0x78, 0x79, 0xE1, 0xE0, 0x00, - 0x1E, 0x1E, 0x78, 0x7B, 0xE3, 0xE7, 0x87, 0x8C, 0x0C, 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0xFF, 0xF7, - 0x8F, 0xBC, 0x3C, 0x39, 0xE1, 0xDF, 0x0F, 0xF0, 0x7F, 0x03, 0xF0, 0x0F, 0x00, 0x70, 0x03, 0x80, - 0x00, 0x00, 0xE0, 0x0F, 0x80, 0x7C, 0x03, 0xC0, 0x04, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x60, - 0x00, 0x37, 0xE0, 0x0F, 0xDF, 0xFF, 0xFC, 0x7F, 0xFF, 0xC0, 0x7F, 0xF0, 0x00, 0x07, 0xFF, 0x00, - 0xFF, 0xFF, 0x1F, 0xFF, 0xFD, 0xF8, 0x03, 0xF6, 0x00, 0x03, 0x00, 0x00, 0x80, 0x3C, 0x07, 0x81, - 0xE0, 0x7C, 0x0F, 0x03, 0xC0, 0x78, 0x1F, 0x07, 0xE0, 0xFE, 0x3D, 0xE7, 0xBC, 0x63, 0x00, 0x00, - 0x00, 0x01, 0xF0, 0x01, 0xFC, 0x00, 0xFE, 0x00, 0x7F, 0x00, 0x3F, 0x80, 0x1F, 0xC0, 0x07, 0xC0, - 0x1C, 0x1C, 0x3F, 0x9F, 0x9F, 0xDF, 0xCF, 0xEF, 0xE7, 0xF7, 0xF3, 0xFB, 0xF8, 0xF8, 0xF8, 0x38, - 0x38, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x38, 0x00, - 0x70, 0x00, 0xF0, 0x01, 0xE0, 0x01, 0xC0, 0x03, 0xC0, 0x07, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, - 0x00, 0x1C, 0x00, 0x38, 0x00, 0x78, 0x00, 0x70, 0x00, 0xE0, 0x00, 0xFD, 0xFB, 0xF7, 0xEF, 0x1E, - 0x3C, 0x78, 0xF1, 0xE3, 0xF7, 0xEF, 0xDF, 0xBC, 0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0xDF, 0xBF, 0x7E, - 0xFD, 0xFB, 0xF7, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x7B, 0xF7, 0xEF, 0xDF, 0x8F, 0x1E, 0x3C, 0x78, - 0xF1, 0xEF, 0xDF, 0xBF, 0x7E, 0x07, 0x80, 0x1E, 0x03, 0xFC, 0x0F, 0xF0, 0xFF, 0xC3, 0xFF, 0x3F, - 0xFC, 0xFF, 0xF7, 0x8F, 0x9E, 0x3E, 0xF0, 0xF3, 0xC3, 0xC0, 0x1E, 0x00, 0x78, 0x07, 0xC0, 0x1F, - 0x03, 0xF0, 0x0F, 0xC0, 0xFC, 0x03, 0xF0, 0x1F, 0x00, 0x7C, 0x03, 0xC0, 0x0F, 0x00, 0xF8, 0x03, - 0xE0, 0x0E, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0x0F, 0x80, 0x3E, 0x01, 0xF0, - 0x07, 0xC0, 0x3E, 0x00, 0xF8, 0x01, 0x00, 0x04, 0x00, 0x07, 0x80, 0xF0, 0xFF, 0x07, 0x8F, 0xFC, - 0x3C, 0xFF, 0xF1, 0xE7, 0x8F, 0x8F, 0x3C, 0x3C, 0x78, 0x01, 0xE3, 0xC0, 0x1F, 0x1E, 0x03, 0xF0, - 0xF0, 0x3F, 0x07, 0x81, 0xF0, 0x3C, 0x0F, 0x01, 0xE0, 0xF8, 0x0F, 0x03, 0x80, 0x78, 0x00, 0x00, - 0x00, 0xE0, 0x1E, 0x0F, 0x80, 0xF0, 0x7C, 0x07, 0x83, 0xE0, 0x3C, 0x04, 0x00, 0xC0, 0x78, 0x0F, - 0x07, 0x83, 0xFC, 0x78, 0x7F, 0xE7, 0x8F, 0xFF, 0x78, 0xF1, 0xF7, 0x8F, 0x0F, 0x78, 0x00, 0xF7, - 0x80, 0x1F, 0x78, 0x07, 0xE7, 0x80, 0xFC, 0x78, 0x0F, 0x87, 0x80, 0xF0, 0x78, 0x1E, 0x07, 0x80, - 0xE0, 0x00, 0x00, 0x07, 0x80, 0xE0, 0x78, 0x1F, 0x0F, 0x81, 0xF0, 0x78, 0x1F, 0x03, 0x00, 0x40, - 0x7F, 0xFF, 0xFF, 0xFF, 0xBF, 0xF0, 0x1E, 0x03, 0xC0, 0x70, 0x0E, 0x03, 0xC0, 0x78, 0x0F, 0x01, - 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1C, 0x03, 0x80, 0xF0, 0x1E, 0x00, 0x00, 0x7F, 0x07, 0xFF, 0x3F, - 0xFD, 0xFF, 0xFF, 0x7F, 0xFB, 0xFF, 0xDF, 0xFE, 0xFF, 0xF7, 0xFF, 0xBF, 0xFD, 0xFD, 0xEF, 0xCF, - 0x30, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x00, 0x0F, 0xF1, 0xFF, 0xDF, 0xFE, - 0xFC, 0xFF, 0xE7, 0xFF, 0x3F, 0xF9, 0xEF, 0xCF, 0x7E, 0x79, 0xFF, 0xC7, 0xFE, 0x7F, 0x83, 0xFF, - 0x1F, 0xFC, 0xE7, 0xE7, 0x3F, 0xB9, 0xFD, 0xCF, 0xEE, 0x7E, 0x73, 0xF3, 0xFF, 0x1F, 0xF0, 0x06, - 0x01, 0xC0, 0xB9, 0x3F, 0xF7, 0xFE, 0x7F, 0x8F, 0xF3, 0xFF, 0x76, 0xE5, 0xC8, 0x38, 0x00, 0x00, - 0x79, 0xEF, 0x9E, 0x30, 0x00, 0x00, 0x00, 0x07, 0x9E, 0x78, 0xF3, 0xCF, 0x1C, 0x70, 0x07, 0xFF, - 0x01, 0xFF, 0xFF, 0x1F, 0xFF, 0xFD, 0xF8, 0x03, 0xF6, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x7E, 0x7E, 0x1F, 0xFF, 0x83, 0xFF, + 0xC0, 0x3F, 0xE0, 0x03, 0xC0, 0x00, 0x0F, 0x80, 0x1F, 0x00, 0x0E, 0x38, 0x38, 0x70, 0x00, 0xFE, + 0x0F, 0xFE, 0x1F, 0xFC, 0x3E, 0xF8, 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, + 0x3E, 0x1E, 0x7E, 0x7C, 0x7F, 0xF8, 0xFF, 0xF0, 0xFD, 0xE0, 0x60, 0x00, 0x07, 0x38, 0x03, 0xFE, + 0x00, 0xFF, 0x00, 0x33, 0x8F, 0x00, 0x03, 0xDC, 0x03, 0xFF, 0x81, 0xFF, 0xE0, 0x7E, 0xF8, 0x1F, + 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, + 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0x7E, 0x7E, 0x1F, 0xFF, 0x83, 0xFF, 0xC0, + 0x3F, 0xE0, 0x03, 0xC0, 0x00, 0x1E, 0x60, 0x7F, 0xC0, 0xFF, 0xB9, 0xCE, 0x70, 0x00, 0xFE, 0x0F, + 0xFE, 0x1F, 0xFC, 0x3E, 0xF8, 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, 0x9F, 0x0F, 0x3E, + 0x1E, 0x7E, 0x7C, 0x7F, 0xF8, 0xFF, 0xF0, 0xFD, 0xE0, 0x60, 0x00, 0x00, 0x03, 0xC0, 0x00, 0xF7, + 0x00, 0xFF, 0xE0, 0x7F, 0xF8, 0x1F, 0xBE, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, + 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, 0x1F, 0x3E, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x7C, 0xF8, + 0x1F, 0x1F, 0x9F, 0x87, 0xFF, 0xE0, 0xFF, 0xF0, 0x0F, 0xF8, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x0F, + 0x00, 0x03, 0xC0, 0x00, 0xF0, 0x00, 0x3C, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x3F, 0x83, 0xFF, + 0x87, 0xFF, 0x0F, 0xBE, 0x1E, 0x7C, 0x3C, 0xF8, 0x79, 0xF0, 0xF3, 0xE1, 0xE7, 0xC3, 0xCF, 0x87, + 0x9F, 0x9F, 0x1F, 0xFE, 0x3F, 0xFC, 0x3F, 0x78, 0x18, 0x00, 0x00, 0x00, 0x3C, 0x00, 0xF8, 0x00, + 0xF0, 0x01, 0xC0, 0x00, 0x07, 0x80, 0x00, 0xF0, 0x00, 0x3C, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1E, + 0x01, 0xEF, 0xC0, 0xFD, 0xF0, 0x3E, 0x3E, 0x1F, 0x0F, 0x87, 0xC1, 0xF3, 0xE0, 0x7C, 0xF8, 0x0F, + 0xFC, 0x01, 0xFE, 0x00, 0x7F, 0x80, 0x0F, 0xC0, 0x03, 0xF0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, + 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x1F, 0x00, 0x1E, 0x00, 0x1C, 0x00, + 0x3C, 0x00, 0x00, 0x1F, 0x07, 0xFE, 0x0F, 0x3C, 0x1E, 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, + 0xC1, 0xEF, 0x03, 0xDE, 0x07, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, + 0x78, 0x07, 0xF0, 0x1F, 0xC0, 0x3F, 0x00, 0x18, 0x00, 0x78, 0x07, 0xBF, 0x03, 0xF7, 0xC0, 0xF8, + 0xF8, 0x7C, 0x3E, 0x1F, 0x07, 0xCF, 0x81, 0xF3, 0xE0, 0x3F, 0xF0, 0x07, 0xF8, 0x01, 0xFE, 0x00, + 0x3F, 0x00, 0x0F, 0xC0, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, + 0x78, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, + 0xE0, 0x00, 0xF8, 0x3F, 0xF0, 0x79, 0xE0, 0xF3, 0xE3, 0xE7, 0xC7, 0x87, 0x8F, 0x0F, 0xBE, 0x0F, + 0x78, 0x1E, 0xF0, 0x3F, 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xFC, 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xC0, + 0x3F, 0xB8, 0xFE, 0xF9, 0xF9, 0xF0, 0xC1, 0xC0, 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x0E, 0x00, 0x03, + 0x80, 0x00, 0x00, 0x1E, 0x01, 0xEF, 0xC0, 0xFD, 0xF0, 0x3E, 0x3E, 0x1F, 0x0F, 0x87, 0xC1, 0xF3, + 0xE0, 0x7C, 0xF8, 0x0F, 0xFC, 0x01, 0xFE, 0x00, 0x7F, 0x80, 0x0F, 0xC0, 0x03, 0xF0, 0x00, 0x78, + 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x07, 0x80, + 0x0F, 0x80, 0x07, 0x00, 0x1C, 0x00, 0x00, 0x1F, 0x07, 0xFE, 0x0F, 0x3C, 0x1E, 0x7C, 0x7C, 0xF8, + 0xF0, 0xF1, 0xE1, 0xF7, 0xC1, 0xEF, 0x03, 0xDE, 0x07, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0x80, + 0x1F, 0x00, 0x3E, 0x00, 0x78, 0x07, 0xF0, 0x1F, 0xC0, 0x3F, 0x00, 0x18, 0x00, 0x03, 0x9C, 0x01, + 0xFE, 0x00, 0x7F, 0x80, 0x19, 0xC0, 0x00, 0x00, 0x1E, 0x01, 0xEF, 0xC0, 0xFD, 0xF0, 0x3E, 0x3E, + 0x1F, 0x0F, 0x87, 0xC1, 0xF3, 0xE0, 0x7C, 0xF8, 0x0F, 0xFC, 0x01, 0xFE, 0x00, 0x7F, 0x80, 0x0F, + 0xC0, 0x03, 0xF0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, + 0x00, 0x07, 0x80, 0x0F, 0x70, 0x3F, 0xE0, 0x7F, 0x80, 0xC6, 0x00, 0x00, 0x1F, 0x07, 0xFE, 0x0F, + 0x3C, 0x1E, 0x7C, 0x7C, 0xF8, 0xF0, 0xF1, 0xE1, 0xF7, 0xC1, 0xEF, 0x03, 0xDE, 0x07, 0xF8, 0x07, + 0xF0, 0x0F, 0xE0, 0x1F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x78, 0x07, 0xF0, 0x1F, 0xC0, 0x3F, 0x00, + 0x18, 0x00, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x7F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xEF, 0xFB, 0xFE, 0xFF, 0xBF, 0xEF, 0xFB, 0xFE, 0xFF, + 0xBF, 0xEF, 0xFB, 0xFE, 0xFF, 0xBF, 0xEF, 0xFB, 0xFE, 0xFF, 0xBF, 0xEF, 0xFB, 0xFE, 0xFF, 0xBF, + 0xEF, 0xFB, 0xFE, 0xFF, 0xBF, 0xEF, 0xFB, 0xFE, 0xFF, 0xBF, 0xEF, 0xF8, 0xFF, 0xFF, 0xFF, 0x00, + 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x18, 0xF3, 0xCE, 0x79, 0xE7, 0x9E, 0x70, 0x39, 0xF7, 0x9E, 0x79, + 0xC7, 0x3C, 0x60, 0x7B, 0xDE, 0xEF, 0x7B, 0xDC, 0xE0, 0x71, 0xE7, 0x9E, 0x38, 0xF3, 0xCF, 0x18, + 0x18, 0x63, 0xCE, 0x3C, 0xE3, 0x9E, 0x79, 0xE7, 0x9E, 0x7B, 0xE7, 0xBC, 0x71, 0xC0, 0x39, 0xEF, + 0xBD, 0xE7, 0xBD, 0xE7, 0xBC, 0xE7, 0x9C, 0xF7, 0x9C, 0x63, 0x80, 0x79, 0xCF, 0x79, 0xEF, 0x39, + 0xEF, 0x3D, 0xE7, 0x3C, 0xE7, 0x3C, 0xE3, 0x00, 0x73, 0xCF, 0x79, 0xEF, 0xBC, 0xF3, 0x9E, 0x7B, + 0xCF, 0x39, 0xE7, 0x18, 0x60, 0x07, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x7F, 0xF7, 0xFF, 0x7F, 0xF0, + 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x07, 0x00, 0x70, 0x06, 0x00, 0x07, 0x00, 0xF0, 0x0F, + 0x00, 0xF0, 0x7F, 0xF7, 0xFF, 0x7F, 0xF0, 0xF0, 0x0F, 0x07, 0xFE, 0x7F, 0xF7, 0xFF, 0x7F, 0xF0, + 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0x70, 0x07, 0x00, 0x00, 0x1F, 0x9F, 0xCF, 0xF7, 0xFB, 0xFD, + 0xFE, 0x7E, 0x1E, 0x00, 0x01, 0x83, 0xC7, 0xCF, 0xFF, 0xBC, 0x70, 0x80, 0x79, 0xEF, 0x9E, 0x30, + 0x78, 0x7B, 0xC7, 0xFE, 0x3E, 0xF0, 0xF3, 0x02, 0x00, 0x78, 0x78, 0x73, 0xC7, 0xC7, 0xFE, 0x3E, + 0x3E, 0xF0, 0xF0, 0xF3, 0x02, 0x02, 0x00, 0x79, 0xEF, 0x9E, 0x30, 0x3C, 0x01, 0xC0, 0x00, 0xFC, + 0x07, 0x80, 0x03, 0xFC, 0x0E, 0x00, 0x07, 0x38, 0x3C, 0x00, 0x0E, 0x78, 0xF0, 0x00, 0x1C, 0xF1, + 0xC0, 0x00, 0x39, 0xC7, 0x00, 0x00, 0x7F, 0x9E, 0x00, 0x00, 0x7E, 0x38, 0x00, 0x00, 0x78, 0xE0, + 0x00, 0x00, 0x03, 0xC7, 0x83, 0xC0, 0x07, 0x3F, 0x8F, 0xE0, 0x1C, 0x7F, 0xBF, 0xC0, 0x78, 0xE7, + 0x73, 0x80, 0xE3, 0xCE, 0xE7, 0x83, 0x87, 0x9D, 0xCE, 0x0F, 0x07, 0x7B, 0x9C, 0x3C, 0x0F, 0xF7, + 0xF8, 0x70, 0x0F, 0xC7, 0xE0, 0x00, 0x06, 0x03, 0x00, 0x3C, 0x01, 0xC0, 0x00, 0x00, 0x7E, 0x03, + 0xC0, 0x00, 0x00, 0xFF, 0x03, 0x80, 0x00, 0x00, 0xE7, 0x07, 0x80, 0x00, 0x00, 0xE7, 0x8F, 0x00, + 0x00, 0x00, 0xE7, 0x8E, 0x00, 0x00, 0x00, 0xE7, 0x1C, 0x00, 0x00, 0x00, 0xFF, 0x3C, 0x00, 0x00, + 0x00, 0x7E, 0x38, 0x00, 0x00, 0x00, 0x3C, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF1, 0xE0, 0xF0, 0x7C, + 0x00, 0xE7, 0xF1, 0xFC, 0xFE, 0x01, 0xC7, 0xFB, 0xFD, 0xFE, 0x03, 0xC7, 0x3B, 0x9D, 0xCF, 0x03, + 0x8F, 0x3B, 0x9F, 0xC7, 0x07, 0x0F, 0x3B, 0x9D, 0xCF, 0x0F, 0x07, 0x7B, 0x9D, 0xEF, 0x1E, 0x07, + 0xFB, 0xFC, 0xFE, 0x1C, 0x03, 0xF1, 0xF8, 0xFC, 0x00, 0x00, 0xC0, 0x60, 0x38, 0x73, 0xFD, 0xEF, + 0x7B, 0xDC, 0x70, 0xE7, 0x9E, 0xF1, 0xEF, 0x1E, 0xF1, 0xEF, 0x1C, 0xF1, 0xCE, 0x1C, 0x70, 0xE1, + 0xDE, 0x78, 0xFF, 0x1E, 0x3F, 0xC7, 0x8F, 0xF1, 0xE3, 0xBC, 0x70, 0xEF, 0x1C, 0x3B, 0x87, 0x1E, + 0x77, 0xBC, 0xE7, 0x3D, 0xEF, 0x71, 0xCF, 0x1E, 0xF1, 0xE7, 0x1E, 0x71, 0xE7, 0x8E, 0x78, 0xE7, + 0x8E, 0x71, 0xC3, 0xBC, 0x78, 0xFF, 0x1E, 0x3D, 0xC7, 0x8F, 0x71, 0xE3, 0xDE, 0x38, 0xF7, 0x8E, + 0x1D, 0xE3, 0x87, 0x0F, 0x01, 0xF0, 0x3F, 0x83, 0xBC, 0x79, 0xEF, 0x0E, 0x00, 0x07, 0xC7, 0xC7, + 0xC7, 0xC7, 0xC3, 0xE1, 0xF0, 0x7C, 0x1F, 0x07, 0xC1, 0xF0, 0x78, 0x00, 0x1F, 0x03, 0xE0, 0x78, + 0x1F, 0x03, 0xE0, 0x7C, 0x3E, 0x1F, 0x07, 0xC3, 0xE1, 0xF0, 0x78, 0x00, 0x03, 0xC0, 0x01, 0xE0, + 0x07, 0xF6, 0x07, 0xFF, 0x83, 0xF7, 0xC0, 0xFF, 0xC3, 0xFF, 0xFD, 0xEF, 0xDE, 0xF7, 0xFF, 0x3F, + 0xFF, 0x07, 0xFF, 0x07, 0xFF, 0x81, 0xFF, 0x80, 0x5E, 0x80, 0x0F, 0x00, 0x03, 0x00, 0x78, 0x79, + 0xE1, 0xE7, 0x87, 0x9E, 0x1E, 0x78, 0x79, 0xE1, 0xE7, 0x87, 0x9E, 0x1E, 0x78, 0x79, 0xE1, 0xE7, + 0x87, 0x9E, 0x1E, 0x78, 0x79, 0xE1, 0xE0, 0x00, 0x1E, 0x1E, 0x78, 0x7B, 0xE3, 0xE7, 0x87, 0x8C, + 0x0C, 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0xFF, 0xF7, 0x8F, 0xBC, 0x3C, 0x39, 0xE1, 0xDF, 0x0F, 0xF0, + 0x7F, 0x03, 0xF0, 0x0F, 0x00, 0x70, 0x03, 0x80, 0x00, 0x00, 0xE0, 0x0F, 0x80, 0x7C, 0x03, 0xC0, + 0x04, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x60, 0x00, 0x37, 0xE0, 0x0F, 0xDF, 0xFF, 0xFC, 0x7F, + 0xFF, 0xC0, 0x7F, 0xF0, 0x00, 0x07, 0xFF, 0x00, 0xFF, 0xFF, 0x1F, 0xFF, 0xFD, 0xF8, 0x03, 0xF6, + 0x00, 0x03, 0x00, 0x00, 0x80, 0x3C, 0x07, 0x81, 0xE0, 0x7C, 0x0F, 0x03, 0xC0, 0x78, 0x1F, 0x07, + 0xE0, 0xFE, 0x3D, 0xE7, 0xBC, 0x63, 0x00, 0x00, 0x00, 0x01, 0xF0, 0x01, 0xFC, 0x00, 0xFE, 0x00, + 0x7F, 0x00, 0x3F, 0x80, 0x1F, 0xC0, 0x07, 0xC0, 0x1C, 0x1C, 0x3F, 0x9F, 0x9F, 0xDF, 0xCF, 0xEF, + 0xE7, 0xF7, 0xF3, 0xFB, 0xF8, 0xF8, 0xF8, 0x38, 0x38, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x06, 0x00, + 0x0E, 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xF0, 0x01, 0xE0, 0x01, 0xC0, 0x03, + 0xC0, 0x07, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x78, 0x00, 0x70, + 0x00, 0xE0, 0x00, 0xFD, 0xFB, 0xF7, 0xEF, 0x1E, 0x3C, 0x78, 0xF1, 0xE3, 0xF7, 0xEF, 0xDF, 0xBC, + 0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0xDF, 0xBF, 0x7E, 0xFD, 0xFB, 0xF7, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, + 0x7B, 0xF7, 0xEF, 0xDF, 0x8F, 0x1E, 0x3C, 0x78, 0xF1, 0xEF, 0xDF, 0xBF, 0x7E, 0x07, 0x80, 0x1E, + 0x03, 0xFC, 0x0F, 0xF0, 0xFF, 0xC3, 0xFF, 0x3F, 0xFC, 0xFF, 0xF7, 0x8F, 0x9E, 0x3E, 0xF0, 0xF3, + 0xC3, 0xC0, 0x1E, 0x00, 0x78, 0x07, 0xC0, 0x1F, 0x03, 0xF0, 0x0F, 0xC0, 0xFC, 0x03, 0xF0, 0x1F, + 0x00, 0x7C, 0x03, 0xC0, 0x0F, 0x00, 0xF8, 0x03, 0xE0, 0x0E, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x38, 0x00, 0xE0, 0x0F, 0x80, 0x3E, 0x01, 0xF0, 0x07, 0xC0, 0x3E, 0x00, 0xF8, 0x01, 0x00, 0x04, + 0x00, 0x07, 0x80, 0xF0, 0xFF, 0x07, 0x8F, 0xFC, 0x3C, 0xFF, 0xF1, 0xE7, 0x8F, 0x8F, 0x3C, 0x3C, + 0x78, 0x01, 0xE3, 0xC0, 0x1F, 0x1E, 0x03, 0xF0, 0xF0, 0x3F, 0x07, 0x81, 0xF0, 0x3C, 0x0F, 0x01, + 0xE0, 0xF8, 0x0F, 0x03, 0x80, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x1E, 0x0F, 0x80, 0xF0, 0x7C, 0x07, + 0x83, 0xE0, 0x3C, 0x04, 0x00, 0xC0, 0x78, 0x0F, 0x07, 0x83, 0xFC, 0x78, 0x7F, 0xE7, 0x8F, 0xFF, + 0x78, 0xF1, 0xF7, 0x8F, 0x0F, 0x78, 0x00, 0xF7, 0x80, 0x1F, 0x78, 0x07, 0xE7, 0x80, 0xFC, 0x78, + 0x0F, 0x87, 0x80, 0xF0, 0x78, 0x1E, 0x07, 0x80, 0xE0, 0x00, 0x00, 0x07, 0x80, 0xE0, 0x78, 0x1F, + 0x0F, 0x81, 0xF0, 0x78, 0x1F, 0x03, 0x00, 0x40, 0x7F, 0xFF, 0xFF, 0xFF, 0xBF, 0xF0, 0x1E, 0x03, + 0xC0, 0x70, 0x0E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1C, 0x03, 0x80, + 0xF0, 0x1E, 0x00, 0x00, 0x7F, 0x07, 0xFF, 0x3F, 0xFD, 0xFF, 0xFF, 0x7F, 0xFB, 0xFF, 0xDF, 0xFE, + 0xFF, 0xF7, 0xFF, 0xBF, 0xFD, 0xFD, 0xEF, 0xCF, 0x30, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, + 0x80, 0x3C, 0x00, 0x0F, 0xF1, 0xFF, 0xDF, 0xFE, 0xFC, 0xFF, 0xE7, 0xFF, 0x3F, 0xF9, 0xEF, 0xCF, + 0x7E, 0x79, 0xFF, 0xC7, 0xFE, 0x7F, 0x83, 0xFF, 0x1F, 0xFC, 0xE7, 0xE7, 0x3F, 0xB9, 0xFD, 0xCF, + 0xEE, 0x7E, 0x73, 0xF3, 0xFF, 0x1F, 0xF0, 0x06, 0x01, 0xC0, 0xB9, 0x3F, 0xF7, 0xFE, 0x7F, 0x8F, + 0xF3, 0xFF, 0x76, 0xE5, 0xC8, 0x38, 0x00, 0x00, 0x79, 0xEF, 0x9E, 0x30, 0x00, 0x00, 0x00, 0x07, + 0x9E, 0x78, 0xF3, 0xCF, 0x1C, 0x70, 0x07, 0xFF, 0x01, 0xFF, 0xFF, 0x1F, 0xFF, 0xFD, 0xF8, 0x03, + 0xF6, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x06, 0x00, 0x03, 0x7E, 0x00, 0xFD, 0xFF, 0xFF, 0xC7, 0xFF, 0xFC, 0x07, 0xFF, 0x00, - 0x0C, 0x01, 0xC1, 0x3A, 0x7F, 0xEF, 0xFC, 0x7F, 0x1F, 0xE7, 0xFE, 0xEF, 0xC9, 0xD0, 0x38, 0x00, - 0x00, 0xC0, 0x1C, 0x13, 0xB7, 0xFE, 0xFF, 0xC7, 0xF1, 0xFE, 0x7F, 0xEE, 0xFC, 0x9D, 0x03, 0x80, - 0x00, 0x00, 0x03, 0x00, 0x01, 0xC3, 0x00, 0xF1, 0xE0, 0x38, 0xF8, 0x1C, 0x3F, 0x0F, 0x0F, 0x83, - 0x81, 0xC1, 0xC0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x1C, 0x00, 0x0F, 0x00, 0x07, 0x87, 0x81, 0xC3, - 0xF0, 0xE0, 0xFC, 0x78, 0x3F, 0x1C, 0x07, 0x8E, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x1F, 0x80, - 0x3F, 0xF0, 0xDF, 0xFF, 0xE7, 0x3F, 0xF8, 0x07, 0xF8, 0x00, 0xF0, 0x03, 0xFE, 0x00, 0xFF, 0xFE, - 0x0F, 0xFF, 0xFD, 0xFF, 0xFF, 0xF7, 0x00, 0x07, 0x00, 0x07, 0x80, 0xDF, 0xC7, 0xFF, 0x9F, 0xFC, - 0x3F, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xE1, 0xFF, 0xC7, 0xFF, 0x8D, 0xFC, 0x07, 0x80, - 0x70, 0xE1, 0xC7, 0x3C, 0xF1, 0xE3, 0xFC, 0x78, 0xF1, 0xDE, 0x3C, 0x79, 0xEF, 0x1E, 0x38, 0xF7, - 0x8E, 0x1C, 0x7B, 0xC7, 0x0E, 0x3D, 0xC3, 0x8F, 0x1C, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, 0xF3, 0xF8, - 0x7E, 0x1F, 0xCF, 0xF3, 0xDF, 0xE7, 0xF8, 0x78, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0x0F, 0x87, - 0xE1, 0xF8, 0xFE, 0x7F, 0xBD, 0xEF, 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x30, 0x7F, 0x9F, 0xE7, 0xFB, - 0xFC, 0xFF, 0xBF, 0xF0, 0x3F, 0xCF, 0xFF, 0x9F, 0xC1, 0xC0, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, 0xFF, - 0xBF, 0xEF, 0xFF, 0xCF, 0xF3, 0xDF, 0xE7, 0xF8, 0x78, 0x7F, 0xDF, 0xF7, 0xFC, 0x1E, 0x07, 0x03, - 0xC0, 0xE0, 0x78, 0x3C, 0x0E, 0x03, 0x80, 0x00, 0x1F, 0xCF, 0xFB, 0xCE, 0xF3, 0x9F, 0xE7, 0xFB, - 0xDF, 0xF3, 0xFF, 0xF7, 0xF8, 0x78, 0x00, 0x1F, 0xCF, 0xFB, 0xDE, 0xE3, 0xFF, 0xFF, 0xFD, 0xFF, - 0xF7, 0xBF, 0xE7, 0xF0, 0x70, 0x0C, 0x0E, 0x07, 0x1F, 0xFF, 0xFF, 0xFC, 0x70, 0x38, 0x00, 0x00, - 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xFD, 0xFE, 0x39, 0xE7, 0x9C, 0xF3, - 0xCF, 0x3C, 0xF1, 0xC7, 0x9E, 0x38, 0x71, 0xE7, 0x8E, 0x3C, 0xF3, 0xCF, 0x3C, 0xE7, 0x9E, 0x70, - 0xEE, 0x7F, 0xFF, 0xFE, 0x7E, 0x3F, 0x1F, 0x8F, 0xC7, 0xE3, 0x80, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, - 0xF3, 0xF8, 0x7E, 0x1F, 0xCF, 0xF3, 0xDF, 0xE7, 0xF8, 0x78, 0x7F, 0xFF, 0xFF, 0x3C, 0xF3, 0xCF, - 0x3C, 0xF1, 0xC0, 0x00, 0x3F, 0xBF, 0xFF, 0xFE, 0x38, 0x3C, 0x7C, 0x7C, 0x78, 0x7F, 0xFF, 0xFF, - 0xF0, 0x00, 0x1F, 0xCF, 0xFB, 0xFE, 0x03, 0x87, 0xE1, 0xF8, 0x0F, 0xF3, 0xFF, 0xE7, 0xF8, 0x70, - 0x0F, 0x87, 0xE1, 0xF8, 0xFE, 0x7F, 0xBD, 0xEF, 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x30, 0x7F, 0x9F, - 0xE7, 0xFB, 0xFC, 0xFF, 0xBF, 0xF0, 0x3F, 0xCF, 0xFF, 0x9F, 0xC1, 0xC0, 0x00, 0x0F, 0xC7, 0xFB, - 0xFF, 0xFF, 0xBF, 0xEF, 0xFF, 0xCF, 0xF3, 0xDF, 0xE7, 0xF8, 0x78, 0x7F, 0xDF, 0xF7, 0xFC, 0x1E, - 0x07, 0x03, 0xC0, 0xE0, 0x78, 0x3C, 0x0E, 0x03, 0x80, 0x00, 0x1F, 0xCF, 0xFB, 0xCE, 0xF3, 0x9F, - 0xE7, 0xFB, 0xDF, 0xF3, 0xFF, 0xF7, 0xF8, 0x78, 0x00, 0x1F, 0xCF, 0xFB, 0xDE, 0xE3, 0xFF, 0xFF, - 0xFD, 0xFF, 0xF7, 0xBF, 0xE7, 0xF0, 0x70, 0x0C, 0x0E, 0x07, 0x1F, 0xFF, 0xFF, 0xFC, 0x70, 0x38, - 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xFD, 0xFE, 0x39, 0xE7, - 0x9C, 0xF3, 0xCF, 0x3C, 0xF1, 0xC7, 0x9E, 0x38, 0x71, 0xE7, 0x8E, 0x3C, 0xF3, 0xCF, 0x3C, 0xE7, - 0x9E, 0x70, 0x1E, 0x3F, 0xDF, 0xE3, 0xF7, 0xFB, 0xDF, 0xFE, 0xFF, 0x3B, 0x80, 0x1E, 0x0F, 0xE7, - 0xFD, 0xFF, 0x7F, 0xDC, 0x47, 0xFC, 0xFE, 0x0E, 0x00, 0x1F, 0x0F, 0xE7, 0xFD, 0xCF, 0x71, 0xDE, - 0xF7, 0xFC, 0xFE, 0x0E, 0x00, 0x73, 0xDE, 0xF3, 0xF8, 0xFC, 0x1F, 0x0F, 0xC7, 0xF9, 0xCF, 0x61, - 0xC0, 0x3E, 0x1F, 0xE7, 0xF8, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0x7F, 0x8F, 0xC1, 0xC0, 0xE0, - 0x70, 0x3B, 0x9F, 0xFF, 0xFF, 0x9F, 0x8F, 0xC7, 0xE3, 0xF1, 0xF8, 0xE0, 0xE0, 0x70, 0x39, 0xFD, - 0xEF, 0xF7, 0xF3, 0xF9, 0xFC, 0xEF, 0x73, 0xF8, 0xE0, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xE0, 0xEE, - 0x73, 0xFF, 0xEF, 0xFF, 0xFD, 0xEF, 0xE7, 0xBF, 0x9E, 0xFE, 0x7B, 0xF9, 0xEF, 0xE3, 0x18, 0xEE, - 0x7F, 0xFF, 0xFE, 0x7E, 0x3F, 0x1F, 0x8F, 0xC7, 0xE3, 0x80, 0xEE, 0x3F, 0xEF, 0xFB, 0xCE, 0xE3, - 0xFD, 0xEF, 0xFB, 0xFC, 0xE6, 0x38, 0x0E, 0x00, 0x00, 0x1F, 0x1F, 0xE7, 0xF1, 0xFC, 0x7F, 0x83, - 0xF7, 0xF9, 0xFE, 0x1E, 0x00, 0x30, 0xF3, 0xF7, 0xEF, 0xCF, 0x1E, 0x3C, 0x7C, 0x78, 0x70, 0x1F, - 0xC0, 0x1F, 0xF0, 0x1F, 0xFC, 0x1F, 0xBE, 0x0F, 0x0F, 0x87, 0x83, 0x83, 0xCF, 0xFF, 0xE7, 0xFF, - 0xF3, 0xFF, 0xF9, 0xFF, 0xFE, 0xF0, 0x0F, 0xFF, 0xE3, 0xFF, 0xF0, 0xFF, 0xF8, 0x0F, 0x00, 0x07, - 0x80, 0x03, 0xFF, 0x81, 0xFF, 0xC0, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x24, 0x00, 0x36, 0x00, 0xFB, - 0x01, 0xFF, 0x81, 0xFF, 0xE1, 0xFF, 0xF1, 0xFB, 0xFD, 0xFB, 0x7E, 0xF9, 0xBF, 0xFC, 0xD8, 0x3C, - 0xCC, 0x1E, 0x6C, 0x0F, 0x36, 0x07, 0xB3, 0x03, 0xFB, 0x1D, 0xFD, 0x9F, 0x7C, 0xDF, 0x3F, 0xFF, - 0x8F, 0xFF, 0x83, 0xFF, 0x81, 0xFF, 0x80, 0xDE, 0x00, 0x6C, 0x00, 0x24, 0x00, 0x03, 0xE0, 0x07, - 0xFC, 0x0F, 0xFF, 0x07, 0xFF, 0xC7, 0xE3, 0xF7, 0xC0, 0xFB, 0xE0, 0x3D, 0xE0, 0x00, 0xF3, 0xDE, - 0x79, 0xFF, 0x3C, 0xFF, 0x1E, 0x7F, 0x8F, 0x3E, 0x07, 0xDE, 0x79, 0xFF, 0x7C, 0xFF, 0xFE, 0x3F, - 0xFE, 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, 0x00, 0x1F, 0xFE, 0x1F, 0xFF, 0x1F, 0xFF, 0x1F, 0xFF, - 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1F, 0xFC, 0x1F, 0xFE, 0x1F, 0xFE, 0x1F, 0xFE, - 0x1E, 0x00, 0x1E, 0x00, 0xFF, 0xC0, 0xFF, 0xC0, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x03, 0xC0, - 0x3F, 0xC1, 0xFF, 0x8F, 0xFE, 0x3C, 0x7D, 0xF0, 0xF7, 0xC0, 0x3F, 0xF0, 0xFF, 0xE3, 0xFF, 0x03, - 0xC0, 0x3F, 0xF8, 0xFF, 0xE0, 0xF0, 0x03, 0xC3, 0xFE, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, - 0x00, 0x1C, 0x00, 0x03, 0xC0, 0xF7, 0xFF, 0xCF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, - 0xF0, 0xFF, 0x0F, 0x0F, 0xF0, 0xF0, 0xFF, 0x1F, 0x0F, 0xF1, 0xF0, 0xFF, 0x3F, 0x0F, 0xF3, 0xF0, - 0xFF, 0x7F, 0x0F, 0xF7, 0xF0, 0xFF, 0xFF, 0x0F, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x3C, 0x03, 0x8F, - 0xC0, 0xF9, 0xF8, 0x1F, 0x3F, 0x83, 0xE7, 0xF8, 0x7C, 0xFF, 0x0F, 0x9F, 0xF1, 0xF7, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xF3, 0xDF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFE, 0xF8, 0xFF, 0x9F, 0x0F, - 0xF3, 0xE0, 0xFE, 0x7C, 0x1F, 0xCF, 0x81, 0xF9, 0xF0, 0x1F, 0x00, 0x7F, 0x80, 0x00, 0xFF, 0xF0, - 0x00, 0xFF, 0xF8, 0x78, 0xFF, 0xFC, 0x78, 0xF8, 0x7C, 0x78, 0xF8, 0x3D, 0xFE, 0xF8, 0x3F, 0xFF, - 0xF8, 0x3F, 0xFF, 0xF8, 0x3D, 0xFE, 0xF8, 0xFC, 0x78, 0xFF, 0xFC, 0x78, 0xFF, 0xF8, 0x78, 0xFF, - 0xE0, 0x78, 0xF8, 0x00, 0x78, 0xF8, 0x00, 0x78, 0xF8, 0x00, 0x78, 0xF8, 0x00, 0x7E, 0xF8, 0x00, - 0x7F, 0xF8, 0x00, 0x3F, 0x00, 0x00, 0x04, 0x7F, 0x80, 0x00, 0x0F, 0xFF, 0x00, 0x00, 0xFF, 0xF8, - 0x00, 0x0F, 0xFF, 0xC0, 0x00, 0xF8, 0x7C, 0x00, 0x0F, 0x83, 0xE3, 0xF0, 0xF8, 0x3E, 0x7F, 0xCF, - 0x83, 0xEF, 0xFE, 0xF8, 0x7D, 0xF3, 0xEF, 0xFF, 0xDF, 0x1E, 0xFF, 0xF9, 0xFC, 0x0F, 0xFF, 0x0F, - 0xF8, 0xFF, 0xF0, 0x7F, 0xEF, 0x9F, 0x01, 0xFE, 0xF8, 0xF8, 0x21, 0xFF, 0x8F, 0x9E, 0x1F, 0xF8, - 0x7D, 0xFF, 0xEF, 0x83, 0xCF, 0xFE, 0xF8, 0x3E, 0x7F, 0xC0, 0x00, 0x00, 0xE0, 0x70, 0x1E, 0x03, - 0xBC, 0x0F, 0x03, 0xDE, 0x0F, 0x81, 0xEF, 0x07, 0xE1, 0xF7, 0xC3, 0xF0, 0xF1, 0xE3, 0xF8, 0x79, - 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0x1E, 0x7F, 0x9E, 0x3F, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xEF, - 0xFF, 0xFF, 0xF0, 0xFF, 0x3F, 0xC0, 0x7F, 0x1F, 0xE0, 0x3F, 0x87, 0xF0, 0x1F, 0xC3, 0xF0, 0x07, - 0xE1, 0xF8, 0x03, 0xE0, 0x7C, 0x01, 0xF0, 0x3E, 0x00, 0xFF, 0xF8, 0xF7, 0xFF, 0xE7, 0xFF, 0xFF, - 0xBF, 0xFF, 0xFD, 0xFF, 0x81, 0xEF, 0xFD, 0xEF, 0x7F, 0xEF, 0x7B, 0xFF, 0x7B, 0xDF, 0xFB, 0xDE, - 0xFF, 0xDE, 0xF7, 0xFE, 0xF0, 0x3D, 0xF7, 0xFF, 0xEF, 0xBF, 0xFF, 0x7D, 0xFF, 0xF0, 0x00, 0x1C, - 0x01, 0xFF, 0x03, 0xFF, 0x03, 0xFF, 0x00, 0x3C, 0x0F, 0xBC, 0x1F, 0xFC, 0x3F, 0xFC, 0x7F, 0xFC, - 0x7C, 0x7C, 0x78, 0x3C, 0x78, 0x3C, 0x78, 0x3C, 0x78, 0x3C, 0x78, 0x3C, 0x7C, 0x7C, 0x7F, 0xFC, - 0x3F, 0xFC, 0x1F, 0xFC, 0x07, 0x00, 0x00, 0x00, 0x7F, 0xFC, 0x7F, 0xFC, 0x7F, 0xFC, 0x00, 0x78, - 0x03, 0xFE, 0x07, 0xFF, 0x0F, 0xFE, 0x0F, 0x8E, 0x1F, 0x00, 0x1F, 0x00, 0x7F, 0xFC, 0xFF, 0xF8, - 0x1E, 0x00, 0x7F, 0xF0, 0x7F, 0xF0, 0xFF, 0xE0, 0x1F, 0x00, 0x1F, 0x00, 0x0F, 0xCE, 0x0F, 0xFE, - 0x07, 0xFF, 0x01, 0xFE, 0x00, 0x70, 0x1E, 0x01, 0xE1, 0xE0, 0x3E, 0x1E, 0x07, 0xC1, 0xE0, 0xFC, - 0x1E, 0x1F, 0x81, 0xE3, 0xF0, 0x1E, 0x7E, 0x07, 0xFF, 0xFC, 0xFF, 0xFF, 0xCF, 0xFF, 0xFC, 0xFF, - 0xFF, 0xC1, 0xFF, 0xC0, 0x1F, 0xFE, 0x01, 0xF3, 0xE0, 0x1E, 0x1F, 0x01, 0xE0, 0xF8, 0x1E, 0x0F, - 0xC1, 0xE0, 0x7C, 0x1E, 0x03, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, - 0x03, 0xE0, 0x03, 0xF8, 0x03, 0xF8, 0x03, 0xF0, 0x0F, 0xE0, 0x0F, 0xF8, 0x0B, 0xF8, 0x07, 0xF0, - 0x0F, 0xE0, 0x0F, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xC0, 0x00, 0x00, - 0x00, 0x3C, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x01, 0xFF, 0xF8, 0x00, 0x00, 0x3F, 0xFF, - 0xC0, 0x00, 0x01, 0xFF, 0xFE, 0x00, 0x00, 0x1B, 0xC3, 0xF0, 0x00, 0x00, 0x3C, 0x1F, 0x03, 0xF0, - 0x03, 0xC0, 0xF8, 0xFF, 0x80, 0x3C, 0x0F, 0x9F, 0xFC, 0x03, 0xC0, 0xF9, 0xFF, 0xE0, 0x3C, 0x0F, - 0xBE, 0x3E, 0x03, 0xC0, 0xFB, 0xC1, 0xE0, 0xFC, 0x0F, 0xBC, 0x1F, 0x3F, 0xC0, 0xFB, 0xC1, 0xF7, - 0xFC, 0x0F, 0xBC, 0x1F, 0x7F, 0xC0, 0xF3, 0xE1, 0xE7, 0x3F, 0x1F, 0x3E, 0x3E, 0x7F, 0xFF, 0xE3, - 0xFF, 0xE7, 0xFF, 0xFC, 0x3F, 0xFC, 0x3F, 0x3F, 0x83, 0xDF, 0x80, 0xC0, 0xE0, 0x3C, 0x60, 0x00, - 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x3C, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xC0, 0x0F, 0xFE, 0x03, 0xFF, 0xC0, 0xFF, 0xF8, 0x3E, - 0x3E, 0x1F, 0xFF, 0xE7, 0xFF, 0xF8, 0xF8, 0x7C, 0x7F, 0xFF, 0x9F, 0xFF, 0xE7, 0xFF, 0xF8, 0xFF, - 0xF8, 0x3F, 0xFE, 0x0F, 0xFF, 0x03, 0xFF, 0x00, 0xF8, 0x00, 0x3E, 0x00, 0x0F, 0x80, 0x03, 0xE0, - 0x00, 0x00, 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x01, 0xFF, 0x01, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0xFC, - 0xFB, 0x9E, 0xF9, 0xCF, 0xFC, 0xE0, 0x3C, 0x70, 0x1E, 0x3F, 0xFF, 0x1F, 0xFF, 0x8F, 0xFF, 0xE7, - 0xFF, 0xF3, 0x9F, 0x7D, 0xCF, 0x3F, 0xFF, 0x8F, 0xFF, 0x83, 0xFF, 0x80, 0xFF, 0x80, 0x0E, 0x00, - 0x07, 0x00, 0x01, 0x80, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x3F, 0x80, 0x1F, 0xE0, 0x07, 0xF8, 0x01, - 0xFF, 0x00, 0xFF, 0xC0, 0x3E, 0xF0, 0x0F, 0x3E, 0x3F, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x7C, 0x7F, - 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xFD, 0xF0, 0x3E, 0x7C, 0x07, 0xDE, 0x01, 0xFF, 0x80, 0x7C, 0x07, - 0xC0, 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xC7, 0xC7, 0x8F, 0x0F, 0x3F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x80, 0x1E, 0x1F, 0x3E, 0x3E, 0x7F, 0xF8, 0x7F, - 0xF0, 0x7F, 0xC0, 0x3C, 0x00, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x01, 0xFF, 0x01, 0xFF, 0xE1, - 0xFF, 0xF1, 0xFF, 0xFD, 0xFB, 0x9E, 0xF9, 0xCF, 0xFC, 0xE0, 0x3C, 0x70, 0x1E, 0x38, 0x0F, 0x1C, - 0x07, 0x8E, 0x03, 0xE7, 0x1D, 0xF3, 0x9F, 0x7D, 0xDF, 0x3F, 0xFF, 0x8F, 0xFF, 0x83, 0xFF, 0x80, - 0xFF, 0x80, 0x0E, 0x00, 0x07, 0x00, 0x01, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, - 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, - 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x3F, 0xF9, - 0xFF, 0xF7, 0xFF, 0x9F, 0xFE, 0x03, 0xE1, 0xFF, 0xF7, 0xFF, 0x9F, 0xFE, 0x03, 0xE1, 0xFF, 0x87, - 0xFC, 0x1F, 0xE0, 0x7E, 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x3E, 0x00, - 0x3C, 0x00, 0xF0, 0x03, 0xD8, 0x0F, 0xE0, 0xFF, 0x83, 0xFC, 0x0F, 0xD8, 0x3F, 0xE0, 0x7F, 0x83, - 0xFC, 0x0F, 0xC3, 0xFF, 0x0F, 0x3C, 0x7C, 0xF1, 0xF3, 0xC7, 0x8F, 0x7E, 0x3F, 0xF0, 0xFF, 0x83, - 0xFC, 0x07, 0x80, 0x03, 0xE0, 0x01, 0xF0, 0x00, 0xF8, 0x00, 0xFE, 0x01, 0xFF, 0x81, 0xFF, 0xF0, - 0xFF, 0xF8, 0xFF, 0xFE, 0x7B, 0xEF, 0x7D, 0xF7, 0xFE, 0xFB, 0xFF, 0x7D, 0xFF, 0x3E, 0xFF, 0x9F, - 0x7C, 0x1F, 0xE0, 0x1F, 0xFE, 0x0F, 0xFF, 0x87, 0xFF, 0xE3, 0xE1, 0xF1, 0xF0, 0x7C, 0xF8, 0x3E, - 0x7C, 0x1F, 0x3E, 0x0F, 0xFF, 0xFF, 0xBF, 0xFF, 0x9F, 0xFF, 0x8F, 0xFF, 0x07, 0xFE, 0x03, 0xFF, - 0x81, 0xFF, 0xC0, 0xFF, 0xE0, 0x1F, 0x00, 0x0F, 0x80, 0x00, 0x03, 0x60, 0x03, 0xB8, 0x01, 0xFC, - 0x01, 0xFF, 0x03, 0xFF, 0xC1, 0xFF, 0xF1, 0xFD, 0xFC, 0xFE, 0xFE, 0xFF, 0x7F, 0x7F, 0xBF, 0xFF, - 0xDF, 0xFF, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xF0, 0x00, 0x7C, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xE0, 0x0D, 0x80, 0x1B, 0x01, 0xFE, 0x07, 0xFF, 0x8F, 0xFF, 0x9F, 0xFF, 0xBE, 0x1F, - 0x7C, 0x1E, 0xF8, 0x3D, 0xF0, 0xFB, 0xFF, 0xE7, 0xFF, 0x8F, 0xFF, 0xDF, 0x1F, 0xBE, 0x0F, 0xFC, - 0x1F, 0xF8, 0x3F, 0xF1, 0xFF, 0xFF, 0xF7, 0xFF, 0xEF, 0xFF, 0x01, 0xB0, 0x03, 0x60, 0x06, 0xC0, - 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x0F, 0x80, 0x03, 0xE0, 0x00, 0xF8, 0x00, 0x3E, 0x00, 0x0F, 0x80, - 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0x9F, 0x00, 0x01, 0xF0, 0x00, 0x1F, - 0x00, 0x01, 0xF0, 0x00, 0x1F, 0x00, 0x01, 0xE0, 0x00, 0x18, 0x00, 0x00, 0x40, 0x00, 0x1C, 0x00, - 0x07, 0xC0, 0x01, 0xFC, 0x00, 0x7F, 0xC0, 0x1F, 0xFC, 0x07, 0xFF, 0xC1, 0xFF, 0x7C, 0x7D, 0xE7, - 0xDF, 0x3C, 0x79, 0xC7, 0x86, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x03, 0xC0, 0x00, 0x78, 0x00, 0x0F, - 0x00, 0x01, 0xE0, 0x00, 0x3C, 0x00, 0x07, 0x80, 0x00, 0x00, 0x40, 0x00, 0x1C, 0x00, 0x07, 0xC0, - 0x00, 0x7C, 0x00, 0x07, 0xC0, 0x00, 0x7C, 0x00, 0x07, 0xC7, 0xFF, 0xFC, 0xFF, 0xFF, 0xDF, 0xFF, - 0xFB, 0xFF, 0xFE, 0x00, 0x07, 0x80, 0x03, 0xE0, 0x00, 0xF8, 0x00, 0x3E, 0x00, 0x0F, 0x80, 0x00, - 0xE0, 0x00, 0x08, 0x00, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0x07, 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, - 0x03, 0xC0, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x21, 0xE0, 0x8E, 0x3C, 0x3B, 0xE7, 0x8F, 0x3E, 0xF3, - 0xE3, 0xFE, 0xF8, 0x3F, 0xFE, 0x03, 0xFF, 0x80, 0x3F, 0xE0, 0x03, 0xF8, 0x00, 0x3E, 0x00, 0x03, - 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0xF8, 0x07, 0x80, 0x0F, - 0x80, 0x3E, 0x00, 0xF8, 0x00, 0xF8, 0x0F, 0x80, 0x03, 0xE0, 0xF8, 0x00, 0x0F, 0x8F, 0xFF, 0xFF, - 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xFC, 0x7C, 0x00, 0x07, 0xC1, - 0xF0, 0x00, 0x7C, 0x07, 0xC0, 0x07, 0xC0, 0x1F, 0x00, 0x7C, 0x00, 0x7C, 0x03, 0xC0, 0x01, 0xC0, - 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xF8, 0x00, 0xFE, 0x00, 0xFF, - 0x80, 0xFF, 0xE0, 0xFF, 0xF8, 0xFB, 0xBE, 0xF9, 0xEF, 0xF8, 0xF3, 0xD8, 0x78, 0xC0, 0x3C, 0x00, - 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x0C, 0x3C, - 0x6F, 0x1E, 0x7F, 0xCF, 0x7D, 0xF7, 0x7C, 0x7F, 0xFC, 0x1F, 0xFC, 0x07, 0xFC, 0x01, 0xFC, 0x00, - 0x7C, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x7F, 0xFC, 0xFF, 0xF9, 0xFF, 0xF3, 0xFF, 0xE7, 0xE0, 0x0F, - 0xE0, 0x1F, 0xE0, 0x3B, 0xE0, 0x73, 0xE0, 0xE3, 0xE1, 0xC3, 0xE3, 0x83, 0xE7, 0x03, 0xE0, 0x03, - 0x80, 0x00, 0x00, 0x3F, 0xFE, 0xFF, 0xFD, 0xFF, 0xF9, 0xFF, 0xF0, 0x0F, 0xE0, 0x3F, 0xC0, 0xFF, - 0x83, 0xEF, 0x0F, 0x9E, 0x3E, 0x3C, 0xF8, 0x7B, 0xE0, 0xF7, 0x81, 0xE6, 0x00, 0x00, 0x00, 0x00, - 0x20, 0x00, 0xE0, 0x3B, 0xE0, 0x7B, 0xE0, 0xF3, 0xE1, 0xE3, 0xE3, 0xC3, 0xE7, 0x83, 0xEF, 0x03, - 0xFE, 0x03, 0xFC, 0x03, 0xFB, 0xFF, 0xF7, 0xFF, 0xEF, 0xFF, 0xC0, 0x00, 0x08, 0xE0, 0x39, 0xC0, - 0xFB, 0x83, 0xE7, 0x0F, 0x8E, 0x3E, 0x1C, 0xF8, 0x3B, 0xE0, 0x7F, 0x80, 0xFE, 0x01, 0xF8, 0x03, - 0xFF, 0xE7, 0xFF, 0xCF, 0xFF, 0x80, 0x00, 0x00, 0x78, 0x00, 0x00, 0x3F, 0x00, 0x30, 0x1F, 0xC0, - 0x3C, 0x0F, 0xE0, 0x3E, 0x00, 0x78, 0x3E, 0x00, 0x3C, 0x3E, 0x00, 0x1E, 0x3E, 0x00, 0x0F, 0x3E, - 0x00, 0x0F, 0xBF, 0xFF, 0xFF, 0xBF, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0x81, 0xF0, - 0x00, 0x00, 0x7C, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x78, - 0x00, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x1F, 0x80, 0x00, 0x1F, 0xC0, 0x40, 0x1F, 0xC0, - 0x70, 0x0F, 0x00, 0x7C, 0x07, 0x80, 0x1F, 0x03, 0xC0, 0x07, 0xC1, 0xE0, 0x01, 0xF0, 0xF0, 0x00, - 0x7C, 0x3F, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xE0, 0x00, 0x01, - 0xE0, 0x00, 0x03, 0xE0, 0x00, 0x03, 0xE0, 0x00, 0x03, 0xE0, 0x00, 0x03, 0xE0, 0x00, 0x00, 0xE0, - 0x00, 0x00, 0x20, 0x00, 0x02, 0x00, 0x00, 0x30, 0x00, 0x03, 0x80, 0x00, 0x3C, 0x00, 0x03, 0xFF, - 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0x3E, 0x00, 0x78, 0xF0, 0x03, 0xC3, 0x80, - 0x1E, 0x0C, 0x00, 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3C, 0x00, 0x01, 0xE0, 0x00, 0x0F, 0x00, 0x00, - 0x78, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x02, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x07, - 0xCF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xF0, 0x03, 0xE7, 0x80, 0x1E, - 0x3C, 0x00, 0xE1, 0xE0, 0x06, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, - 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, - 0x3C, 0x08, 0xF0, 0x73, 0xC3, 0xEF, 0x1F, 0x3C, 0xF8, 0xF7, 0xFF, 0xFF, 0xFF, 0x7F, 0xFC, 0xF8, - 0x01, 0xF0, 0x03, 0xE0, 0x07, 0x00, 0x60, 0x00, 0x07, 0x80, 0x00, 0x3C, 0x00, 0x01, 0xE0, 0x00, - 0x0F, 0x00, 0x00, 0x78, 0x01, 0x83, 0xC0, 0x0E, 0x1E, 0x00, 0x78, 0xF0, 0x03, 0xE7, 0xFF, 0xFF, - 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF8, 0x3F, 0xFF, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0x00, - 0x3C, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xF0, 0x00, 0x3C, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, - 0xF0, 0x00, 0x3C, 0x00, 0xFF, 0xF0, 0x1F, 0xF8, 0x03, 0xFC, 0x00, 0x7E, 0x00, 0x0F, 0x00, 0x01, - 0x80, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xF0, 0x00, 0x07, - 0x81, 0x00, 0x3C, 0x18, 0x01, 0xE3, 0xC0, 0x0F, 0x3E, 0x00, 0x7B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0x9F, 0x00, 0x00, 0x78, 0x00, 0x01, 0xC0, 0x00, 0x06, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x7F, 0xC0, 0x03, 0xFF, 0x80, 0x1F, 0xFF, 0x00, 0xFC, 0x7E, 0x03, 0xC0, - 0x78, 0x1E, 0x00, 0xF0, 0x78, 0x03, 0xC1, 0xE0, 0x0F, 0x37, 0xA0, 0x01, 0xFF, 0xC0, 0x07, 0xFF, - 0x80, 0x0F, 0xFC, 0x00, 0x1F, 0xE0, 0x00, 0x3F, 0x00, 0x00, 0x78, 0x00, 0x00, 0xC0, 0x00, 0x07, - 0xF8, 0x00, 0x7F, 0xF8, 0x03, 0xFF, 0xF0, 0x1F, 0xCF, 0xC0, 0x7C, 0x0F, 0x81, 0xE0, 0x1E, 0x0F, - 0x00, 0x3C, 0x3C, 0x00, 0xF0, 0x00, 0x1B, 0xD8, 0x00, 0xFF, 0xF0, 0x03, 0xFF, 0xC0, 0x07, 0xFE, - 0x00, 0x0F, 0xF0, 0x00, 0x1F, 0x80, 0x00, 0x3C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x0C, 0x00, - 0x03, 0x80, 0x00, 0xF0, 0x00, 0x3E, 0x00, 0x0F, 0xF8, 0x03, 0xFF, 0xC0, 0x3F, 0xFC, 0x03, 0xEF, - 0xC0, 0x3C, 0x78, 0x03, 0x87, 0x80, 0x30, 0xF0, 0x00, 0x1E, 0x00, 0x01, 0xFC, 0x00, 0x3B, 0x80, - 0x0F, 0x78, 0x01, 0xEF, 0x00, 0x7C, 0xF0, 0x1F, 0x1F, 0x8F, 0xC1, 0xFF, 0xF8, 0x1F, 0xFC, 0x00, - 0xFF, 0x00, 0x00, 0x40, 0x00, 0x0C, 0x00, 0x01, 0xC0, 0x00, 0x3C, 0x00, 0x07, 0xC0, 0x07, 0xFC, - 0x03, 0xFF, 0xC0, 0xFF, 0xF0, 0x3F, 0x7C, 0x07, 0x8F, 0x01, 0xE1, 0xC0, 0x3C, 0x30, 0x07, 0x00, - 0x01, 0xE0, 0x00, 0x3C, 0x00, 0x3B, 0x80, 0x0F, 0x78, 0x01, 0xEF, 0x00, 0x7C, 0xF0, 0x1F, 0x1F, - 0x8F, 0xC1, 0xFF, 0xF8, 0x1F, 0xFC, 0x00, 0xFF, 0x00, 0x00, 0x08, 0x00, 0x03, 0x80, 0x00, 0xF8, - 0x00, 0x0F, 0x80, 0x00, 0xF9, 0xFF, 0xFF, 0xBF, 0xFF, 0xF7, 0xFF, 0xFE, 0x02, 0x0F, 0x80, 0xE3, - 0xE0, 0x3C, 0xF8, 0x0F, 0x8E, 0x03, 0xE0, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0x3E, - 0x00, 0x03, 0xE0, 0x00, 0x3C, 0x00, 0x03, 0x80, 0x00, 0x20, 0x00, 0x02, 0x00, 0x00, 0xE0, 0x00, - 0x3C, 0x00, 0x0F, 0x80, 0x03, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0x3E, 0x08, - 0x03, 0xE3, 0x80, 0x3C, 0xF8, 0x03, 0x8F, 0x80, 0x20, 0xF9, 0xFF, 0xFF, 0xBF, 0xFF, 0xF7, 0xFF, - 0xFE, 0x00, 0x0F, 0x80, 0x03, 0xE0, 0x00, 0xF8, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x00, 0xC0, 0x00, - 0x3C, 0x00, 0x0F, 0x80, 0x03, 0xE0, 0x00, 0xF8, 0x00, 0x3F, 0xFF, 0xCF, 0xFF, 0xFB, 0xFF, 0xFF, - 0xFC, 0x00, 0x1F, 0x00, 0x01, 0xFF, 0xFF, 0x9F, 0xFF, 0xF1, 0xFF, 0xFE, 0x1F, 0x00, 0x01, 0xF0, - 0x00, 0x1F, 0x00, 0x01, 0xE0, 0x00, 0x18, 0x00, 0x00, 0x40, 0x00, 0x1C, 0x00, 0x07, 0xC0, 0x00, - 0x7C, 0x00, 0x07, 0xC1, 0xFF, 0xFC, 0x3F, 0xFF, 0xC7, 0xFF, 0xFC, 0x00, 0x07, 0xC0, 0x00, 0xFB, - 0xFF, 0xFE, 0x7F, 0xFF, 0x8F, 0xFF, 0xE0, 0x00, 0xF8, 0x00, 0x3E, 0x00, 0x0F, 0x80, 0x00, 0xE0, - 0x00, 0x08, 0x00, 0x00, 0xC0, 0x18, 0x00, 0x0F, 0x01, 0xE0, 0x00, 0xF8, 0x0F, 0x80, 0x0F, 0x80, - 0x3E, 0x00, 0xF8, 0x00, 0xF8, 0x0F, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFE, - 0xFC, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x07, 0xDF, 0xFF, 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xC1, 0xFF, - 0xFF, 0xFC, 0x07, 0xC0, 0x07, 0xC0, 0x1F, 0x00, 0x7C, 0x00, 0x7C, 0x07, 0xC0, 0x01, 0xE0, 0x3C, - 0x00, 0x06, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x3C, 0x03, 0xF0, 0x3F, 0xC3, 0xFF, 0x3F, 0xFD, 0xFF, - 0xC0, 0xF0, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x0F, 0x01, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x07, - 0x80, 0x3C, 0x01, 0xE0, 0x07, 0x00, 0x3C, 0x01, 0xE0, 0x0F, 0x03, 0xFE, 0x1F, 0xF0, 0xFF, 0x80, - 0xF0, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x3F, 0xC0, 0x78, 0x3F, 0xFD, 0xFF, 0xC7, 0xFC, 0x1F, 0xC0, - 0x7C, 0x01, 0xC0, 0x04, 0x00, 0x00, 0x00, 0x03, 0x80, 0x80, 0x0E, 0x06, 0x00, 0x38, 0x38, 0x00, - 0xE1, 0xE0, 0x03, 0x8F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF9, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0x87, 0x80, - 0x0E, 0x0E, 0x00, 0x38, 0x18, 0x00, 0xE0, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x70, 0x01, - 0x81, 0xC0, 0x07, 0x07, 0x00, 0x1E, 0x1F, 0xFF, 0xFC, 0x7F, 0xFF, 0xF9, 0xFF, 0xFF, 0xE7, 0xFF, - 0xFF, 0x1C, 0x00, 0x78, 0x70, 0x01, 0xC1, 0xC0, 0x06, 0x07, 0x00, 0x10, 0x1C, 0x00, 0x08, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x0F, 0xE0, 0x00, 0x0F, 0xF8, 0x00, 0x0F, 0x9E, 0x00, - 0x0F, 0x87, 0x80, 0x0F, 0x81, 0xE0, 0x0F, 0x80, 0x78, 0x0F, 0x80, 0x1E, 0x0F, 0xF0, 0x7F, 0x8F, - 0xF8, 0x3F, 0xE7, 0xFC, 0x1F, 0xF0, 0x1E, 0x0F, 0x00, 0x0F, 0x07, 0x80, 0x07, 0x83, 0xC0, 0x03, - 0xC1, 0xE0, 0x01, 0xFF, 0xF0, 0x00, 0xFF, 0xF8, 0x00, 0x7F, 0xFC, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x0F, 0xE0, 0x00, 0x0F, 0xF8, 0x00, 0x0F, 0x9E, 0x00, 0x0F, - 0x87, 0x80, 0x0F, 0x81, 0xE0, 0x0F, 0x80, 0x78, 0x0F, 0x80, 0x1E, 0x0F, 0xF0, 0x7F, 0x8F, 0xF8, - 0x3F, 0xE7, 0xFC, 0x1F, 0xF0, 0x1E, 0x0F, 0x00, 0x0F, 0xFF, 0x80, 0x07, 0xFF, 0xC0, 0x03, 0xFF, - 0xE0, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF8, 0x00, 0x7F, 0xFC, 0x00, 0x3F, 0xFE, 0x00, 0x1E, 0x0F, - 0x00, 0x0F, 0xFF, 0x80, 0x07, 0xFF, 0xC0, 0x03, 0xFF, 0xE0, 0x00, 0x0F, 0x80, 0xFF, 0x83, 0xFF, - 0x07, 0xFC, 0x00, 0xF8, 0x01, 0xE0, 0xFB, 0xC7, 0xFF, 0x3F, 0xFD, 0xFF, 0xF7, 0x87, 0xDE, 0x0F, - 0x78, 0x3D, 0xE0, 0xF7, 0x87, 0xDF, 0x1E, 0x3F, 0xF8, 0xFF, 0xC1, 0xFE, 0x00, 0xE0, 0x00, 0x00, - 0x1F, 0xCC, 0x7F, 0xF9, 0xFF, 0xE7, 0xFF, 0xCF, 0x8F, 0xBE, 0x3F, 0xF8, 0xEF, 0xF3, 0x9F, 0xFE, - 0x3D, 0xF8, 0xFB, 0xFF, 0xE3, 0xFF, 0xCF, 0xFF, 0x3B, 0xF8, 0x61, 0xC0, 0x01, 0xE0, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x03, 0x7E, 0x00, 0xFD, + 0xFF, 0xFF, 0xC7, 0xFF, 0xFC, 0x07, 0xFF, 0x00, 0x0C, 0x01, 0xC1, 0x3A, 0x7F, 0xEF, 0xFC, 0x7F, + 0x1F, 0xE7, 0xFE, 0xEF, 0xC9, 0xD0, 0x38, 0x00, 0x00, 0xC0, 0x1C, 0x13, 0xB7, 0xFE, 0xFF, 0xC7, + 0xF1, 0xFE, 0x7F, 0xEE, 0xFC, 0x9D, 0x03, 0x80, 0x00, 0x00, 0x03, 0x00, 0x01, 0xC3, 0x00, 0xF1, + 0xE0, 0x38, 0xF8, 0x1C, 0x3F, 0x0F, 0x0F, 0x83, 0x81, 0xC1, 0xC0, 0x00, 0xF0, 0x00, 0x78, 0x00, + 0x1C, 0x00, 0x0F, 0x00, 0x07, 0x87, 0x81, 0xC3, 0xF0, 0xE0, 0xFC, 0x78, 0x3F, 0x1C, 0x07, 0x8E, + 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x1F, 0x80, 0x3F, 0xF0, 0xDF, 0xFF, 0xE7, 0x3F, 0xF8, 0x07, + 0xF8, 0x00, 0xF0, 0x03, 0xFE, 0x00, 0xFF, 0xFE, 0x0F, 0xFF, 0xFD, 0xFF, 0xFF, 0xF7, 0x00, 0x07, + 0x00, 0x07, 0x80, 0xDF, 0xC7, 0xFF, 0x9F, 0xFC, 0x3F, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, + 0xE1, 0xFF, 0xC7, 0xFF, 0x8D, 0xFC, 0x07, 0x80, 0x70, 0xE1, 0xC7, 0x3C, 0xF1, 0xE3, 0xFC, 0x78, + 0xF1, 0xDE, 0x3C, 0x79, 0xEF, 0x1E, 0x38, 0xF7, 0x8E, 0x1C, 0x7B, 0xC7, 0x0E, 0x3D, 0xC3, 0x8F, + 0x1C, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, 0xF3, 0xF8, 0x7E, 0x1F, 0xCF, 0xF3, 0xDF, 0xE7, 0xF8, 0x78, + 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0x0F, 0x87, 0xE1, 0xF8, 0xFE, 0x7F, 0xBD, 0xEF, 0xFF, 0xFF, + 0xFF, 0xC1, 0xE0, 0x30, 0x7F, 0x9F, 0xE7, 0xFB, 0xFC, 0xFF, 0xBF, 0xF0, 0x3F, 0xCF, 0xFF, 0x9F, + 0xC1, 0xC0, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, 0xFF, 0xBF, 0xEF, 0xFF, 0xCF, 0xF3, 0xDF, 0xE7, 0xF8, + 0x78, 0x7F, 0xDF, 0xF7, 0xFC, 0x1E, 0x07, 0x03, 0xC0, 0xE0, 0x78, 0x3C, 0x0E, 0x03, 0x80, 0x00, + 0x1F, 0xCF, 0xFB, 0xCE, 0xF3, 0x9F, 0xE7, 0xFB, 0xDF, 0xF3, 0xFF, 0xF7, 0xF8, 0x78, 0x00, 0x1F, + 0xCF, 0xFB, 0xDE, 0xE3, 0xFF, 0xFF, 0xFD, 0xFF, 0xF7, 0xBF, 0xE7, 0xF0, 0x70, 0x0C, 0x0E, 0x07, + 0x1F, 0xFF, 0xFF, 0xFC, 0x70, 0x38, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xFF, 0xFF, 0xE0, + 0x0F, 0xFF, 0xFD, 0xFE, 0x39, 0xE7, 0x9C, 0xF3, 0xCF, 0x3C, 0xF1, 0xC7, 0x9E, 0x38, 0x71, 0xE7, + 0x8E, 0x3C, 0xF3, 0xCF, 0x3C, 0xE7, 0x9E, 0x70, 0xEE, 0x7F, 0xFF, 0xFE, 0x7E, 0x3F, 0x1F, 0x8F, + 0xC7, 0xE3, 0x80, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, 0xF3, 0xF8, 0x7E, 0x1F, 0xCF, 0xF3, 0xDF, 0xE7, + 0xF8, 0x78, 0x7F, 0xFF, 0xFF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF1, 0xC0, 0x00, 0x3F, 0xBF, 0xFF, 0xFE, + 0x38, 0x3C, 0x7C, 0x7C, 0x78, 0x7F, 0xFF, 0xFF, 0xF0, 0x00, 0x1F, 0xCF, 0xFB, 0xFE, 0x03, 0x87, + 0xE1, 0xF8, 0x0F, 0xF3, 0xFF, 0xE7, 0xF8, 0x70, 0x0F, 0x87, 0xE1, 0xF8, 0xFE, 0x7F, 0xBD, 0xEF, + 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x30, 0x7F, 0x9F, 0xE7, 0xFB, 0xFC, 0xFF, 0xBF, 0xF0, 0x3F, 0xCF, + 0xFF, 0x9F, 0xC1, 0xC0, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, 0xFF, 0xBF, 0xEF, 0xFF, 0xCF, 0xF3, 0xDF, + 0xE7, 0xF8, 0x78, 0x7F, 0xDF, 0xF7, 0xFC, 0x1E, 0x07, 0x03, 0xC0, 0xE0, 0x78, 0x3C, 0x0E, 0x03, + 0x80, 0x00, 0x1F, 0xCF, 0xFB, 0xCE, 0xF3, 0x9F, 0xE7, 0xFB, 0xDF, 0xF3, 0xFF, 0xF7, 0xF8, 0x78, + 0x00, 0x1F, 0xCF, 0xFB, 0xDE, 0xE3, 0xFF, 0xFF, 0xFD, 0xFF, 0xF7, 0xBF, 0xE7, 0xF0, 0x70, 0x0C, + 0x0E, 0x07, 0x1F, 0xFF, 0xFF, 0xFC, 0x70, 0x38, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xFF, + 0xFF, 0xE0, 0x0F, 0xFF, 0xFD, 0xFE, 0x39, 0xE7, 0x9C, 0xF3, 0xCF, 0x3C, 0xF1, 0xC7, 0x9E, 0x38, + 0x71, 0xE7, 0x8E, 0x3C, 0xF3, 0xCF, 0x3C, 0xE7, 0x9E, 0x70, 0x1E, 0x3F, 0xDF, 0xE3, 0xF7, 0xFB, + 0xDF, 0xFE, 0xFF, 0x3B, 0x80, 0x1E, 0x0F, 0xE7, 0xFD, 0xFF, 0x7F, 0xDC, 0x47, 0xFC, 0xFE, 0x0E, + 0x00, 0x1F, 0x0F, 0xE7, 0xFD, 0xCF, 0x71, 0xDE, 0xF7, 0xFC, 0xFE, 0x0E, 0x00, 0x73, 0xDE, 0xF3, + 0xF8, 0xFC, 0x1F, 0x0F, 0xC7, 0xF9, 0xCF, 0x61, 0xC0, 0x3E, 0x1F, 0xE7, 0xF8, 0x8F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xCF, 0x7F, 0x8F, 0xC1, 0xC0, 0xE0, 0x70, 0x3B, 0x9F, 0xFF, 0xFF, 0x9F, 0x8F, 0xC7, + 0xE3, 0xF1, 0xF8, 0xE0, 0xE0, 0x70, 0x39, 0xFD, 0xEF, 0xF7, 0xF3, 0xF9, 0xFC, 0xEF, 0x73, 0xF8, + 0xE0, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xE0, 0xEE, 0x73, 0xFF, 0xEF, 0xFF, 0xFD, 0xEF, 0xE7, 0xBF, + 0x9E, 0xFE, 0x7B, 0xF9, 0xEF, 0xE3, 0x18, 0xEE, 0x7F, 0xFF, 0xFE, 0x7E, 0x3F, 0x1F, 0x8F, 0xC7, + 0xE3, 0x80, 0xEE, 0x3F, 0xEF, 0xFB, 0xCE, 0xE3, 0xFD, 0xEF, 0xFB, 0xFC, 0xE6, 0x38, 0x0E, 0x00, + 0x00, 0x1F, 0x1F, 0xE7, 0xF1, 0xFC, 0x7F, 0x83, 0xF7, 0xF9, 0xFE, 0x1E, 0x00, 0x30, 0xF3, 0xF7, + 0xEF, 0xCF, 0x1E, 0x3C, 0x7C, 0x78, 0x70, 0x1F, 0xC0, 0x1F, 0xF0, 0x1F, 0xFC, 0x1F, 0xBE, 0x0F, + 0x0F, 0x87, 0x83, 0x83, 0xCF, 0xFF, 0xE7, 0xFF, 0xF3, 0xFF, 0xF9, 0xFF, 0xFE, 0xF0, 0x0F, 0xFF, + 0xE3, 0xFF, 0xF0, 0xFF, 0xF8, 0x0F, 0x00, 0x07, 0x80, 0x03, 0xFF, 0x81, 0xFF, 0xC0, 0xFF, 0xE0, + 0x00, 0x00, 0x00, 0x24, 0x00, 0x36, 0x00, 0xFB, 0x01, 0xFF, 0x81, 0xFF, 0xE1, 0xFF, 0xF1, 0xFB, + 0xFD, 0xFB, 0x7E, 0xF9, 0xBF, 0xFC, 0xD8, 0x3C, 0xCC, 0x1E, 0x6C, 0x0F, 0x36, 0x07, 0xB3, 0x03, + 0xFB, 0x1D, 0xFD, 0x9F, 0x7C, 0xDF, 0x3F, 0xFF, 0x8F, 0xFF, 0x83, 0xFF, 0x81, 0xFF, 0x80, 0xDE, + 0x00, 0x6C, 0x00, 0x24, 0x00, 0x03, 0xE0, 0x07, 0xFC, 0x0F, 0xFF, 0x07, 0xFF, 0xC7, 0xE3, 0xF7, + 0xC0, 0xFB, 0xE0, 0x3D, 0xE0, 0x00, 0xF3, 0xDE, 0x79, 0xFF, 0x3C, 0xFF, 0x1E, 0x7F, 0x8F, 0x3E, + 0x07, 0xDE, 0x79, 0xFF, 0x7C, 0xFF, 0xFE, 0x3F, 0xFE, 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, 0x00, + 0x1F, 0xFE, 0x1F, 0xFF, 0x1F, 0xFF, 0x1F, 0xFF, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, + 0x1F, 0xFC, 0x1F, 0xFE, 0x1F, 0xFE, 0x1F, 0xFE, 0x1E, 0x00, 0x1E, 0x00, 0xFF, 0xC0, 0xFF, 0xC0, + 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x03, 0xC0, 0x3F, 0xC1, 0xFF, 0x8F, 0xFE, 0x3C, 0x7D, 0xF0, + 0xF7, 0xC0, 0x3F, 0xF0, 0xFF, 0xE3, 0xFF, 0x03, 0xC0, 0x3F, 0xF8, 0xFF, 0xE0, 0xF0, 0x03, 0xC3, + 0xFE, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x1C, 0x00, 0x03, 0xC0, 0xF7, 0xFF, 0xCF, + 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0xF0, 0xFF, 0x0F, 0x0F, 0xF0, 0xF0, 0xFF, 0x1F, + 0x0F, 0xF1, 0xF0, 0xFF, 0x3F, 0x0F, 0xF3, 0xF0, 0xFF, 0x7F, 0x0F, 0xF7, 0xF0, 0xFF, 0xFF, 0x0F, + 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x3C, 0x03, 0x8F, 0xC0, 0xF9, 0xF8, 0x1F, 0x3F, 0x83, 0xE7, 0xF8, + 0x7C, 0xFF, 0x0F, 0x9F, 0xF1, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xF3, 0xDF, 0x7F, + 0xFF, 0xFF, 0xFF, 0xFE, 0xF8, 0xFF, 0x9F, 0x0F, 0xF3, 0xE0, 0xFE, 0x7C, 0x1F, 0xCF, 0x81, 0xF9, + 0xF0, 0x1F, 0x00, 0x7F, 0x80, 0x00, 0xFF, 0xF0, 0x00, 0xFF, 0xF8, 0x78, 0xFF, 0xFC, 0x78, 0xF8, + 0x7C, 0x78, 0xF8, 0x3D, 0xFE, 0xF8, 0x3F, 0xFF, 0xF8, 0x3F, 0xFF, 0xF8, 0x3D, 0xFE, 0xF8, 0xFC, + 0x78, 0xFF, 0xFC, 0x78, 0xFF, 0xF8, 0x78, 0xFF, 0xE0, 0x78, 0xF8, 0x00, 0x78, 0xF8, 0x00, 0x78, + 0xF8, 0x00, 0x78, 0xF8, 0x00, 0x7E, 0xF8, 0x00, 0x7F, 0xF8, 0x00, 0x3F, 0x00, 0x00, 0x04, 0x7F, + 0x80, 0x00, 0x0F, 0xFF, 0x00, 0x00, 0xFF, 0xF8, 0x00, 0x0F, 0xFF, 0xC0, 0x00, 0xF8, 0x7C, 0x00, + 0x0F, 0x83, 0xE3, 0xF0, 0xF8, 0x3E, 0x7F, 0xCF, 0x83, 0xEF, 0xFE, 0xF8, 0x7D, 0xF3, 0xEF, 0xFF, + 0xDF, 0x1E, 0xFF, 0xF9, 0xFC, 0x0F, 0xFF, 0x0F, 0xF8, 0xFF, 0xF0, 0x7F, 0xEF, 0x9F, 0x01, 0xFE, + 0xF8, 0xF8, 0x21, 0xFF, 0x8F, 0x9E, 0x1F, 0xF8, 0x7D, 0xFF, 0xEF, 0x83, 0xCF, 0xFE, 0xF8, 0x3E, + 0x7F, 0xC0, 0x00, 0x00, 0xE0, 0x70, 0x1E, 0x03, 0xBC, 0x0F, 0x03, 0xDE, 0x0F, 0x81, 0xEF, 0x07, + 0xE1, 0xF7, 0xC3, 0xF0, 0xF1, 0xE3, 0xF8, 0x79, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0x1E, 0x7F, + 0x9E, 0x3F, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xF0, 0xFF, 0x3F, 0xC0, 0x7F, 0x1F, + 0xE0, 0x3F, 0x87, 0xF0, 0x1F, 0xC3, 0xF0, 0x07, 0xE1, 0xF8, 0x03, 0xE0, 0x7C, 0x01, 0xF0, 0x3E, + 0x00, 0xFF, 0xF8, 0xF7, 0xFF, 0xE7, 0xFF, 0xFF, 0xBF, 0xFF, 0xFD, 0xFF, 0x81, 0xEF, 0xFD, 0xEF, + 0x7F, 0xEF, 0x7B, 0xFF, 0x7B, 0xDF, 0xFB, 0xDE, 0xFF, 0xDE, 0xF7, 0xFE, 0xF0, 0x3D, 0xF7, 0xFF, + 0xEF, 0xBF, 0xFF, 0x7D, 0xFF, 0xF0, 0x00, 0x1C, 0x01, 0xFF, 0x03, 0xFF, 0x03, 0xFF, 0x00, 0x3C, + 0x0F, 0xBC, 0x1F, 0xFC, 0x3F, 0xFC, 0x7F, 0xFC, 0x7C, 0x7C, 0x78, 0x3C, 0x78, 0x3C, 0x78, 0x3C, + 0x78, 0x3C, 0x78, 0x3C, 0x7C, 0x7C, 0x7F, 0xFC, 0x3F, 0xFC, 0x1F, 0xFC, 0x07, 0x00, 0x00, 0x00, + 0x7F, 0xFC, 0x7F, 0xFC, 0x7F, 0xFC, 0x00, 0x78, 0x03, 0xFE, 0x07, 0xFF, 0x0F, 0xFE, 0x0F, 0x8E, + 0x1F, 0x00, 0x1F, 0x00, 0x7F, 0xFC, 0xFF, 0xF8, 0x1E, 0x00, 0x7F, 0xF0, 0x7F, 0xF0, 0xFF, 0xE0, + 0x1F, 0x00, 0x1F, 0x00, 0x0F, 0xCE, 0x0F, 0xFE, 0x07, 0xFF, 0x01, 0xFE, 0x00, 0x70, 0x1E, 0x01, + 0xE1, 0xE0, 0x3E, 0x1E, 0x07, 0xC1, 0xE0, 0xFC, 0x1E, 0x1F, 0x81, 0xE3, 0xF0, 0x1E, 0x7E, 0x07, + 0xFF, 0xFC, 0xFF, 0xFF, 0xCF, 0xFF, 0xFC, 0xFF, 0xFF, 0xC1, 0xFF, 0xC0, 0x1F, 0xFE, 0x01, 0xF3, + 0xE0, 0x1E, 0x1F, 0x01, 0xE0, 0xF8, 0x1E, 0x0F, 0xC1, 0xE0, 0x7C, 0x1E, 0x03, 0xE0, 0x7F, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xF8, 0x03, 0xF8, 0x03, 0xF0, + 0x0F, 0xE0, 0x0F, 0xF8, 0x0B, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x0F, 0xE0, 0x03, 0xE0, 0x03, 0xE0, + 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, + 0x00, 0x01, 0xFF, 0xF8, 0x00, 0x00, 0x3F, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFE, 0x00, 0x00, 0x1B, + 0xC3, 0xF0, 0x00, 0x00, 0x3C, 0x1F, 0x03, 0xF0, 0x03, 0xC0, 0xF8, 0xFF, 0x80, 0x3C, 0x0F, 0x9F, + 0xFC, 0x03, 0xC0, 0xF9, 0xFF, 0xE0, 0x3C, 0x0F, 0xBE, 0x3E, 0x03, 0xC0, 0xFB, 0xC1, 0xE0, 0xFC, + 0x0F, 0xBC, 0x1F, 0x3F, 0xC0, 0xFB, 0xC1, 0xF7, 0xFC, 0x0F, 0xBC, 0x1F, 0x7F, 0xC0, 0xF3, 0xE1, + 0xE7, 0x3F, 0x1F, 0x3E, 0x3E, 0x7F, 0xFF, 0xE3, 0xFF, 0xE7, 0xFF, 0xFC, 0x3F, 0xFC, 0x3F, 0x3F, + 0x83, 0xDF, 0x80, 0xC0, 0xE0, 0x3C, 0x60, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x3C, 0x00, + 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xC0, + 0x0F, 0xFE, 0x03, 0xFF, 0xC0, 0xFF, 0xF8, 0x3E, 0x3E, 0x1F, 0xFF, 0xE7, 0xFF, 0xF8, 0xF8, 0x7C, + 0x7F, 0xFF, 0x9F, 0xFF, 0xE7, 0xFF, 0xF8, 0xFF, 0xF8, 0x3F, 0xFE, 0x0F, 0xFF, 0x03, 0xFF, 0x00, + 0xF8, 0x00, 0x3E, 0x00, 0x0F, 0x80, 0x03, 0xE0, 0x00, 0x00, 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x01, + 0xFF, 0x01, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0xFC, 0xFB, 0x9E, 0xF9, 0xCF, 0xFC, 0xE0, 0x3C, 0x70, + 0x1E, 0x3F, 0xFF, 0x1F, 0xFF, 0x8F, 0xFF, 0xE7, 0xFF, 0xF3, 0x9F, 0x7D, 0xCF, 0x3F, 0xFF, 0x8F, + 0xFF, 0x83, 0xFF, 0x80, 0xFF, 0x80, 0x0E, 0x00, 0x07, 0x00, 0x01, 0x80, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x3F, 0x80, 0x1F, 0xE0, 0x07, 0xF8, 0x01, 0xFF, 0x00, 0xFF, 0xC0, 0x3E, 0xF0, 0x0F, 0x3E, - 0x07, 0xCF, 0x81, 0xF1, 0xE0, 0x78, 0x7C, 0x3E, 0x1F, 0x0F, 0x83, 0xC3, 0xC0, 0xF9, 0xF0, 0x3E, - 0x7F, 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, 0xFC, 0x7F, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, - 0xFF, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3F, 0xF0, 0x7F, 0xE0, 0xFF, 0xC1, 0xFF, 0x83, 0xFF, - 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3F, 0xF0, 0x7F, 0xE0, 0xFF, 0xC1, 0xFF, 0x83, 0xFF, 0x07, - 0xDC, 0x07, 0x00, 0x7F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFE, 0x3E, 0x00, 0x1F, 0x00, 0x1F, - 0x80, 0x0F, 0x80, 0x07, 0xC0, 0x03, 0xE0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x03, 0xE0, 0x07, - 0xC0, 0x0F, 0x80, 0x0F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x07, 0x80, 0x07, 0x80, 0x03, 0x80, 0x03, - 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x00, - 0xE0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x00, - 0x38, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x70, 0xF1, 0xE3, - 0xF3, 0xC7, 0xE7, 0x0F, 0xEE, 0x03, 0xFC, 0x03, 0xF8, 0x07, 0xE0, 0x0F, 0xC0, 0x0F, 0x80, 0x1E, - 0x00, 0x03, 0x00, 0x60, 0x1F, 0xC3, 0xF8, 0x3F, 0xF7, 0xFC, 0x7F, 0xFF, 0xFE, 0x7D, 0xFF, 0x9E, - 0x78, 0x7E, 0x0F, 0x78, 0x3E, 0x0F, 0x78, 0xFF, 0x1F, 0x7F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFE, 0x3F, - 0xE7, 0xFC, 0x0F, 0x81, 0xF0, 0x03, 0xC1, 0xFC, 0x3F, 0x0F, 0xE1, 0xE0, 0x3C, 0x07, 0x80, 0xF0, - 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, - 0x01, 0xE0, 0x3C, 0x0F, 0x87, 0xF0, 0xFC, 0x1F, 0x00, 0x79, 0xEF, 0x9E, 0x30, 0x00, 0x00, 0x01, - 0xE7, 0xBE, 0x78, 0xC0, 0x3E, 0x1D, 0xFC, 0x77, 0xFF, 0xDF, 0xFF, 0x71, 0xF8, 0x03, 0xC3, 0xE1, - 0xDF, 0xE7, 0x7F, 0xFD, 0xEF, 0xF7, 0x1F, 0x80, 0x18, 0x00, 0xE0, 0x0F, 0x00, 0x71, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xE0, 0x0F, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1E, 0x00, - 0xF0, 0x00, 0x00, 0x08, 0x07, 0xC3, 0xFE, 0xFF, 0xFF, 0xFC, 0x7E, 0x03, 0xFF, 0x0F, 0xFF, 0x0F, - 0xF8, 0x07, 0xC0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x80, 0x07, 0xC0, 0x3F, 0xE1, - 0xFF, 0xE1, 0xFF, 0x80, 0xFC, 0x7F, 0xFF, 0xFE, 0xFF, 0x87, 0xC0, 0x20, 0x01, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, - 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x3F, 0x80, 0x3F, 0x80, 0x0F, 0xC0, 0x01, 0xF8, 0x03, 0xE0, - 0x00, 0x0F, 0x80, 0xF8, 0x00, 0x00, 0xF0, 0x1E, 0x00, 0x00, 0x0F, 0x07, 0x80, 0x1C, 0x00, 0xF0, - 0xE0, 0x07, 0x80, 0x1E, 0x3C, 0x00, 0xF0, 0x01, 0xE7, 0x00, 0x1E, 0x00, 0x3D, 0xE0, 0x03, 0xC0, - 0x03, 0xBC, 0x00, 0x78, 0x00, 0x77, 0x80, 0x0F, 0x00, 0x0F, 0xE0, 0x7F, 0xFF, 0xC1, 0xFC, 0x0F, - 0xFF, 0xF8, 0x3F, 0x81, 0xFF, 0xFF, 0x07, 0xF0, 0x3F, 0xFF, 0xE0, 0xFF, 0x00, 0x1E, 0x00, 0x1F, - 0xE0, 0x03, 0xC0, 0x03, 0x9C, 0x00, 0x78, 0x00, 0x73, 0xC0, 0x0F, 0x00, 0x1E, 0x78, 0x01, 0xE0, - 0x03, 0xC7, 0x80, 0x3C, 0x00, 0xF0, 0xF0, 0x03, 0x80, 0x3E, 0x0F, 0x00, 0x00, 0x07, 0x80, 0xF0, - 0x00, 0x01, 0xE0, 0x1F, 0x00, 0x00, 0xF8, 0x01, 0xF8, 0x00, 0x3E, 0x00, 0x1F, 0xE0, 0x3F, 0x80, - 0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, - 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, - 0x3F, 0x80, 0x3F, 0x80, 0x0F, 0xC0, 0x01, 0xF8, 0x03, 0xE0, 0x00, 0x0F, 0x80, 0xF8, 0x00, 0x00, - 0xF0, 0x1E, 0x00, 0x00, 0x0F, 0x07, 0x80, 0x00, 0x00, 0xF0, 0xE0, 0x00, 0x00, 0x1E, 0x3C, 0x00, - 0x00, 0x01, 0xE7, 0x00, 0x00, 0x00, 0x3D, 0xE0, 0x00, 0x00, 0x03, 0xBC, 0x00, 0x00, 0x00, 0x77, - 0x80, 0x00, 0x00, 0x0F, 0xE0, 0x1F, 0xFF, 0x01, 0xFC, 0x03, 0xFF, 0xE0, 0x3F, 0x80, 0x7F, 0xFC, - 0x07, 0xF0, 0x0F, 0xFF, 0x80, 0xFF, 0x00, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x03, 0x9C, 0x00, - 0x00, 0x00, 0x73, 0xC0, 0x00, 0x00, 0x1E, 0x78, 0x00, 0x00, 0x03, 0xC7, 0x80, 0x00, 0x00, 0xF0, - 0xF0, 0x00, 0x00, 0x3E, 0x0F, 0x00, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x01, 0xE0, 0x1F, 0x00, 0x00, - 0xF8, 0x01, 0xF8, 0x00, 0x3E, 0x00, 0x1F, 0xE0, 0x3F, 0x80, 0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x07, - 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, - 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x3F, 0x80, 0x3F, 0x80, 0x0F, 0xC0, - 0x01, 0xF8, 0x03, 0xE0, 0x00, 0x0F, 0x80, 0xF8, 0x00, 0x00, 0xF0, 0x1E, 0x00, 0x00, 0x0F, 0x07, - 0x80, 0x00, 0x00, 0xF0, 0xE0, 0x00, 0x00, 0x1E, 0x3C, 0x02, 0x02, 0x01, 0xE7, 0x00, 0xE0, 0xE0, - 0x3D, 0xE0, 0x3E, 0x3E, 0x03, 0xBC, 0x07, 0xEF, 0x80, 0x77, 0x80, 0x7F, 0xE0, 0x0F, 0xE0, 0x07, - 0xF8, 0x01, 0xFC, 0x00, 0x7E, 0x00, 0x3F, 0x80, 0x0F, 0xE0, 0x07, 0xF0, 0x03, 0xFE, 0x00, 0xFF, - 0x00, 0xFF, 0xE0, 0x1F, 0xE0, 0x3E, 0x7E, 0x03, 0x9C, 0x07, 0x87, 0xC0, 0x73, 0xC0, 0x60, 0x70, - 0x1E, 0x78, 0x00, 0x00, 0x03, 0xC7, 0x80, 0x00, 0x00, 0xF0, 0xF0, 0x00, 0x00, 0x3E, 0x0F, 0x00, - 0x00, 0x07, 0x80, 0xF0, 0x00, 0x01, 0xE0, 0x1F, 0x00, 0x00, 0xF8, 0x01, 0xF8, 0x00, 0x3E, 0x00, - 0x1F, 0xE0, 0x3F, 0x80, 0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0xF8, - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, - 0x7F, 0xFF, 0xF0, 0x00, 0x3F, 0x80, 0x3F, 0x80, 0x0F, 0xC0, 0x01, 0xF8, 0x03, 0xE0, 0x00, 0x0F, - 0x80, 0xF8, 0x00, 0x00, 0xF0, 0x1E, 0x00, 0x00, 0x0F, 0x07, 0x80, 0x00, 0x00, 0xF0, 0xE0, 0x00, - 0x18, 0x1E, 0x3C, 0x00, 0x07, 0x81, 0xE7, 0x00, 0x01, 0xF0, 0x3D, 0xE0, 0x00, 0x3E, 0x03, 0xBC, - 0x00, 0x0F, 0x80, 0x77, 0x80, 0x03, 0xE0, 0x0F, 0xE0, 0x00, 0xF8, 0x01, 0xFC, 0x00, 0x3E, 0x00, - 0x3F, 0x80, 0x0F, 0x80, 0x07, 0xF0, 0x03, 0xF0, 0x00, 0xFF, 0x00, 0x7C, 0x00, 0x1F, 0xE0, 0x1F, - 0x00, 0x03, 0x9C, 0x07, 0xC0, 0x00, 0x73, 0xC1, 0xF0, 0x00, 0x1E, 0x78, 0x1C, 0x00, 0x03, 0xC7, - 0x81, 0x80, 0x00, 0xF0, 0xF0, 0x00, 0x00, 0x3E, 0x0F, 0x00, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x01, - 0xE0, 0x1F, 0x00, 0x00, 0xF8, 0x01, 0xF8, 0x00, 0x3E, 0x00, 0x1F, 0xE0, 0x3F, 0x80, 0x00, 0xFF, - 0xFF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x0F, 0x9E, 0x3F, 0x7C, - 0xFE, 0xF9, 0xE1, 0xF3, 0xC1, 0xE7, 0x80, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x78, 0x7C, 0xF0, - 0xF9, 0xE1, 0xF3, 0xC3, 0xE7, 0x87, 0xCF, 0x0F, 0x9E, 0x1F, 0x3C, 0x3E, 0x78, 0x7C, 0xF0, 0xF8, - 0x0F, 0xBE, 0x0F, 0xDF, 0x0F, 0xEF, 0x87, 0x87, 0xC3, 0xC3, 0xE1, 0xE1, 0xF3, 0xFE, 0xF9, 0xFF, - 0x7C, 0xFF, 0xBE, 0x1E, 0x1F, 0x0F, 0x0F, 0x87, 0x87, 0xC3, 0xC3, 0xE1, 0xE1, 0xF0, 0xF0, 0xF8, - 0x78, 0x7C, 0x3C, 0x1F, 0x1E, 0x0F, 0x8F, 0x03, 0xC0, + 0x3F, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x7C, 0x7F, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xFD, 0xF0, 0x3E, + 0x7C, 0x07, 0xDE, 0x01, 0xFF, 0x80, 0x7C, 0x07, 0xC0, 0x3F, 0xE0, 0xFF, 0xE3, 0xFF, 0xC7, 0xC7, + 0x8F, 0x0F, 0x3F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, + 0x80, 0x1E, 0x1F, 0x3E, 0x3E, 0x7F, 0xF8, 0x7F, 0xF0, 0x7F, 0xC0, 0x3C, 0x00, 0x01, 0xC0, 0x00, + 0xE0, 0x00, 0xF8, 0x01, 0xFF, 0x01, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0xFD, 0xFB, 0x9E, 0xF9, 0xCF, + 0xFC, 0xE0, 0x3C, 0x70, 0x1E, 0x38, 0x0F, 0x1C, 0x07, 0x8E, 0x03, 0xE7, 0x1D, 0xF3, 0x9F, 0x7D, + 0xDF, 0x3F, 0xFF, 0x8F, 0xFF, 0x83, 0xFF, 0x80, 0xFF, 0x80, 0x0E, 0x00, 0x07, 0x00, 0x01, 0x80, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x7F, 0xFF, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, + 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x3F, 0xF9, 0xFF, 0xF7, 0xFF, 0x9F, 0xFE, 0x03, 0xE1, 0xFF, + 0xF7, 0xFF, 0x9F, 0xFE, 0x03, 0xE1, 0xFF, 0x87, 0xFC, 0x1F, 0xE0, 0x7E, 0x00, 0xF8, 0x01, 0xF0, + 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x3E, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xD8, 0x0F, 0xE0, 0xFF, + 0x83, 0xFC, 0x0F, 0xD8, 0x3F, 0xE0, 0x7F, 0x83, 0xFC, 0x0F, 0xC3, 0xFF, 0x0F, 0x3C, 0x7C, 0xF1, + 0xF3, 0xC7, 0x8F, 0x7E, 0x3F, 0xF0, 0xFF, 0x83, 0xFC, 0x07, 0x80, 0x03, 0xE0, 0x01, 0xF0, 0x00, + 0xF8, 0x00, 0xFE, 0x01, 0xFF, 0x81, 0xFF, 0xF0, 0xFF, 0xF8, 0xFF, 0xFE, 0x7B, 0xEF, 0x7D, 0xF7, + 0xFE, 0xFB, 0xFF, 0x7D, 0xFF, 0x3E, 0xFF, 0x9F, 0x7C, 0x1F, 0xE0, 0x1F, 0xFE, 0x0F, 0xFF, 0x87, + 0xFF, 0xE3, 0xE1, 0xF1, 0xF0, 0x7C, 0xF8, 0x3E, 0x7C, 0x1F, 0x3E, 0x0F, 0xFF, 0xFF, 0xBF, 0xFF, + 0x9F, 0xFF, 0x8F, 0xFF, 0x07, 0xFE, 0x03, 0xFF, 0x81, 0xFF, 0xC0, 0xFF, 0xE0, 0x1F, 0x00, 0x0F, + 0x80, 0x00, 0x03, 0x60, 0x03, 0xB8, 0x01, 0xFC, 0x01, 0xFF, 0x03, 0xFF, 0xC1, 0xFF, 0xF1, 0xFD, + 0xFC, 0xFE, 0xFE, 0xFF, 0x7F, 0x7F, 0xBF, 0xFF, 0xDF, 0xFF, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, + 0xF0, 0x00, 0x7C, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x0D, 0x80, 0x1B, 0x01, 0xFE, + 0x07, 0xFF, 0x8F, 0xFF, 0x9F, 0xFF, 0xBE, 0x1F, 0x7C, 0x1E, 0xF8, 0x3D, 0xF0, 0xFB, 0xFF, 0xE7, + 0xFF, 0x8F, 0xFF, 0xDF, 0x1F, 0xBE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3F, 0xF1, 0xFF, 0xFF, 0xF7, 0xFF, + 0xEF, 0xFF, 0x01, 0xB0, 0x03, 0x60, 0x06, 0xC0, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x0F, 0x80, 0x03, + 0xE0, 0x00, 0xF8, 0x00, 0x3E, 0x00, 0x0F, 0x80, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, + 0xFF, 0xFF, 0x9F, 0x00, 0x01, 0xF0, 0x00, 0x1F, 0x00, 0x01, 0xF0, 0x00, 0x1F, 0x00, 0x01, 0xE0, + 0x00, 0x18, 0x00, 0x00, 0x40, 0x00, 0x1C, 0x00, 0x07, 0xC0, 0x01, 0xFC, 0x00, 0x7F, 0xC0, 0x1F, + 0xFC, 0x07, 0xFF, 0xC1, 0xFF, 0x7C, 0x7D, 0xE7, 0xDF, 0x3C, 0x79, 0xC7, 0x86, 0x00, 0xF0, 0x00, + 0x1E, 0x00, 0x03, 0xC0, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x01, 0xE0, 0x00, 0x3C, 0x00, 0x07, 0x80, + 0x00, 0x00, 0x40, 0x00, 0x1C, 0x00, 0x07, 0xC0, 0x00, 0x7C, 0x00, 0x07, 0xC0, 0x00, 0x7C, 0x00, + 0x07, 0xC7, 0xFF, 0xFC, 0xFF, 0xFF, 0xDF, 0xFF, 0xFB, 0xFF, 0xFE, 0x00, 0x07, 0x80, 0x03, 0xE0, + 0x00, 0xF8, 0x00, 0x3E, 0x00, 0x0F, 0x80, 0x00, 0xE0, 0x00, 0x08, 0x00, 0x00, 0xE0, 0x00, 0x3C, + 0x00, 0x07, 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x03, 0xC0, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x21, + 0xE0, 0x8E, 0x3C, 0x3B, 0xE7, 0x8F, 0x3E, 0xF3, 0xE3, 0xFE, 0xF8, 0x3F, 0xFE, 0x03, 0xFF, 0x80, + 0x3F, 0xE0, 0x03, 0xF8, 0x00, 0x3E, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0xE0, 0x00, 0xF8, 0x07, 0x80, 0x0F, 0x80, 0x3E, 0x00, 0xF8, 0x00, 0xF8, 0x0F, 0x80, + 0x03, 0xE0, 0xF8, 0x00, 0x0F, 0x8F, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xDF, 0xFF, 0xFF, 0xFC, 0x7C, 0x00, 0x07, 0xC1, 0xF0, 0x00, 0x7C, 0x07, 0xC0, 0x07, 0xC0, 0x1F, + 0x00, 0x7C, 0x00, 0x7C, 0x03, 0xC0, 0x01, 0xC0, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xE0, 0x00, 0xF8, 0x00, 0xFE, 0x00, 0xFF, 0x80, 0xFF, 0xE0, 0xFF, 0xF8, 0xFB, 0xBE, 0xF9, + 0xEF, 0xF8, 0xF3, 0xD8, 0x78, 0xC0, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0xC0, + 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x0C, 0x3C, 0x6F, 0x1E, 0x7F, 0xCF, 0x7D, 0xF7, 0x7C, 0x7F, + 0xFC, 0x1F, 0xFC, 0x07, 0xFC, 0x01, 0xFC, 0x00, 0x7C, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x7F, 0xFC, + 0xFF, 0xF9, 0xFF, 0xF3, 0xFF, 0xE7, 0xE0, 0x0F, 0xE0, 0x1F, 0xE0, 0x3B, 0xE0, 0x73, 0xE0, 0xE3, + 0xE1, 0xC3, 0xE3, 0x83, 0xE7, 0x03, 0xE0, 0x03, 0x80, 0x00, 0x00, 0x3F, 0xFE, 0xFF, 0xFD, 0xFF, + 0xF9, 0xFF, 0xF0, 0x0F, 0xE0, 0x3F, 0xC0, 0xFF, 0x83, 0xEF, 0x0F, 0x9E, 0x3E, 0x3C, 0xF8, 0x7B, + 0xE0, 0xF7, 0x81, 0xE6, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xE0, 0x3B, 0xE0, 0x7B, 0xE0, 0xF3, + 0xE1, 0xE3, 0xE3, 0xC3, 0xE7, 0x83, 0xEF, 0x03, 0xFE, 0x03, 0xFC, 0x03, 0xFB, 0xFF, 0xF7, 0xFF, + 0xEF, 0xFF, 0xC0, 0x00, 0x08, 0xE0, 0x39, 0xC0, 0xFB, 0x83, 0xE7, 0x0F, 0x8E, 0x3E, 0x1C, 0xF8, + 0x3B, 0xE0, 0x7F, 0x80, 0xFE, 0x01, 0xF8, 0x03, 0xFF, 0xE7, 0xFF, 0xCF, 0xFF, 0x80, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x3F, 0x00, 0x30, 0x1F, 0xC0, 0x3C, 0x0F, 0xE0, 0x3E, 0x00, 0x78, 0x3E, 0x00, + 0x3C, 0x3E, 0x00, 0x1E, 0x3E, 0x00, 0x0F, 0x3E, 0x00, 0x0F, 0xBF, 0xFF, 0xFF, 0xBF, 0xFF, 0xFF, + 0xDF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0x81, 0xF0, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x1F, 0x00, 0x00, + 0x07, 0xC0, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0x1F, 0x80, 0x00, 0x1F, 0xC0, 0x40, 0x1F, 0xC0, 0x70, 0x0F, 0x00, 0x7C, 0x07, 0x80, 0x1F, 0x03, + 0xC0, 0x07, 0xC1, 0xE0, 0x01, 0xF0, 0xF0, 0x00, 0x7C, 0x3F, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xC7, + 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x03, 0xE0, 0x00, 0x03, 0xE0, 0x00, + 0x03, 0xE0, 0x00, 0x03, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x20, 0x00, 0x02, 0x00, 0x00, 0x30, + 0x00, 0x03, 0x80, 0x00, 0x3C, 0x00, 0x03, 0xFF, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, + 0xFF, 0x3E, 0x00, 0x78, 0xF0, 0x03, 0xC3, 0x80, 0x1E, 0x0C, 0x00, 0xF0, 0x00, 0x07, 0x80, 0x00, + 0x3C, 0x00, 0x01, 0xE0, 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x02, + 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x07, 0xCF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0xF0, 0x03, 0xE7, 0x80, 0x1E, 0x3C, 0x00, 0xE1, 0xE0, 0x06, 0x0F, 0x00, 0x00, + 0x78, 0x00, 0x03, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x08, 0xF0, 0x73, 0xC3, 0xEF, 0x1F, 0x3C, + 0xF8, 0xF7, 0xFF, 0xFF, 0xFF, 0x7F, 0xFC, 0xF8, 0x01, 0xF0, 0x03, 0xE0, 0x07, 0x00, 0x60, 0x00, + 0x07, 0x80, 0x00, 0x3C, 0x00, 0x01, 0xE0, 0x00, 0x0F, 0x00, 0x00, 0x78, 0x01, 0x83, 0xC0, 0x0E, + 0x1E, 0x00, 0x78, 0xF0, 0x03, 0xE7, 0xFF, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x01, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF8, + 0x3F, 0xFF, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0x00, 0x3C, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xF0, + 0x00, 0x3C, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xF0, 0x00, 0x3C, 0x00, 0xFF, 0xF0, 0x1F, 0xF8, + 0x03, 0xFC, 0x00, 0x7E, 0x00, 0x0F, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x03, + 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xF0, 0x00, 0x07, 0x81, 0x00, 0x3C, 0x18, 0x01, 0xE3, 0xC0, 0x0F, + 0x3E, 0x00, 0x7B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0x9F, 0x00, 0x00, + 0x78, 0x00, 0x01, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xC0, 0x03, 0xFF, + 0x80, 0x1F, 0xFF, 0x00, 0xFC, 0x7E, 0x03, 0xC0, 0x78, 0x1E, 0x00, 0xF0, 0x78, 0x03, 0xC1, 0xE0, + 0x0F, 0x37, 0xA0, 0x01, 0xFF, 0xC0, 0x07, 0xFF, 0x80, 0x0F, 0xFC, 0x00, 0x1F, 0xE0, 0x00, 0x3F, + 0x00, 0x00, 0x78, 0x00, 0x00, 0xC0, 0x00, 0x07, 0xF8, 0x00, 0x7F, 0xF8, 0x03, 0xFF, 0xF0, 0x1F, + 0xCF, 0xC0, 0x7C, 0x0F, 0x81, 0xE0, 0x1E, 0x0F, 0x00, 0x3C, 0x3C, 0x00, 0xF0, 0x00, 0x1B, 0xD8, + 0x00, 0xFF, 0xF0, 0x03, 0xFF, 0xC0, 0x07, 0xFE, 0x00, 0x0F, 0xF0, 0x00, 0x1F, 0x80, 0x00, 0x3C, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x03, 0x80, 0x00, 0xF0, 0x00, 0x3E, 0x00, 0x0F, + 0xF8, 0x03, 0xFF, 0xC0, 0x3F, 0xFC, 0x03, 0xEF, 0xC0, 0x3C, 0x78, 0x03, 0x87, 0x80, 0x30, 0xF0, + 0x00, 0x1E, 0x00, 0x01, 0xFC, 0x00, 0x3B, 0x80, 0x0F, 0x78, 0x01, 0xEF, 0x00, 0x7C, 0xF0, 0x1F, + 0x1F, 0x8F, 0xC1, 0xFF, 0xF8, 0x1F, 0xFC, 0x00, 0xFF, 0x00, 0x00, 0x40, 0x00, 0x0C, 0x00, 0x01, + 0xC0, 0x00, 0x3C, 0x00, 0x07, 0xC0, 0x07, 0xFC, 0x03, 0xFF, 0xC0, 0xFF, 0xF0, 0x3F, 0x7C, 0x07, + 0x8F, 0x01, 0xE1, 0xC0, 0x3C, 0x30, 0x07, 0x00, 0x01, 0xE0, 0x00, 0x3C, 0x00, 0x3B, 0x80, 0x0F, + 0x78, 0x01, 0xEF, 0x00, 0x7C, 0xF0, 0x1F, 0x1F, 0x8F, 0xC1, 0xFF, 0xF8, 0x1F, 0xFC, 0x00, 0xFF, + 0x00, 0x00, 0x08, 0x00, 0x03, 0x80, 0x00, 0xF8, 0x00, 0x0F, 0x80, 0x00, 0xF9, 0xFF, 0xFF, 0xBF, + 0xFF, 0xF7, 0xFF, 0xFE, 0x02, 0x0F, 0x80, 0xE3, 0xE0, 0x3C, 0xF8, 0x0F, 0x8E, 0x03, 0xE0, 0x80, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0x3E, 0x00, 0x03, 0xE0, 0x00, 0x3C, 0x00, 0x03, 0x80, + 0x00, 0x20, 0x00, 0x02, 0x00, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0x0F, 0x80, 0x03, 0xE0, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0x3E, 0x08, 0x03, 0xE3, 0x80, 0x3C, 0xF8, 0x03, 0x8F, 0x80, + 0x20, 0xF9, 0xFF, 0xFF, 0xBF, 0xFF, 0xF7, 0xFF, 0xFE, 0x00, 0x0F, 0x80, 0x03, 0xE0, 0x00, 0xF8, + 0x00, 0x0E, 0x00, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x0F, 0x80, 0x03, 0xE0, 0x00, 0xF8, + 0x00, 0x3F, 0xFF, 0xCF, 0xFF, 0xFB, 0xFF, 0xFF, 0xFC, 0x00, 0x1F, 0x00, 0x01, 0xFF, 0xFF, 0x9F, + 0xFF, 0xF1, 0xFF, 0xFE, 0x1F, 0x00, 0x01, 0xF0, 0x00, 0x1F, 0x00, 0x01, 0xE0, 0x00, 0x18, 0x00, + 0x00, 0x40, 0x00, 0x1C, 0x00, 0x07, 0xC0, 0x00, 0x7C, 0x00, 0x07, 0xC1, 0xFF, 0xFC, 0x3F, 0xFF, + 0xC7, 0xFF, 0xFC, 0x00, 0x07, 0xC0, 0x00, 0xFB, 0xFF, 0xFE, 0x7F, 0xFF, 0x8F, 0xFF, 0xE0, 0x00, + 0xF8, 0x00, 0x3E, 0x00, 0x0F, 0x80, 0x00, 0xE0, 0x00, 0x08, 0x00, 0x00, 0xC0, 0x18, 0x00, 0x0F, + 0x01, 0xE0, 0x00, 0xF8, 0x0F, 0x80, 0x0F, 0x80, 0x3E, 0x00, 0xF8, 0x00, 0xF8, 0x0F, 0xFF, 0xFF, + 0xE0, 0xFF, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFE, 0xFC, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x07, 0xDF, + 0xFF, 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFC, 0x07, 0xC0, 0x07, 0xC0, 0x1F, 0x00, + 0x7C, 0x00, 0x7C, 0x07, 0xC0, 0x01, 0xE0, 0x3C, 0x00, 0x06, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x3C, + 0x03, 0xF0, 0x3F, 0xC3, 0xFF, 0x3F, 0xFD, 0xFF, 0xC0, 0xF0, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x0F, + 0x01, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x07, 0x00, 0x3C, 0x01, + 0xE0, 0x0F, 0x03, 0xFE, 0x1F, 0xF0, 0xFF, 0x80, 0xF0, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x3F, 0xC0, + 0x78, 0x3F, 0xFD, 0xFF, 0xC7, 0xFC, 0x1F, 0xC0, 0x7C, 0x01, 0xC0, 0x04, 0x00, 0x00, 0x00, 0x03, + 0x80, 0x80, 0x0E, 0x06, 0x00, 0x38, 0x38, 0x00, 0xE1, 0xE0, 0x03, 0x8F, 0xFF, 0xFE, 0x7F, 0xFF, + 0xF9, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0x87, 0x80, 0x0E, 0x0E, 0x00, 0x38, 0x18, 0x00, 0xE0, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x70, 0x01, 0x81, 0xC0, 0x07, 0x07, 0x00, 0x1E, 0x1F, 0xFF, + 0xFC, 0x7F, 0xFF, 0xF9, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0x1C, 0x00, 0x78, 0x70, 0x01, 0xC1, 0xC0, + 0x06, 0x07, 0x00, 0x10, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x0F, + 0xE0, 0x00, 0x0F, 0xF8, 0x00, 0x0F, 0x9E, 0x00, 0x0F, 0x87, 0x80, 0x0F, 0x81, 0xE0, 0x0F, 0x80, + 0x78, 0x0F, 0x80, 0x1E, 0x0F, 0xF0, 0x7F, 0x8F, 0xF8, 0x3F, 0xE7, 0xFC, 0x1F, 0xF0, 0x1E, 0x0F, + 0x00, 0x0F, 0x07, 0x80, 0x07, 0x83, 0xC0, 0x03, 0xC1, 0xE0, 0x01, 0xFF, 0xF0, 0x00, 0xFF, 0xF8, + 0x00, 0x7F, 0xFC, 0x00, 0x00, 0x08, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x0F, 0xE0, + 0x00, 0x0F, 0xF8, 0x00, 0x0F, 0x9E, 0x00, 0x0F, 0x87, 0x80, 0x0F, 0x81, 0xE0, 0x0F, 0x80, 0x78, + 0x0F, 0x80, 0x1E, 0x0F, 0xF0, 0x7F, 0x8F, 0xF8, 0x3F, 0xE7, 0xFC, 0x1F, 0xF0, 0x1E, 0x0F, 0x00, + 0x0F, 0xFF, 0x80, 0x07, 0xFF, 0xC0, 0x03, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF8, 0x00, + 0x7F, 0xFC, 0x00, 0x3F, 0xFE, 0x00, 0x1E, 0x0F, 0x00, 0x0F, 0xFF, 0x80, 0x07, 0xFF, 0xC0, 0x03, + 0xFF, 0xE0, 0x00, 0x0F, 0x80, 0xFF, 0x83, 0xFF, 0x07, 0xFC, 0x00, 0xF8, 0x01, 0xE0, 0xFB, 0xC7, + 0xFF, 0x3F, 0xFD, 0xFF, 0xF7, 0x87, 0xDE, 0x0F, 0x78, 0x3D, 0xE0, 0xF7, 0x87, 0xDF, 0x1E, 0x3F, + 0xF8, 0xFF, 0xC1, 0xFE, 0x00, 0xE0, 0x00, 0x00, 0x1F, 0xCC, 0x7F, 0xF9, 0xFF, 0xE7, 0xFF, 0xCF, + 0x8F, 0xBE, 0x3F, 0xF8, 0xEF, 0xF3, 0x9F, 0xFE, 0x3D, 0xF8, 0xFB, 0xFF, 0xE3, 0xFF, 0xCF, 0xFF, + 0x3B, 0xF8, 0x61, 0xC0, 0x01, 0xE0, 0x00, 0xFC, 0x00, 0x3F, 0x80, 0x1F, 0xE0, 0x07, 0xF8, 0x01, + 0xFF, 0x00, 0xFF, 0xC0, 0x3E, 0xF0, 0x0F, 0x3E, 0x07, 0xCF, 0x81, 0xF1, 0xE0, 0x78, 0x7C, 0x3E, + 0x1F, 0x0F, 0x83, 0xC3, 0xC0, 0xF9, 0xF0, 0x3E, 0x7F, 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, 0xFC, 0x7F, + 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3F, + 0xF0, 0x7F, 0xE0, 0xFF, 0xC1, 0xFF, 0x83, 0xFF, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x3F, 0xF0, + 0x7F, 0xE0, 0xFF, 0xC1, 0xFF, 0x83, 0xFF, 0x07, 0xDC, 0x07, 0x00, 0x7F, 0xFE, 0xFF, 0xFF, 0xFF, + 0xFF, 0x7F, 0xFE, 0x3E, 0x00, 0x1F, 0x00, 0x1F, 0x80, 0x0F, 0x80, 0x07, 0xC0, 0x03, 0xE0, 0x01, + 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x0F, 0x80, 0x1F, 0x00, 0x3E, + 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, + 0x00, 0x07, 0x80, 0x07, 0x80, 0x03, 0x80, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0xC0, 0x01, + 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0xE0, 0x00, 0xE0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, + 0x70, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x38, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0x80, 0x07, + 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x70, 0xF1, 0xE3, 0xF3, 0xC7, 0xE7, 0x0F, 0xEE, 0x03, 0xFC, 0x03, + 0xF8, 0x07, 0xE0, 0x0F, 0xC0, 0x0F, 0x80, 0x1E, 0x00, 0x03, 0x00, 0x60, 0x1F, 0xC3, 0xF8, 0x3F, + 0xF7, 0xFC, 0x7F, 0xFF, 0xFE, 0x7D, 0xFF, 0x9E, 0x78, 0x7E, 0x0F, 0x78, 0x3E, 0x0F, 0x78, 0xFF, + 0x1F, 0x7F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFE, 0x3F, 0xE7, 0xFC, 0x0F, 0x81, 0xF0, 0x03, 0xC1, 0xFC, + 0x3F, 0x0F, 0xE1, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, + 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x0F, 0x87, 0xF0, 0xFC, 0x1F, + 0x00, 0x79, 0xEF, 0x9E, 0x30, 0x00, 0x00, 0x01, 0xE7, 0xBE, 0x78, 0xC0, 0x3E, 0x1D, 0xFC, 0x77, + 0xFF, 0xDF, 0xFF, 0x71, 0xF8, 0x03, 0xC3, 0xE1, 0xDF, 0xE7, 0x7F, 0xFD, 0xEF, 0xF7, 0x1F, 0x80, + 0x18, 0x00, 0xE0, 0x0F, 0x00, 0x71, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xE0, 0x0F, 0x07, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1E, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x07, 0xC3, 0xFE, 0xFF, + 0xFF, 0xFC, 0x7E, 0x03, 0xFF, 0x0F, 0xFF, 0x0F, 0xF8, 0x07, 0xC0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xE0, 0x80, 0x07, 0xC0, 0x3F, 0xE1, 0xFF, 0xE1, 0xFF, 0x80, 0xFC, 0x7F, 0xFF, 0xFE, + 0xFF, 0x87, 0xC0, 0x20, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x3F, 0x80, + 0x3F, 0x80, 0x0F, 0xC0, 0x01, 0xF8, 0x03, 0xE0, 0x00, 0x0F, 0x80, 0xF8, 0x00, 0x00, 0xF0, 0x1E, + 0x00, 0x00, 0x0F, 0x07, 0x80, 0x1C, 0x00, 0xF0, 0xE0, 0x07, 0x80, 0x1E, 0x3C, 0x00, 0xF0, 0x01, + 0xE7, 0x00, 0x1E, 0x00, 0x3D, 0xE0, 0x03, 0xC0, 0x03, 0xBC, 0x00, 0x78, 0x00, 0x77, 0x80, 0x0F, + 0x00, 0x0F, 0xE0, 0x7F, 0xFF, 0xC1, 0xFC, 0x0F, 0xFF, 0xF8, 0x3F, 0x81, 0xFF, 0xFF, 0x07, 0xF0, + 0x3F, 0xFF, 0xE0, 0xFF, 0x00, 0x1E, 0x00, 0x1F, 0xE0, 0x03, 0xC0, 0x03, 0x9C, 0x00, 0x78, 0x00, + 0x73, 0xC0, 0x0F, 0x00, 0x1E, 0x78, 0x01, 0xE0, 0x03, 0xC7, 0x80, 0x3C, 0x00, 0xF0, 0xF0, 0x03, + 0x80, 0x3E, 0x0F, 0x00, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x01, 0xE0, 0x1F, 0x00, 0x00, 0xF8, 0x01, + 0xF8, 0x00, 0x3E, 0x00, 0x1F, 0xE0, 0x3F, 0x80, 0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, + 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x01, + 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x3F, 0x80, 0x3F, 0x80, 0x0F, 0xC0, 0x01, 0xF8, + 0x03, 0xE0, 0x00, 0x0F, 0x80, 0xF8, 0x00, 0x00, 0xF0, 0x1E, 0x00, 0x00, 0x0F, 0x07, 0x80, 0x00, + 0x00, 0xF0, 0xE0, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x01, 0xE7, 0x00, 0x00, 0x00, 0x3D, 0xE0, + 0x00, 0x00, 0x03, 0xBC, 0x00, 0x00, 0x00, 0x77, 0x80, 0x00, 0x00, 0x0F, 0xE0, 0x1F, 0xFF, 0x01, + 0xFC, 0x03, 0xFF, 0xE0, 0x3F, 0x80, 0x7F, 0xFC, 0x07, 0xF0, 0x0F, 0xFF, 0x80, 0xFF, 0x00, 0x00, + 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x03, 0x9C, 0x00, 0x00, 0x00, 0x73, 0xC0, 0x00, 0x00, 0x1E, 0x78, + 0x00, 0x00, 0x03, 0xC7, 0x80, 0x00, 0x00, 0xF0, 0xF0, 0x00, 0x00, 0x3E, 0x0F, 0x00, 0x00, 0x07, + 0x80, 0xF0, 0x00, 0x01, 0xE0, 0x1F, 0x00, 0x00, 0xF8, 0x01, 0xF8, 0x00, 0x3E, 0x00, 0x1F, 0xE0, + 0x3F, 0x80, 0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, + 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFF, + 0xF0, 0x00, 0x3F, 0x80, 0x3F, 0x80, 0x0F, 0xC0, 0x01, 0xF8, 0x03, 0xE0, 0x00, 0x0F, 0x80, 0xF8, + 0x00, 0x00, 0xF0, 0x1E, 0x00, 0x00, 0x0F, 0x07, 0x80, 0x00, 0x00, 0xF0, 0xE0, 0x00, 0x00, 0x1E, + 0x3C, 0x02, 0x02, 0x01, 0xE7, 0x00, 0xE0, 0xE0, 0x3D, 0xE0, 0x3E, 0x3E, 0x03, 0xBC, 0x07, 0xEF, + 0x80, 0x77, 0x80, 0x7F, 0xE0, 0x0F, 0xE0, 0x07, 0xF8, 0x01, 0xFC, 0x00, 0x7E, 0x00, 0x3F, 0x80, + 0x0F, 0xE0, 0x07, 0xF0, 0x03, 0xFE, 0x00, 0xFF, 0x00, 0xFF, 0xE0, 0x1F, 0xE0, 0x3E, 0x7E, 0x03, + 0x9C, 0x07, 0x87, 0xC0, 0x73, 0xC0, 0x60, 0x70, 0x1E, 0x78, 0x00, 0x00, 0x03, 0xC7, 0x80, 0x00, + 0x00, 0xF0, 0xF0, 0x00, 0x00, 0x3E, 0x0F, 0x00, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x01, 0xE0, 0x1F, + 0x00, 0x00, 0xF8, 0x01, 0xF8, 0x00, 0x3E, 0x00, 0x1F, 0xE0, 0x3F, 0x80, 0x00, 0xFF, 0xFF, 0xE0, + 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, + 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x3F, 0x80, 0x3F, 0x80, + 0x0F, 0xC0, 0x01, 0xF8, 0x03, 0xE0, 0x00, 0x0F, 0x80, 0xF8, 0x00, 0x00, 0xF0, 0x1E, 0x00, 0x00, + 0x0F, 0x07, 0x80, 0x00, 0x00, 0xF0, 0xE0, 0x00, 0x18, 0x1E, 0x3C, 0x00, 0x07, 0x81, 0xE7, 0x00, + 0x01, 0xF0, 0x3D, 0xE0, 0x00, 0x3E, 0x03, 0xBC, 0x00, 0x0F, 0x80, 0x77, 0x80, 0x03, 0xE0, 0x0F, + 0xE0, 0x00, 0xF8, 0x01, 0xFC, 0x00, 0x3E, 0x00, 0x3F, 0x80, 0x0F, 0x80, 0x07, 0xF0, 0x03, 0xF0, + 0x00, 0xFF, 0x00, 0x7C, 0x00, 0x1F, 0xE0, 0x1F, 0x00, 0x03, 0x9C, 0x07, 0xC0, 0x00, 0x73, 0xC1, + 0xF0, 0x00, 0x1E, 0x78, 0x1C, 0x00, 0x03, 0xC7, 0x81, 0x80, 0x00, 0xF0, 0xF0, 0x00, 0x00, 0x3E, + 0x0F, 0x00, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x01, 0xE0, 0x1F, 0x00, 0x00, 0xF8, 0x01, 0xF8, 0x00, + 0x3E, 0x00, 0x1F, 0xE0, 0x3F, 0x80, 0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, + 0x3F, 0xF8, 0x00, 0x00, 0x0F, 0x9E, 0x3F, 0x7C, 0xFE, 0xF9, 0xE1, 0xF3, 0xC1, 0xE7, 0x80, 0x3F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x78, 0x7C, 0xF0, 0xF9, 0xE1, 0xF3, 0xC3, 0xE7, 0x87, 0xCF, 0x0F, + 0x9E, 0x1F, 0x3C, 0x3E, 0x78, 0x7C, 0xF0, 0xF8, 0x0F, 0xBE, 0x0F, 0xDF, 0x0F, 0xEF, 0x87, 0x87, + 0xC3, 0xC3, 0xE1, 0xE1, 0xF3, 0xFE, 0xF9, 0xFF, 0x7C, 0xFF, 0xBE, 0x1E, 0x1F, 0x0F, 0x0F, 0x87, + 0x87, 0xC3, 0xC3, 0xE1, 0xE1, 0xF0, 0xF0, 0xF8, 0x78, 0x7C, 0x3C, 0x1F, 0x1E, 0x0F, 0x8F, 0x03, + 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x9C, 0x00, 0x03, 0xCF, 0x00, 0x01, 0xE7, 0x80, 0x00, + 0xF3, 0xC0, 0x00, 0x7F, 0xE0, 0x00, 0x3F, 0xF0, 0x00, 0x1F, 0x7F, 0x00, 0xFF, 0x3F, 0xFF, 0xFF, + 0x8F, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x01, 0xDC, 0x00, + 0x00, 0xEE, 0x00, 0x00, 0x77, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x07, 0x00, 0x00, 0x03, 0x80, 0x00, + 0x01, 0x80, 0x00, 0x70, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x0E, 0x03, 0xC0, + 0x00, 0x3E, 0x0F, 0x80, 0x00, 0xFC, 0x1F, 0x00, 0x03, 0xF8, 0x1F, 0xC0, 0xFF, 0xFF, 0x3F, 0xFF, + 0xFF, 0xFF, 0x3F, 0xFF, 0xFD, 0xFE, 0x3F, 0xFF, 0xE1, 0xFC, 0x0F, 0xFC, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x60, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x1D, 0xC0, 0x00, 0x00, 0x1F, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x0E, 0x3E, 0x1E, + 0x1E, 0x1F, 0x1F, 0x0F, 0x1F, 0xFF, 0xFE, 0xFE, 0xF8, 0x00, 0x00, 0x6E, 0xFE, 0xFE, 0x7C, 0x38, + 0x38, 0x18, 0x0E, 0x00, 0x78, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x7F, 0xFB, 0xFF, 0xDF, 0xFF, 0xF3, + 0xF0, 0x00, 0x00, 0x00, 0xEE, 0x07, 0xF0, 0x3B, 0x80, 0xF8, 0x03, 0x80, 0x1C, 0x00, 0x60, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x18, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x07, 0xE0, 0x00, 0x03, 0xB0, 0x00, + 0x03, 0xF8, 0x00, 0x01, 0xF8, 0x00, 0x00, 0x00, 0x1E, 0x70, 0x00, 0x0F, 0x3C, 0x00, 0x07, 0x9E, + 0x00, 0x03, 0xCF, 0x00, 0x01, 0xFF, 0x80, 0x00, 0xFF, 0xC0, 0x00, 0x7D, 0xFC, 0x03, 0xFC, 0xFF, + 0xFF, 0xFE, 0x3F, 0xFF, 0xFE, 0x0F, 0xFF, 0xFC, 0x00, 0xFF, 0xF0, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x03, 0xB8, 0x00, 0x00, + 0x0F, 0xF0, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x01, 0xE0, 0x00, 0x0E, 0x03, 0xC0, 0x00, 0x3E, 0x0F, 0x80, 0x00, 0xFC, 0x1F, 0x00, + 0x03, 0xF8, 0x1F, 0xC0, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFD, 0xFE, 0x3F, 0xFF, + 0xE1, 0xFC, 0x0F, 0xFC, 0x00, 0x00, 0x60, 0x60, 0x7E, 0x7E, 0x77, 0xFE, 0xFC, 0x00, 0x00, 0x1C, + 0x3C, 0x3C, 0x3E, 0x3E, 0x1E, 0x1E, 0x1E, 0xFE, 0xFE, 0xFC, 0xF8, 0x00, 0x1C, 0x00, 0xE0, 0x07, + 0xE0, 0x3F, 0x81, 0xDC, 0x1F, 0xC0, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x70, 0x03, 0xC0, 0x3E, 0x01, + 0xF0, 0x0F, 0x83, 0xFF, 0xDF, 0xFE, 0xFF, 0xFF, 0x9F, 0x80, 0x00, 0x7F, 0x00, 0x7F, 0xF0, 0x1F, + 0xFF, 0x8F, 0xFF, 0xE6, 0x3F, 0xF0, 0x07, 0xFC, 0x0F, 0xFC, 0x1F, 0xF0, 0x1F, 0xC0, 0x1F, 0x80, + 0x1F, 0x80, 0x0F, 0x92, 0x07, 0x9F, 0x87, 0x87, 0xC3, 0xC1, 0x81, 0xE0, 0xE0, 0xF8, 0x20, 0x7C, + 0x00, 0x1F, 0x80, 0xCF, 0xFF, 0xF3, 0xFF, 0xF8, 0xFF, 0xFC, 0x0F, 0xF8, 0x7F, 0x00, 0x07, 0xFF, + 0x00, 0x1F, 0xFF, 0x80, 0xFF, 0xFE, 0x06, 0x3F, 0xF0, 0x00, 0x7F, 0xC0, 0x0F, 0xFC, 0x01, 0xFF, + 0xC0, 0x1F, 0xCF, 0x01, 0xF8, 0x7F, 0x9F, 0x83, 0xFE, 0xF9, 0x2F, 0xF7, 0x9F, 0xBF, 0xF8, 0x7C, + 0x03, 0xC1, 0x80, 0x1E, 0x0E, 0x00, 0xF8, 0x20, 0x07, 0xC0, 0x00, 0x1F, 0x80, 0xC0, 0xFF, 0xFF, + 0x03, 0xFF, 0xF8, 0x0F, 0xFF, 0xC0, 0x0F, 0xF8, 0x00, 0x7F, 0x00, 0x7F, 0xC0, 0x7F, 0xF0, 0x7F, + 0xFE, 0x00, 0xFE, 0x00, 0x7E, 0x03, 0xFE, 0xFF, 0xF8, 0xFF, 0xE0, 0xFF, 0x80, 0xFE, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0E, 0xE0, 0x0E, 0xE0, 0x0E, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x03, 0x80, 0x03, + 0x00, 0x7F, 0x00, 0x0F, 0xF8, 0x01, 0xFF, 0xC0, 0x3F, 0xFF, 0x00, 0x0F, 0xE0, 0x00, 0xFC, 0x00, + 0xFF, 0x87, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x19, 0x80, 0x07, 0xF8, 0x00, 0xFF, 0x00, 0x07, 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x01, 0x80, + 0x00, 0x18, 0x01, 0x80, 0x1F, 0x81, 0xF8, 0x1D, 0xC3, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x18, + 0x07, 0x80, 0x7C, 0x03, 0xE0, 0x3E, 0x01, 0xF0, 0x1F, 0x00, 0xF0, 0x0F, 0x7F, 0xF7, 0xFF, 0x7F, + 0xE7, 0xFC, 0x06, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xFC, + 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0xF8, 0x00, 0x3E, 0x00, + 0x1F, 0x00, 0x07, 0xC0, 0x03, 0xE0, 0x00, 0xF0, 0x00, 0x7C, 0x1F, 0xFF, 0xCF, 0xFF, 0xE7, 0xFF, + 0xFB, 0xFE, 0xF8, 0x18, 0x00, 0x00, 0x00, 0xF0, 0x1E, 0x03, 0xC0, 0xDC, 0x3F, 0xC7, 0xF8, 0x66, + 0x00, 0x00, 0x00, 0x04, 0x03, 0xC0, 0xF8, 0x0F, 0x81, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x1F, 0x03, + 0xE0, 0xF8, 0x7F, 0x3F, 0xC7, 0xF0, 0x78, 0x0C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x38, + 0x01, 0xDC, 0x03, 0xF8, 0x07, 0x70, 0x06, 0x40, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x3C, 0x00, + 0xF8, 0x00, 0xF8, 0x01, 0xF0, 0x01, 0xE0, 0x03, 0xFC, 0x07, 0xF8, 0x1F, 0xF8, 0x3F, 0xE0, 0xF8, + 0x07, 0xF0, 0x3F, 0xC0, 0x7F, 0x00, 0xF8, 0x00, 0xC0, 0x00, 0x0E, 0x01, 0xC0, 0x3F, 0x07, 0xF0, + 0xEE, 0x3F, 0x87, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x04, 0x03, 0xC0, 0xF8, 0x0F, 0x81, 0xF0, 0x1E, + 0x03, 0xC0, 0x78, 0x1F, 0x03, 0xE0, 0xF8, 0x7F, 0x3F, 0xC7, 0xF0, 0x78, 0x0C, 0x00, 0x0C, 0x00, + 0x18, 0x00, 0x3F, 0x00, 0x7E, 0x00, 0xEC, 0x03, 0xF8, 0x07, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x3C, 0x00, 0xF8, 0x00, 0xF8, 0x01, 0xF0, 0x01, 0xE0, 0x03, 0xFC, 0x07, 0xF8, + 0x1F, 0xF8, 0x3F, 0xE0, 0xF8, 0x07, 0xF0, 0x3F, 0xC0, 0x7F, 0x00, 0xF8, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x7C, 0x00, 0x03, 0xF8, 0x00, 0x1F, 0xF0, 0x00, 0x7F, 0x80, 0x01, 0xFC, + 0x00, 0x03, 0xE0, 0x00, 0x07, 0xE0, 0x70, 0x0F, 0xE0, 0xF0, 0x07, 0xE1, 0xE0, 0x07, 0xC3, 0xC0, + 0x07, 0xCF, 0x80, 0x07, 0x9F, 0x00, 0x0F, 0x1F, 0xC0, 0x7E, 0x3F, 0xFF, 0xFC, 0x3F, 0xFF, 0xF8, + 0x3F, 0xFF, 0xC0, 0x0F, 0xFC, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x0F, 0xE0, + 0x00, 0x0F, 0xF8, 0x00, 0x0F, 0xF8, 0x00, 0x03, 0xF8, 0x00, 0x01, 0xF8, 0x00, 0x00, 0x7E, 0x00, + 0x70, 0x0F, 0xC0, 0x1E, 0x01, 0xF8, 0x07, 0x80, 0x3F, 0x01, 0xE0, 0x07, 0xC0, 0xF8, 0x00, 0xF8, + 0x3E, 0x00, 0x1F, 0x07, 0xF0, 0x1F, 0xF9, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xC7, 0xFF, 0xFB, + 0xF0, 0x3F, 0xF8, 0x00, 0x00, 0xE0, 0x3E, 0x0F, 0xF1, 0xFF, 0x7F, 0xCF, 0xE0, 0xF8, 0x0F, 0xC0, + 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xF8, 0x07, 0xC0, 0x7C, 0xFF, 0xCF, 0xFC, 0xFF, 0x8F, 0xF0, 0xE0, + 0x00, 0x00, 0x60, 0x07, 0xC0, 0x3F, 0xC0, 0xFF, 0x87, 0xFC, 0x1F, 0xC0, 0x3E, 0x00, 0x7E, 0x00, + 0x7E, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7C, 0x00, 0x7C, 0x00, 0xFC, 0x3F, 0xFF, 0x7F, 0xFF, 0xFF, + 0xFF, 0xFE, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF0, 0x00, 0x07, 0xE0, 0x00, 0x3F, + 0x80, 0x01, 0xFC, 0xC0, 0x03, 0xE7, 0xC0, 0x03, 0x3F, 0x80, 0x01, 0xFF, 0x00, 0x07, 0xF8, 0x00, + 0x1F, 0xC0, 0x00, 0x3E, 0x00, 0x00, 0x7E, 0x07, 0x00, 0xFE, 0x0F, 0x00, 0x7E, 0x1E, 0x00, 0x7C, + 0x3C, 0x00, 0x7C, 0xF8, 0x00, 0x79, 0xF0, 0x00, 0xF1, 0xFC, 0x07, 0xE3, 0xFF, 0xFF, 0xC3, 0xFF, + 0xFF, 0x83, 0xFF, 0xFC, 0x00, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x3F, 0x80, 0x00, 0x1F, 0x80, 0x00, 0x1F, 0x8C, 0x00, 0x07, 0x8F, 0x00, 0x01, 0xCF, 0xE0, 0x00, + 0x0F, 0xF8, 0x00, 0x0F, 0xF8, 0x00, 0x03, 0xF8, 0x00, 0x01, 0xF8, 0x00, 0x00, 0x7E, 0x00, 0x70, + 0x0F, 0xC0, 0x1E, 0x01, 0xF8, 0x07, 0x80, 0x3F, 0x01, 0xE0, 0x07, 0xC0, 0xF8, 0x00, 0xF8, 0x3E, + 0x00, 0x1F, 0x07, 0xF0, 0x1F, 0xF9, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xC7, 0xFF, 0xFB, 0xF0, + 0x3F, 0xF8, 0x00, 0x00, 0xC0, 0x1E, 0x03, 0xF8, 0x7F, 0x07, 0xE7, 0x3C, 0xF9, 0x9F, 0xE1, 0xFF, + 0x3F, 0xE3, 0xF8, 0x1F, 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0xC0, 0x3F, 0x00, 0xF8, 0x03, 0xE0, 0x1F, + 0x1F, 0xF8, 0xFF, 0xC7, 0xFC, 0x3F, 0xC1, 0xC0, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x0F, 0xE0, 0x3F, + 0x80, 0x7E, 0x30, 0x79, 0xF0, 0x67, 0xF8, 0x0F, 0xF8, 0x3F, 0xE0, 0x7F, 0x00, 0x7C, 0x00, 0x7E, + 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xC0, 0x03, 0xE0, 0x03, 0xF0, 0x7F, 0xFE, 0x7F, + 0xFF, 0x7F, 0xFF, 0x7F, 0xBF, 0x70, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0xF8, 0x00, 0x3E, + 0x78, 0x07, 0x9E, 0x01, 0xE7, 0x80, 0x7F, 0xE0, 0x1F, 0xF8, 0x07, 0xFE, 0x01, 0xEF, 0x80, 0x79, + 0xF0, 0x7E, 0x7F, 0xFF, 0x0F, 0xFF, 0x81, 0xFF, 0xC0, 0x1F, 0xC0, 0x00, 0x03, 0x00, 0x00, 0x1F, + 0x00, 0x00, 0x1E, 0x03, 0xC0, 0x3C, 0x07, 0x80, 0x7C, 0x0F, 0x00, 0xFF, 0x3E, 0x01, 0xFF, 0x7C, + 0x03, 0xFE, 0xF8, 0x07, 0xFD, 0xF0, 0x0F, 0x01, 0xF0, 0x7E, 0x03, 0xFF, 0xF8, 0x03, 0xFF, 0xF0, + 0x03, 0xFF, 0xC0, 0x01, 0xFE, 0x00, 0x00, 0x0C, 0x01, 0xC0, 0x7C, 0x1F, 0xC1, 0xFC, 0x7F, 0x9F, + 0xFB, 0xCF, 0xF9, 0xFF, 0x3F, 0xFF, 0xBF, 0xF7, 0xFC, 0x7F, 0x00, 0x00, 0x0F, 0xC0, 0x1F, 0xF0, + 0x1F, 0xF8, 0x0F, 0xFE, 0x0F, 0x8F, 0x07, 0x87, 0xC3, 0xC3, 0xFD, 0xE0, 0xFF, 0xF8, 0x3F, 0xB8, + 0x0F, 0xC0, 0x00, 0x00, 0x1C, 0x78, 0xF1, 0xF3, 0xE3, 0xC7, 0x8F, 0xFF, 0xFF, 0xF7, 0xC1, 0x07, + 0x0E, 0x3C, 0x38, 0x18, 0x00, 0x78, 0x03, 0xF0, 0x77, 0xE3, 0xFF, 0xCF, 0xFF, 0xBF, 0xC7, 0xFE, + 0x0F, 0xF0, 0x1F, 0xC0, 0x1F, 0x80, 0x3F, 0x00, 0x3E, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x3C, + 0x00, 0x0F, 0xC0, 0x01, 0xFC, 0x00, 0x3F, 0xE0, 0x0F, 0xFE, 0x01, 0xEF, 0xE0, 0x3D, 0xFE, 0x07, + 0xBF, 0xC0, 0xFF, 0x7D, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0x06, 0x03, 0x80, + 0x00, 0x78, 0x00, 0x1F, 0xC0, 0x01, 0xFE, 0x00, 0x3F, 0xE0, 0x07, 0xDE, 0x00, 0x79, 0xE0, 0x07, + 0x9E, 0x00, 0x7B, 0xC0, 0x7F, 0xFF, 0xCF, 0xFF, 0xFE, 0xFF, 0xFF, 0xEF, 0xFF, 0xFE, 0x0F, 0x8F, + 0x00, 0x7C, 0xF0, 0x07, 0xFF, 0x00, 0x3F, 0xF0, 0x01, 0xFE, 0x00, 0x07, 0xC0, 0x0C, 0x00, 0x0F, + 0x00, 0x1F, 0x80, 0x1F, 0xC0, 0x3F, 0xE0, 0x3F, 0xF8, 0x79, 0xFC, 0x79, 0xFC, 0x7F, 0xFE, 0x3F, + 0xDE, 0xFF, 0xDE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFC, 0x00, 0x60, 0x03, 0xE0, 0x07, 0xF0, 0x03, + 0xFC, 0x03, 0xFE, 0x01, 0xEF, 0x01, 0xF7, 0x80, 0xF3, 0xC0, 0x7B, 0xE0, 0xFF, 0xFF, 0x7F, 0xFF, + 0xBF, 0xFF, 0xFF, 0xFF, 0xE3, 0xC7, 0xC1, 0xF3, 0xE0, 0xFF, 0xE0, 0x3F, 0xF0, 0x0F, 0xF0, 0x01, + 0xF0, 0x00, 0x01, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x1F, 0xE0, 0x00, 0x7F, 0xC0, 0x01, 0xF7, 0xC0, + 0x07, 0xC7, 0x80, 0x1F, 0x8E, 0x00, 0x3C, 0x00, 0x00, 0xF8, 0x00, 0x01, 0xFE, 0x00, 0xFD, 0xFF, + 0xFF, 0xFB, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xE0, 0x7F, 0xFC, 0x00, 0x00, 0x7E, 0x03, 0xFF, 0x8F, + 0xFF, 0xCF, 0xFF, 0xEF, 0xFF, 0xC7, 0xC0, 0x03, 0xC0, 0x01, 0xFC, 0x1C, 0xFF, 0xFE, 0x3F, 0xFF, + 0x0F, 0xFF, 0x80, 0xFF, 0x80, 0x1E, 0x00, 0x00, 0x7C, 0x00, 0x00, 0xE0, 0x00, 0x01, 0xC0, 0x00, + 0x01, 0xF0, 0x00, 0x07, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xF0, 0x00, 0x0F, + 0xF0, 0x00, 0x3F, 0xE0, 0x00, 0xFB, 0xE0, 0x03, 0xE3, 0xC0, 0x0F, 0xC7, 0x00, 0x1E, 0x00, 0x00, + 0x7C, 0x00, 0x00, 0xFF, 0x00, 0x7E, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF0, 0x3F, + 0xFE, 0x00, 0x1C, 0x00, 0x1E, 0x00, 0x0C, 0x00, 0x07, 0x40, 0x03, 0xE0, 0x03, 0xF0, 0x00, 0xE0, + 0x00, 0x00, 0xFC, 0x07, 0xFF, 0x1F, 0xFF, 0x9F, 0xFF, 0xDF, 0xFF, 0x8F, 0x80, 0x07, 0x80, 0x03, + 0xF8, 0x39, 0xFF, 0xFC, 0x7F, 0xFE, 0x1F, 0xFF, 0x01, 0xFF, 0x00, 0x00, 0x07, 0xF0, 0x00, 0x7F, + 0x80, 0x07, 0xFC, 0x00, 0x7F, 0xC0, 0x03, 0xC0, 0x00, 0x3E, 0x01, 0xE1, 0xE0, 0x0F, 0x0F, 0xC0, + 0x78, 0x3F, 0x87, 0xC0, 0xFF, 0x3E, 0x03, 0xF9, 0xF0, 0x03, 0xCF, 0x80, 0x3E, 0x3F, 0x07, 0xF1, + 0xFF, 0xFF, 0x07, 0xFF, 0xF0, 0x1F, 0xFF, 0x00, 0x3F, 0xC0, 0x00, 0x00, 0x00, 0x01, 0xE0, 0x00, + 0x07, 0x80, 0x70, 0x1E, 0x01, 0xFE, 0xF8, 0x0F, 0xFF, 0xE0, 0x3F, 0xFF, 0x80, 0x3F, 0xFE, 0x00, + 0x7C, 0x7E, 0x03, 0xE1, 0xFF, 0xFF, 0x83, 0xFF, 0xFC, 0x07, 0xFF, 0xE0, 0x07, 0xFC, 0x00, 0x0E, + 0x3E, 0x1E, 0x1E, 0x1F, 0x1F, 0x0F, 0x1F, 0xFF, 0xFE, 0xFE, 0xF8, 0x00, 0x00, 0xE7, 0xFF, 0xFF, + 0xE7, 0x0E, 0x00, 0x78, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x7F, 0xFB, 0xFF, 0xDF, 0xFF, 0xF3, 0xF0, + 0x00, 0x00, 0x00, 0xE6, 0x07, 0xF8, 0x3F, 0xC1, 0xDC, 0x00, 0x1F, 0x87, 0xF0, 0xFE, 0x3F, 0xC7, + 0x80, 0xF0, 0x1F, 0x71, 0xFF, 0x7F, 0xFF, 0xFF, 0xFE, 0x3E, 0x04, 0x00, 0x00, 0x1F, 0xE7, 0xF9, + 0xBE, 0x00, 0x00, 0x01, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1F, 0x07, 0xC1, 0xF0, 0x7C, + 0x1F, 0x07, 0xC1, 0xF0, 0x7C, 0x0F, 0x03, 0xC0, 0xF0, 0x00, 0x07, 0xF8, 0x7F, 0xC6, 0x78, 0x00, + 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x1E, 0x01, 0xF0, 0x1F, 0x01, 0xF0, 0x1F, 0x01, 0xF0, 0x1F, 0x01, + 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0xE0, 0xFF, 0x0F, 0xF0, 0x7F, 0x00, 0x00, 0x7B, + 0xEE, 0x3A, 0xFF, 0xFE, 0x00, 0x79, 0xE7, 0x9E, 0x79, 0xE7, 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xF3, + 0xCF, 0x3C, 0x78, 0x3E, 0x0E, 0x03, 0x80, 0x7C, 0x3F, 0x0E, 0x01, 0xE0, 0x78, 0x1E, 0x07, 0x81, + 0xF0, 0x7C, 0x1F, 0x07, 0xC1, 0xF0, 0x7C, 0x1F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xF8, 0xFF, + 0x3F, 0xC7, 0xF0, 0x00, 0x0F, 0x01, 0xF0, 0x1C, 0x01, 0xD0, 0x0F, 0x81, 0xF8, 0x1C, 0x00, 0x00, + 0x0F, 0x81, 0xFC, 0x3F, 0xE7, 0xFE, 0x79, 0xE7, 0x9F, 0x7F, 0xF7, 0xFF, 0x7F, 0xF3, 0xFE, 0x03, + 0xE1, 0xFE, 0xFF, 0xC7, 0xF8, 0x7E, 0x07, 0x80, 0x0F, 0x00, 0x1F, 0x00, 0x1C, 0x00, 0x1D, 0x00, + 0x0F, 0x80, 0x1F, 0x80, 0x1C, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x1F, 0xC0, 0x3F, 0xE0, 0x7F, 0xE0, + 0x79, 0xE0, 0x79, 0xF0, 0x7F, 0xFC, 0x7F, 0xFE, 0x7F, 0xFE, 0x3F, 0xFE, 0x03, 0xF8, 0x1F, 0xE0, + 0xFF, 0xC0, 0x7F, 0x80, 0x7E, 0x00, 0x78, 0x00, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0xBE, 0xFB, 0xEF, + 0xBE, 0xFB, 0xEF, 0x9E, 0x79, 0xE7, 0x80, 0x7B, 0xEE, 0x38, 0x7F, 0xFE, 0x00, 0xF0, 0x78, 0x3C, + 0x1E, 0x0F, 0x87, 0xC3, 0xE1, 0xF0, 0xF8, 0x7C, 0x3E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xFE, 0x7F, + 0xBF, 0xCF, 0xE0, 0x03, 0x83, 0xC1, 0xC0, 0xE8, 0x7C, 0x3E, 0x1C, 0x00, 0x0E, 0x00, 0x00, 0xF0, + 0x3F, 0x8E, 0x03, 0xFC, 0x3A, 0x3F, 0xE1, 0xF3, 0xFE, 0x1F, 0x9E, 0x00, 0x61, 0xF0, 0x0F, 0x0F, + 0x00, 0x78, 0x7E, 0x03, 0xC1, 0xFC, 0x3E, 0x07, 0xF9, 0xF0, 0x1F, 0xCF, 0x80, 0x1E, 0x7C, 0x01, + 0xF1, 0xF8, 0x3F, 0x8F, 0xFF, 0xF8, 0x3F, 0xFF, 0x80, 0xFF, 0xF8, 0x01, 0xFE, 0x00, 0x0E, 0x00, + 0x00, 0x78, 0x00, 0x03, 0x80, 0x00, 0x07, 0x40, 0x00, 0x1F, 0x00, 0x00, 0xFC, 0x00, 0x01, 0x80, + 0x00, 0x1E, 0x00, 0x00, 0x78, 0x07, 0x01, 0xE0, 0x1F, 0xEF, 0x80, 0xFF, 0xFE, 0x03, 0xFF, 0xF8, + 0x03, 0xFF, 0xE0, 0x07, 0xC7, 0xE0, 0x3E, 0x1F, 0xFF, 0xF8, 0x3F, 0xFF, 0xC0, 0x7F, 0xFE, 0x00, + 0x7F, 0xC0, 0x00, 0x78, 0xF3, 0x83, 0xA7, 0xDF, 0x98, 0x00, 0x1C, 0x78, 0xF1, 0xF3, 0xE3, 0xC7, + 0x8F, 0xFF, 0xFF, 0xF7, 0xC0, 0x00, 0x1E, 0x03, 0xE0, 0x38, 0x03, 0xA0, 0x3F, 0x03, 0xF0, 0x38, + 0x00, 0x00, 0x1E, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x3E, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xF7, 0xF0, + 0x00, 0xF7, 0xBD, 0xEF, 0x7B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x7B, 0xDE, 0xF0, 0x78, 0x3C, + 0x1E, 0x0F, 0x87, 0xC3, 0xE1, 0xF0, 0xF8, 0x7C, 0x3E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xFE, 0x7F, + 0xBF, 0xCF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x9C, 0x00, 0x03, 0xCF, 0x00, 0x01, + 0xE7, 0x80, 0x00, 0xF3, 0xC0, 0x00, 0x7F, 0xE0, 0x00, 0x3F, 0xF0, 0x00, 0x1F, 0x7F, 0x00, 0xFF, + 0x3F, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x0E, 0x03, 0xC0, 0x00, 0x3E, 0x0F, + 0x80, 0x00, 0xFC, 0x1F, 0x00, 0x03, 0xF8, 0x1F, 0xC0, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, + 0xFF, 0xFD, 0xFE, 0x3F, 0xFF, 0xE1, 0xFC, 0x0F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x1C, + 0x00, 0x00, 0x1C, 0x78, 0xF1, 0xF3, 0xE3, 0xC7, 0x8F, 0xFF, 0xFF, 0xF7, 0xC0, 0x00, 0x0C, 0x3C, + 0x78, 0x70, 0x1E, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x3E, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xF7, 0xF0, + 0x00, 0x00, 0x01, 0xC0, 0x3C, 0x03, 0xE0, 0x1C, 0x00, 0x00, 0x07, 0x79, 0xFF, 0x1D, 0xE1, 0x98, + 0x00, 0x03, 0x00, 0x70, 0x1F, 0x07, 0xF0, 0x7F, 0x1F, 0xE7, 0xFE, 0xF3, 0xFE, 0x7F, 0xCF, 0xFF, + 0xEF, 0xFD, 0xFF, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x1E, 0xE0, 0x1F, 0xE0, 0x1F, 0xE0, 0x0C, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x01, 0xE0, 0x01, 0xF0, 0x07, 0xF0, 0x1F, 0xF0, 0x3F, 0xF0, 0x7F, 0xF0, + 0x7D, 0xF0, 0xF8, 0xF0, 0xF8, 0xF0, 0xFF, 0xF0, 0x7F, 0xF0, 0x7F, 0xFE, 0x3F, 0xFF, 0x02, 0x7F, + 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3B, 0x80, 0x00, 0x3D, 0xE0, 0x00, 0x1E, 0xF0, + 0x00, 0x06, 0x30, 0xF3, 0x80, 0x00, 0x79, 0xE0, 0x00, 0x3C, 0xF0, 0x00, 0x1E, 0x78, 0x00, 0x0F, + 0xFC, 0x00, 0x07, 0xFE, 0x00, 0x03, 0xEF, 0xE0, 0x1F, 0xE7, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xF0, + 0x7F, 0xFF, 0xE0, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7, 0x00, 0x00, 0x01, 0xFE, + 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x03, 0x30, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x01, 0xC0, 0x78, 0x00, 0x07, 0xC1, 0xF0, 0x00, 0x1F, 0x83, 0xE0, 0x00, 0x7F, + 0x03, 0xF8, 0x1F, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xBF, 0xC7, 0xFF, 0xFC, 0x3F, + 0x81, 0xFF, 0x80, 0x00, 0x00, 0x3B, 0xBD, 0xEE, 0xF6, 0x30, 0x00, 0x38, 0x3C, 0x1E, 0x0F, 0x87, + 0xC1, 0xE0, 0xF0, 0x79, 0xFC, 0xFE, 0x7E, 0x3E, 0x00, 0x00, 0x00, 0x01, 0xDE, 0x0F, 0xF0, 0x7F, + 0x81, 0x98, 0x00, 0x00, 0x38, 0x01, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC1, 0xFF, 0xEF, 0xFF, 0x7F, + 0xFF, 0xCF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x07, 0x80, 0x00, 0x03, 0x80, + 0x00, 0x07, 0x70, 0x00, 0x03, 0xF8, 0x00, 0x01, 0xDC, 0x00, 0x00, 0x66, 0x1E, 0x70, 0x00, 0x0F, + 0x3C, 0x00, 0x07, 0x9E, 0x00, 0x03, 0xCF, 0x00, 0x01, 0xFF, 0x80, 0x00, 0xFF, 0xC0, 0x00, 0x7D, + 0xFC, 0x03, 0xFC, 0xFF, 0xFF, 0xFE, 0x3F, 0xFF, 0xFE, 0x0F, 0xFF, 0xFC, 0x00, 0xFF, 0xF0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x03, 0xB0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x19, 0x80, 0x00, 0x70, + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x0E, 0x03, 0xC0, 0x00, 0x3E, 0x0F, 0x80, + 0x00, 0xFC, 0x1F, 0x00, 0x03, 0xF8, 0x1F, 0xC0, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, + 0xFD, 0xFE, 0x3F, 0xFF, 0xE1, 0xFC, 0x0F, 0xFC, 0x00, 0x00, 0x00, 0x38, 0x3C, 0x38, 0xEE, 0xFF, + 0xFE, 0x66, 0x00, 0x1C, 0x3C, 0x3C, 0x3E, 0x3E, 0x1E, 0x1E, 0x1E, 0xFE, 0xFE, 0xFC, 0xF8, 0x00, + 0x00, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x01, 0xB8, 0x1F, 0xE0, 0xFF, 0x03, 0x30, 0x00, 0x00, 0x70, + 0x03, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x83, 0xFF, 0xDF, 0xFE, 0xFF, 0xFF, 0x9F, 0x80, 0x00, 0x7E, + 0x00, 0x7F, 0xE0, 0x1F, 0xFF, 0x8F, 0xFF, 0xE6, 0x3F, 0xF0, 0x07, 0xF8, 0x0F, 0xFC, 0x1F, 0xF0, + 0x1F, 0xC0, 0x1F, 0x80, 0x1F, 0x80, 0x0F, 0x80, 0x0F, 0x87, 0x07, 0x87, 0x83, 0xC3, 0xC1, 0xE0, + 0xE0, 0xF8, 0x00, 0x7C, 0x00, 0x1F, 0x81, 0xCF, 0xFF, 0xE3, 0xFF, 0xF8, 0xFF, 0xFC, 0x1F, 0xF0, + 0x7E, 0x00, 0x07, 0xFE, 0x00, 0x1F, 0xFF, 0x80, 0xFF, 0xFE, 0x06, 0x3F, 0xF0, 0x00, 0x7F, 0x80, + 0x0F, 0xFC, 0x01, 0xFF, 0xC0, 0x1F, 0xDE, 0x01, 0xF8, 0xFF, 0x9F, 0x83, 0xFE, 0xF8, 0x0F, 0xFF, + 0x87, 0x3F, 0x78, 0x78, 0x03, 0xC3, 0xC0, 0x1E, 0x0E, 0x00, 0xF8, 0x00, 0x07, 0xC0, 0x00, 0x1F, + 0x81, 0xC0, 0xFF, 0xFE, 0x03, 0xFF, 0xF8, 0x0F, 0xFF, 0xC0, 0x1F, 0xF0, 0x00, 0x7F, 0x00, 0x7F, + 0xC0, 0x7F, 0xF0, 0x7F, 0xFE, 0x00, 0xFE, 0x00, 0x7E, 0x03, 0xFE, 0xFF, 0xF8, 0xFF, 0xE0, 0xFF, + 0x80, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x07, 0xC0, 0x07, 0xC0, 0x03, 0x80, 0x7F, + 0x00, 0x0F, 0xF8, 0x01, 0xFF, 0xC0, 0x3F, 0xFF, 0x00, 0x0F, 0xE0, 0x00, 0xFC, 0x00, 0xFF, 0x87, + 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x01, 0xE0, 0x00, 0x3C, 0x00, 0x03, 0x80, 0x00, 0x7E, 0x00, 0x7F, 0xE0, 0x1F, 0xFF, 0x8F, 0xFF, + 0xE6, 0x3F, 0xF0, 0x07, 0xF8, 0x0F, 0xFC, 0x1F, 0xF0, 0x1F, 0xC0, 0x1F, 0x80, 0x1F, 0x80, 0x0F, + 0x80, 0x0F, 0x80, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF8, 0x00, 0x7C, 0x00, 0x1F, 0x81, + 0xCF, 0xFF, 0xE3, 0xFF, 0xF8, 0xFF, 0xFC, 0x1F, 0xF0, 0x7E, 0x00, 0x07, 0xFE, 0x00, 0x1F, 0xFF, + 0x80, 0xFF, 0xFE, 0x06, 0x3F, 0xF0, 0x00, 0x7F, 0x80, 0x0F, 0xFC, 0x01, 0xFF, 0xC0, 0x1F, 0xDE, + 0x01, 0xF8, 0xFF, 0x9F, 0x83, 0xFE, 0xF8, 0x0F, 0xFF, 0x80, 0x3F, 0x78, 0x00, 0x03, 0xC0, 0x00, + 0x1E, 0x00, 0x00, 0xF8, 0x00, 0x07, 0xC0, 0x00, 0x1F, 0x81, 0xC0, 0xFF, 0xFE, 0x03, 0xFF, 0xF8, + 0x0F, 0xFF, 0xC0, 0x1F, 0xF0, 0x00, 0x7F, 0x00, 0x7F, 0xC0, 0x7F, 0xF0, 0x7F, 0xFE, 0x00, 0xFE, + 0x00, 0x7E, 0x03, 0xFE, 0xFF, 0xF8, 0xFF, 0xE0, 0xFF, 0x80, 0xFE, 0x00, 0x00, 0x00, 0x7F, 0x00, + 0x0F, 0xF8, 0x01, 0xFF, 0xC0, 0x3F, 0xFF, 0x00, 0x0F, 0xE0, 0x00, 0xFC, 0x00, 0xFF, 0x87, 0xFF, + 0xFE, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0xE0, 0x00, + 0xF0, 0x00, 0x78, 0x00, 0x18, 0x00, 0x00, 0x01, 0xF8, 0x01, 0xFF, 0x80, 0x7F, 0xFE, 0x3F, 0xFF, + 0x98, 0xFF, 0xC0, 0x1F, 0xE0, 0x3F, 0xF0, 0x7F, 0xC0, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x3E, + 0x00, 0x3E, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0xE0, 0x01, 0xF0, 0x00, 0x7E, 0x07, + 0x3F, 0xFF, 0x8F, 0xFF, 0xE3, 0xFF, 0xF0, 0x7F, 0xC0, 0x01, 0x80, 0x00, 0x1E, 0x00, 0x00, 0xF0, + 0x00, 0x07, 0x80, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0xF8, 0x00, 0x1F, 0xF8, 0x00, 0x7F, 0xFE, + 0x03, 0xFF, 0xF8, 0x18, 0xFF, 0xC0, 0x01, 0xFE, 0x00, 0x3F, 0xF0, 0x07, 0xFF, 0x00, 0x7F, 0x78, + 0x07, 0xE3, 0xFE, 0x7E, 0x0F, 0xFB, 0xE0, 0x3F, 0xFE, 0x00, 0xFD, 0xE0, 0x00, 0x0F, 0x00, 0x00, + 0x78, 0x00, 0x03, 0xE0, 0x00, 0x1F, 0x00, 0x00, 0x7E, 0x07, 0x03, 0xFF, 0xF8, 0x0F, 0xFF, 0xE0, + 0x3F, 0xFF, 0x00, 0x7F, 0xC0, 0x00, 0x08, 0x00, 0x3C, 0x00, 0x3E, 0x00, 0x3C, 0x00, 0x18, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x7F, 0xC0, 0x7F, 0xF0, 0x7F, 0xFE, 0x00, 0xFE, 0x00, 0x7E, + 0x03, 0xFE, 0xFF, 0xF8, 0xFF, 0xE0, 0xFF, 0x80, 0xFE, 0x00, 0x00, 0x00, 0x08, 0x00, 0x07, 0x80, + 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xF8, 0x00, 0x7F, + 0xC0, 0x0F, 0xFE, 0x01, 0xFF, 0xF8, 0x00, 0x7F, 0x00, 0x07, 0xE0, 0x07, 0xFC, 0x3F, 0xFF, 0xF7, + 0xFF, 0xFF, 0xFF, 0x9F, 0xFF, 0xC1, 0xF8, 0x00, 0x00, 0x00, 0x01, 0x80, 0x78, 0x07, 0xC0, 0x3E, + 0x03, 0xE0, 0x1F, 0x01, 0xF0, 0x0F, 0x00, 0xF7, 0xFF, 0x7F, 0xF7, 0xFE, 0x7F, 0xC0, 0x60, 0x01, + 0x80, 0x03, 0xC0, 0x01, 0xF0, 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x0F, 0x80, 0x07, 0xC0, 0x01, 0xE0, + 0x00, 0xF8, 0x3F, 0xFF, 0x9F, 0xFF, 0xCF, 0xFF, 0xF7, 0xFD, 0xF0, 0x30, 0x00, 0x04, 0x00, 0xE0, + 0x1F, 0x00, 0xF0, 0x06, 0x00, 0x00, 0x01, 0x80, 0x78, 0x07, 0xC0, 0x3E, 0x03, 0xE0, 0x1F, 0x01, + 0xF0, 0x0F, 0x00, 0xF7, 0xFF, 0x7F, 0xF7, 0xFE, 0x7F, 0xC0, 0x60, 0x04, 0x00, 0x07, 0x00, 0x07, + 0xC0, 0x01, 0xE0, 0x00, 0x60, 0x00, 0x00, 0x00, 0x06, 0x00, 0x0F, 0x00, 0x07, 0xC0, 0x01, 0xF0, + 0x00, 0xF8, 0x00, 0x3E, 0x00, 0x1F, 0x00, 0x07, 0x80, 0x03, 0xE0, 0xFF, 0xFE, 0x7F, 0xFF, 0x3F, + 0xFF, 0xDF, 0xF7, 0xC0, 0xC0, 0x00, 0x01, 0x01, 0xE0, 0xF8, 0x1F, 0x07, 0xC0, 0xF0, 0x3C, 0x0F, + 0x07, 0xC1, 0xF0, 0xF8, 0xFE, 0xFF, 0x3F, 0x87, 0x81, 0x80, 0x01, 0x00, 0x0F, 0x00, 0x3E, 0x00, + 0x3E, 0x00, 0x7C, 0x00, 0x78, 0x00, 0xFF, 0x01, 0xFE, 0x07, 0xFE, 0x0F, 0xF8, 0x3E, 0x01, 0xFC, + 0x0F, 0xF0, 0x1F, 0xC0, 0x3E, 0x00, 0x30, 0x00, 0x03, 0x01, 0xE0, 0x78, 0x1E, 0x03, 0x00, 0x00, + 0x00, 0x04, 0x07, 0x83, 0xE0, 0x7C, 0x1F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0x07, 0xC3, 0xE3, 0xFB, + 0xFC, 0xFE, 0x1E, 0x06, 0x00, 0x02, 0x00, 0x0E, 0x00, 0x3E, 0x00, 0x38, 0x00, 0x70, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x80, 0x07, 0x80, 0x1F, 0x00, 0x1F, 0x00, 0x3E, 0x00, 0x3C, 0x00, 0x7F, 0x80, + 0xFF, 0x03, 0xFF, 0x07, 0xFC, 0x1F, 0x00, 0xFE, 0x07, 0xF8, 0x0F, 0xE0, 0x1F, 0x00, 0x18, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x03, 0xE0, 0x00, 0x60, + 0x61, 0xF0, 0x00, 0xF8, 0x3C, 0x78, 0x00, 0x3C, 0x3E, 0x3D, 0xE0, 0x1E, 0x1E, 0x1E, 0xF0, 0x0F, + 0x8F, 0x0F, 0x78, 0x07, 0xFF, 0xFF, 0xFC, 0x03, 0xFF, 0xFF, 0xFE, 0x01, 0xFF, 0xFF, 0xDF, 0x00, + 0xFF, 0xDF, 0xCF, 0x80, 0x78, 0x00, 0x03, 0xE0, 0xFC, 0x00, 0x01, 0xFF, 0xFC, 0x00, 0x00, 0x7F, + 0xFE, 0x00, 0x00, 0x1F, 0xFE, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x30, 0x30, 0x78, 0x00, 0x03, 0xE0, 0xF1, 0xE0, 0x00, + 0x07, 0x87, 0xC7, 0x81, 0xE0, 0x1E, 0x1E, 0x1E, 0x07, 0x80, 0x7C, 0x78, 0x7C, 0x1E, 0x01, 0xFF, + 0xFF, 0xFE, 0xF8, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0x80, 0x7F, 0xEF, 0xEF, + 0xFE, 0x01, 0xE0, 0x00, 0x00, 0x7C, 0x1F, 0x80, 0x00, 0x01, 0xFF, 0xFC, 0x00, 0x00, 0x03, 0xFF, + 0xF0, 0x00, 0x00, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, + 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF0, 0x40, 0xE1, 0xE1, 0xE1, 0xE3, 0xE3, 0xC3, 0xC7, 0xC7, 0x87, + 0x87, 0x9F, 0x1F, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0xF7, 0xBF, 0xBF, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x03, 0xC0, 0x08, 0x1C, 0x3C, 0x01, 0xE1, 0xE1, + 0xE0, 0x1E, 0x1E, 0x1E, 0x01, 0xE1, 0xE1, 0xE0, 0x3E, 0x3E, 0x3E, 0x0F, 0xFF, 0xFF, 0xFE, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xF7, 0xF3, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x01, + 0xD8, 0x00, 0x00, 0x01, 0xFE, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x33, 0x0E, 0x00, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x00, 0xC0, 0xC3, 0xE0, 0x01, 0xF0, 0x78, 0xF0, 0x00, + 0x78, 0x7C, 0x7B, 0xC0, 0x3C, 0x3C, 0x3D, 0xE0, 0x1F, 0x1E, 0x1E, 0xF0, 0x0F, 0xFF, 0xFF, 0xF8, + 0x07, 0xFF, 0xFF, 0xFC, 0x03, 0xFF, 0xFF, 0xBE, 0x01, 0xFF, 0xBF, 0x9F, 0x00, 0xF0, 0x00, 0x07, + 0xC1, 0xF8, 0x00, 0x03, 0xFF, 0xF8, 0x00, 0x00, 0xFF, 0xFC, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, + 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x00, 0x1D, 0x80, 0x00, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x30, 0x30, 0x78, 0x00, 0x03, 0xE0, 0xF1, 0xE0, 0x00, 0x07, + 0x87, 0xC7, 0x81, 0xE0, 0x1E, 0x1E, 0x1E, 0x07, 0x80, 0x7C, 0x78, 0x7C, 0x1E, 0x01, 0xFF, 0xFF, + 0xFE, 0xF8, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0x80, 0x7F, 0xEF, 0xEF, 0xFE, + 0x01, 0xE0, 0x00, 0x00, 0x7C, 0x1F, 0x80, 0x00, 0x01, 0xFF, 0xFC, 0x00, 0x00, 0x03, 0xFF, 0xF0, + 0x00, 0x00, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0x78, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x1F, 0xE0, + 0x00, 0x19, 0x8E, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x78, 0x20, 0x70, 0xF0, 0xF0, 0xF1, 0xF1, 0xE1, + 0xE3, 0xE3, 0xC3, 0xC3, 0xCF, 0x8F, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xFB, + 0xDF, 0xDF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x1E, 0x00, + 0x00, 0x01, 0xC0, 0x00, 0x00, 0x77, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x03, + 0x30, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x03, 0xC0, 0x08, 0x1C, 0x3C, 0x01, 0xE1, 0xE1, 0xE0, + 0x1E, 0x1E, 0x1E, 0x01, 0xE1, 0xE1, 0xE0, 0x3E, 0x3E, 0x3E, 0x0F, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xF7, 0xF3, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, + 0x80, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x0C, 0x07, 0xFF, 0x00, 0x07, 0xC1, 0xFF, 0xF0, 0x00, 0xF8, + 0x7E, 0x3E, 0xF0, 0x0F, 0x1F, 0x03, 0xDE, 0x01, 0xF7, 0xC0, 0xFB, 0xC0, 0x3F, 0xFF, 0xFE, 0xF8, + 0x07, 0xFF, 0xFF, 0xDF, 0x00, 0xFF, 0xFF, 0xF3, 0xE0, 0x1F, 0xFF, 0xF8, 0x7C, 0x03, 0xC3, 0xF8, + 0x07, 0xC1, 0xF8, 0x00, 0x00, 0xFF, 0xFE, 0x00, 0x00, 0x0F, 0xFF, 0xC0, 0x00, 0x00, 0xFF, 0xF0, + 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x01, 0xFF, + 0x80, 0x00, 0x0C, 0x07, 0xFF, 0x00, 0x00, 0x7C, 0x1F, 0xFF, 0x00, 0x00, 0xF8, 0x7E, 0x3E, 0x0F, + 0x00, 0xF1, 0xF0, 0x3C, 0x1E, 0x01, 0xF7, 0xC0, 0xF8, 0x3C, 0x03, 0xFF, 0xFF, 0xFC, 0xF8, 0x07, + 0xFF, 0xFF, 0xFD, 0xF0, 0x0F, 0xFF, 0xFF, 0xFB, 0xE0, 0x1F, 0xFF, 0xFF, 0xF7, 0xC0, 0x3C, 0x3F, + 0x80, 0x07, 0xC1, 0xF8, 0x00, 0x00, 0x0F, 0xFF, 0xE0, 0x00, 0x00, 0x0F, 0xFF, 0xC0, 0x00, 0x00, + 0x0F, 0xFF, 0x00, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x01, 0xF8, 0x00, 0x07, 0xFC, + 0x10, 0x0F, 0xFE, 0x1E, 0x1F, 0xFF, 0x1E, 0x3F, 0x1F, 0x3C, 0x7C, 0x1F, 0x3C, 0xF8, 0x1F, 0xFF, + 0xF1, 0xFE, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFC, 0xF7, 0xFF, 0xF0, 0x00, 0xFF, 0x80, 0x00, 0x01, + 0xF8, 0x00, 0x00, 0x7F, 0xC0, 0x10, 0x0F, 0xFE, 0x01, 0xE1, 0xFF, 0xF0, 0x1E, 0x3F, 0x1F, 0x03, + 0xC7, 0xC1, 0xF0, 0x3C, 0xF8, 0x1F, 0x0F, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, + 0xFE, 0xF7, 0xFF, 0xFF, 0xE0, 0x0F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x07, + 0x80, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0xFF, 0xC0, + 0x00, 0x60, 0x3F, 0xF8, 0x00, 0x3E, 0x0F, 0xFF, 0x80, 0x07, 0xC3, 0xF1, 0xF7, 0x80, 0x78, 0xF8, + 0x1E, 0xF0, 0x0F, 0xBE, 0x07, 0xDE, 0x01, 0xFF, 0xFF, 0xF7, 0xC0, 0x3F, 0xFF, 0xFE, 0xF8, 0x07, + 0xFF, 0xFF, 0x9F, 0x00, 0xFF, 0xFF, 0xC3, 0xE0, 0x1E, 0x1F, 0xC0, 0x3E, 0x0F, 0xC0, 0x00, 0x07, + 0xFF, 0xF0, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x3F, 0xC0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, + 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x06, 0x03, + 0xFF, 0x80, 0x00, 0x3E, 0x0F, 0xFF, 0x80, 0x00, 0x7C, 0x3F, 0x1F, 0x07, 0x80, 0x78, 0xF8, 0x1E, + 0x0F, 0x00, 0xFB, 0xE0, 0x7C, 0x1E, 0x01, 0xFF, 0xFF, 0xFE, 0x7C, 0x03, 0xFF, 0xFF, 0xFE, 0xF8, + 0x07, 0xFF, 0xFF, 0xFD, 0xF0, 0x0F, 0xFF, 0xFF, 0xFB, 0xE0, 0x1E, 0x1F, 0xC0, 0x03, 0xE0, 0xFC, + 0x00, 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, 0x07, 0xFF, 0xE0, 0x00, 0x00, 0x07, 0xFF, 0x80, 0x00, + 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xF0, 0x00, + 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xF8, 0x00, 0x07, + 0xFC, 0x10, 0x0F, 0xFE, 0x1E, 0x1F, 0xFF, 0x1E, 0x3F, 0x1F, 0x3C, 0x7C, 0x1F, 0x3C, 0xF8, 0x1F, + 0xFF, 0xF1, 0xFE, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFC, 0xF7, 0xFF, 0xF0, 0x00, 0xFF, 0x80, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x80, 0x00, 0x07, 0xFC, 0x01, 0x00, + 0xFF, 0xE0, 0x1E, 0x1F, 0xFF, 0x01, 0xE3, 0xF1, 0xF0, 0x3C, 0x7C, 0x1F, 0x03, 0xCF, 0x81, 0xF0, + 0xFF, 0xF1, 0xFF, 0xCF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xEF, 0x7F, 0xFF, 0xFE, 0x00, 0xFF, + 0x80, 0x00, 0x07, 0x00, 0x00, 0x7C, 0x00, 0x03, 0xE0, 0x00, 0x0F, 0x00, 0x00, 0x78, 0x00, 0x03, + 0xC0, 0x00, 0x1E, 0x00, 0x00, 0xF0, 0x00, 0x07, 0x9F, 0xC0, 0x3D, 0xFF, 0x01, 0xFF, 0xFC, 0x0F, + 0xFF, 0xE0, 0x7F, 0x0F, 0x03, 0xF0, 0x78, 0x1F, 0x03, 0xCF, 0xFF, 0xFE, 0xFF, 0xFF, 0xE7, 0xFF, + 0xFE, 0x3F, 0xFF, 0xE0, 0x3F, 0xF0, 0x00, 0x07, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x0F, 0x80, 0x00, + 0x07, 0x80, 0x00, 0x07, 0x80, 0x00, 0x07, 0x80, 0x00, 0x07, 0x80, 0x00, 0x07, 0x80, 0x00, 0x07, + 0x9F, 0xC0, 0x07, 0xBF, 0xE0, 0x07, 0xFF, 0xF0, 0x07, 0xFF, 0xF0, 0x07, 0xF0, 0xF0, 0x07, 0xE0, + 0xF0, 0x07, 0xC0, 0xF0, 0x7F, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x1F, 0xF8, 0x00, 0x3C, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xF0, 0x00, 0x3C, 0x00, 0x0F, 0x00, + 0x03, 0xC0, 0x00, 0xF0, 0x60, 0x3C, 0xFF, 0x0F, 0x7F, 0xE3, 0xFF, 0xF8, 0xFF, 0xFF, 0x3F, 0x87, + 0xCF, 0xC1, 0xF3, 0xE0, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xBF, 0xFF, 0x8F, 0xFF, 0x83, 0xF0, 0x00, + 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, + 0x03, 0xC0, 0x00, 0x0F, 0x06, 0x00, 0x3C, 0xFF, 0x00, 0xF7, 0xFE, 0x03, 0xFF, 0xF8, 0x0F, 0xFF, + 0xF0, 0x3F, 0x87, 0xC0, 0xFC, 0x1F, 0x03, 0xE0, 0xFC, 0x3F, 0xFF, 0xFC, 0xFF, 0xFF, 0xFB, 0xFF, + 0xFF, 0xEF, 0xFF, 0xBF, 0xBF, 0x00, 0x00, 0x07, 0x00, 0x00, 0x7C, 0x18, 0x03, 0xE1, 0xE0, 0x0F, + 0x0F, 0x00, 0x78, 0x78, 0x03, 0xC1, 0x80, 0x1E, 0x00, 0x00, 0xF0, 0x00, 0x07, 0x9F, 0xC0, 0x3D, + 0xFF, 0x01, 0xFF, 0xFC, 0x0F, 0xFF, 0xE0, 0x7F, 0x0F, 0x03, 0xF0, 0x78, 0x1F, 0x03, 0xCF, 0xFF, + 0xFE, 0xFF, 0xFF, 0xE7, 0xFF, 0xFE, 0x3F, 0xFF, 0xE0, 0x3F, 0xF0, 0x00, 0x07, 0x00, 0x00, 0x0F, + 0x82, 0x00, 0x0F, 0x8F, 0x00, 0x07, 0x8F, 0x80, 0x07, 0x8F, 0x00, 0x07, 0x86, 0x00, 0x07, 0x80, + 0x00, 0x07, 0x80, 0x00, 0x07, 0x9F, 0xC0, 0x07, 0xBF, 0xE0, 0x07, 0xFF, 0xF0, 0x07, 0xFF, 0xF0, + 0x07, 0xF0, 0xF0, 0x07, 0xE0, 0xF0, 0x07, 0xC0, 0xF0, 0x7F, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF8, 0x00, 0x3C, 0x00, 0x0F, 0x04, 0x03, 0xC3, 0x80, 0xF1, + 0xF0, 0x3C, 0x78, 0x0F, 0x0C, 0x03, 0xC0, 0x00, 0xF0, 0x60, 0x3C, 0xFF, 0x0F, 0x7F, 0xE3, 0xFF, + 0xF8, 0xFF, 0xFF, 0x3F, 0x87, 0xCF, 0xC1, 0xF3, 0xE0, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xBF, 0xFF, + 0x8F, 0xFF, 0x83, 0xF0, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x40, 0x03, 0xC3, 0x80, 0x0F, 0x1F, 0x00, + 0x3C, 0x78, 0x00, 0xF0, 0xC0, 0x03, 0xC0, 0x00, 0x0F, 0x06, 0x00, 0x3C, 0xFF, 0x00, 0xF7, 0xFE, + 0x03, 0xFF, 0xF8, 0x0F, 0xFF, 0xF0, 0x3F, 0x87, 0xC0, 0xFC, 0x1F, 0x03, 0xE0, 0xFC, 0x3F, 0xFF, + 0xFC, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0xEF, 0xFF, 0xBF, 0xBF, 0x00, 0x00, 0x0F, 0xC0, 0x1F, 0xE0, + 0x1F, 0xF0, 0x0F, 0xF8, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0xC0, 0xFF, 0xE0, 0x3F, 0xF8, 0x0F, + 0xFC, 0x0F, 0xF8, 0x07, 0xE0, 0x07, 0xC0, 0x03, 0xC0, 0x03, 0xE0, 0x01, 0xE0, 0x00, 0xF0, 0x00, + 0x78, 0x00, 0x3E, 0x00, 0x0F, 0xC0, 0xE7, 0xFF, 0xF1, 0xFF, 0xF8, 0x7F, 0xFC, 0x0F, 0xF8, 0x07, + 0xE0, 0x0F, 0xF8, 0x0F, 0xFE, 0x07, 0xFF, 0x03, 0xC7, 0x81, 0xF3, 0xC0, 0x7F, 0xE0, 0x1F, 0xE0, + 0x07, 0xFF, 0x07, 0xFF, 0x8F, 0xFF, 0xEF, 0xFF, 0xE7, 0xC0, 0x07, 0xC0, 0x03, 0xC0, 0x01, 0xE0, + 0x00, 0xF0, 0x00, 0x7E, 0x0E, 0x3F, 0xFF, 0x0F, 0xFF, 0xC3, 0xFF, 0xE0, 0x7F, 0x80, 0x03, 0xC0, + 0x7F, 0x07, 0xF8, 0x7F, 0xC3, 0xE4, 0x1E, 0x00, 0xF0, 0x07, 0x86, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xEF, 0xF8, 0x00, 0x00, 0x03, 0xF0, 0x07, 0xFC, 0x07, 0xFF, 0x07, 0xFF, 0x83, 0xE3, 0xC1, + 0xFB, 0xE0, 0x7F, 0xE0, 0x1F, 0xF0, 0x7F, 0xFF, 0x3F, 0xFF, 0xDF, 0xFF, 0xEF, 0xCF, 0xF0, 0x00, + 0x00, 0x03, 0x00, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1F, 0x80, 0x3F, 0xC0, 0x3F, 0xE0, 0x1F, 0xF0, 0x0F, 0x00, 0x07, 0x80, 0x03, 0xC1, 0x81, 0xFF, + 0xC0, 0x7F, 0xF0, 0x1F, 0xF8, 0x1F, 0xF0, 0x0F, 0xC0, 0x0F, 0x80, 0x07, 0x80, 0x07, 0xC0, 0x03, + 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x7C, 0x00, 0x1F, 0x81, 0xCF, 0xFF, 0xE3, 0xFF, 0xF0, 0xFF, + 0xF8, 0x1F, 0xF0, 0x01, 0x80, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x1F, 0x80, 0x3F, 0xE0, 0x3F, 0xF8, 0x1F, 0xFC, 0x0F, 0x1E, 0x07, 0xCF, 0x01, 0xFF, 0x80, 0x7F, + 0x80, 0x1F, 0xFC, 0x1F, 0xFE, 0x3F, 0xFF, 0xBF, 0xFF, 0x9F, 0x00, 0x1F, 0x00, 0x0F, 0x00, 0x07, + 0x80, 0x03, 0xC0, 0x01, 0xF8, 0x38, 0xFF, 0xFC, 0x3F, 0xFF, 0x0F, 0xFF, 0x81, 0xFE, 0x00, 0x03, + 0x00, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x30, 0x00, 0x00, 0x0F, 0x01, 0xFC, 0x1F, 0xE1, 0xFF, 0x0F, + 0x90, 0x78, 0x03, 0xC0, 0x1E, 0x18, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0xE0, 0x00, 0x00, + 0x00, 0x80, 0x00, 0xE0, 0x00, 0xF8, 0x00, 0x78, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0xC0, 0x1F, + 0xF0, 0x1F, 0xFC, 0x1F, 0xFE, 0x0F, 0x8F, 0x07, 0xEF, 0x81, 0xFF, 0x80, 0x7F, 0xC1, 0xFF, 0xFC, + 0xFF, 0xFF, 0x7F, 0xFF, 0xBF, 0x3F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x1F, 0x00, 0x00, 0x07, 0x80, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, + 0x01, 0xFC, 0x00, 0x01, 0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0xFB, 0xEE, 0x00, 0x78, 0xF7, 0x80, + 0x3E, 0x7B, 0xC0, 0x1F, 0xFD, 0xE0, 0x0F, 0xFF, 0xF0, 0x03, 0xFF, 0xF8, 0x00, 0x7F, 0xBF, 0x80, + 0x3F, 0x9F, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xC0, 0x1F, 0xFF, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x3F, 0x81, + 0xC0, 0x01, 0xFF, 0x07, 0x80, 0x0F, 0xFC, 0x1E, 0x00, 0x3C, 0xF8, 0x78, 0x00, 0xF1, 0xE3, 0xE0, + 0x03, 0xCF, 0x8F, 0x80, 0x0F, 0xFC, 0x1F, 0xC0, 0x7F, 0xFE, 0x7F, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, + 0xFF, 0xF1, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xE0, 0x00, 0x02, 0x00, 0x70, 0x0F, 0x80, 0x78, 0x03, + 0x00, 0x00, 0x07, 0x81, 0xFC, 0x3F, 0xE3, 0xFE, 0x7D, 0xF7, 0x8F, 0x7C, 0xF7, 0xFF, 0x7F, 0xF3, + 0xFF, 0x0F, 0xFF, 0xFE, 0xFF, 0xEF, 0xFC, 0xFF, 0x08, 0x00, 0x03, 0x00, 0x07, 0x80, 0x07, 0x80, + 0x07, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xC0, 0x1F, 0xE0, 0x3F, 0xE0, 0x3F, 0xF0, + 0x7C, 0xF0, 0x78, 0xF0, 0x7D, 0xF0, 0x3F, 0xF0, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xDE, 0x00, 0x1F, 0xE0, 0x01, 0xFE, 0x00, 0x0C, 0xC0, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x1F, 0xE0, 0x03, 0xFE, 0x00, 0x3F, 0xF7, 0x87, 0xCF, + 0x78, 0x7C, 0xF7, 0x87, 0xFF, 0xF8, 0x3F, 0xFF, 0x83, 0xFF, 0xF8, 0x1F, 0xFF, 0x80, 0x1F, 0x7E, + 0x07, 0xE7, 0xFF, 0xFC, 0x3F, 0xFF, 0xC1, 0xFF, 0xF0, 0x07, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1D, 0xE0, 0x00, 0x3F, 0xE0, 0x00, 0x1D, 0xE0, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x07, + 0x80, 0x00, 0x1F, 0xC0, 0x00, 0x3F, 0xE0, 0x00, 0x3F, 0xF0, 0x00, 0x3D, 0xF0, 0x70, 0x78, 0xF0, + 0x78, 0x7F, 0xFE, 0x78, 0x3F, 0xFF, 0xF8, 0x3F, 0xFF, 0xF8, 0x1F, 0xFE, 0xF8, 0x01, 0xF8, 0xF8, + 0x03, 0xE0, 0x7E, 0x0F, 0xE0, 0x7F, 0xFF, 0xC0, 0x3F, 0xFF, 0x80, 0x1F, 0xFF, 0x00, 0x07, 0xFC, + 0x00, 0x00, 0x01, 0xDC, 0x3D, 0xE1, 0xDE, 0x18, 0xC0, 0x00, 0x07, 0x81, 0xFC, 0x3F, 0xE3, 0xFE, + 0x7D, 0xF7, 0x8F, 0x7C, 0xF7, 0xFF, 0x7F, 0xF3, 0xFF, 0x0F, 0xFF, 0xFE, 0xFF, 0xEF, 0xFC, 0xFF, + 0x08, 0x00, 0x00, 0x00, 0x1D, 0xE0, 0x1F, 0xE0, 0x1F, 0xE0, 0x0C, 0xC0, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0xC0, 0x1F, 0xE0, 0x3F, 0xE0, 0x3F, 0xF0, 0x7C, 0xF0, 0x78, 0xF0, 0x7D, 0xF0, 0x3F, 0xF0, + 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x78, + 0x00, 0x00, 0xF0, 0x00, 0x01, 0xE0, 0x00, 0xC3, 0xC0, 0x07, 0xC7, 0x80, 0x0F, 0x8F, 0x00, 0x3C, + 0x1E, 0x00, 0x3C, 0x3C, 0xE0, 0x38, 0x79, 0xE0, 0xF8, 0xF3, 0xC7, 0xE1, 0xE7, 0x87, 0x83, 0xDF, + 0x0C, 0x07, 0xBE, 0x00, 0x0F, 0x3F, 0x80, 0xFE, 0x7F, 0xFF, 0xFC, 0x7F, 0xFF, 0xF0, 0x7F, 0xFF, + 0xC0, 0x1F, 0xFC, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0xF0, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0xC3, 0xC0, 0x00, 0x7C, 0x78, 0x00, 0x0F, 0x8F, 0x00, 0x03, 0xC1, 0xE0, 0x00, + 0x3C, 0x3C, 0x0E, 0x03, 0x87, 0x81, 0xE0, 0xF8, 0xF0, 0x3C, 0x7E, 0x1E, 0x07, 0x87, 0x83, 0xC1, + 0xF0, 0xC0, 0x7C, 0x3E, 0x00, 0x0F, 0x83, 0xF8, 0x0F, 0xFE, 0x7F, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, + 0xFC, 0x7F, 0xFF, 0xDF, 0x01, 0xFF, 0xC0, 0x00, 0x00, 0xE0, 0x3E, 0x0F, 0xF1, 0xFF, 0x7F, 0xCF, + 0xE0, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xF8, 0x07, 0xC0, 0x7C, 0xFF, 0xCF, 0xFC, + 0xFF, 0x8F, 0xF0, 0xE0, 0x00, 0x00, 0x60, 0x07, 0xC0, 0x3F, 0xC0, 0xFF, 0x87, 0xFC, 0x1F, 0xC0, + 0x3E, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7C, 0x00, 0x7C, 0x00, 0xFC, 0x3F, + 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0x80, 0x00, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, + 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, + 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0xDE, 0x01, 0xF7, 0x80, 0x7D, 0xE0, 0x1F, 0xF8, 0x07, 0xBE, + 0x01, 0xEF, 0x80, 0x7B, 0xE0, 0x3E, 0x7C, 0x1F, 0x1F, 0xFF, 0xC3, 0xFF, 0xE0, 0x7F, 0xF0, 0x07, + 0xF0, 0x00, 0x00, 0x07, 0x80, 0x00, 0x1E, 0x00, 0x00, 0x78, 0x00, 0x01, 0xE0, 0x00, 0x07, 0x80, + 0x00, 0x1E, 0x00, 0x00, 0x78, 0x00, 0x01, 0xE0, 0x00, 0x07, 0x80, 0x00, 0x1E, 0x00, 0x00, 0x78, + 0x00, 0x01, 0xE0, 0x00, 0x07, 0x81, 0xE0, 0x1F, 0x07, 0x80, 0x7C, 0x1E, 0x01, 0xFE, 0xF8, 0x07, + 0xFF, 0xE0, 0x1F, 0xFF, 0x80, 0x7F, 0xFE, 0x01, 0xE0, 0x7C, 0x1F, 0x81, 0xFF, 0xFC, 0x03, 0xFF, + 0xF0, 0x07, 0xFF, 0x80, 0x07, 0xF8, 0x00, 0x3C, 0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x78, + 0xF1, 0xE3, 0xC7, 0xCF, 0x7E, 0xFD, 0xFB, 0xE0, 0x00, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, + 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xE0, 0x7C, 0x0F, 0x87, 0xFE, 0xFF, + 0xFF, 0xFF, 0xDF, 0x80, 0x00, 0x00, 0x00, 0x7E, 0x07, 0xF8, 0x7F, 0xE7, 0xFF, 0x9C, 0x7C, 0x21, + 0xE0, 0xFF, 0x1F, 0xFB, 0xFF, 0xDF, 0xFF, 0xF8, 0x6F, 0x80, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, + 0x07, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x7F, 0x00, 0x3F, 0xE0, + 0x0F, 0xFE, 0x03, 0xFB, 0xC0, 0x7C, 0x78, 0x1F, 0xC7, 0x83, 0xFF, 0xFE, 0xFF, 0xFF, 0xFE, 0xFF, + 0xFF, 0xCF, 0xFF, 0xF8, 0x78, 0x0F, 0x00, 0x01, 0xE0, 0x00, 0x3E, 0x00, 0x07, 0xC0, 0x00, 0x78, + 0x00, 0x0F, 0x00, 0x01, 0xE0, 0x00, 0x3E, 0x00, 0x03, 0x80, 0x00, 0x01, 0xC0, 0x1F, 0x80, 0xFF, + 0x07, 0xFE, 0x1F, 0xF8, 0xF9, 0xF3, 0xC7, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xEF, 0x7F, 0x00, 0x00, + 0x01, 0xC0, 0x01, 0xF8, 0x00, 0xFF, 0x00, 0x7F, 0xE0, 0x1E, 0xF8, 0x0F, 0x1F, 0x07, 0xE7, 0xC3, + 0xFD, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFC, 0x07, 0x80, 0x00, 0xC0, 0x00, 0x78, 0x00, + 0x1E, 0x00, 0x07, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x3E, 0x00, 0x0F, 0x9E, + 0x01, 0xE7, 0x80, 0x79, 0xE0, 0x1F, 0xF8, 0x07, 0xFE, 0x01, 0xFF, 0x80, 0x7B, 0xE0, 0x1E, 0x7C, + 0x1F, 0x9F, 0xFF, 0xC3, 0xFF, 0xE0, 0x7F, 0xF0, 0x07, 0xF0, 0x00, 0x00, 0xC0, 0x00, 0x03, 0xC0, + 0x00, 0x07, 0x80, 0x00, 0x0F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x07, 0xC0, 0x00, 0x07, 0x80, 0xF0, 0x0F, 0x01, 0xE0, 0x1F, 0x03, 0xC0, 0x3F, 0xCF, 0x80, 0x7F, + 0xDF, 0x00, 0xFF, 0xBE, 0x01, 0xFF, 0x7C, 0x03, 0xC0, 0x7C, 0x1F, 0x80, 0xFF, 0xFE, 0x00, 0xFF, + 0xFC, 0x00, 0xFF, 0xF0, 0x00, 0x7F, 0x80, 0x00, 0x10, 0x71, 0xF1, 0xE1, 0x80, 0x07, 0x1E, 0x3C, + 0x7C, 0xF8, 0xF1, 0xE3, 0xFF, 0xFF, 0xFD, 0xF0, 0x00, 0x0C, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x0C, + 0x00, 0x00, 0x00, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x1E, 0x03, 0xE0, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, + 0x7F, 0x00, 0x00, 0x0C, 0x01, 0xC0, 0x7C, 0x1F, 0xC1, 0xFC, 0x7F, 0x9F, 0xFB, 0xCF, 0xF9, 0xFF, + 0x3F, 0xFF, 0xBF, 0xF7, 0xFC, 0x7F, 0x00, 0x00, 0x01, 0xE0, 0x01, 0xF0, 0x07, 0xF0, 0x1F, 0xF0, + 0x3F, 0xF0, 0x7F, 0xF0, 0x7D, 0xF0, 0xF8, 0xF0, 0xF8, 0xF0, 0xFF, 0xF0, 0x7F, 0xF0, 0x7F, 0xFE, + 0x3F, 0xFF, 0x02, 0x7F, 0x00, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x0F, 0x00, 0x1F, 0x80, 0x1F, 0xC0, + 0x3F, 0xE0, 0x3F, 0xF8, 0x79, 0xFC, 0x79, 0xFC, 0x7F, 0xFE, 0x3F, 0xDE, 0xFF, 0xDE, 0xFF, 0xFE, + 0xFF, 0xFE, 0xFF, 0xFC, 0x00, 0x60, 0x03, 0xE0, 0x07, 0xF0, 0x03, 0xFC, 0x03, 0xFE, 0x01, 0xEF, + 0x01, 0xF7, 0x80, 0xF3, 0xC0, 0x7B, 0xE0, 0xFF, 0xFF, 0x7F, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xE3, + 0xC7, 0xC1, 0xF3, 0xE0, 0xFF, 0xE0, 0x3F, 0xF0, 0x0F, 0xF0, 0x01, 0xF0, 0x00, 0x00, 0x00, 0xF8, + 0x1F, 0xC3, 0xFE, 0x7F, 0xE7, 0x9E, 0x79, 0xF7, 0xFF, 0x7F, 0xF7, 0xFF, 0x3F, 0xE0, 0x3E, 0x1F, + 0xEF, 0xFC, 0x7F, 0x87, 0xE0, 0x78, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x1F, 0xC0, 0x3F, 0xE0, 0x7F, + 0xE0, 0x79, 0xE0, 0x79, 0xF0, 0x7F, 0xFC, 0x7F, 0xFE, 0x7F, 0xFE, 0x3F, 0xFE, 0x03, 0xF8, 0x1F, + 0xE0, 0xFF, 0xC0, 0x7F, 0x80, 0x7E, 0x00, 0x78, 0x00, 0x00, 0x07, 0xF0, 0x00, 0x7F, 0x80, 0x07, + 0xFC, 0x00, 0x7F, 0xC0, 0x03, 0xC0, 0x00, 0x3E, 0x01, 0xE1, 0xE0, 0x0F, 0x0F, 0xC0, 0x78, 0x3F, + 0x87, 0xC0, 0xFF, 0x3E, 0x03, 0xF9, 0xF0, 0x03, 0xCF, 0x80, 0x3E, 0x3F, 0x07, 0xF1, 0xFF, 0xFF, + 0x07, 0xFF, 0xF0, 0x1F, 0xFF, 0x00, 0x3F, 0xC0, 0x00, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x07, 0x80, + 0x70, 0x1E, 0x01, 0xFE, 0xF8, 0x0F, 0xFF, 0xE0, 0x3F, 0xFF, 0x80, 0x3F, 0xFE, 0x00, 0x7C, 0x7E, + 0x03, 0xE1, 0xFF, 0xFF, 0x83, 0xFF, 0xFC, 0x07, 0xFF, 0xE0, 0x07, 0xFC, 0x00, 0x00, 0x07, 0xF0, + 0x00, 0x7F, 0x80, 0x07, 0xFC, 0x00, 0x7F, 0xC0, 0x03, 0xC0, 0x00, 0x3E, 0x01, 0xE1, 0xE0, 0x0F, + 0x0F, 0xC0, 0x78, 0x3F, 0x87, 0xC0, 0xFF, 0x3E, 0x03, 0xF9, 0xF0, 0x03, 0xCF, 0x80, 0x3E, 0x3F, + 0x07, 0xF1, 0xFF, 0xFF, 0x07, 0xFF, 0xF0, 0x1F, 0xFF, 0x00, 0x3F, 0xC0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x73, 0x80, 0x03, 0xFC, 0x00, 0x1F, 0xE0, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x01, 0xE0, + 0x00, 0x07, 0x80, 0x70, 0x1E, 0x01, 0xFE, 0xF8, 0x0F, 0xFF, 0xE0, 0x3F, 0xFF, 0x80, 0x3F, 0xFE, + 0x00, 0x7C, 0x7E, 0x03, 0xE1, 0xFF, 0xFF, 0x83, 0xFF, 0xFC, 0x07, 0xFF, 0xE0, 0x07, 0xFC, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEE, 0x00, 0x07, 0xBC, 0x00, 0x1E, 0xF0, 0x00, 0x3B, 0x80, + 0x00, 0x0E, 0x3E, 0x1E, 0x1E, 0x1F, 0x1F, 0x0F, 0x1F, 0xFF, 0xFE, 0xFE, 0xF8, 0x00, 0x00, 0xE7, + 0xFF, 0xFF, 0xE7, 0x0E, 0x00, 0x78, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x7F, 0xFB, 0xFF, 0xDF, 0xFF, + 0xF3, 0xF0, 0x00, 0x00, 0x00, 0xE6, 0x07, 0xF8, 0x3F, 0xC1, 0xDC, 0x00, 0x00, 0x00, 0x3F, 0xC0, + 0x3F, 0xE1, 0x8D, 0xF3, 0xC0, 0x01, 0xE0, 0x00, 0xF8, 0x38, 0x7C, 0x3E, 0x3E, 0x1F, 0xDF, 0x07, + 0xFF, 0x00, 0xFF, 0x80, 0x3F, 0xC0, 0x0F, 0xE0, 0x03, 0xF0, 0x03, 0xFC, 0x03, 0xFE, 0x03, 0xE7, + 0x83, 0xFF, 0xC1, 0xFF, 0xE0, 0xFF, 0xE0, 0x7F, 0xE0, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x7F, 0xFC, + 0x06, 0xFB, 0xC0, 0x00, 0x3E, 0x00, 0x03, 0xE0, 0x30, 0x3E, 0x07, 0x83, 0xE0, 0x7C, 0x3E, 0x03, + 0xE3, 0xE0, 0x1F, 0x3E, 0x00, 0xFB, 0xE0, 0x07, 0xFE, 0x00, 0x7F, 0xE0, 0x03, 0xFE, 0x00, 0x1F, + 0xE0, 0x01, 0xFE, 0x07, 0xFF, 0xFC, 0x7F, 0xFF, 0xE7, 0xFD, 0xFE, 0x7F, 0x8F, 0xE0, 0x00, 0x00, + 0x78, 0x01, 0xF0, 0x03, 0x80, 0x07, 0x40, 0x0F, 0xC1, 0x9F, 0x8F, 0x38, 0x1E, 0x00, 0x3E, 0x38, + 0x7C, 0xF8, 0xF9, 0xFD, 0xF1, 0xFF, 0xC0, 0xFF, 0x80, 0xFF, 0x00, 0xFE, 0x00, 0xFC, 0x03, 0xFC, + 0x0F, 0xF8, 0x3E, 0x78, 0xFF, 0xF1, 0xFF, 0xE3, 0xFF, 0x87, 0xFE, 0x00, 0x3C, 0x00, 0x0F, 0x80, + 0x01, 0xC0, 0x00, 0x3A, 0x00, 0x03, 0xE7, 0x80, 0xFC, 0xF0, 0x1C, 0x1F, 0x00, 0x03, 0xE0, 0x60, + 0x7C, 0x1E, 0x0F, 0x83, 0xE1, 0xF0, 0x3E, 0x3E, 0x03, 0xE7, 0xC0, 0x3E, 0xF8, 0x03, 0xFF, 0x00, + 0x7F, 0xE0, 0x07, 0xFC, 0x00, 0x7F, 0x80, 0x0F, 0xF0, 0x7F, 0xFF, 0xCF, 0xFF, 0xFD, 0xFF, 0x7F, + 0xBF, 0xC7, 0xF0, 0x00, 0x00, 0x00, 0x30, 0x03, 0xC0, 0x0F, 0x00, 0x3E, 0x70, 0xFB, 0xE3, 0xEF, + 0xEF, 0x9F, 0xFC, 0x1F, 0xF0, 0x3F, 0xC0, 0x7F, 0x00, 0xFC, 0x07, 0xF8, 0x3F, 0xE1, 0xF3, 0xCF, + 0xFF, 0x3F, 0xFC, 0xFF, 0xE3, 0xFF, 0x00, 0x00, 0x1C, 0x00, 0xF0, 0x03, 0x80, 0x0E, 0x80, 0x3E, + 0x00, 0xF8, 0x03, 0x80, 0x00, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x01, 0xF0, 0x00, 0x3E, 0x06, 0x07, + 0xC1, 0xE0, 0xF8, 0x3E, 0x1F, 0x03, 0xE3, 0xE0, 0x3E, 0x7C, 0x03, 0xEF, 0x80, 0x3F, 0xF0, 0x07, + 0xFE, 0x00, 0x7F, 0xC0, 0x07, 0xF8, 0x00, 0xFF, 0x07, 0xFF, 0xFC, 0xFF, 0xFF, 0xDF, 0xF7, 0xFB, + 0xFC, 0x7F, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x78, 0x00, 0x0E, 0x00, 0x01, 0xD0, 0x00, 0x3E, 0x00, + 0x07, 0xC0, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x30, 0x03, 0xC0, 0x0F, 0x00, 0x3E, 0x70, 0xFB, 0xE3, + 0xEF, 0xEF, 0x9F, 0xFC, 0x1F, 0xF0, 0x3F, 0xC0, 0x7F, 0x00, 0xFC, 0x07, 0xF8, 0x3F, 0xE1, 0xF3, + 0xCF, 0xFF, 0x3F, 0xFC, 0xFF, 0xE3, 0xFF, 0x00, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x01, 0xF0, 0x00, + 0x3E, 0x06, 0x07, 0xC1, 0xE0, 0xF8, 0x3E, 0x1F, 0x03, 0xE3, 0xE0, 0x3E, 0x7C, 0x03, 0xEF, 0x80, + 0x3F, 0xF0, 0x07, 0xFE, 0x00, 0x7F, 0xC0, 0x07, 0xF8, 0x00, 0xFF, 0x07, 0xFF, 0xFC, 0xFF, 0xFF, + 0xDF, 0xF7, 0xFB, 0xFC, 0x7F, 0x00, 0x00, 0x00, }; static const EpdGlyph inter_12_boldGlyphs[] = { @@ -2823,656 +3336,851 @@ static const EpdGlyph inter_12_boldGlyphs[] = { { 12, 6, 0, -5, 20, 9, 15738 }, // U+030B { 11, 4, 0, 0, 19, 6, 15747 }, // U+030C { 11, 4, 0, 1, 19, 6, 15753 }, // U+030F - { 5, 9, 0, 5, 13, 6, 15759 }, // U+0313 - { 5, 9, 0, 5, 13, 6, 15765 }, // U+0315 - { 5, 7, 0, -6, 19, 5, 15771 }, // U+031B - { 5, 4, 113, 1, -2, 3, 15776 }, // U+0323 - { 4, 6, 0, -10, -1, 3, 15779 }, // U+0326 - { 6, 7, 0, 2, 1, 6, 15782 }, // U+0327 - { 7, 7, 0, 0, 1, 7, 15788 }, // U+0328 - { 11, 4, 0, 0, -2, 6, 15795 }, // U+032C - { 12, 17, 0, -14, 15, 26, 15801 }, // U+0337 - { 15, 20, 0, -15, 19, 38, 15827 }, // U+0338 - { 10, 4, 0, -13, 19, 5, 15865 }, // U+0342 - { 5, 5, 0, 0, 15, 4, 15870 }, // U+0343 - { 10, 6, 0, -13, 20, 8, 15874 }, // U+0346 - { 9, 7, 0, -12, -2, 8, 15882 }, // U+0347 - { 7, 7, 0, -11, 0, 7, 15890 }, // U+0348 - { 6, 6, 0, -10, -1, 5, 15897 }, // U+0349 - { 11, 6, 0, -14, 20, 9, 15902 }, // U+034A - { 10, 11, 0, -13, 25, 14, 15911 }, // U+034B - { 10, 7, 0, -13, 22, 9, 15925 }, // U+034C - { 12, 6, 0, -14, -1, 9, 15934 }, // U+034D - { 6, 7, 0, -11, 0, 6, 15943 }, // U+034E - { 0, 0, 0, 0, 0, 0, 15949 }, // U+034F - { 10, 10, 0, -12, 25, 13, 15949 }, // U+0350 - { 4, 6, 0, -10, 19, 3, 15962 }, // U+0351 - { 10, 8, 0, -13, 22, 10, 15965 }, // U+0352 - { 7, 7, 0, -11, 0, 7, 15975 }, // U+0353 - { 7, 7, 0, -11, 0, 7, 15982 }, // U+0354 - { 7, 7, 0, -11, 0, 7, 15989 }, // U+0355 - { 15, 7, 0, -14, 0, 14, 15996 }, // U+0356 - { 4, 7, 0, 0, 21, 4, 16010 }, // U+0357 - { 5, 4, 0, 1, 19, 3, 16014 }, // U+0358 - { 6, 7, 0, -11, 0, 6, 16017 }, // U+0359 - { 11, 6, 0, -13, 0, 9, 16023 }, // U+035A - { 6, 7, 0, -11, 21, 6, 16032 }, // U+035B - { 20, 5, 0, -11, -1, 13, 16038 }, // U+035C - { 19, 5, 0, -10, 19, 12, 16051 }, // U+035D - { 15, 3, 0, -15, 19, 6, 16063 }, // U+035E - { 15, 3, 0, -15, -2, 6, 16069 }, // U+035F - { 26, 5, 0, -14, 19, 17, 16075 }, // U+0360 - { 19, 6, 0, -10, 19, 15, 16092 }, // U+0361 - { 22, 6, 0, -11, -1, 17, 16107 }, // U+0362 - { 7, 8, 0, -11, 22, 7, 16124 }, // U+0363 - { 7, 8, 0, -11, 22, 7, 16131 }, // U+0364 - { 3, 11, 0, -9, 25, 5, 16138 }, // U+0365 - { 8, 8, 0, -12, 22, 8, 16143 }, // U+0366 - { 7, 8, 0, -11, 22, 7, 16151 }, // U+0367 - { 7, 8, 0, -11, 22, 7, 16158 }, // U+0368 - { 8, 11, 0, -12, 25, 11, 16165 }, // U+0369 - { 7, 11, 0, -11, 25, 10, 16176 }, // U+036A - { 12, 8, 0, -14, 22, 12, 16186 }, // U+036B - { 5, 8, 0, -10, 22, 5, 16198 }, // U+036C - { 5, 10, 0, 0, 24, 7, 16203 }, // U+036D - { 8, 8, 0, -12, 22, 8, 16210 }, // U+036E - { 7, 8, 0, -11, 22, 7, 16218 }, // U+036F - { 13, 24, 243, 1, 24, 39, 16225 }, // U+0400 - { 13, 24, 243, 1, 24, 39, 16264 }, // U+0401 - { 21, 21, 341, 0, 19, 56, 16303 }, // U+0402 - { 13, 24, 230, 1, 24, 39, 16359 }, // U+0403 - { 17, 20, 295, 1, 19, 43, 16398 }, // U+0404 - { 15, 20, 262, 1, 19, 38, 16441 }, // U+0405 - { 5, 19, 110, 1, 19, 12, 16479 }, // U+0406 - { 11, 24, 110, -2, 24, 33, 16491 }, // U+0407 - { 13, 20, 232, 0, 19, 33, 16524 }, // U+0408 - { 27, 19, 444, 0, 19, 65, 16557 }, // U+0409 - { 27, 19, 448, 1, 19, 65, 16622 }, // U+040A - { 21, 19, 355, 0, 19, 50, 16687 }, // U+040B - { 17, 24, 285, 1, 24, 51, 16737 }, // U+040C - { 17, 24, 302, 1, 24, 51, 16788 }, // U+040D - { 17, 24, 274, 0, 24, 51, 16839 }, // U+040E - { 16, 23, 289, 1, 19, 46, 16890 }, // U+040F - { 18, 19, 296, 0, 19, 43, 16936 }, // U+0410 - { 15, 19, 257, 1, 19, 36, 16979 }, // U+0411 - { 15, 19, 263, 1, 19, 36, 17015 }, // U+0412 - { 13, 19, 230, 1, 19, 31, 17051 }, // U+0413 - { 21, 23, 342, 0, 19, 61, 17082 }, // U+0414 - { 13, 19, 243, 1, 19, 31, 17143 }, // U+0415 - { 26, 19, 422, 0, 19, 62, 17174 }, // U+0416 - { 14, 20, 256, 1, 19, 35, 17236 }, // U+0417 - { 17, 19, 302, 1, 19, 41, 17271 }, // U+0418 - { 17, 24, 302, 1, 24, 51, 17312 }, // U+0419 - { 17, 19, 285, 1, 19, 41, 17363 }, // U+041A - { 18, 19, 297, 0, 19, 43, 17404 }, // U+041B - { 21, 19, 369, 1, 19, 50, 17447 }, // U+041C - { 16, 19, 296, 1, 19, 38, 17497 }, // U+041D - { 18, 20, 306, 1, 19, 45, 17535 }, // U+041E - { 16, 19, 296, 1, 19, 38, 17580 }, // U+041F - { 15, 19, 258, 1, 19, 36, 17618 }, // U+0420 - { 17, 20, 295, 1, 19, 43, 17654 }, // U+0421 - { 16, 19, 264, 0, 19, 38, 17697 }, // U+0422 - { 17, 19, 274, 0, 19, 41, 17735 }, // U+0423 - { 19, 22, 333, 1, 20, 53, 17776 }, // U+0424 - { 18, 19, 292, 0, 19, 43, 17829 }, // U+0425 - { 18, 23, 305, 1, 19, 52, 17872 }, // U+0426 - { 15, 19, 281, 1, 19, 36, 17924 }, // U+0427 - { 22, 19, 393, 1, 19, 53, 17960 }, // U+0428 - { 24, 23, 404, 1, 19, 69, 18013 }, // U+0429 - { 20, 19, 330, 0, 19, 48, 18082 }, // U+042A - { 21, 19, 363, 1, 19, 50, 18130 }, // U+042B - { 15, 19, 263, 1, 19, 36, 18180 }, // U+042C - { 18, 20, 295, 0, 19, 45, 18216 }, // U+042D - { 24, 20, 415, 1, 19, 60, 18261 }, // U+042E - { 15, 19, 262, 0, 19, 36, 18321 }, // U+042F - { 13, 15, 230, 0, 14, 25, 18357 }, // U+0430 - { 15, 20, 241, 0, 19, 38, 18382 }, // U+0431 - { 13, 14, 227, 1, 14, 23, 18420 }, // U+0432 - { 10, 14, 180, 1, 14, 18, 18443 }, // U+0433 - { 16, 18, 263, 0, 14, 36, 18461 }, // U+0434 - { 14, 15, 235, 0, 14, 27, 18497 }, // U+0435 - { 21, 14, 343, 0, 14, 37, 18524 }, // U+0436 - { 13, 15, 205, 0, 14, 25, 18561 }, // U+0437 - { 13, 14, 247, 1, 14, 23, 18586 }, // U+0438 - { 13, 19, 247, 1, 19, 31, 18609 }, // U+0439 - { 14, 14, 231, 1, 14, 25, 18640 }, // U+043A - { 14, 14, 240, 0, 14, 25, 18665 }, // U+043B - { 18, 14, 322, 1, 14, 32, 18690 }, // U+043C - { 13, 14, 246, 1, 14, 23, 18722 }, // U+043D - { 15, 15, 242, 0, 14, 29, 18745 }, // U+043E - { 13, 14, 243, 1, 14, 23, 18774 }, // U+043F - { 14, 20, 249, 1, 14, 35, 18797 }, // U+0440 - { 14, 15, 232, 0, 14, 27, 18832 }, // U+0441 - { 13, 14, 204, 0, 14, 23, 18859 }, // U+0442 - { 15, 20, 237, 0, 14, 38, 18882 }, // U+0443 - { 19, 25, 308, 0, 19, 60, 18920 }, // U+0444 - { 14, 14, 228, 0, 14, 25, 18980 }, // U+0445 - { 15, 18, 261, 1, 14, 34, 19005 }, // U+0446 - { 13, 14, 238, 1, 14, 23, 19039 }, // U+0447 - { 19, 14, 342, 1, 14, 34, 19062 }, // U+0448 - { 21, 18, 353, 1, 14, 48, 19096 }, // U+0449 - { 16, 14, 268, 0, 14, 28, 19144 }, // U+044A - { 18, 14, 317, 1, 14, 32, 19172 }, // U+044B - { 13, 14, 231, 1, 14, 23, 19204 }, // U+044C - { 14, 15, 233, 0, 14, 27, 19227 }, // U+044D - { 20, 15, 344, 1, 14, 38, 19254 }, // U+044E - { 13, 14, 229, 0, 14, 23, 19292 }, // U+044F - { 14, 20, 235, 0, 19, 35, 19315 }, // U+0450 - { 14, 20, 235, 0, 19, 35, 19350 }, // U+0451 - { 15, 25, 246, -1, 19, 47, 19385 }, // U+0452 - { 10, 19, 180, 1, 19, 24, 19432 }, // U+0453 - { 14, 15, 233, 0, 14, 27, 19456 }, // U+0454 - { 13, 15, 221, 0, 14, 25, 19483 }, // U+0455 - { 5, 19, 106, 1, 19, 12, 19508 }, // U+0456 - { 10, 19, 106, -2, 19, 24, 19520 }, // U+0457 - { 7, 25, 106, -1, 19, 22, 19544 }, // U+0458 - { 23, 14, 374, 0, 14, 41, 19566 }, // U+0459 - { 21, 14, 366, 1, 14, 37, 19607 }, // U+045A - { 15, 19, 246, -1, 19, 36, 19644 }, // U+045B - { 14, 19, 231, 1, 19, 34, 19680 }, // U+045C - { 13, 19, 247, 1, 19, 31, 19714 }, // U+045D - { 15, 25, 237, 0, 19, 47, 19745 }, // U+045E - { 13, 18, 243, 1, 14, 30, 19792 }, // U+045F - { 22, 20, 389, 1, 19, 55, 19822 }, // U+0460 - { 20, 15, 350, 1, 14, 38, 19877 }, // U+0461 - { 17, 19, 283, 0, 19, 41, 19915 }, // U+0462 - { 15, 19, 254, 0, 19, 36, 19956 }, // U+0463 - { 23, 20, 398, 1, 19, 58, 19992 }, // U+0464 - { 19, 15, 329, 1, 14, 36, 20050 }, // U+0465 - { 18, 19, 296, 0, 19, 43, 20086 }, // U+0466 - { 15, 14, 235, 0, 14, 27, 20129 }, // U+0467 - { 24, 19, 395, 1, 19, 57, 20156 }, // U+0468 - { 20, 14, 331, 1, 14, 35, 20213 }, // U+0469 - { 20, 19, 351, 1, 19, 48, 20248 }, // U+046A - { 17, 14, 298, 1, 14, 30, 20296 }, // U+046B - { 27, 19, 471, 1, 19, 65, 20326 }, // U+046C - { 24, 14, 406, 1, 14, 42, 20391 }, // U+046D - { 12, 30, 226, 1, 24, 45, 20433 }, // U+046E - { 12, 25, 215, 0, 19, 38, 20478 }, // U+046F - { 20, 19, 356, 1, 19, 48, 20516 }, // U+0470 - { 19, 23, 330, 1, 17, 55, 20564 }, // U+0471 - { 18, 20, 306, 1, 19, 45, 20619 }, // U+0472 - { 15, 15, 242, 0, 14, 29, 20664 }, // U+0473 - { 19, 19, 300, 0, 19, 46, 20693 }, // U+0474 - { 15, 14, 244, 0, 14, 27, 20739 }, // U+0475 - { 19, 24, 300, 0, 24, 57, 20766 }, // U+0476 - { 15, 19, 244, 0, 19, 36, 20823 }, // U+0477 - { 33, 25, 543, 1, 19, 104, 20859 }, // U+0478 - { 30, 20, 472, 0, 14, 75, 20963 }, // U+0479 - { 16, 19, 286, 1, 19, 38, 21038 }, // U+0480 - { 14, 19, 232, 0, 14, 34, 21076 }, // U+0481 - { 14, 17, 265, 1, 17, 30, 21110 }, // U+0482 - { 10, 5, 0, -13, 19, 7, 21140 }, // U+0483 - { 11, 4, 0, -13, 19, 6, 21147 }, // U+0484 - { 4, 6, 0, -9, 21, 3, 21153 }, // U+0485 - { 5, 6, 0, -9, 21, 4, 21156 }, // U+0486 - { 12, 4, 0, -14, 19, 6, 21160 }, // U+0487 - { 25, 22, 0, -20, 18, 69, 21166 }, // U+0488 - { 24, 25, 0, -19, 19, 75, 21235 }, // U+0489 - { 19, 28, 302, 1, 24, 67, 21310 }, // U+048A - { 16, 23, 247, 1, 19, 46, 21377 }, // U+048B - { 17, 19, 283, 0, 19, 41, 21423 }, // U+048C - { 15, 17, 254, 0, 17, 32, 21464 }, // U+048D - { 15, 19, 261, 1, 19, 36, 21496 }, // U+048E - { 14, 20, 243, 1, 14, 35, 21532 }, // U+048F - { 13, 22, 230, 1, 22, 36, 21567 }, // U+0490 - { 10, 17, 180, 1, 17, 22, 21603 }, // U+0491 - { 15, 19, 230, -1, 19, 36, 21625 }, // U+0492 - { 12, 14, 180, -1, 14, 21, 21661 }, // U+0493 - { 15, 21, 264, 1, 19, 40, 21682 }, // U+0494 - { 13, 19, 227, 1, 14, 31, 21722 }, // U+0495 - { 28, 23, 436, 0, 19, 81, 21753 }, // U+0496 - { 23, 18, 356, 0, 14, 52, 21834 }, // U+0497 - { 14, 25, 256, 1, 19, 44, 21886 }, // U+0498 - { 13, 20, 205, 0, 14, 33, 21930 }, // U+0499 - { 18, 23, 302, 1, 19, 52, 21963 }, // U+049A - { 15, 18, 245, 1, 14, 34, 22015 }, // U+049B - { 17, 19, 285, 1, 19, 41, 22049 }, // U+049C - { 15, 14, 251, 1, 14, 27, 22090 }, // U+049D - { 22, 19, 351, 0, 19, 53, 22117 }, // U+04A0 - { 17, 14, 267, 0, 14, 30, 22170 }, // U+04A1 - { 18, 23, 307, 1, 19, 52, 22200 }, // U+04A2 - { 15, 18, 257, 1, 14, 34, 22252 }, // U+04A3 - { 24, 19, 399, 1, 19, 57, 22286 }, // U+04A4 - { 17, 14, 284, 1, 14, 30, 22343 }, // U+04A5 - { 27, 21, 448, 1, 19, 71, 22373 }, // U+04A6 - { 22, 18, 375, 1, 14, 50, 22444 }, // U+04A7 - { 25, 20, 417, 1, 19, 63, 22494 }, // U+04A8 - { 21, 15, 334, 0, 14, 40, 22557 }, // U+04A9 - { 17, 25, 295, 1, 19, 54, 22597 }, // U+04AA - { 14, 20, 232, 0, 14, 35, 22651 }, // U+04AB - { 16, 23, 264, 0, 19, 46, 22686 }, // U+04AC - { 13, 18, 204, 0, 14, 30, 22732 }, // U+04AD - { 18, 19, 289, 0, 19, 43, 22762 }, // U+04AE - { 15, 20, 236, 0, 14, 38, 22805 }, // U+04AF - { 18, 19, 289, 0, 19, 43, 22843 }, // U+04B0 - { 15, 20, 236, 0, 14, 38, 22886 }, // U+04B1 - { 19, 23, 306, 0, 19, 55, 22924 }, // U+04B2 - { 16, 18, 242, 0, 14, 36, 22979 }, // U+04B3 - { 24, 23, 379, 0, 19, 69, 23015 }, // U+04B4 - { 18, 18, 283, 0, 14, 41, 23084 }, // U+04B5 - { 17, 23, 292, 1, 19, 49, 23125 }, // U+04B6 - { 15, 18, 249, 1, 14, 34, 23174 }, // U+04B7 - { 15, 19, 281, 1, 19, 36, 23208 }, // U+04B8 - { 13, 14, 238, 1, 14, 23, 23244 }, // U+04B9 - { 16, 19, 281, 1, 19, 38, 23267 }, // U+04BA - { 13, 19, 246, 1, 19, 31, 23305 }, // U+04BB - { 22, 20, 355, 0, 19, 55, 23336 }, // U+04BC - { 18, 15, 299, 0, 14, 34, 23391 }, // U+04BD - { 22, 23, 355, 0, 19, 64, 23425 }, // U+04BE - { 18, 20, 299, 0, 14, 45, 23489 }, // U+04BF - { 5, 19, 110, 1, 19, 12, 23534 }, // U+04C0 - { 26, 24, 422, 0, 24, 78, 23546 }, // U+04C1 - { 21, 19, 343, 0, 19, 50, 23624 }, // U+04C2 - { 16, 24, 276, 1, 19, 48, 23674 }, // U+04C3 - { 14, 18, 246, 1, 14, 32, 23722 }, // U+04C4 - { 20, 23, 297, 0, 19, 58, 23754 }, // U+04C5 - { 16, 18, 240, 0, 14, 36, 23812 }, // U+04C6 - { 16, 25, 296, 1, 19, 50, 23848 }, // U+04C7 - { 13, 20, 246, 1, 14, 33, 23898 }, // U+04C8 - { 19, 23, 296, 1, 19, 55, 23931 }, // U+04C9 - { 16, 18, 246, 1, 14, 36, 23986 }, // U+04CA - { 17, 23, 292, 1, 19, 49, 24022 }, // U+04CB - { 15, 18, 249, 1, 14, 34, 24071 }, // U+04CC - { 23, 23, 369, 1, 19, 67, 24105 }, // U+04CD - { 20, 18, 322, 1, 14, 45, 24172 }, // U+04CE - { 5, 19, 106, 1, 19, 12, 24217 }, // U+04CF - { 18, 24, 296, 0, 24, 54, 24229 }, // U+04D0 - { 13, 20, 230, 0, 19, 33, 24283 }, // U+04D1 - { 18, 24, 296, 0, 24, 54, 24316 }, // U+04D2 - { 13, 20, 230, 0, 19, 33, 24370 }, // U+04D3 - { 25, 19, 409, 0, 19, 60, 24403 }, // U+04D4 - { 22, 15, 362, 0, 14, 42, 24463 }, // U+04D5 - { 13, 24, 243, 1, 24, 39, 24505 }, // U+04D6 - { 14, 20, 235, 0, 19, 35, 24544 }, // U+04D7 - { 17, 20, 298, 1, 19, 43, 24579 }, // U+04D8 - { 14, 15, 235, 0, 14, 27, 24622 }, // U+04D9 - { 17, 25, 298, 1, 24, 54, 24649 }, // U+04DA - { 14, 20, 235, 0, 19, 35, 24703 }, // U+04DB - { 26, 24, 422, 0, 24, 78, 24738 }, // U+04DC - { 21, 19, 343, 0, 19, 50, 24816 }, // U+04DD - { 14, 25, 256, 1, 24, 44, 24866 }, // U+04DE - { 13, 20, 205, 0, 19, 33, 24910 }, // U+04DF - { 14, 20, 256, 1, 19, 35, 24943 }, // U+04E0 - { 14, 20, 256, 1, 14, 35, 24978 }, // U+04E1 - { 17, 24, 302, 1, 24, 51, 25013 }, // U+04E2 - { 13, 19, 247, 1, 19, 31, 25064 }, // U+04E3 - { 17, 24, 302, 1, 24, 51, 25095 }, // U+04E4 - { 13, 19, 247, 1, 19, 31, 25146 }, // U+04E5 - { 18, 25, 306, 1, 24, 57, 25177 }, // U+04E6 - { 15, 20, 242, 0, 19, 38, 25234 }, // U+04E7 - { 18, 20, 306, 1, 19, 45, 25272 }, // U+04E8 - { 15, 15, 242, 0, 14, 29, 25317 }, // U+04E9 - { 18, 25, 306, 1, 24, 57, 25346 }, // U+04EA - { 15, 20, 242, 0, 19, 38, 25403 }, // U+04EB - { 18, 25, 295, 0, 24, 57, 25441 }, // U+04EC - { 14, 20, 233, 0, 19, 35, 25498 }, // U+04ED - { 17, 24, 274, 0, 24, 51, 25533 }, // U+04EE - { 15, 25, 237, 0, 19, 47, 25584 }, // U+04EF - { 17, 24, 274, 0, 24, 51, 25631 }, // U+04F0 - { 15, 25, 237, 0, 19, 47, 25682 }, // U+04F1 - { 17, 24, 274, 0, 24, 51, 25729 }, // U+04F2 - { 15, 25, 237, 0, 19, 47, 25780 }, // U+04F3 - { 15, 24, 281, 1, 24, 45, 25827 }, // U+04F4 - { 13, 19, 238, 1, 19, 31, 25872 }, // U+04F5 - { 13, 23, 230, 1, 19, 38, 25903 }, // U+04F6 - { 10, 18, 180, 1, 14, 23, 25941 }, // U+04F7 - { 21, 24, 363, 1, 24, 63, 25964 }, // U+04F8 - { 18, 19, 317, 1, 19, 43, 26027 }, // U+04F9 - { 15, 25, 246, 0, 19, 47, 26070 }, // U+04FA - { 12, 20, 180, -1, 14, 30, 26117 }, // U+04FB - { 19, 25, 313, 0, 19, 60, 26147 }, // U+04FC - { 13, 20, 214, 0, 14, 33, 26207 }, // U+04FD - { 18, 19, 292, 0, 19, 43, 26240 }, // U+04FE - { 14, 14, 228, 0, 14, 25, 26283 }, // U+04FF - { 15, 15, 272, 1, 15, 29, 26308 }, // U+05D0 - { 14, 15, 226, 0, 15, 27, 26337 }, // U+05D1 - { 10, 15, 181, 0, 15, 19, 26364 }, // U+05D2 - { 14, 15, 219, 0, 15, 27, 26383 }, // U+05D3 - { 14, 15, 262, 1, 15, 27, 26410 }, // U+05D4 - { 5, 15, 118, 1, 15, 10, 26437 }, // U+05D5 - { 10, 15, 162, 0, 15, 19, 26447 }, // U+05D6 - { 15, 15, 272, 1, 15, 29, 26466 }, // U+05D7 - { 15, 15, 264, 1, 15, 29, 26495 }, // U+05D8 - { 5, 9, 113, 1, 15, 6, 26524 }, // U+05D9 - { 12, 20, 211, 0, 15, 30, 26530 }, // U+05DA - { 13, 15, 217, 0, 15, 25, 26560 }, // U+05DB - { 13, 19, 206, 0, 19, 31, 26585 }, // U+05DC - { 15, 15, 272, 1, 15, 29, 26616 }, // U+05DD - { 15, 15, 276, 1, 15, 29, 26645 }, // U+05DE - { 5, 20, 118, 1, 15, 13, 26674 }, // U+05DF - { 9, 15, 160, 0, 15, 17, 26687 }, // U+05E0 - { 15, 15, 265, 1, 15, 29, 26704 }, // U+05E1 - { 15, 16, 246, 0, 15, 30, 26733 }, // U+05E2 - { 14, 20, 259, 1, 15, 35, 26763 }, // U+05E3 - { 15, 15, 262, 1, 15, 29, 26798 }, // U+05E4 - { 14, 20, 233, 0, 15, 35, 26827 }, // U+05E5 - { 14, 15, 246, 0, 15, 27, 26862 }, // U+05E6 - { 15, 20, 255, 1, 15, 38, 26889 }, // U+05E7 - { 12, 15, 214, 0, 15, 23, 26927 }, // U+05E8 - { 18, 15, 321, 1, 15, 34, 26950 }, // U+05E9 - { 14, 15, 266, 1, 15, 27, 26984 }, // U+05EA - { 18, 25, 296, 0, 19, 57, 27011 }, // U+1EA0 - { 13, 20, 230, 0, 14, 33, 27068 }, // U+1EA1 - { 18, 24, 296, 0, 24, 54, 27101 }, // U+1EA2 - { 13, 20, 230, 0, 19, 33, 27155 }, // U+1EA3 - { 18, 28, 296, 0, 28, 63, 27188 }, // U+1EA4 - { 14, 24, 230, 0, 23, 42, 27251 }, // U+1EA5 - { 18, 28, 296, 0, 28, 63, 27293 }, // U+1EA6 - { 13, 24, 230, 0, 23, 39, 27356 }, // U+1EA7 - { 18, 28, 296, 0, 28, 63, 27395 }, // U+1EA8 - { 13, 24, 230, 0, 23, 39, 27458 }, // U+1EA9 - { 18, 28, 296, 0, 28, 63, 27497 }, // U+1EAA - { 13, 24, 230, 0, 23, 39, 27560 }, // U+1EAB - { 18, 30, 296, 0, 24, 68, 27599 }, // U+1EAC - { 13, 25, 230, 0, 19, 41, 27667 }, // U+1EAD - { 18, 28, 296, 0, 28, 63, 27708 }, // U+1EAE - { 13, 24, 230, 0, 23, 39, 27771 }, // U+1EAF - { 18, 28, 296, 0, 28, 63, 27810 }, // U+1EB0 - { 13, 24, 230, 0, 23, 39, 27873 }, // U+1EB1 - { 18, 28, 296, 0, 28, 63, 27912 }, // U+1EB2 - { 13, 24, 230, 0, 23, 39, 27975 }, // U+1EB3 - { 18, 28, 296, 0, 28, 63, 28014 }, // U+1EB4 - { 13, 24, 230, 0, 23, 39, 28077 }, // U+1EB5 - { 18, 30, 296, 0, 24, 68, 28116 }, // U+1EB6 - { 13, 25, 230, 0, 19, 41, 28184 }, // U+1EB7 - { 13, 25, 243, 1, 19, 41, 28225 }, // U+1EB8 - { 14, 20, 235, 0, 14, 35, 28266 }, // U+1EB9 - { 13, 24, 243, 1, 24, 39, 28301 }, // U+1EBA - { 14, 20, 235, 0, 19, 35, 28340 }, // U+1EBB - { 13, 24, 243, 1, 24, 39, 28375 }, // U+1EBC - { 14, 20, 235, 0, 19, 35, 28414 }, // U+1EBD - { 14, 28, 243, 1, 28, 49, 28449 }, // U+1EBE - { 14, 24, 235, 0, 23, 42, 28498 }, // U+1EBF - { 13, 28, 243, 1, 28, 46, 28540 }, // U+1EC0 - { 14, 24, 235, 0, 23, 42, 28586 }, // U+1EC1 - { 13, 28, 243, 1, 28, 46, 28628 }, // U+1EC2 - { 14, 24, 235, 0, 23, 42, 28674 }, // U+1EC3 - { 13, 28, 243, 1, 28, 46, 28716 }, // U+1EC4 - { 14, 24, 235, 0, 23, 42, 28762 }, // U+1EC5 - { 13, 30, 243, 1, 24, 49, 28804 }, // U+1EC6 - { 14, 25, 235, 0, 19, 44, 28853 }, // U+1EC7 - { 5, 24, 110, 1, 24, 15, 28897 }, // U+1EC8 - { 6, 19, 106, 0, 19, 15, 28912 }, // U+1EC9 - { 5, 25, 110, 1, 19, 16, 28927 }, // U+1ECA - { 5, 25, 106, 1, 19, 16, 28943 }, // U+1ECB - { 18, 25, 306, 1, 19, 57, 28959 }, // U+1ECC - { 15, 20, 242, 0, 14, 38, 29016 }, // U+1ECD - { 18, 25, 306, 1, 24, 57, 29054 }, // U+1ECE - { 15, 20, 242, 0, 19, 38, 29111 }, // U+1ECF - { 18, 29, 306, 1, 28, 66, 29149 }, // U+1ED0 - { 15, 24, 242, 0, 23, 45, 29215 }, // U+1ED1 - { 18, 29, 306, 1, 28, 66, 29260 }, // U+1ED2 - { 15, 24, 242, 0, 23, 45, 29326 }, // U+1ED3 - { 18, 29, 306, 1, 28, 66, 29371 }, // U+1ED4 - { 15, 24, 242, 0, 23, 45, 29437 }, // U+1ED5 - { 18, 29, 306, 1, 28, 66, 29482 }, // U+1ED6 - { 15, 24, 242, 0, 23, 45, 29548 }, // U+1ED7 - { 18, 30, 306, 1, 24, 68, 29593 }, // U+1ED8 - { 15, 25, 242, 0, 19, 47, 29661 }, // U+1ED9 - { 18, 25, 309, 1, 24, 57, 29708 }, // U+1EDA - { 15, 20, 242, 0, 19, 38, 29765 }, // U+1EDB - { 18, 25, 309, 1, 24, 57, 29803 }, // U+1EDC - { 15, 20, 242, 0, 19, 38, 29860 }, // U+1EDD - { 18, 25, 309, 1, 24, 57, 29898 }, // U+1EDE - { 15, 20, 242, 0, 19, 38, 29955 }, // U+1EDF - { 18, 25, 309, 1, 24, 57, 29993 }, // U+1EE0 - { 15, 20, 242, 0, 19, 38, 30050 }, // U+1EE1 - { 18, 27, 309, 1, 21, 61, 30088 }, // U+1EE2 - { 15, 23, 242, 0, 17, 44, 30149 }, // U+1EE3 - { 16, 25, 290, 1, 19, 50, 30193 }, // U+1EE4 - { 13, 20, 246, 1, 14, 33, 30243 }, // U+1EE5 - { 16, 25, 290, 1, 24, 50, 30276 }, // U+1EE6 - { 13, 20, 246, 1, 19, 33, 30326 }, // U+1EE7 - { 18, 25, 290, 1, 24, 57, 30359 }, // U+1EE8 - { 15, 20, 263, 1, 19, 38, 30416 }, // U+1EE9 - { 18, 25, 290, 1, 24, 57, 30454 }, // U+1EEA - { 15, 20, 263, 1, 19, 38, 30511 }, // U+1EEB - { 18, 25, 290, 1, 24, 57, 30549 }, // U+1EEC - { 15, 20, 263, 1, 19, 38, 30606 }, // U+1EED - { 18, 25, 290, 1, 24, 57, 30644 }, // U+1EEE - { 15, 20, 263, 1, 19, 38, 30701 }, // U+1EEF - { 18, 27, 290, 1, 21, 61, 30739 }, // U+1EF0 - { 15, 23, 263, 1, 17, 44, 30800 }, // U+1EF1 - { 18, 24, 289, 0, 24, 54, 30844 }, // U+1EF2 - { 15, 25, 237, 0, 19, 47, 30898 }, // U+1EF3 - { 18, 25, 289, 0, 19, 57, 30945 }, // U+1EF4 - { 15, 20, 237, 0, 14, 38, 31002 }, // U+1EF5 - { 18, 24, 289, 0, 24, 54, 31040 }, // U+1EF6 - { 15, 25, 237, 0, 19, 47, 31094 }, // U+1EF7 - { 18, 24, 289, 0, 24, 54, 31141 }, // U+1EF8 - { 15, 25, 237, 0, 19, 47, 31195 }, // U+1EF9 - { 0, 0, 227, 0, 0, 0, 31242 }, // U+2000 - { 0, 0, 451, 0, 0, 0, 31242 }, // U+2001 - { 0, 0, 200, 0, 0, 0, 31242 }, // U+2002 - { 0, 0, 400, 0, 0, 0, 31242 }, // U+2003 - { 0, 0, 134, 0, 0, 0, 31242 }, // U+2004 - { 0, 0, 100, 0, 0, 0, 31242 }, // U+2005 - { 0, 0, 66, 0, 0, 0, 31242 }, // U+2006 - { 0, 0, 258, 0, 0, 0, 31242 }, // U+2007 - { 0, 0, 126, 0, 0, 0, 31242 }, // U+2008 - { 0, 0, 61, 0, 0, 0, 31242 }, // U+2009 - { 0, 0, 29, 0, 0, 0, 31242 }, // U+200A - { 0, 0, 0, 0, 0, 0, 31242 }, // U+200B - { 0, 0, 0, 0, 0, 0, 31242 }, // U+200C - { 0, 0, 0, 0, 0, 0, 31242 }, // U+200D - { 0, 0, 0, 0, 0, 0, 31242 }, // U+200E - { 0, 0, 0, 0, 0, 0, 31242 }, // U+200F - { 9, 4, 179, 1, 9, 5, 31242 }, // U+2010 - { 9, 4, 179, 1, 9, 5, 31247 }, // U+2011 - { 16, 4, 258, 0, 10, 8, 31252 }, // U+2012 - { 13, 4, 200, 0, 9, 7, 31260 }, // U+2013 - { 25, 4, 400, 0, 9, 13, 31267 }, // U+2014 - { 25, 4, 400, 0, 9, 13, 31280 }, // U+2015 - { 10, 31, 223, 2, 25, 39, 31293 }, // U+2016 - { 12, 6, 188, 0, 0, 9, 31332 }, // U+2017 - { 6, 9, 117, 1, 19, 7, 31341 }, // U+2018 - { 6, 9, 117, 1, 19, 7, 31348 }, // U+2019 - { 5, 9, 108, 1, 4, 6, 31355 }, // U+201A - { 6, 9, 117, 0, 19, 7, 31361 }, // U+201B - { 12, 9, 206, 1, 19, 14, 31368 }, // U+201C - { 11, 9, 203, 1, 19, 13, 31382 }, // U+201D - { 11, 9, 195, 1, 4, 13, 31395 }, // U+201E - { 11, 9, 203, 0, 19, 13, 31408 }, // U+201F - { 12, 15, 232, 1, 19, 23, 31421 }, // U+2020 - { 12, 19, 232, 1, 19, 29, 31444 }, // U+2021 - { 9, 9, 186, 1, 12, 11, 31473 }, // U+2022 - { 7, 9, 186, 3, 12, 8, 31484 }, // U+2023 - { 6, 5, 126, 1, 4, 4, 31492 }, // U+2024 - { 13, 5, 251, 1, 4, 9, 31496 }, // U+2025 - { 21, 5, 377, 1, 4, 14, 31505 }, // U+2026 - { 6, 5, 126, 1, 11, 4, 31519 }, // U+2027 - { 0, 0, 0, 0, 0, 0, 31523 }, // U+202A - { 0, 0, 0, 0, 0, 0, 31523 }, // U+202B - { 0, 0, 0, 0, 0, 0, 31523 }, // U+202C - { 0, 0, 0, 0, 0, 0, 31523 }, // U+202D - { 0, 0, 0, 0, 0, 0, 31523 }, // U+202E - { 0, 0, 61, 0, 0, 0, 31523 }, // U+202F - { 31, 20, 548, 2, 19, 78, 31523 }, // U+2030 - { 40, 20, 697, 2, 19, 100, 31601 }, // U+2031 - { 5, 8, 108, 1, 19, 5, 31701 }, // U+2032 - { 12, 8, 215, 1, 19, 12, 31706 }, // U+2033 - { 18, 8, 322, 1, 19, 18, 31718 }, // U+2034 - { 5, 8, 108, 1, 19, 5, 31736 }, // U+2035 - { 12, 8, 215, 1, 19, 12, 31741 }, // U+2036 - { 18, 8, 322, 1, 19, 18, 31753 }, // U+2037 - { 12, 6, 217, 1, -1, 9, 31771 }, // U+2038 - { 9, 13, 169, 1, 14, 15, 31780 }, // U+2039 - { 10, 13, 169, 0, 14, 17, 31795 }, // U+203A - { 17, 16, 321, 2, 16, 34, 31812 }, // U+203B - { 14, 20, 254, 1, 19, 35, 31846 }, // U+203C - { 13, 20, 222, 0, 19, 33, 31881 }, // U+203D - { 13, 3, 197, 0, 21, 5, 31914 }, // U+203E - { 21, 5, 338, 0, -1, 14, 31919 }, // U+203F - { 21, 5, 338, 0, 19, 14, 31933 }, // U+2040 - { 11, 14, 189, 0, 8, 20, 31947 }, // U+2041 - { 17, 16, 303, 1, 17, 34, 31967 }, // U+2042 - { 8, 4, 186, 2, 9, 4, 32001 }, // U+2043 - { 16, 19, 82, -5, 19, 38, 32005 }, // U+2044 - { 7, 24, 147, 2, 20, 21, 32043 }, // U+2045 - { 7, 24, 147, 1, 20, 21, 32064 }, // U+2046 - { 27, 20, 449, 0, 19, 68, 32085 }, // U+2047 - { 21, 20, 352, 0, 19, 53, 32153 }, // U+2048 - { 20, 20, 352, 1, 19, 50, 32206 }, // U+2049 - { 11, 20, 184, 0, 14, 28, 32256 }, // U+204A - { 13, 19, 237, 1, 19, 31, 32284 }, // U+204B - { 13, 11, 241, 1, 13, 18, 32315 }, // U+204C - { 13, 11, 241, 1, 13, 18, 32333 }, // U+204D - { 11, 12, 221, 1, 9, 17, 32351 }, // U+204E - { 6, 18, 129, 1, 13, 14, 32368 }, // U+204F - { 21, 25, 337, 0, 19, 66, 32382 }, // U+2050 - { 11, 24, 228, 3, 19, 33, 32448 }, // U+2051 - { 18, 19, 318, 1, 19, 43, 32481 }, // U+2052 - { 17, 7, 302, 1, 12, 15, 32524 }, // U+2053 - { 21, 5, 338, 0, -1, 14, 32539 }, // U+2054 - { 14, 13, 284, 2, 13, 23, 32553 }, // U+2055 - { 25, 8, 429, 1, 19, 25, 32576 }, // U+2057 - { 0, 0, 116, 0, 0, 0, 32601 }, // U+205F - { 0, 0, 0, 0, 0, 0, 32601 }, // U+2060 - { 0, 0, 0, 0, 0, 0, 32601 }, // U+2061 - { 0, 0, 0, 0, 0, 0, 32601 }, // U+2062 - { 0, 0, 0, 0, 0, 0, 32601 }, // U+2063 - { 0, 0, 0, 0, 0, 0, 32601 }, // U+2064 - { 0, 0, 0, 0, 0, 0, 32601 }, // U+2066 - { 0, 0, 0, 0, 0, 0, 32601 }, // U+2067 - { 0, 0, 0, 0, 0, 0, 32601 }, // U+2068 - { 0, 0, 0, 0, 0, 0, 32601 }, // U+2069 - { 0, 0, 0, 0, 0, 0, 32601 }, // U+206A - { 0, 0, 0, 0, 0, 0, 32601 }, // U+206B - { 0, 0, 0, 0, 0, 0, 32601 }, // U+206C - { 0, 0, 0, 0, 0, 0, 32601 }, // U+206D - { 0, 0, 0, 0, 0, 0, 32601 }, // U+206E - { 0, 0, 0, 0, 0, 0, 32601 }, // U+206F - { 10, 12, 192, 1, 22, 15, 32601 }, // U+2070 - { 4, 12, 86, 1, 22, 6, 32616 }, // U+2071 - { 10, 11, 189, 1, 21, 14, 32622 }, // U+2074 - { 10, 11, 182, 1, 21, 14, 32636 }, // U+2075 - { 10, 12, 184, 1, 22, 15, 32650 }, // U+2076 - { 10, 11, 164, 0, 21, 14, 32665 }, // U+2077 - { 10, 12, 186, 1, 22, 15, 32679 }, // U+2078 - { 10, 12, 184, 1, 22, 15, 32694 }, // U+2079 - { 9, 9, 184, 1, 20, 11, 32709 }, // U+207A - { 9, 3, 184, 1, 17, 4, 32720 }, // U+207B - { 9, 7, 184, 1, 19, 8, 32724 }, // U+207C - { 6, 13, 111, 1, 22, 10, 32732 }, // U+207D - { 6, 13, 111, 0, 22, 10, 32742 }, // U+207E - { 9, 9, 178, 1, 19, 11, 32752 }, // U+207F - { 10, 12, 192, 1, 7, 15, 32763 }, // U+2080 - { 6, 11, 128, 1, 6, 9, 32778 }, // U+2081 - { 9, 12, 178, 1, 7, 14, 32787 }, // U+2082 - { 10, 12, 183, 1, 7, 15, 32801 }, // U+2083 - { 10, 11, 189, 1, 6, 14, 32816 }, // U+2084 - { 10, 11, 182, 1, 6, 14, 32830 }, // U+2085 - { 10, 12, 184, 1, 7, 15, 32844 }, // U+2086 - { 10, 11, 164, 0, 6, 14, 32859 }, // U+2087 - { 10, 12, 186, 1, 7, 15, 32873 }, // U+2088 - { 10, 12, 184, 1, 7, 15, 32888 }, // U+2089 - { 9, 9, 184, 1, 5, 11, 32903 }, // U+208A - { 9, 3, 184, 1, 2, 4, 32914 }, // U+208B - { 9, 7, 184, 1, 4, 8, 32918 }, // U+208C - { 6, 13, 111, 1, 7, 10, 32926 }, // U+208D - { 6, 13, 111, 0, 7, 10, 32936 }, // U+208E - { 9, 9, 164, 0, 4, 11, 32946 }, // U+2090 - { 10, 9, 168, 0, 4, 12, 32957 }, // U+2091 - { 10, 9, 173, 0, 4, 12, 32969 }, // U+2092 - { 10, 9, 167, 0, 4, 12, 32981 }, // U+2093 - { 10, 11, 190, 1, 5, 14, 32993 }, // U+2094 - { 9, 11, 178, 1, 6, 13, 33007 }, // U+2095 - { 9, 11, 166, 1, 6, 13, 33020 }, // U+2096 - { 4, 11, 86, 1, 6, 6, 33033 }, // U+2097 - { 14, 9, 253, 1, 4, 16, 33039 }, // U+2098 - { 9, 9, 178, 1, 4, 11, 33055 }, // U+2099 - { 10, 12, 178, 1, 4, 15, 33066 }, // U+209A - { 10, 9, 161, 0, 4, 12, 33081 }, // U+209B - { 7, 11, 109, 0, 6, 10, 33093 }, // U+209C - { 17, 20, 302, 1, 19, 43, 33103 }, // U+20A0 - { 17, 24, 295, 1, 21, 51, 33146 }, // U+20A1 - { 17, 20, 288, 1, 19, 43, 33197 }, // U+20A2 - { 16, 19, 245, -1, 19, 38, 33240 }, // U+20A3 - { 14, 19, 253, 1, 19, 34, 33278 }, // U+20A4 - { 20, 18, 343, 1, 16, 45, 33312 }, // U+20A5 - { 19, 19, 301, 0, 19, 46, 33357 }, // U+20A6 - { 24, 20, 399, 1, 19, 60, 33403 }, // U+20A7 - { 28, 20, 466, 1, 19, 70, 33463 }, // U+20A8 - { 25, 19, 414, 0, 19, 60, 33533 }, // U+20A9 - { 21, 14, 366, 1, 14, 37, 33593 }, // U+20AA - { 16, 24, 249, 0, 19, 48, 33630 }, // U+20AB - { 16, 20, 272, 0, 19, 40, 33678 }, // U+20AC - { 20, 19, 318, 0, 19, 48, 33718 }, // U+20AD - { 16, 19, 264, 0, 19, 38, 33766 }, // U+20AE - { 36, 27, 580, 0, 21, 122, 33804 }, // U+20AF - { 18, 19, 283, 0, 19, 43, 33926 }, // U+20B1 - { 17, 24, 299, 1, 21, 51, 33969 }, // U+20B2 - { 18, 19, 296, 0, 19, 43, 34020 }, // U+20B3 - { 15, 20, 293, 2, 19, 38, 34063 }, // U+20B4 - { 17, 24, 295, 1, 21, 51, 34101 }, // U+20B5 - { 16, 19, 265, 0, 19, 38, 34152 }, // U+20B8 - { 14, 19, 226, 0, 19, 34, 34190 }, // U+20B9 - { 14, 20, 256, 1, 19, 35, 34224 }, // U+20BA - { 17, 14, 302, 1, 14, 30, 34259 }, // U+20BC - { 17, 19, 294, 1, 19, 41, 34289 }, // U+20BD - { 17, 19, 302, 1, 19, 41, 34330 }, // U+20BE - { 15, 24, 263, 1, 21, 45, 34371 }, // U+20BF - { 19, 18, 380, 2, 17, 43, 34416 }, // U+2190 - { 19, 19, 360, 2, 19, 46, 34459 }, // U+2191 - { 19, 18, 380, 3, 17, 43, 34505 }, // U+2192 - { 19, 20, 360, 2, 19, 48, 34548 }, // U+2193 - { 29, 18, 534, 2, 17, 66, 34596 }, // U+2194 - { 17, 30, 342, 2, 23, 64, 34662 }, // U+2195 - { 15, 15, 307, 2, 14, 29, 34726 }, // U+2196 - { 15, 15, 307, 2, 14, 29, 34755 }, // U+2197 - { 15, 14, 307, 2, 14, 27, 34784 }, // U+2198 - { 15, 14, 307, 2, 14, 27, 34811 }, // U+2199 - { 25, 20, 461, 2, 19, 63, 34838 }, // U+21A9 - { 25, 20, 461, 2, 19, 63, 34901 }, // U+21AA - { 21, 19, 403, 2, 19, 50, 34964 }, // U+21B0 - { 21, 19, 403, 2, 19, 50, 35014 }, // U+21B1 - { 14, 17, 360, 4, 17, 30, 35064 }, // U+21B2 - { 21, 18, 403, 2, 19, 48, 35094 }, // U+21B3 - { 18, 20, 356, 2, 19, 45, 35142 }, // U+21B4 - { 21, 18, 403, 2, 19, 48, 35187 }, // U+21B5 - { 22, 16, 360, 0, 17, 44, 35235 }, // U+21B6 - { 22, 16, 360, 0, 17, 44, 35279 }, // U+21B7 - { 19, 23, 380, 2, 23, 55, 35323 }, // U+21BA - { 19, 23, 380, 2, 23, 55, 35378 }, // U+21BB - { 19, 21, 360, 2, 19, 50, 35433 }, // U+21C4 - { 19, 21, 360, 2, 19, 50, 35483 }, // U+21C6 - { 19, 18, 380, 2, 17, 43, 35533 }, // U+21D0 - { 19, 18, 380, 3, 17, 43, 35576 }, // U+21D2 - { 29, 18, 534, 2, 17, 66, 35619 }, // U+21D4 - { 13, 19, 252, 1, 19, 31, 35685 }, // U+21DE - { 13, 20, 252, 1, 19, 33, 35716 }, // U+21DF - { 22, 13, 418, 2, 16, 36, 35749 }, // U+21E4 - { 22, 13, 418, 2, 16, 36, 35785 }, // U+21E5 - { 25, 20, 438, 1, 20, 63, 35821 }, // U+21E7 - { 25, 25, 438, 1, 20, 79, 35884 }, // U+21EA - { 14, 20, 266, 1, 19, 35, 35963 }, // U+2202 - { 15, 16, 274, 1, 15, 30, 35998 }, // U+2205 - { 18, 19, 293, 0, 19, 43, 36028 }, // U+2206 - { 15, 23, 304, 2, 19, 44, 36071 }, // U+220F - { 16, 23, 294, 1, 19, 46, 36115 }, // U+2211 - { 13, 4, 269, 2, 9, 7, 36161 }, // U+2212 - { 17, 17, 59, -7, 17, 37, 36168 }, // U+2215 - { 15, 19, 266, 1, 19, 36, 36205 }, // U+221A - { 24, 12, 390, 0, 14, 36, 36241 }, // U+221E - { 11, 26, 152, -1, 20, 36, 36277 }, // U+222B - { 6, 14, 126, 1, 16, 11, 36313 }, // U+2236 - { 14, 12, 269, 1, 13, 21, 36324 }, // U+2248 - { 13, 15, 269, 2, 15, 25, 36345 }, // U+2260 - { 13, 15, 269, 2, 15, 25, 36370 }, // U+2264 - { 13, 15, 269, 2, 15, 25, 36395 }, // U+2265 - { 35, 35, 556, 0, 27, 154, 36420 }, // U+2295 - { 35, 35, 556, 0, 27, 154, 36574 }, // U+2296 - { 35, 35, 556, 0, 27, 154, 36728 }, // U+2297 - { 35, 35, 556, 0, 27, 154, 36882 }, // U+2298 - { 15, 19, 259, 0, 19, 36, 37036 }, // U+FB01 - { 17, 19, 266, 0, 19, 41, 37072 }, // U+FB02 + { 6, 7, 0, -3, 18, 6, 15759 }, // U+0312 + { 5, 9, 0, 5, 13, 6, 15765 }, // U+0313 + { 5, 9, 0, 5, 13, 6, 15771 }, // U+0315 + { 5, 7, 0, -6, 19, 5, 15777 }, // U+031B + { 5, 4, 113, 1, -2, 3, 15782 }, // U+0323 + { 4, 6, 0, -10, -1, 3, 15785 }, // U+0326 + { 6, 7, 0, 2, 1, 6, 15788 }, // U+0327 + { 7, 7, 0, 0, 1, 7, 15794 }, // U+0328 + { 11, 4, 0, 0, -2, 6, 15801 }, // U+032C + { 12, 17, 0, -14, 15, 26, 15807 }, // U+0337 + { 15, 20, 0, -15, 19, 38, 15833 }, // U+0338 + { 10, 4, 0, -13, 19, 5, 15871 }, // U+0342 + { 5, 5, 0, 0, 15, 4, 15876 }, // U+0343 + { 10, 6, 0, -13, 20, 8, 15880 }, // U+0346 + { 9, 7, 0, -12, -2, 8, 15888 }, // U+0347 + { 7, 7, 0, -11, 0, 7, 15896 }, // U+0348 + { 6, 6, 0, -10, -1, 5, 15903 }, // U+0349 + { 11, 6, 0, -14, 20, 9, 15908 }, // U+034A + { 10, 11, 0, -13, 25, 14, 15917 }, // U+034B + { 10, 7, 0, -13, 22, 9, 15931 }, // U+034C + { 12, 6, 0, -14, -1, 9, 15940 }, // U+034D + { 6, 7, 0, -11, 0, 6, 15949 }, // U+034E + { 0, 0, 0, 0, 0, 0, 15955 }, // U+034F + { 10, 10, 0, -12, 25, 13, 15955 }, // U+0350 + { 4, 6, 0, -10, 19, 3, 15968 }, // U+0351 + { 10, 8, 0, -13, 22, 10, 15971 }, // U+0352 + { 7, 7, 0, -11, 0, 7, 15981 }, // U+0353 + { 7, 7, 0, -11, 0, 7, 15988 }, // U+0354 + { 7, 7, 0, -11, 0, 7, 15995 }, // U+0355 + { 15, 7, 0, -14, 0, 14, 16002 }, // U+0356 + { 4, 7, 0, 0, 21, 4, 16016 }, // U+0357 + { 5, 4, 0, 1, 19, 3, 16020 }, // U+0358 + { 6, 7, 0, -11, 0, 6, 16023 }, // U+0359 + { 11, 6, 0, -13, 0, 9, 16029 }, // U+035A + { 6, 7, 0, -11, 21, 6, 16038 }, // U+035B + { 20, 5, 0, -11, -1, 13, 16044 }, // U+035C + { 19, 5, 0, -10, 19, 12, 16057 }, // U+035D + { 15, 3, 0, -15, 19, 6, 16069 }, // U+035E + { 15, 3, 0, -15, -2, 6, 16075 }, // U+035F + { 26, 5, 0, -14, 19, 17, 16081 }, // U+0360 + { 19, 6, 0, -10, 19, 15, 16098 }, // U+0361 + { 22, 6, 0, -11, -1, 17, 16113 }, // U+0362 + { 7, 8, 0, -11, 22, 7, 16130 }, // U+0363 + { 7, 8, 0, -11, 22, 7, 16137 }, // U+0364 + { 3, 11, 0, -9, 25, 5, 16144 }, // U+0365 + { 8, 8, 0, -12, 22, 8, 16149 }, // U+0366 + { 7, 8, 0, -11, 22, 7, 16157 }, // U+0367 + { 7, 8, 0, -11, 22, 7, 16164 }, // U+0368 + { 8, 11, 0, -12, 25, 11, 16171 }, // U+0369 + { 7, 11, 0, -11, 25, 10, 16182 }, // U+036A + { 12, 8, 0, -14, 22, 12, 16192 }, // U+036B + { 5, 8, 0, -10, 22, 5, 16204 }, // U+036C + { 5, 10, 0, 0, 24, 7, 16209 }, // U+036D + { 8, 8, 0, -12, 22, 8, 16216 }, // U+036E + { 7, 8, 0, -11, 22, 7, 16224 }, // U+036F + { 13, 24, 243, 1, 24, 39, 16231 }, // U+0400 + { 13, 24, 243, 1, 24, 39, 16270 }, // U+0401 + { 21, 21, 341, 0, 19, 56, 16309 }, // U+0402 + { 13, 24, 230, 1, 24, 39, 16365 }, // U+0403 + { 17, 20, 295, 1, 19, 43, 16404 }, // U+0404 + { 15, 20, 262, 1, 19, 38, 16447 }, // U+0405 + { 5, 19, 110, 1, 19, 12, 16485 }, // U+0406 + { 11, 24, 110, -2, 24, 33, 16497 }, // U+0407 + { 13, 20, 232, 0, 19, 33, 16530 }, // U+0408 + { 27, 19, 444, 0, 19, 65, 16563 }, // U+0409 + { 27, 19, 448, 1, 19, 65, 16628 }, // U+040A + { 21, 19, 355, 0, 19, 50, 16693 }, // U+040B + { 17, 24, 285, 1, 24, 51, 16743 }, // U+040C + { 17, 24, 302, 1, 24, 51, 16794 }, // U+040D + { 17, 24, 274, 0, 24, 51, 16845 }, // U+040E + { 16, 23, 289, 1, 19, 46, 16896 }, // U+040F + { 18, 19, 296, 0, 19, 43, 16942 }, // U+0410 + { 15, 19, 257, 1, 19, 36, 16985 }, // U+0411 + { 15, 19, 263, 1, 19, 36, 17021 }, // U+0412 + { 13, 19, 230, 1, 19, 31, 17057 }, // U+0413 + { 21, 23, 342, 0, 19, 61, 17088 }, // U+0414 + { 13, 19, 243, 1, 19, 31, 17149 }, // U+0415 + { 26, 19, 422, 0, 19, 62, 17180 }, // U+0416 + { 14, 20, 256, 1, 19, 35, 17242 }, // U+0417 + { 17, 19, 302, 1, 19, 41, 17277 }, // U+0418 + { 17, 24, 302, 1, 24, 51, 17318 }, // U+0419 + { 17, 19, 285, 1, 19, 41, 17369 }, // U+041A + { 18, 19, 297, 0, 19, 43, 17410 }, // U+041B + { 21, 19, 369, 1, 19, 50, 17453 }, // U+041C + { 16, 19, 296, 1, 19, 38, 17503 }, // U+041D + { 18, 20, 306, 1, 19, 45, 17541 }, // U+041E + { 16, 19, 296, 1, 19, 38, 17586 }, // U+041F + { 15, 19, 258, 1, 19, 36, 17624 }, // U+0420 + { 17, 20, 295, 1, 19, 43, 17660 }, // U+0421 + { 16, 19, 264, 0, 19, 38, 17703 }, // U+0422 + { 17, 19, 274, 0, 19, 41, 17741 }, // U+0423 + { 19, 22, 333, 1, 20, 53, 17782 }, // U+0424 + { 18, 19, 292, 0, 19, 43, 17835 }, // U+0425 + { 18, 23, 305, 1, 19, 52, 17878 }, // U+0426 + { 15, 19, 281, 1, 19, 36, 17930 }, // U+0427 + { 22, 19, 393, 1, 19, 53, 17966 }, // U+0428 + { 24, 23, 404, 1, 19, 69, 18019 }, // U+0429 + { 20, 19, 330, 0, 19, 48, 18088 }, // U+042A + { 21, 19, 363, 1, 19, 50, 18136 }, // U+042B + { 15, 19, 263, 1, 19, 36, 18186 }, // U+042C + { 18, 20, 295, 0, 19, 45, 18222 }, // U+042D + { 24, 20, 415, 1, 19, 60, 18267 }, // U+042E + { 15, 19, 262, 0, 19, 36, 18327 }, // U+042F + { 13, 15, 230, 0, 14, 25, 18363 }, // U+0430 + { 15, 20, 241, 0, 19, 38, 18388 }, // U+0431 + { 13, 14, 227, 1, 14, 23, 18426 }, // U+0432 + { 10, 14, 180, 1, 14, 18, 18449 }, // U+0433 + { 16, 18, 263, 0, 14, 36, 18467 }, // U+0434 + { 14, 15, 235, 0, 14, 27, 18503 }, // U+0435 + { 21, 14, 343, 0, 14, 37, 18530 }, // U+0436 + { 13, 15, 205, 0, 14, 25, 18567 }, // U+0437 + { 13, 14, 247, 1, 14, 23, 18592 }, // U+0438 + { 13, 19, 247, 1, 19, 31, 18615 }, // U+0439 + { 14, 14, 231, 1, 14, 25, 18646 }, // U+043A + { 14, 14, 240, 0, 14, 25, 18671 }, // U+043B + { 18, 14, 322, 1, 14, 32, 18696 }, // U+043C + { 13, 14, 246, 1, 14, 23, 18728 }, // U+043D + { 15, 15, 242, 0, 14, 29, 18751 }, // U+043E + { 13, 14, 243, 1, 14, 23, 18780 }, // U+043F + { 14, 20, 249, 1, 14, 35, 18803 }, // U+0440 + { 14, 15, 232, 0, 14, 27, 18838 }, // U+0441 + { 13, 14, 204, 0, 14, 23, 18865 }, // U+0442 + { 15, 20, 237, 0, 14, 38, 18888 }, // U+0443 + { 19, 25, 308, 0, 19, 60, 18926 }, // U+0444 + { 14, 14, 228, 0, 14, 25, 18986 }, // U+0445 + { 15, 18, 261, 1, 14, 34, 19011 }, // U+0446 + { 13, 14, 238, 1, 14, 23, 19045 }, // U+0447 + { 19, 14, 342, 1, 14, 34, 19068 }, // U+0448 + { 21, 18, 353, 1, 14, 48, 19102 }, // U+0449 + { 16, 14, 268, 0, 14, 28, 19150 }, // U+044A + { 18, 14, 317, 1, 14, 32, 19178 }, // U+044B + { 13, 14, 231, 1, 14, 23, 19210 }, // U+044C + { 14, 15, 233, 0, 14, 27, 19233 }, // U+044D + { 20, 15, 344, 1, 14, 38, 19260 }, // U+044E + { 13, 14, 229, 0, 14, 23, 19298 }, // U+044F + { 14, 20, 235, 0, 19, 35, 19321 }, // U+0450 + { 14, 20, 235, 0, 19, 35, 19356 }, // U+0451 + { 15, 25, 246, -1, 19, 47, 19391 }, // U+0452 + { 10, 19, 180, 1, 19, 24, 19438 }, // U+0453 + { 14, 15, 233, 0, 14, 27, 19462 }, // U+0454 + { 13, 15, 221, 0, 14, 25, 19489 }, // U+0455 + { 5, 19, 106, 1, 19, 12, 19514 }, // U+0456 + { 10, 19, 106, -2, 19, 24, 19526 }, // U+0457 + { 7, 25, 106, -1, 19, 22, 19550 }, // U+0458 + { 23, 14, 374, 0, 14, 41, 19572 }, // U+0459 + { 21, 14, 366, 1, 14, 37, 19613 }, // U+045A + { 15, 19, 246, -1, 19, 36, 19650 }, // U+045B + { 14, 19, 231, 1, 19, 34, 19686 }, // U+045C + { 13, 19, 247, 1, 19, 31, 19720 }, // U+045D + { 15, 25, 237, 0, 19, 47, 19751 }, // U+045E + { 13, 18, 243, 1, 14, 30, 19798 }, // U+045F + { 22, 20, 389, 1, 19, 55, 19828 }, // U+0460 + { 20, 15, 350, 1, 14, 38, 19883 }, // U+0461 + { 17, 19, 283, 0, 19, 41, 19921 }, // U+0462 + { 15, 19, 254, 0, 19, 36, 19962 }, // U+0463 + { 23, 20, 398, 1, 19, 58, 19998 }, // U+0464 + { 19, 15, 329, 1, 14, 36, 20056 }, // U+0465 + { 18, 19, 296, 0, 19, 43, 20092 }, // U+0466 + { 15, 14, 235, 0, 14, 27, 20135 }, // U+0467 + { 24, 19, 395, 1, 19, 57, 20162 }, // U+0468 + { 20, 14, 331, 1, 14, 35, 20219 }, // U+0469 + { 20, 19, 351, 1, 19, 48, 20254 }, // U+046A + { 17, 14, 298, 1, 14, 30, 20302 }, // U+046B + { 27, 19, 471, 1, 19, 65, 20332 }, // U+046C + { 24, 14, 406, 1, 14, 42, 20397 }, // U+046D + { 12, 30, 226, 1, 24, 45, 20439 }, // U+046E + { 12, 25, 215, 0, 19, 38, 20484 }, // U+046F + { 20, 19, 356, 1, 19, 48, 20522 }, // U+0470 + { 19, 23, 330, 1, 17, 55, 20570 }, // U+0471 + { 18, 20, 306, 1, 19, 45, 20625 }, // U+0472 + { 15, 15, 242, 0, 14, 29, 20670 }, // U+0473 + { 19, 19, 300, 0, 19, 46, 20699 }, // U+0474 + { 15, 14, 244, 0, 14, 27, 20745 }, // U+0475 + { 19, 24, 300, 0, 24, 57, 20772 }, // U+0476 + { 15, 19, 244, 0, 19, 36, 20829 }, // U+0477 + { 33, 25, 543, 1, 19, 104, 20865 }, // U+0478 + { 30, 20, 472, 0, 14, 75, 20969 }, // U+0479 + { 16, 19, 286, 1, 19, 38, 21044 }, // U+0480 + { 14, 19, 232, 0, 14, 34, 21082 }, // U+0481 + { 14, 17, 265, 1, 17, 30, 21116 }, // U+0482 + { 10, 5, 0, -13, 19, 7, 21146 }, // U+0483 + { 11, 4, 0, -13, 19, 6, 21153 }, // U+0484 + { 4, 6, 0, -9, 21, 3, 21159 }, // U+0485 + { 5, 6, 0, -9, 21, 4, 21162 }, // U+0486 + { 12, 4, 0, -14, 19, 6, 21166 }, // U+0487 + { 25, 22, 0, -20, 18, 69, 21172 }, // U+0488 + { 24, 25, 0, -19, 19, 75, 21241 }, // U+0489 + { 19, 28, 302, 1, 24, 67, 21316 }, // U+048A + { 16, 23, 247, 1, 19, 46, 21383 }, // U+048B + { 17, 19, 283, 0, 19, 41, 21429 }, // U+048C + { 15, 17, 254, 0, 17, 32, 21470 }, // U+048D + { 15, 19, 261, 1, 19, 36, 21502 }, // U+048E + { 14, 20, 243, 1, 14, 35, 21538 }, // U+048F + { 13, 22, 230, 1, 22, 36, 21573 }, // U+0490 + { 10, 17, 180, 1, 17, 22, 21609 }, // U+0491 + { 15, 19, 230, -1, 19, 36, 21631 }, // U+0492 + { 12, 14, 180, -1, 14, 21, 21667 }, // U+0493 + { 15, 21, 264, 1, 19, 40, 21688 }, // U+0494 + { 13, 19, 227, 1, 14, 31, 21728 }, // U+0495 + { 28, 23, 436, 0, 19, 81, 21759 }, // U+0496 + { 23, 18, 356, 0, 14, 52, 21840 }, // U+0497 + { 14, 25, 256, 1, 19, 44, 21892 }, // U+0498 + { 13, 20, 205, 0, 14, 33, 21936 }, // U+0499 + { 18, 23, 302, 1, 19, 52, 21969 }, // U+049A + { 15, 18, 245, 1, 14, 34, 22021 }, // U+049B + { 17, 19, 285, 1, 19, 41, 22055 }, // U+049C + { 15, 14, 251, 1, 14, 27, 22096 }, // U+049D + { 22, 19, 351, 0, 19, 53, 22123 }, // U+04A0 + { 17, 14, 267, 0, 14, 30, 22176 }, // U+04A1 + { 18, 23, 307, 1, 19, 52, 22206 }, // U+04A2 + { 15, 18, 257, 1, 14, 34, 22258 }, // U+04A3 + { 24, 19, 399, 1, 19, 57, 22292 }, // U+04A4 + { 17, 14, 284, 1, 14, 30, 22349 }, // U+04A5 + { 27, 21, 448, 1, 19, 71, 22379 }, // U+04A6 + { 22, 18, 375, 1, 14, 50, 22450 }, // U+04A7 + { 25, 20, 417, 1, 19, 63, 22500 }, // U+04A8 + { 21, 15, 334, 0, 14, 40, 22563 }, // U+04A9 + { 17, 25, 295, 1, 19, 54, 22603 }, // U+04AA + { 14, 20, 232, 0, 14, 35, 22657 }, // U+04AB + { 16, 23, 264, 0, 19, 46, 22692 }, // U+04AC + { 13, 18, 204, 0, 14, 30, 22738 }, // U+04AD + { 18, 19, 289, 0, 19, 43, 22768 }, // U+04AE + { 15, 20, 236, 0, 14, 38, 22811 }, // U+04AF + { 18, 19, 289, 0, 19, 43, 22849 }, // U+04B0 + { 15, 20, 236, 0, 14, 38, 22892 }, // U+04B1 + { 19, 23, 306, 0, 19, 55, 22930 }, // U+04B2 + { 16, 18, 242, 0, 14, 36, 22985 }, // U+04B3 + { 24, 23, 379, 0, 19, 69, 23021 }, // U+04B4 + { 18, 18, 283, 0, 14, 41, 23090 }, // U+04B5 + { 17, 23, 292, 1, 19, 49, 23131 }, // U+04B6 + { 15, 18, 249, 1, 14, 34, 23180 }, // U+04B7 + { 15, 19, 281, 1, 19, 36, 23214 }, // U+04B8 + { 13, 14, 238, 1, 14, 23, 23250 }, // U+04B9 + { 16, 19, 281, 1, 19, 38, 23273 }, // U+04BA + { 13, 19, 246, 1, 19, 31, 23311 }, // U+04BB + { 22, 20, 355, 0, 19, 55, 23342 }, // U+04BC + { 18, 15, 299, 0, 14, 34, 23397 }, // U+04BD + { 22, 23, 355, 0, 19, 64, 23431 }, // U+04BE + { 18, 20, 299, 0, 14, 45, 23495 }, // U+04BF + { 5, 19, 110, 1, 19, 12, 23540 }, // U+04C0 + { 26, 24, 422, 0, 24, 78, 23552 }, // U+04C1 + { 21, 19, 343, 0, 19, 50, 23630 }, // U+04C2 + { 16, 24, 276, 1, 19, 48, 23680 }, // U+04C3 + { 14, 18, 246, 1, 14, 32, 23728 }, // U+04C4 + { 20, 23, 297, 0, 19, 58, 23760 }, // U+04C5 + { 16, 18, 240, 0, 14, 36, 23818 }, // U+04C6 + { 16, 25, 296, 1, 19, 50, 23854 }, // U+04C7 + { 13, 20, 246, 1, 14, 33, 23904 }, // U+04C8 + { 19, 23, 296, 1, 19, 55, 23937 }, // U+04C9 + { 16, 18, 246, 1, 14, 36, 23992 }, // U+04CA + { 17, 23, 292, 1, 19, 49, 24028 }, // U+04CB + { 15, 18, 249, 1, 14, 34, 24077 }, // U+04CC + { 23, 23, 369, 1, 19, 67, 24111 }, // U+04CD + { 20, 18, 322, 1, 14, 45, 24178 }, // U+04CE + { 5, 19, 106, 1, 19, 12, 24223 }, // U+04CF + { 18, 24, 296, 0, 24, 54, 24235 }, // U+04D0 + { 13, 20, 230, 0, 19, 33, 24289 }, // U+04D1 + { 18, 24, 296, 0, 24, 54, 24322 }, // U+04D2 + { 13, 20, 230, 0, 19, 33, 24376 }, // U+04D3 + { 25, 19, 409, 0, 19, 60, 24409 }, // U+04D4 + { 22, 15, 362, 0, 14, 42, 24469 }, // U+04D5 + { 13, 24, 243, 1, 24, 39, 24511 }, // U+04D6 + { 14, 20, 235, 0, 19, 35, 24550 }, // U+04D7 + { 17, 20, 298, 1, 19, 43, 24585 }, // U+04D8 + { 14, 15, 235, 0, 14, 27, 24628 }, // U+04D9 + { 17, 25, 298, 1, 24, 54, 24655 }, // U+04DA + { 14, 20, 235, 0, 19, 35, 24709 }, // U+04DB + { 26, 24, 422, 0, 24, 78, 24744 }, // U+04DC + { 21, 19, 343, 0, 19, 50, 24822 }, // U+04DD + { 14, 25, 256, 1, 24, 44, 24872 }, // U+04DE + { 13, 20, 205, 0, 19, 33, 24916 }, // U+04DF + { 14, 20, 256, 1, 19, 35, 24949 }, // U+04E0 + { 14, 20, 256, 1, 14, 35, 24984 }, // U+04E1 + { 17, 24, 302, 1, 24, 51, 25019 }, // U+04E2 + { 13, 19, 247, 1, 19, 31, 25070 }, // U+04E3 + { 17, 24, 302, 1, 24, 51, 25101 }, // U+04E4 + { 13, 19, 247, 1, 19, 31, 25152 }, // U+04E5 + { 18, 25, 306, 1, 24, 57, 25183 }, // U+04E6 + { 15, 20, 242, 0, 19, 38, 25240 }, // U+04E7 + { 18, 20, 306, 1, 19, 45, 25278 }, // U+04E8 + { 15, 15, 242, 0, 14, 29, 25323 }, // U+04E9 + { 18, 25, 306, 1, 24, 57, 25352 }, // U+04EA + { 15, 20, 242, 0, 19, 38, 25409 }, // U+04EB + { 18, 25, 295, 0, 24, 57, 25447 }, // U+04EC + { 14, 20, 233, 0, 19, 35, 25504 }, // U+04ED + { 17, 24, 274, 0, 24, 51, 25539 }, // U+04EE + { 15, 25, 237, 0, 19, 47, 25590 }, // U+04EF + { 17, 24, 274, 0, 24, 51, 25637 }, // U+04F0 + { 15, 25, 237, 0, 19, 47, 25688 }, // U+04F1 + { 17, 24, 274, 0, 24, 51, 25735 }, // U+04F2 + { 15, 25, 237, 0, 19, 47, 25786 }, // U+04F3 + { 15, 24, 281, 1, 24, 45, 25833 }, // U+04F4 + { 13, 19, 238, 1, 19, 31, 25878 }, // U+04F5 + { 13, 23, 230, 1, 19, 38, 25909 }, // U+04F6 + { 10, 18, 180, 1, 14, 23, 25947 }, // U+04F7 + { 21, 24, 363, 1, 24, 63, 25970 }, // U+04F8 + { 18, 19, 317, 1, 19, 43, 26033 }, // U+04F9 + { 15, 25, 246, 0, 19, 47, 26076 }, // U+04FA + { 12, 20, 180, -1, 14, 30, 26123 }, // U+04FB + { 19, 25, 313, 0, 19, 60, 26153 }, // U+04FC + { 13, 20, 214, 0, 14, 33, 26213 }, // U+04FD + { 18, 19, 292, 0, 19, 43, 26246 }, // U+04FE + { 14, 14, 228, 0, 14, 25, 26289 }, // U+04FF + { 15, 15, 272, 1, 15, 29, 26314 }, // U+05D0 + { 14, 15, 226, 0, 15, 27, 26343 }, // U+05D1 + { 10, 15, 181, 0, 15, 19, 26370 }, // U+05D2 + { 14, 15, 219, 0, 15, 27, 26389 }, // U+05D3 + { 14, 15, 262, 1, 15, 27, 26416 }, // U+05D4 + { 5, 15, 118, 1, 15, 10, 26443 }, // U+05D5 + { 10, 15, 162, 0, 15, 19, 26453 }, // U+05D6 + { 15, 15, 272, 1, 15, 29, 26472 }, // U+05D7 + { 15, 15, 264, 1, 15, 29, 26501 }, // U+05D8 + { 5, 9, 113, 1, 15, 6, 26530 }, // U+05D9 + { 12, 20, 211, 0, 15, 30, 26536 }, // U+05DA + { 13, 15, 217, 0, 15, 25, 26566 }, // U+05DB + { 13, 19, 206, 0, 19, 31, 26591 }, // U+05DC + { 15, 15, 272, 1, 15, 29, 26622 }, // U+05DD + { 15, 15, 276, 1, 15, 29, 26651 }, // U+05DE + { 5, 20, 118, 1, 15, 13, 26680 }, // U+05DF + { 9, 15, 160, 0, 15, 17, 26693 }, // U+05E0 + { 15, 15, 265, 1, 15, 29, 26710 }, // U+05E1 + { 15, 16, 246, 0, 15, 30, 26739 }, // U+05E2 + { 14, 20, 259, 1, 15, 35, 26769 }, // U+05E3 + { 15, 15, 262, 1, 15, 29, 26804 }, // U+05E4 + { 14, 20, 233, 0, 15, 35, 26833 }, // U+05E5 + { 14, 15, 246, 0, 15, 27, 26868 }, // U+05E6 + { 15, 20, 255, 1, 15, 38, 26895 }, // U+05E7 + { 12, 15, 214, 0, 15, 23, 26933 }, // U+05E8 + { 18, 15, 321, 1, 15, 34, 26956 }, // U+05E9 + { 14, 15, 266, 1, 15, 27, 26990 }, // U+05EA + { 5, 8, 129, 1, 7, 5, 27017 }, // U+060C + { 7, 19, 136, 0, 18, 17, 27022 }, // U+061B + { 13, 20, 205, 0, 19, 33, 27039 }, // U+061F + { 11, 13, 188, 0, 11, 18, 27072 }, // U+0621 + { 10, 5, 116, 0, 4, 7, 27090 }, // U+0640 + { 6, 7, 229, 4, 11, 6, 27097 }, // U+0660 + { 6, 19, 229, 4, 18, 15, 27103 }, // U+0661 + { 13, 19, 229, 1, 18, 31, 27118 }, // U+0662 + { 14, 19, 229, 0, 18, 34, 27149 }, // U+0663 + { 12, 19, 229, 1, 18, 29, 27183 }, // U+0664 + { 12, 14, 229, 1, 14, 21, 27212 }, // U+0665 + { 13, 19, 229, 1, 18, 31, 27233 }, // U+0666 + { 15, 19, 229, 0, 18, 36, 27264 }, // U+0667 + { 15, 19, 229, 0, 18, 36, 27300 }, // U+0668 + { 12, 18, 229, 1, 18, 27, 27336 }, // U+0669 + { 18, 16, 296, 0, 10, 36, 27363 }, // U+06BA + { 7, 3, 130, 1, 3, 3, 27399 }, // U+06D4 + { 6, 7, 229, 4, 11, 6, 27402 }, // U+06F0 + { 6, 19, 229, 4, 18, 15, 27408 }, // U+06F1 + { 13, 19, 229, 1, 18, 31, 27423 }, // U+06F2 + { 14, 19, 229, 0, 18, 34, 27454 }, // U+06F3 + { 13, 19, 229, 1, 18, 31, 27488 }, // U+06F4 + { 14, 17, 229, 0, 17, 30, 27519 }, // U+06F5 + { 12, 18, 229, 1, 17, 27, 27549 }, // U+06F6 + { 15, 19, 229, 0, 18, 36, 27576 }, // U+06F7 + { 15, 19, 229, 0, 18, 36, 27612 }, // U+06F8 + { 12, 18, 229, 1, 18, 27, 27648 }, // U+06F9 + { 18, 25, 296, 0, 19, 57, 27675 }, // U+1EA0 + { 13, 20, 230, 0, 14, 33, 27732 }, // U+1EA1 + { 18, 24, 296, 0, 24, 54, 27765 }, // U+1EA2 + { 13, 20, 230, 0, 19, 33, 27819 }, // U+1EA3 + { 18, 28, 296, 0, 28, 63, 27852 }, // U+1EA4 + { 14, 24, 230, 0, 23, 42, 27915 }, // U+1EA5 + { 18, 28, 296, 0, 28, 63, 27957 }, // U+1EA6 + { 13, 24, 230, 0, 23, 39, 28020 }, // U+1EA7 + { 18, 28, 296, 0, 28, 63, 28059 }, // U+1EA8 + { 13, 24, 230, 0, 23, 39, 28122 }, // U+1EA9 + { 18, 28, 296, 0, 28, 63, 28161 }, // U+1EAA + { 13, 24, 230, 0, 23, 39, 28224 }, // U+1EAB + { 18, 30, 296, 0, 24, 68, 28263 }, // U+1EAC + { 13, 25, 230, 0, 19, 41, 28331 }, // U+1EAD + { 18, 28, 296, 0, 28, 63, 28372 }, // U+1EAE + { 13, 24, 230, 0, 23, 39, 28435 }, // U+1EAF + { 18, 28, 296, 0, 28, 63, 28474 }, // U+1EB0 + { 13, 24, 230, 0, 23, 39, 28537 }, // U+1EB1 + { 18, 28, 296, 0, 28, 63, 28576 }, // U+1EB2 + { 13, 24, 230, 0, 23, 39, 28639 }, // U+1EB3 + { 18, 28, 296, 0, 28, 63, 28678 }, // U+1EB4 + { 13, 24, 230, 0, 23, 39, 28741 }, // U+1EB5 + { 18, 30, 296, 0, 24, 68, 28780 }, // U+1EB6 + { 13, 25, 230, 0, 19, 41, 28848 }, // U+1EB7 + { 13, 25, 243, 1, 19, 41, 28889 }, // U+1EB8 + { 14, 20, 235, 0, 14, 35, 28930 }, // U+1EB9 + { 13, 24, 243, 1, 24, 39, 28965 }, // U+1EBA + { 14, 20, 235, 0, 19, 35, 29004 }, // U+1EBB + { 13, 24, 243, 1, 24, 39, 29039 }, // U+1EBC + { 14, 20, 235, 0, 19, 35, 29078 }, // U+1EBD + { 14, 28, 243, 1, 28, 49, 29113 }, // U+1EBE + { 14, 24, 235, 0, 23, 42, 29162 }, // U+1EBF + { 13, 28, 243, 1, 28, 46, 29204 }, // U+1EC0 + { 14, 24, 235, 0, 23, 42, 29250 }, // U+1EC1 + { 13, 28, 243, 1, 28, 46, 29292 }, // U+1EC2 + { 14, 24, 235, 0, 23, 42, 29338 }, // U+1EC3 + { 13, 28, 243, 1, 28, 46, 29380 }, // U+1EC4 + { 14, 24, 235, 0, 23, 42, 29426 }, // U+1EC5 + { 13, 30, 243, 1, 24, 49, 29468 }, // U+1EC6 + { 14, 25, 235, 0, 19, 44, 29517 }, // U+1EC7 + { 5, 24, 110, 1, 24, 15, 29561 }, // U+1EC8 + { 6, 19, 106, 0, 19, 15, 29576 }, // U+1EC9 + { 5, 25, 110, 1, 19, 16, 29591 }, // U+1ECA + { 5, 25, 106, 1, 19, 16, 29607 }, // U+1ECB + { 18, 25, 306, 1, 19, 57, 29623 }, // U+1ECC + { 15, 20, 242, 0, 14, 38, 29680 }, // U+1ECD + { 18, 25, 306, 1, 24, 57, 29718 }, // U+1ECE + { 15, 20, 242, 0, 19, 38, 29775 }, // U+1ECF + { 18, 29, 306, 1, 28, 66, 29813 }, // U+1ED0 + { 15, 24, 242, 0, 23, 45, 29879 }, // U+1ED1 + { 18, 29, 306, 1, 28, 66, 29924 }, // U+1ED2 + { 15, 24, 242, 0, 23, 45, 29990 }, // U+1ED3 + { 18, 29, 306, 1, 28, 66, 30035 }, // U+1ED4 + { 15, 24, 242, 0, 23, 45, 30101 }, // U+1ED5 + { 18, 29, 306, 1, 28, 66, 30146 }, // U+1ED6 + { 15, 24, 242, 0, 23, 45, 30212 }, // U+1ED7 + { 18, 30, 306, 1, 24, 68, 30257 }, // U+1ED8 + { 15, 25, 242, 0, 19, 47, 30325 }, // U+1ED9 + { 18, 25, 309, 1, 24, 57, 30372 }, // U+1EDA + { 15, 20, 242, 0, 19, 38, 30429 }, // U+1EDB + { 18, 25, 309, 1, 24, 57, 30467 }, // U+1EDC + { 15, 20, 242, 0, 19, 38, 30524 }, // U+1EDD + { 18, 25, 309, 1, 24, 57, 30562 }, // U+1EDE + { 15, 20, 242, 0, 19, 38, 30619 }, // U+1EDF + { 18, 25, 309, 1, 24, 57, 30657 }, // U+1EE0 + { 15, 20, 242, 0, 19, 38, 30714 }, // U+1EE1 + { 18, 27, 309, 1, 21, 61, 30752 }, // U+1EE2 + { 15, 23, 242, 0, 17, 44, 30813 }, // U+1EE3 + { 16, 25, 290, 1, 19, 50, 30857 }, // U+1EE4 + { 13, 20, 246, 1, 14, 33, 30907 }, // U+1EE5 + { 16, 25, 290, 1, 24, 50, 30940 }, // U+1EE6 + { 13, 20, 246, 1, 19, 33, 30990 }, // U+1EE7 + { 18, 25, 290, 1, 24, 57, 31023 }, // U+1EE8 + { 15, 20, 263, 1, 19, 38, 31080 }, // U+1EE9 + { 18, 25, 290, 1, 24, 57, 31118 }, // U+1EEA + { 15, 20, 263, 1, 19, 38, 31175 }, // U+1EEB + { 18, 25, 290, 1, 24, 57, 31213 }, // U+1EEC + { 15, 20, 263, 1, 19, 38, 31270 }, // U+1EED + { 18, 25, 290, 1, 24, 57, 31308 }, // U+1EEE + { 15, 20, 263, 1, 19, 38, 31365 }, // U+1EEF + { 18, 27, 290, 1, 21, 61, 31403 }, // U+1EF0 + { 15, 23, 263, 1, 17, 44, 31464 }, // U+1EF1 + { 18, 24, 289, 0, 24, 54, 31508 }, // U+1EF2 + { 15, 25, 237, 0, 19, 47, 31562 }, // U+1EF3 + { 18, 25, 289, 0, 19, 57, 31609 }, // U+1EF4 + { 15, 20, 237, 0, 14, 38, 31666 }, // U+1EF5 + { 18, 24, 289, 0, 24, 54, 31704 }, // U+1EF6 + { 15, 25, 237, 0, 19, 47, 31758 }, // U+1EF7 + { 18, 24, 289, 0, 24, 54, 31805 }, // U+1EF8 + { 15, 25, 237, 0, 19, 47, 31859 }, // U+1EF9 + { 0, 0, 227, 0, 0, 0, 31906 }, // U+2000 + { 0, 0, 451, 0, 0, 0, 31906 }, // U+2001 + { 0, 0, 200, 0, 0, 0, 31906 }, // U+2002 + { 0, 0, 400, 0, 0, 0, 31906 }, // U+2003 + { 0, 0, 134, 0, 0, 0, 31906 }, // U+2004 + { 0, 0, 100, 0, 0, 0, 31906 }, // U+2005 + { 0, 0, 66, 0, 0, 0, 31906 }, // U+2006 + { 0, 0, 258, 0, 0, 0, 31906 }, // U+2007 + { 0, 0, 126, 0, 0, 0, 31906 }, // U+2008 + { 0, 0, 61, 0, 0, 0, 31906 }, // U+2009 + { 0, 0, 29, 0, 0, 0, 31906 }, // U+200A + { 0, 0, 0, 0, 0, 0, 31906 }, // U+200B + { 0, 0, 0, 0, 0, 0, 31906 }, // U+200C + { 0, 0, 0, 0, 0, 0, 31906 }, // U+200D + { 0, 0, 0, 0, 0, 0, 31906 }, // U+200E + { 0, 0, 0, 0, 0, 0, 31906 }, // U+200F + { 9, 4, 179, 1, 9, 5, 31906 }, // U+2010 + { 9, 4, 179, 1, 9, 5, 31911 }, // U+2011 + { 16, 4, 258, 0, 10, 8, 31916 }, // U+2012 + { 13, 4, 200, 0, 9, 7, 31924 }, // U+2013 + { 25, 4, 400, 0, 9, 13, 31931 }, // U+2014 + { 25, 4, 400, 0, 9, 13, 31944 }, // U+2015 + { 10, 31, 223, 2, 25, 39, 31957 }, // U+2016 + { 12, 6, 188, 0, 0, 9, 31996 }, // U+2017 + { 6, 9, 117, 1, 19, 7, 32005 }, // U+2018 + { 6, 9, 117, 1, 19, 7, 32012 }, // U+2019 + { 5, 9, 108, 1, 4, 6, 32019 }, // U+201A + { 6, 9, 117, 0, 19, 7, 32025 }, // U+201B + { 12, 9, 206, 1, 19, 14, 32032 }, // U+201C + { 11, 9, 203, 1, 19, 13, 32046 }, // U+201D + { 11, 9, 195, 1, 4, 13, 32059 }, // U+201E + { 11, 9, 203, 0, 19, 13, 32072 }, // U+201F + { 12, 15, 232, 1, 19, 23, 32085 }, // U+2020 + { 12, 19, 232, 1, 19, 29, 32108 }, // U+2021 + { 9, 9, 186, 1, 12, 11, 32137 }, // U+2022 + { 7, 9, 186, 3, 12, 8, 32148 }, // U+2023 + { 6, 5, 126, 1, 4, 4, 32156 }, // U+2024 + { 13, 5, 251, 1, 4, 9, 32160 }, // U+2025 + { 21, 5, 377, 1, 4, 14, 32169 }, // U+2026 + { 6, 5, 126, 1, 11, 4, 32183 }, // U+2027 + { 0, 0, 0, 0, 0, 0, 32187 }, // U+202A + { 0, 0, 0, 0, 0, 0, 32187 }, // U+202B + { 0, 0, 0, 0, 0, 0, 32187 }, // U+202C + { 0, 0, 0, 0, 0, 0, 32187 }, // U+202D + { 0, 0, 0, 0, 0, 0, 32187 }, // U+202E + { 0, 0, 61, 0, 0, 0, 32187 }, // U+202F + { 31, 20, 548, 2, 19, 78, 32187 }, // U+2030 + { 40, 20, 697, 2, 19, 100, 32265 }, // U+2031 + { 5, 8, 108, 1, 19, 5, 32365 }, // U+2032 + { 12, 8, 215, 1, 19, 12, 32370 }, // U+2033 + { 18, 8, 322, 1, 19, 18, 32382 }, // U+2034 + { 5, 8, 108, 1, 19, 5, 32400 }, // U+2035 + { 12, 8, 215, 1, 19, 12, 32405 }, // U+2036 + { 18, 8, 322, 1, 19, 18, 32417 }, // U+2037 + { 12, 6, 217, 1, -1, 9, 32435 }, // U+2038 + { 9, 13, 169, 1, 14, 15, 32444 }, // U+2039 + { 10, 13, 169, 0, 14, 17, 32459 }, // U+203A + { 17, 16, 321, 2, 16, 34, 32476 }, // U+203B + { 14, 20, 254, 1, 19, 35, 32510 }, // U+203C + { 13, 20, 222, 0, 19, 33, 32545 }, // U+203D + { 13, 3, 197, 0, 21, 5, 32578 }, // U+203E + { 21, 5, 338, 0, -1, 14, 32583 }, // U+203F + { 21, 5, 338, 0, 19, 14, 32597 }, // U+2040 + { 11, 14, 189, 0, 8, 20, 32611 }, // U+2041 + { 17, 16, 303, 1, 17, 34, 32631 }, // U+2042 + { 8, 4, 186, 2, 9, 4, 32665 }, // U+2043 + { 16, 19, 82, -5, 19, 38, 32669 }, // U+2044 + { 7, 24, 147, 2, 20, 21, 32707 }, // U+2045 + { 7, 24, 147, 1, 20, 21, 32728 }, // U+2046 + { 27, 20, 449, 0, 19, 68, 32749 }, // U+2047 + { 21, 20, 352, 0, 19, 53, 32817 }, // U+2048 + { 20, 20, 352, 1, 19, 50, 32870 }, // U+2049 + { 11, 20, 184, 0, 14, 28, 32920 }, // U+204A + { 13, 19, 237, 1, 19, 31, 32948 }, // U+204B + { 13, 11, 241, 1, 13, 18, 32979 }, // U+204C + { 13, 11, 241, 1, 13, 18, 32997 }, // U+204D + { 11, 12, 221, 1, 9, 17, 33015 }, // U+204E + { 6, 18, 129, 1, 13, 14, 33032 }, // U+204F + { 21, 25, 337, 0, 19, 66, 33046 }, // U+2050 + { 11, 24, 228, 3, 19, 33, 33112 }, // U+2051 + { 18, 19, 318, 1, 19, 43, 33145 }, // U+2052 + { 17, 7, 302, 1, 12, 15, 33188 }, // U+2053 + { 21, 5, 338, 0, -1, 14, 33203 }, // U+2054 + { 14, 13, 284, 2, 13, 23, 33217 }, // U+2055 + { 25, 8, 429, 1, 19, 25, 33240 }, // U+2057 + { 0, 0, 116, 0, 0, 0, 33265 }, // U+205F + { 0, 0, 0, 0, 0, 0, 33265 }, // U+2060 + { 0, 0, 0, 0, 0, 0, 33265 }, // U+2061 + { 0, 0, 0, 0, 0, 0, 33265 }, // U+2062 + { 0, 0, 0, 0, 0, 0, 33265 }, // U+2063 + { 0, 0, 0, 0, 0, 0, 33265 }, // U+2064 + { 0, 0, 0, 0, 0, 0, 33265 }, // U+2066 + { 0, 0, 0, 0, 0, 0, 33265 }, // U+2067 + { 0, 0, 0, 0, 0, 0, 33265 }, // U+2068 + { 0, 0, 0, 0, 0, 0, 33265 }, // U+2069 + { 0, 0, 0, 0, 0, 0, 33265 }, // U+206A + { 0, 0, 0, 0, 0, 0, 33265 }, // U+206B + { 0, 0, 0, 0, 0, 0, 33265 }, // U+206C + { 0, 0, 0, 0, 0, 0, 33265 }, // U+206D + { 0, 0, 0, 0, 0, 0, 33265 }, // U+206E + { 0, 0, 0, 0, 0, 0, 33265 }, // U+206F + { 10, 12, 192, 1, 22, 15, 33265 }, // U+2070 + { 4, 12, 86, 1, 22, 6, 33280 }, // U+2071 + { 10, 11, 189, 1, 21, 14, 33286 }, // U+2074 + { 10, 11, 182, 1, 21, 14, 33300 }, // U+2075 + { 10, 12, 184, 1, 22, 15, 33314 }, // U+2076 + { 10, 11, 164, 0, 21, 14, 33329 }, // U+2077 + { 10, 12, 186, 1, 22, 15, 33343 }, // U+2078 + { 10, 12, 184, 1, 22, 15, 33358 }, // U+2079 + { 9, 9, 184, 1, 20, 11, 33373 }, // U+207A + { 9, 3, 184, 1, 17, 4, 33384 }, // U+207B + { 9, 7, 184, 1, 19, 8, 33388 }, // U+207C + { 6, 13, 111, 1, 22, 10, 33396 }, // U+207D + { 6, 13, 111, 0, 22, 10, 33406 }, // U+207E + { 9, 9, 178, 1, 19, 11, 33416 }, // U+207F + { 10, 12, 192, 1, 7, 15, 33427 }, // U+2080 + { 6, 11, 128, 1, 6, 9, 33442 }, // U+2081 + { 9, 12, 178, 1, 7, 14, 33451 }, // U+2082 + { 10, 12, 183, 1, 7, 15, 33465 }, // U+2083 + { 10, 11, 189, 1, 6, 14, 33480 }, // U+2084 + { 10, 11, 182, 1, 6, 14, 33494 }, // U+2085 + { 10, 12, 184, 1, 7, 15, 33508 }, // U+2086 + { 10, 11, 164, 0, 6, 14, 33523 }, // U+2087 + { 10, 12, 186, 1, 7, 15, 33537 }, // U+2088 + { 10, 12, 184, 1, 7, 15, 33552 }, // U+2089 + { 9, 9, 184, 1, 5, 11, 33567 }, // U+208A + { 9, 3, 184, 1, 2, 4, 33578 }, // U+208B + { 9, 7, 184, 1, 4, 8, 33582 }, // U+208C + { 6, 13, 111, 1, 7, 10, 33590 }, // U+208D + { 6, 13, 111, 0, 7, 10, 33600 }, // U+208E + { 9, 9, 164, 0, 4, 11, 33610 }, // U+2090 + { 10, 9, 168, 0, 4, 12, 33621 }, // U+2091 + { 10, 9, 173, 0, 4, 12, 33633 }, // U+2092 + { 10, 9, 167, 0, 4, 12, 33645 }, // U+2093 + { 10, 11, 190, 1, 5, 14, 33657 }, // U+2094 + { 9, 11, 178, 1, 6, 13, 33671 }, // U+2095 + { 9, 11, 166, 1, 6, 13, 33684 }, // U+2096 + { 4, 11, 86, 1, 6, 6, 33697 }, // U+2097 + { 14, 9, 253, 1, 4, 16, 33703 }, // U+2098 + { 9, 9, 178, 1, 4, 11, 33719 }, // U+2099 + { 10, 12, 178, 1, 4, 15, 33730 }, // U+209A + { 10, 9, 161, 0, 4, 12, 33745 }, // U+209B + { 7, 11, 109, 0, 6, 10, 33757 }, // U+209C + { 17, 20, 302, 1, 19, 43, 33767 }, // U+20A0 + { 17, 24, 295, 1, 21, 51, 33810 }, // U+20A1 + { 17, 20, 288, 1, 19, 43, 33861 }, // U+20A2 + { 16, 19, 245, -1, 19, 38, 33904 }, // U+20A3 + { 14, 19, 253, 1, 19, 34, 33942 }, // U+20A4 + { 20, 18, 343, 1, 16, 45, 33976 }, // U+20A5 + { 19, 19, 301, 0, 19, 46, 34021 }, // U+20A6 + { 24, 20, 399, 1, 19, 60, 34067 }, // U+20A7 + { 28, 20, 466, 1, 19, 70, 34127 }, // U+20A8 + { 25, 19, 414, 0, 19, 60, 34197 }, // U+20A9 + { 21, 14, 366, 1, 14, 37, 34257 }, // U+20AA + { 16, 24, 249, 0, 19, 48, 34294 }, // U+20AB + { 16, 20, 272, 0, 19, 40, 34342 }, // U+20AC + { 20, 19, 318, 0, 19, 48, 34382 }, // U+20AD + { 16, 19, 264, 0, 19, 38, 34430 }, // U+20AE + { 36, 27, 580, 0, 21, 122, 34468 }, // U+20AF + { 18, 19, 283, 0, 19, 43, 34590 }, // U+20B1 + { 17, 24, 299, 1, 21, 51, 34633 }, // U+20B2 + { 18, 19, 296, 0, 19, 43, 34684 }, // U+20B3 + { 15, 20, 293, 2, 19, 38, 34727 }, // U+20B4 + { 17, 24, 295, 1, 21, 51, 34765 }, // U+20B5 + { 16, 19, 265, 0, 19, 38, 34816 }, // U+20B8 + { 14, 19, 226, 0, 19, 34, 34854 }, // U+20B9 + { 14, 20, 256, 1, 19, 35, 34888 }, // U+20BA + { 17, 14, 302, 1, 14, 30, 34923 }, // U+20BC + { 17, 19, 294, 1, 19, 41, 34953 }, // U+20BD + { 17, 19, 302, 1, 19, 41, 34994 }, // U+20BE + { 15, 24, 263, 1, 21, 45, 35035 }, // U+20BF + { 19, 18, 380, 2, 17, 43, 35080 }, // U+2190 + { 19, 19, 360, 2, 19, 46, 35123 }, // U+2191 + { 19, 18, 380, 3, 17, 43, 35169 }, // U+2192 + { 19, 20, 360, 2, 19, 48, 35212 }, // U+2193 + { 29, 18, 534, 2, 17, 66, 35260 }, // U+2194 + { 17, 30, 342, 2, 23, 64, 35326 }, // U+2195 + { 15, 15, 307, 2, 14, 29, 35390 }, // U+2196 + { 15, 15, 307, 2, 14, 29, 35419 }, // U+2197 + { 15, 14, 307, 2, 14, 27, 35448 }, // U+2198 + { 15, 14, 307, 2, 14, 27, 35475 }, // U+2199 + { 25, 20, 461, 2, 19, 63, 35502 }, // U+21A9 + { 25, 20, 461, 2, 19, 63, 35565 }, // U+21AA + { 21, 19, 403, 2, 19, 50, 35628 }, // U+21B0 + { 21, 19, 403, 2, 19, 50, 35678 }, // U+21B1 + { 14, 17, 360, 4, 17, 30, 35728 }, // U+21B2 + { 21, 18, 403, 2, 19, 48, 35758 }, // U+21B3 + { 18, 20, 356, 2, 19, 45, 35806 }, // U+21B4 + { 21, 18, 403, 2, 19, 48, 35851 }, // U+21B5 + { 22, 16, 360, 0, 17, 44, 35899 }, // U+21B6 + { 22, 16, 360, 0, 17, 44, 35943 }, // U+21B7 + { 19, 23, 380, 2, 23, 55, 35987 }, // U+21BA + { 19, 23, 380, 2, 23, 55, 36042 }, // U+21BB + { 19, 21, 360, 2, 19, 50, 36097 }, // U+21C4 + { 19, 21, 360, 2, 19, 50, 36147 }, // U+21C6 + { 19, 18, 380, 2, 17, 43, 36197 }, // U+21D0 + { 19, 18, 380, 3, 17, 43, 36240 }, // U+21D2 + { 29, 18, 534, 2, 17, 66, 36283 }, // U+21D4 + { 13, 19, 252, 1, 19, 31, 36349 }, // U+21DE + { 13, 20, 252, 1, 19, 33, 36380 }, // U+21DF + { 22, 13, 418, 2, 16, 36, 36413 }, // U+21E4 + { 22, 13, 418, 2, 16, 36, 36449 }, // U+21E5 + { 25, 20, 438, 1, 20, 63, 36485 }, // U+21E7 + { 25, 25, 438, 1, 20, 79, 36548 }, // U+21EA + { 14, 20, 266, 1, 19, 35, 36627 }, // U+2202 + { 15, 16, 274, 1, 15, 30, 36662 }, // U+2205 + { 18, 19, 293, 0, 19, 43, 36692 }, // U+2206 + { 15, 23, 304, 2, 19, 44, 36735 }, // U+220F + { 16, 23, 294, 1, 19, 46, 36779 }, // U+2211 + { 13, 4, 269, 2, 9, 7, 36825 }, // U+2212 + { 17, 17, 59, -7, 17, 37, 36832 }, // U+2215 + { 15, 19, 266, 1, 19, 36, 36869 }, // U+221A + { 24, 12, 390, 0, 14, 36, 36905 }, // U+221E + { 11, 26, 152, -1, 20, 36, 36941 }, // U+222B + { 6, 14, 126, 1, 16, 11, 36977 }, // U+2236 + { 14, 12, 269, 1, 13, 21, 36988 }, // U+2248 + { 13, 15, 269, 2, 15, 25, 37009 }, // U+2260 + { 13, 15, 269, 2, 15, 25, 37034 }, // U+2264 + { 13, 15, 269, 2, 15, 25, 37059 }, // U+2265 + { 35, 35, 556, 0, 27, 154, 37084 }, // U+2295 + { 35, 35, 556, 0, 27, 154, 37238 }, // U+2296 + { 35, 35, 556, 0, 27, 154, 37392 }, // U+2297 + { 35, 35, 556, 0, 27, 154, 37546 }, // U+2298 + { 15, 19, 259, 0, 19, 36, 37700 }, // U+FB01 + { 17, 19, 266, 0, 19, 41, 37736 }, // U+FB02 + { 25, 21, 410, 0, 12, 66, 37777 }, // U+FB56 + { 31, 19, 457, 0, 10, 74, 37843 }, // U+FB57 + { 8, 21, 144, 0, 12, 21, 37917 }, // U+FB58 + { 13, 18, 164, 0, 9, 30, 37938 }, // U+FB59 + { 25, 19, 410, 0, 18, 60, 37968 }, // U+FB66 + { 31, 19, 457, 0, 18, 74, 38028 }, // U+FB67 + { 8, 22, 127, 0, 21, 22, 38102 }, // U+FB68 + { 13, 19, 164, 0, 18, 31, 38124 }, // U+FB69 + { 17, 23, 275, 0, 13, 49, 38155 }, // U+FB7A + { 21, 23, 304, 0, 13, 61, 38204 }, // U+FB7B + { 16, 20, 252, 0, 11, 40, 38265 }, // U+FB7C + { 19, 20, 262, 0, 11, 48, 38305 }, // U+FB7D + { 12, 23, 207, 0, 22, 35, 38353 }, // U+FB88 + { 17, 23, 228, 0, 22, 49, 38388 }, // U+FB89 + { 11, 26, 164, -1, 20, 36, 38437 }, // U+FB8A + { 15, 26, 180, -1, 20, 49, 38473 }, // U+FB8B + { 11, 26, 164, -1, 20, 36, 38522 }, // U+FB8C + { 15, 26, 180, -1, 20, 49, 38558 }, // U+FB8D + { 23, 19, 364, 0, 18, 55, 38607 }, // U+FB8E + { 26, 19, 381, 0, 18, 62, 38662 }, // U+FB8F + { 12, 19, 182, 0, 18, 29, 38724 }, // U+FB90 + { 15, 19, 203, 0, 18, 36, 38753 }, // U+FB91 + { 23, 23, 364, 0, 22, 67, 38789 }, // U+FB92 + { 26, 23, 381, 0, 22, 75, 38856 }, // U+FB93 + { 13, 23, 182, -1, 22, 38, 38931 }, // U+FB94 + { 16, 23, 203, -1, 22, 46, 38969 }, // U+FB95 + { 18, 16, 296, 0, 10, 36, 39015 }, // U+FB9E + { 23, 15, 322, 0, 9, 44, 39051 }, // U+FB9F + { 11, 15, 188, 0, 14, 21, 39095 }, // U+FBA6 + { 17, 11, 234, 0, 10, 24, 39116 }, // U+FBA7 + { 7, 17, 127, 0, 12, 15, 39140 }, // U+FBA8 + { 15, 13, 206, 0, 6, 25, 39155 }, // U+FBA9 + { 19, 15, 308, 0, 14, 36, 39180 }, // U+FBAA + { 20, 18, 274, 0, 12, 45, 39216 }, // U+FBAB + { 16, 15, 256, 0, 14, 30, 39261 }, // U+FBAC + { 17, 18, 228, 0, 12, 39, 39291 }, // U+FBAD + { 23, 14, 377, 0, 13, 41, 39330 }, // U+FBAE + { 17, 12, 224, 0, 4, 26, 39371 }, // U+FBAF + { 23, 21, 377, 0, 20, 61, 39397 }, // U+FBB0 + { 17, 19, 224, 0, 11, 41, 39458 }, // U+FBB1 + { 21, 18, 328, 0, 12, 48, 39499 }, // U+FBFC + { 22, 13, 320, 0, 7, 36, 39547 }, // U+FBFD + { 8, 18, 144, 0, 12, 18, 39583 }, // U+FBFE + { 13, 15, 164, 0, 9, 25, 39601 }, // U+FBFF + { 11, 13, 188, 0, 11, 18, 39626 }, // U+FE80 + { 10, 23, 108, -2, 23, 29, 39644 }, // U+FE81 + { 12, 25, 127, -2, 24, 38, 39673 }, // U+FE82 + { 6, 25, 108, 0, 25, 19, 39711 }, // U+FE83 + { 10, 27, 127, 0, 26, 34, 39730 }, // U+FE84 + { 12, 24, 199, 0, 18, 36, 39764 }, // U+FE85 + { 16, 24, 210, 0, 18, 48, 39800 }, // U+FE86 + { 6, 27, 109, 1, 19, 21, 39848 }, // U+FE87 + { 9, 27, 127, 1, 19, 31, 39869 }, // U+FE88 + { 21, 19, 328, 0, 13, 50, 39900 }, // U+FE89 + { 22, 19, 320, 0, 13, 53, 39950 }, // U+FE8A + { 7, 21, 127, 0, 20, 19, 40003 }, // U+FE8B + { 12, 18, 154, 0, 17, 27, 40022 }, // U+FE8C + { 5, 19, 109, 1, 19, 12, 40049 }, // U+FE8D + { 9, 20, 127, 1, 19, 23, 40061 }, // U+FE8E + { 25, 19, 410, 0, 12, 60, 40084 }, // U+FE8F + { 31, 17, 457, 0, 10, 66, 40144 }, // U+FE90 + { 7, 18, 127, 0, 12, 16, 40210 }, // U+FE91 + { 12, 15, 154, 0, 9, 23, 40226 }, // U+FE92 + { 11, 21, 188, 0, 20, 29, 40249 }, // U+FE93 + { 16, 23, 222, 0, 22, 46, 40278 }, // U+FE94 + { 25, 16, 410, 0, 15, 50, 40324 }, // U+FE95 + { 31, 16, 457, 0, 15, 62, 40374 }, // U+FE96 + { 9, 19, 127, -1, 18, 22, 40436 }, // U+FE97 + { 13, 16, 164, 0, 15, 26, 40458 }, // U+FE98 + { 25, 19, 410, 0, 18, 60, 40484 }, // U+FE99 + { 31, 19, 457, 0, 18, 74, 40544 }, // U+FE9A + { 8, 22, 127, 0, 21, 22, 40618 }, // U+FE9B + { 13, 19, 164, 0, 18, 31, 40640 }, // U+FE9C + { 17, 23, 275, 0, 13, 49, 40671 }, // U+FE9D + { 21, 23, 296, 0, 13, 61, 40720 }, // U+FE9E + { 16, 17, 252, 0, 11, 34, 40781 }, // U+FE9F + { 19, 17, 262, 0, 11, 41, 40815 }, // U+FEA0 + { 17, 23, 275, 0, 13, 49, 40856 }, // U+FEA1 + { 21, 23, 296, 0, 13, 61, 40905 }, // U+FEA2 + { 16, 12, 252, 0, 11, 24, 40966 }, // U+FEA3 + { 19, 12, 262, 0, 11, 29, 40990 }, // U+FEA4 + { 17, 29, 275, 0, 19, 62, 41019 }, // U+FEA5 + { 21, 29, 296, 0, 19, 77, 41081 }, // U+FEA6 + { 16, 19, 252, 0, 18, 38, 41158 }, // U+FEA7 + { 19, 19, 262, 0, 18, 46, 41196 }, // U+FEA8 + { 12, 14, 207, 0, 13, 21, 41242 }, // U+FEA9 + { 17, 14, 228, 0, 13, 30, 41263 }, // U+FEAA + { 12, 20, 207, 0, 19, 30, 41293 }, // U+FEAB + { 17, 20, 228, 0, 19, 43, 41323 }, // U+FEAC + { 10, 16, 164, -1, 10, 20, 41366 }, // U+FEAD + { 15, 16, 180, -1, 10, 30, 41386 }, // U+FEAE + { 10, 23, 164, -1, 17, 29, 41416 }, // U+FEAF + { 15, 23, 180, -1, 17, 44, 41445 }, // U+FEB0 + { 33, 18, 548, 0, 12, 75, 41489 }, // U+FEB1 + { 38, 17, 576, 0, 11, 81, 41564 }, // U+FEB2 + { 23, 13, 380, 0, 12, 38, 41645 }, // U+FEB3 + { 28, 12, 408, 0, 11, 42, 41683 }, // U+FEB4 + { 33, 25, 548, 0, 19, 104, 41725 }, // U+FEB5 + { 38, 25, 576, 0, 19, 119, 41829 }, // U+FEB6 + { 23, 20, 380, 0, 19, 58, 41948 }, // U+FEB7 + { 28, 20, 408, 0, 19, 70, 42006 }, // U+FEB8 + { 35, 17, 569, 0, 11, 75, 42076 }, // U+FEB9 + { 39, 17, 577, 0, 11, 83, 42151 }, // U+FEBA + { 24, 12, 390, 0, 11, 36, 42234 }, // U+FEBB + { 28, 12, 402, 0, 11, 42, 42270 }, // U+FEBC + { 35, 24, 569, 0, 18, 105, 42312 }, // U+FEBD + { 39, 24, 577, 0, 18, 117, 42417 }, // U+FEBE + { 24, 19, 390, 0, 18, 57, 42534 }, // U+FEBF + { 28, 19, 402, 0, 18, 67, 42591 }, // U+FEC0 + { 21, 20, 338, 0, 19, 53, 42658 }, // U+FEC1 + { 24, 20, 344, 0, 19, 60, 42711 }, // U+FEC2 + { 18, 20, 295, 0, 19, 45, 42771 }, // U+FEC3 + { 22, 20, 305, 0, 19, 55, 42816 }, // U+FEC4 + { 21, 20, 338, 0, 19, 53, 42871 }, // U+FEC5 + { 24, 20, 344, 0, 19, 60, 42924 }, // U+FEC6 + { 18, 20, 295, 0, 19, 45, 42984 }, // U+FEC7 + { 22, 20, 305, 0, 19, 55, 43029 }, // U+FEC8 + { 17, 24, 228, 0, 14, 51, 43084 }, // U+FEC9 + { 17, 22, 228, 0, 12, 47, 43135 }, // U+FECA + { 13, 14, 223, 0, 13, 23, 43182 }, // U+FECB + { 17, 13, 221, -1, 12, 28, 43205 }, // U+FECC + { 17, 31, 228, 0, 21, 66, 43233 }, // U+FECD + { 17, 28, 228, 0, 18, 60, 43299 }, // U+FECE + { 13, 20, 223, 0, 19, 33, 43359 }, // U+FECF + { 17, 19, 221, -1, 18, 41, 43392 }, // U+FED0 + { 25, 22, 410, 0, 21, 69, 43433 }, // U+FED1 + { 30, 20, 446, 0, 19, 75, 43502 }, // U+FED2 + { 12, 22, 202, 0, 21, 33, 43577 }, // U+FED3 + { 16, 20, 212, 0, 19, 40, 43610 }, // U+FED4 + { 20, 23, 333, 0, 18, 58, 43650 }, // U+FED5 + { 24, 23, 342, 0, 16, 69, 43708 }, // U+FED6 + { 12, 22, 202, 0, 21, 33, 43777 }, // U+FED7 + { 16, 20, 212, 0, 19, 40, 43810 }, // U+FED8 + { 23, 20, 372, 0, 19, 58, 43850 }, // U+FED9 + { 27, 20, 390, 0, 19, 68, 43908 }, // U+FEDA + { 12, 19, 182, 0, 18, 29, 43976 }, // U+FEDB + { 15, 19, 203, 0, 18, 36, 44005 }, // U+FEDC + { 18, 25, 296, 0, 19, 57, 44041 }, // U+FEDD + { 22, 25, 316, 0, 19, 69, 44098 }, // U+FEDE + { 7, 20, 119, 0, 19, 18, 44167 }, // U+FEDF + { 11, 20, 138, 0, 19, 28, 44185 }, // U+FEE0 + { 13, 22, 219, 0, 12, 36, 44213 }, // U+FEE1 + { 19, 21, 264, 0, 11, 50, 44249 }, // U+FEE2 + { 14, 12, 233, 0, 11, 21, 44299 }, // U+FEE3 + { 18, 12, 250, 0, 11, 27, 44320 }, // U+FEE4 + { 18, 21, 296, 0, 15, 48, 44347 }, // U+FEE5 + { 23, 21, 322, 0, 15, 61, 44395 }, // U+FEE6 + { 7, 19, 127, 0, 18, 17, 44456 }, // U+FEE7 + { 12, 17, 154, 0, 16, 26, 44473 }, // U+FEE8 + { 11, 15, 188, 0, 14, 21, 44499 }, // U+FEE9 + { 16, 16, 222, 0, 15, 32, 44520 }, // U+FEEA + { 16, 15, 256, 0, 14, 30, 44552 }, // U+FEEB + { 17, 18, 228, 0, 12, 39, 44582 }, // U+FEEC + { 12, 17, 199, 0, 11, 26, 44621 }, // U+FEED + { 16, 17, 210, 0, 11, 34, 44647 }, // U+FEEE + { 21, 18, 328, 0, 12, 48, 44681 }, // U+FEEF + { 22, 13, 320, 0, 7, 36, 44729 }, // U+FEF0 + { 21, 24, 328, 0, 12, 63, 44765 }, // U+FEF1 + { 22, 19, 320, 0, 7, 53, 44828 }, // U+FEF2 + { 8, 18, 144, 0, 12, 18, 44881 }, // U+FEF3 + { 13, 15, 164, 0, 9, 25, 44899 }, // U+FEF4 + { 17, 21, 253, -2, 21, 45, 44924 }, // U+FEF5 + { 20, 22, 272, 0, 21, 55, 44969 }, // U+FEF6 + { 15, 23, 253, 0, 23, 44, 45024 }, // U+FEF7 + { 19, 24, 272, 1, 23, 57, 45068 }, // U+FEF8 + { 14, 27, 253, 1, 19, 48, 45125 }, // U+FEF9 + { 19, 27, 272, 1, 19, 65, 45173 }, // U+FEFA + { 14, 19, 253, 1, 19, 34, 45238 }, // U+FEFB + { 19, 20, 272, 1, 19, 48, 45272 }, // U+FEFC }; static const EpdUnicodeInterval inter_12_boldIntervals[] = { - { 0x0, 0x7F, 0x0 }, - { 0x80, 0xFF, 0x80 }, - { 0x100, 0x148, 0x100 }, + { 0x0, 0x148, 0x0 }, { 0x14A, 0x17F, 0x149 }, { 0x1A0, 0x1A1, 0x17F }, { 0x1AF, 0x1B0, 0x181 }, @@ -3480,59 +4188,75 @@ static const EpdUnicodeInterval inter_12_boldIntervals[] = { { 0x300, 0x304, 0x1DE }, { 0x306, 0x30C, 0x1E3 }, { 0x30F, 0x30F, 0x1EA }, - { 0x313, 0x313, 0x1EB }, - { 0x315, 0x315, 0x1EC }, - { 0x31B, 0x31B, 0x1ED }, - { 0x323, 0x323, 0x1EE }, - { 0x326, 0x328, 0x1EF }, - { 0x32C, 0x32C, 0x1F2 }, - { 0x337, 0x338, 0x1F3 }, - { 0x342, 0x343, 0x1F5 }, - { 0x346, 0x36F, 0x1F7 }, - { 0x400, 0x479, 0x221 }, - { 0x480, 0x49D, 0x29B }, - { 0x4A0, 0x4FF, 0x2B9 }, - { 0x5D0, 0x5EA, 0x319 }, - { 0x1EA0, 0x1EF9, 0x334 }, - { 0x2000, 0x2027, 0x38E }, - { 0x202A, 0x2055, 0x3B6 }, - { 0x2057, 0x2057, 0x3E2 }, - { 0x205F, 0x2064, 0x3E3 }, - { 0x2066, 0x206F, 0x3E9 }, - { 0x2070, 0x2071, 0x3F3 }, - { 0x2074, 0x208E, 0x3F5 }, - { 0x2090, 0x209C, 0x410 }, - { 0x20A0, 0x20AF, 0x41D }, - { 0x20B1, 0x20B5, 0x42D }, - { 0x20B8, 0x20BA, 0x432 }, - { 0x20BC, 0x20BF, 0x435 }, - { 0x2190, 0x2199, 0x439 }, - { 0x21A9, 0x21AA, 0x443 }, - { 0x21B0, 0x21B7, 0x445 }, - { 0x21BA, 0x21BB, 0x44D }, - { 0x21C4, 0x21C4, 0x44F }, - { 0x21C6, 0x21C6, 0x450 }, - { 0x21D0, 0x21D0, 0x451 }, - { 0x21D2, 0x21D2, 0x452 }, - { 0x21D4, 0x21D4, 0x453 }, - { 0x21DE, 0x21DF, 0x454 }, - { 0x21E4, 0x21E5, 0x456 }, - { 0x21E7, 0x21E7, 0x458 }, - { 0x21EA, 0x21EA, 0x459 }, - { 0x2202, 0x2202, 0x45A }, - { 0x2205, 0x2206, 0x45B }, - { 0x220F, 0x220F, 0x45D }, - { 0x2211, 0x2212, 0x45E }, - { 0x2215, 0x2215, 0x460 }, - { 0x221A, 0x221A, 0x461 }, - { 0x221E, 0x221E, 0x462 }, - { 0x222B, 0x222B, 0x463 }, - { 0x2236, 0x2236, 0x464 }, - { 0x2248, 0x2248, 0x465 }, - { 0x2260, 0x2260, 0x466 }, - { 0x2264, 0x2265, 0x467 }, - { 0x2295, 0x2298, 0x469 }, - { 0xFB01, 0xFB02, 0x46D }, + { 0x312, 0x313, 0x1EB }, + { 0x315, 0x315, 0x1ED }, + { 0x31B, 0x31B, 0x1EE }, + { 0x323, 0x323, 0x1EF }, + { 0x326, 0x328, 0x1F0 }, + { 0x32C, 0x32C, 0x1F3 }, + { 0x337, 0x338, 0x1F4 }, + { 0x342, 0x343, 0x1F6 }, + { 0x346, 0x36F, 0x1F8 }, + { 0x400, 0x479, 0x222 }, + { 0x480, 0x49D, 0x29C }, + { 0x4A0, 0x4FF, 0x2BA }, + { 0x5D0, 0x5EA, 0x31A }, + { 0x60C, 0x60C, 0x335 }, + { 0x61B, 0x61B, 0x336 }, + { 0x61F, 0x61F, 0x337 }, + { 0x621, 0x621, 0x338 }, + { 0x640, 0x640, 0x339 }, + { 0x660, 0x669, 0x33A }, + { 0x6BA, 0x6BA, 0x344 }, + { 0x6D4, 0x6D4, 0x345 }, + { 0x6F0, 0x6F9, 0x346 }, + { 0x1EA0, 0x1EF9, 0x350 }, + { 0x2000, 0x2027, 0x3AA }, + { 0x202A, 0x2055, 0x3D2 }, + { 0x2057, 0x2057, 0x3FE }, + { 0x205F, 0x2064, 0x3FF }, + { 0x2066, 0x2071, 0x405 }, + { 0x2074, 0x208E, 0x411 }, + { 0x2090, 0x209C, 0x42C }, + { 0x20A0, 0x20AF, 0x439 }, + { 0x20B1, 0x20B5, 0x449 }, + { 0x20B8, 0x20BA, 0x44E }, + { 0x20BC, 0x20BF, 0x451 }, + { 0x2190, 0x2199, 0x455 }, + { 0x21A9, 0x21AA, 0x45F }, + { 0x21B0, 0x21B7, 0x461 }, + { 0x21BA, 0x21BB, 0x469 }, + { 0x21C4, 0x21C4, 0x46B }, + { 0x21C6, 0x21C6, 0x46C }, + { 0x21D0, 0x21D0, 0x46D }, + { 0x21D2, 0x21D2, 0x46E }, + { 0x21D4, 0x21D4, 0x46F }, + { 0x21DE, 0x21DF, 0x470 }, + { 0x21E4, 0x21E5, 0x472 }, + { 0x21E7, 0x21E7, 0x474 }, + { 0x21EA, 0x21EA, 0x475 }, + { 0x2202, 0x2202, 0x476 }, + { 0x2205, 0x2206, 0x477 }, + { 0x220F, 0x220F, 0x479 }, + { 0x2211, 0x2212, 0x47A }, + { 0x2215, 0x2215, 0x47C }, + { 0x221A, 0x221A, 0x47D }, + { 0x221E, 0x221E, 0x47E }, + { 0x222B, 0x222B, 0x47F }, + { 0x2236, 0x2236, 0x480 }, + { 0x2248, 0x2248, 0x481 }, + { 0x2260, 0x2260, 0x482 }, + { 0x2264, 0x2265, 0x483 }, + { 0x2295, 0x2298, 0x485 }, + { 0xFB01, 0xFB02, 0x489 }, + { 0xFB56, 0xFB59, 0x48B }, + { 0xFB66, 0xFB69, 0x48F }, + { 0xFB7A, 0xFB7D, 0x493 }, + { 0xFB88, 0xFB95, 0x497 }, + { 0xFB9E, 0xFB9F, 0x4A5 }, + { 0xFBA6, 0xFBB1, 0x4A7 }, + { 0xFBFC, 0xFBFF, 0x4B3 }, + { 0xFE80, 0xFEFC, 0x4B7 }, }; static const EpdKernClassEntry inter_12_boldKernLeftClasses[] = { @@ -4313,6 +5037,10 @@ static const EpdKernClassEntry inter_12_boldKernLeftClasses[] = { { 0x2264, 20 }, // U+2264 { 0x2265, 20 }, // U+2265 { 0xFB02, 153 }, // U+FB02 + { 0xFB8C, 154 }, // U+FB8C + { 0xFEAD, 154 }, // U+FEAD + { 0xFEAE, 154 }, // U+FEAE + { 0xFEAF, 154 }, // U+FEAF }; static const EpdKernClassEntry inter_12_boldKernRightClasses[] = { @@ -5092,169 +5820,191 @@ static const EpdKernClassEntry inter_12_boldKernRightClasses[] = { { 0x2260, 27 }, // U+2260 { 0x2264, 27 }, // U+2264 { 0x2265, 27 }, // U+2265 + { 0xFB8E, 146 }, // U+FB8E + { 0xFBA6, 146 }, // U+FBA6 + { 0xFBAA, 146 }, // U+FBAA + { 0xFE8D, 147 }, // U+FE8D + { 0xFEA3, 146 }, // U+FEA3 + { 0xFEA9, 148 }, // U+FEA9 + { 0xFEB1, 148 }, // U+FEB1 + { 0xFEB3, 148 }, // U+FEB3 + { 0xFEB9, 148 }, // U+FEB9 + { 0xFEBB, 148 }, // U+FEBB + { 0xFEC1, 148 }, // U+FEC1 + { 0xFEC3, 148 }, // U+FEC3 + { 0xFEC9, 149 }, // U+FEC9 + { 0xFECB, 147 }, // U+FECB + { 0xFECD, 149 }, // U+FECD + { 0xFEDB, 146 }, // U+FEDB + { 0xFEDF, 147 }, // U+FEDF + { 0xFEE1, 146 }, // U+FEE1 + { 0xFEE3, 146 }, // U+FEE3 + { 0xFEE9, 146 }, // U+FEE9 + { 0xFEEB, 146 }, // U+FEEB }; static const int8_t inter_12_boldKernMatrix[] = { - 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -36, 0, -36, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, -66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, -4, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 18, 18, 0, 0, -4, -4, -4, 0, 0, 0, 0, 18, 0, 0, -6, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -27, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -25, -16, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, 0, 0, -16, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, -2, 0, 0, 0, 0, 6, 0, 0, 5, -2, 0, 0, 0, 0, 6, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -54, 0, -54, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, -5, -38, 0, -5, -38, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -2, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -31, 0, -18, -11, -17, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, -54, -54, -31, 0, 0, 0, 0, 0, 0, 0, -44, -12, 0, 0, -60, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -12, -5, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -38, 0, -8, 0, -3, -11, -10, -5, 0, 0, 0, -40, -18, 0, 0, -18, 0, -13, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, -8, -18, 0, -13, 0, 0, 0, -17, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, 0, -38, 0, -54, -54, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -25, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -12, 0, -15, -13, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 7, 0, 7, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -18, 0, -20, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 5, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 4, 0, 14, 0, 27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 16, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -13, 0, -13, 0, 0, -16, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -16, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -9, 0, -9, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, -23, 0, 1, -18, 0, 0, 2, 0, -50, 0, -50, 0, -6, 0, 0, -7, -23, -4, 8, -6, -4, -13, -36, 0, 0, 0, -43, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 2, -63, -18, -20, 0, 0, 0, -20, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, -20, 0, 0, 0, 36, -20, 0, 0, -18, 0, 0, 36, 36, 0, -20, 0, 0, 0, 0, 0, 0, 0, 36, -44, -44, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -18, 0, -27, -25, -20, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -18, -18, -18, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -32, 0, -29, -29, -23, -36, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, -13, -13, -20, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -13, 0, -16, -10, -22, -18, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 10, 0, 0, 0, -42, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -13, 0, -16, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -16, -40, 0, 0, 0, 0, - 0, 0, -32, 0, -6, 0, 0, 0, 0, -38, -13, 0, -13, 0, 5, -3, -24, 0, 0, 0, 0, 0, 0, 0, 0, -15, -20, -25, -17, 6, 0, -17, 0, -36, -10, -34, -30, 0, -34, 0, -38, 0, 0, -9, -5, 1, 0, 0, -4, -12, 0, -27, -27, 0, 0, -13, 0, -29, -13, -13, 0, -28, -38, 0, 0, -21, -13, 0, -13, 0, 0, 0, -13, 0, 0, -9, 1, 1, 1, 0, -9, 0, -27, 0, 0, 0, 0, 0, 0, -9, -36, -36, -59, 0, 0, 1, 0, 0, 0, 0, 0, 0, -17, 0, -36, 0, 0, -12, -27, -12, -4, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, -52, -41, 0, 0, 0, -13, 0, -41, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -20, -20, -20, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 2, 0, 0, 0, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 29, 29, 0, 0, 0, 0, 0, 0, 0, 0, 18, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -29, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, -13, -7, -10, 0, 0, 0, -7, 0, 0, -13, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -7, -7, -10, 0, 0, 0, 0, -10, -13, -13, -7, 0, 0, 0, 0, 0, -10, 0, 0, 0, -13, 0, 0, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -48, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -12, 0, -9, 0, -6, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -12, -12, -12, 0, 0, 0, 14, 13, 0, 0, -6, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -31, 0, -31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -13, 0, -17, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 2, 0, 0, 0, 0, -13, -16, -25, 0, 0, -31, 0, 0, -20, -51, 0, -6, 0, 0, -13, 0, -49, 0, 0, 0, 0, 0, -13, 0, 0, -16, 2, 2, 2, 34, -16, -13, -16, 0, 0, 0, 34, 34, 0, -16, 0, 0, 0, -13, 0, 2, 34, 34, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, -44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, - 0, 0, -36, 0, 0, 0, 0, 0, 0, -68, -27, 0, -47, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -11, 0, 0, -11, 0, -37, 0, -31, -15, 0, -39, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -42, 0, -38, -25, -79, 0, -32, -70, 0, 0, -32, -52, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -37, -37, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -37, 0, 0, -9, -50, -9, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -34, -35, 0, -18, -18, -13, 0, -62, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -8, -16, -8, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, -38, -1, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, -6, -6, -5, 0, 0, 0, 0, -5, 0, 0, -6, 0, 0, 0, 0, 0, -5, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, 0, 0, 0, -54, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -8, 0, 0, -12, 0, 0, 0, -3, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -3, -6, 0, 0, 0, 18, -6, 0, 0, -3, 0, 0, 18, 18, 0, -6, -2, -2, -2, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -2, 0, -13, 0, 0, 0, -23, -13, -23, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -36, 0, -13, -36, 0, 0, 0, 0, 0, 3, 0, 0, -23, -32, -35, -1, 0, 0, -24, -31, -37, -32, -27, -27, -14, -21, -23, 0, 0, -52, -23, 0, 0, 0, 0, -73, 0, -55, 0, -20, 0, 0, 0, -13, -48, -48, -55, 0, 0, 0, 36, -55, -57, -40, -32, 0, 0, 36, 36, 0, -35, 0, 0, 0, -48, 0, 0, 57, 36, -54, -54, 0, 0, -13, 0, 0, 0, 0, -37, -23, -37, -31, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -23, -38, -23, -38, -22, -12, 0, 0, 0, -13, 0, 0, 0, 0, -25, -29, -25, 0, -30, -34, 0, -16, -38, 0, 0, 6, 0, 0, 0, 0, 0, -32, -25, -26, 0, 0, 0, -6, -18, 0, 0, 0, 0, 0, 0, -23, 0, 0, -23, -48, 0, 0, 0, 0, 0, 0, -39, 0, 0, -12, 0, 0, -16, -25, -41, -26, 0, 0, 0, 34, -44, 0, 0, -25, 0, 0, 34, 34, 0, -26, 0, 0, 0, 0, 0, 0, 16, 34, -48, -48, 0, 0, -16, 0, 0, 0, 0, 0, -7, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, 0, 0, -16, -38, 0, 0, 0, 0, 0, 0, 0, -57, -61, 0, 0, 0, 0, - 0, 0, 0, 0, -2, 0, -20, 0, 0, 0, -17, -38, -17, -38, -12, 0, 0, 0, 0, -13, 0, 0, 0, 0, -20, -29, -25, 0, -10, -30, 0, -10, -25, 0, 0, 2, 8, 0, 0, 0, 0, 0, -23, -21, 0, 0, 0, -13, -13, 0, 0, 0, 0, 0, 0, -17, 0, 0, -23, -17, 0, 0, 0, 0, 0, 0, -30, 0, 0, -12, 0, 0, 0, -23, -23, -21, 0, 0, 0, 0, -21, 0, 0, -23, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, -40, -45, 0, 0, 0, 0, - 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -17, 0, -17, 0, 0, -15, 0, 0, -9, -16, 0, 0, -9, 0, 0, -23, -20, 0, -22, 0, 0, -22, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, -16, -37, 0, -3, 0, 0, 0, 0, -35, 0, 0, 0, 0, 0, -18, -23, -23, -11, 0, 0, 0, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, -11, -12, -12, -12, 0, 0, 0, 34, 0, 0, 0, 0, -9, -42, -13, -12, 0, 0, -25, -35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, - 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, -26, -26, -26, -26, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -18, -34, 0, -18, -13, 3, 0, 0, 0, 0, 0, 0, 0, 0, -42, -45, 0, -8, 0, -19, -27, 0, -16, 0, 0, 0, 0, -26, 0, 0, -36, -46, 0, -3, 0, 0, -16, 0, -46, 0, -25, 0, 0, 0, -34, -69, -62, -45, -8, -23, -8, 0, -45, -16, 0, -79, 0, 0, 0, 0, 0, -67, 3, 3, 3, -16, 0, -8, 34, 0, 0, 0, 0, 0, -18, 0, 3, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -64, 0, 0, 0, 0, 0, 0, 0, -70, -75, 0, 0, 0, -13, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, -13, -15, 0, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 13, 34, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, -2, 0, 0, 0, 0, 6, 0, 0, 5, -2, 0, 0, 0, 0, 6, 0, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -32, 0, -6, 0, 0, 0, 0, -38, -13, 0, -13, 0, 5, -3, -24, 0, 0, 0, 0, 0, 0, 0, 0, -15, -20, -25, -17, 6, 0, -17, 0, -52, -10, -56, -53, 0, -34, 0, -38, 0, 0, -9, -5, 1, 0, 0, -4, -12, 0, -47, -27, 0, 0, -13, 0, -29, -13, -13, 0, -28, -38, 0, 0, -21, -13, 0, -13, 0, 0, 0, -13, 0, 0, -9, 1, 1, 1, 0, -9, 0, -47, 0, 0, 0, 0, 0, 0, -9, -52, -52, -52, 0, 0, 1, 0, 0, 0, 0, 0, 0, -17, 0, -52, 0, 0, -12, -27, -12, -4, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, -69, -63, 0, 0, 0, -13, 0, -63, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, -18, -43, 0, -18, -23, -18, 0, -18, -18, 0, 0, 0, 0, -16, 0, 11, -16, 0, -23, -16, -32, 0, 0, 0, 0, -27, 0, 0, 0, 0, 5, 38, 12, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 0, 0, 0, -32, 0, 0, 11, 0, 0, 38, 0, -23, -23, -23, 0, 11, 5, 0, 0, 23, 23, 0, -18, -16, -43, -20, 16, 16, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, - 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -31, 0, -20, -20, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -31, -31, -31, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -31, 0, 0, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, -18, -11, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -10, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -35, 0, -26, -21, -11, -46, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -7, -5, -9, -8, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -7, 4, 0, 0, 0, 0, 0, 0, -35, -35, -35, 0, 0, 0, 16, 0, 0, 0, -11, 0, 0, 0, -35, 0, 0, 0, -5, 0, 0, -89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -23, -8, -23, -5, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -25, 0, 0, 0, 0, 0, 2, 0, 0, -6, -8, -8, -18, 3, 2, 0, -7, -16, 0, 0, 8, 0, 0, -8, 0, 0, -16, -8, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, -7, -8, -8, -8, 3, 3, 3, 41, -8, 0, 0, -8, 0, 0, 41, 56, 2, -8, 0, 0, 0, 0, 0, 3, 18, 54, -32, -32, 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, -7, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, -36, 0, 0, -7, -23, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 40, 26, 0, 0, 0, 0, 0, 0, 0, 0, 35, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -23, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, -23, 0, 0, -20, -50, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -9, 0, 3, 0, 0, 0, 0, 0, 0, -9, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -25, -13, -25, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -16, 5, 0, -23, -23, 0, 0, 0, 0, 0, -13, 0, 0, -5, -8, 8, 2, 5, 3, 0, 8, 0, 6, 4, 6, 0, -13, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -5, -5, -8, 2, 2, 2, 5, -8, 0, 6, -5, 0, 5, 5, 5, 5, -8, -23, -23, -23, 0, 5, 2, 14, 5, 0, 0, 0, 0, 0, 0, -23, -9, -9, 8, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 6, -32, 0, 0, 0, -25, 6, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, - 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, -18, -13, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -29, -29, -29, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 1, -1, 1, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -20, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 1, 0, 0, -13, 0, -2, -2, 0, -3, 0, 0, 0, 2, -4, -13, 0, 3, 0, -3, -7, 0, 9, 8, 0, 0, -7, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, -2, 2, 2, -4, 0, 0, 0, 0, -4, 0, 9, 2, 0, 1, 0, 0, 3, -4, -13, -13, -13, 0, 1, 0, 18, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -7, 0, -7, -3, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -16, 6, 0, 0, 0, -2, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, -20, -50, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -16, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -18, -14, -18, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -16, 0, 0, -27, 1, 0, -25, -13, 0, 0, 0, 0, -13, 0, 0, -32, -5, -7, 6, 0, 0, 0, -1, 7, 0, 0, 0, 0, 0, -14, 0, 0, -16, -14, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, -5, -5, -7, 0, 0, 0, 8, -7, 0, 0, -5, 0, 1, 8, 8, 0, -7, -13, -38, -13, 0, 1, 0, 1, 8, 0, 0, 0, 0, 0, 0, -13, -18, -18, 7, 0, 7, -1, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 3, -32, 0, 0, 0, -48, 3, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -29, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, -23, -20, 0, 0, 0, 0, 0, -13, 0, 0, -2, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -5, 0, 0, 0, 0, -5, 0, 0, -2, 0, 0, 0, 0, 0, -5, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -32, 0, 0, 0, -29, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -2, -9, 6, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, -9, 0, 0, -13, -9, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, -2, -2, -9, 0, 0, 0, 0, -9, 0, 0, -2, 0, 0, 0, 0, 0, -9, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 6, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -8, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -2, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -31, 0, -18, -11, -33, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, -54, -54, -31, 0, 0, 0, 0, 0, 0, 0, -33, -12, 0, 0, -60, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -13, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -27, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 12, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -20, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, -2, -28, 0, -2, -28, 0, 0, 0, 0, -3, -3, 0, 0, 0, -4, -11, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, -11, 0, 0, 0, 0, -11, 0, 0, -4, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -2, 0, 0, 0, 0, 0, 0, 0, -4, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, -20, -50, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -36, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -2, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -62, 0, -34, -11, -35, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, -62, -62, -62, 0, 0, 0, 0, 0, 0, 0, -35, -12, 0, 0, -62, 0, 0, 0, 0, 0, 0, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -20, -18, 0, 0, -20, -13, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -52, 0, -25, -23, -16, -36, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -52, -52, -52, 0, 0, 0, 0, 0, -13, -13, -36, -18, 0, 0, -79, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -48, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -27, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -13, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -36, 0, -16, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -36, -36, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -41, 0, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, -16, -23, 0, -20, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -50, 0, 0, 0, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -20, -20, -20, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 42, 41, 0, 0, 0, 0, 0, 0, 0, 0, 30, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 26, 26, 5, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 39, 26, 39, 66, 0, 0, 0, 0, 0, 27, 66, 66, 39, 0, 36, 36, 36, 0, 56, 39, 20, 66, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -51, 0, -20, -20, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -51, -51, -51, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -51, 0, 0, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, -18, -11, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -10, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -55, 0, -26, -21, -11, -46, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -7, -5, -9, -8, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -7, 4, 0, 0, 0, 0, 0, 0, -55, -55, -55, 0, 0, 0, 16, 0, 0, 0, -11, 0, 0, 0, -55, 0, 0, 0, -5, 0, 0, -89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -60, 0, -20, -20, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -60, -60, -60, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -60, 0, 0, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, -18, -11, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 13, 0, 0, 0, 0, 3, 49, 23, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 16, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 16, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 7, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 32, 16, 0, 0, 0, 0, 0, 12, 16, 16, 11, 0, 16, 16, 16, 0, 12, 32, 18, 16, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 16, 16, 0, 0, 0, 0, 0, 16, 16, 16, 16, 0, 13, 13, 13, 0, 14, 13, 16, 16, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 43, 39, 5, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 43, 43, 43, 66, 0, 0, 0, 0, 0, 27, 66, 66, 39, 0, 36, 36, 36, 0, 56, 43, 43, 66, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 43, 43, 5, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 43, 43, 43, 66, 0, 0, 0, 0, 0, 27, 66, 66, 43, 0, 36, 36, 36, 0, 56, 43, 20, 66, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 43, 26, 5, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 43, 43, 43, 66, 0, 0, 0, 0, 0, 27, 66, 66, 39, 0, 36, 36, 36, 0, 56, 43, 20, 66, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 42, 41, 0, 0, 0, 0, 0, 0, 0, 0, 30, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -36, 0, 0, 0, 0, 0, 0, -70, -46, 0, -46, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -11, 0, 0, -11, 0, -37, 0, -31, -15, 0, -39, 0, -70, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -45, 0, -38, -25, -46, 0, -32, -70, 0, 0, -32, -46, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -37, -37, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -37, 0, 0, -9, -50, -9, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -34, -35, 0, -18, -18, -13, 0, -62, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 5, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 4, 0, 14, 0, 27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 32, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -36, 0, 0, 0, 0, 0, 0, -68, -41, 0, -41, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -11, 0, 0, -11, 0, -37, 0, -31, -15, 0, -39, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -40, 0, -38, -25, -41, 0, -32, -70, 0, 0, -32, -41, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -37, -37, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -37, 0, 0, -9, -50, -9, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -34, -35, 0, -18, -18, -13, 0, -62, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 5, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 4, 0, 13, 0, 27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 16, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -36, 0, 0, 0, 0, 0, 0, -66, -27, 0, -47, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -11, 0, 0, -11, 0, -37, 0, -31, -15, 0, -57, 0, -66, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -42, 0, -38, -25, -79, 0, -32, -66, 0, 0, -32, -52, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -37, -37, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -37, 0, 0, -9, -50, -9, 0, -57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -34, -48, 0, -18, -18, -13, 0, -48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 13, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 4, 0, 13, 0, 27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 16, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -2, 0, -13, 0, 0, 0, -39, -13, -39, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -36, 0, -13, -36, 0, 0, 0, 0, 0, 3, 0, 0, -23, -32, -35, -1, 0, 0, -24, -31, -37, -56, -27, -27, -34, -37, -39, 0, 0, -52, -39, 0, 0, 0, 0, -56, 0, -39, 0, -20, 0, 0, 0, -13, -48, -48, -55, 0, 0, 0, 36, -55, -56, -40, -32, 0, 0, 36, 36, 0, -35, 0, 0, 0, -56, 0, 0, 57, 36, -54, -54, 0, 0, -13, 0, 0, 0, 0, -37, -23, -37, -31, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, - 0, 0, 0, 0, -2, 0, -13, 0, 0, 0, -39, -13, -39, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -36, 0, -13, -36, 0, 0, 0, 0, 0, 3, 0, 0, -23, -32, -35, -1, 0, 0, -24, -31, -37, -32, -27, -27, -14, -21, -39, 0, 0, -52, -39, 0, 0, 0, 0, -73, 0, -39, 0, -20, 0, 0, 0, -13, -48, -48, -55, 0, 0, 0, 36, -55, -57, -40, -32, 0, 0, 36, 36, 0, -35, 0, 0, 0, -48, 0, 0, 57, 36, -54, -54, 0, 0, -13, 0, 0, 0, 0, -37, -23, -37, -31, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, - 0, 0, 0, 0, -2, 0, -13, 0, 0, 0, -23, -13, -23, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -61, 0, -13, -84, 0, 0, 0, 0, 0, 3, 0, 0, -23, -32, -35, -1, 0, 0, -24, -31, -37, -32, -27, -27, -14, -21, -23, 0, 0, -52, -23, 0, 0, 0, 0, -73, 0, -55, 0, -20, 0, 0, 0, -13, -48, -48, -55, 0, 0, 0, 36, -55, -57, -40, -32, 0, 0, 36, 36, 0, -35, 0, 0, 0, -48, 0, 0, 57, 36, -54, -54, 0, 0, -13, 0, 0, 0, 0, -37, -23, -37, -31, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -37, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, -25, -16, 0, 0, -24, -24, -16, -24, -23, -22, -23, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, -24, -24, -25, 0, 0, 0, 0, -25, -24, -23, -24, 0, 0, 0, 0, 0, -25, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 3, 9, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, 0, 0, 0, 0, 0, 12, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, 0, 0, 0, 0, -22, 0, 7, 0, 0, 0, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -8, -29, -29, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 41, 41, 0, 0, 0, 0, 0, 0, 0, 0, 42, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -10, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -66, 0, -26, -21, -11, -46, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -7, -5, -9, -8, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -7, 4, 0, 0, 0, 0, 0, 0, -66, -66, -66, 0, 0, 0, 16, 0, 0, 0, -11, 0, 0, 0, -66, 0, 0, 0, -5, 0, 0, -89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, -38, -38, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -16, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 29, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 41, 41, 0, 0, 0, 0, 0, 0, 0, 0, 35, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 39, 43, 5, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 39, 39, 39, 66, 0, 0, 0, 0, 0, 27, 66, 66, 43, 0, 36, 36, 36, 0, 56, 39, 38, 81, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 8, 8, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -46, 0, -46, 0, 0, -15, 0, 0, -9, -16, 0, 0, -9, 0, 0, -20, -20, 0, -22, 0, 0, -22, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, 0, 0, -36, -46, 0, -3, 0, 0, 0, 0, -46, 0, 0, 0, 0, 0, -18, -23, -23, -11, 0, 0, 0, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, -11, -12, -12, -12, 0, 0, 0, 34, 0, 0, 0, 0, -9, -42, -13, -12, 0, 0, -25, -35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -13, 0, -16, -10, -22, -18, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 10, 0, 0, 0, -42, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -13, 0, -36, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -16, -40, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -13, 0, 0, 0, 0, 0, 0, -13, -13, 8, -13, 8, 0, -7, 0, 0, 0, -5, 0, 0, -4, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -8, -12, 0, 0, 0, 0, -12, 0, -11, -12, 0, 0, -13, 0, 0, 0, -13, 0, 0, -13, 0, 0, -12, -13, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, -8, 0, -11, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -17, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 8, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -50, -13, -50, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -16, 5, 0, -23, -23, 0, 0, 0, 0, 0, -13, 0, 0, -5, -8, 8, 2, 5, 3, 0, 8, 0, 6, 4, 6, 0, -13, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -5, -5, -8, 2, 2, 2, 5, -8, 0, 6, -5, 0, 5, 5, 5, 5, -8, -23, -23, -23, 0, 5, 2, 14, 5, 0, 0, 0, 0, 0, 0, -23, -9, -9, 8, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 6, -61, 0, 0, 0, -50, 6, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, -43, 0, -11, -68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -14, -11, -7, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, -43, -43, -43, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -43, 0, 0, -13, 0, -47, 0, -68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -68, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, -26, -26, -26, -26, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -18, -34, 0, -18, -13, 3, 0, 0, 0, 0, 0, 0, 0, 0, -42, -87, 0, -8, 0, -19, -27, 0, -16, 0, 0, 0, 0, -26, 0, 0, -36, -46, 0, -3, 0, 0, -16, 0, -46, 0, -25, 0, 0, 0, -34, -69, -62, -87, -8, -23, -8, 0, -87, -16, 0, -79, 0, 0, 0, 0, 0, -87, 3, 3, 3, -16, 0, -8, 34, 0, 0, 0, 0, 0, -18, 0, 3, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -64, 0, 0, 0, 0, 0, 0, 0, -70, -75, 0, 0, 0, -13, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -8, -16, 4, -8, 0, -8, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -4, 0, -4, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -8, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, 0, -12, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -4, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 40, 26, 0, 0, 0, 0, 0, 0, 0, 0, 30, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -10, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -35, 0, -26, -21, -11, -46, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -7, -5, -9, -8, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -7, 4, 0, 0, 0, 0, 0, 0, -35, -35, -35, 0, 0, 0, 32, 0, 0, 0, -11, 0, 0, 0, -35, 0, 0, 0, -5, 0, 0, -89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, -26, -39, -26, -39, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -18, -34, 0, -18, -13, 3, 0, 0, 0, 0, 0, 0, 0, 0, -42, -45, 0, -8, 0, -19, -27, 0, -16, 0, 0, 0, 0, -26, 0, 0, -36, -46, 0, -3, 0, 0, -16, 0, -46, 0, -25, 0, 0, 0, -34, -69, -62, -45, -8, -23, -8, 0, -45, -16, 0, -79, 0, 0, 0, 0, 0, -67, 3, 3, 3, -16, 0, -8, 34, 0, 0, 0, 0, 0, -18, 0, 3, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -39, 0, 0, 0, 0, 0, 0, 0, -70, -75, 0, 0, 0, -13, - 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, -43, 0, -43, -38, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, -59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -25, -45, 0, 0, 0, 0, 0, -57, -34, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, -50, 0, 0, -27, 0, 0, 0, -21, -52, -21, -52, -57, -18, 0, 0, 0, -38, 0, -25, -16, 0, -23, -38, -23, 0, -23, -55, 0, -23, -50, 0, 0, 0, 0, 0, 0, 0, 0, -16, -23, -25, 0, 0, 0, -12, -28, 4, -16, 4, 0, 0, 0, -39, 0, 0, -34, -21, 0, 0, 0, 0, -34, -4, -21, 0, 0, -27, 0, 0, 0, -23, -23, -25, 0, 0, 0, 0, -25, -16, 4, -23, 0, 0, 0, 0, 0, -25, 0, 0, 0, -16, 0, 0, 0, 0, -20, -36, 0, 0, -23, 0, 0, -48, -25, 4, 0, 4, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, -36, -25, 0, -43, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, 0, 0, 0, 0, 0, -20, -13, 0, 0, 0, 0, 0, 0, -38, -38, -38, 0, 0, 0, 0, 0, 20, 20, 0, 0, 0, -25, -59, 25, 25, -20, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, -50, -38, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 0, -16, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, - -12, -5, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -38, 0, -8, 0, -3, -11, -10, -5, 0, 0, 0, -40, -18, 0, 0, -18, 0, -13, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, -8, -18, 0, -13, 0, 0, 0, -17, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, 0, -38, 0, -54, -73, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 5, -3, 0, -13, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, -16, 0, -23, 0, -57, -40, 0, -70, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 3, 0, -23, -23, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -7, -29, 0, 0, 0, 0, 0, 0, -70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -6, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -27, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -36, 0, -36, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, -66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, -4, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 18, 18, 0, 0, -4, -4, -4, 0, 0, 0, 0, 18, 0, 0, -6, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -27, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -25, -16, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, 0, 0, -16, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, -2, 0, 0, 0, 0, 6, 0, 0, 5, -2, 0, 0, 0, 0, 6, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -54, 0, -54, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, -5, -38, 0, -5, -38, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -2, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -31, 0, -18, -11, -17, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, -54, -54, -31, 0, 0, 0, 0, 0, 0, 0, -44, -12, 0, 0, -60, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -12, -5, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -38, 0, -8, 0, -3, -11, -10, -5, 0, 0, 0, -40, -18, 0, 0, -18, 0, -13, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, -8, -18, 0, -13, 0, 0, 0, -17, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, 0, -38, 0, -54, -54, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -25, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -12, 0, -15, -13, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 7, 0, 7, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -18, 0, -20, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 5, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 4, 0, 14, 0, 27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 16, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -13, 0, -13, 0, 0, -16, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -16, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -9, 0, -9, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -23, 0, 1, -18, 0, 0, 2, 0, -50, 0, -50, 0, -6, 0, 0, -7, -23, -4, 8, -6, -4, -13, -36, 0, 0, 0, -43, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 2, -63, -18, -20, 0, 0, 0, -20, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, -20, 0, 0, 0, 36, -20, 0, 0, -18, 0, 0, 36, 36, 0, -20, 0, 0, 0, 0, 0, 0, 0, 36, -44, -44, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -18, 0, -27, -25, -20, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -18, -18, -18, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -32, 0, -29, -29, -23, -36, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, -13, -13, -20, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -13, 0, -16, -10, -22, -18, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 10, 0, 0, 0, -42, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -13, 0, -16, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -16, -40, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -32, 0, -6, 0, 0, 0, 0, -38, -13, 0, -13, 0, 5, -3, -24, 0, 0, 0, 0, 0, 0, 0, 0, -15, -20, -25, -17, 6, 0, -17, 0, -36, -10, -34, -30, 0, -34, 0, -38, 0, 0, -9, -5, 1, 0, 0, -4, -12, 0, -27, -27, 0, 0, -13, 0, -29, -13, -13, 0, -28, -38, 0, 0, -21, -13, 0, -13, 0, 0, 0, -13, 0, 0, -9, 1, 1, 1, 0, -9, 0, -27, 0, 0, 0, 0, 0, 0, -9, -36, -36, -59, 0, 0, 1, 0, 0, 0, 0, 0, 0, -17, 0, -36, 0, 0, -12, -27, -12, -4, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, -52, -41, 0, 0, 0, -13, 0, -41, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -20, -20, -20, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 2, 0, 0, 0, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 29, 29, 0, 0, 0, 0, 0, 0, 0, 0, 18, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -29, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, -13, -7, -10, 0, 0, 0, -7, 0, 0, -13, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -7, -7, -10, 0, 0, 0, 0, -10, -13, -13, -7, 0, 0, 0, 0, 0, -10, 0, 0, 0, -13, 0, 0, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -48, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -12, 0, -9, 0, -6, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -12, -12, -12, 0, 0, 0, 14, 13, 0, 0, -6, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -31, 0, -31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -13, 0, -17, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 2, 0, 0, 0, 0, -13, -16, -25, 0, 0, -31, 0, 0, -20, -51, 0, -6, 0, 0, -13, 0, -49, 0, 0, 0, 0, 0, -13, 0, 0, -16, 2, 2, 2, 34, -16, -13, -16, 0, 0, 0, 34, 34, 0, -16, 0, 0, 0, -13, 0, 2, 34, 34, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, -44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, + 0, 0, -36, 0, 0, 0, 0, 0, 0, -68, -27, 0, -47, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -11, 0, 0, -11, 0, -37, 0, -31, -15, 0, -39, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -42, 0, -38, -25, -79, 0, -32, -70, 0, 0, -32, -52, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -37, -37, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -37, 0, 0, -9, -50, -9, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -34, -35, 0, -18, -18, -13, 0, -62, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -8, -16, -8, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, -38, -1, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, -6, -6, -5, 0, 0, 0, 0, -5, 0, 0, -6, 0, 0, 0, 0, 0, -5, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, 0, 0, 0, -54, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -8, 0, 0, -12, 0, 0, 0, -3, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -3, -6, 0, 0, 0, 18, -6, 0, 0, -3, 0, 0, 18, 18, 0, -6, -2, -2, -2, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2, 0, -13, 0, 0, 0, -23, -13, -23, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -36, 0, -13, -36, 0, 0, 0, 0, 0, 3, 0, 0, -23, -32, -35, -1, 0, 0, -24, -31, -37, -32, -27, -27, -14, -21, -23, 0, 0, -52, -23, 0, 0, 0, 0, -73, 0, -55, 0, -20, 0, 0, 0, -13, -48, -48, -55, 0, 0, 0, 36, -55, -57, -40, -32, 0, 0, 36, 36, 0, -35, 0, 0, 0, -48, 0, 0, 57, 36, -54, -54, 0, 0, -13, 0, 0, 0, 0, -37, -23, -37, -31, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -23, -38, -23, -38, -22, -12, 0, 0, 0, -13, 0, 0, 0, 0, -25, -29, -25, 0, -30, -34, 0, -16, -38, 0, 0, 6, 0, 0, 0, 0, 0, -32, -25, -26, 0, 0, 0, -6, -18, 0, 0, 0, 0, 0, 0, -23, 0, 0, -23, -48, 0, 0, 0, 0, 0, 0, -39, 0, 0, -12, 0, 0, -16, -25, -41, -26, 0, 0, 0, 34, -44, 0, 0, -25, 0, 0, 34, 34, 0, -26, 0, 0, 0, 0, 0, 0, 16, 34, -48, -48, 0, 0, -16, 0, 0, 0, 0, 0, -7, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, 0, 0, -16, -38, 0, 0, 0, 0, 0, 0, 0, -57, -61, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2, 0, -20, 0, 0, 0, -17, -38, -17, -38, -12, 0, 0, 0, 0, -13, 0, 0, 0, 0, -20, -29, -25, 0, -10, -30, 0, -10, -25, 0, 0, 2, 8, 0, 0, 0, 0, 0, -23, -21, 0, 0, 0, -13, -13, 0, 0, 0, 0, 0, 0, -17, 0, 0, -23, -17, 0, 0, 0, 0, 0, 0, -30, 0, 0, -12, 0, 0, 0, -23, -23, -21, 0, 0, 0, 0, -21, 0, 0, -23, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, -40, -45, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -17, 0, -17, 0, 0, -15, 0, 0, -9, -16, 0, 0, -9, 0, 0, -23, -20, 0, -22, 0, 0, -22, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, -16, -37, 0, -3, 0, 0, 0, 0, -35, 0, 0, 0, 0, 0, -18, -23, -23, -11, 0, 0, 0, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, -11, -12, -12, -12, 0, 0, 0, 34, 0, 0, 0, 0, -9, -42, -13, -12, 0, 0, -25, -35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, -26, -26, -26, -26, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -18, -34, 0, -18, -13, 3, 0, 0, 0, 0, 0, 0, 0, 0, -42, -45, 0, -8, 0, -19, -27, 0, -16, 0, 0, 0, 0, -26, 0, 0, -36, -46, 0, -3, 0, 0, -16, 0, -46, 0, -25, 0, 0, 0, -34, -69, -62, -45, -8, -23, -8, 0, -45, -16, 0, -79, 0, 0, 0, 0, 0, -67, 3, 3, 3, -16, 0, -8, 34, 0, 0, 0, 0, 0, -18, 0, 3, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -64, 0, 0, 0, 0, 0, 0, 0, -70, -75, 0, 0, 0, -13, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, -13, -15, 0, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 13, 34, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, -2, 0, 0, 0, 0, 6, 0, 0, 5, -2, 0, 0, 0, 0, 6, 0, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -32, 0, -6, 0, 0, 0, 0, -38, -13, 0, -13, 0, 5, -3, -24, 0, 0, 0, 0, 0, 0, 0, 0, -15, -20, -25, -17, 6, 0, -17, 0, -52, -10, -56, -53, 0, -34, 0, -38, 0, 0, -9, -5, 1, 0, 0, -4, -12, 0, -47, -27, 0, 0, -13, 0, -29, -13, -13, 0, -28, -38, 0, 0, -21, -13, 0, -13, 0, 0, 0, -13, 0, 0, -9, 1, 1, 1, 0, -9, 0, -47, 0, 0, 0, 0, 0, 0, -9, -52, -52, -52, 0, 0, 1, 0, 0, 0, 0, 0, 0, -17, 0, -52, 0, 0, -12, -27, -12, -4, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, -69, -63, 0, 0, 0, -13, 0, -63, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, -18, -43, 0, -18, -23, -18, 0, -18, -18, 0, 0, 0, 0, -16, 0, 11, -16, 0, -23, -16, -32, 0, 0, 0, 0, -27, 0, 0, 0, 0, 5, 38, 12, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 0, 0, 0, -32, 0, 0, 11, 0, 0, 38, 0, -23, -23, -23, 0, 11, 5, 0, 0, 23, 23, 0, -18, -16, -43, -20, 16, 16, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, + 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -31, 0, -20, -20, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -31, -31, -31, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -31, 0, 0, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, -18, -11, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -10, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -35, 0, -26, -21, -11, -46, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -7, -5, -9, -8, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -7, 4, 0, 0, 0, 0, 0, 0, -35, -35, -35, 0, 0, 0, 16, 0, 0, 0, -11, 0, 0, 0, -35, 0, 0, 0, -5, 0, 0, -89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -23, -8, -23, -5, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -25, 0, 0, 0, 0, 0, 2, 0, 0, -6, -8, -8, -18, 3, 2, 0, -7, -16, 0, 0, 8, 0, 0, -8, 0, 0, -16, -8, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, -7, -8, -8, -8, 3, 3, 3, 41, -8, 0, 0, -8, 0, 0, 41, 56, 2, -8, 0, 0, 0, 0, 0, 3, 18, 54, -32, -32, 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, -7, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, -36, 0, 0, -7, -23, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 40, 26, 0, 0, 0, 0, 0, 0, 0, 0, 35, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -23, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, -23, 0, 0, -20, -50, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -9, 0, 3, 0, 0, 0, 0, 0, 0, -9, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -25, -13, -25, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -16, 5, 0, -23, -23, 0, 0, 0, 0, 0, -13, 0, 0, -5, -8, 8, 2, 5, 3, 0, 8, 0, 6, 4, 6, 0, -13, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -5, -5, -8, 2, 2, 2, 5, -8, 0, 6, -5, 0, 5, 5, 5, 5, -8, -23, -23, -23, 0, 5, 2, 14, 5, 0, 0, 0, 0, 0, 0, -23, -9, -9, 8, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 6, -32, 0, 0, 0, -25, 6, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, -18, -13, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -29, -29, -29, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 1, -1, 1, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -20, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 1, 0, 0, -13, 0, -2, -2, 0, -3, 0, 0, 0, 2, -4, -13, 0, 3, 0, -3, -7, 0, 9, 8, 0, 0, -7, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, -2, 2, 2, -4, 0, 0, 0, 0, -4, 0, 9, 2, 0, 1, 0, 0, 3, -4, -13, -13, -13, 0, 1, 0, 18, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -7, 0, -7, -3, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -16, 6, 0, 0, 0, -2, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, -20, -50, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -16, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -18, -14, -18, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -16, 0, 0, -27, 1, 0, -25, -13, 0, 0, 0, 0, -13, 0, 0, -32, -5, -7, 6, 0, 0, 0, -1, 7, 0, 0, 0, 0, 0, -14, 0, 0, -16, -14, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, -5, -5, -7, 0, 0, 0, 8, -7, 0, 0, -5, 0, 1, 8, 8, 0, -7, -13, -38, -13, 0, 1, 0, 1, 8, 0, 0, 0, 0, 0, 0, -13, -18, -18, 7, 0, 7, -1, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 3, -32, 0, 0, 0, -48, 3, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -29, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, -23, -20, 0, 0, 0, 0, 0, -13, 0, 0, -2, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -5, 0, 0, 0, 0, -5, 0, 0, -2, 0, 0, 0, 0, 0, -5, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -32, 0, 0, 0, -29, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -2, -9, 6, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, -9, 0, 0, -13, -9, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, -2, -2, -9, 0, 0, 0, 0, -9, 0, 0, -2, 0, 0, 0, 0, 0, -9, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 6, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -8, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -2, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -31, 0, -18, -11, -33, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, -54, -54, -31, 0, 0, 0, 0, 0, 0, 0, -33, -12, 0, 0, -60, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -13, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -27, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 12, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -20, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, -2, -28, 0, -2, -28, 0, 0, 0, 0, -3, -3, 0, 0, 0, -4, -11, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, -11, 0, 0, 0, 0, -11, 0, 0, -4, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -2, 0, 0, 0, 0, 0, 0, 0, -4, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, -20, -50, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -36, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -2, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -62, 0, -34, -11, -35, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, -62, -62, -62, 0, 0, 0, 0, 0, 0, 0, -35, -12, 0, 0, -62, 0, 0, 0, 0, 0, 0, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -20, -18, 0, 0, -20, -13, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -52, 0, -25, -23, -16, -36, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -52, -52, -52, 0, 0, 0, 0, 0, -13, -13, -36, -18, 0, 0, -79, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -48, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -27, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -13, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -36, 0, -16, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -36, -36, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -41, 0, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, -16, -23, 0, -20, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -50, 0, 0, 0, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -20, -20, -20, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 42, 41, 0, 0, 0, 0, 0, 0, 0, 0, 30, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 26, 26, 5, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 39, 26, 39, 66, 0, 0, 0, 0, 0, 27, 66, 66, 39, 0, 36, 36, 36, 0, 56, 39, 20, 66, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -51, 0, -20, -20, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -51, -51, -51, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -51, 0, 0, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, -18, -11, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -10, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -55, 0, -26, -21, -11, -46, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -7, -5, -9, -8, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -7, 4, 0, 0, 0, 0, 0, 0, -55, -55, -55, 0, 0, 0, 16, 0, 0, 0, -11, 0, 0, 0, -55, 0, 0, 0, -5, 0, 0, -89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -60, 0, -20, -20, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -60, -60, -60, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -60, 0, 0, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, -18, -11, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 13, 0, 0, 0, 0, 3, 49, 23, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 16, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 16, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 7, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 32, 16, 0, 0, 0, 0, 0, 12, 16, 16, 11, 0, 16, 16, 16, 0, 12, 32, 18, 16, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 16, 16, 0, 0, 0, 0, 0, 16, 16, 16, 16, 0, 13, 13, 13, 0, 14, 13, 16, 16, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 43, 39, 5, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 43, 43, 43, 66, 0, 0, 0, 0, 0, 27, 66, 66, 39, 0, 36, 36, 36, 0, 56, 43, 43, 66, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 43, 43, 5, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 43, 43, 43, 66, 0, 0, 0, 0, 0, 27, 66, 66, 43, 0, 36, 36, 36, 0, 56, 43, 20, 66, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 43, 26, 5, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 43, 43, 43, 66, 0, 0, 0, 0, 0, 27, 66, 66, 39, 0, 36, 36, 36, 0, 56, 43, 20, 66, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 42, 41, 0, 0, 0, 0, 0, 0, 0, 0, 30, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -36, 0, 0, 0, 0, 0, 0, -70, -46, 0, -46, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -11, 0, 0, -11, 0, -37, 0, -31, -15, 0, -39, 0, -70, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -45, 0, -38, -25, -46, 0, -32, -70, 0, 0, -32, -46, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -37, -37, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -37, 0, 0, -9, -50, -9, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -34, -35, 0, -18, -18, -13, 0, -62, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 5, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 4, 0, 14, 0, 27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 32, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -36, 0, 0, 0, 0, 0, 0, -68, -41, 0, -41, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -11, 0, 0, -11, 0, -37, 0, -31, -15, 0, -39, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -40, 0, -38, -25, -41, 0, -32, -70, 0, 0, -32, -41, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -37, -37, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -37, 0, 0, -9, -50, -9, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -34, -35, 0, -18, -18, -13, 0, -62, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 5, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 4, 0, 13, 0, 27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 16, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -36, 0, 0, 0, 0, 0, 0, -66, -27, 0, -47, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -11, 0, 0, -11, 0, -37, 0, -31, -15, 0, -57, 0, -66, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -42, 0, -38, -25, -79, 0, -32, -66, 0, 0, -32, -52, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -37, -37, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -37, 0, 0, -9, -50, -9, 0, -57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, -34, -48, 0, -18, -18, -13, 0, -48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 13, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 4, 0, 13, 0, 27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 16, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2, 0, -13, 0, 0, 0, -39, -13, -39, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -36, 0, -13, -36, 0, 0, 0, 0, 0, 3, 0, 0, -23, -32, -35, -1, 0, 0, -24, -31, -37, -56, -27, -27, -34, -37, -39, 0, 0, -52, -39, 0, 0, 0, 0, -56, 0, -39, 0, -20, 0, 0, 0, -13, -48, -48, -55, 0, 0, 0, 36, -55, -56, -40, -32, 0, 0, 36, 36, 0, -35, 0, 0, 0, -56, 0, 0, 57, 36, -54, -54, 0, 0, -13, 0, 0, 0, 0, -37, -23, -37, -31, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2, 0, -13, 0, 0, 0, -39, -13, -39, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -36, 0, -13, -36, 0, 0, 0, 0, 0, 3, 0, 0, -23, -32, -35, -1, 0, 0, -24, -31, -37, -32, -27, -27, -14, -21, -39, 0, 0, -52, -39, 0, 0, 0, 0, -73, 0, -39, 0, -20, 0, 0, 0, -13, -48, -48, -55, 0, 0, 0, 36, -55, -57, -40, -32, 0, 0, 36, 36, 0, -35, 0, 0, 0, -48, 0, 0, 57, 36, -54, -54, 0, 0, -13, 0, 0, 0, 0, -37, -23, -37, -31, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2, 0, -13, 0, 0, 0, -23, -13, -23, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -61, 0, -13, -84, 0, 0, 0, 0, 0, 3, 0, 0, -23, -32, -35, -1, 0, 0, -24, -31, -37, -32, -27, -27, -14, -21, -23, 0, 0, -52, -23, 0, 0, 0, 0, -73, 0, -55, 0, -20, 0, 0, 0, -13, -48, -48, -55, 0, 0, 0, 36, -55, -57, -40, -32, 0, 0, 36, 36, 0, -35, 0, 0, 0, -48, 0, 0, 57, 36, -54, -54, 0, 0, -13, 0, 0, 0, 0, -37, -23, -37, -31, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -37, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, -25, -16, 0, 0, -24, -24, -16, -24, -23, -22, -23, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, -24, -24, -25, 0, 0, 0, 0, -25, -24, -23, -24, 0, 0, 0, 0, 0, -25, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 3, 9, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, 0, 0, 0, 0, 0, 12, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, 0, 0, 0, 0, -22, 0, 7, 0, 0, 0, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -8, -29, -29, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 41, 41, 0, 0, 0, 0, 0, 0, 0, 0, 42, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -10, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -66, 0, -26, -21, -11, -46, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -7, -5, -9, -8, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -7, 4, 0, 0, 0, 0, 0, 0, -66, -66, -66, 0, 0, 0, 16, 0, 0, 0, -11, 0, 0, 0, -66, 0, 0, 0, -5, 0, 0, -89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -15, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, -38, -38, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -16, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 29, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 41, 41, 0, 0, 0, 0, 0, 0, 0, 0, 35, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 39, 43, 5, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 39, 39, 39, 66, 0, 0, 0, 0, 0, 27, 66, 66, 43, 0, 36, 36, 36, 0, 56, 39, 38, 81, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 8, 8, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -46, 0, -46, 0, 0, -15, 0, 0, -9, -16, 0, 0, -9, 0, 0, -20, -20, 0, -22, 0, 0, -22, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, 0, 0, -36, -46, 0, -3, 0, 0, 0, 0, -46, 0, 0, 0, 0, 0, -18, -23, -23, -11, 0, 0, 0, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, -11, -12, -12, -12, 0, 0, 0, 34, 0, 0, 0, 0, -9, -42, -13, -12, 0, 0, -25, -35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -13, 0, -16, -10, -22, -18, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 10, 0, 0, 0, -42, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -13, 0, -36, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -16, -40, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -13, 0, 0, 0, 0, 0, 0, -13, -13, 8, -13, 8, 0, -7, 0, 0, 0, -5, 0, 0, -4, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -8, -12, 0, 0, 0, 0, -12, 0, -11, -12, 0, 0, -13, 0, 0, 0, -13, 0, 0, -13, 0, 0, -12, -13, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, -8, 0, -11, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -17, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 8, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -50, -13, -50, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -16, 5, 0, -23, -23, 0, 0, 0, 0, 0, -13, 0, 0, -5, -8, 8, 2, 5, 3, 0, 8, 0, 6, 4, 6, 0, -13, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -5, -5, -8, 2, 2, 2, 5, -8, 0, 6, -5, 0, 5, 5, 5, 5, -8, -23, -23, -23, 0, 5, 2, 14, 5, 0, 0, 0, 0, 0, 0, -23, -9, -9, 8, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 6, -61, 0, 0, 0, -50, 6, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, -43, 0, -11, -68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -14, -11, -7, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, -43, -43, -43, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -43, 0, 0, -13, 0, -47, 0, -68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -68, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, -26, -26, -26, -26, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -18, -34, 0, -18, -13, 3, 0, 0, 0, 0, 0, 0, 0, 0, -42, -87, 0, -8, 0, -19, -27, 0, -16, 0, 0, 0, 0, -26, 0, 0, -36, -46, 0, -3, 0, 0, -16, 0, -46, 0, -25, 0, 0, 0, -34, -69, -62, -87, -8, -23, -8, 0, -87, -16, 0, -79, 0, 0, 0, 0, 0, -87, 3, 3, 3, -16, 0, -8, 34, 0, 0, 0, 0, 0, -18, 0, 3, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -64, 0, 0, 0, 0, 0, 0, 0, -70, -75, 0, 0, 0, -13, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -8, -16, 4, -8, 0, -8, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -4, 0, -4, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -8, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, 0, -12, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -4, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 40, 26, 0, 0, 0, 0, 0, 0, 0, 0, 30, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -10, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -35, 0, -26, -21, -11, -46, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -7, -5, -9, -8, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -7, 4, 0, 0, 0, 0, 0, 0, -35, -35, -35, 0, 0, 0, 32, 0, 0, 0, -11, 0, 0, 0, -35, 0, 0, 0, -5, 0, 0, -89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -46, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, -26, -39, -26, -39, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -18, -34, 0, -18, -13, 3, 0, 0, 0, 0, 0, 0, 0, 0, -42, -45, 0, -8, 0, -19, -27, 0, -16, 0, 0, 0, 0, -26, 0, 0, -36, -46, 0, -3, 0, 0, -16, 0, -46, 0, -25, 0, 0, 0, -34, -69, -62, -45, -8, -23, -8, 0, -45, -16, 0, -79, 0, 0, 0, 0, 0, -67, 3, 3, 3, -16, 0, -8, 34, 0, 0, 0, 0, 0, -18, 0, 3, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -39, 0, 0, 0, 0, 0, 0, 0, -70, -75, 0, 0, 0, -13, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, -43, 0, -43, -38, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, -59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -25, -45, 0, 0, 0, 0, 0, -57, -34, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -50, 0, 0, -27, 0, 0, 0, -21, -52, -21, -52, -57, -18, 0, 0, 0, -38, 0, -25, -16, 0, -23, -38, -23, 0, -23, -55, 0, -23, -50, 0, 0, 0, 0, 0, 0, 0, 0, -16, -23, -25, 0, 0, 0, -12, -28, 4, -16, 4, 0, 0, 0, -39, 0, 0, -34, -21, 0, 0, 0, 0, -34, -4, -21, 0, 0, -27, 0, 0, 0, -23, -23, -25, 0, 0, 0, 0, -25, -16, 4, -23, 0, 0, 0, 0, 0, -25, 0, 0, 0, -16, 0, 0, 0, 0, -20, -36, 0, 0, -23, 0, 0, -48, -25, 4, 0, 4, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, -36, -25, 0, -43, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, 0, 0, 0, 0, 0, -20, -13, 0, 0, 0, 0, 0, 0, -38, -38, -38, 0, 0, 0, 0, 0, 20, 20, 0, 0, 0, -25, -59, 25, 25, -20, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, -50, -38, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 0, -16, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, + -12, -5, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -38, 0, -8, 0, -3, -11, -10, -5, 0, 0, 0, -40, -18, 0, 0, -18, 0, -13, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, -8, -18, 0, -13, 0, 0, 0, -17, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, 0, -38, 0, -54, -73, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 5, -3, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, -16, 0, -23, 0, -57, -40, 0, -70, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 3, 0, -23, -23, -37, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -7, -29, 0, 0, 0, 0, 0, 0, -70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -6, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -27, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -10, -12, 20, }; static constexpr EpdFontData inter_12_bold = { inter_12_boldBitmaps, inter_12_boldGlyphs, inter_12_boldIntervals, - 63, + 77, 30, 25, -7, @@ -5265,10 +6015,10 @@ static constexpr EpdFontData inter_12_bold = { inter_12_boldKernLeftClasses, inter_12_boldKernRightClasses, inter_12_boldKernMatrix, - 777, - 776, - 153, - 145, + 781, + 797, + 154, + 149, nullptr, 0, }; diff --git a/lib/EpdFont/builtinFonts/inter_12_regular.h b/lib/EpdFont/builtinFonts/inter_12_regular.h index 3f6f00df7a..5fe3989040 100644 --- a/lib/EpdFont/builtinFonts/inter_12_regular.h +++ b/lib/EpdFont/builtinFonts/inter_12_regular.h @@ -3,12 +3,12 @@ * name: inter_12_regular * size: 12 * mode: 1-bit - * Command used: fontconvert.py inter_12_regular 12 ../builtinFonts/source/Inter/Inter-Regular.ttf ../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-Regular.ttf --additional-intervals 0x05D0,0x05EA + * Command used: fontconvert.py inter_12_regular 12 ../builtinFonts/source/Inter/Inter-Regular.ttf ../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-Regular.ttf ../builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf --additional-intervals 0x05D0,0x05EA --additional-intervals 0x060C,0x060C --additional-intervals 0x061B,0x061B --additional-intervals 0x061F,0x061F --additional-intervals 0x0621,0x0621 --additional-intervals 0x0640,0x0640 --additional-intervals 0x0660,0x0669 --additional-intervals 0x06BA,0x06BA --additional-intervals 0x06D4,0x06D4 --additional-intervals 0x06F0,0x06F9 --additional-intervals 0xFB56,0xFB59 --additional-intervals 0xFB66,0xFB69 --additional-intervals 0xFB7A,0xFB7D --additional-intervals 0xFB88,0xFB95 --additional-intervals 0xFB9E,0xFB9F --additional-intervals 0xFBA6,0xFBB1 --additional-intervals 0xFBFC,0xFBFF --additional-intervals 0xFE80,0xFEFC */ #pragma once #include "EpdFontData.h" -static const uint8_t inter_12_regularBitmaps[33906] = { +static const uint8_t inter_12_regularBitmaps[40694] = { 0x63, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x38, 0x00, 0x77, 0x9C, 0x40, 0x67, 0xDF, 0xBF, 0x7E, 0xFD, 0xFB, 0xF7, 0x06, 0x08, 0x06, 0x1C, 0x06, 0x1C, 0x0E, 0x18, 0x0E, 0x18, 0x3F, 0xFE, 0x7F, 0xFF, 0x7F, 0xFE, 0x0C, 0x38, 0x1C, 0x30, 0x1C, 0x30, 0x1C, 0x70, 0xFF, 0xFE, 0xFF, 0xFC, @@ -903,1232 +903,1656 @@ static const uint8_t inter_12_regularBitmaps[33906] = { 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x73, 0x8E, 0x70, 0x39, 0xDC, 0xC0, 0x3C, 0x7E, 0x77, 0xE7, 0x33, 0x7F, 0xFF, 0x00, 0xFF, 0xFE, 0xC7, 0xE6, 0x7E, 0x3C, 0x6F, 0xF4, 0xE3, 0x73, 0xF9, 0xC8, 0x40, 0xF7, 0x8C, 0xE0, 0x38, 0xFB, 0xFF, 0x3E, 0xEF, 0xCE, 0x00, 0x33, 0x77, 0x66, - 0xEE, 0xCC, 0xE7, 0x7E, 0x3E, 0x1C, 0x77, 0x3B, 0x8E, 0xE3, 0x30, 0x31, 0xCE, 0x73, 0xB9, 0xCC, - 0x31, 0xCE, 0x73, 0xB9, 0xCC, 0x77, 0x76, 0xEC, 0x6F, 0xF4, 0x37, 0x77, 0xE6, 0x61, 0x87, 0x9E, - 0x7B, 0xE0, 0x00, 0x33, 0xB9, 0xCF, 0xFC, 0xC0, 0xE7, 0x7E, 0x3E, 0x1C, 0x00, 0xE0, 0x18, 0x07, - 0x00, 0xC0, 0x38, 0x06, 0x01, 0xC0, 0x30, 0x0E, 0x01, 0x80, 0x70, 0x0C, 0x03, 0x80, 0x60, 0x1C, - 0x03, 0x00, 0x60, 0x00, 0x00, 0x06, 0x00, 0x18, 0x00, 0x70, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, - 0x18, 0x00, 0x70, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x60, 0x01, 0xC0, 0x03, 0x00, - 0x0E, 0x00, 0x18, 0x00, 0x60, 0x01, 0xC0, 0x03, 0x00, 0x00, 0x73, 0x7F, 0xFE, 0x00, 0x77, 0x6E, - 0xE0, 0xFF, 0xFF, 0xF8, 0x7C, 0x3E, 0x18, 0x00, 0xFF, 0xFF, 0x00, 0x7F, 0xFF, 0x00, 0x01, 0xDB, - 0xB7, 0x6E, 0xDD, 0x80, 0xFF, 0xF7, 0xC7, 0x1C, 0x20, 0x00, 0x3F, 0xFF, 0xFF, 0xFD, 0xF0, 0xC0, - 0x0E, 0x07, 0x80, 0xE0, 0xE7, 0x7F, 0xDB, 0xE6, 0xF0, 0x78, 0x0E, 0x00, 0x00, 0x10, 0x9F, 0xFF, - 0xF9, 0xE7, 0x7F, 0xB9, 0xC0, 0x19, 0x86, 0x39, 0xFF, 0xBF, 0xF3, 0x1C, 0x33, 0x00, 0x18, 0x79, - 0xFE, 0xF1, 0x83, 0x00, 0x00, 0x38, 0x0F, 0xC1, 0xFC, 0x0F, 0xC1, 0xF1, 0xFF, 0xF8, 0xF8, 0x30, - 0x00, 0x7F, 0x6E, 0x40, 0x3C, 0xFE, 0xE7, 0xC7, 0x38, 0x3C, 0x38, 0x00, 0xDD, 0xF1, 0xE7, 0xCD, - 0xC1, 0x00, 0x00, 0x1C, 0xFF, 0xCF, 0xC3, 0xC1, 0x80, 0x01, 0xC3, 0xF0, 0xF7, 0xFE, 0x30, 0x00, - 0x00, 0x23, 0xC1, 0xC7, 0xC7, 0x07, 0xBC, 0x3E, 0xD9, 0xE7, 0x64, 0x19, 0xC0, 0x4F, 0x73, 0x7F, - 0x60, 0x6F, 0xF4, 0x00, 0xCF, 0xBF, 0x71, 0xE4, 0x00, 0x33, 0x3F, 0xEC, 0xCF, 0x33, 0xFF, 0xDF, - 0xE0, 0x31, 0xC6, 0x1F, 0x78, 0x63, 0x80, 0x40, 0x00, 0xBC, 0x00, 0xE7, 0xF3, 0xF0, 0x7F, 0xF8, - 0x03, 0xF0, 0x00, 0x40, 0x00, 0xBC, 0x00, 0xE7, 0xF3, 0xF0, 0x7F, 0xF8, 0x03, 0xF0, 0x00, 0x7F, - 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0x7F, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0x3F, 0x80, 0x03, 0x7F, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFE, 0xC0, 0x07, 0xFC, 0x00, 0xC0, 0x03, 0xFF, 0x03, 0xFF, 0xF1, 0xE0, 0x1E, - 0xE0, 0x01, 0xC0, 0x00, 0x03, 0x00, 0x00, 0x3C, 0xFF, 0xFF, 0xEF, 0xFF, 0xFE, 0x00, 0x01, 0xC0, - 0x00, 0x38, 0x39, 0xFB, 0x33, 0xEF, 0xD9, 0xBF, 0x3E, 0x39, 0xFB, 0x37, 0xEF, 0xD9, 0xBF, 0x1C, - 0xFF, 0xFF, 0xF0, 0x3C, 0x7E, 0x66, 0x67, 0x67, 0x66, 0x7E, 0x3C, 0xCF, 0x3C, 0xF3, 0xCF, 0x3F, - 0xDF, 0x3C, 0xFD, 0x9B, 0x06, 0x0C, 0xDF, 0x9E, 0x06, 0x0C, 0x19, 0xF7, 0xEC, 0xF9, 0xF3, 0x66, - 0xFC, 0xF8, 0xC3, 0x0C, 0x3E, 0xFF, 0x3C, 0xF3, 0xCF, 0x3C, 0xC0, 0xFB, 0xBF, 0xFC, 0xCF, 0x33, - 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0xF7, 0xB9, 0x8C, 0x63, 0x18, 0x23, 0xBF, 0xF7, 0x39, 0xCE, 0x39, - 0xC0, 0x66, 0x66, 0x7E, 0x3E, 0x3C, 0x3C, 0x18, 0x18, 0xE6, 0xDD, 0xF1, 0xC3, 0x8F, 0x9B, 0xF3, - 0x3C, 0x01, 0xC0, 0x0E, 0x00, 0xE0, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, - 0x0E, 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, - 0x00, 0xFF, 0xFF, 0xFF, 0x31, 0xC7, 0x9C, 0x39, 0xC1, 0x08, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, + 0xEE, 0xCC, 0xE7, 0x7E, 0x3E, 0x1C, 0x77, 0x3B, 0x8E, 0xE3, 0x30, 0x37, 0x7E, 0xEE, 0xE0, 0x31, + 0xCE, 0x73, 0xB9, 0xCC, 0x31, 0xCE, 0x73, 0xB9, 0xCC, 0x77, 0x76, 0xEC, 0x6F, 0xF4, 0x37, 0x77, + 0xE6, 0x61, 0x87, 0x9E, 0x7B, 0xE0, 0x00, 0x33, 0xB9, 0xCF, 0xFC, 0xC0, 0xE7, 0x7E, 0x3E, 0x1C, + 0x00, 0xE0, 0x18, 0x07, 0x00, 0xC0, 0x38, 0x06, 0x01, 0xC0, 0x30, 0x0E, 0x01, 0x80, 0x70, 0x0C, + 0x03, 0x80, 0x60, 0x1C, 0x03, 0x00, 0x60, 0x00, 0x00, 0x06, 0x00, 0x18, 0x00, 0x70, 0x00, 0xC0, + 0x03, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x70, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x60, + 0x01, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x60, 0x01, 0xC0, 0x03, 0x00, 0x00, 0x73, 0x7F, + 0xFE, 0x00, 0x77, 0x6E, 0xE0, 0xFF, 0xFF, 0xF8, 0x7C, 0x3E, 0x18, 0x00, 0xFF, 0xFF, 0x00, 0x7F, + 0xFF, 0x00, 0x01, 0xDB, 0xB7, 0x6E, 0xDD, 0x80, 0xFF, 0xF7, 0xC7, 0x1C, 0x20, 0x00, 0x3F, 0xFF, + 0xFF, 0xFD, 0xF0, 0xC0, 0x0E, 0x07, 0x80, 0xE0, 0xE7, 0x7F, 0xDB, 0xE6, 0xF0, 0x78, 0x0E, 0x00, + 0x00, 0x10, 0x9F, 0xFF, 0xF9, 0xE7, 0x7F, 0xB9, 0xC0, 0x19, 0x86, 0x39, 0xFF, 0xBF, 0xF3, 0x1C, + 0x33, 0x00, 0x18, 0x79, 0xFE, 0xF1, 0x83, 0x00, 0x00, 0x38, 0x0F, 0xC1, 0xFC, 0x0F, 0xC1, 0xF1, + 0xFF, 0xF8, 0xF8, 0x30, 0x00, 0x7F, 0x6E, 0x40, 0x3C, 0xFE, 0xE7, 0xC7, 0x38, 0x3C, 0x38, 0x00, + 0xDD, 0xF1, 0xE7, 0xCD, 0xC1, 0x00, 0x00, 0x1C, 0xFF, 0xCF, 0xC3, 0xC1, 0x80, 0x01, 0xC3, 0xF0, + 0xF7, 0xFE, 0x30, 0x00, 0x00, 0x23, 0xC1, 0xC7, 0xC7, 0x07, 0xBC, 0x3E, 0xD9, 0xE7, 0x64, 0x19, + 0xC0, 0x4F, 0x73, 0x7F, 0x60, 0x6F, 0xF4, 0x00, 0xCF, 0xBF, 0x71, 0xE4, 0x00, 0x33, 0x3F, 0xEC, + 0xCF, 0x33, 0xFF, 0xDF, 0xE0, 0x31, 0xC6, 0x1F, 0x78, 0x63, 0x80, 0x40, 0x00, 0xBC, 0x00, 0xE7, + 0xF3, 0xF0, 0x7F, 0xF8, 0x03, 0xF0, 0x00, 0x40, 0x00, 0xBC, 0x00, 0xE7, 0xF3, 0xF0, 0x7F, 0xF8, + 0x03, 0xF0, 0x00, 0x7F, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0x7F, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0x3F, + 0x80, 0x03, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xC0, 0x07, 0xFC, 0x00, 0xC0, 0x03, 0xFF, 0x03, + 0xFF, 0xF1, 0xE0, 0x1E, 0xE0, 0x01, 0xC0, 0x00, 0x03, 0x00, 0x00, 0x3C, 0xFF, 0xFF, 0xEF, 0xFF, + 0xFE, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x39, 0xFB, 0x33, 0xEF, 0xD9, 0xBF, 0x3E, 0x39, 0xFB, 0x37, + 0xEF, 0xD9, 0xBF, 0x1C, 0xFF, 0xFF, 0xF0, 0x3C, 0x7E, 0x66, 0x67, 0x67, 0x66, 0x7E, 0x3C, 0xCF, + 0x3C, 0xF3, 0xCF, 0x3F, 0xDF, 0x3C, 0xFD, 0x9B, 0x06, 0x0C, 0xDF, 0x9E, 0x06, 0x0C, 0x19, 0xF7, + 0xEC, 0xF9, 0xF3, 0x66, 0xFC, 0xF8, 0xC3, 0x0C, 0x3E, 0xFF, 0x3C, 0xF3, 0xCF, 0x3C, 0xC0, 0xFB, + 0xBF, 0xFC, 0xCF, 0x33, 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0xF7, 0xB9, 0x8C, 0x63, 0x18, 0x23, 0xBF, + 0xF7, 0x39, 0xCE, 0x39, 0xC0, 0x66, 0x66, 0x7E, 0x3E, 0x3C, 0x3C, 0x18, 0x18, 0xE6, 0xDD, 0xF1, + 0xC3, 0x8F, 0x9B, 0xF3, 0x3C, 0x01, 0xC0, 0x0E, 0x00, 0xE0, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, - 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, 0x83, 0xFF, 0xF0, 0x03, - 0xC0, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x01, 0xE0, 0x00, 0x3C, 0xE0, 0x07, 0xFF, 0x00, 0xFF, 0xF0, - 0x1F, 0x8F, 0x03, 0xC0, 0xF0, 0x78, 0x0E, 0x0F, 0x01, 0xC1, 0xE0, 0x38, 0x3C, 0x07, 0x07, 0x81, - 0xE0, 0xF0, 0xF8, 0x1E, 0x7E, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x03, 0x80, 0x70, 0x07, 0x00, 0x60, - 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, - 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x03, 0xE0, - 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x1E, 0x78, 0x0F, 0x70, 0x07, 0xF0, 0x07, 0xE0, 0x00, 0xFF, 0xC0, - 0xFF, 0xE0, 0xFF, 0xE0, 0xE0, 0x00, 0xE0, 0x00, 0x70, 0x07, 0x70, 0x07, 0x78, 0x0F, 0x3E, 0x3E, - 0x1F, 0xFC, 0x0F, 0xF8, 0x01, 0xC0, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1E, 0x70, 0x3B, 0x80, - 0x7F, 0x00, 0x1E, 0x00, 0x7F, 0x00, 0xFF, 0x80, 0x7F, 0x00, 0x3E, 0x00, 0x3C, 0x00, 0x7E, 0x01, - 0xFC, 0x0F, 0x78, 0x79, 0xFF, 0xE1, 0xFE, 0x01, 0xE0, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0x80, 0xE3, 0x73, 0xF9, 0xC8, 0x40, 0x00, 0xC0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, - 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x00, 0x60, 0x1C, 0x03, - 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x7C, 0x0F, 0xC1, 0xF8, - 0x3F, 0x07, 0xF1, 0xEF, 0xF8, 0xFE, 0x07, 0x00, 0x0F, 0xFE, 0x00, 0x03, 0xFF, 0x80, 0x00, 0xFF, - 0xE0, 0x00, 0x30, 0x38, 0x00, 0x1C, 0x0E, 0x00, 0x07, 0x03, 0x80, 0x01, 0xC0, 0xE0, 0x00, 0x70, - 0x38, 0x00, 0x1C, 0x0F, 0xFE, 0x07, 0x03, 0xFF, 0xC1, 0xC0, 0xE0, 0xF8, 0x70, 0x38, 0x0F, 0x1C, - 0x0E, 0x01, 0xC7, 0x03, 0x80, 0x71, 0xC0, 0xE0, 0x1C, 0xE0, 0x38, 0x0E, 0x38, 0x0E, 0x0F, 0xBE, - 0x03, 0xFF, 0xCF, 0x00, 0xFF, 0xE0, 0xC0, 0x0C, 0x00, 0x70, 0x07, 0x00, 0x38, 0x03, 0x80, 0x1C, - 0x01, 0xC0, 0x0E, 0x00, 0xE0, 0x07, 0x00, 0x70, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x1C, 0x00, 0xFF, - 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xB8, 0x03, 0x83, 0xDC, 0x01, 0xC0, 0xFE, 0x00, 0xE0, 0x3F, 0x00, - 0x70, 0x1F, 0x80, 0x38, 0x0F, 0xC0, 0x1C, 0x0F, 0xE0, 0x0E, 0x0F, 0x70, 0x07, 0xFF, 0xB8, 0x03, - 0xFF, 0x00, 0xFF, 0xFE, 0x0F, 0xFF, 0xE0, 0xFF, 0xFE, 0x00, 0x78, 0x00, 0x07, 0x80, 0x00, 0x78, - 0x00, 0x07, 0x80, 0x00, 0x78, 0x00, 0x07, 0xFF, 0x80, 0x7F, 0xFC, 0x07, 0xE3, 0xE0, 0x78, 0x0E, - 0x07, 0x80, 0xE0, 0x78, 0x0E, 0x07, 0x80, 0xE0, 0x78, 0x0E, 0x07, 0x80, 0xE0, 0x78, 0x0E, 0x07, - 0x80, 0xE0, 0x03, 0xC0, 0x0E, 0x00, 0x30, 0x01, 0xC0, 0x00, 0x03, 0x00, 0x6E, 0x03, 0xF8, 0x1E, - 0xE0, 0xF3, 0x83, 0x8E, 0x1E, 0x38, 0xF0, 0xE7, 0x83, 0xFC, 0x0F, 0xE0, 0x3F, 0xC0, 0xE7, 0x83, - 0x8F, 0x0E, 0x1C, 0x38, 0x78, 0xE0, 0xF3, 0x81, 0xEE, 0x03, 0xB8, 0x0F, 0x0E, 0x00, 0x0E, 0x00, - 0x1C, 0x00, 0x18, 0x00, 0x00, 0x18, 0x01, 0xB8, 0x07, 0xF0, 0x1F, 0xE0, 0x3F, 0xC0, 0xFF, 0x83, - 0xFF, 0x07, 0x7E, 0x1E, 0xFC, 0x79, 0xF8, 0xE3, 0xF3, 0xC7, 0xE7, 0x0F, 0xDC, 0x1F, 0xF8, 0x3F, - 0xE0, 0x7F, 0x80, 0xFF, 0x01, 0xFC, 0x03, 0xF8, 0x07, 0x0C, 0x70, 0x1C, 0xC0, 0x1F, 0x80, 0x1E, - 0x00, 0x00, 0x0C, 0x00, 0xFC, 0x03, 0xB8, 0x0F, 0x78, 0x1C, 0x70, 0x38, 0xF0, 0xE0, 0xE1, 0xC1, - 0xC7, 0x81, 0xCE, 0x03, 0x9C, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x0F, 0x00, 0x1E, 0x00, 0x38, - 0x00, 0xF0, 0x07, 0xC0, 0x0F, 0x00, 0xC0, 0x0F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, - 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, - 0xF8, 0x07, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0xC1, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x70, 0x00, 0x01, - 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x00, 0x7C, 0x00, 0x77, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x1C, 0x70, - 0x0E, 0x38, 0x07, 0x1C, 0x07, 0x07, 0x03, 0x83, 0x81, 0xFF, 0xC1, 0xFF, 0xF0, 0xE0, 0x38, 0xF0, - 0x1E, 0x70, 0x07, 0x38, 0x03, 0xBC, 0x00, 0xE0, 0xFF, 0xE7, 0xFF, 0x3F, 0xF9, 0xC0, 0x0E, 0x00, - 0x70, 0x03, 0x80, 0x1C, 0x00, 0xFF, 0x87, 0xFF, 0x3F, 0xFD, 0xC0, 0xFE, 0x03, 0xF0, 0x1F, 0x80, - 0xFC, 0x0F, 0xE0, 0xF7, 0xFF, 0xBF, 0xF0, 0xFF, 0x07, 0xFE, 0x3F, 0xFD, 0xC1, 0xEE, 0x07, 0x70, - 0x1F, 0x80, 0xFC, 0x0E, 0xFF, 0xF7, 0xFE, 0x3F, 0xFD, 0xC0, 0xFE, 0x03, 0xF0, 0x1F, 0x80, 0xFC, - 0x07, 0xE0, 0xFF, 0xFF, 0xBF, 0xF8, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, - 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, - 0x00, 0xE0, 0x00, 0x07, 0xFF, 0x01, 0xFF, 0xE0, 0x3F, 0xFC, 0x07, 0x03, 0x80, 0xE0, 0x70, 0x1C, - 0x0E, 0x03, 0x81, 0xC0, 0x70, 0x38, 0x0E, 0x07, 0x01, 0xC0, 0xE0, 0x38, 0x1C, 0x0E, 0x03, 0x81, - 0xC0, 0x70, 0x38, 0x0E, 0x07, 0x01, 0xC1, 0xC0, 0x38, 0x78, 0x07, 0x9F, 0xFF, 0xFB, 0xFF, 0xFF, - 0x70, 0x00, 0x6E, 0x00, 0x0D, 0xC0, 0x01, 0xB8, 0x00, 0x30, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0x00, - 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xEF, 0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, - 0x0E, 0x00, 0xE0, 0x0F, 0xFF, 0xFF, 0xF0, 0x70, 0x18, 0x0C, 0x78, 0x38, 0x1E, 0x38, 0x38, 0x1C, - 0x3C, 0x38, 0x38, 0x1C, 0x38, 0x78, 0x0E, 0x38, 0x70, 0x0F, 0x38, 0xF0, 0x07, 0x38, 0xE0, 0x07, - 0xBD, 0xC0, 0x03, 0xFF, 0xC0, 0x07, 0xFF, 0xC0, 0x07, 0x38, 0xE0, 0x0F, 0x38, 0xF0, 0x0E, 0x38, - 0x70, 0x1C, 0x38, 0x78, 0x3C, 0x38, 0x38, 0x38, 0x38, 0x1C, 0x78, 0x38, 0x1E, 0x70, 0x38, 0x0E, - 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0xF0, 0xE7, 0x07, 0xB0, 0x1C, 0x00, 0xE0, 0x0E, 0x07, 0xF0, 0x3E, - 0x01, 0xFC, 0x00, 0xF0, 0x03, 0x80, 0x1F, 0x80, 0xFE, 0x07, 0x78, 0x7B, 0xFF, 0x87, 0xF8, 0x0E, - 0x00, 0xC0, 0x0D, 0xC0, 0x3F, 0x80, 0xFF, 0x01, 0xFE, 0x07, 0xFC, 0x1F, 0xF8, 0x3B, 0xF0, 0xF7, - 0xE3, 0xCF, 0xC7, 0x1F, 0x9E, 0x3F, 0x38, 0x7E, 0xE0, 0xFF, 0xC1, 0xFF, 0x03, 0xFC, 0x07, 0xF8, - 0x0F, 0xE0, 0x1F, 0xC0, 0x38, 0x1C, 0x60, 0x1D, 0xC0, 0x3F, 0x00, 0x3C, 0x00, 0x00, 0x18, 0x01, - 0xB8, 0x07, 0xF0, 0x1F, 0xE0, 0x3F, 0xC0, 0xFF, 0x83, 0xFF, 0x07, 0x7E, 0x1E, 0xFC, 0x79, 0xF8, - 0xE3, 0xF3, 0xC7, 0xE7, 0x0F, 0xDC, 0x1F, 0xF8, 0x3F, 0xE0, 0x7F, 0x80, 0xFF, 0x01, 0xFC, 0x03, - 0xF8, 0x07, 0xC0, 0x1B, 0x80, 0xFE, 0x07, 0xB8, 0x3C, 0xE0, 0xE3, 0x87, 0x8E, 0x3C, 0x39, 0xE0, - 0xFF, 0x03, 0xF8, 0x0F, 0xF0, 0x39, 0xE0, 0xE3, 0xC3, 0x87, 0x0E, 0x1E, 0x38, 0x3C, 0xE0, 0x7B, - 0x80, 0xEE, 0x03, 0xC0, 0x0F, 0xFE, 0x0F, 0xFF, 0x0F, 0xFF, 0x0E, 0x07, 0x1C, 0x07, 0x1C, 0x07, + 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x31, 0xC7, 0x9C, 0x39, 0xC1, 0x08, 0x00, 0x0F, + 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, + 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, + 0x83, 0xFF, 0xF0, 0x03, 0xC0, 0x00, 0x78, 0x00, 0x0F, 0x00, 0x01, 0xE0, 0x00, 0x3C, 0xE0, 0x07, + 0xFF, 0x00, 0xFF, 0xF0, 0x1F, 0x8F, 0x03, 0xC0, 0xF0, 0x78, 0x0E, 0x0F, 0x01, 0xC1, 0xE0, 0x38, + 0x3C, 0x07, 0x07, 0x81, 0xE0, 0xF0, 0xF8, 0x1E, 0x7E, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x03, 0x80, + 0x70, 0x07, 0x00, 0x60, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, + 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, + 0x0E, 0x00, 0x03, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x1E, 0x78, 0x0F, 0x70, 0x07, 0xF0, 0x07, + 0xE0, 0x00, 0xFF, 0xC0, 0xFF, 0xE0, 0xFF, 0xE0, 0xE0, 0x00, 0xE0, 0x00, 0x70, 0x07, 0x70, 0x07, + 0x78, 0x0F, 0x3E, 0x3E, 0x1F, 0xFC, 0x0F, 0xF8, 0x01, 0xC0, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, + 0x1E, 0x70, 0x3B, 0x80, 0x7F, 0x00, 0x1E, 0x00, 0x7F, 0x00, 0xFF, 0x80, 0x7F, 0x00, 0x3E, 0x00, + 0x3C, 0x00, 0x7E, 0x01, 0xFC, 0x0F, 0x78, 0x79, 0xFF, 0xE1, 0xFE, 0x01, 0xE0, 0xDF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xE3, 0x73, 0xF9, 0xC8, 0x40, 0x00, 0xC0, 0x70, 0x38, 0x1C, 0x0E, + 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, + 0x00, 0x60, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, + 0x7C, 0x0F, 0xC1, 0xF8, 0x3F, 0x07, 0xF1, 0xEF, 0xF8, 0xFE, 0x07, 0x00, 0x0F, 0xFE, 0x00, 0x03, + 0xFF, 0x80, 0x00, 0xFF, 0xE0, 0x00, 0x30, 0x38, 0x00, 0x1C, 0x0E, 0x00, 0x07, 0x03, 0x80, 0x01, + 0xC0, 0xE0, 0x00, 0x70, 0x38, 0x00, 0x1C, 0x0F, 0xFE, 0x07, 0x03, 0xFF, 0xC1, 0xC0, 0xE0, 0xF8, + 0x70, 0x38, 0x0F, 0x1C, 0x0E, 0x01, 0xC7, 0x03, 0x80, 0x71, 0xC0, 0xE0, 0x1C, 0xE0, 0x38, 0x0E, + 0x38, 0x0E, 0x0F, 0xBE, 0x03, 0xFF, 0xCF, 0x00, 0xFF, 0xE0, 0xC0, 0x0C, 0x00, 0x70, 0x07, 0x00, + 0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x0E, 0x00, 0xE0, 0x07, 0x00, 0x70, 0x03, 0x80, 0x38, 0x01, + 0xC0, 0x1C, 0x00, 0xFF, 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xB8, 0x03, 0x83, 0xDC, 0x01, 0xC0, 0xFE, + 0x00, 0xE0, 0x3F, 0x00, 0x70, 0x1F, 0x80, 0x38, 0x0F, 0xC0, 0x1C, 0x0F, 0xE0, 0x0E, 0x0F, 0x70, + 0x07, 0xFF, 0xB8, 0x03, 0xFF, 0x00, 0xFF, 0xFE, 0x0F, 0xFF, 0xE0, 0xFF, 0xFE, 0x00, 0x78, 0x00, + 0x07, 0x80, 0x00, 0x78, 0x00, 0x07, 0x80, 0x00, 0x78, 0x00, 0x07, 0xFF, 0x80, 0x7F, 0xFC, 0x07, + 0xE3, 0xE0, 0x78, 0x0E, 0x07, 0x80, 0xE0, 0x78, 0x0E, 0x07, 0x80, 0xE0, 0x78, 0x0E, 0x07, 0x80, + 0xE0, 0x78, 0x0E, 0x07, 0x80, 0xE0, 0x03, 0xC0, 0x0E, 0x00, 0x30, 0x01, 0xC0, 0x00, 0x03, 0x00, + 0x6E, 0x03, 0xF8, 0x1E, 0xE0, 0xF3, 0x83, 0x8E, 0x1E, 0x38, 0xF0, 0xE7, 0x83, 0xFC, 0x0F, 0xE0, + 0x3F, 0xC0, 0xE7, 0x83, 0x8F, 0x0E, 0x1C, 0x38, 0x78, 0xE0, 0xF3, 0x81, 0xEE, 0x03, 0xB8, 0x0F, + 0x0E, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x18, 0x00, 0x00, 0x18, 0x01, 0xB8, 0x07, 0xF0, 0x1F, 0xE0, + 0x3F, 0xC0, 0xFF, 0x83, 0xFF, 0x07, 0x7E, 0x1E, 0xFC, 0x79, 0xF8, 0xE3, 0xF3, 0xC7, 0xE7, 0x0F, + 0xDC, 0x1F, 0xF8, 0x3F, 0xE0, 0x7F, 0x80, 0xFF, 0x01, 0xFC, 0x03, 0xF8, 0x07, 0x0C, 0x70, 0x1C, + 0xC0, 0x1F, 0x80, 0x1E, 0x00, 0x00, 0x0C, 0x00, 0xFC, 0x03, 0xB8, 0x0F, 0x78, 0x1C, 0x70, 0x38, + 0xF0, 0xE0, 0xE1, 0xC1, 0xC7, 0x81, 0xCE, 0x03, 0x9C, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x0F, + 0x00, 0x1E, 0x00, 0x38, 0x00, 0xF0, 0x07, 0xC0, 0x0F, 0x00, 0xC0, 0x0F, 0x80, 0x7E, 0x01, 0xF8, + 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, + 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0xC1, 0xC0, 0x07, 0x00, 0x1C, + 0x00, 0x70, 0x00, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x00, 0x7C, 0x00, 0x77, 0x00, 0x3B, 0x80, + 0x1D, 0xC0, 0x1C, 0x70, 0x0E, 0x38, 0x07, 0x1C, 0x07, 0x07, 0x03, 0x83, 0x81, 0xFF, 0xC1, 0xFF, + 0xF0, 0xE0, 0x38, 0xF0, 0x1E, 0x70, 0x07, 0x38, 0x03, 0xBC, 0x00, 0xE0, 0xFF, 0xE7, 0xFF, 0x3F, + 0xF9, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xFF, 0x87, 0xFF, 0x3F, 0xFD, 0xC0, 0xFE, + 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x0F, 0xE0, 0xF7, 0xFF, 0xBF, 0xF0, 0xFF, 0x07, 0xFE, 0x3F, 0xFD, + 0xC1, 0xEE, 0x07, 0x70, 0x1F, 0x80, 0xFC, 0x0E, 0xFF, 0xF7, 0xFE, 0x3F, 0xFD, 0xC0, 0xFE, 0x03, + 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0xFF, 0xFF, 0xBF, 0xF8, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0x00, + 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, + 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x00, 0x07, 0xFF, 0x01, 0xFF, 0xE0, 0x3F, 0xFC, 0x07, 0x03, + 0x80, 0xE0, 0x70, 0x1C, 0x0E, 0x03, 0x81, 0xC0, 0x70, 0x38, 0x0E, 0x07, 0x01, 0xC0, 0xE0, 0x38, + 0x1C, 0x0E, 0x03, 0x81, 0xC0, 0x70, 0x38, 0x0E, 0x07, 0x01, 0xC1, 0xC0, 0x38, 0x78, 0x07, 0x9F, + 0xFF, 0xFB, 0xFF, 0xFF, 0x70, 0x00, 0x6E, 0x00, 0x0D, 0xC0, 0x01, 0xB8, 0x00, 0x30, 0xFF, 0xEF, + 0xFF, 0xFF, 0xFE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xEF, 0xFE, 0xFF, 0xEE, 0x00, + 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFF, 0xFF, 0xF0, 0x70, 0x18, 0x0C, 0x78, 0x38, + 0x1E, 0x38, 0x38, 0x1C, 0x3C, 0x38, 0x38, 0x1C, 0x38, 0x78, 0x0E, 0x38, 0x70, 0x0F, 0x38, 0xF0, + 0x07, 0x38, 0xE0, 0x07, 0xBD, 0xC0, 0x03, 0xFF, 0xC0, 0x07, 0xFF, 0xC0, 0x07, 0x38, 0xE0, 0x0F, + 0x38, 0xF0, 0x0E, 0x38, 0x70, 0x1C, 0x38, 0x78, 0x3C, 0x38, 0x38, 0x38, 0x38, 0x1C, 0x78, 0x38, + 0x1E, 0x70, 0x38, 0x0E, 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0xF0, 0xE7, 0x07, 0xB0, 0x1C, 0x00, 0xE0, + 0x0E, 0x07, 0xF0, 0x3E, 0x01, 0xFC, 0x00, 0xF0, 0x03, 0x80, 0x1F, 0x80, 0xFE, 0x07, 0x78, 0x7B, + 0xFF, 0x87, 0xF8, 0x0E, 0x00, 0xC0, 0x0D, 0xC0, 0x3F, 0x80, 0xFF, 0x01, 0xFE, 0x07, 0xFC, 0x1F, + 0xF8, 0x3B, 0xF0, 0xF7, 0xE3, 0xCF, 0xC7, 0x1F, 0x9E, 0x3F, 0x38, 0x7E, 0xE0, 0xFF, 0xC1, 0xFF, + 0x03, 0xFC, 0x07, 0xF8, 0x0F, 0xE0, 0x1F, 0xC0, 0x38, 0x1C, 0x60, 0x1D, 0xC0, 0x3F, 0x00, 0x3C, + 0x00, 0x00, 0x18, 0x01, 0xB8, 0x07, 0xF0, 0x1F, 0xE0, 0x3F, 0xC0, 0xFF, 0x83, 0xFF, 0x07, 0x7E, + 0x1E, 0xFC, 0x79, 0xF8, 0xE3, 0xF3, 0xC7, 0xE7, 0x0F, 0xDC, 0x1F, 0xF8, 0x3F, 0xE0, 0x7F, 0x80, + 0xFF, 0x01, 0xFC, 0x03, 0xF8, 0x07, 0xC0, 0x1B, 0x80, 0xFE, 0x07, 0xB8, 0x3C, 0xE0, 0xE3, 0x87, + 0x8E, 0x3C, 0x39, 0xE0, 0xFF, 0x03, 0xF8, 0x0F, 0xF0, 0x39, 0xE0, 0xE3, 0xC3, 0x87, 0x0E, 0x1E, + 0x38, 0x3C, 0xE0, 0x7B, 0x80, 0xEE, 0x03, 0xC0, 0x0F, 0xFE, 0x0F, 0xFF, 0x0F, 0xFF, 0x0E, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, - 0x1C, 0x07, 0x38, 0x07, 0x78, 0x07, 0xF8, 0x07, 0xF0, 0x07, 0xE0, 0x01, 0xDE, 0x00, 0x7F, 0xC0, - 0x0F, 0xFC, 0x01, 0xFF, 0x80, 0x7F, 0xF8, 0x0F, 0xFF, 0x03, 0xBF, 0xE0, 0x77, 0xEE, 0x0E, 0xFD, - 0xC3, 0x9F, 0xBC, 0x73, 0xF3, 0x9C, 0x7E, 0x73, 0x8F, 0xC7, 0x71, 0xF8, 0xFC, 0x3F, 0x1F, 0x87, - 0xE1, 0xF0, 0xFC, 0x3C, 0x1F, 0x83, 0x83, 0x80, 0xC0, 0x0D, 0xC0, 0x1F, 0x80, 0x3F, 0x00, 0x7E, - 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x7E, 0x00, - 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0x80, 0x38, 0x03, 0xE0, 0x07, 0xFC, - 0x07, 0xFF, 0x07, 0x83, 0xC7, 0x80, 0xF3, 0x80, 0x3B, 0xC0, 0x1F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, - 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3B, 0x80, 0x39, 0xC0, 0x1C, 0xF0, 0x1E, 0x3E, 0x3E, - 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, 0x00, 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x7E, 0x00, - 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0x80, 0x3F, 0x00, 0x7E, 0x00, 0xFC, - 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0x80, 0x38, 0xFE, 0x07, 0xFE, 0x3F, 0xFD, - 0xC1, 0xEE, 0x07, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x7F, 0x07, 0xBF, 0xFD, 0xFF, 0xCF, 0xF0, - 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x00, 0x03, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, - 0x3C, 0x1E, 0x78, 0x0F, 0x70, 0x07, 0xF0, 0x07, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, - 0xE0, 0x00, 0xE0, 0x00, 0x70, 0x07, 0x70, 0x07, 0x78, 0x0F, 0x3E, 0x3E, 0x1F, 0xFC, 0x0F, 0xF8, - 0x01, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, - 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, - 0x1E, 0x00, 0x78, 0x00, 0x60, 0x07, 0xE0, 0x1D, 0xC0, 0x7B, 0xC0, 0xE3, 0x81, 0xC7, 0x87, 0x07, - 0x0E, 0x0E, 0x3C, 0x0E, 0x70, 0x1C, 0xE0, 0x1F, 0x80, 0x3F, 0x00, 0x7E, 0x00, 0x78, 0x00, 0xF0, - 0x01, 0xC0, 0x07, 0x80, 0x3E, 0x00, 0x78, 0x00, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x03, 0x80, 0x03, - 0xFE, 0x01, 0xFF, 0xF0, 0x7F, 0xFF, 0x1E, 0x38, 0xE3, 0x87, 0x0E, 0xE0, 0xE1, 0xDC, 0x1C, 0x3B, - 0x83, 0x83, 0xF0, 0x70, 0x7E, 0x0E, 0x1D, 0xC1, 0xC3, 0x9C, 0x38, 0x73, 0xC7, 0x3C, 0x3F, 0xFF, - 0x83, 0xFF, 0xC0, 0x1F, 0xF0, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x70, 0x07, 0x3C, - 0x07, 0x8E, 0x03, 0x87, 0x83, 0x81, 0xE3, 0xC0, 0x71, 0xC0, 0x3D, 0xC0, 0x0F, 0xE0, 0x03, 0xE0, - 0x00, 0xE0, 0x00, 0xF8, 0x00, 0xFE, 0x00, 0x77, 0x00, 0x73, 0xC0, 0x78, 0xF0, 0x38, 0x38, 0x38, - 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xC0, 0xC0, 0x0C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, + 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x38, 0x07, 0x78, 0x07, 0xF8, 0x07, 0xF0, 0x07, 0xE0, 0x01, + 0xDE, 0x00, 0x7F, 0xC0, 0x0F, 0xFC, 0x01, 0xFF, 0x80, 0x7F, 0xF8, 0x0F, 0xFF, 0x03, 0xBF, 0xE0, + 0x77, 0xEE, 0x0E, 0xFD, 0xC3, 0x9F, 0xBC, 0x73, 0xF3, 0x9C, 0x7E, 0x73, 0x8F, 0xC7, 0x71, 0xF8, + 0xFC, 0x3F, 0x1F, 0x87, 0xE1, 0xF0, 0xFC, 0x3C, 0x1F, 0x83, 0x83, 0x80, 0xC0, 0x0D, 0xC0, 0x1F, + 0x80, 0x3F, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0x80, 0x38, + 0x03, 0xE0, 0x07, 0xFC, 0x07, 0xFF, 0x07, 0x83, 0xC7, 0x80, 0xF3, 0x80, 0x3B, 0xC0, 0x1F, 0xC0, + 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3B, 0x80, 0x39, 0xC0, 0x1C, + 0xF0, 0x1E, 0x3E, 0x3E, 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, 0x00, 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0x80, 0x3F, + 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0x80, 0x38, 0xFE, + 0x07, 0xFE, 0x3F, 0xFD, 0xC1, 0xEE, 0x07, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x7F, 0x07, 0xBF, + 0xFD, 0xFF, 0xCF, 0xF0, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x00, 0x03, 0xE0, + 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x1E, 0x78, 0x0F, 0x70, 0x07, 0xF0, 0x07, 0xE0, 0x00, 0xE0, 0x00, + 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x70, 0x07, 0x70, 0x07, 0x78, 0x0F, 0x3E, 0x3E, + 0x1F, 0xFC, 0x0F, 0xF8, 0x01, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x07, 0x80, 0x1E, + 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, + 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x00, 0x60, 0x07, 0xE0, 0x1D, 0xC0, 0x7B, 0xC0, 0xE3, + 0x81, 0xC7, 0x87, 0x07, 0x0E, 0x0E, 0x3C, 0x0E, 0x70, 0x1C, 0xE0, 0x1F, 0x80, 0x3F, 0x00, 0x7E, + 0x00, 0x78, 0x00, 0xF0, 0x01, 0xC0, 0x07, 0x80, 0x3E, 0x00, 0x78, 0x00, 0x00, 0xC0, 0x00, 0x1C, + 0x00, 0x03, 0x80, 0x03, 0xFE, 0x01, 0xFF, 0xF0, 0x7F, 0xFF, 0x1E, 0x38, 0xE3, 0x87, 0x0E, 0xE0, + 0xE1, 0xDC, 0x1C, 0x3B, 0x83, 0x83, 0xF0, 0x70, 0x7E, 0x0E, 0x1D, 0xC1, 0xC3, 0x9C, 0x38, 0x73, + 0xC7, 0x3C, 0x3F, 0xFF, 0x83, 0xFF, 0xC0, 0x1F, 0xF0, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x70, 0x07, 0x3C, 0x07, 0x8E, 0x03, 0x87, 0x83, 0x81, 0xE3, 0xC0, 0x71, 0xC0, 0x3D, 0xC0, + 0x0F, 0xE0, 0x03, 0xE0, 0x00, 0xE0, 0x00, 0xF8, 0x00, 0xFE, 0x00, 0x77, 0x00, 0x73, 0xC0, 0x78, + 0xF0, 0x38, 0x38, 0x38, 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xC0, 0xC0, 0x0C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, - 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x07, 0x00, 0x07, - 0x00, 0x07, 0x00, 0x07, 0x60, 0x0D, 0xE0, 0x1F, 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xFC, - 0x03, 0xB8, 0x07, 0x70, 0x1E, 0xF0, 0xFC, 0xFF, 0xF8, 0xFF, 0xF0, 0x30, 0xE0, 0x01, 0xC0, 0x03, - 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0xC0, 0x60, 0x6E, 0x0E, 0x07, 0xE0, 0xE0, 0x7E, - 0x0E, 0x07, 0xE0, 0xE0, 0x7E, 0x0E, 0x07, 0xE0, 0xE0, 0x7E, 0x0E, 0x07, 0xE0, 0xE0, 0x7E, 0x0E, + 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x60, 0x0D, 0xE0, 0x1F, 0xC0, 0x3F, 0x80, 0x7F, + 0x00, 0xFE, 0x01, 0xFC, 0x03, 0xB8, 0x07, 0x70, 0x1E, 0xF0, 0xFC, 0xFF, 0xF8, 0xFF, 0xF0, 0x30, + 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0xC0, 0x60, 0x6E, 0x0E, 0x07, 0xE0, 0xE0, 0x7E, 0x0E, 0x07, 0xE0, 0xE0, 0x7E, 0x0E, 0x07, 0xE0, 0xE0, 0x7E, 0x0E, 0x07, - 0xE0, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xC0, 0x60, 0x63, 0x83, 0x81, 0xCE, 0x0E, 0x07, - 0x38, 0x38, 0x1C, 0xE0, 0xE0, 0x73, 0x83, 0x81, 0xCE, 0x0E, 0x07, 0x38, 0x38, 0x1C, 0xE0, 0xE0, - 0x73, 0x83, 0x81, 0xCE, 0x0E, 0x07, 0x38, 0x38, 0x1C, 0xE0, 0xE0, 0x73, 0x83, 0x81, 0xCE, 0x0E, - 0x07, 0x38, 0x38, 0x1C, 0xE0, 0xE0, 0x73, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x70, 0x00, 0x01, 0xC0, 0xFE, 0x00, 0x1F, 0xE0, 0x03, 0xFC, 0x00, 0x03, - 0x80, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x00, 0x07, 0xFF, 0x00, 0xFF, 0xF0, - 0x1C, 0x1F, 0x03, 0x80, 0xE0, 0x70, 0x1C, 0x0E, 0x03, 0x81, 0xC0, 0x70, 0x38, 0x0E, 0x07, 0x07, - 0xC0, 0xFF, 0xF0, 0x1F, 0xFC, 0x00, 0xC0, 0x01, 0xB8, 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xE0, - 0x01, 0xF8, 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xFF, 0xC1, 0xFF, 0xFC, 0x7E, 0x0F, 0x9F, 0x80, - 0xE7, 0xE0, 0x39, 0xF8, 0x0E, 0x7E, 0x03, 0x9F, 0x80, 0xE7, 0xE0, 0xF1, 0xFF, 0xFC, 0x7F, 0xFC, - 0x1C, 0xC0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xFF, 0xC7, - 0xFF, 0xB8, 0x3F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0xF7, 0xFF, 0xBF, 0xF0, - 0x07, 0xC0, 0x1F, 0xF0, 0x3F, 0xF8, 0x78, 0x3C, 0x70, 0x1E, 0xE0, 0x0E, 0xE0, 0x07, 0x00, 0x07, - 0x03, 0xFF, 0x03, 0xFF, 0x03, 0xFF, 0x00, 0x07, 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x0E, 0x70, 0x1E, - 0x7C, 0x7C, 0x3F, 0xF8, 0x1F, 0xF0, 0x03, 0x80, 0xC0, 0x1F, 0x03, 0x81, 0xFF, 0x0E, 0x0F, 0xFE, - 0x38, 0x7C, 0x3C, 0xE1, 0xC0, 0x7B, 0x8E, 0x00, 0xEE, 0x38, 0x01, 0xF9, 0xE0, 0x07, 0xFF, 0x00, - 0x1F, 0xFC, 0x00, 0x7F, 0xF0, 0x01, 0xF9, 0xC0, 0x07, 0xE3, 0x80, 0x1F, 0x8E, 0x00, 0x7E, 0x38, - 0x03, 0xB8, 0x70, 0x1E, 0xE1, 0xF1, 0xF3, 0x83, 0xFF, 0x8E, 0x03, 0xFC, 0x00, 0x03, 0x80, 0x01, - 0xFC, 0x7F, 0xF3, 0xFF, 0xCF, 0x07, 0x78, 0x1D, 0xC0, 0x77, 0x01, 0xDC, 0x07, 0x78, 0x1C, 0xFF, - 0xF1, 0xFF, 0xC3, 0xFF, 0x0E, 0x1C, 0x78, 0x71, 0xC1, 0xCF, 0x07, 0x38, 0x1D, 0xE0, 0x77, 0x01, - 0xC0, 0x1F, 0x8F, 0xF9, 0xFF, 0xF0, 0x72, 0x0E, 0x01, 0xCF, 0xFB, 0xFF, 0xF8, 0xFC, 0x1F, 0x83, - 0xF0, 0xFF, 0xFE, 0xFF, 0xC7, 0x00, 0x00, 0x20, 0x03, 0x83, 0xF8, 0x7F, 0xC7, 0xE0, 0x38, 0x03, - 0xBF, 0x1F, 0xFC, 0xF8, 0xF7, 0x83, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x0E, - 0x70, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x7F, 0x8F, 0xFC, 0xFF, 0xEE, 0x0F, 0xE0, 0x7E, 0x0E, - 0xFF, 0xCF, 0xFE, 0xE0, 0xFE, 0x07, 0xE0, 0x7E, 0x0F, 0xFF, 0xEF, 0xFC, 0x7F, 0xFF, 0xFF, 0xFF, - 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x00, 0x0F, 0xF8, - 0x3F, 0xF0, 0x7F, 0xE0, 0xE1, 0xC1, 0xC3, 0x83, 0x87, 0x07, 0x0E, 0x0E, 0x1C, 0x1C, 0x38, 0x30, - 0x70, 0xE0, 0xE3, 0xC1, 0xCF, 0xFF, 0xFF, 0xFF, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1C, - 0x0F, 0x81, 0xFE, 0x1F, 0xF8, 0xE0, 0xEE, 0x07, 0x70, 0x1B, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0x00, - 0x38, 0x1C, 0xF1, 0xE3, 0xFE, 0x0F, 0xE0, 0x1C, 0x00, 0x70, 0x60, 0xE7, 0x87, 0x1E, 0x38, 0x71, - 0xC1, 0xC7, 0x38, 0x0E, 0x77, 0x00, 0xF7, 0xF0, 0x07, 0xFE, 0x00, 0x7F, 0xE0, 0x07, 0xFE, 0x00, - 0xE7, 0x70, 0x1E, 0x77, 0x83, 0xC7, 0x3C, 0x78, 0x71, 0xE7, 0x07, 0x0E, 0x1F, 0x0F, 0xF3, 0xFF, - 0x70, 0xE0, 0x1C, 0x07, 0x83, 0xE0, 0x7C, 0x03, 0xC0, 0x3B, 0x87, 0x70, 0xEF, 0xFC, 0xFF, 0x03, - 0x00, 0x60, 0x7E, 0x07, 0xE0, 0xFE, 0x1F, 0xE1, 0xFE, 0x3F, 0xE7, 0x7E, 0x77, 0xEE, 0x7F, 0xC7, - 0xFC, 0x7F, 0x87, 0xF0, 0x7F, 0x07, 0x38, 0xC1, 0x9C, 0x1F, 0x80, 0xF0, 0x00, 0x06, 0x07, 0xE0, - 0x7E, 0x0F, 0xE1, 0xFE, 0x1F, 0xE3, 0xFE, 0x77, 0xE7, 0x7E, 0xE7, 0xFC, 0x7F, 0xC7, 0xF8, 0x7F, - 0x07, 0xF0, 0x70, 0x60, 0x77, 0x07, 0xB8, 0x79, 0xC7, 0x8E, 0x78, 0x73, 0x83, 0xF8, 0x1F, 0xC0, - 0xFF, 0x07, 0x3C, 0x38, 0xF1, 0xC3, 0xCE, 0x0E, 0x70, 0x78, 0x1F, 0xF8, 0xFF, 0xC7, 0xFE, 0x38, - 0x71, 0xC3, 0x8E, 0x1C, 0x70, 0xE3, 0x07, 0x38, 0x39, 0xC1, 0xCE, 0x0E, 0xF0, 0x7F, 0x03, 0xF8, - 0x1C, 0x70, 0x07, 0x7C, 0x07, 0xFE, 0x03, 0xFF, 0x83, 0xFF, 0xC1, 0xFF, 0x71, 0xDF, 0xB8, 0xEF, - 0xDC, 0x77, 0xE7, 0x73, 0xF3, 0xB9, 0xF8, 0xF8, 0xFC, 0x7C, 0x7E, 0x1C, 0x3F, 0x0E, 0x1C, 0x60, - 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, - 0x07, 0xE0, 0x7E, 0x07, 0x0F, 0x81, 0xFF, 0x1F, 0xF8, 0xE0, 0xEE, 0x07, 0x70, 0x1F, 0x80, 0xFC, - 0x07, 0xE0, 0x3F, 0x01, 0xFC, 0x1C, 0xF1, 0xE3, 0xFE, 0x0F, 0xE0, 0x1C, 0x00, 0x7F, 0xFF, 0xFF, - 0xFF, 0xFE, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, - 0x7E, 0x07, 0x67, 0xC7, 0x7F, 0x3F, 0xFD, 0xF0, 0xEF, 0x03, 0xF8, 0x1F, 0x80, 0xFC, 0x07, 0xE0, - 0x3F, 0x81, 0xFC, 0x1F, 0xF0, 0xEF, 0xFF, 0x77, 0xF3, 0x8C, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, - 0x00, 0x00, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xE7, 0x07, 0xE0, 0x7E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, - 0x0E, 0x00, 0xF0, 0x77, 0x8F, 0x3F, 0xE1, 0xFC, 0x07, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xE0, - 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0xE0, - 0x39, 0xC0, 0xE7, 0x03, 0x9C, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0xCE, 0x07, 0x38, 0x1C, 0xC0, 0x3F, - 0x00, 0xFC, 0x03, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x01, 0xC0, 0x0E, 0x00, 0xF8, 0x07, 0xC0, - 0x0C, 0x00, 0x01, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x01, 0xFC, 0x0F, 0xFE, 0x3F, - 0xFE, 0x73, 0x9D, 0xC7, 0x1F, 0x8E, 0x3F, 0x1C, 0x7E, 0x38, 0xFC, 0x71, 0xF8, 0xE3, 0xF9, 0xCF, - 0x73, 0x9C, 0x7F, 0xF0, 0x7F, 0xC0, 0x3E, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x00, - 0x00, 0x70, 0x3B, 0xC3, 0x8E, 0x3C, 0x39, 0xC1, 0xDC, 0x07, 0xE0, 0x1E, 0x00, 0xF0, 0x0F, 0x80, - 0xFE, 0x07, 0x78, 0x71, 0xC7, 0x87, 0x38, 0x3C, 0x60, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, - 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xFF, 0xFF, 0xFF, - 0xF0, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x70, 0x60, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x76, - 0x07, 0x70, 0x77, 0xFF, 0x3F, 0xF1, 0xFF, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x60, 0xC1, 0xF8, - 0x38, 0x7E, 0x0E, 0x1F, 0x83, 0x87, 0xE0, 0xE1, 0xF8, 0x38, 0x7E, 0x0E, 0x1F, 0x83, 0x87, 0xE0, - 0xE1, 0xF8, 0x38, 0x7E, 0x0E, 0x1F, 0x83, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x60, 0xC1, 0xCE, - 0x0E, 0x1C, 0xE0, 0xE1, 0xCE, 0x0E, 0x1C, 0xE0, 0xE1, 0xCE, 0x0E, 0x1C, 0xE0, 0xE1, 0xCE, 0x0E, - 0x1C, 0xE0, 0xE1, 0xCE, 0x0E, 0x1C, 0xE0, 0xE1, 0xCE, 0x0E, 0x1C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0x00, 0x00, 0x70, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x07, 0xFC, 0x01, 0xFC, 0x03, 0xF8, 0x00, - 0x70, 0x00, 0xE0, 0x01, 0xFF, 0x03, 0xFF, 0x07, 0xFF, 0x0E, 0x0E, 0x1C, 0x1C, 0x38, 0x38, 0x70, - 0xF0, 0xFF, 0xC1, 0xFF, 0x00, 0x60, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xFF, - 0x87, 0xFF, 0xE7, 0xFF, 0xE7, 0xE0, 0x77, 0xE0, 0x77, 0xE0, 0x77, 0xE1, 0xF7, 0xFF, 0xE7, 0xFF, - 0xC7, 0x60, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xF8, 0xFF, 0xEF, 0xFE, 0xE0, 0x7E, 0x07, - 0xE0, 0x7E, 0x1F, 0xFF, 0xEF, 0xFC, 0x1F, 0x07, 0xFC, 0x7F, 0xEE, 0x0E, 0xE0, 0x70, 0x07, 0x1F, - 0xF1, 0xFF, 0x0F, 0xF0, 0x07, 0xE0, 0xFF, 0x1E, 0x7F, 0xC3, 0xF8, 0x0E, 0x00, 0x60, 0x3E, 0x1C, - 0x1F, 0xF3, 0x87, 0xFE, 0x70, 0xE0, 0xEE, 0x38, 0x1D, 0xC7, 0x01, 0xFF, 0xE0, 0x3F, 0xFC, 0x07, - 0xFF, 0x80, 0xFC, 0x70, 0x1F, 0x8F, 0x07, 0x70, 0xF1, 0xEE, 0x0F, 0xF9, 0xC0, 0xFE, 0x00, 0x07, - 0x00, 0x0F, 0xF3, 0xFF, 0x7F, 0xF7, 0x07, 0x70, 0x77, 0x07, 0x78, 0x73, 0xFF, 0x1F, 0xF1, 0xC7, - 0x3C, 0x73, 0x87, 0x70, 0x7F, 0x07, 0x1C, 0x00, 0xE0, 0x03, 0x80, 0x0C, 0x00, 0x00, 0x07, 0xC0, - 0xFF, 0x0F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0D, 0xFF, 0xFF, 0xFF, 0xF0, 0x03, 0x80, 0x1C, 0x0E, - 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x38, 0xC1, 0xCF, 0x0E, 0x70, 0x21, 0x00, 0x00, 0x07, - 0xC0, 0xFF, 0x0F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0D, 0xFF, 0xFF, 0xFF, 0xF0, 0x03, 0x80, 0x1C, - 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x30, 0x03, 0x80, 0x3F, 0xC1, 0xFE, 0x07, 0x00, + 0xE0, 0xE0, 0x7E, 0x0E, 0x07, 0xE0, 0xE0, 0x7E, 0x0E, 0x07, 0xE0, 0xE0, 0x7E, 0x0E, 0x07, 0xE0, + 0xE0, 0x7E, 0x0E, 0x07, 0xE0, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xC0, 0x60, 0x63, 0x83, + 0x81, 0xCE, 0x0E, 0x07, 0x38, 0x38, 0x1C, 0xE0, 0xE0, 0x73, 0x83, 0x81, 0xCE, 0x0E, 0x07, 0x38, + 0x38, 0x1C, 0xE0, 0xE0, 0x73, 0x83, 0x81, 0xCE, 0x0E, 0x07, 0x38, 0x38, 0x1C, 0xE0, 0xE0, 0x73, + 0x83, 0x81, 0xCE, 0x0E, 0x07, 0x38, 0x38, 0x1C, 0xE0, 0xE0, 0x73, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC0, 0x00, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x70, 0x00, 0x01, 0xC0, 0xFE, 0x00, 0x1F, 0xE0, + 0x03, 0xFC, 0x00, 0x03, 0x80, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x00, 0x07, + 0xFF, 0x00, 0xFF, 0xF0, 0x1C, 0x1F, 0x03, 0x80, 0xE0, 0x70, 0x1C, 0x0E, 0x03, 0x81, 0xC0, 0x70, + 0x38, 0x0E, 0x07, 0x07, 0xC0, 0xFF, 0xF0, 0x1F, 0xFC, 0x00, 0xC0, 0x01, 0xB8, 0x00, 0x7E, 0x00, + 0x1F, 0x80, 0x07, 0xE0, 0x01, 0xF8, 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xFF, 0xC1, 0xFF, 0xFC, + 0x7E, 0x0F, 0x9F, 0x80, 0xE7, 0xE0, 0x39, 0xF8, 0x0E, 0x7E, 0x03, 0x9F, 0x80, 0xE7, 0xE0, 0xF1, + 0xFF, 0xFC, 0x7F, 0xFC, 0x1C, 0xC0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, + 0x1C, 0x00, 0xFF, 0xC7, 0xFF, 0xB8, 0x3F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, + 0xF7, 0xFF, 0xBF, 0xF0, 0x07, 0xC0, 0x1F, 0xF0, 0x3F, 0xF8, 0x78, 0x3C, 0x70, 0x1E, 0xE0, 0x0E, + 0xE0, 0x07, 0x00, 0x07, 0x03, 0xFF, 0x03, 0xFF, 0x03, 0xFF, 0x00, 0x07, 0x00, 0x07, 0xE0, 0x07, + 0xE0, 0x0E, 0x70, 0x1E, 0x7C, 0x7C, 0x3F, 0xF8, 0x1F, 0xF0, 0x03, 0x80, 0xC0, 0x1F, 0x03, 0x81, + 0xFF, 0x0E, 0x0F, 0xFE, 0x38, 0x7C, 0x3C, 0xE1, 0xC0, 0x7B, 0x8E, 0x00, 0xEE, 0x38, 0x01, 0xF9, + 0xE0, 0x07, 0xFF, 0x00, 0x1F, 0xFC, 0x00, 0x7F, 0xF0, 0x01, 0xF9, 0xC0, 0x07, 0xE3, 0x80, 0x1F, + 0x8E, 0x00, 0x7E, 0x38, 0x03, 0xB8, 0x70, 0x1E, 0xE1, 0xF1, 0xF3, 0x83, 0xFF, 0x8E, 0x03, 0xFC, + 0x00, 0x03, 0x80, 0x01, 0xFC, 0x7F, 0xF3, 0xFF, 0xCF, 0x07, 0x78, 0x1D, 0xC0, 0x77, 0x01, 0xDC, + 0x07, 0x78, 0x1C, 0xFF, 0xF1, 0xFF, 0xC3, 0xFF, 0x0E, 0x1C, 0x78, 0x71, 0xC1, 0xCF, 0x07, 0x38, + 0x1D, 0xE0, 0x77, 0x01, 0xC0, 0x1F, 0x8F, 0xF9, 0xFF, 0xF0, 0x72, 0x0E, 0x01, 0xCF, 0xFB, 0xFF, + 0xF8, 0xFC, 0x1F, 0x83, 0xF0, 0xFF, 0xFE, 0xFF, 0xC7, 0x00, 0x00, 0x20, 0x03, 0x83, 0xF8, 0x7F, + 0xC7, 0xE0, 0x38, 0x03, 0xBF, 0x1F, 0xFC, 0xF8, 0xF7, 0x83, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, + 0x1F, 0x80, 0xFC, 0x0E, 0x70, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x7F, 0x8F, 0xFC, 0xFF, 0xEE, + 0x0F, 0xE0, 0x7E, 0x0E, 0xFF, 0xCF, 0xFE, 0xE0, 0xFE, 0x07, 0xE0, 0x7E, 0x0F, 0xFF, 0xEF, 0xFC, + 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, + 0x38, 0x00, 0x0F, 0xF8, 0x3F, 0xF0, 0x7F, 0xE0, 0xE1, 0xC1, 0xC3, 0x83, 0x87, 0x07, 0x0E, 0x0E, + 0x1C, 0x1C, 0x38, 0x30, 0x70, 0xE0, 0xE3, 0xC1, 0xCF, 0xFF, 0xFF, 0xFF, 0xF8, 0x03, 0xF0, 0x07, + 0xE0, 0x0F, 0xC0, 0x1C, 0x0F, 0x81, 0xFE, 0x1F, 0xF8, 0xE0, 0xEE, 0x07, 0x70, 0x1B, 0xFF, 0xFF, + 0xFF, 0xE0, 0x07, 0x00, 0x38, 0x1C, 0xF1, 0xE3, 0xFE, 0x0F, 0xE0, 0x1C, 0x00, 0x70, 0x60, 0xE7, + 0x87, 0x1E, 0x38, 0x71, 0xC1, 0xC7, 0x38, 0x0E, 0x77, 0x00, 0xF7, 0xF0, 0x07, 0xFE, 0x00, 0x7F, + 0xE0, 0x07, 0xFE, 0x00, 0xE7, 0x70, 0x1E, 0x77, 0x83, 0xC7, 0x3C, 0x78, 0x71, 0xE7, 0x07, 0x0E, + 0x1F, 0x0F, 0xF3, 0xFF, 0x70, 0xE0, 0x1C, 0x07, 0x83, 0xE0, 0x7C, 0x03, 0xC0, 0x3B, 0x87, 0x70, + 0xEF, 0xFC, 0xFF, 0x03, 0x00, 0x60, 0x7E, 0x07, 0xE0, 0xFE, 0x1F, 0xE1, 0xFE, 0x3F, 0xE7, 0x7E, + 0x77, 0xEE, 0x7F, 0xC7, 0xFC, 0x7F, 0x87, 0xF0, 0x7F, 0x07, 0x38, 0xC1, 0x9C, 0x1F, 0x80, 0xF0, + 0x00, 0x06, 0x07, 0xE0, 0x7E, 0x0F, 0xE1, 0xFE, 0x1F, 0xE3, 0xFE, 0x77, 0xE7, 0x7E, 0xE7, 0xFC, + 0x7F, 0xC7, 0xF8, 0x7F, 0x07, 0xF0, 0x70, 0x60, 0x77, 0x07, 0xB8, 0x79, 0xC7, 0x8E, 0x78, 0x73, + 0x83, 0xF8, 0x1F, 0xC0, 0xFF, 0x07, 0x3C, 0x38, 0xF1, 0xC3, 0xCE, 0x0E, 0x70, 0x78, 0x1F, 0xF8, + 0xFF, 0xC7, 0xFE, 0x38, 0x71, 0xC3, 0x8E, 0x1C, 0x70, 0xE3, 0x07, 0x38, 0x39, 0xC1, 0xCE, 0x0E, + 0xF0, 0x7F, 0x03, 0xF8, 0x1C, 0x70, 0x07, 0x7C, 0x07, 0xFE, 0x03, 0xFF, 0x83, 0xFF, 0xC1, 0xFF, + 0x71, 0xDF, 0xB8, 0xEF, 0xDC, 0x77, 0xE7, 0x73, 0xF3, 0xB9, 0xF8, 0xF8, 0xFC, 0x7C, 0x7E, 0x1C, + 0x3F, 0x0E, 0x1C, 0x60, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, + 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x0F, 0x81, 0xFF, 0x1F, 0xF8, 0xE0, 0xEE, 0x07, + 0x70, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xFC, 0x1C, 0xF1, 0xE3, 0xFE, 0x0F, 0xE0, 0x1C, + 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFE, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, + 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x67, 0xC7, 0x7F, 0x3F, 0xFD, 0xF0, 0xEF, 0x03, 0xF8, 0x1F, + 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x81, 0xFC, 0x1F, 0xF0, 0xEF, 0xFF, 0x77, 0xF3, 0x8C, 0x1C, 0x00, + 0xE0, 0x07, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xE7, 0x07, 0xE0, 0x7E, 0x00, + 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xF0, 0x77, 0x8F, 0x3F, 0xE1, 0xFC, 0x07, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF0, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, + 0x0E, 0x00, 0xE0, 0xE0, 0x39, 0xC0, 0xE7, 0x03, 0x9C, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0xCE, 0x07, + 0x38, 0x1C, 0xC0, 0x3F, 0x00, 0xFC, 0x03, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x01, 0xC0, 0x0E, + 0x00, 0xF8, 0x07, 0xC0, 0x0C, 0x00, 0x01, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x01, + 0xFC, 0x0F, 0xFE, 0x3F, 0xFE, 0x73, 0x9D, 0xC7, 0x1F, 0x8E, 0x3F, 0x1C, 0x7E, 0x38, 0xFC, 0x71, + 0xF8, 0xE3, 0xF9, 0xCF, 0x73, 0x9C, 0x7F, 0xF0, 0x7F, 0xC0, 0x3E, 0x00, 0x38, 0x00, 0x70, 0x00, + 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x70, 0x3B, 0xC3, 0x8E, 0x3C, 0x39, 0xC1, 0xDC, 0x07, 0xE0, 0x1E, + 0x00, 0xF0, 0x0F, 0x80, 0xFE, 0x07, 0x78, 0x71, 0xC7, 0x87, 0x38, 0x3C, 0x60, 0x73, 0x81, 0xCE, + 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, + 0x1C, 0xFF, 0xFF, 0xFF, 0xF0, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x70, 0x60, 0x7E, 0x07, 0xE0, + 0x7E, 0x07, 0xE0, 0x76, 0x07, 0x70, 0x77, 0xFF, 0x3F, 0xF1, 0xFF, 0x00, 0x70, 0x07, 0x00, 0x70, + 0x07, 0x60, 0xC1, 0xF8, 0x38, 0x7E, 0x0E, 0x1F, 0x83, 0x87, 0xE0, 0xE1, 0xF8, 0x38, 0x7E, 0x0E, + 0x1F, 0x83, 0x87, 0xE0, 0xE1, 0xF8, 0x38, 0x7E, 0x0E, 0x1F, 0x83, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF0, 0x60, 0xC1, 0xCE, 0x0E, 0x1C, 0xE0, 0xE1, 0xCE, 0x0E, 0x1C, 0xE0, 0xE1, 0xCE, 0x0E, 0x1C, + 0xE0, 0xE1, 0xCE, 0x0E, 0x1C, 0xE0, 0xE1, 0xCE, 0x0E, 0x1C, 0xE0, 0xE1, 0xCE, 0x0E, 0x1C, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x70, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x07, 0xFC, 0x01, + 0xFC, 0x03, 0xF8, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xFF, 0x03, 0xFF, 0x07, 0xFF, 0x0E, 0x0E, 0x1C, + 0x1C, 0x38, 0x38, 0x70, 0xF0, 0xFF, 0xC1, 0xFF, 0x00, 0x60, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0xE0, 0x07, 0xFF, 0x87, 0xFF, 0xE7, 0xFF, 0xE7, 0xE0, 0x77, 0xE0, 0x77, 0xE0, 0x77, 0xE1, + 0xF7, 0xFF, 0xE7, 0xFF, 0xC7, 0x60, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xF8, 0xFF, 0xEF, + 0xFE, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x1F, 0xFF, 0xEF, 0xFC, 0x1F, 0x07, 0xFC, 0x7F, 0xEE, 0x0E, + 0xE0, 0x70, 0x07, 0x1F, 0xF1, 0xFF, 0x0F, 0xF0, 0x07, 0xE0, 0xFF, 0x1E, 0x7F, 0xC3, 0xF8, 0x0E, + 0x00, 0x60, 0x3E, 0x1C, 0x1F, 0xF3, 0x87, 0xFE, 0x70, 0xE0, 0xEE, 0x38, 0x1D, 0xC7, 0x01, 0xFF, + 0xE0, 0x3F, 0xFC, 0x07, 0xFF, 0x80, 0xFC, 0x70, 0x1F, 0x8F, 0x07, 0x70, 0xF1, 0xEE, 0x0F, 0xF9, + 0xC0, 0xFE, 0x00, 0x07, 0x00, 0x0F, 0xF3, 0xFF, 0x7F, 0xF7, 0x07, 0x70, 0x77, 0x07, 0x78, 0x73, + 0xFF, 0x1F, 0xF1, 0xC7, 0x3C, 0x73, 0x87, 0x70, 0x7F, 0x07, 0x1C, 0x00, 0xE0, 0x03, 0x80, 0x0C, + 0x00, 0x00, 0x07, 0xC0, 0xFF, 0x0F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0D, 0xFF, 0xFF, 0xFF, 0xF0, + 0x03, 0x80, 0x1C, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x38, 0xC1, 0xCF, 0x0E, 0x70, + 0x21, 0x00, 0x00, 0x07, 0xC0, 0xFF, 0x0F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0D, 0xFF, 0xFF, 0xFF, + 0xF0, 0x03, 0x80, 0x1C, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x30, 0x03, 0x80, 0x3F, + 0xC1, 0xFE, 0x07, 0x00, 0x39, 0xE1, 0xFF, 0xCF, 0xFE, 0x78, 0x7B, 0xC1, 0xDC, 0x0E, 0xE0, 0x77, + 0x03, 0xB8, 0x1D, 0xC0, 0xEE, 0x07, 0x70, 0x3B, 0x81, 0xDC, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, + 0x03, 0xE0, 0x1E, 0x00, 0xC0, 0x07, 0x01, 0xC0, 0xE0, 0x30, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xE0, + 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x00, 0x0F, 0x83, 0xFE, + 0x7F, 0xE7, 0x07, 0xE0, 0x7E, 0x00, 0xFF, 0x8F, 0xF8, 0xFF, 0x0E, 0x00, 0xF0, 0x77, 0x8F, 0x3F, + 0xE1, 0xFC, 0x07, 0x00, 0x1F, 0x0F, 0xF9, 0xFF, 0x70, 0x7E, 0x01, 0xE0, 0x1F, 0xC1, 0xFE, 0x07, + 0xE0, 0x1F, 0x83, 0xF8, 0xF7, 0xFC, 0x7F, 0x03, 0x80, 0x6F, 0xF4, 0x06, 0xEE, 0xEE, 0xEE, 0xEE, + 0xEE, 0xEE, 0xE0, 0x73, 0xB9, 0xDC, 0xE4, 0x20, 0x00, 0x60, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, + 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x00, 0x18, 0xF3, 0xC4, 0x00, 0x63, 0x8E, 0x38, + 0xE3, 0x8E, 0x38, 0xE3, 0x8E, 0x38, 0xE3, 0x8E, 0x38, 0xEF, 0xBC, 0x00, 0x1F, 0xF8, 0x00, 0x7F, + 0xF0, 0x01, 0xFF, 0xC0, 0x07, 0x0F, 0x00, 0x1C, 0x3C, 0x00, 0x70, 0xFF, 0xC1, 0xC3, 0xFF, 0x87, + 0x0F, 0xFF, 0x1C, 0x3C, 0x1C, 0x70, 0xF0, 0x73, 0x83, 0xC1, 0xCE, 0x0F, 0x0F, 0xF8, 0x3F, 0xFB, + 0xC0, 0xFF, 0xC0, 0x60, 0x70, 0x07, 0x03, 0x80, 0x38, 0x1C, 0x01, 0xC0, 0xE0, 0x0E, 0x07, 0x00, + 0x70, 0x38, 0x03, 0xFF, 0xFF, 0x1F, 0xFF, 0xFE, 0xF0, 0x70, 0xF7, 0x03, 0x83, 0xB8, 0x1C, 0x1D, + 0xC0, 0xE1, 0xEE, 0x07, 0xFF, 0x70, 0x3F, 0xF0, 0x30, 0x03, 0x80, 0x3F, 0xC1, 0xFE, 0x07, 0x00, 0x39, 0xE1, 0xFF, 0xCF, 0xFE, 0x78, 0x7B, 0xC1, 0xDC, 0x0E, 0xE0, 0x77, 0x03, 0xB8, 0x1D, 0xC0, - 0xEE, 0x07, 0x70, 0x3B, 0x81, 0xDC, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x03, 0xE0, 0x1E, 0x00, - 0xC0, 0x07, 0x01, 0xC0, 0xE0, 0x30, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xE0, 0x38, 0x0E, 0x03, 0x80, - 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xE7, 0x07, 0xE0, - 0x7E, 0x00, 0xFF, 0x8F, 0xF8, 0xFF, 0x0E, 0x00, 0xF0, 0x77, 0x8F, 0x3F, 0xE1, 0xFC, 0x07, 0x00, - 0x1F, 0x0F, 0xF9, 0xFF, 0x70, 0x7E, 0x01, 0xE0, 0x1F, 0xC1, 0xFE, 0x07, 0xE0, 0x1F, 0x83, 0xF8, - 0xF7, 0xFC, 0x7F, 0x03, 0x80, 0x6F, 0xF4, 0x06, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xE0, 0x73, - 0xB9, 0xDC, 0xE4, 0x20, 0x00, 0x60, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, - 0x38, 0x1C, 0x0E, 0x07, 0x00, 0x18, 0xF3, 0xC4, 0x00, 0x63, 0x8E, 0x38, 0xE3, 0x8E, 0x38, 0xE3, - 0x8E, 0x38, 0xE3, 0x8E, 0x38, 0xEF, 0xBC, 0x00, 0x1F, 0xF8, 0x00, 0x7F, 0xF0, 0x01, 0xFF, 0xC0, - 0x07, 0x0F, 0x00, 0x1C, 0x3C, 0x00, 0x70, 0xFF, 0xC1, 0xC3, 0xFF, 0x87, 0x0F, 0xFF, 0x1C, 0x3C, - 0x1C, 0x70, 0xF0, 0x73, 0x83, 0xC1, 0xCE, 0x0F, 0x0F, 0xF8, 0x3F, 0xFB, 0xC0, 0xFF, 0xC0, 0x60, - 0x70, 0x07, 0x03, 0x80, 0x38, 0x1C, 0x01, 0xC0, 0xE0, 0x0E, 0x07, 0x00, 0x70, 0x38, 0x03, 0xFF, - 0xFF, 0x1F, 0xFF, 0xFE, 0xF0, 0x70, 0xF7, 0x03, 0x83, 0xB8, 0x1C, 0x1D, 0xC0, 0xE1, 0xEE, 0x07, - 0xFF, 0x70, 0x3F, 0xF0, 0x30, 0x03, 0x80, 0x3F, 0xC1, 0xFE, 0x07, 0x00, 0x39, 0xE1, 0xFF, 0xCF, - 0xFE, 0x78, 0x7B, 0xC1, 0xDC, 0x0E, 0xE0, 0x77, 0x03, 0xB8, 0x1D, 0xC0, 0xEE, 0x07, 0x70, 0x3B, - 0x81, 0xDC, 0x0E, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0x3B, 0x83, 0xDC, 0x3C, - 0xE3, 0xC7, 0x3C, 0x39, 0xC1, 0xFC, 0x0F, 0xE0, 0x7F, 0x83, 0x9E, 0x1C, 0x78, 0xE1, 0xE7, 0x07, - 0x38, 0x3C, 0x1C, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x00, 0x06, 0x07, 0xE0, 0x7E, 0x0F, 0xE1, 0xFE, - 0x1F, 0xE3, 0xFE, 0x77, 0xE7, 0x7E, 0xE7, 0xFC, 0x7F, 0xC7, 0xF8, 0x7F, 0x07, 0xF0, 0x70, 0x18, - 0xE0, 0x73, 0x80, 0xFC, 0x01, 0xE0, 0x00, 0x03, 0x80, 0xE7, 0x03, 0x9C, 0x0E, 0x70, 0x70, 0xE1, - 0xC3, 0x87, 0x0F, 0x38, 0x1C, 0xE0, 0x73, 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0x78, - 0x01, 0xC0, 0x07, 0x00, 0x38, 0x03, 0xE0, 0x1F, 0x00, 0x30, 0x00, 0x60, 0x7E, 0x07, 0xE0, 0x7E, - 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xFF, 0xFF, 0xFF, - 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0xC0, 0xC0, 0xDC, 0x1C, 0x3B, 0x83, 0x87, 0x70, 0x70, 0xEE, - 0x0E, 0x1D, 0xC1, 0xC3, 0xB8, 0x38, 0x77, 0x07, 0x0E, 0xE0, 0xE1, 0xDC, 0x1C, 0x3B, 0x83, 0x87, - 0x70, 0x70, 0xEE, 0x0E, 0x1D, 0xC1, 0xC3, 0xB8, 0x38, 0x77, 0x0F, 0x0E, 0xF3, 0xF3, 0xCF, 0xFF, - 0xF0, 0xFE, 0xFC, 0x06, 0x06, 0x00, 0x60, 0xC1, 0xB8, 0x30, 0x6E, 0x0C, 0x1B, 0x83, 0x06, 0xE0, - 0xC1, 0xB8, 0x30, 0x6E, 0x0C, 0x1B, 0x83, 0x06, 0xE0, 0xC1, 0xB8, 0x30, 0x67, 0x1E, 0x39, 0xC7, - 0x8E, 0x7F, 0xFF, 0x8F, 0xCF, 0xC0, 0xC0, 0xC0, 0x18, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, - 0xFF, 0xE0, 0xFF, 0xC0, 0x1C, 0x00, 0x1C, 0x00, 0x1F, 0xFC, 0x1F, 0xFE, 0x1C, 0x1F, 0x1C, 0x07, - 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x1F, 0x1F, 0xFE, 0x1F, 0xF8, 0x18, 0x00, - 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x1F, 0xF8, 0x3F, 0xF0, 0x7F, 0xE0, 0x1C, 0x00, 0x38, - 0x00, 0x7F, 0xC0, 0xFF, 0xC1, 0xFF, 0xC3, 0x83, 0x87, 0x07, 0x0E, 0x0E, 0x1C, 0x3C, 0x3F, 0xF0, - 0x7F, 0xC0, 0xC0, 0x1E, 0x07, 0x03, 0xFE, 0x38, 0x7F, 0xF9, 0xC3, 0xC3, 0xEE, 0x38, 0x07, 0x73, - 0xC0, 0x3F, 0x9C, 0x00, 0xFC, 0xE0, 0x00, 0xFF, 0xFC, 0x07, 0xFF, 0xF0, 0x3F, 0xFF, 0x81, 0xCE, - 0x00, 0x0E, 0x70, 0x00, 0x73, 0x80, 0x1F, 0x8E, 0x01, 0xDC, 0x78, 0x1E, 0xE1, 0xE1, 0xE7, 0x07, - 0xFF, 0x38, 0x1F, 0xE0, 0x00, 0x1C, 0x00, 0x60, 0x7C, 0x38, 0x3F, 0xCE, 0x1F, 0xFB, 0x8F, 0x0E, - 0xE3, 0x81, 0xB9, 0xC0, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xF8, 0x39, 0xE0, 0x0E, 0x38, 0x3B, - 0x8F, 0x0E, 0xE1, 0xFF, 0x38, 0x3F, 0x80, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x00, - 0x7C, 0x00, 0x77, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x1C, 0x70, 0x0E, 0x38, 0x07, 0x1C, 0x07, 0x07, - 0x03, 0xFF, 0x81, 0xFF, 0xC1, 0xFF, 0xF0, 0xE7, 0x38, 0xF3, 0x9E, 0x71, 0xC7, 0x38, 0xE3, 0xBC, - 0x70, 0xE0, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0F, 0xC0, 0x3F, 0x81, 0xCE, 0x07, 0x38, - 0x3F, 0xF0, 0xFF, 0xC3, 0xBF, 0x9C, 0xCE, 0x73, 0x3B, 0x8C, 0x70, 0xC0, 0x0C, 0x07, 0x00, 0xF0, - 0x38, 0x0F, 0x81, 0xC0, 0x7C, 0x0E, 0x03, 0xF0, 0x70, 0x3B, 0x83, 0x81, 0xDC, 0x1C, 0x0E, 0x70, - 0xE0, 0xE3, 0x87, 0x07, 0x0E, 0x38, 0x78, 0x71, 0xFF, 0xFF, 0x8F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF3, - 0x8E, 0x73, 0x9C, 0x73, 0x8E, 0xE7, 0x1C, 0x77, 0x38, 0xE3, 0xF9, 0xC7, 0x0E, 0x60, 0x38, 0x38, - 0x1E, 0x0E, 0x07, 0x83, 0x81, 0xF0, 0xE0, 0xFC, 0x38, 0x3B, 0x8E, 0x1C, 0xE3, 0xFF, 0x38, 0xFF, - 0xFF, 0x3F, 0xFF, 0xCE, 0x3B, 0xBB, 0x9C, 0xEE, 0xE7, 0x3B, 0xB9, 0xCE, 0x70, 0x3F, 0xFF, 0x87, - 0xFF, 0xF0, 0x7F, 0xFC, 0x0F, 0x07, 0x80, 0xE0, 0xE0, 0x0E, 0x3C, 0x01, 0xE7, 0x00, 0x1D, 0xC0, - 0x03, 0xF8, 0x03, 0xFF, 0xE0, 0xFF, 0xFE, 0x3E, 0x73, 0xE7, 0x0E, 0x1C, 0xE1, 0xC1, 0xFC, 0x38, - 0x3F, 0x87, 0x07, 0xF0, 0xE0, 0xFE, 0x1C, 0x1F, 0xC3, 0x83, 0x80, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, - 0xF8, 0x1C, 0x78, 0x0E, 0x70, 0x0E, 0xE0, 0x0F, 0xF0, 0x3F, 0xFC, 0x7F, 0xFE, 0xF3, 0x8E, 0xE3, - 0x8E, 0xE3, 0x87, 0xE3, 0x87, 0xE3, 0x87, 0xC0, 0x7F, 0xFE, 0x70, 0x7F, 0xFF, 0x38, 0x1F, 0xFF, - 0x9C, 0x07, 0x03, 0x8E, 0x03, 0xC3, 0x87, 0x00, 0xE1, 0xC3, 0x80, 0x39, 0xC1, 0xC0, 0x1D, 0xE0, - 0xE0, 0x07, 0xE0, 0x7F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFF, 0xDC, 0x3F, 0x71, 0xEE, 0x1C, 0x38, 0x7F, - 0x0E, 0x1C, 0x1F, 0x87, 0x0E, 0x0F, 0xC3, 0x87, 0x07, 0xE1, 0xC3, 0x83, 0xF0, 0xE1, 0xC1, 0xF8, - 0x70, 0xE0, 0xE0, 0x60, 0xFF, 0xFB, 0x83, 0xFF, 0xCE, 0x07, 0xFF, 0x38, 0x1E, 0x38, 0xE0, 0x39, - 0xC3, 0x80, 0x77, 0x0F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0xFF, 0xFF, 0xFB, 0x87, 0x1C, 0xFE, 0x18, - 0x71, 0xF8, 0xE1, 0xC7, 0xE3, 0x87, 0x1F, 0x8E, 0x1C, 0x70, 0x30, 0xC1, 0x9C, 0x0F, 0x80, 0xF0, - 0x02, 0x07, 0xC0, 0x7F, 0x87, 0xFC, 0x01, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x01, 0xE1, 0xFC, 0x3F, - 0x83, 0xFC, 0x01, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x1E, 0x3F, 0xC7, 0xF8, 0xFE, 0x0E, - 0x00, 0xE0, 0x07, 0x80, 0x38, 0x01, 0x00, 0x73, 0x8F, 0xC1, 0xF0, 0x38, 0x00, 0x3F, 0x8F, 0xFB, - 0xFF, 0x01, 0xC0, 0x70, 0x1C, 0xFE, 0x3F, 0x8F, 0xF0, 0x1C, 0x07, 0x03, 0xDF, 0xFF, 0xFB, 0x80, - 0xC0, 0x38, 0x0F, 0x81, 0xE0, 0x00, 0x00, 0xC0, 0xC0, 0xF8, 0x70, 0x7E, 0x1C, 0x1F, 0x87, 0x07, - 0xE1, 0xC1, 0xF8, 0x70, 0x7E, 0x1C, 0x1F, 0x87, 0x07, 0xE1, 0xC1, 0xFC, 0x70, 0xE7, 0x1C, 0x78, - 0xFF, 0xFC, 0x1F, 0xFE, 0x01, 0xFE, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x70, 0x00, - 0x1C, 0x00, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x0C, 0x38, 0x7E, 0x1C, 0x3F, 0x0E, 0x1F, 0x87, - 0x0F, 0xC3, 0x87, 0xE1, 0xC3, 0xF0, 0xE1, 0xF8, 0x70, 0xEE, 0x38, 0x77, 0x1C, 0x73, 0x8E, 0x38, - 0xE7, 0x3C, 0x7F, 0xFC, 0x1F, 0xFC, 0x01, 0xF0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, - 0x00, 0x00, 0x00, 0x03, 0xE0, 0x07, 0xFC, 0x07, 0xFF, 0x07, 0x83, 0xC7, 0x80, 0xF3, 0x80, 0x3B, - 0xC0, 0x1F, 0xC0, 0x07, 0xE0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x7E, 0x00, 0x3B, 0x80, - 0x39, 0xC0, 0x1C, 0xF0, 0x1E, 0x3E, 0x3E, 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, 0x00, 0x0F, 0x81, - 0xFF, 0x1F, 0xF8, 0xE0, 0xEE, 0x07, 0x70, 0x1F, 0xFD, 0xFF, 0xFF, 0xE3, 0xFF, 0x01, 0xFC, 0x1C, - 0xF1, 0xE3, 0xFE, 0x0F, 0xE0, 0x1C, 0x00, 0x60, 0x01, 0xF8, 0x01, 0xDC, 0x01, 0xEE, 0x00, 0xE3, - 0x80, 0xE1, 0xC0, 0x70, 0xE0, 0x38, 0x38, 0x38, 0x1C, 0x1C, 0x0E, 0x1E, 0x03, 0x8E, 0x01, 0xC7, - 0x00, 0xE7, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x07, 0xC0, 0x03, 0xE0, 0x01, 0xF0, 0x00, 0x70, 0x00, - 0xE0, 0x1C, 0xE0, 0x79, 0xC1, 0xF3, 0x83, 0x83, 0x87, 0x07, 0x1C, 0x0E, 0x38, 0x0E, 0x70, 0x1D, - 0xC0, 0x1B, 0x80, 0x3F, 0x00, 0x7C, 0x00, 0x78, 0x00, 0xE0, 0x00, 0x1C, 0xE0, 0x07, 0x70, 0x03, - 0x98, 0x00, 0xEE, 0x00, 0x00, 0x03, 0x00, 0x0F, 0xC0, 0x0E, 0xE0, 0x0F, 0x70, 0x07, 0x1C, 0x07, - 0x0E, 0x03, 0x87, 0x01, 0xC1, 0xC1, 0xC0, 0xE0, 0xE0, 0x70, 0xF0, 0x1C, 0x70, 0x0E, 0x38, 0x07, - 0x38, 0x01, 0xDC, 0x00, 0xEE, 0x00, 0x3E, 0x00, 0x1F, 0x00, 0x0F, 0x80, 0x03, 0x80, 0x73, 0x80, - 0x77, 0x00, 0xE6, 0x00, 0xEE, 0x00, 0x00, 0x1C, 0x03, 0x9C, 0x0F, 0x38, 0x3E, 0x70, 0x70, 0x70, - 0xE0, 0xE3, 0x81, 0xC7, 0x01, 0xCE, 0x03, 0xB8, 0x03, 0x70, 0x07, 0xE0, 0x0F, 0x80, 0x0F, 0x00, - 0x1C, 0x00, 0x03, 0xE0, 0x00, 0x00, 0x0F, 0xF8, 0x00, 0x00, 0x1F, 0xFC, 0x00, 0x00, 0x3C, 0x1E, - 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x70, 0x07, 0x38, 0x06, 0xF0, 0x07, 0x9C, 0x0E, 0xE0, 0x03, - 0x9C, 0x0E, 0xE0, 0x03, 0x9C, 0x1C, 0xE0, 0x03, 0x8E, 0x1C, 0xE0, 0x03, 0x8E, 0x1C, 0xE0, 0x03, - 0x87, 0x38, 0xE0, 0x03, 0x87, 0x38, 0x70, 0x07, 0x07, 0x38, 0x70, 0x07, 0x03, 0xF0, 0x78, 0x0F, - 0x03, 0xF0, 0x3E, 0x3E, 0x03, 0xE0, 0x1F, 0xFC, 0x01, 0xE0, 0x0F, 0xF8, 0x01, 0xE0, 0x01, 0xC0, - 0x01, 0xC0, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x00, - 0x1F, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0F, 0x83, 0x80, 0xE7, 0xFC, 0x70, 0x39, 0xFF, 0x8E, 0x07, - 0x38, 0x38, 0xE0, 0xEE, 0x07, 0x1C, 0x39, 0xC0, 0x73, 0x87, 0x38, 0x0E, 0x39, 0xC7, 0x01, 0xC7, - 0x38, 0xE0, 0x38, 0xE7, 0x1C, 0x07, 0x0F, 0xC3, 0xC1, 0xC1, 0xF8, 0x3C, 0x78, 0x1F, 0x03, 0xFE, - 0x03, 0xC0, 0x3F, 0x80, 0x78, 0x01, 0xC0, 0x0E, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x78, 0x00, - 0x00, 0x3E, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x40, 0x00, 0x03, 0xE0, 0x0F, 0xF8, 0x3F, 0xFC, - 0x3C, 0x3E, 0x78, 0x0E, 0x70, 0x0E, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, - 0xE0, 0x06, 0x70, 0x0F, 0x78, 0x0F, 0x3C, 0x3F, 0x3F, 0xFF, 0x0F, 0xF7, 0x03, 0xC7, 0x00, 0x07, - 0x0F, 0x83, 0xFE, 0x7F, 0xE7, 0x07, 0xE0, 0x7E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xF0, - 0x07, 0x80, 0x3F, 0x01, 0xF0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x00, 0x38, 0x00, - 0xC0, 0x87, 0x03, 0xB8, 0x0F, 0xE0, 0x1F, 0x00, 0x1F, 0x00, 0xFE, 0x43, 0xBB, 0xDC, 0x0F, 0xE0, - 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x03, 0x9C, 0x0E, 0x00, 0x70, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0x80, - 0xE0, 0x00, 0x0F, 0xDF, 0xFB, 0xE3, 0x00, 0x00, 0xEE, 0x73, 0x00, 0x77, 0x6C, 0x00, 0x7E, 0x1F, - 0xFB, 0x1F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x19, 0x80, 0x01, 0x80, 0x07, - 0x01, 0xF0, 0x07, 0xC1, 0x98, 0x03, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x03, 0x1E, 0x00, 0x07, 0xD9, 0x80, 0x03, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x78, 0x1F, 0x80, 0x3E, 0x0C, 0xC0, 0x13, 0x00, - 0x03, 0x80, 0x00, 0x03, 0xE0, 0x00, 0x03, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x18, 0x00, 0x0C, 0x18, 0x00, 0x0E, 0x18, 0x38, 0x06, 0x00, 0xF0, 0x07, 0x00, - 0xE0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x18, 0x78, 0x00, 0x1F, - 0x18, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x60, 0x07, - 0x00, 0x60, 0x1E, 0x08, 0x70, 0x18, 0x1C, 0x30, 0x00, 0x18, 0x30, 0x00, 0x18, 0x00, 0x00, 0x30, - 0x00, 0x00, 0x00, 0x00, 0x1C, 0x60, 0x07, 0x70, 0x03, 0xF0, 0x00, 0xF0, 0x00, 0x00, 0x06, 0x00, - 0x63, 0x80, 0x79, 0xC0, 0x7C, 0xE0, 0x3E, 0x70, 0x3F, 0x38, 0x3F, 0x9C, 0x1D, 0xCE, 0x1E, 0xE7, - 0x1E, 0x73, 0x8E, 0x39, 0xCF, 0x1C, 0xE7, 0x0E, 0x77, 0x07, 0x3F, 0x83, 0x9F, 0x81, 0xCF, 0x80, - 0xE7, 0xC0, 0x73, 0xC0, 0x3F, 0xE0, 0x1F, 0x00, 0x03, 0x80, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, - 0x38, 0xC0, 0x33, 0x80, 0x7E, 0x00, 0x78, 0x00, 0x00, 0x0C, 0x0E, 0x38, 0x1C, 0x70, 0x78, 0xE1, - 0xF1, 0xC3, 0xE3, 0x8F, 0xC7, 0x3B, 0x8E, 0x77, 0x1D, 0xCE, 0x3F, 0x1C, 0x7E, 0x38, 0xF8, 0x71, - 0xE0, 0xFB, 0xC1, 0xF0, 0x00, 0xE0, 0x01, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x1C, 0x00, + 0xEE, 0x07, 0x70, 0x3B, 0x81, 0xDC, 0x0E, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x0C, 0x00, 0x00, 0x30, + 0x3B, 0x83, 0xDC, 0x3C, 0xE3, 0xC7, 0x3C, 0x39, 0xC1, 0xFC, 0x0F, 0xE0, 0x7F, 0x83, 0x9E, 0x1C, + 0x78, 0xE1, 0xE7, 0x07, 0x38, 0x3C, 0x1C, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x00, 0x06, 0x07, 0xE0, + 0x7E, 0x0F, 0xE1, 0xFE, 0x1F, 0xE3, 0xFE, 0x77, 0xE7, 0x7E, 0xE7, 0xFC, 0x7F, 0xC7, 0xF8, 0x7F, + 0x07, 0xF0, 0x70, 0x18, 0xE0, 0x73, 0x80, 0xFC, 0x01, 0xE0, 0x00, 0x03, 0x80, 0xE7, 0x03, 0x9C, + 0x0E, 0x70, 0x70, 0xE1, 0xC3, 0x87, 0x0F, 0x38, 0x1C, 0xE0, 0x73, 0x00, 0xFC, 0x03, 0xF0, 0x0F, + 0x80, 0x1E, 0x00, 0x78, 0x01, 0xC0, 0x07, 0x00, 0x38, 0x03, 0xE0, 0x1F, 0x00, 0x30, 0x00, 0x60, + 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, + 0x07, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0xC0, 0xC0, 0xDC, 0x1C, 0x3B, 0x83, + 0x87, 0x70, 0x70, 0xEE, 0x0E, 0x1D, 0xC1, 0xC3, 0xB8, 0x38, 0x77, 0x07, 0x0E, 0xE0, 0xE1, 0xDC, + 0x1C, 0x3B, 0x83, 0x87, 0x70, 0x70, 0xEE, 0x0E, 0x1D, 0xC1, 0xC3, 0xB8, 0x38, 0x77, 0x0F, 0x0E, + 0xF3, 0xF3, 0xCF, 0xFF, 0xF0, 0xFE, 0xFC, 0x06, 0x06, 0x00, 0x60, 0xC1, 0xB8, 0x30, 0x6E, 0x0C, + 0x1B, 0x83, 0x06, 0xE0, 0xC1, 0xB8, 0x30, 0x6E, 0x0C, 0x1B, 0x83, 0x06, 0xE0, 0xC1, 0xB8, 0x30, + 0x67, 0x1E, 0x39, 0xC7, 0x8E, 0x7F, 0xFF, 0x8F, 0xCF, 0xC0, 0xC0, 0xC0, 0x18, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0xFF, 0xE0, 0xFF, 0xC0, 0x1C, 0x00, 0x1C, 0x00, 0x1F, 0xFC, 0x1F, 0xFE, 0x1C, 0x1F, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x1F, 0x1F, 0xFE, - 0x1F, 0xF8, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, 0xF8, 0x07, 0x00, 0x0E, - 0x00, 0x1F, 0xF0, 0x3F, 0xF0, 0x7F, 0xF0, 0xE0, 0xE1, 0xC1, 0xC3, 0x83, 0x87, 0x0F, 0x0F, 0xFC, - 0x1F, 0xF0, 0xFE, 0x07, 0xFE, 0x3F, 0xFD, 0xC1, 0xEE, 0x07, 0xF0, 0x1F, 0x80, 0xFC, 0x37, 0xE3, - 0xFF, 0x0F, 0xBF, 0xFD, 0xFF, 0xCF, 0xF7, 0x70, 0x3B, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, - 0x00, 0x67, 0xC7, 0x7F, 0x3F, 0xFD, 0xF0, 0xEF, 0x03, 0xF8, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, - 0x85, 0xFC, 0x7F, 0xF1, 0xEF, 0xFF, 0x77, 0xFF, 0x8C, 0xDC, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x00, 0x60, 0x06, 0x00, 0x6F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, - 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, - 0xE0, 0x0E, 0x00, 0x01, 0xC0, 0x70, 0x1D, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0x80, 0xE0, 0x38, 0x0E, - 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x00, 0x1F, 0xFC, 0x3F, 0xFC, 0x7F, 0xF8, 0xE0, - 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x01, 0xFF, 0xC3, 0xFF, 0x80, 0xE0, 0x01, - 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0x3F, 0xEF, 0xFD, - 0xFF, 0xB8, 0x07, 0x00, 0xE0, 0x3F, 0xC7, 0xF8, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, - 0x00, 0xFF, 0xE7, 0xFF, 0xBF, 0xFD, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0xE0, 0xFF, 0xC7, - 0xFF, 0xBE, 0x3D, 0xE0, 0xFE, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x7F, 0x0F, 0xB9, 0xFC, - 0x0F, 0x80, 0x00, 0x00, 0x7F, 0xCF, 0xFC, 0xFF, 0xCE, 0x00, 0xE0, 0x0E, 0x00, 0xE7, 0x0F, 0xFC, - 0xFF, 0xEF, 0x0E, 0xF0, 0x6E, 0x07, 0xE0, 0x7E, 0x06, 0x00, 0xE0, 0x3E, 0x0F, 0xC0, 0xF8, 0x70, - 0x18, 0x0C, 0x3C, 0x1C, 0x0F, 0x0E, 0x0E, 0x07, 0x07, 0x87, 0x07, 0x01, 0xC3, 0x87, 0x80, 0x71, - 0xC3, 0x80, 0x3C, 0xE3, 0xC0, 0x0E, 0x71, 0xC0, 0x07, 0xBD, 0xC0, 0x01, 0xFF, 0xE0, 0x01, 0xFF, - 0xF0, 0x00, 0xE7, 0x1C, 0x00, 0xF3, 0x8F, 0x00, 0x71, 0xC3, 0x80, 0x70, 0xE1, 0xE0, 0x78, 0x70, - 0x70, 0x38, 0x38, 0x1C, 0x3C, 0x1C, 0x0F, 0xDC, 0x0E, 0x03, 0xE0, 0x00, 0x00, 0x70, 0x00, 0x00, - 0x38, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x70, 0x60, 0xE3, 0xC3, 0x8F, 0x0E, 0x1C, 0x70, 0x38, - 0xE7, 0x00, 0xE7, 0x70, 0x07, 0xBF, 0x80, 0x1F, 0xF8, 0x00, 0xFF, 0xC0, 0x07, 0xFE, 0x00, 0x73, - 0xB8, 0x07, 0x9D, 0xE0, 0x78, 0xE7, 0x87, 0x87, 0x1F, 0xB8, 0x38, 0x7C, 0x00, 0x00, 0xE0, 0x00, - 0x07, 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x07, 0x80, 0xFF, 0x0F, 0xFC, 0xF0, 0xE7, 0x07, 0xB0, - 0x1C, 0x00, 0xE0, 0x0E, 0x07, 0xF0, 0x3E, 0x01, 0xFC, 0x00, 0xF0, 0x03, 0x80, 0x1F, 0x80, 0xFE, - 0x07, 0x78, 0x7B, 0xFF, 0x87, 0xF8, 0x0E, 0x00, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0xF8, 0x00, 0x00, - 0x1F, 0x0F, 0xF3, 0xFF, 0x70, 0xE0, 0x1C, 0x07, 0x83, 0xE0, 0x7C, 0x03, 0xC0, 0x3B, 0x87, 0x70, - 0xEF, 0xFC, 0xFF, 0x03, 0x00, 0x78, 0x0F, 0x01, 0xE0, 0x7C, 0x00, 0x00, 0xC0, 0x18, 0xE0, 0x3C, - 0xE0, 0x78, 0xE0, 0xF0, 0xE0, 0xE0, 0xE1, 0xE0, 0xE3, 0xC0, 0xE7, 0x80, 0xFF, 0x00, 0xFE, 0x00, - 0xFF, 0x00, 0xE7, 0x80, 0xE3, 0xC0, 0xE1, 0xC0, 0xE1, 0xE0, 0xE0, 0xF0, 0xE0, 0x78, 0xE0, 0x3E, - 0xE0, 0x3E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x60, 0x77, 0x07, 0xB8, 0x79, 0xC7, - 0x8E, 0x78, 0x73, 0x83, 0xF8, 0x1F, 0xC0, 0xFF, 0x07, 0x3C, 0x38, 0xF1, 0xC3, 0xCE, 0x0F, 0xF0, - 0x7C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0xC0, 0x1B, 0x80, 0xFE, 0x03, 0xB8, 0x1E, 0xE0, - 0x73, 0x83, 0x8F, 0xCE, 0x3F, 0x70, 0xFF, 0xC3, 0xFE, 0x0F, 0xF8, 0x3F, 0xF0, 0xFD, 0xE3, 0xF3, - 0x8E, 0x0F, 0x38, 0x1C, 0xE0, 0x7B, 0x80, 0xEE, 0x01, 0xC0, 0x60, 0x3B, 0x81, 0xEE, 0x07, 0x38, - 0x3C, 0xEE, 0xE3, 0xBF, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0xC3, 0xBF, 0x8E, 0xEE, 0x38, 0x1C, 0xE0, - 0x7B, 0x80, 0xE0, 0xFE, 0x00, 0xEF, 0xF0, 0x1E, 0xFF, 0x03, 0xC0, 0x70, 0x38, 0x07, 0x07, 0x00, - 0x70, 0xF0, 0x07, 0x1E, 0x00, 0x71, 0xC0, 0x07, 0xF8, 0x00, 0x7F, 0x80, 0x07, 0xF8, 0x00, 0x73, - 0xC0, 0x07, 0x1E, 0x00, 0x70, 0xE0, 0x07, 0x07, 0x00, 0x70, 0x78, 0x07, 0x03, 0xC0, 0x70, 0x1C, - 0x07, 0x00, 0xE0, 0xFC, 0x0E, 0xFE, 0x1E, 0xFE, 0x3C, 0x0E, 0x38, 0x0E, 0x70, 0x0E, 0xF0, 0x0F, - 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0E, 0x70, 0x0E, 0x78, 0x0E, 0x3C, 0x0E, 0x1E, 0x0E, 0x0E, 0xC0, - 0x0C, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, 0xC0, 0x1C, - 0xFF, 0xFE, 0x7F, 0xFF, 0x3F, 0xFF, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, 0xC0, - 0x1C, 0xE0, 0x0E, 0x70, 0x07, 0xB8, 0x03, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, - 0x60, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCF, 0xFF, 0x3F, 0xFC, 0xE0, 0x73, - 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x7F, 0x81, 0xF0, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x70, - 0xC0, 0x0F, 0xFB, 0x80, 0x3F, 0xFE, 0x00, 0xFF, 0xF8, 0x03, 0x80, 0xE0, 0x0E, 0x03, 0x80, 0x38, - 0x0E, 0x00, 0xE0, 0x38, 0x03, 0x80, 0xFF, 0xFE, 0x03, 0xFF, 0xF8, 0x0F, 0xFF, 0xE0, 0x38, 0x03, - 0x80, 0xE0, 0x0E, 0x03, 0x80, 0x38, 0x0E, 0x00, 0xE0, 0x38, 0x03, 0x80, 0xE0, 0x0E, 0x03, 0x80, - 0x38, 0x0E, 0x00, 0xE0, 0x00, 0x60, 0x7F, 0xE0, 0x7F, 0xE0, 0x7F, 0xE0, 0x70, 0xE0, 0x70, 0xE0, - 0x70, 0xFF, 0xF0, 0xFF, 0xF0, 0xE0, 0x70, 0xE0, 0x70, 0xE0, 0x70, 0xE0, 0x70, 0xE0, 0x70, 0xE0, - 0x70, 0xFF, 0xFC, 0x00, 0x7F, 0xFF, 0x00, 0x3F, 0xFF, 0x80, 0x1C, 0x01, 0xC0, 0x0E, 0x00, 0xE0, - 0x07, 0x00, 0x70, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x1C, 0xC0, 0xE0, 0x0F, 0xFC, 0x70, 0x07, 0xFF, - 0x38, 0x03, 0xC7, 0xDC, 0x01, 0xC0, 0xEE, 0x00, 0xE0, 0x7F, 0x00, 0x70, 0x1F, 0x80, 0x38, 0x0F, - 0xC0, 0x1C, 0x0F, 0xE0, 0x0E, 0x07, 0x70, 0x07, 0x0F, 0xB8, 0x03, 0xBF, 0x80, 0x00, 0x1F, 0x80, - 0x00, 0x00, 0x00, 0x7F, 0xF0, 0x07, 0xFF, 0x80, 0x3F, 0xFC, 0x01, 0xC0, 0xE0, 0x0E, 0x07, 0x00, - 0x70, 0x38, 0x03, 0x81, 0xFE, 0x1C, 0x0F, 0xFC, 0xE0, 0x7F, 0xF7, 0x03, 0x83, 0xF8, 0x1C, 0x0F, - 0xC0, 0xE0, 0x7E, 0x07, 0x03, 0xF0, 0x38, 0x1C, 0x00, 0x01, 0xC0, 0x00, 0x3E, 0x00, 0x01, 0xE0, - 0x00, 0x0C, 0x00, 0x03, 0x80, 0x00, 0x1F, 0x00, 0x00, 0x7E, 0x00, 0x01, 0xE0, 0x7F, 0x07, 0x83, - 0xFF, 0x0E, 0x07, 0x9E, 0x3C, 0x1E, 0x0E, 0x70, 0x38, 0x1C, 0xE0, 0x70, 0x39, 0xC0, 0xE0, 0x73, - 0x81, 0xC0, 0xE7, 0x03, 0x81, 0xCE, 0x07, 0x07, 0x8E, 0x07, 0x0E, 0x1C, 0x0F, 0x3C, 0x3C, 0x0F, - 0xF0, 0x3E, 0x1F, 0xC4, 0x3F, 0xFF, 0xF8, 0x1F, 0xFF, 0xF8, 0x07, 0x03, 0x80, 0x0E, 0x00, 0x0F, - 0x80, 0x07, 0xC7, 0xC1, 0xC7, 0xF8, 0xE1, 0xEF, 0x38, 0xE1, 0xEE, 0x38, 0x3B, 0x8E, 0x0E, 0xE3, - 0x83, 0xB8, 0xF1, 0xCF, 0x1C, 0xF1, 0xE3, 0xF8, 0x3F, 0xFF, 0xC7, 0xFF, 0xF0, 0x38, 0x70, 0x03, - 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x1E, 0x78, 0x0F, 0x70, 0x07, 0xF0, 0x07, 0xE0, 0x00, 0xE0, - 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x70, 0x07, 0x70, 0x07, 0x78, 0x0F, 0x3E, - 0x3E, 0x1F, 0xFC, 0x0F, 0xF8, 0x01, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x01, 0xF0, 0x01, 0xE0, 0x00, - 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xE7, 0x07, 0xE0, 0x7E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, - 0xF0, 0x77, 0x8F, 0x3F, 0xE1, 0xFC, 0x07, 0x00, 0x78, 0x07, 0xC0, 0x7C, 0x07, 0x80, 0x00, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, - 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, - 0x00, 0x70, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xE0, 0x0E, 0x00, - 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x80, 0xF8, 0x03, 0x80, 0x38, - 0x03, 0x80, 0x38, 0x60, 0x03, 0x38, 0x03, 0x9E, 0x03, 0xC7, 0x01, 0xC3, 0xC1, 0xC0, 0xE1, 0xE0, - 0x38, 0xE0, 0x1E, 0xE0, 0x07, 0x70, 0x01, 0xF0, 0x00, 0xF8, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, - 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0xE0, 0x39, 0xC0, 0xE7, + 0x1F, 0xF8, 0x18, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x1F, 0xF8, 0x3F, 0xF0, 0x7F, + 0xE0, 0x1C, 0x00, 0x38, 0x00, 0x7F, 0xC0, 0xFF, 0xC1, 0xFF, 0xC3, 0x83, 0x87, 0x07, 0x0E, 0x0E, + 0x1C, 0x3C, 0x3F, 0xF0, 0x7F, 0xC0, 0xC0, 0x1E, 0x07, 0x03, 0xFE, 0x38, 0x7F, 0xF9, 0xC3, 0xC3, + 0xEE, 0x38, 0x07, 0x73, 0xC0, 0x3F, 0x9C, 0x00, 0xFC, 0xE0, 0x00, 0xFF, 0xFC, 0x07, 0xFF, 0xF0, + 0x3F, 0xFF, 0x81, 0xCE, 0x00, 0x0E, 0x70, 0x00, 0x73, 0x80, 0x1F, 0x8E, 0x01, 0xDC, 0x78, 0x1E, + 0xE1, 0xE1, 0xE7, 0x07, 0xFF, 0x38, 0x1F, 0xE0, 0x00, 0x1C, 0x00, 0x60, 0x7C, 0x38, 0x3F, 0xCE, + 0x1F, 0xFB, 0x8F, 0x0E, 0xE3, 0x81, 0xB9, 0xC0, 0x0F, 0xFF, 0xC3, 0xFF, 0xF0, 0xFF, 0xF8, 0x39, + 0xE0, 0x0E, 0x38, 0x3B, 0x8F, 0x0E, 0xE1, 0xFF, 0x38, 0x3F, 0x80, 0x03, 0x80, 0x01, 0xC0, 0x00, + 0xE0, 0x00, 0xF8, 0x00, 0x7C, 0x00, 0x77, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x1C, 0x70, 0x0E, 0x38, + 0x07, 0x1C, 0x07, 0x07, 0x03, 0xFF, 0x81, 0xFF, 0xC1, 0xFF, 0xF0, 0xE7, 0x38, 0xF3, 0x9E, 0x71, + 0xC7, 0x38, 0xE3, 0xBC, 0x70, 0xE0, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0F, 0xC0, 0x3F, + 0x81, 0xCE, 0x07, 0x38, 0x3F, 0xF0, 0xFF, 0xC3, 0xBF, 0x9C, 0xCE, 0x73, 0x3B, 0x8C, 0x70, 0xC0, + 0x0C, 0x07, 0x00, 0xF0, 0x38, 0x0F, 0x81, 0xC0, 0x7C, 0x0E, 0x03, 0xF0, 0x70, 0x3B, 0x83, 0x81, + 0xDC, 0x1C, 0x0E, 0x70, 0xE0, 0xE3, 0x87, 0x07, 0x0E, 0x38, 0x78, 0x71, 0xFF, 0xFF, 0x8F, 0xFF, + 0xFE, 0x7F, 0xFF, 0xF3, 0x8E, 0x73, 0x9C, 0x73, 0x8E, 0xE7, 0x1C, 0x77, 0x38, 0xE3, 0xF9, 0xC7, + 0x0E, 0x60, 0x38, 0x38, 0x1E, 0x0E, 0x07, 0x83, 0x81, 0xF0, 0xE0, 0xFC, 0x38, 0x3B, 0x8E, 0x1C, + 0xE3, 0xFF, 0x38, 0xFF, 0xFF, 0x3F, 0xFF, 0xCE, 0x3B, 0xBB, 0x9C, 0xEE, 0xE7, 0x3B, 0xB9, 0xCE, + 0x70, 0x3F, 0xFF, 0x87, 0xFF, 0xF0, 0x7F, 0xFC, 0x0F, 0x07, 0x80, 0xE0, 0xE0, 0x0E, 0x3C, 0x01, + 0xE7, 0x00, 0x1D, 0xC0, 0x03, 0xF8, 0x03, 0xFF, 0xE0, 0xFF, 0xFE, 0x3E, 0x73, 0xE7, 0x0E, 0x1C, + 0xE1, 0xC1, 0xFC, 0x38, 0x3F, 0x87, 0x07, 0xF0, 0xE0, 0xFE, 0x1C, 0x1F, 0xC3, 0x83, 0x80, 0x7F, + 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1C, 0x78, 0x0E, 0x70, 0x0E, 0xE0, 0x0F, 0xF0, 0x3F, 0xFC, 0x7F, + 0xFE, 0xF3, 0x8E, 0xE3, 0x8E, 0xE3, 0x87, 0xE3, 0x87, 0xE3, 0x87, 0xC0, 0x7F, 0xFE, 0x70, 0x7F, + 0xFF, 0x38, 0x1F, 0xFF, 0x9C, 0x07, 0x03, 0x8E, 0x03, 0xC3, 0x87, 0x00, 0xE1, 0xC3, 0x80, 0x39, + 0xC1, 0xC0, 0x1D, 0xE0, 0xE0, 0x07, 0xE0, 0x7F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFF, 0xDC, 0x3F, 0x71, + 0xEE, 0x1C, 0x38, 0x7F, 0x0E, 0x1C, 0x1F, 0x87, 0x0E, 0x0F, 0xC3, 0x87, 0x07, 0xE1, 0xC3, 0x83, + 0xF0, 0xE1, 0xC1, 0xF8, 0x70, 0xE0, 0xE0, 0x60, 0xFF, 0xFB, 0x83, 0xFF, 0xCE, 0x07, 0xFF, 0x38, + 0x1E, 0x38, 0xE0, 0x39, 0xC3, 0x80, 0x77, 0x0F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0xFF, 0xFF, 0xFB, + 0x87, 0x1C, 0xFE, 0x18, 0x71, 0xF8, 0xE1, 0xC7, 0xE3, 0x87, 0x1F, 0x8E, 0x1C, 0x70, 0x30, 0xC1, + 0x9C, 0x0F, 0x80, 0xF0, 0x02, 0x07, 0xC0, 0x7F, 0x87, 0xFC, 0x01, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, + 0x01, 0xE1, 0xFC, 0x3F, 0x83, 0xFC, 0x01, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x1E, 0x3F, + 0xC7, 0xF8, 0xFE, 0x0E, 0x00, 0xE0, 0x07, 0x80, 0x38, 0x01, 0x00, 0x73, 0x8F, 0xC1, 0xF0, 0x38, + 0x00, 0x3F, 0x8F, 0xFB, 0xFF, 0x01, 0xC0, 0x70, 0x1C, 0xFE, 0x3F, 0x8F, 0xF0, 0x1C, 0x07, 0x03, + 0xDF, 0xFF, 0xFB, 0x80, 0xC0, 0x38, 0x0F, 0x81, 0xE0, 0x00, 0x00, 0xC0, 0xC0, 0xF8, 0x70, 0x7E, + 0x1C, 0x1F, 0x87, 0x07, 0xE1, 0xC1, 0xF8, 0x70, 0x7E, 0x1C, 0x1F, 0x87, 0x07, 0xE1, 0xC1, 0xFC, + 0x70, 0xE7, 0x1C, 0x78, 0xFF, 0xFC, 0x1F, 0xFE, 0x01, 0xFE, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x01, + 0xC0, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x0C, 0x38, 0x7E, 0x1C, + 0x3F, 0x0E, 0x1F, 0x87, 0x0F, 0xC3, 0x87, 0xE1, 0xC3, 0xF0, 0xE1, 0xF8, 0x70, 0xEE, 0x38, 0x77, + 0x1C, 0x73, 0x8E, 0x38, 0xE7, 0x3C, 0x7F, 0xFC, 0x1F, 0xFC, 0x01, 0xF0, 0x00, 0x70, 0x00, 0x38, + 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x07, 0xFC, 0x07, 0xFF, 0x07, 0x83, 0xC7, + 0x80, 0xF3, 0x80, 0x3B, 0xC0, 0x1F, 0xC0, 0x07, 0xE0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, + 0x7E, 0x00, 0x3B, 0x80, 0x39, 0xC0, 0x1C, 0xF0, 0x1E, 0x3E, 0x3E, 0x0F, 0xFE, 0x03, 0xFE, 0x00, + 0x38, 0x00, 0x0F, 0x81, 0xFF, 0x1F, 0xF8, 0xE0, 0xEE, 0x07, 0x70, 0x1F, 0xFD, 0xFF, 0xFF, 0xE3, + 0xFF, 0x01, 0xFC, 0x1C, 0xF1, 0xE3, 0xFE, 0x0F, 0xE0, 0x1C, 0x00, 0x60, 0x01, 0xF8, 0x01, 0xDC, + 0x01, 0xEE, 0x00, 0xE3, 0x80, 0xE1, 0xC0, 0x70, 0xE0, 0x38, 0x38, 0x38, 0x1C, 0x1C, 0x0E, 0x1E, + 0x03, 0x8E, 0x01, 0xC7, 0x00, 0xE7, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x07, 0xC0, 0x03, 0xE0, 0x01, + 0xF0, 0x00, 0x70, 0x00, 0xE0, 0x1C, 0xE0, 0x79, 0xC1, 0xF3, 0x83, 0x83, 0x87, 0x07, 0x1C, 0x0E, + 0x38, 0x0E, 0x70, 0x1D, 0xC0, 0x1B, 0x80, 0x3F, 0x00, 0x7C, 0x00, 0x78, 0x00, 0xE0, 0x00, 0x1C, + 0xE0, 0x07, 0x70, 0x03, 0x98, 0x00, 0xEE, 0x00, 0x00, 0x03, 0x00, 0x0F, 0xC0, 0x0E, 0xE0, 0x0F, + 0x70, 0x07, 0x1C, 0x07, 0x0E, 0x03, 0x87, 0x01, 0xC1, 0xC1, 0xC0, 0xE0, 0xE0, 0x70, 0xF0, 0x1C, + 0x70, 0x0E, 0x38, 0x07, 0x38, 0x01, 0xDC, 0x00, 0xEE, 0x00, 0x3E, 0x00, 0x1F, 0x00, 0x0F, 0x80, + 0x03, 0x80, 0x73, 0x80, 0x77, 0x00, 0xE6, 0x00, 0xEE, 0x00, 0x00, 0x1C, 0x03, 0x9C, 0x0F, 0x38, + 0x3E, 0x70, 0x70, 0x70, 0xE0, 0xE3, 0x81, 0xC7, 0x01, 0xCE, 0x03, 0xB8, 0x03, 0x70, 0x07, 0xE0, + 0x0F, 0x80, 0x0F, 0x00, 0x1C, 0x00, 0x03, 0xE0, 0x00, 0x00, 0x0F, 0xF8, 0x00, 0x00, 0x1F, 0xFC, + 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x70, 0x07, 0x38, 0x06, 0xF0, 0x07, + 0x9C, 0x0E, 0xE0, 0x03, 0x9C, 0x0E, 0xE0, 0x03, 0x9C, 0x1C, 0xE0, 0x03, 0x8E, 0x1C, 0xE0, 0x03, + 0x8E, 0x1C, 0xE0, 0x03, 0x87, 0x38, 0xE0, 0x03, 0x87, 0x38, 0x70, 0x07, 0x07, 0x38, 0x70, 0x07, + 0x03, 0xF0, 0x78, 0x0F, 0x03, 0xF0, 0x3E, 0x3E, 0x03, 0xE0, 0x1F, 0xFC, 0x01, 0xE0, 0x0F, 0xF8, + 0x01, 0xE0, 0x01, 0xC0, 0x01, 0xC0, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, + 0x0F, 0x80, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0F, 0x83, 0x80, 0xE7, 0xFC, 0x70, + 0x39, 0xFF, 0x8E, 0x07, 0x38, 0x38, 0xE0, 0xEE, 0x07, 0x1C, 0x39, 0xC0, 0x73, 0x87, 0x38, 0x0E, + 0x39, 0xC7, 0x01, 0xC7, 0x38, 0xE0, 0x38, 0xE7, 0x1C, 0x07, 0x0F, 0xC3, 0xC1, 0xC1, 0xF8, 0x3C, + 0x78, 0x1F, 0x03, 0xFE, 0x03, 0xC0, 0x3F, 0x80, 0x78, 0x01, 0xC0, 0x0E, 0x00, 0x00, 0x01, 0xC0, + 0x00, 0x00, 0x78, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x40, 0x00, 0x03, 0xE0, + 0x0F, 0xF8, 0x3F, 0xFC, 0x3C, 0x3E, 0x78, 0x0E, 0x70, 0x0E, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, + 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x06, 0x70, 0x0F, 0x78, 0x0F, 0x3C, 0x3F, 0x3F, 0xFF, 0x0F, 0xF7, + 0x03, 0xC7, 0x00, 0x07, 0x0F, 0x83, 0xFE, 0x7F, 0xE7, 0x07, 0xE0, 0x7E, 0x00, 0xE0, 0x0E, 0x00, + 0xE0, 0x0E, 0x00, 0xF0, 0x07, 0x80, 0x3F, 0x01, 0xF0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, + 0x00, 0x00, 0x38, 0x00, 0xC0, 0x87, 0x03, 0xB8, 0x0F, 0xE0, 0x1F, 0x00, 0x1F, 0x00, 0xFE, 0x43, + 0xBB, 0xDC, 0x0F, 0xE0, 0x0F, 0x80, 0x1F, 0x80, 0x7F, 0x03, 0x9C, 0x0E, 0x00, 0x70, 0x00, 0x01, + 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x00, 0x0F, 0xDF, 0xFB, 0xE3, 0x00, 0x00, 0xEE, 0x73, 0x00, 0x77, + 0x6C, 0x00, 0x7E, 0x1F, 0xFB, 0x1F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x19, + 0x80, 0x01, 0x80, 0x07, 0x01, 0xF0, 0x07, 0xC1, 0x98, 0x03, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x03, 0x1E, 0x00, 0x07, 0xD9, 0x80, 0x03, 0x70, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x78, 0x1F, 0x80, 0x3E, + 0x0C, 0xC0, 0x13, 0x00, 0x03, 0x80, 0x00, 0x03, 0xE0, 0x00, 0x03, 0xB0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x0C, 0x18, 0x00, 0x0E, 0x18, 0x38, 0x06, + 0x00, 0xF0, 0x07, 0x00, 0xE0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, + 0x18, 0x78, 0x00, 0x1F, 0x18, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x60, 0x07, 0x00, 0x60, 0x1E, 0x08, 0x70, 0x18, 0x1C, 0x30, 0x00, 0x18, 0x30, 0x00, + 0x18, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x60, 0x07, 0x70, 0x03, 0xF0, 0x00, 0xF0, + 0x00, 0x00, 0x06, 0x00, 0x63, 0x80, 0x79, 0xC0, 0x7C, 0xE0, 0x3E, 0x70, 0x3F, 0x38, 0x3F, 0x9C, + 0x1D, 0xCE, 0x1E, 0xE7, 0x1E, 0x73, 0x8E, 0x39, 0xCF, 0x1C, 0xE7, 0x0E, 0x77, 0x07, 0x3F, 0x83, + 0x9F, 0x81, 0xCF, 0x80, 0xE7, 0xC0, 0x73, 0xC0, 0x3F, 0xE0, 0x1F, 0x00, 0x03, 0x80, 0x03, 0x80, + 0x01, 0xC0, 0x00, 0xE0, 0x38, 0xC0, 0x33, 0x80, 0x7E, 0x00, 0x78, 0x00, 0x00, 0x0C, 0x0E, 0x38, + 0x1C, 0x70, 0x78, 0xE1, 0xF1, 0xC3, 0xE3, 0x8F, 0xC7, 0x3B, 0x8E, 0x77, 0x1D, 0xCE, 0x3F, 0x1C, + 0x7E, 0x38, 0xF8, 0x71, 0xE0, 0xFB, 0xC1, 0xF0, 0x00, 0xE0, 0x01, 0x80, 0x07, 0x00, 0x0E, 0x00, + 0x18, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0xFF, 0xE0, 0xFF, 0xC0, 0x1C, 0x00, 0x1C, 0x00, + 0x1F, 0xFC, 0x1F, 0xFE, 0x1C, 0x1F, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, 0x1C, 0x07, + 0x1C, 0x1F, 0x1F, 0xFE, 0x1F, 0xF8, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x07, 0xFE, 0x0F, 0xFC, 0x1F, + 0xF8, 0x07, 0x00, 0x0E, 0x00, 0x1F, 0xF0, 0x3F, 0xF0, 0x7F, 0xF0, 0xE0, 0xE1, 0xC1, 0xC3, 0x83, + 0x87, 0x0F, 0x0F, 0xFC, 0x1F, 0xF0, 0xFE, 0x07, 0xFE, 0x3F, 0xFD, 0xC1, 0xEE, 0x07, 0xF0, 0x1F, + 0x80, 0xFC, 0x37, 0xE3, 0xFF, 0x0F, 0xBF, 0xFD, 0xFF, 0xCF, 0xF7, 0x70, 0x3B, 0x80, 0x1C, 0x00, + 0xE0, 0x07, 0x00, 0x38, 0x00, 0x67, 0xC7, 0x7F, 0x3F, 0xFD, 0xF0, 0xEF, 0x03, 0xF8, 0x1F, 0x80, + 0xFC, 0x07, 0xE0, 0x3F, 0x85, 0xFC, 0x7F, 0xF1, 0xEF, 0xFF, 0x77, 0xFF, 0x8C, 0xDC, 0x00, 0xE0, + 0x07, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x60, 0x06, 0x00, 0x6F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, + 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, + 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x01, 0xC0, 0x70, 0x1D, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, + 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x00, 0x1F, 0xFC, 0x3F, + 0xFC, 0x7F, 0xF8, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x01, 0xFF, 0xC3, + 0xFF, 0x80, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, + 0x00, 0x3F, 0xEF, 0xFD, 0xFF, 0xB8, 0x07, 0x00, 0xE0, 0x3F, 0xC7, 0xF8, 0x70, 0x0E, 0x01, 0xC0, + 0x38, 0x07, 0x00, 0xE0, 0x00, 0xFF, 0xE7, 0xFF, 0xBF, 0xFD, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, + 0x1C, 0xE0, 0xFF, 0xC7, 0xFF, 0xBE, 0x3D, 0xE0, 0xFE, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, + 0x7F, 0x0F, 0xB9, 0xFC, 0x0F, 0x80, 0x00, 0x00, 0x7F, 0xCF, 0xFC, 0xFF, 0xCE, 0x00, 0xE0, 0x0E, + 0x00, 0xE7, 0x0F, 0xFC, 0xFF, 0xEF, 0x0E, 0xF0, 0x6E, 0x07, 0xE0, 0x7E, 0x06, 0x00, 0xE0, 0x3E, + 0x0F, 0xC0, 0xF8, 0x70, 0x18, 0x0C, 0x3C, 0x1C, 0x0F, 0x0E, 0x0E, 0x07, 0x07, 0x87, 0x07, 0x01, + 0xC3, 0x87, 0x80, 0x71, 0xC3, 0x80, 0x3C, 0xE3, 0xC0, 0x0E, 0x71, 0xC0, 0x07, 0xBD, 0xC0, 0x01, + 0xFF, 0xE0, 0x01, 0xFF, 0xF0, 0x00, 0xE7, 0x1C, 0x00, 0xF3, 0x8F, 0x00, 0x71, 0xC3, 0x80, 0x70, + 0xE1, 0xE0, 0x78, 0x70, 0x70, 0x38, 0x38, 0x1C, 0x3C, 0x1C, 0x0F, 0xDC, 0x0E, 0x03, 0xE0, 0x00, + 0x00, 0x70, 0x00, 0x00, 0x38, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x70, 0x60, 0xE3, 0xC3, 0x8F, + 0x0E, 0x1C, 0x70, 0x38, 0xE7, 0x00, 0xE7, 0x70, 0x07, 0xBF, 0x80, 0x1F, 0xF8, 0x00, 0xFF, 0xC0, + 0x07, 0xFE, 0x00, 0x73, 0xB8, 0x07, 0x9D, 0xE0, 0x78, 0xE7, 0x87, 0x87, 0x1F, 0xB8, 0x38, 0x7C, + 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x07, 0x80, 0xFF, 0x0F, 0xFC, + 0xF0, 0xE7, 0x07, 0xB0, 0x1C, 0x00, 0xE0, 0x0E, 0x07, 0xF0, 0x3E, 0x01, 0xFC, 0x00, 0xF0, 0x03, + 0x80, 0x1F, 0x80, 0xFE, 0x07, 0x78, 0x7B, 0xFF, 0x87, 0xF8, 0x0E, 0x00, 0x3C, 0x01, 0xE0, 0x0F, + 0x00, 0xF8, 0x00, 0x00, 0x1F, 0x0F, 0xF3, 0xFF, 0x70, 0xE0, 0x1C, 0x07, 0x83, 0xE0, 0x7C, 0x03, + 0xC0, 0x3B, 0x87, 0x70, 0xEF, 0xFC, 0xFF, 0x03, 0x00, 0x78, 0x0F, 0x01, 0xE0, 0x7C, 0x00, 0x00, + 0xC0, 0x18, 0xE0, 0x3C, 0xE0, 0x78, 0xE0, 0xF0, 0xE0, 0xE0, 0xE1, 0xE0, 0xE3, 0xC0, 0xE7, 0x80, + 0xFF, 0x00, 0xFE, 0x00, 0xFF, 0x00, 0xE7, 0x80, 0xE3, 0xC0, 0xE1, 0xC0, 0xE1, 0xE0, 0xE0, 0xF0, + 0xE0, 0x78, 0xE0, 0x3E, 0xE0, 0x3E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x60, 0x77, + 0x07, 0xB8, 0x79, 0xC7, 0x8E, 0x78, 0x73, 0x83, 0xF8, 0x1F, 0xC0, 0xFF, 0x07, 0x3C, 0x38, 0xF1, + 0xC3, 0xCE, 0x0F, 0xF0, 0x7C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0xC0, 0x1B, 0x80, 0xFE, + 0x03, 0xB8, 0x1E, 0xE0, 0x73, 0x83, 0x8F, 0xCE, 0x3F, 0x70, 0xFF, 0xC3, 0xFE, 0x0F, 0xF8, 0x3F, + 0xF0, 0xFD, 0xE3, 0xF3, 0x8E, 0x0F, 0x38, 0x1C, 0xE0, 0x7B, 0x80, 0xEE, 0x01, 0xC0, 0x60, 0x3B, + 0x81, 0xEE, 0x07, 0x38, 0x3C, 0xEE, 0xE3, 0xBF, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0xC3, 0xBF, 0x8E, + 0xEE, 0x38, 0x1C, 0xE0, 0x7B, 0x80, 0xE0, 0xFE, 0x00, 0xEF, 0xF0, 0x1E, 0xFF, 0x03, 0xC0, 0x70, + 0x38, 0x07, 0x07, 0x00, 0x70, 0xF0, 0x07, 0x1E, 0x00, 0x71, 0xC0, 0x07, 0xF8, 0x00, 0x7F, 0x80, + 0x07, 0xF8, 0x00, 0x73, 0xC0, 0x07, 0x1E, 0x00, 0x70, 0xE0, 0x07, 0x07, 0x00, 0x70, 0x78, 0x07, + 0x03, 0xC0, 0x70, 0x1C, 0x07, 0x00, 0xE0, 0xFC, 0x0E, 0xFE, 0x1E, 0xFE, 0x3C, 0x0E, 0x38, 0x0E, + 0x70, 0x0E, 0xF0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0E, 0x70, 0x0E, 0x78, 0x0E, 0x3C, 0x0E, + 0x1E, 0x0E, 0x0E, 0xC0, 0x0C, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, + 0x80, 0x39, 0xC0, 0x1C, 0xFF, 0xFE, 0x7F, 0xFF, 0x3F, 0xFF, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, + 0x73, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x70, 0x07, 0xB8, 0x03, 0xC0, 0x00, 0xE0, 0x00, 0x70, + 0x00, 0x38, 0x00, 0x1C, 0x60, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCF, 0xFF, + 0x3F, 0xFC, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x7F, 0x81, 0xF0, 0x01, 0xC0, 0x07, + 0x00, 0x1C, 0x00, 0x70, 0xC0, 0x0F, 0xFB, 0x80, 0x3F, 0xFE, 0x00, 0xFF, 0xF8, 0x03, 0x80, 0xE0, + 0x0E, 0x03, 0x80, 0x38, 0x0E, 0x00, 0xE0, 0x38, 0x03, 0x80, 0xFF, 0xFE, 0x03, 0xFF, 0xF8, 0x0F, + 0xFF, 0xE0, 0x38, 0x03, 0x80, 0xE0, 0x0E, 0x03, 0x80, 0x38, 0x0E, 0x00, 0xE0, 0x38, 0x03, 0x80, + 0xE0, 0x0E, 0x03, 0x80, 0x38, 0x0E, 0x00, 0xE0, 0x00, 0x60, 0x7F, 0xE0, 0x7F, 0xE0, 0x7F, 0xE0, + 0x70, 0xE0, 0x70, 0xE0, 0x70, 0xFF, 0xF0, 0xFF, 0xF0, 0xE0, 0x70, 0xE0, 0x70, 0xE0, 0x70, 0xE0, + 0x70, 0xE0, 0x70, 0xE0, 0x70, 0xFF, 0xFC, 0x00, 0x7F, 0xFF, 0x00, 0x3F, 0xFF, 0x80, 0x1C, 0x01, + 0xC0, 0x0E, 0x00, 0xE0, 0x07, 0x00, 0x70, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x1C, 0xC0, 0xE0, 0x0F, + 0xFC, 0x70, 0x07, 0xFF, 0x38, 0x03, 0xC7, 0xDC, 0x01, 0xC0, 0xEE, 0x00, 0xE0, 0x7F, 0x00, 0x70, + 0x1F, 0x80, 0x38, 0x0F, 0xC0, 0x1C, 0x0F, 0xE0, 0x0E, 0x07, 0x70, 0x07, 0x0F, 0xB8, 0x03, 0xBF, + 0x80, 0x00, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x7F, 0xF0, 0x07, 0xFF, 0x80, 0x3F, 0xFC, 0x01, 0xC0, + 0xE0, 0x0E, 0x07, 0x00, 0x70, 0x38, 0x03, 0x81, 0xFE, 0x1C, 0x0F, 0xFC, 0xE0, 0x7F, 0xF7, 0x03, + 0x83, 0xF8, 0x1C, 0x0F, 0xC0, 0xE0, 0x7E, 0x07, 0x03, 0xF0, 0x38, 0x1C, 0x00, 0x01, 0xC0, 0x00, + 0x3E, 0x00, 0x01, 0xE0, 0x00, 0x0C, 0x00, 0x03, 0x80, 0x00, 0x1F, 0x00, 0x00, 0x7E, 0x00, 0x01, + 0xE0, 0x7F, 0x07, 0x83, 0xFF, 0x0E, 0x07, 0x9E, 0x3C, 0x1E, 0x0E, 0x70, 0x38, 0x1C, 0xE0, 0x70, + 0x39, 0xC0, 0xE0, 0x73, 0x81, 0xC0, 0xE7, 0x03, 0x81, 0xCE, 0x07, 0x07, 0x8E, 0x07, 0x0E, 0x1C, + 0x0F, 0x3C, 0x3C, 0x0F, 0xF0, 0x3E, 0x1F, 0xC4, 0x3F, 0xFF, 0xF8, 0x1F, 0xFF, 0xF8, 0x07, 0x03, + 0x80, 0x0E, 0x00, 0x0F, 0x80, 0x07, 0xC7, 0xC1, 0xC7, 0xF8, 0xE1, 0xEF, 0x38, 0xE1, 0xEE, 0x38, + 0x3B, 0x8E, 0x0E, 0xE3, 0x83, 0xB8, 0xF1, 0xCF, 0x1C, 0xF1, 0xE3, 0xF8, 0x3F, 0xFF, 0xC7, 0xFF, + 0xF0, 0x38, 0x70, 0x03, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x1E, 0x78, 0x0F, 0x70, 0x07, 0xF0, + 0x07, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x70, 0x07, 0x70, + 0x07, 0x78, 0x0F, 0x3E, 0x3E, 0x1F, 0xFC, 0x0F, 0xF8, 0x01, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x01, + 0xF0, 0x01, 0xE0, 0x00, 0x00, 0x0F, 0x83, 0xFE, 0x7F, 0xE7, 0x07, 0xE0, 0x7E, 0x00, 0xE0, 0x0E, + 0x00, 0xE0, 0x0E, 0x00, 0xF0, 0x77, 0x8F, 0x3F, 0xE1, 0xFC, 0x07, 0x00, 0x78, 0x07, 0xC0, 0x7C, + 0x07, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, + 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, + 0x80, 0x1F, 0x00, 0x7C, 0x00, 0x70, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF0, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x80, + 0xF8, 0x03, 0x80, 0x38, 0x03, 0x80, 0x38, 0x60, 0x03, 0x38, 0x03, 0x9E, 0x03, 0xC7, 0x01, 0xC3, + 0xC1, 0xC0, 0xE1, 0xE0, 0x38, 0xE0, 0x1E, 0xE0, 0x07, 0x70, 0x01, 0xF0, 0x00, 0xF8, 0x00, 0x38, + 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, + 0xE0, 0x39, 0xC0, 0xE7, 0x03, 0x9C, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0xCE, 0x07, 0x38, 0x1C, 0xE0, + 0x3F, 0x00, 0xFC, 0x03, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, + 0x70, 0x00, 0x00, 0x60, 0x03, 0x38, 0x03, 0x9E, 0x03, 0xC7, 0x01, 0xC3, 0xC1, 0xC0, 0xE1, 0xE0, + 0x38, 0xE0, 0x1E, 0xE0, 0x07, 0x70, 0x01, 0xF0, 0x00, 0xF8, 0x00, 0x38, 0x00, 0x1C, 0x00, 0xFF, + 0xE0, 0x7F, 0xF0, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0xE0, 0x39, 0xC0, 0xE7, 0x03, 0x9C, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x3F, 0x00, 0xFC, 0x03, - 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x70, 0x00, 0x00, 0x60, - 0x03, 0x38, 0x03, 0x9E, 0x03, 0xC7, 0x01, 0xC3, 0xC1, 0xC0, 0xE1, 0xE0, 0x38, 0xE0, 0x1E, 0xE0, - 0x07, 0x70, 0x01, 0xF0, 0x00, 0xF8, 0x00, 0x38, 0x00, 0x1C, 0x00, 0xFF, 0xE0, 0x7F, 0xF0, 0x03, - 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0xE0, 0x39, 0xC0, 0xE7, 0x03, 0x9C, 0x1C, 0x38, - 0x70, 0xE1, 0xC3, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x3F, 0x00, 0xFC, 0x03, 0xE0, 0x07, 0x80, 0x1E, - 0x01, 0xFE, 0x07, 0xF8, 0x07, 0x00, 0x1C, 0x00, 0x70, 0x00, 0x00, 0x70, 0x07, 0x1E, 0x03, 0xC3, - 0x80, 0xE0, 0xF0, 0x70, 0x1E, 0x3C, 0x03, 0x8E, 0x00, 0xF7, 0x00, 0x1F, 0xC0, 0x03, 0xE0, 0x00, - 0x70, 0x00, 0x3E, 0x00, 0x1F, 0xC0, 0x07, 0x70, 0x03, 0x9E, 0x01, 0xE3, 0xC0, 0x70, 0x70, 0x38, - 0x1E, 0x1E, 0x03, 0xF7, 0x00, 0x7C, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x70, 0x00, 0x1C, 0x70, - 0x39, 0xE1, 0xC3, 0x8F, 0x07, 0x38, 0x1D, 0xC0, 0x3F, 0x00, 0x78, 0x01, 0xE0, 0x0F, 0x80, 0x7F, - 0x01, 0xDE, 0x0E, 0x38, 0x78, 0x7D, 0xC1, 0xF0, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x70, 0xFF, - 0xFC, 0x63, 0xFF, 0xF1, 0xCF, 0xFF, 0xC7, 0x01, 0xC0, 0x1C, 0x07, 0x00, 0x70, 0x1C, 0x01, 0xC0, - 0x70, 0x07, 0x01, 0xC0, 0x1C, 0x07, 0x00, 0x70, 0x1C, 0x01, 0xC0, 0x70, 0x07, 0x01, 0xC0, 0x1C, - 0x07, 0x00, 0x70, 0x1C, 0x01, 0xC0, 0x70, 0x07, 0x01, 0xC0, 0x1C, 0x07, 0x00, 0x70, 0x1F, 0xFF, - 0xF0, 0x7F, 0xFF, 0xC0, 0x00, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x70, 0x00, 0x01, 0xC0, 0xFF, - 0x8E, 0x7F, 0xC7, 0x3F, 0xE3, 0x83, 0x81, 0xC1, 0xC0, 0xE0, 0xE0, 0x70, 0x70, 0x38, 0x38, 0x1C, - 0x1C, 0x0E, 0x0E, 0x07, 0x07, 0x03, 0x83, 0x81, 0xC1, 0xFF, 0xF0, 0xFF, 0xF8, 0x00, 0x1C, 0x00, - 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x60, 0x0C, 0xF0, 0x0E, 0xF0, 0x0E, 0xF0, 0x0E, 0xF0, 0x0E, - 0xF0, 0x0E, 0xF0, 0x0E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x7E, 0x3F, 0xFE, 0x1F, 0xFE, 0x03, 0x0E, - 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x07, 0x00, 0x07, - 0x00, 0x07, 0x00, 0x07, 0x60, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x71, 0x81, 0xC7, 0x07, - 0x1F, 0xFC, 0x3F, 0xF0, 0x7F, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x7C, 0x01, 0xF0, 0x01, 0xC0, 0x07, - 0x00, 0x1C, 0x00, 0x70, 0x60, 0x0D, 0xE0, 0x1F, 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0xFE, 0x01, 0xFC, - 0x03, 0xB9, 0xC7, 0x73, 0x9E, 0xF7, 0xFC, 0xFF, 0xF8, 0xFF, 0xF0, 0x38, 0xE0, 0x71, 0xC0, 0xE3, - 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x60, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x76, - 0x77, 0x77, 0x77, 0xFF, 0x3F, 0xF1, 0xFF, 0x07, 0x70, 0x77, 0x06, 0x70, 0x07, 0xC0, 0x03, 0x80, - 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x39, 0xF0, 0xFF, 0xF3, 0xFF, 0xEF, 0x07, - 0xB8, 0x0F, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xC0, 0x60, - 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x78, 0xFF, 0xEF, 0xFE, 0xF0, 0xFF, 0x07, 0xE0, 0x7E, - 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x70, 0x00, 0x3C, 0x00, 0x1F, - 0xF0, 0x03, 0xFF, 0xC0, 0x7C, 0x3C, 0x07, 0x01, 0xE0, 0xE0, 0x0E, 0xEE, 0x00, 0xFE, 0xE0, 0x07, - 0xFE, 0x00, 0x7F, 0xFF, 0xFF, 0x7F, 0xFF, 0xF1, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x07, 0x0F, - 0x00, 0xE0, 0x70, 0x1E, 0x07, 0xC3, 0xC0, 0x3F, 0xF8, 0x00, 0xFF, 0x00, 0x03, 0x80, 0x00, 0xF8, - 0x01, 0xFF, 0x00, 0xFF, 0xD8, 0xE0, 0xEE, 0x70, 0x3F, 0x70, 0x1D, 0xFF, 0xFE, 0x7F, 0xFF, 0x0E, - 0x00, 0x07, 0x00, 0x01, 0xC0, 0xC0, 0xF0, 0xE0, 0x3F, 0xF0, 0x0F, 0xF0, 0x01, 0xC0, 0x00, 0x3C, - 0x00, 0x1F, 0xF0, 0x03, 0xFF, 0xC0, 0x7C, 0x3C, 0x07, 0x01, 0xE0, 0xE0, 0x0E, 0xEE, 0x00, 0xFE, - 0xE0, 0x07, 0xFE, 0x00, 0x7F, 0xFF, 0xFF, 0x7F, 0xFF, 0xF1, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0xE0, - 0x07, 0x0F, 0x00, 0xE0, 0x70, 0x1E, 0x07, 0xC3, 0xC0, 0x3F, 0xF8, 0x00, 0xFF, 0x00, 0x03, 0x80, - 0x00, 0x38, 0x00, 0x03, 0x80, 0x00, 0x38, 0x00, 0x00, 0xF8, 0x01, 0xFF, 0x00, 0xFF, 0xD8, 0xE0, - 0xEE, 0x70, 0x3F, 0x70, 0x1D, 0xFF, 0xFE, 0x7F, 0xFF, 0x0E, 0x00, 0x07, 0x00, 0x01, 0xC0, 0xC0, - 0xF0, 0xE0, 0x3F, 0xF0, 0x0F, 0xF0, 0x01, 0xC0, 0x00, 0xF0, 0x00, 0x3C, 0x00, 0x3E, 0x00, 0x1E, - 0x00, 0x00, 0x00, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0xC7, 0x00, 0x00, 0xE7, - 0x00, 0x00, 0x7E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x70, 0x18, 0x0C, 0x78, 0x38, 0x1E, - 0x38, 0x38, 0x1C, 0x3C, 0x38, 0x38, 0x1C, 0x38, 0x78, 0x0E, 0x38, 0x70, 0x0F, 0x38, 0xF0, 0x07, - 0x38, 0xE0, 0x07, 0xBD, 0xC0, 0x03, 0xFF, 0xC0, 0x07, 0xFF, 0xC0, 0x07, 0x38, 0xE0, 0x0F, 0x38, - 0xF0, 0x0E, 0x38, 0x70, 0x1C, 0x38, 0x78, 0x3C, 0x38, 0x38, 0x38, 0x38, 0x1C, 0x78, 0x38, 0x1E, - 0x70, 0x38, 0x0E, 0x03, 0x8C, 0x00, 0x39, 0xC0, 0x01, 0xF8, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x07, - 0x06, 0x0E, 0x78, 0x71, 0xE3, 0x87, 0x1C, 0x1C, 0x73, 0x80, 0xE7, 0x70, 0x0F, 0x7F, 0x00, 0x7F, - 0xE0, 0x07, 0xFE, 0x00, 0x7F, 0xE0, 0x0E, 0x77, 0x01, 0xE7, 0x78, 0x3C, 0x73, 0xC7, 0x87, 0x1E, - 0x70, 0x70, 0xE0, 0xC0, 0x1B, 0x80, 0xEE, 0x07, 0xB8, 0x3C, 0xE1, 0xE3, 0x87, 0x0E, 0x38, 0x39, - 0xE0, 0xFF, 0x03, 0xFF, 0x0F, 0xFF, 0x38, 0x3C, 0xE0, 0x3B, 0x80, 0xEE, 0x01, 0xF8, 0x07, 0xE0, - 0x1F, 0x80, 0xFE, 0x03, 0x80, 0x1E, 0x01, 0xF0, 0x07, 0xC0, 0x1C, 0x00, 0x60, 0x77, 0x07, 0xB8, - 0x79, 0xC7, 0x8E, 0x78, 0x73, 0x83, 0xFE, 0x1F, 0xFC, 0xFF, 0xF7, 0x03, 0xB8, 0x1D, 0xC0, 0x7E, - 0x03, 0xF0, 0x38, 0x03, 0xC0, 0x3C, 0x01, 0xC0, 0x00, 0x00, 0x0F, 0xFE, 0x03, 0xFF, 0xC0, 0xFF, - 0xF0, 0x38, 0x1C, 0x1C, 0x07, 0x07, 0x01, 0xC1, 0xC0, 0x70, 0x70, 0x1C, 0x1C, 0x07, 0x07, 0x01, - 0xC1, 0xC0, 0x70, 0x70, 0x1C, 0x1C, 0x07, 0x07, 0x01, 0xC1, 0xC0, 0x70, 0xE0, 0x1C, 0x78, 0x07, - 0x3E, 0x01, 0xFF, 0x00, 0x7C, 0x00, 0x0E, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x00, 0x30, 0x1F, 0xF8, - 0x3F, 0xF0, 0x7F, 0xE0, 0xE1, 0xC1, 0xC3, 0x83, 0x87, 0x07, 0x0E, 0x0C, 0x1C, 0x38, 0x38, 0x70, - 0x70, 0xE0, 0xE3, 0xC1, 0xCF, 0x03, 0xFE, 0x07, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, - 0xC0, 0x0D, 0xC0, 0x1F, 0x80, 0x3F, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, - 0xC0, 0x1F, 0x80, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x03, 0xC0, 0x1F, 0x00, 0x3E, 0x00, 0x30, 0x60, - 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, - 0x07, 0xE0, 0x7E, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x1F, 0x03, 0xE0, 0x18, 0xC0, 0x0C, 0x70, - 0x07, 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, 0xC0, 0x1C, 0xFF, 0xFE, - 0x7F, 0xFF, 0x3F, 0xFF, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, 0xC0, 0x1C, 0xE0, - 0x0E, 0x70, 0x07, 0xF8, 0x03, 0xE0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x38, 0x60, 0x71, - 0xC0, 0xE3, 0x81, 0xC7, 0x03, 0x8E, 0x07, 0x1C, 0x0E, 0x3F, 0xFC, 0x7F, 0xF8, 0xE0, 0x71, 0xC0, - 0xE3, 0x81, 0xC7, 0x03, 0x8E, 0x07, 0xDC, 0x0F, 0x80, 0x07, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, - 0x60, 0x0C, 0xF0, 0x0E, 0xF0, 0x0E, 0xF0, 0x0E, 0xF0, 0x0E, 0xF0, 0x0E, 0xF0, 0x0E, 0x70, 0x0E, - 0x70, 0x1E, 0x78, 0x7E, 0x3F, 0xFE, 0x1F, 0xFE, 0x03, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, - 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x60, 0x73, - 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x71, 0x81, 0xC7, 0x07, 0x1F, 0xFC, 0x3F, 0xF0, 0x7F, 0xC0, - 0x07, 0x00, 0x1C, 0x00, 0x7C, 0x01, 0xF0, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x70, 0xE0, 0x01, - 0xC7, 0x80, 0x1F, 0x3C, 0x00, 0xF9, 0xF0, 0x07, 0xCF, 0x80, 0x7E, 0x7E, 0x03, 0xF3, 0xF0, 0x3B, - 0x9F, 0x81, 0xDC, 0xEE, 0x0E, 0xE7, 0x70, 0xE7, 0x3B, 0xC7, 0x39, 0xCE, 0x71, 0xCE, 0x73, 0x8E, - 0x71, 0xDC, 0x73, 0x8F, 0xC3, 0x9C, 0x7E, 0x1C, 0xE1, 0xF0, 0xE7, 0x0F, 0x07, 0xF8, 0x38, 0x3C, - 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x03, 0x80, 0x70, 0x07, 0x1F, 0x01, 0xF3, - 0xE0, 0x3E, 0x7E, 0x0F, 0xCF, 0xC1, 0xF9, 0xDC, 0x77, 0x3B, 0x8E, 0xE7, 0x71, 0xDC, 0xE7, 0x73, - 0x9C, 0xEE, 0x73, 0x8F, 0x8E, 0x71, 0xF1, 0xCE, 0x1C, 0x3F, 0xC3, 0x87, 0x80, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x70, 0x6E, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xE0, - 0x06, 0x30, 0x03, 0xB8, 0x01, 0xFC, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x07, - 0xC0, 0x03, 0xE0, 0x03, 0xB8, 0x01, 0xDC, 0x00, 0xEE, 0x00, 0xE3, 0x80, 0x71, 0xC0, 0x38, 0xE0, - 0x38, 0x38, 0x1C, 0x1C, 0x0F, 0xFE, 0x0F, 0xFF, 0x87, 0x01, 0xC7, 0x80, 0xF3, 0x80, 0x39, 0xC0, - 0x1D, 0xE0, 0x07, 0x31, 0xC7, 0x30, 0x7E, 0x07, 0x80, 0x00, 0x3F, 0x1F, 0xF3, 0xFF, 0xE0, 0xE4, - 0x1C, 0x03, 0x9F, 0xF7, 0xFF, 0xF1, 0xF8, 0x3F, 0x07, 0xE1, 0xFF, 0xFD, 0xFF, 0x8E, 0x00, 0x06, - 0x30, 0x07, 0xBC, 0x03, 0xDE, 0x00, 0x82, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x07, 0xC0, - 0x03, 0xE0, 0x03, 0xB8, 0x01, 0xDC, 0x00, 0xEE, 0x00, 0xE3, 0x80, 0x71, 0xC0, 0x38, 0xE0, 0x38, - 0x38, 0x1C, 0x1C, 0x0F, 0xFE, 0x0F, 0xFF, 0x87, 0x01, 0xC7, 0x80, 0xF3, 0x80, 0x39, 0xC0, 0x1D, - 0xE0, 0x07, 0x31, 0xCF, 0x38, 0xE7, 0x08, 0x40, 0x00, 0x3F, 0x1F, 0xF3, 0xFF, 0xE0, 0xE4, 0x1C, - 0x03, 0x9F, 0xF7, 0xFF, 0xF1, 0xF8, 0x3F, 0x07, 0xE1, 0xFF, 0xFD, 0xFF, 0x8E, 0x00, 0x00, 0x7F, - 0xFE, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFE, 0x00, 0xEE, 0x00, 0x01, 0xCE, 0x00, 0x01, 0xCE, 0x00, - 0x03, 0x8E, 0x00, 0x03, 0x8E, 0x00, 0x07, 0x0F, 0xFC, 0x07, 0x0F, 0xFE, 0x0E, 0x0F, 0xFE, 0x0E, - 0x1E, 0x00, 0x1F, 0xFE, 0x00, 0x1F, 0xFE, 0x00, 0x3C, 0x0E, 0x00, 0x38, 0x0E, 0x00, 0x78, 0x0E, - 0x00, 0x70, 0x0F, 0xFF, 0xF0, 0x0F, 0xFF, 0x1F, 0x0F, 0x83, 0xFE, 0xFF, 0x1F, 0xFF, 0xFD, 0xC1, - 0xE0, 0xE2, 0x0F, 0x03, 0x80, 0x70, 0x1C, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xF8, 0xE0, 0x07, 0x07, - 0x00, 0x38, 0x3C, 0x0D, 0xC3, 0xF0, 0xEF, 0xFF, 0xFF, 0x3F, 0xCF, 0xF0, 0x70, 0x1C, 0x00, 0x31, - 0xC3, 0x98, 0x1F, 0x80, 0xF0, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, - 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, - 0xFF, 0xFF, 0xFF, 0x38, 0xC0, 0xCE, 0x07, 0xE0, 0x1E, 0x00, 0x00, 0x07, 0xC0, 0xFF, 0x0F, 0xFC, - 0x70, 0x77, 0x03, 0xB8, 0x0D, 0xFF, 0xFF, 0xFF, 0xF0, 0x03, 0x80, 0x1C, 0x0E, 0x78, 0xF1, 0xFF, - 0x07, 0xF0, 0x0E, 0x00, 0x03, 0xC0, 0x07, 0xFC, 0x0F, 0xFF, 0x0F, 0x87, 0xC7, 0x80, 0xE7, 0x80, - 0x7B, 0x80, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x7F, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0xEE, 0x00, 0x77, - 0x80, 0x39, 0xC0, 0x3C, 0xF0, 0x3C, 0x3C, 0x3E, 0x0F, 0xFE, 0x03, 0xFC, 0x00, 0x30, 0x00, 0x1F, - 0x83, 0xFE, 0x1F, 0xF9, 0xC1, 0xE2, 0x07, 0x00, 0x3B, 0xFF, 0xDF, 0xFF, 0xFF, 0xF7, 0x03, 0xB8, - 0x1C, 0xE1, 0xC7, 0xFE, 0x1F, 0xE0, 0x18, 0x00, 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x00, 0x84, - 0x00, 0x00, 0x00, 0x1E, 0x00, 0x3F, 0xE0, 0x7F, 0xF8, 0x7C, 0x3E, 0x3C, 0x07, 0x3C, 0x03, 0xDC, - 0x00, 0xE0, 0x00, 0x70, 0x00, 0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0x70, 0x03, 0xBC, 0x01, - 0xCE, 0x01, 0xE7, 0x81, 0xE1, 0xE1, 0xF0, 0x7F, 0xF0, 0x1F, 0xE0, 0x01, 0x80, 0x00, 0x18, 0xC1, - 0xEF, 0x0E, 0x78, 0x21, 0x00, 0x00, 0x0F, 0xC1, 0xFF, 0x0F, 0xFC, 0xE0, 0xF1, 0x03, 0x80, 0x1D, - 0xFF, 0xEF, 0xFF, 0xFF, 0xFB, 0x81, 0xDC, 0x0E, 0x70, 0xE3, 0xFF, 0x0F, 0xF0, 0x0C, 0x00, 0x00, - 0xE7, 0x00, 0x00, 0xE7, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x70, 0x18, + 0xE0, 0x07, 0x80, 0x1E, 0x01, 0xFE, 0x07, 0xF8, 0x07, 0x00, 0x1C, 0x00, 0x70, 0x00, 0x00, 0x70, + 0x07, 0x1E, 0x03, 0xC3, 0x80, 0xE0, 0xF0, 0x70, 0x1E, 0x3C, 0x03, 0x8E, 0x00, 0xF7, 0x00, 0x1F, + 0xC0, 0x03, 0xE0, 0x00, 0x70, 0x00, 0x3E, 0x00, 0x1F, 0xC0, 0x07, 0x70, 0x03, 0x9E, 0x01, 0xE3, + 0xC0, 0x70, 0x70, 0x38, 0x1E, 0x1E, 0x03, 0xF7, 0x00, 0x7C, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, + 0x70, 0x00, 0x1C, 0x70, 0x39, 0xE1, 0xC3, 0x8F, 0x07, 0x38, 0x1D, 0xC0, 0x3F, 0x00, 0x78, 0x01, + 0xE0, 0x0F, 0x80, 0x7F, 0x01, 0xDE, 0x0E, 0x38, 0x78, 0x7D, 0xC1, 0xF0, 0x01, 0xC0, 0x07, 0x00, + 0x1C, 0x00, 0x70, 0xFF, 0xFC, 0x63, 0xFF, 0xF1, 0xCF, 0xFF, 0xC7, 0x01, 0xC0, 0x1C, 0x07, 0x00, + 0x70, 0x1C, 0x01, 0xC0, 0x70, 0x07, 0x01, 0xC0, 0x1C, 0x07, 0x00, 0x70, 0x1C, 0x01, 0xC0, 0x70, + 0x07, 0x01, 0xC0, 0x1C, 0x07, 0x00, 0x70, 0x1C, 0x01, 0xC0, 0x70, 0x07, 0x01, 0xC0, 0x1C, 0x07, + 0x00, 0x70, 0x1F, 0xFF, 0xF0, 0x7F, 0xFF, 0xC0, 0x00, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x70, + 0x00, 0x01, 0xC0, 0xFF, 0x8E, 0x7F, 0xC7, 0x3F, 0xE3, 0x83, 0x81, 0xC1, 0xC0, 0xE0, 0xE0, 0x70, + 0x70, 0x38, 0x38, 0x1C, 0x1C, 0x0E, 0x0E, 0x07, 0x07, 0x03, 0x83, 0x81, 0xC1, 0xFF, 0xF0, 0xFF, + 0xF8, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x60, 0x0C, 0xF0, 0x0E, 0xF0, 0x0E, + 0xF0, 0x0E, 0xF0, 0x0E, 0xF0, 0x0E, 0xF0, 0x0E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x7E, 0x3F, 0xFE, + 0x1F, 0xFE, 0x03, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x0F, + 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x60, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, + 0x71, 0x81, 0xC7, 0x07, 0x1F, 0xFC, 0x3F, 0xF0, 0x7F, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x7C, 0x01, + 0xF0, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x70, 0x60, 0x0D, 0xE0, 0x1F, 0xC0, 0x3F, 0x80, 0x7F, + 0x00, 0xFE, 0x01, 0xFC, 0x03, 0xB9, 0xC7, 0x73, 0x9E, 0xF7, 0xFC, 0xFF, 0xF8, 0xFF, 0xF0, 0x38, + 0xE0, 0x71, 0xC0, 0xE3, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x60, 0x7E, 0x07, 0xE0, + 0x7E, 0x07, 0xE0, 0x76, 0x77, 0x77, 0x77, 0xFF, 0x3F, 0xF1, 0xFF, 0x07, 0x70, 0x77, 0x06, 0x70, + 0x07, 0xC0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x39, 0xF0, 0xFF, + 0xF3, 0xFF, 0xEF, 0x07, 0xB8, 0x0F, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, + 0x7E, 0x01, 0xC0, 0x60, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x78, 0xFF, 0xEF, 0xFE, 0xF0, + 0xFF, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x70, + 0x00, 0x3C, 0x00, 0x1F, 0xF0, 0x03, 0xFF, 0xC0, 0x7C, 0x3C, 0x07, 0x01, 0xE0, 0xE0, 0x0E, 0xEE, + 0x00, 0xFE, 0xE0, 0x07, 0xFE, 0x00, 0x7F, 0xFF, 0xFF, 0x7F, 0xFF, 0xF1, 0xE0, 0x00, 0x0E, 0x00, + 0x00, 0xE0, 0x07, 0x0F, 0x00, 0xE0, 0x70, 0x1E, 0x07, 0xC3, 0xC0, 0x3F, 0xF8, 0x00, 0xFF, 0x00, + 0x03, 0x80, 0x00, 0xF8, 0x01, 0xFF, 0x00, 0xFF, 0xD8, 0xE0, 0xEE, 0x70, 0x3F, 0x70, 0x1D, 0xFF, + 0xFE, 0x7F, 0xFF, 0x0E, 0x00, 0x07, 0x00, 0x01, 0xC0, 0xC0, 0xF0, 0xE0, 0x3F, 0xF0, 0x0F, 0xF0, + 0x01, 0xC0, 0x00, 0x3C, 0x00, 0x1F, 0xF0, 0x03, 0xFF, 0xC0, 0x7C, 0x3C, 0x07, 0x01, 0xE0, 0xE0, + 0x0E, 0xEE, 0x00, 0xFE, 0xE0, 0x07, 0xFE, 0x00, 0x7F, 0xFF, 0xFF, 0x7F, 0xFF, 0xF1, 0xE0, 0x00, + 0x0E, 0x00, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0xE0, 0x70, 0x1E, 0x07, 0xC3, 0xC0, 0x3F, 0xF8, 0x00, + 0xFF, 0x00, 0x03, 0x80, 0x00, 0x38, 0x00, 0x03, 0x80, 0x00, 0x38, 0x00, 0x00, 0xF8, 0x01, 0xFF, + 0x00, 0xFF, 0xD8, 0xE0, 0xEE, 0x70, 0x3F, 0x70, 0x1D, 0xFF, 0xFE, 0x7F, 0xFF, 0x0E, 0x00, 0x07, + 0x00, 0x01, 0xC0, 0xC0, 0xF0, 0xE0, 0x3F, 0xF0, 0x0F, 0xF0, 0x01, 0xC0, 0x00, 0xF0, 0x00, 0x3C, + 0x00, 0x3E, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, + 0xC7, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x70, 0x18, 0x0C, 0x78, 0x38, 0x1E, 0x38, 0x38, 0x1C, 0x3C, 0x38, 0x38, 0x1C, 0x38, 0x78, 0x0E, 0x38, 0x70, 0x0F, 0x38, 0xF0, 0x07, 0x38, 0xE0, 0x07, 0xBD, 0xC0, 0x03, 0xFF, 0xC0, 0x07, 0xFF, 0xC0, 0x07, 0x38, 0xE0, 0x0F, 0x38, 0xF0, 0x0E, 0x38, 0x70, 0x1C, 0x38, 0x78, 0x3C, 0x38, 0x38, 0x38, 0x38, - 0x1C, 0x78, 0x38, 0x1E, 0x70, 0x38, 0x0E, 0x03, 0x9C, 0x00, 0x39, 0xC0, 0x03, 0x9C, 0x00, 0x10, - 0x80, 0x00, 0x00, 0x07, 0x06, 0x0E, 0x78, 0x71, 0xE3, 0x87, 0x1C, 0x1C, 0x73, 0x80, 0xE7, 0x70, + 0x1C, 0x78, 0x38, 0x1E, 0x70, 0x38, 0x0E, 0x03, 0x8C, 0x00, 0x39, 0xC0, 0x01, 0xF8, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0x07, 0x06, 0x0E, 0x78, 0x71, 0xE3, 0x87, 0x1C, 0x1C, 0x73, 0x80, 0xE7, 0x70, 0x0F, 0x7F, 0x00, 0x7F, 0xE0, 0x07, 0xFE, 0x00, 0x7F, 0xE0, 0x0E, 0x77, 0x01, 0xE7, 0x78, 0x3C, - 0x73, 0xC7, 0x87, 0x1E, 0x70, 0x70, 0xE0, 0x18, 0xC1, 0xEF, 0x0F, 0x38, 0x10, 0x80, 0x00, 0x03, - 0xC0, 0x7F, 0x87, 0xFE, 0x78, 0x73, 0x83, 0xD8, 0x0E, 0x00, 0x70, 0x07, 0x03, 0xF8, 0x1F, 0x00, - 0xFE, 0x00, 0x78, 0x01, 0xC0, 0x0F, 0xC0, 0x7F, 0x03, 0xBC, 0x3D, 0xFF, 0xC3, 0xFC, 0x07, 0x00, - 0x73, 0x8E, 0x71, 0xCE, 0x10, 0x80, 0x00, 0x3E, 0x1F, 0xE7, 0xFE, 0xE1, 0xC0, 0x38, 0x0F, 0x07, - 0xC0, 0xF8, 0x07, 0x80, 0x77, 0x0E, 0xE1, 0xDF, 0xF9, 0xFE, 0x06, 0x00, 0x3F, 0xF3, 0xFF, 0xDF, - 0xFE, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x80, 0x7F, 0x00, 0x7C, 0x00, 0xF0, - 0x03, 0x80, 0x1F, 0x80, 0xFE, 0x07, 0x78, 0x7B, 0xFF, 0x87, 0xF8, 0x0E, 0x00, 0x3F, 0xF3, 0xFF, - 0xDF, 0xFE, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x80, 0x7F, 0x00, 0x7C, 0x00, - 0xF0, 0x03, 0x80, 0x1F, 0x80, 0xFE, 0x07, 0x78, 0x7B, 0xFF, 0x87, 0xF8, 0x0E, 0x00, 0x0F, 0xE0, - 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x0C, 0x00, 0xDC, 0x03, 0xF8, 0x0F, 0xF0, 0x1F, 0xE0, 0x7F, 0xC1, - 0xFF, 0x83, 0xBF, 0x0F, 0x7E, 0x3C, 0xFC, 0x71, 0xF9, 0xE3, 0xF3, 0x87, 0xEE, 0x0F, 0xFC, 0x1F, - 0xF0, 0x3F, 0xC0, 0x7F, 0x80, 0xFE, 0x01, 0xFC, 0x03, 0x80, 0x1F, 0xC1, 0xFC, 0x00, 0x00, 0x00, - 0x60, 0x7E, 0x07, 0xE0, 0xFE, 0x1F, 0xE1, 0xFE, 0x3F, 0xE7, 0x7E, 0x77, 0xEE, 0x7F, 0xC7, 0xFC, - 0x7F, 0x87, 0xF0, 0x7F, 0x07, 0x0C, 0x60, 0x3D, 0xE0, 0x73, 0xC0, 0x42, 0x00, 0x00, 0x18, 0x01, - 0xB8, 0x07, 0xF0, 0x1F, 0xE0, 0x3F, 0xC0, 0xFF, 0x83, 0xFF, 0x07, 0x7E, 0x1E, 0xFC, 0x79, 0xF8, - 0xE3, 0xF3, 0xC7, 0xE7, 0x0F, 0xDC, 0x1F, 0xF8, 0x3F, 0xE0, 0x7F, 0x80, 0xFF, 0x01, 0xFC, 0x03, - 0xF8, 0x07, 0x18, 0xC3, 0x9E, 0x39, 0xC1, 0x08, 0x00, 0x06, 0x07, 0xE0, 0x7E, 0x0F, 0xE1, 0xFE, - 0x1F, 0xE3, 0xFE, 0x77, 0xE7, 0x7E, 0xE7, 0xFC, 0x7F, 0xC7, 0xF8, 0x7F, 0x07, 0xF0, 0x70, 0x06, - 0x30, 0x07, 0xBC, 0x03, 0xDE, 0x00, 0x82, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x3F, 0xE0, 0x3F, 0xF8, - 0x3C, 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, - 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, - 0x1F, 0xF0, 0x01, 0xC0, 0x00, 0x18, 0xC1, 0xEF, 0x0E, 0x78, 0x21, 0x00, 0x00, 0x07, 0xC0, 0xFF, - 0x8F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFE, 0x0E, 0x78, - 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x03, 0xE0, 0x07, 0xFC, 0x07, 0xFF, 0x07, 0x83, 0xC7, 0x80, - 0xF3, 0x80, 0x3B, 0xC0, 0x1F, 0xC0, 0x07, 0xE0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x7E, - 0x00, 0x3B, 0x80, 0x39, 0xC0, 0x1C, 0xF0, 0x1E, 0x3E, 0x3E, 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, - 0x00, 0x0F, 0x81, 0xFF, 0x1F, 0xF8, 0xE0, 0xEE, 0x07, 0x70, 0x1F, 0xFD, 0xFF, 0xFF, 0xE3, 0xFF, - 0x01, 0xFC, 0x1C, 0xF1, 0xE3, 0xFE, 0x0F, 0xE0, 0x1C, 0x00, 0x06, 0x30, 0x07, 0xBC, 0x03, 0xDE, - 0x00, 0x82, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x3F, 0xE0, 0x3F, 0xF8, 0x3C, 0x1E, 0x3C, 0x07, 0x9C, - 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0xF0, 0x01, - 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, 0x01, 0xC0, 0x00, - 0x18, 0xC1, 0xEF, 0x0E, 0x78, 0x21, 0x00, 0x00, 0x07, 0xC0, 0xFF, 0x8F, 0xFC, 0x70, 0x77, 0x03, - 0xB8, 0x0F, 0xFE, 0xFF, 0xFF, 0xF1, 0xFF, 0x80, 0xFE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, - 0x00, 0x0C, 0x60, 0x1E, 0xF0, 0x1E, 0x70, 0x04, 0x20, 0x00, 0x00, 0x07, 0xC0, 0x1F, 0xF0, 0x3F, - 0xF8, 0x78, 0x3C, 0x70, 0x1E, 0xE0, 0x0E, 0xE0, 0x07, 0x00, 0x07, 0x03, 0xFF, 0x03, 0xFF, 0x03, - 0xFF, 0x00, 0x07, 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x0E, 0x70, 0x1E, 0x7C, 0x7C, 0x3F, 0xF8, 0x1F, - 0xF0, 0x03, 0x80, 0x31, 0x87, 0xBC, 0x79, 0xC1, 0x08, 0x00, 0x01, 0xF0, 0x7F, 0xC7, 0xFE, 0xE0, - 0xEE, 0x07, 0x00, 0x71, 0xFF, 0x1F, 0xF0, 0xFF, 0x00, 0x7E, 0x0F, 0xF1, 0xE7, 0xFC, 0x3F, 0x80, - 0xE0, 0x0F, 0xF0, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x06, 0x00, 0x7E, 0x01, 0xDC, 0x07, 0xBC, 0x0E, - 0x38, 0x1C, 0x78, 0x70, 0x70, 0xE0, 0xE3, 0xC0, 0xE7, 0x01, 0xCE, 0x01, 0xF8, 0x03, 0xF0, 0x07, - 0xE0, 0x07, 0x80, 0x0F, 0x00, 0x1C, 0x00, 0x78, 0x03, 0xE0, 0x07, 0x80, 0x00, 0x1F, 0xE0, 0x7F, - 0x80, 0x00, 0x00, 0x00, 0xE0, 0x39, 0xC0, 0xE7, 0x03, 0x9C, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0xCE, - 0x07, 0x38, 0x1C, 0xC0, 0x3F, 0x00, 0xFC, 0x03, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x01, 0xC0, - 0x0E, 0x00, 0xF8, 0x07, 0xC0, 0x0C, 0x00, 0x0C, 0x70, 0x3C, 0xE0, 0x39, 0xC0, 0x21, 0x00, 0x00, - 0x0C, 0x00, 0xFC, 0x03, 0xB8, 0x0F, 0x78, 0x1C, 0x70, 0x38, 0xF0, 0xE0, 0xE1, 0xC1, 0xC7, 0x81, - 0xCE, 0x03, 0x9C, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x0F, 0x00, 0x1E, 0x00, 0x38, 0x00, 0xF0, - 0x07, 0xC0, 0x0F, 0x00, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x02, 0x10, 0x00, 0x03, 0x80, 0xE7, 0x03, - 0x9C, 0x0E, 0x70, 0x70, 0xE1, 0xC3, 0x87, 0x0F, 0x38, 0x1C, 0xE0, 0x73, 0x00, 0xFC, 0x03, 0xF0, - 0x0F, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xC0, 0x07, 0x00, 0x38, 0x03, 0xE0, 0x1F, 0x00, 0x30, 0x00, - 0x07, 0x70, 0x0C, 0xE0, 0x3B, 0x80, 0x66, 0x00, 0x00, 0x0C, 0x00, 0xFC, 0x03, 0xB8, 0x0F, 0x78, - 0x1C, 0x70, 0x38, 0xF0, 0xE0, 0xE1, 0xC1, 0xC7, 0x81, 0xCE, 0x03, 0x9C, 0x03, 0xF0, 0x07, 0xE0, - 0x0F, 0xC0, 0x0F, 0x00, 0x1E, 0x00, 0x38, 0x00, 0xF0, 0x07, 0xC0, 0x0F, 0x00, 0x0E, 0xF0, 0x3B, - 0x81, 0xDC, 0x06, 0x60, 0x00, 0x03, 0x80, 0xE7, 0x03, 0x9C, 0x0E, 0x70, 0x70, 0xE1, 0xC3, 0x87, - 0x0F, 0x38, 0x1C, 0xE0, 0x73, 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xC0, - 0x07, 0x00, 0x38, 0x03, 0xE0, 0x1F, 0x00, 0x30, 0x00, 0x0C, 0x60, 0x3D, 0xE0, 0x7B, 0xC0, 0x41, - 0x00, 0x00, 0x0C, 0x01, 0xBC, 0x03, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0xC0, 0x3F, 0x80, 0x77, - 0x00, 0xEE, 0x03, 0xDE, 0x1F, 0x9F, 0xFF, 0x1F, 0xFE, 0x06, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, - 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x39, 0xC3, 0x9C, 0x39, 0xC1, 0x08, 0x00, 0x06, 0x07, 0xE0, - 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x60, 0x77, 0x07, 0x7F, 0xF3, 0xFF, 0x1F, 0xF0, 0x07, 0x00, 0x70, - 0x07, 0x00, 0x70, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, - 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x80, 0xF8, 0x03, - 0x80, 0x38, 0x03, 0x80, 0x38, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, - 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xF8, 0x3E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x00, 0x06, 0x30, 0x03, - 0xDE, 0x00, 0xF7, 0x80, 0x10, 0x40, 0x00, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x1F, 0x80, 0x07, 0xE0, - 0x01, 0xF8, 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xE0, 0x01, 0xFF, 0xF0, 0x7F, 0xFF, 0x1F, 0x83, - 0xE7, 0xE0, 0x39, 0xF8, 0x0E, 0x7E, 0x03, 0x9F, 0x80, 0xE7, 0xE0, 0x39, 0xF8, 0x3C, 0x7F, 0xFF, - 0x1F, 0xFF, 0x07, 0x0E, 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x04, 0x20, 0x00, 0x00, 0x60, 0x07, 0xE0, - 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xFF, 0x87, 0xFF, 0xE7, 0xFF, 0xE7, 0xE0, 0x77, 0xE0, - 0x77, 0xE0, 0x77, 0xE1, 0xF7, 0xFF, 0xE7, 0xFF, 0xC7, 0x1F, 0xF8, 0xFF, 0xF3, 0xFF, 0xCE, 0x00, - 0x38, 0x00, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x03, 0xFF, 0x0F, 0xFC, 0x0E, 0x00, 0x38, 0x00, - 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00, 0xF8, 0x03, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x07, - 0xE0, 0x1F, 0x00, 0x30, 0x00, 0x3F, 0xEF, 0xFD, 0xFF, 0xB8, 0x07, 0x00, 0xE0, 0x3F, 0xC7, 0xF8, - 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0xC0, 0xF8, 0x03, 0x00, 0x60, 0x1C, 0x0F, 0x81, 0xE0, 0x30, - 0x00, 0x70, 0x07, 0x1E, 0x03, 0xC3, 0x80, 0xE0, 0xF0, 0x70, 0x1E, 0x3C, 0x03, 0x8E, 0x00, 0xF7, - 0x00, 0x1F, 0xC0, 0x03, 0xE0, 0x00, 0x70, 0x00, 0x3E, 0x00, 0x1F, 0xC0, 0x07, 0x70, 0x03, 0x9E, - 0x01, 0xE3, 0xC0, 0x70, 0x70, 0x38, 0x1E, 0x1E, 0x03, 0xF7, 0x00, 0x7C, 0x00, 0x07, 0x00, 0x01, - 0xC0, 0x00, 0x70, 0x00, 0xF8, 0x00, 0x3E, 0x00, 0x04, 0x00, 0x70, 0x73, 0x83, 0x8E, 0x38, 0x73, - 0xC1, 0xDC, 0x0F, 0xC0, 0x3E, 0x01, 0xE0, 0x0F, 0x80, 0xFE, 0x07, 0x70, 0x71, 0xC7, 0x8E, 0x38, - 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x1F, 0x80, 0xF8, 0x03, 0x00, 0x70, 0x07, 0x3C, 0x07, 0x8E, - 0x03, 0x87, 0x83, 0x81, 0xE3, 0xC0, 0x71, 0xC0, 0x3D, 0xC0, 0x0F, 0xE0, 0x3F, 0xFE, 0x1F, 0xFF, - 0x0F, 0xFF, 0x80, 0xFE, 0x00, 0x77, 0x00, 0x73, 0xC0, 0x78, 0xF0, 0x38, 0x38, 0x38, 0x1E, 0x3C, - 0x07, 0x9C, 0x01, 0xC0, 0x70, 0x3B, 0xC3, 0x8E, 0x3C, 0x39, 0xC1, 0xDC, 0x07, 0xE1, 0xFF, 0xCF, - 0xFE, 0x0F, 0x80, 0xFE, 0x07, 0x78, 0x71, 0xC7, 0x87, 0x38, 0x3C, 0x70, 0x3B, 0xC1, 0xCE, 0x0E, - 0x38, 0x71, 0xC3, 0x87, 0x1C, 0x7D, 0xEF, 0xFE, 0x73, 0xE3, 0x8E, 0x1C, 0x70, 0xE1, 0xC7, 0x07, - 0x38, 0x39, 0xC0, 0xE0, 0xFF, 0xCF, 0xFC, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, - 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xCF, 0xFF, 0xFF, 0xF0, 0xFC, 0xFC, 0x0E, 0x0E, 0x0E, - 0x0E, 0x0E, 0x06, 0x06, 0x07, 0x07, 0x0F, 0x7F, 0xF7, 0xE7, 0xFF, 0xFF, 0xFF, 0xC0, 0x38, 0x01, - 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, - 0x70, 0x03, 0x80, 0xFF, 0xCF, 0xFE, 0x00, 0xF0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x7E, 0x07, 0xE0, - 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, - 0xFF, 0xFF, 0x18, 0x38, 0x30, 0x30, 0x30, 0x30, 0x30, 0x38, 0x38, 0x38, 0x38, 0x18, 0x1C, 0xFF, - 0xC7, 0xFF, 0x38, 0x3D, 0xC0, 0xEE, 0x03, 0x70, 0x1B, 0x80, 0xDC, 0x06, 0xE0, 0x37, 0x01, 0xB8, - 0x0D, 0xC0, 0x6E, 0x03, 0x70, 0x1B, 0x80, 0xC0, 0xE1, 0xF3, 0x87, 0xEE, 0x07, 0xB8, 0x07, 0xE0, - 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1D, 0x80, 0x77, 0x01, 0xDC, 0x0E, 0x7C, 0xF8, 0xFF, - 0xC1, 0xFE, 0x00, 0x66, 0x66, 0x66, 0x66, 0xFF, 0x1F, 0xF8, 0x1F, 0x00, 0xF0, 0x0E, 0x01, 0xC0, - 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, - 0x01, 0x80, 0x30, 0xFF, 0x1F, 0xF0, 0x0F, 0x00, 0xF0, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, - 0x1C, 0x03, 0x80, 0xF0, 0x3D, 0xFF, 0x3F, 0xC0, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, - 0xFF, 0x00, 0x70, 0x0E, 0x00, 0xE0, 0x1C, 0x01, 0xC0, 0x1C, 0x03, 0x80, 0x38, 0x07, 0x00, 0x70, - 0x07, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xC7, 0xFF, 0x38, 0x3D, 0xC0, 0xEE, 0x03, 0x70, 0x1B, 0x80, - 0xDC, 0x06, 0xE0, 0x37, 0x01, 0xB8, 0x0D, 0xC0, 0x6E, 0x03, 0x7F, 0xFB, 0xFF, 0xC0, 0x73, 0xF0, - 0xDF, 0xE3, 0xE7, 0x8E, 0x0E, 0x38, 0x1C, 0xE0, 0x73, 0x01, 0xDC, 0x07, 0x70, 0x1D, 0xC0, 0x77, - 0x01, 0xD8, 0x07, 0xE0, 0x1F, 0x8F, 0xFE, 0x3F, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xF0, 0xFB, 0xF3, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1F, 0xFF, 0xC0, 0xFF, 0xE3, 0xFF, - 0xC3, 0x0F, 0x9C, 0x0E, 0x60, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x76, 0x01, - 0xDC, 0x0E, 0x7C, 0xF8, 0xFF, 0xC0, 0xFE, 0x00, 0x70, 0x19, 0xC0, 0x67, 0x01, 0x8E, 0x06, 0x38, - 0x18, 0x70, 0x61, 0xC1, 0x87, 0x06, 0x0E, 0x38, 0x38, 0xE0, 0xE3, 0x81, 0xDC, 0x07, 0xF0, 0xFF, - 0x87, 0xF8, 0x1F, 0x00, 0xFF, 0xC7, 0xFF, 0x38, 0x7D, 0xC0, 0xEE, 0x03, 0xF0, 0x1F, 0x80, 0xFC, - 0x07, 0xFE, 0x3B, 0xF1, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, - 0x01, 0xC0, 0x0E, 0x00, 0x70, 0xFF, 0x87, 0xFF, 0x38, 0x79, 0xC0, 0xEE, 0x07, 0x70, 0x1B, 0x80, - 0xFF, 0x87, 0xFC, 0x38, 0x01, 0x80, 0x1C, 0x00, 0xE0, 0x1E, 0x7F, 0xF3, 0xFE, 0x00, 0xE0, 0x7E, - 0x07, 0x70, 0x77, 0x07, 0x70, 0x73, 0x87, 0x38, 0x73, 0x87, 0x1D, 0xE1, 0xFE, 0x1F, 0x80, 0xE0, - 0x0E, 0x00, 0xE0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x38, 0x03, 0x80, 0x38, 0x70, 0x77, 0x87, 0x38, - 0x71, 0xC7, 0x1C, 0x70, 0xE7, 0x07, 0xF0, 0x7E, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x0E, 0x00, 0x77, - 0xFF, 0x7F, 0xF0, 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x38, 0x03, 0x9C, 0x1C, - 0xE0, 0xC7, 0x0E, 0x38, 0x71, 0xC7, 0x0E, 0x38, 0x71, 0xC3, 0x9C, 0x1C, 0x00, 0xE0, 0x07, 0x00, - 0x38, 0x01, 0xC0, 0x00, 0xFF, 0x8F, 0xFC, 0x01, 0xE0, 0x0E, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, - 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0xE1, 0xC3, 0xE1, 0xC3, 0xE1, - 0xC3, 0xE1, 0xC3, 0xE1, 0xC3, 0xE1, 0xC3, 0xE1, 0xC3, 0x7F, 0xC7, 0x7F, 0x87, 0x60, 0x07, 0x70, - 0x07, 0x78, 0x0E, 0x3E, 0x3E, 0x1F, 0xFC, 0x0F, 0xF8, 0xFF, 0xF3, 0xFF, 0xE0, 0xC7, 0x86, 0x07, - 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1F, - 0xE0, 0x7F, 0x81, 0xC0, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x00, 0x7C, 0x00, 0x77, 0x00, 0x3B, - 0x80, 0x1D, 0xC0, 0x1C, 0x70, 0x0E, 0x38, 0x07, 0x1C, 0x07, 0x07, 0x03, 0x83, 0x81, 0xFF, 0xC1, - 0xFF, 0xF0, 0xE0, 0x38, 0xF0, 0x1E, 0x70, 0x07, 0x38, 0x03, 0xBC, 0x00, 0xE0, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x00, 0x80, 0x00, 0x1F, 0x8F, 0xF9, 0xFF, 0xF0, 0x72, - 0x0E, 0x01, 0xCF, 0xFB, 0xFF, 0xF8, 0xFC, 0x1F, 0x83, 0xF0, 0xFF, 0xFE, 0xFF, 0xC7, 0x00, 0x00, - 0x0E, 0x01, 0xE0, 0x38, 0x02, 0x00, 0x03, 0xC0, 0x01, 0xF0, 0x00, 0x30, 0x00, 0x38, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x07, 0x00, 0x07, 0xC0, 0x03, 0xE0, 0x03, 0xB8, 0x01, 0xDC, 0x00, 0xEE, 0x00, - 0xE3, 0x80, 0x71, 0xC0, 0x38, 0xE0, 0x38, 0x38, 0x1C, 0x1C, 0x0F, 0xFE, 0x0F, 0xFF, 0x87, 0x01, - 0xC7, 0x80, 0xF3, 0x80, 0x39, 0xC0, 0x1D, 0xE0, 0x07, 0x1E, 0x03, 0xE0, 0x1C, 0x03, 0x00, 0x00, - 0x3F, 0x1F, 0xF3, 0xFF, 0xE0, 0xE4, 0x1C, 0x03, 0x9F, 0xF7, 0xFF, 0xF1, 0xF8, 0x3F, 0x07, 0xE1, - 0xFF, 0xFD, 0xFF, 0x8E, 0x00, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x1C, 0x00, - 0x1F, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x7C, 0x00, 0x3E, - 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x03, 0x83, 0x81, - 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, 0x38, 0x03, 0x9C, 0x01, 0xDE, 0x00, - 0x70, 0x00, 0x70, 0x0E, 0x00, 0xC0, 0x1C, 0x0E, 0x01, 0xF0, 0x1F, 0x83, 0x9C, 0x00, 0x01, 0xF8, - 0x7F, 0xC7, 0xFE, 0xE0, 0xE2, 0x0E, 0x00, 0xE3, 0xFE, 0x7F, 0xEF, 0x8E, 0xE0, 0xEE, 0x0E, 0xE1, - 0xEF, 0xFE, 0x7F, 0xE1, 0xC0, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x0C, 0x00, 0x07, 0x00, 0x1C, 0x00, - 0x1F, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x7C, 0x00, 0x3E, - 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x03, 0x83, 0x81, - 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, 0x38, 0x03, 0x9C, 0x01, 0xDE, 0x00, - 0x70, 0x03, 0x80, 0x70, 0x07, 0x00, 0x60, 0xE0, 0x3E, 0x07, 0xE1, 0xCE, 0x00, 0x03, 0xF1, 0xFF, - 0x3F, 0xFE, 0x0E, 0x41, 0xC0, 0x39, 0xFF, 0x7F, 0xFF, 0x1F, 0x83, 0xF0, 0x7E, 0x1F, 0xFF, 0xDF, - 0xF8, 0xE0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x1C, 0x00, 0x1F, 0x00, 0x1D, - 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x3B, 0x80, - 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x03, 0x83, 0x81, 0xC1, 0xC0, 0xFF, - 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, 0x38, 0x03, 0x9C, 0x01, 0xDE, 0x00, 0x70, 0x03, 0xC0, - 0x78, 0x03, 0x00, 0xE0, 0xE0, 0x3E, 0x07, 0xE1, 0xCE, 0x00, 0x03, 0xF1, 0xFF, 0x3F, 0xFE, 0x0E, - 0x41, 0xC0, 0x39, 0xFF, 0x7F, 0xFF, 0x1F, 0x83, 0xF0, 0x7E, 0x1F, 0xFF, 0xDF, 0xF8, 0xE0, 0x03, - 0x10, 0x03, 0xF8, 0x01, 0xFC, 0x00, 0x8C, 0x00, 0x1C, 0x00, 0x1F, 0x00, 0x1D, 0xC0, 0x0E, 0x70, - 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, - 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x03, 0x83, 0x81, 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, - 0x70, 0x1C, 0x78, 0x0F, 0x38, 0x03, 0x9C, 0x01, 0xDE, 0x00, 0x70, 0x18, 0x87, 0xF8, 0xFE, 0x18, - 0x80, 0xE0, 0x3E, 0x07, 0xE1, 0xCE, 0x00, 0x03, 0xF1, 0xFF, 0x3F, 0xFE, 0x0E, 0x41, 0xC0, 0x39, - 0xFF, 0x7F, 0xFF, 0x1F, 0x83, 0xF0, 0x7E, 0x1F, 0xFF, 0xDF, 0xF8, 0xE0, 0x01, 0xC0, 0x01, 0xF0, - 0x01, 0xDC, 0x00, 0xC7, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x07, 0xC0, 0x03, 0xE0, 0x03, - 0xB8, 0x01, 0xDC, 0x00, 0xEE, 0x00, 0xE3, 0x80, 0x71, 0xC0, 0x38, 0xE0, 0x38, 0x38, 0x1C, 0x1C, - 0x0F, 0xFE, 0x0F, 0xFF, 0x87, 0x01, 0xC7, 0x80, 0xF3, 0x80, 0x39, 0xC0, 0x1D, 0xE0, 0x07, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x04, 0x00, 0x0F, 0x03, 0xE0, 0xEE, - 0x18, 0xE0, 0x00, 0x3F, 0x1F, 0xF3, 0xFF, 0xE0, 0xE4, 0x1C, 0x03, 0x9F, 0xF7, 0xFF, 0xF1, 0xF8, - 0x3F, 0x07, 0xE1, 0xFF, 0xFD, 0xFF, 0x8E, 0x00, 0x00, 0x1C, 0x03, 0xC0, 0x70, 0x04, 0x00, 0x00, - 0x70, 0x00, 0x70, 0x00, 0x38, 0x00, 0x18, 0x00, 0x63, 0x00, 0x31, 0x80, 0x1F, 0xC0, 0x07, 0xC0, - 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, - 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x03, 0x83, 0x81, 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, - 0x70, 0x1C, 0x78, 0x0F, 0x38, 0x03, 0x9C, 0x01, 0xDE, 0x00, 0x70, 0x03, 0x80, 0x70, 0x1C, 0x03, - 0x03, 0x1C, 0x73, 0x8F, 0xE0, 0xF8, 0x00, 0x03, 0xF1, 0xFF, 0x3F, 0xFE, 0x0E, 0x41, 0xC0, 0x39, - 0xFF, 0x7F, 0xFF, 0x1F, 0x83, 0xF0, 0x7E, 0x1F, 0xFF, 0xDF, 0xF8, 0xE0, 0x07, 0x00, 0x01, 0xC0, - 0x00, 0xE0, 0x00, 0x38, 0x00, 0x63, 0x00, 0x31, 0x80, 0x1F, 0xC0, 0x07, 0xC0, 0x00, 0x00, 0x00, - 0xE0, 0x00, 0x70, 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, - 0x07, 0x1C, 0x03, 0x8E, 0x03, 0x83, 0x81, 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, - 0x0F, 0x38, 0x03, 0x9C, 0x01, 0xDE, 0x00, 0x70, 0x38, 0x03, 0x80, 0x30, 0x07, 0x03, 0x18, 0x63, - 0x0F, 0xE0, 0xF8, 0x00, 0x03, 0xF1, 0xFF, 0x3F, 0xFE, 0x0E, 0x41, 0xC0, 0x39, 0xFF, 0x7F, 0xFF, - 0x1F, 0x83, 0xF0, 0x7E, 0x1F, 0xFF, 0xDF, 0xF8, 0xE0, 0x03, 0xC0, 0x01, 0xF0, 0x00, 0x38, 0x00, - 0x18, 0x00, 0x63, 0x00, 0x31, 0x80, 0x1F, 0xC0, 0x07, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, - 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x03, - 0x8E, 0x03, 0x83, 0x81, 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, 0x38, 0x03, - 0x9C, 0x01, 0xDE, 0x00, 0x70, 0x1E, 0x03, 0xE0, 0x1C, 0x03, 0x03, 0x1C, 0x73, 0x8F, 0xE0, 0xF8, - 0x00, 0x03, 0xF1, 0xFF, 0x3F, 0xFE, 0x0E, 0x41, 0xC0, 0x39, 0xFF, 0x7F, 0xFF, 0x1F, 0x83, 0xF0, - 0x7E, 0x1F, 0xFF, 0xDF, 0xF8, 0xE0, 0x03, 0x10, 0x03, 0xF8, 0x01, 0xFC, 0x00, 0xCC, 0x00, 0x63, - 0x00, 0x31, 0x80, 0x1F, 0xC0, 0x07, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x7C, 0x00, - 0x3E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x03, 0x83, - 0x81, 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, 0x38, 0x03, 0x9C, 0x01, 0xDE, - 0x00, 0x70, 0x18, 0x87, 0xF8, 0xFE, 0x18, 0x83, 0x18, 0x73, 0x0F, 0xE0, 0xF8, 0x00, 0x03, 0xF1, - 0xFF, 0x3F, 0xFE, 0x0E, 0x41, 0xC0, 0x39, 0xFF, 0x7F, 0xFF, 0x1F, 0x83, 0xF0, 0x7E, 0x1F, 0xFF, - 0xDF, 0xF8, 0xE0, 0x06, 0x30, 0x03, 0xB8, 0x01, 0xFC, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x00, + 0x73, 0xC7, 0x87, 0x1E, 0x70, 0x70, 0xE0, 0xC0, 0x1B, 0x80, 0xEE, 0x07, 0xB8, 0x3C, 0xE1, 0xE3, + 0x87, 0x0E, 0x38, 0x39, 0xE0, 0xFF, 0x03, 0xFF, 0x0F, 0xFF, 0x38, 0x3C, 0xE0, 0x3B, 0x80, 0xEE, + 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0xFE, 0x03, 0x80, 0x1E, 0x01, 0xF0, 0x07, 0xC0, 0x1C, 0x00, + 0x60, 0x77, 0x07, 0xB8, 0x79, 0xC7, 0x8E, 0x78, 0x73, 0x83, 0xFE, 0x1F, 0xFC, 0xFF, 0xF7, 0x03, + 0xB8, 0x1D, 0xC0, 0x7E, 0x03, 0xF0, 0x38, 0x03, 0xC0, 0x3C, 0x01, 0xC0, 0x00, 0x00, 0x0F, 0xFE, + 0x03, 0xFF, 0xC0, 0xFF, 0xF0, 0x38, 0x1C, 0x1C, 0x07, 0x07, 0x01, 0xC1, 0xC0, 0x70, 0x70, 0x1C, + 0x1C, 0x07, 0x07, 0x01, 0xC1, 0xC0, 0x70, 0x70, 0x1C, 0x1C, 0x07, 0x07, 0x01, 0xC1, 0xC0, 0x70, + 0xE0, 0x1C, 0x78, 0x07, 0x3E, 0x01, 0xFF, 0x00, 0x7C, 0x00, 0x0E, 0x00, 0x03, 0x80, 0x00, 0xE0, + 0x00, 0x30, 0x1F, 0xF8, 0x3F, 0xF0, 0x7F, 0xE0, 0xE1, 0xC1, 0xC3, 0x83, 0x87, 0x07, 0x0E, 0x0C, + 0x1C, 0x38, 0x38, 0x70, 0x70, 0xE0, 0xE3, 0xC1, 0xCF, 0x03, 0xFE, 0x07, 0xC0, 0x03, 0x00, 0x0E, + 0x00, 0x1C, 0x00, 0x38, 0xC0, 0x0D, 0xC0, 0x1F, 0x80, 0x3F, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, + 0x03, 0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x03, + 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0x80, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x03, 0xC0, 0x1F, 0x00, + 0x3E, 0x00, 0x30, 0x60, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, + 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x1F, 0x03, 0xE0, + 0x18, 0xC0, 0x0C, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, + 0xC0, 0x1C, 0xFF, 0xFE, 0x7F, 0xFF, 0x3F, 0xFF, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, + 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x70, 0x07, 0xF8, 0x03, 0xE0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, + 0x00, 0x38, 0x60, 0x71, 0xC0, 0xE3, 0x81, 0xC7, 0x03, 0x8E, 0x07, 0x1C, 0x0E, 0x3F, 0xFC, 0x7F, + 0xF8, 0xE0, 0x71, 0xC0, 0xE3, 0x81, 0xC7, 0x03, 0x8E, 0x07, 0xDC, 0x0F, 0x80, 0x07, 0x00, 0x1C, + 0x00, 0x38, 0x00, 0x70, 0x60, 0x0C, 0xF0, 0x0E, 0xF0, 0x0E, 0xF0, 0x0E, 0xF0, 0x0E, 0xF0, 0x0E, + 0xF0, 0x0E, 0x70, 0x0E, 0x70, 0x1E, 0x78, 0x7E, 0x3F, 0xFE, 0x1F, 0xFE, 0x03, 0x0E, 0x00, 0x0E, + 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, + 0x00, 0x07, 0x60, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x71, 0x81, 0xC7, 0x07, 0x1F, 0xFC, + 0x3F, 0xF0, 0x7F, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x7C, 0x01, 0xF0, 0x01, 0xC0, 0x07, 0x00, 0x1C, + 0x00, 0x70, 0xE0, 0x01, 0xC7, 0x80, 0x1F, 0x3C, 0x00, 0xF9, 0xF0, 0x07, 0xCF, 0x80, 0x7E, 0x7E, + 0x03, 0xF3, 0xF0, 0x3B, 0x9F, 0x81, 0xDC, 0xEE, 0x0E, 0xE7, 0x70, 0xE7, 0x3B, 0xC7, 0x39, 0xCE, + 0x71, 0xCE, 0x73, 0x8E, 0x71, 0xDC, 0x73, 0x8F, 0xC3, 0x9C, 0x7E, 0x1C, 0xE1, 0xF0, 0xE7, 0x0F, + 0x07, 0xF8, 0x38, 0x3C, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x03, 0x80, 0x70, + 0x07, 0x1F, 0x01, 0xF3, 0xE0, 0x3E, 0x7E, 0x0F, 0xCF, 0xC1, 0xF9, 0xDC, 0x77, 0x3B, 0x8E, 0xE7, + 0x71, 0xDC, 0xE7, 0x73, 0x9C, 0xEE, 0x73, 0x8F, 0x8E, 0x71, 0xF1, 0xCE, 0x1C, 0x3F, 0xC3, 0x87, + 0x80, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x70, 0x6E, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, + 0xEE, 0xEE, 0xEE, 0xE0, 0x06, 0x30, 0x03, 0xB8, 0x01, 0xFC, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x07, 0x00, 0x07, 0xC0, 0x03, 0xE0, 0x03, 0xB8, 0x01, 0xDC, 0x00, 0xEE, 0x00, 0xE3, 0x80, + 0x71, 0xC0, 0x38, 0xE0, 0x38, 0x38, 0x1C, 0x1C, 0x0F, 0xFE, 0x0F, 0xFF, 0x87, 0x01, 0xC7, 0x80, + 0xF3, 0x80, 0x39, 0xC0, 0x1D, 0xE0, 0x07, 0x31, 0xC7, 0x30, 0x7E, 0x07, 0x80, 0x00, 0x3F, 0x1F, + 0xF3, 0xFF, 0xE0, 0xE4, 0x1C, 0x03, 0x9F, 0xF7, 0xFF, 0xF1, 0xF8, 0x3F, 0x07, 0xE1, 0xFF, 0xFD, + 0xFF, 0x8E, 0x00, 0x06, 0x30, 0x07, 0xBC, 0x03, 0xDE, 0x00, 0x82, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x07, 0xC0, 0x03, 0xE0, 0x03, 0xB8, 0x01, 0xDC, 0x00, 0xEE, 0x00, 0xE3, 0x80, 0x71, 0xC0, 0x38, 0xE0, 0x38, 0x38, 0x1C, 0x1C, 0x0F, 0xFE, 0x0F, 0xFF, 0x87, 0x01, 0xC7, 0x80, 0xF3, - 0x80, 0x39, 0xC0, 0x1D, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, - 0x00, 0x04, 0x00, 0x31, 0xC7, 0x30, 0x7E, 0x07, 0x80, 0x00, 0x3F, 0x1F, 0xF3, 0xFF, 0xE0, 0xE4, - 0x1C, 0x03, 0x9F, 0xF7, 0xFF, 0xF1, 0xF8, 0x3F, 0x07, 0xE1, 0xFF, 0xFD, 0xFF, 0x8E, 0x00, 0x00, - 0x1C, 0x03, 0xC0, 0x70, 0x04, 0x00, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, - 0x0E, 0x00, 0xFF, 0xEF, 0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, - 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x60, 0x0F, 0x00, 0xF0, 0x04, 0x00, 0x0F, 0x81, 0xFE, 0x1F, - 0xF8, 0xE0, 0xEE, 0x07, 0x70, 0x1B, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0x00, 0x38, 0x1C, 0xF1, 0xE3, - 0xFE, 0x0F, 0xE0, 0x1C, 0x00, 0x00, 0x07, 0x00, 0x78, 0x01, 0xC0, 0x04, 0x00, 0x1E, 0x01, 0xF0, - 0x07, 0x00, 0x60, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, - 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, - 0xFF, 0x0F, 0x00, 0x78, 0x00, 0xC0, 0x0E, 0x00, 0x00, 0x07, 0xC0, 0xFF, 0x0F, 0xFC, 0x70, 0x77, - 0x03, 0xB8, 0x0D, 0xFF, 0xFF, 0xFF, 0xF0, 0x03, 0x80, 0x1C, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, - 0x0E, 0x00, 0x18, 0xC3, 0xFC, 0x3F, 0x80, 0x00, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, + 0x80, 0x39, 0xC0, 0x1D, 0xE0, 0x07, 0x31, 0xCF, 0x38, 0xE7, 0x08, 0x40, 0x00, 0x3F, 0x1F, 0xF3, + 0xFF, 0xE0, 0xE4, 0x1C, 0x03, 0x9F, 0xF7, 0xFF, 0xF1, 0xF8, 0x3F, 0x07, 0xE1, 0xFF, 0xFD, 0xFF, + 0x8E, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFE, 0x00, 0xEE, 0x00, 0x01, 0xCE, + 0x00, 0x01, 0xCE, 0x00, 0x03, 0x8E, 0x00, 0x03, 0x8E, 0x00, 0x07, 0x0F, 0xFC, 0x07, 0x0F, 0xFE, + 0x0E, 0x0F, 0xFE, 0x0E, 0x1E, 0x00, 0x1F, 0xFE, 0x00, 0x1F, 0xFE, 0x00, 0x3C, 0x0E, 0x00, 0x38, + 0x0E, 0x00, 0x78, 0x0E, 0x00, 0x70, 0x0F, 0xFF, 0xF0, 0x0F, 0xFF, 0x1F, 0x0F, 0x83, 0xFE, 0xFF, + 0x1F, 0xFF, 0xFD, 0xC1, 0xE0, 0xE2, 0x0F, 0x03, 0x80, 0x70, 0x1C, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, + 0xF8, 0xE0, 0x07, 0x07, 0x00, 0x38, 0x3C, 0x0D, 0xC3, 0xF0, 0xEF, 0xFF, 0xFF, 0x3F, 0xCF, 0xF0, + 0x70, 0x1C, 0x00, 0x31, 0xC3, 0x98, 0x1F, 0x80, 0xF0, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, + 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, + 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x38, 0xC0, 0xCE, 0x07, 0xE0, 0x1E, 0x00, 0x00, 0x07, + 0xC0, 0xFF, 0x0F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0D, 0xFF, 0xFF, 0xFF, 0xF0, 0x03, 0x80, 0x1C, + 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x03, 0xC0, 0x07, 0xFC, 0x0F, 0xFF, 0x0F, 0x87, + 0xC7, 0x80, 0xE7, 0x80, 0x7B, 0x80, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x7F, 0xFF, 0xFF, 0xFF, 0xFC, + 0x00, 0xEE, 0x00, 0x77, 0x80, 0x39, 0xC0, 0x3C, 0xF0, 0x3C, 0x3C, 0x3E, 0x0F, 0xFE, 0x03, 0xFC, + 0x00, 0x30, 0x00, 0x1F, 0x83, 0xFE, 0x1F, 0xF9, 0xC1, 0xE2, 0x07, 0x00, 0x3B, 0xFF, 0xDF, 0xFF, + 0xFF, 0xF7, 0x03, 0xB8, 0x1C, 0xE1, 0xC7, 0xFE, 0x1F, 0xE0, 0x18, 0x00, 0x0E, 0x70, 0x07, 0x38, + 0x03, 0x9C, 0x00, 0x84, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x3F, 0xE0, 0x7F, 0xF8, 0x7C, 0x3E, 0x3C, + 0x07, 0x3C, 0x03, 0xDC, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, + 0x70, 0x03, 0xBC, 0x01, 0xCE, 0x01, 0xE7, 0x81, 0xE1, 0xE1, 0xF0, 0x7F, 0xF0, 0x1F, 0xE0, 0x01, + 0x80, 0x00, 0x18, 0xC1, 0xEF, 0x0E, 0x78, 0x21, 0x00, 0x00, 0x0F, 0xC1, 0xFF, 0x0F, 0xFC, 0xE0, + 0xF1, 0x03, 0x80, 0x1D, 0xFF, 0xEF, 0xFF, 0xFF, 0xFB, 0x81, 0xDC, 0x0E, 0x70, 0xE3, 0xFF, 0x0F, + 0xF0, 0x0C, 0x00, 0x00, 0xE7, 0x00, 0x00, 0xE7, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x42, 0x00, 0x00, + 0x00, 0x00, 0x70, 0x18, 0x0C, 0x78, 0x38, 0x1E, 0x38, 0x38, 0x1C, 0x3C, 0x38, 0x38, 0x1C, 0x38, + 0x78, 0x0E, 0x38, 0x70, 0x0F, 0x38, 0xF0, 0x07, 0x38, 0xE0, 0x07, 0xBD, 0xC0, 0x03, 0xFF, 0xC0, + 0x07, 0xFF, 0xC0, 0x07, 0x38, 0xE0, 0x0F, 0x38, 0xF0, 0x0E, 0x38, 0x70, 0x1C, 0x38, 0x78, 0x3C, + 0x38, 0x38, 0x38, 0x38, 0x1C, 0x78, 0x38, 0x1E, 0x70, 0x38, 0x0E, 0x03, 0x9C, 0x00, 0x39, 0xC0, + 0x03, 0x9C, 0x00, 0x10, 0x80, 0x00, 0x00, 0x07, 0x06, 0x0E, 0x78, 0x71, 0xE3, 0x87, 0x1C, 0x1C, + 0x73, 0x80, 0xE7, 0x70, 0x0F, 0x7F, 0x00, 0x7F, 0xE0, 0x07, 0xFE, 0x00, 0x7F, 0xE0, 0x0E, 0x77, + 0x01, 0xE7, 0x78, 0x3C, 0x73, 0xC7, 0x87, 0x1E, 0x70, 0x70, 0xE0, 0x18, 0xC1, 0xEF, 0x0F, 0x38, + 0x10, 0x80, 0x00, 0x03, 0xC0, 0x7F, 0x87, 0xFE, 0x78, 0x73, 0x83, 0xD8, 0x0E, 0x00, 0x70, 0x07, + 0x03, 0xF8, 0x1F, 0x00, 0xFE, 0x00, 0x78, 0x01, 0xC0, 0x0F, 0xC0, 0x7F, 0x03, 0xBC, 0x3D, 0xFF, + 0xC3, 0xFC, 0x07, 0x00, 0x73, 0x8E, 0x71, 0xCE, 0x10, 0x80, 0x00, 0x3E, 0x1F, 0xE7, 0xFE, 0xE1, + 0xC0, 0x38, 0x0F, 0x07, 0xC0, 0xF8, 0x07, 0x80, 0x77, 0x0E, 0xE1, 0xDF, 0xF9, 0xFE, 0x06, 0x00, + 0x3F, 0xF3, 0xFF, 0xDF, 0xFE, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x80, 0x7F, + 0x00, 0x7C, 0x00, 0xF0, 0x03, 0x80, 0x1F, 0x80, 0xFE, 0x07, 0x78, 0x7B, 0xFF, 0x87, 0xF8, 0x0E, + 0x00, 0x3F, 0xF3, 0xFF, 0xDF, 0xFE, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x80, + 0x7F, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0x80, 0x1F, 0x80, 0xFE, 0x07, 0x78, 0x7B, 0xFF, 0x87, 0xF8, + 0x0E, 0x00, 0x0F, 0xE0, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x0C, 0x00, 0xDC, 0x03, 0xF8, 0x0F, 0xF0, + 0x1F, 0xE0, 0x7F, 0xC1, 0xFF, 0x83, 0xBF, 0x0F, 0x7E, 0x3C, 0xFC, 0x71, 0xF9, 0xE3, 0xF3, 0x87, + 0xEE, 0x0F, 0xFC, 0x1F, 0xF0, 0x3F, 0xC0, 0x7F, 0x80, 0xFE, 0x01, 0xFC, 0x03, 0x80, 0x1F, 0xC1, + 0xFC, 0x00, 0x00, 0x00, 0x60, 0x7E, 0x07, 0xE0, 0xFE, 0x1F, 0xE1, 0xFE, 0x3F, 0xE7, 0x7E, 0x77, + 0xEE, 0x7F, 0xC7, 0xFC, 0x7F, 0x87, 0xF0, 0x7F, 0x07, 0x0C, 0x60, 0x3D, 0xE0, 0x73, 0xC0, 0x42, + 0x00, 0x00, 0x18, 0x01, 0xB8, 0x07, 0xF0, 0x1F, 0xE0, 0x3F, 0xC0, 0xFF, 0x83, 0xFF, 0x07, 0x7E, + 0x1E, 0xFC, 0x79, 0xF8, 0xE3, 0xF3, 0xC7, 0xE7, 0x0F, 0xDC, 0x1F, 0xF8, 0x3F, 0xE0, 0x7F, 0x80, + 0xFF, 0x01, 0xFC, 0x03, 0xF8, 0x07, 0x18, 0xC3, 0x9E, 0x39, 0xC1, 0x08, 0x00, 0x06, 0x07, 0xE0, + 0x7E, 0x0F, 0xE1, 0xFE, 0x1F, 0xE3, 0xFE, 0x77, 0xE7, 0x7E, 0xE7, 0xFC, 0x7F, 0xC7, 0xF8, 0x7F, + 0x07, 0xF0, 0x70, 0x06, 0x30, 0x07, 0xBC, 0x03, 0xDE, 0x00, 0x82, 0x00, 0x00, 0x00, 0x1F, 0x00, + 0x3F, 0xE0, 0x3F, 0xF8, 0x3C, 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, + 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, + 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, 0x01, 0xC0, 0x00, 0x18, 0xC1, 0xEF, 0x0E, 0x78, 0x21, 0x00, + 0x00, 0x07, 0xC0, 0xFF, 0x8F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, + 0x80, 0xFE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x03, 0xE0, 0x07, 0xFC, 0x07, 0xFF, + 0x07, 0x83, 0xC7, 0x80, 0xF3, 0x80, 0x3B, 0xC0, 0x1F, 0xC0, 0x07, 0xE0, 0x03, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFC, 0x00, 0x7E, 0x00, 0x3B, 0x80, 0x39, 0xC0, 0x1C, 0xF0, 0x1E, 0x3E, 0x3E, 0x0F, 0xFE, + 0x03, 0xFE, 0x00, 0x38, 0x00, 0x0F, 0x81, 0xFF, 0x1F, 0xF8, 0xE0, 0xEE, 0x07, 0x70, 0x1F, 0xFD, + 0xFF, 0xFF, 0xE3, 0xFF, 0x01, 0xFC, 0x1C, 0xF1, 0xE3, 0xFE, 0x0F, 0xE0, 0x1C, 0x00, 0x06, 0x30, + 0x07, 0xBC, 0x03, 0xDE, 0x00, 0x82, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x3F, 0xE0, 0x3F, 0xF8, 0x3C, + 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, + 0xF0, 0x01, 0xC0, 0x00, 0x18, 0xC1, 0xEF, 0x0E, 0x78, 0x21, 0x00, 0x00, 0x07, 0xC0, 0xFF, 0x8F, + 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xFE, 0xFF, 0xFF, 0xF1, 0xFF, 0x80, 0xFE, 0x0E, 0x78, 0xF1, + 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x0C, 0x60, 0x1E, 0xF0, 0x1E, 0x70, 0x04, 0x20, 0x00, 0x00, 0x07, + 0xC0, 0x1F, 0xF0, 0x3F, 0xF8, 0x78, 0x3C, 0x70, 0x1E, 0xE0, 0x0E, 0xE0, 0x07, 0x00, 0x07, 0x03, + 0xFF, 0x03, 0xFF, 0x03, 0xFF, 0x00, 0x07, 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x0E, 0x70, 0x1E, 0x7C, + 0x7C, 0x3F, 0xF8, 0x1F, 0xF0, 0x03, 0x80, 0x31, 0x87, 0xBC, 0x79, 0xC1, 0x08, 0x00, 0x01, 0xF0, + 0x7F, 0xC7, 0xFE, 0xE0, 0xEE, 0x07, 0x00, 0x71, 0xFF, 0x1F, 0xF0, 0xFF, 0x00, 0x7E, 0x0F, 0xF1, + 0xE7, 0xFC, 0x3F, 0x80, 0xE0, 0x0F, 0xF0, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x06, 0x00, 0x7E, 0x01, + 0xDC, 0x07, 0xBC, 0x0E, 0x38, 0x1C, 0x78, 0x70, 0x70, 0xE0, 0xE3, 0xC0, 0xE7, 0x01, 0xCE, 0x01, + 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x07, 0x80, 0x0F, 0x00, 0x1C, 0x00, 0x78, 0x03, 0xE0, 0x07, 0x80, + 0x00, 0x1F, 0xE0, 0x7F, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x39, 0xC0, 0xE7, 0x03, 0x9C, 0x1C, 0x38, + 0x70, 0xE1, 0xC3, 0xCE, 0x07, 0x38, 0x1C, 0xC0, 0x3F, 0x00, 0xFC, 0x03, 0xE0, 0x07, 0x80, 0x1E, + 0x00, 0x70, 0x01, 0xC0, 0x0E, 0x00, 0xF8, 0x07, 0xC0, 0x0C, 0x00, 0x0C, 0x70, 0x3C, 0xE0, 0x39, + 0xC0, 0x21, 0x00, 0x00, 0x0C, 0x00, 0xFC, 0x03, 0xB8, 0x0F, 0x78, 0x1C, 0x70, 0x38, 0xF0, 0xE0, + 0xE1, 0xC1, 0xC7, 0x81, 0xCE, 0x03, 0x9C, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x0F, 0x00, 0x1E, + 0x00, 0x38, 0x00, 0xF0, 0x07, 0xC0, 0x0F, 0x00, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x02, 0x10, 0x00, + 0x03, 0x80, 0xE7, 0x03, 0x9C, 0x0E, 0x70, 0x70, 0xE1, 0xC3, 0x87, 0x0F, 0x38, 0x1C, 0xE0, 0x73, + 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xC0, 0x07, 0x00, 0x38, 0x03, 0xE0, + 0x1F, 0x00, 0x30, 0x00, 0x07, 0x70, 0x0C, 0xE0, 0x3B, 0x80, 0x66, 0x00, 0x00, 0x0C, 0x00, 0xFC, + 0x03, 0xB8, 0x0F, 0x78, 0x1C, 0x70, 0x38, 0xF0, 0xE0, 0xE1, 0xC1, 0xC7, 0x81, 0xCE, 0x03, 0x9C, + 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x0F, 0x00, 0x1E, 0x00, 0x38, 0x00, 0xF0, 0x07, 0xC0, 0x0F, + 0x00, 0x0E, 0xF0, 0x3B, 0x81, 0xDC, 0x06, 0x60, 0x00, 0x03, 0x80, 0xE7, 0x03, 0x9C, 0x0E, 0x70, + 0x70, 0xE1, 0xC3, 0x87, 0x0F, 0x38, 0x1C, 0xE0, 0x73, 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0x80, 0x1E, + 0x00, 0x78, 0x01, 0xC0, 0x07, 0x00, 0x38, 0x03, 0xE0, 0x1F, 0x00, 0x30, 0x00, 0x0C, 0x60, 0x3D, + 0xE0, 0x7B, 0xC0, 0x41, 0x00, 0x00, 0x0C, 0x01, 0xBC, 0x03, 0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, + 0xC0, 0x3F, 0x80, 0x77, 0x00, 0xEE, 0x03, 0xDE, 0x1F, 0x9F, 0xFF, 0x1F, 0xFE, 0x06, 0x1C, 0x00, + 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x39, 0xC3, 0x9C, 0x39, 0xC1, 0x08, + 0x00, 0x06, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x60, 0x77, 0x07, 0x7F, 0xF3, 0xFF, 0x1F, + 0xF0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0x00, 0xE0, 0x0E, 0x00, + 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, + 0x0F, 0x80, 0xF8, 0x03, 0x80, 0x38, 0x03, 0x80, 0x38, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, + 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xF8, 0x3E, 0x03, 0x80, 0xE0, 0x38, 0x0E, + 0x00, 0x06, 0x30, 0x03, 0xDE, 0x00, 0xF7, 0x80, 0x10, 0x40, 0x00, 0x00, 0x30, 0x00, 0x6E, 0x00, + 0x1F, 0x80, 0x07, 0xE0, 0x01, 0xF8, 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xE0, 0x01, 0xFF, 0xF0, + 0x7F, 0xFF, 0x1F, 0x83, 0xE7, 0xE0, 0x39, 0xF8, 0x0E, 0x7E, 0x03, 0x9F, 0x80, 0xE7, 0xE0, 0x39, + 0xF8, 0x3C, 0x7F, 0xFF, 0x1F, 0xFF, 0x07, 0x0E, 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x04, 0x20, 0x00, + 0x00, 0x60, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xFF, 0x87, 0xFF, 0xE7, 0xFF, + 0xE7, 0xE0, 0x77, 0xE0, 0x77, 0xE0, 0x77, 0xE1, 0xF7, 0xFF, 0xE7, 0xFF, 0xC7, 0x1F, 0xF8, 0xFF, + 0xF3, 0xFF, 0xCE, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x03, 0xFF, 0x0F, 0xFC, + 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00, 0xF8, 0x03, 0xE0, 0x03, 0x80, + 0x0E, 0x00, 0x38, 0x07, 0xE0, 0x1F, 0x00, 0x30, 0x00, 0x3F, 0xEF, 0xFD, 0xFF, 0xB8, 0x07, 0x00, + 0xE0, 0x3F, 0xC7, 0xF8, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0xC0, 0xF8, 0x03, 0x00, 0x60, 0x1C, + 0x0F, 0x81, 0xE0, 0x30, 0x00, 0x70, 0x07, 0x1E, 0x03, 0xC3, 0x80, 0xE0, 0xF0, 0x70, 0x1E, 0x3C, + 0x03, 0x8E, 0x00, 0xF7, 0x00, 0x1F, 0xC0, 0x03, 0xE0, 0x00, 0x70, 0x00, 0x3E, 0x00, 0x1F, 0xC0, + 0x07, 0x70, 0x03, 0x9E, 0x01, 0xE3, 0xC0, 0x70, 0x70, 0x38, 0x1E, 0x1E, 0x03, 0xF7, 0x00, 0x7C, + 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x70, 0x00, 0xF8, 0x00, 0x3E, 0x00, 0x04, 0x00, 0x70, 0x73, + 0x83, 0x8E, 0x38, 0x73, 0xC1, 0xDC, 0x0F, 0xC0, 0x3E, 0x01, 0xE0, 0x0F, 0x80, 0xFE, 0x07, 0x70, + 0x71, 0xC7, 0x8E, 0x38, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x1F, 0x80, 0xF8, 0x03, 0x00, 0x70, + 0x07, 0x3C, 0x07, 0x8E, 0x03, 0x87, 0x83, 0x81, 0xE3, 0xC0, 0x71, 0xC0, 0x3D, 0xC0, 0x0F, 0xE0, + 0x3F, 0xFE, 0x1F, 0xFF, 0x0F, 0xFF, 0x80, 0xFE, 0x00, 0x77, 0x00, 0x73, 0xC0, 0x78, 0xF0, 0x38, + 0x38, 0x38, 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xC0, 0x70, 0x3B, 0xC3, 0x8E, 0x3C, 0x39, 0xC1, 0xDC, + 0x07, 0xE1, 0xFF, 0xCF, 0xFE, 0x0F, 0x80, 0xFE, 0x07, 0x78, 0x71, 0xC7, 0x87, 0x38, 0x3C, 0x70, + 0x3B, 0xC1, 0xCE, 0x0E, 0x38, 0x71, 0xC3, 0x87, 0x1C, 0x7D, 0xEF, 0xFE, 0x73, 0xE3, 0x8E, 0x1C, + 0x70, 0xE1, 0xC7, 0x07, 0x38, 0x39, 0xC0, 0xE0, 0xFF, 0xCF, 0xFC, 0x01, 0xC0, 0x1C, 0x01, 0xC0, + 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xCF, 0xFF, 0xFF, 0xF0, 0xFC, + 0xFC, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x06, 0x06, 0x07, 0x07, 0x0F, 0x7F, 0xF7, 0xE7, 0xFF, 0xFF, + 0xFF, 0xC0, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, + 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0xFF, 0xCF, 0xFE, 0x00, 0xF0, 0x07, 0x00, 0x70, 0x07, + 0x00, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x70, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0x18, 0x38, 0x30, 0x30, 0x30, 0x30, 0x30, 0x38, 0x38, 0x38, + 0x38, 0x18, 0x1C, 0xFF, 0xC7, 0xFF, 0x38, 0x3D, 0xC0, 0xEE, 0x03, 0x70, 0x1B, 0x80, 0xDC, 0x06, + 0xE0, 0x37, 0x01, 0xB8, 0x0D, 0xC0, 0x6E, 0x03, 0x70, 0x1B, 0x80, 0xC0, 0xE1, 0xF3, 0x87, 0xEE, + 0x07, 0xB8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1D, 0x80, 0x77, 0x01, 0xDC, + 0x0E, 0x7C, 0xF8, 0xFF, 0xC1, 0xFE, 0x00, 0x66, 0x66, 0x66, 0x66, 0xFF, 0x1F, 0xF8, 0x1F, 0x00, + 0xF0, 0x0E, 0x01, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, + 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0xFF, 0x1F, 0xF0, 0x0F, 0x00, 0xF0, 0x0E, 0x01, 0xC0, + 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0xF0, 0x3D, 0xFF, 0x3F, 0xC0, 0xE0, 0x0E, 0x00, 0xE0, + 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x70, 0x0E, 0x00, 0xE0, 0x1C, 0x01, 0xC0, 0x1C, 0x03, 0x80, + 0x38, 0x07, 0x00, 0x70, 0x07, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xC7, 0xFF, 0x38, 0x3D, 0xC0, 0xEE, + 0x03, 0x70, 0x1B, 0x80, 0xDC, 0x06, 0xE0, 0x37, 0x01, 0xB8, 0x0D, 0xC0, 0x6E, 0x03, 0x7F, 0xFB, + 0xFF, 0xC0, 0x73, 0xF0, 0xDF, 0xE3, 0xE7, 0x8E, 0x0E, 0x38, 0x1C, 0xE0, 0x73, 0x01, 0xDC, 0x07, + 0x70, 0x1D, 0xC0, 0x77, 0x01, 0xD8, 0x07, 0xE0, 0x1F, 0x8F, 0xFE, 0x3F, 0xC0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xFB, 0xF3, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1F, 0xFF, + 0xC0, 0xFF, 0xE3, 0xFF, 0xC3, 0x0F, 0x9C, 0x0E, 0x60, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, + 0x1F, 0x80, 0x76, 0x01, 0xDC, 0x0E, 0x7C, 0xF8, 0xFF, 0xC0, 0xFE, 0x00, 0x70, 0x19, 0xC0, 0x67, + 0x01, 0x8E, 0x06, 0x38, 0x18, 0x70, 0x61, 0xC1, 0x87, 0x06, 0x0E, 0x38, 0x38, 0xE0, 0xE3, 0x81, + 0xDC, 0x07, 0xF0, 0xFF, 0x87, 0xF8, 0x1F, 0x00, 0xFF, 0xC7, 0xFF, 0x38, 0x7D, 0xC0, 0xEE, 0x03, + 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xFE, 0x3B, 0xF1, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, + 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0xFF, 0x87, 0xFF, 0x38, 0x79, 0xC0, 0xEE, + 0x07, 0x70, 0x1B, 0x80, 0xFF, 0x87, 0xFC, 0x38, 0x01, 0x80, 0x1C, 0x00, 0xE0, 0x1E, 0x7F, 0xF3, + 0xFE, 0x00, 0xE0, 0x7E, 0x07, 0x70, 0x77, 0x07, 0x70, 0x73, 0x87, 0x38, 0x73, 0x87, 0x1D, 0xE1, + 0xFE, 0x1F, 0x80, 0xE0, 0x0E, 0x00, 0xE0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x38, 0x03, 0x80, 0x38, + 0x70, 0x77, 0x87, 0x38, 0x71, 0xC7, 0x1C, 0x70, 0xE7, 0x07, 0xF0, 0x7E, 0x03, 0x80, 0x1C, 0x01, + 0xC0, 0x0E, 0x00, 0x77, 0xFF, 0x7F, 0xF0, 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0x60, 0x07, 0x00, + 0x38, 0x03, 0x9C, 0x1C, 0xE0, 0xC7, 0x0E, 0x38, 0x71, 0xC7, 0x0E, 0x38, 0x71, 0xC3, 0x9C, 0x1C, + 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x00, 0xFF, 0x8F, 0xFC, 0x01, 0xE0, 0x0E, 0x00, 0x60, + 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0xE1, + 0xC3, 0xE1, 0xC3, 0xE1, 0xC3, 0xE1, 0xC3, 0xE1, 0xC3, 0xE1, 0xC3, 0xE1, 0xC3, 0x7F, 0xC7, 0x7F, + 0x87, 0x60, 0x07, 0x70, 0x07, 0x78, 0x0E, 0x3E, 0x3E, 0x1F, 0xFC, 0x0F, 0xF8, 0xFF, 0xF3, 0xFF, + 0xE0, 0xC7, 0x86, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, + 0xCE, 0x07, 0x38, 0x1F, 0xE0, 0x7F, 0x81, 0xC0, 0x67, 0xEE, 0xFF, 0x00, 0x19, 0xCE, 0x77, 0x39, + 0xC0, 0x00, 0x1D, 0xE7, 0x30, 0x00, 0x07, 0xF9, 0xFF, 0x78, 0x6E, 0x01, 0xC0, 0x38, 0x07, 0x80, + 0x78, 0x07, 0x80, 0x78, 0x07, 0x00, 0x60, 0x0C, 0x00, 0x00, 0x00, 0x0F, 0x01, 0xE0, 0x3C, 0x03, + 0x00, 0x1F, 0x0F, 0xC7, 0xA1, 0xC0, 0x70, 0x1C, 0x03, 0xF8, 0x7F, 0xFF, 0x3F, 0x04, 0x00, 0xFF, + 0xFF, 0xFC, 0x03, 0x3C, 0xE2, 0x00, 0x07, 0x39, 0xCE, 0x31, 0xCE, 0x73, 0x9C, 0xE7, 0x18, 0xC7, + 0x39, 0xC0, 0x00, 0x1C, 0x1B, 0x83, 0x70, 0xEF, 0xFC, 0xFF, 0x1F, 0x03, 0x80, 0x70, 0x0E, 0x01, + 0xC0, 0x38, 0x07, 0x00, 0x60, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0x00, 0x00, 0x00, 0x01, 0x86, + 0x67, 0x19, 0xDC, 0x67, 0x73, 0x9D, 0xFF, 0xF3, 0xFF, 0x8F, 0x98, 0x38, 0x00, 0xE0, 0x03, 0x80, + 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80, 0x06, 0x00, 0x18, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xC0, 0xF8, 0x3E, 0x0F, 0x01, 0xC0, 0x30, 0x07, 0x00, 0xFE, 0x0F, 0xC1, 0xF0, 0x38, 0x0E, + 0x01, 0xC0, 0x38, 0x07, 0x86, 0xFF, 0xEF, 0xF8, 0x00, 0x00, 0x1F, 0x07, 0xF1, 0xFE, 0x70, 0xEE, + 0x1D, 0xC1, 0xB8, 0x3F, 0x07, 0xE0, 0xDC, 0x3B, 0xCF, 0x3F, 0xC3, 0xF8, 0x00, 0x00, 0x00, 0x07, + 0xFC, 0x7F, 0xC7, 0xFC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, + 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x06, 0x00, 0x70, 0x07, 0x00, 0x00, 0x00, 0x03, 0x80, 0x7F, 0x01, + 0xDC, 0x0E, 0x70, 0x38, 0xE0, 0xE3, 0x87, 0x0E, 0x1C, 0x1C, 0x60, 0x73, 0x81, 0xCE, 0x03, 0xB0, + 0x0F, 0xC0, 0x3F, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x0C, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x38, 0x01, 0xE0, 0x07, 0xC0, 0x1F, 0x00, 0xFC, 0x03, 0xF8, 0x0E, 0xE0, 0x73, 0x81, 0xCF, + 0x06, 0x1C, 0x38, 0x70, 0xE0, 0xE7, 0x03, 0x9C, 0x0E, 0xF0, 0x1F, 0x80, 0x70, 0x00, 0x80, 0x3E, + 0x0F, 0xE3, 0xDC, 0x71, 0xCC, 0x39, 0xC7, 0x3C, 0xE3, 0xFC, 0x3F, 0x80, 0x30, 0x07, 0x00, 0xE0, + 0x1C, 0x03, 0x80, 0x70, 0x06, 0x00, 0xC0, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x20, 0x07, + 0x70, 0x07, 0x70, 0x07, 0x60, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0x70, 0x07, 0x78, 0x1E, 0x7F, 0xFC, + 0x3F, 0xF8, 0x0F, 0xF0, 0xFF, 0xF0, 0x03, 0x3C, 0xE2, 0x00, 0x07, 0x39, 0xCE, 0x31, 0xCE, 0x73, + 0x9C, 0xE7, 0x18, 0xC7, 0x39, 0xC0, 0x00, 0x1C, 0x1B, 0x83, 0x70, 0xEF, 0xFC, 0xFF, 0x1F, 0x03, + 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0x60, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x86, 0x67, 0x19, 0xDC, 0x67, 0x73, 0x9D, 0xFF, 0xF3, 0xFF, 0x8F, 0x98, 0x38, + 0x00, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80, 0x06, 0x00, 0x18, 0x00, 0x60, + 0x00, 0x00, 0x00, 0xE7, 0x9D, 0xF3, 0xB8, 0x77, 0x0F, 0xFC, 0xFF, 0xDF, 0xE3, 0x80, 0x70, 0x0E, + 0x01, 0xC0, 0x38, 0x03, 0x00, 0x60, 0x0C, 0x01, 0xC0, 0x38, 0x00, 0x06, 0x00, 0xF0, 0x07, 0x00, + 0xF8, 0x1F, 0x81, 0xDC, 0x39, 0xC3, 0x8E, 0x70, 0xE7, 0x06, 0x60, 0x76, 0x07, 0x60, 0x76, 0x67, + 0x76, 0x67, 0xFE, 0x3F, 0xC0, 0x1F, 0x8F, 0xE3, 0x91, 0xC0, 0x70, 0x1C, 0x03, 0x88, 0xFF, 0x1F, + 0x83, 0xC1, 0xE0, 0x70, 0x38, 0x0E, 0x07, 0x01, 0xC0, 0xE0, 0x00, 0x00, 0x00, 0x03, 0x80, 0x7F, + 0x01, 0xDC, 0x0E, 0x70, 0x38, 0xE0, 0xE3, 0x87, 0x0E, 0x1C, 0x1C, 0x60, 0x73, 0x81, 0xCE, 0x03, + 0xB0, 0x0F, 0xC0, 0x3F, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x0C, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x38, 0x01, 0xE0, 0x07, 0xC0, 0x1F, 0x00, 0xFC, 0x03, 0xF8, 0x0E, 0xE0, 0x73, 0x81, + 0xCF, 0x06, 0x1C, 0x38, 0x70, 0xE0, 0xE7, 0x03, 0x9C, 0x0E, 0xF0, 0x1F, 0x80, 0x70, 0x00, 0x80, + 0x3E, 0x0F, 0xE3, 0xDC, 0x71, 0xCC, 0x39, 0xC7, 0x3C, 0xE3, 0xFC, 0x3F, 0x80, 0x30, 0x07, 0x00, + 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x06, 0x00, 0xC0, 0x00, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x00, + 0x7C, 0x00, 0x77, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x1C, 0x70, 0x0E, 0x38, 0x07, 0x1C, 0x07, 0x07, + 0x03, 0x83, 0x81, 0xFF, 0xC1, 0xFF, 0xF0, 0xE0, 0x38, 0xF0, 0x1E, 0x70, 0x07, 0x38, 0x03, 0xBC, + 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x00, 0x80, 0x00, 0x1F, + 0x8F, 0xF9, 0xFF, 0xF0, 0x72, 0x0E, 0x01, 0xCF, 0xFB, 0xFF, 0xF8, 0xFC, 0x1F, 0x83, 0xF0, 0xFF, + 0xFE, 0xFF, 0xC7, 0x00, 0x00, 0x0E, 0x01, 0xE0, 0x38, 0x02, 0x00, 0x03, 0xC0, 0x01, 0xF0, 0x00, + 0x30, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x07, 0xC0, 0x03, 0xE0, 0x03, 0xB8, + 0x01, 0xDC, 0x00, 0xEE, 0x00, 0xE3, 0x80, 0x71, 0xC0, 0x38, 0xE0, 0x38, 0x38, 0x1C, 0x1C, 0x0F, + 0xFE, 0x0F, 0xFF, 0x87, 0x01, 0xC7, 0x80, 0xF3, 0x80, 0x39, 0xC0, 0x1D, 0xE0, 0x07, 0x1E, 0x03, + 0xE0, 0x1C, 0x03, 0x00, 0x00, 0x3F, 0x1F, 0xF3, 0xFF, 0xE0, 0xE4, 0x1C, 0x03, 0x9F, 0xF7, 0xFF, + 0xF1, 0xF8, 0x3F, 0x07, 0xE1, 0xFF, 0xFD, 0xFF, 0x8E, 0x00, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x0E, + 0x00, 0x07, 0x00, 0x1C, 0x00, 0x1F, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, + 0x70, 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, 0x1C, + 0x03, 0x8E, 0x03, 0x83, 0x81, 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, 0x38, + 0x03, 0x9C, 0x01, 0xDE, 0x00, 0x70, 0x00, 0x70, 0x0E, 0x00, 0xC0, 0x1C, 0x0E, 0x01, 0xF0, 0x1F, + 0x83, 0x9C, 0x00, 0x01, 0xF8, 0x7F, 0xC7, 0xFE, 0xE0, 0xE2, 0x0E, 0x00, 0xE3, 0xFE, 0x7F, 0xEF, + 0x8E, 0xE0, 0xEE, 0x0E, 0xE1, 0xEF, 0xFE, 0x7F, 0xE1, 0xC0, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x0C, + 0x00, 0x07, 0x00, 0x1C, 0x00, 0x1F, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, + 0x70, 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, 0x1C, + 0x03, 0x8E, 0x03, 0x83, 0x81, 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, 0x38, + 0x03, 0x9C, 0x01, 0xDE, 0x00, 0x70, 0x03, 0x80, 0x70, 0x07, 0x00, 0x60, 0xE0, 0x3E, 0x07, 0xE1, + 0xCE, 0x00, 0x03, 0xF1, 0xFF, 0x3F, 0xFE, 0x0E, 0x41, 0xC0, 0x39, 0xFF, 0x7F, 0xFF, 0x1F, 0x83, + 0xF0, 0x7E, 0x1F, 0xFF, 0xDF, 0xF8, 0xE0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x0E, 0x00, 0x06, 0x00, + 0x1C, 0x00, 0x1F, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x7C, + 0x00, 0x3E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x03, + 0x83, 0x81, 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, 0x38, 0x03, 0x9C, 0x01, + 0xDE, 0x00, 0x70, 0x03, 0xC0, 0x78, 0x03, 0x00, 0xE0, 0xE0, 0x3E, 0x07, 0xE1, 0xCE, 0x00, 0x03, + 0xF1, 0xFF, 0x3F, 0xFE, 0x0E, 0x41, 0xC0, 0x39, 0xFF, 0x7F, 0xFF, 0x1F, 0x83, 0xF0, 0x7E, 0x1F, + 0xFF, 0xDF, 0xF8, 0xE0, 0x03, 0x10, 0x03, 0xF8, 0x01, 0xFC, 0x00, 0x8C, 0x00, 0x1C, 0x00, 0x1F, + 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x7C, 0x00, 0x3E, 0x00, + 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x03, 0x83, 0x81, 0xC1, + 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, 0x38, 0x03, 0x9C, 0x01, 0xDE, 0x00, 0x70, + 0x18, 0x87, 0xF8, 0xFE, 0x18, 0x80, 0xE0, 0x3E, 0x07, 0xE1, 0xCE, 0x00, 0x03, 0xF1, 0xFF, 0x3F, + 0xFE, 0x0E, 0x41, 0xC0, 0x39, 0xFF, 0x7F, 0xFF, 0x1F, 0x83, 0xF0, 0x7E, 0x1F, 0xFF, 0xDF, 0xF8, + 0xE0, 0x01, 0xC0, 0x01, 0xF0, 0x01, 0xDC, 0x00, 0xC7, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x00, + 0x07, 0xC0, 0x03, 0xE0, 0x03, 0xB8, 0x01, 0xDC, 0x00, 0xEE, 0x00, 0xE3, 0x80, 0x71, 0xC0, 0x38, + 0xE0, 0x38, 0x38, 0x1C, 0x1C, 0x0F, 0xFE, 0x0F, 0xFF, 0x87, 0x01, 0xC7, 0x80, 0xF3, 0x80, 0x39, + 0xC0, 0x1D, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x04, + 0x00, 0x0F, 0x03, 0xE0, 0xEE, 0x18, 0xE0, 0x00, 0x3F, 0x1F, 0xF3, 0xFF, 0xE0, 0xE4, 0x1C, 0x03, + 0x9F, 0xF7, 0xFF, 0xF1, 0xF8, 0x3F, 0x07, 0xE1, 0xFF, 0xFD, 0xFF, 0x8E, 0x00, 0x00, 0x1C, 0x03, + 0xC0, 0x70, 0x04, 0x00, 0x00, 0x70, 0x00, 0x70, 0x00, 0x38, 0x00, 0x18, 0x00, 0x63, 0x00, 0x31, + 0x80, 0x1F, 0xC0, 0x07, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x7C, 0x00, 0x3E, 0x00, + 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x03, 0x83, 0x81, 0xC1, + 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, 0x38, 0x03, 0x9C, 0x01, 0xDE, 0x00, 0x70, + 0x03, 0x80, 0x70, 0x1C, 0x03, 0x03, 0x1C, 0x73, 0x8F, 0xE0, 0xF8, 0x00, 0x03, 0xF1, 0xFF, 0x3F, + 0xFE, 0x0E, 0x41, 0xC0, 0x39, 0xFF, 0x7F, 0xFF, 0x1F, 0x83, 0xF0, 0x7E, 0x1F, 0xFF, 0xDF, 0xF8, + 0xE0, 0x07, 0x00, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x63, 0x00, 0x31, 0x80, 0x1F, 0xC0, + 0x07, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x3B, 0x80, 0x1D, + 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x03, 0x83, 0x81, 0xC1, 0xC0, 0xFF, 0xE0, + 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, 0x38, 0x03, 0x9C, 0x01, 0xDE, 0x00, 0x70, 0x38, 0x03, 0x80, + 0x30, 0x07, 0x03, 0x18, 0x63, 0x0F, 0xE0, 0xF8, 0x00, 0x03, 0xF1, 0xFF, 0x3F, 0xFE, 0x0E, 0x41, + 0xC0, 0x39, 0xFF, 0x7F, 0xFF, 0x1F, 0x83, 0xF0, 0x7E, 0x1F, 0xFF, 0xDF, 0xF8, 0xE0, 0x03, 0xC0, + 0x01, 0xF0, 0x00, 0x38, 0x00, 0x18, 0x00, 0x63, 0x00, 0x31, 0x80, 0x1F, 0xC0, 0x07, 0xC0, 0x00, + 0x00, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, + 0x0E, 0x38, 0x07, 0x1C, 0x03, 0x8E, 0x03, 0x83, 0x81, 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, + 0x1C, 0x78, 0x0F, 0x38, 0x03, 0x9C, 0x01, 0xDE, 0x00, 0x70, 0x1E, 0x03, 0xE0, 0x1C, 0x03, 0x03, + 0x1C, 0x73, 0x8F, 0xE0, 0xF8, 0x00, 0x03, 0xF1, 0xFF, 0x3F, 0xFE, 0x0E, 0x41, 0xC0, 0x39, 0xFF, + 0x7F, 0xFF, 0x1F, 0x83, 0xF0, 0x7E, 0x1F, 0xFF, 0xDF, 0xF8, 0xE0, 0x03, 0x10, 0x03, 0xF8, 0x01, + 0xFC, 0x00, 0xCC, 0x00, 0x63, 0x00, 0x31, 0x80, 0x1F, 0xC0, 0x07, 0xC0, 0x00, 0x00, 0x00, 0xE0, + 0x00, 0x70, 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x0E, 0xE0, 0x0E, 0x38, 0x07, + 0x1C, 0x03, 0x8E, 0x03, 0x83, 0x81, 0xC1, 0xC0, 0xFF, 0xE0, 0xFF, 0xF8, 0x70, 0x1C, 0x78, 0x0F, + 0x38, 0x03, 0x9C, 0x01, 0xDE, 0x00, 0x70, 0x18, 0x87, 0xF8, 0xFE, 0x18, 0x83, 0x18, 0x73, 0x0F, + 0xE0, 0xF8, 0x00, 0x03, 0xF1, 0xFF, 0x3F, 0xFE, 0x0E, 0x41, 0xC0, 0x39, 0xFF, 0x7F, 0xFF, 0x1F, + 0x83, 0xF0, 0x7E, 0x1F, 0xFF, 0xDF, 0xF8, 0xE0, 0x06, 0x30, 0x03, 0xB8, 0x01, 0xFC, 0x00, 0x38, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x07, 0xC0, 0x03, 0xE0, 0x03, 0xB8, 0x01, 0xDC, 0x00, + 0xEE, 0x00, 0xE3, 0x80, 0x71, 0xC0, 0x38, 0xE0, 0x38, 0x38, 0x1C, 0x1C, 0x0F, 0xFE, 0x0F, 0xFF, + 0x87, 0x01, 0xC7, 0x80, 0xF3, 0x80, 0x39, 0xC0, 0x1D, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x04, 0x00, 0x31, 0xC7, 0x30, 0x7E, 0x07, 0x80, 0x00, 0x3F, + 0x1F, 0xF3, 0xFF, 0xE0, 0xE4, 0x1C, 0x03, 0x9F, 0xF7, 0xFF, 0xF1, 0xF8, 0x3F, 0x07, 0xE1, 0xFF, + 0xFD, 0xFF, 0x8E, 0x00, 0x00, 0x1C, 0x03, 0xC0, 0x70, 0x04, 0x00, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, + 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xEF, 0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, + 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x60, 0x0F, 0x00, 0xF0, 0x04, + 0x00, 0x0F, 0x81, 0xFE, 0x1F, 0xF8, 0xE0, 0xEE, 0x07, 0x70, 0x1B, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, + 0x00, 0x38, 0x1C, 0xF1, 0xE3, 0xFE, 0x0F, 0xE0, 0x1C, 0x00, 0x00, 0x07, 0x00, 0x78, 0x01, 0xC0, + 0x04, 0x00, 0x1E, 0x01, 0xF0, 0x07, 0x00, 0x60, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, - 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x0C, 0xC0, 0xFE, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x07, 0xC0, + 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x78, 0x00, 0xC0, 0x0E, 0x00, 0x00, 0x07, 0xC0, 0xFF, 0x0F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0D, 0xFF, 0xFF, 0xFF, 0xF0, 0x03, 0x80, 0x1C, 0x0E, - 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x00, 0xC0, 0x1C, 0x0E, 0x01, 0xF0, - 0x1F, 0x83, 0x9C, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, - 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, - 0xFF, 0x00, 0x70, 0x03, 0x80, 0x38, 0x01, 0x80, 0x70, 0x07, 0xC0, 0x7F, 0x07, 0x38, 0x00, 0x00, - 0x7C, 0x0F, 0xF0, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, 0xDF, 0xFF, 0xFF, 0xFF, 0x00, 0x38, 0x01, - 0xC0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x0C, 0x0E, 0x01, - 0xF0, 0x1F, 0x83, 0x9C, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, - 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, - 0xFF, 0xFF, 0x03, 0x80, 0x0E, 0x00, 0x70, 0x01, 0x80, 0x70, 0x07, 0xC0, 0x7F, 0x07, 0x38, 0x00, - 0x00, 0x7C, 0x0F, 0xF0, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, 0xDF, 0xFF, 0xFF, 0xFF, 0x00, 0x38, - 0x01, 0xC0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x03, 0xC0, 0x3C, 0x00, 0xC0, 0x1C, 0x0E, - 0x01, 0xF0, 0x1F, 0x83, 0x9C, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, - 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, - 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0x1F, 0x00, 0x38, 0x01, 0x80, 0x70, 0x07, 0xC0, 0x7F, 0x07, 0x38, - 0x00, 0x00, 0x7C, 0x0F, 0xF0, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, 0xDF, 0xFF, 0xFF, 0xFF, 0x00, - 0x38, 0x01, 0xC0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x18, 0x83, 0xFC, 0x3F, 0x83, 0x10, - 0x0E, 0x01, 0xF0, 0x1F, 0x83, 0x9C, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, + 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x18, 0xC3, 0xFC, 0x3F, 0x80, 0x00, 0x00, 0x0F, 0xFE, + 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, + 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x0C, 0xC0, 0xFE, 0x0F, 0xF0, + 0x00, 0x00, 0x00, 0x07, 0xC0, 0xFF, 0x0F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0D, 0xFF, 0xFF, 0xFF, + 0xF0, 0x03, 0x80, 0x1C, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x00, + 0xC0, 0x1C, 0x0E, 0x01, 0xF0, 0x1F, 0x83, 0x9C, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, + 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, + 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x70, 0x03, 0x80, 0x38, 0x01, 0x80, 0x70, 0x07, 0xC0, + 0x7F, 0x07, 0x38, 0x00, 0x00, 0x7C, 0x0F, 0xF0, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, 0xDF, 0xFF, + 0xFF, 0xFF, 0x00, 0x38, 0x01, 0xC0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x03, 0x80, 0x38, + 0x01, 0xC0, 0x0C, 0x0E, 0x01, 0xF0, 0x1F, 0x83, 0x9C, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, + 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, + 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x03, 0x80, 0x0E, 0x00, 0x70, 0x01, 0x80, 0x70, 0x07, + 0xC0, 0x7F, 0x07, 0x38, 0x00, 0x00, 0x7C, 0x0F, 0xF0, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, 0xDF, + 0xFF, 0xFF, 0xFF, 0x00, 0x38, 0x01, 0xC0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x03, 0xC0, + 0x3C, 0x00, 0xC0, 0x1C, 0x0E, 0x01, 0xF0, 0x1F, 0x83, 0x9C, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, + 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, + 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0x1F, 0x00, 0x38, 0x01, 0x80, 0x70, + 0x07, 0xC0, 0x7F, 0x07, 0x38, 0x00, 0x00, 0x7C, 0x0F, 0xF0, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, + 0xDF, 0xFF, 0xFF, 0xFF, 0x00, 0x38, 0x01, 0xC0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x18, + 0x83, 0xFC, 0x3F, 0x83, 0x10, 0x0E, 0x01, 0xF0, 0x1F, 0x83, 0x9C, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, + 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, + 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x0C, 0xC0, 0xFE, 0x0F, 0xF0, 0x22, 0x00, + 0x70, 0x07, 0xC0, 0x7F, 0x07, 0x38, 0x00, 0x00, 0x7C, 0x0F, 0xF0, 0xFF, 0xC7, 0x07, 0x70, 0x3B, + 0x80, 0xDF, 0xFF, 0xFF, 0xFF, 0x00, 0x38, 0x01, 0xC0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, + 0x0F, 0x01, 0xF0, 0x3B, 0x83, 0x9C, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, - 0x00, 0xFF, 0xFF, 0xFF, 0x0C, 0xC0, 0xFE, 0x0F, 0xF0, 0x22, 0x00, 0x70, 0x07, 0xC0, 0x7F, 0x07, - 0x38, 0x00, 0x00, 0x7C, 0x0F, 0xF0, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, 0xDF, 0xFF, 0xFF, 0xFF, - 0x00, 0x38, 0x01, 0xC0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x0F, 0x01, 0xF0, 0x3B, 0x83, - 0x9C, 0x00, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFE, - 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x00, - 0x00, 0x00, 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x40, 0x0F, 0x00, 0x7C, 0x07, 0x70, 0x73, 0x80, 0x00, - 0x07, 0xC0, 0xFF, 0x0F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0D, 0xFF, 0xFF, 0xFF, 0xF0, 0x03, 0x80, - 0x1C, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x00, 0x03, 0x80, 0x3C, 0x00, 0xE0, 0x02, - 0x00, 0xF7, 0x8C, 0xE0, 0x31, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, - 0x7B, 0xCE, 0x60, 0x19, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x9C, 0x67, 0x77, 0x77, 0x77, - 0x77, 0x77, 0x77, 0x77, 0x77, 0x70, 0x07, 0xF7, 0x20, 0x6F, 0xF4, 0x06, 0xEE, 0xEE, 0xEE, 0xEE, - 0xEE, 0xEE, 0xE0, 0x06, 0xFF, 0x40, 0x03, 0xE0, 0x07, 0xFC, 0x07, 0xFF, 0x07, 0x83, 0xC7, 0x80, - 0xF3, 0x80, 0x3B, 0xC0, 0x1F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, - 0x00, 0x3B, 0x80, 0x39, 0xC0, 0x1C, 0xF0, 0x1E, 0x3E, 0x3E, 0x0F, 0xFE, 0x03, 0xFE, 0x00, 0x38, - 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x00, 0x80, 0x00, 0x0F, 0x81, 0xFF, 0x1F, - 0xF8, 0xE0, 0xEE, 0x07, 0x70, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xFC, 0x1C, 0xF1, 0xE3, - 0xFE, 0x0F, 0xE0, 0x1C, 0x00, 0x00, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x04, 0x00, 0x03, 0xC0, 0x01, - 0xF0, 0x00, 0x30, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x3F, 0xE0, 0x3F, 0xF8, 0x3C, 0x1E, - 0x3C, 0x07, 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, - 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, - 0x01, 0xC0, 0x00, 0x0F, 0x00, 0x78, 0x00, 0xC0, 0x0E, 0x00, 0x00, 0x07, 0xC0, 0xFF, 0x8F, 0xFC, - 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFE, 0x0E, 0x78, 0xF1, 0xFF, - 0x07, 0xF0, 0x0E, 0x00, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x1C, 0x00, 0x1F, - 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x01, 0xF0, 0x03, 0xFE, 0x03, 0xFF, 0x83, 0xC1, 0xE3, - 0xC0, 0x79, 0xC0, 0x1D, 0xE0, 0x0F, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, - 0x3F, 0x00, 0x1D, 0xC0, 0x1C, 0xE0, 0x0E, 0x78, 0x0F, 0x1F, 0x1F, 0x07, 0xFF, 0x01, 0xFF, 0x00, - 0x1C, 0x00, 0x00, 0x70, 0x03, 0x80, 0x38, 0x01, 0x80, 0x70, 0x07, 0xC0, 0x77, 0x07, 0x38, 0x00, - 0x00, 0x7C, 0x0F, 0xF8, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, - 0x0F, 0xE0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x0C, 0x00, + 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x40, 0x0F, 0x00, 0x7C, + 0x07, 0x70, 0x73, 0x80, 0x00, 0x07, 0xC0, 0xFF, 0x0F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0D, 0xFF, + 0xFF, 0xFF, 0xF0, 0x03, 0x80, 0x1C, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x00, 0x03, + 0x80, 0x3C, 0x00, 0xE0, 0x02, 0x00, 0xF7, 0x8C, 0xE0, 0x31, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, + 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x7B, 0xCE, 0x60, 0x19, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, + 0x9C, 0x67, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x70, 0x07, 0xF7, 0x20, 0x6F, 0xF4, + 0x06, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xE0, 0x06, 0xFF, 0x40, 0x03, 0xE0, 0x07, 0xFC, 0x07, + 0xFF, 0x07, 0x83, 0xC7, 0x80, 0xF3, 0x80, 0x3B, 0xC0, 0x1F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, + 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3B, 0x80, 0x39, 0xC0, 0x1C, 0xF0, 0x1E, 0x3E, 0x3E, 0x0F, + 0xFE, 0x03, 0xFE, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x00, 0x80, + 0x00, 0x0F, 0x81, 0xFF, 0x1F, 0xF8, 0xE0, 0xEE, 0x07, 0x70, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, + 0x01, 0xFC, 0x1C, 0xF1, 0xE3, 0xFE, 0x0F, 0xE0, 0x1C, 0x00, 0x00, 0x07, 0x00, 0x38, 0x01, 0xC0, + 0x04, 0x00, 0x03, 0xC0, 0x01, 0xF0, 0x00, 0x30, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x3F, + 0xE0, 0x3F, 0xF8, 0x3C, 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, + 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, + 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, 0x01, 0xC0, 0x00, 0x0F, 0x00, 0x78, 0x00, 0xC0, 0x0E, 0x00, 0x00, + 0x07, 0xC0, 0xFF, 0x8F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, + 0xFE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x1C, 0x00, 0x1F, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x01, 0xF0, 0x03, 0xFE, 0x03, 0xFF, 0x83, 0xC1, 0xE3, 0xC0, 0x79, 0xC0, 0x1D, 0xE0, 0x0F, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3F, 0x00, 0x1D, 0xC0, 0x1C, 0xE0, 0x0E, 0x78, 0x0F, 0x1F, 0x1F, - 0x07, 0xFF, 0x01, 0xFF, 0x00, 0x1C, 0x00, 0x03, 0x80, 0x0E, 0x00, 0x70, 0x01, 0xC0, 0x70, 0x07, + 0x07, 0xFF, 0x01, 0xFF, 0x00, 0x1C, 0x00, 0x00, 0x70, 0x03, 0x80, 0x38, 0x01, 0x80, 0x70, 0x07, 0xC0, 0x77, 0x07, 0x38, 0x00, 0x00, 0x7C, 0x0F, 0xF8, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, 0xFC, - 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xE0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x00, 0x78, - 0x00, 0x3C, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x1C, 0x00, 0x1F, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, + 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xE0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x00, 0xE0, + 0x00, 0x38, 0x00, 0x0C, 0x00, 0x07, 0x00, 0x1C, 0x00, 0x1F, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x01, 0xF0, 0x03, 0xFE, 0x03, 0xFF, 0x83, 0xC1, 0xE3, 0xC0, 0x79, 0xC0, 0x1D, 0xE0, 0x0F, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3F, 0x00, 0x1D, 0xC0, 0x1C, 0xE0, - 0x0E, 0x78, 0x0F, 0x1F, 0x1F, 0x07, 0xFF, 0x01, 0xFF, 0x00, 0x1C, 0x00, 0x03, 0xC0, 0x1F, 0x00, - 0x38, 0x01, 0x80, 0x70, 0x07, 0xC0, 0x77, 0x07, 0x38, 0x00, 0x00, 0x7C, 0x0F, 0xF8, 0xFF, 0xC7, + 0x0E, 0x78, 0x0F, 0x1F, 0x1F, 0x07, 0xFF, 0x01, 0xFF, 0x00, 0x1C, 0x00, 0x03, 0x80, 0x0E, 0x00, + 0x70, 0x01, 0xC0, 0x70, 0x07, 0xC0, 0x77, 0x07, 0x38, 0x00, 0x00, 0x7C, 0x0F, 0xF8, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xE0, 0xE7, 0x8F, 0x1F, 0xF0, - 0x7F, 0x00, 0xE0, 0x03, 0x10, 0x03, 0xF8, 0x01, 0xFC, 0x00, 0x8C, 0x00, 0x1C, 0x00, 0x1F, 0x00, + 0x7F, 0x00, 0xE0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x1C, 0x00, 0x1F, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x01, 0xF0, 0x03, 0xFE, 0x03, 0xFF, 0x83, 0xC1, 0xE3, 0xC0, 0x79, 0xC0, 0x1D, 0xE0, 0x0F, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3F, 0x00, 0x1D, 0xC0, 0x1C, 0xE0, 0x0E, 0x78, 0x0F, 0x1F, 0x1F, 0x07, 0xFF, 0x01, 0xFF, 0x00, 0x1C, - 0x00, 0x0C, 0xC0, 0xFE, 0x07, 0xF0, 0x23, 0x00, 0x70, 0x07, 0xC0, 0x77, 0x07, 0x38, 0x00, 0x00, + 0x00, 0x03, 0xC0, 0x1F, 0x00, 0x38, 0x01, 0x80, 0x70, 0x07, 0xC0, 0x77, 0x07, 0x38, 0x00, 0x00, 0x7C, 0x0F, 0xF8, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, - 0xE0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x01, 0xC0, 0x01, 0xF0, 0x01, 0xDC, 0x00, 0xC7, - 0x00, 0x00, 0x00, 0x1F, 0x00, 0x3F, 0xE0, 0x3F, 0xF8, 0x3C, 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xDE, - 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, - 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, 0x01, 0xC0, 0x00, 0x00, 0x00, - 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x04, 0x00, 0x07, 0x00, 0x7C, 0x07, 0x70, 0x71, 0x80, 0x00, - 0x07, 0xC0, 0xFF, 0x8F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, - 0xFE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x00, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x02, - 0x00, 0x00, 0x70, 0x00, 0x70, 0x00, 0x38, 0x00, 0x38, 0x70, 0x00, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, - 0x3F, 0xFE, 0x3C, 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0x80, - 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, 0xF0, - 0x7F, 0xF0, 0x1F, 0xF0, 0x01, 0xC0, 0x00, 0x01, 0xC0, 0x1C, 0x00, 0xC6, 0x0E, 0x70, 0x03, 0x87, - 0xFC, 0xFF, 0xEF, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFE, - 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x38, - 0x70, 0x00, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x3F, 0xFE, 0x3C, 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xDE, - 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, - 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, 0x01, 0xC0, 0x00, 0x1C, 0x00, - 0x70, 0x03, 0x86, 0x0C, 0x70, 0x03, 0x87, 0xFC, 0xFF, 0xEF, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, - 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x03, - 0xC0, 0x01, 0xF0, 0x00, 0x30, 0x00, 0x38, 0x70, 0x00, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x3F, 0xFE, - 0x3C, 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, - 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, - 0x1F, 0xF0, 0x01, 0xC0, 0x00, 0x0F, 0x00, 0x78, 0x00, 0xC6, 0x0E, 0x70, 0x03, 0x87, 0xFC, 0xFF, - 0xEF, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFE, 0x0E, 0x78, - 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x03, 0x30, 0x03, 0xF8, 0x01, 0xFC, 0x00, 0x00, 0x70, 0x00, + 0xE0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x03, 0x10, 0x03, 0xF8, 0x01, 0xFC, 0x00, 0x8C, + 0x00, 0x1C, 0x00, 0x1F, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x00, 0x00, 0x01, 0xF0, 0x03, 0xFE, 0x03, + 0xFF, 0x83, 0xC1, 0xE3, 0xC0, 0x79, 0xC0, 0x1D, 0xE0, 0x0F, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, + 0xFC, 0x00, 0x7E, 0x00, 0x3F, 0x00, 0x1D, 0xC0, 0x1C, 0xE0, 0x0E, 0x78, 0x0F, 0x1F, 0x1F, 0x07, + 0xFF, 0x01, 0xFF, 0x00, 0x1C, 0x00, 0x0C, 0xC0, 0xFE, 0x07, 0xF0, 0x23, 0x00, 0x70, 0x07, 0xC0, + 0x77, 0x07, 0x38, 0x00, 0x00, 0x7C, 0x0F, 0xF8, 0xFF, 0xC7, 0x07, 0x70, 0x3B, 0x80, 0xFC, 0x07, + 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xE0, 0xE7, 0x8F, 0x1F, 0xF0, 0x7F, 0x00, 0xE0, 0x01, 0xC0, 0x01, + 0xF0, 0x01, 0xDC, 0x00, 0xC7, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x3F, 0xE0, 0x3F, 0xF8, 0x3C, 0x1E, + 0x3C, 0x07, 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, + 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, + 0x01, 0xC0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x04, 0x00, 0x07, 0x00, 0x7C, + 0x07, 0x70, 0x71, 0x80, 0x00, 0x07, 0xC0, 0xFF, 0x8F, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, + 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x00, 0x03, + 0x80, 0x1C, 0x00, 0xE0, 0x02, 0x00, 0x00, 0x70, 0x00, 0x70, 0x00, 0x38, 0x00, 0x38, 0x70, 0x00, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x3F, 0xFE, 0x3C, 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, - 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, 0x01, 0xC0, 0x00, 0x0C, 0xC0, 0xFE, 0x07, - 0xF6, 0x00, 0x70, 0x03, 0x87, 0xFC, 0xFF, 0xEF, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, 0x7E, - 0x03, 0xF0, 0x1F, 0x80, 0xFE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x00, 0x03, 0x80, - 0x01, 0xC0, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0xF1, 0xE0, 0xF1, 0xE0, 0x3C, 0xE0, 0x0E, 0xF0, 0x07, - 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0E, 0xE0, 0x0E, 0x70, - 0x07, 0x3C, 0x07, 0x8F, 0x8F, 0x83, 0xFF, 0x80, 0xFF, 0x80, 0x0E, 0x00, 0x00, 0x00, 0x03, 0x80, - 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x00, 0x18, 0x01, 0xC0, 0x0E, 0x1F, 0xF3, 0xFF, 0xBF, - 0xF1, 0xC1, 0xDC, 0x0E, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF8, 0x39, 0xE3, 0xC7, - 0xFC, 0x1F, 0xC0, 0x38, 0x00, 0x00, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x08, 0x00, 0xC0, 0x0D, 0xC0, - 0x1F, 0x80, 0x3F, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, - 0x80, 0x3F, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x07, 0x78, 0x0E, 0x78, 0x7C, 0x7F, 0xF0, 0x7F, - 0xC0, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x01, 0xE0, 0x01, 0xC0, 0x01, 0x00, 0x60, 0x7E, 0x07, 0xE0, - 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x70, 0x77, 0x8F, 0x7F, 0xF3, - 0xFF, 0x04, 0x00, 0x00, 0x07, 0x00, 0x70, 0x07, 0x00, 0x20, 0x07, 0x80, 0x0F, 0x00, 0x06, 0x00, - 0x1C, 0x00, 0x00, 0x18, 0x01, 0xB8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0x80, 0x3F, 0x00, - 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0x80, 0x3F, 0x00, 0xEF, - 0x01, 0xCF, 0x0F, 0x8F, 0xFE, 0x0F, 0xF8, 0x07, 0x80, 0x0F, 0x00, 0xF0, 0x03, 0x00, 0x70, 0x00, - 0x06, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x77, - 0x07, 0x78, 0xF7, 0xFF, 0x3F, 0xF0, 0x40, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x70, 0x70, + 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, 0x01, 0xC0, 0x00, 0x01, 0xC0, 0x1C, 0x00, + 0xC6, 0x0E, 0x70, 0x03, 0x87, 0xFC, 0xFF, 0xEF, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, 0x7E, + 0x03, 0xF0, 0x1F, 0x80, 0xFE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x07, 0x00, 0x01, + 0xC0, 0x00, 0xE0, 0x00, 0x38, 0x70, 0x00, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x3F, 0xFE, 0x3C, 0x1E, + 0x3C, 0x07, 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, + 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, + 0x01, 0xC0, 0x00, 0x1C, 0x00, 0x70, 0x03, 0x86, 0x0C, 0x70, 0x03, 0x87, 0xFC, 0xFF, 0xEF, 0xFC, + 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFE, 0x0E, 0x78, 0xF1, 0xFF, + 0x07, 0xF0, 0x0E, 0x00, 0x03, 0xC0, 0x01, 0xF0, 0x00, 0x30, 0x00, 0x38, 0x70, 0x00, 0x38, 0x1F, + 0xFC, 0x3F, 0xFE, 0x3F, 0xFE, 0x3C, 0x1E, 0x3C, 0x07, 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, + 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, + 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, 0x01, 0xC0, 0x00, 0x0F, 0x00, 0x78, 0x00, 0xC6, 0x0E, + 0x70, 0x03, 0x87, 0xFC, 0xFF, 0xEF, 0xFC, 0x70, 0x77, 0x03, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, + 0x1F, 0x80, 0xFE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x0E, 0x00, 0x03, 0x30, 0x03, 0xF8, 0x01, + 0xFC, 0x00, 0x00, 0x70, 0x00, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x3F, 0xFE, 0x3C, 0x1E, 0x3C, 0x07, + 0x9C, 0x01, 0xDE, 0x00, 0xFE, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, + 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xF1, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, 0x01, 0xC0, + 0x00, 0x0C, 0xC0, 0xFE, 0x07, 0xF6, 0x00, 0x70, 0x03, 0x87, 0xFC, 0xFF, 0xEF, 0xFC, 0x70, 0x77, + 0x03, 0xB8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, + 0x0E, 0x00, 0x00, 0x03, 0x80, 0x01, 0xC0, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0xF1, 0xE0, 0xF1, 0xE0, + 0x3C, 0xE0, 0x0E, 0xF0, 0x07, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x3F, 0x00, 0x1F, + 0x80, 0x0E, 0xE0, 0x0E, 0x70, 0x07, 0x3C, 0x07, 0x8F, 0x8F, 0x83, 0xFF, 0x80, 0xFF, 0x80, 0x0E, + 0x00, 0x00, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x00, 0x18, 0x01, 0xC0, + 0x0E, 0x1F, 0xF3, 0xFF, 0xBF, 0xF1, 0xC1, 0xDC, 0x0E, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, + 0x03, 0xF8, 0x39, 0xE3, 0xC7, 0xFC, 0x1F, 0xC0, 0x38, 0x00, 0x00, 0x0E, 0x00, 0x70, 0x03, 0x80, + 0x08, 0x00, 0xC0, 0x0D, 0xC0, 0x1F, 0x80, 0x3F, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, + 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0x80, 0x3F, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x07, 0x78, 0x0E, + 0x78, 0x7C, 0x7F, 0xF0, 0x7F, 0xC0, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x01, 0xE0, 0x01, 0xC0, 0x01, + 0x00, 0x60, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, + 0x70, 0x77, 0x8F, 0x7F, 0xF3, 0xFF, 0x04, 0x00, 0x00, 0x07, 0x00, 0x70, 0x07, 0x00, 0x20, 0x07, + 0x80, 0x0F, 0x00, 0x06, 0x00, 0x1C, 0x00, 0x00, 0x18, 0x01, 0xB8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, + 0xC0, 0x1F, 0x80, 0x3F, 0x00, 0x7E, 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, + 0x1F, 0x80, 0x3F, 0x00, 0xEF, 0x01, 0xCF, 0x0F, 0x8F, 0xFE, 0x0F, 0xF8, 0x07, 0x80, 0x0F, 0x00, + 0xF0, 0x03, 0x00, 0x70, 0x00, 0x06, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, + 0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x07, 0x78, 0xF7, 0xFF, 0x3F, 0xF0, 0x40, 0x01, 0xC0, 0x00, 0xE0, + 0x00, 0xE0, 0x00, 0x70, 0x70, 0x00, 0x3E, 0x00, 0x7F, 0x80, 0x3F, 0xC0, 0x1E, 0xE0, 0x0E, 0x70, + 0x07, 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, + 0x70, 0x07, 0x38, 0x03, 0x9C, 0x03, 0x8F, 0x01, 0xC3, 0xC3, 0xE0, 0xFF, 0xE0, 0x3F, 0xE0, 0x07, + 0x80, 0x00, 0x03, 0xC0, 0x0E, 0x00, 0x30, 0xC1, 0xC7, 0x00, 0x1D, 0x81, 0xFE, 0x07, 0xF8, 0x1C, + 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x1C, 0x1C, 0x78, 0xF1, + 0xFF, 0xC3, 0xFF, 0x01, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x60, 0x70, 0x00, 0x3E, + 0x00, 0x7F, 0x80, 0x3F, 0xC0, 0x1E, 0xE0, 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, + 0xE7, 0x00, 0x73, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x03, 0x8F, + 0x01, 0xC3, 0xC3, 0xE0, 0xFF, 0xE0, 0x3F, 0xE0, 0x07, 0x80, 0x00, 0x1C, 0x00, 0x78, 0x00, 0xE0, + 0xC1, 0x87, 0x00, 0x1D, 0x81, 0xFE, 0x07, 0xF8, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, + 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x1C, 0x1C, 0x78, 0xF1, 0xFF, 0xC3, 0xFF, 0x01, 0x00, 0x07, 0x80, + 0x03, 0xC0, 0x00, 0x60, 0x00, 0x70, 0x70, 0x00, 0x3E, 0x00, 0x7F, 0x80, 0x3F, 0xC0, 0x1E, 0xE0, + 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, 0xC0, 0x1C, + 0xE0, 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x03, 0x8F, 0x01, 0xC3, 0xC3, 0xE0, 0xFF, 0xE0, 0x3F, + 0xE0, 0x07, 0x80, 0x00, 0x0F, 0x00, 0x3C, 0x00, 0x30, 0xC1, 0xC7, 0x00, 0x1D, 0x81, 0xFE, 0x07, + 0xF8, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x1C, 0x1C, + 0x78, 0xF1, 0xFF, 0xC3, 0xFF, 0x01, 0x00, 0x0E, 0x60, 0x07, 0xF0, 0x07, 0xF8, 0x00, 0x00, 0x70, 0x00, 0x3E, 0x00, 0x7F, 0x80, 0x3F, 0xC0, 0x1E, 0xE0, 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, - 0x03, 0x8F, 0x01, 0xC3, 0xC3, 0xE0, 0xFF, 0xE0, 0x3F, 0xE0, 0x07, 0x80, 0x00, 0x03, 0xC0, 0x0E, - 0x00, 0x30, 0xC1, 0xC7, 0x00, 0x1D, 0x81, 0xFE, 0x07, 0xF8, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, + 0x03, 0x8F, 0x01, 0xC3, 0xC3, 0xE0, 0xFF, 0xE0, 0x3F, 0xE0, 0x07, 0x80, 0x00, 0x0C, 0xC0, 0x7F, + 0x03, 0xFC, 0xC0, 0x07, 0x00, 0x1D, 0x81, 0xFE, 0x07, 0xF8, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x1C, 0x1C, 0x78, 0xF1, 0xFF, 0xC3, 0xFF, 0x01, 0x00, - 0x0E, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x60, 0x70, 0x00, 0x3E, 0x00, 0x7F, 0x80, 0x3F, 0xC0, - 0x1E, 0xE0, 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, - 0xC0, 0x1C, 0xE0, 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x03, 0x8F, 0x01, 0xC3, 0xC3, 0xE0, 0xFF, - 0xE0, 0x3F, 0xE0, 0x07, 0x80, 0x00, 0x1C, 0x00, 0x78, 0x00, 0xE0, 0xC1, 0x87, 0x00, 0x1D, 0x81, - 0xFE, 0x07, 0xF8, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, - 0x1C, 0x1C, 0x78, 0xF1, 0xFF, 0xC3, 0xFF, 0x01, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x00, 0x60, 0x00, - 0x70, 0x70, 0x00, 0x3E, 0x00, 0x7F, 0x80, 0x3F, 0xC0, 0x1E, 0xE0, 0x0E, 0x70, 0x07, 0x38, 0x03, - 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x70, 0x07, 0x38, - 0x03, 0x9C, 0x03, 0x8F, 0x01, 0xC3, 0xC3, 0xE0, 0xFF, 0xE0, 0x3F, 0xE0, 0x07, 0x80, 0x00, 0x0F, - 0x00, 0x3C, 0x00, 0x30, 0xC1, 0xC7, 0x00, 0x1D, 0x81, 0xFE, 0x07, 0xF8, 0x1C, 0xE0, 0x73, 0x81, - 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x1C, 0x1C, 0x78, 0xF1, 0xFF, 0xC3, 0xFF, - 0x01, 0x00, 0x0E, 0x60, 0x07, 0xF0, 0x07, 0xF8, 0x00, 0x00, 0x70, 0x00, 0x3E, 0x00, 0x7F, 0x80, - 0x3F, 0xC0, 0x1E, 0xE0, 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, - 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x03, 0x8F, 0x01, 0xC3, 0xC3, - 0xE0, 0xFF, 0xE0, 0x3F, 0xE0, 0x07, 0x80, 0x00, 0x0C, 0xC0, 0x7F, 0x03, 0xFC, 0xC0, 0x07, 0x00, - 0x1D, 0x81, 0xFE, 0x07, 0xF8, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, - 0xCE, 0x07, 0x1C, 0x1C, 0x78, 0xF1, 0xFF, 0xC3, 0xFF, 0x01, 0x00, 0x00, 0x03, 0x80, 0x01, 0xF0, - 0x03, 0xFC, 0x01, 0xFE, 0x00, 0xF7, 0x00, 0x73, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x70, 0x07, - 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x1C, 0x78, - 0x0E, 0x1E, 0x1F, 0x07, 0xFF, 0x01, 0xFF, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x80, - 0x01, 0xC0, 0x00, 0x40, 0x00, 0x00, 0x0C, 0x00, 0x70, 0x01, 0xD8, 0x1F, 0xE0, 0x7F, 0x81, 0xCE, - 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x71, 0xC1, 0xC7, 0x8F, 0x1F, - 0xFC, 0x3F, 0xF0, 0x10, 0x00, 0x00, 0x01, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x20, 0x00, 0x07, 0x00, - 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x00, 0x03, 0x00, 0x19, 0xC0, 0x1C, 0xF0, 0x1E, 0x38, - 0x0E, 0x1E, 0x0E, 0x07, 0x0F, 0x01, 0xC7, 0x00, 0xF7, 0x00, 0x3B, 0x80, 0x0F, 0x80, 0x07, 0xC0, - 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, - 0x80, 0x0E, 0x00, 0x38, 0x00, 0x70, 0x01, 0xC0, 0x00, 0x03, 0x80, 0xE7, 0x03, 0x9C, 0x0E, 0x70, - 0x70, 0xE1, 0xC3, 0x87, 0x0F, 0x38, 0x1C, 0xE0, 0x73, 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0x80, 0x1E, - 0x00, 0x78, 0x01, 0xC0, 0x07, 0x00, 0x38, 0x03, 0xE0, 0x1F, 0x00, 0x30, 0x00, 0x60, 0x03, 0x38, - 0x03, 0x9E, 0x03, 0xC7, 0x01, 0xC3, 0xC1, 0xC0, 0xE1, 0xE0, 0x38, 0xE0, 0x1E, 0xE0, 0x07, 0x70, - 0x01, 0xF0, 0x00, 0xF8, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, - 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, - 0x00, 0x80, 0x00, 0xE0, 0x39, 0xC0, 0xE7, 0x03, 0x9C, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0xCE, 0x07, - 0x38, 0x1C, 0xC0, 0x3F, 0x00, 0xFC, 0x03, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x01, 0xC0, 0x0E, - 0x70, 0xFB, 0xC7, 0xC7, 0x0C, 0x08, 0x03, 0xC0, 0x01, 0xF0, 0x00, 0x30, 0x00, 0x38, 0x00, 0x00, - 0x03, 0x00, 0x19, 0xC0, 0x1C, 0xF0, 0x1E, 0x38, 0x0E, 0x1E, 0x0E, 0x07, 0x0F, 0x01, 0xC7, 0x00, - 0xF7, 0x00, 0x3B, 0x80, 0x0F, 0x80, 0x07, 0xC0, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, - 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x07, 0x80, 0x1E, 0x00, 0x38, 0x00, 0xC0, - 0x00, 0x03, 0x80, 0xE7, 0x03, 0x9C, 0x0E, 0x70, 0x70, 0xE1, 0xC3, 0x87, 0x0F, 0x38, 0x1C, 0xE0, - 0x73, 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xC0, 0x07, 0x00, 0x38, 0x03, - 0xE0, 0x1F, 0x00, 0x30, 0x00, 0x03, 0x30, 0x03, 0xF8, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x03, - 0x00, 0x19, 0xC0, 0x1C, 0xF0, 0x1E, 0x38, 0x0E, 0x1E, 0x0E, 0x07, 0x0F, 0x01, 0xC7, 0x00, 0xF7, - 0x00, 0x3B, 0x80, 0x0F, 0x80, 0x07, 0xC0, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, - 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x0E, 0x60, 0x7F, 0x81, 0xFC, 0x00, 0x00, 0x00, - 0x03, 0x80, 0xE7, 0x03, 0x9C, 0x0E, 0x70, 0x70, 0xE1, 0xC3, 0x87, 0x0F, 0x38, 0x1C, 0xE0, 0x73, - 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xC0, 0x07, 0x00, 0x38, 0x03, 0xE0, - 0x1F, 0x00, 0x30, 0x00, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, - 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0x7D, 0xF7, 0xDF, 0x7D, - 0xF7, 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, - 0xDF, 0x7D, 0xC0, 0x7F, 0xFB, 0xFF, 0xC0, 0x00, 0xFF, 0xF7, 0xFF, 0x80, 0x00, 0x11, 0xCC, 0xE7, - 0x39, 0xCE, 0x33, 0x9C, 0xE7, 0x31, 0x8C, 0x77, 0x76, 0xEE, 0xEC, 0x63, 0x9C, 0xE3, 0x18, 0xE7, - 0x11, 0x1D, 0xCC, 0xEE, 0x77, 0x33, 0xB9, 0xDC, 0xEE, 0x33, 0x39, 0xDD, 0xCE, 0xE7, 0x73, 0x39, - 0x98, 0xCC, 0x73, 0x3B, 0x9D, 0xCC, 0xEE, 0x67, 0x33, 0xB9, 0x9C, 0x67, 0x3B, 0x9D, 0xCE, 0x63, - 0x39, 0x9C, 0xEE, 0x73, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xFF, 0xFC, 0x30, 0x0C, 0x03, - 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xFF, 0xFC, - 0x30, 0x0C, 0x03, 0x8F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, - 0x1F, 0x9F, 0xCF, 0xF7, 0xFB, 0xFD, 0xFE, 0x7E, 0x1E, 0x00, 0x01, 0x83, 0xC7, 0xCF, 0xFF, 0xBE, - 0x70, 0x80, 0x03, 0xBC, 0xE2, 0x00, 0x00, 0x0E, 0x1F, 0xC7, 0xB8, 0x72, 0x00, 0x00, 0x00, 0x1C, - 0x38, 0xFF, 0x1E, 0x3D, 0xC3, 0x8F, 0x20, 0x00, 0x00, 0x03, 0xBC, 0xE2, 0x00, 0x3C, 0x01, 0x80, - 0x07, 0xE0, 0x38, 0x00, 0x7F, 0x07, 0x00, 0x0E, 0x70, 0x70, 0x00, 0xE3, 0x0E, 0x00, 0x0E, 0x71, - 0xC0, 0x00, 0xE7, 0x1C, 0x00, 0x07, 0xE3, 0x80, 0x00, 0x3E, 0x70, 0x00, 0x00, 0x07, 0x00, 0x00, - 0x00, 0xE0, 0x00, 0x00, 0x1C, 0x7C, 0x3E, 0x01, 0xCF, 0xE7, 0xF0, 0x38, 0xE6, 0x67, 0x07, 0x0C, - 0x7E, 0x30, 0x60, 0xC7, 0xE3, 0x0E, 0x0C, 0x7E, 0x31, 0xC0, 0xFE, 0x7F, 0x38, 0x07, 0xE7, 0xE0, - 0x00, 0x38, 0x1C, 0x3C, 0x01, 0x80, 0x00, 0x03, 0xF0, 0x1C, 0x00, 0x00, 0x1F, 0xC1, 0xC0, 0x00, - 0x01, 0xCE, 0x0E, 0x00, 0x00, 0x0E, 0x30, 0xE0, 0x00, 0x00, 0x73, 0x8E, 0x00, 0x00, 0x03, 0x9C, - 0x70, 0x00, 0x00, 0x0F, 0xC7, 0x00, 0x00, 0x00, 0x3E, 0x70, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, - 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x03, 0x8F, 0x87, 0xC7, 0xE0, 0x1C, 0xFE, 0x7F, 0x3F, 0x81, - 0xC7, 0x33, 0x3B, 0x9C, 0x1C, 0x31, 0xF8, 0xDC, 0xE0, 0xC1, 0x8F, 0xC6, 0xE7, 0x0E, 0x0C, 0x7E, - 0x37, 0x38, 0xE0, 0x7F, 0x3F, 0x9F, 0xCE, 0x01, 0xF9, 0xF8, 0xFC, 0x00, 0x07, 0x03, 0x81, 0x80, - 0x6E, 0xEE, 0xEE, 0xCC, 0x63, 0x71, 0xF8, 0xFC, 0xEE, 0x77, 0x3B, 0x1D, 0x8C, 0x63, 0x0F, 0x8E, - 0x7E, 0x39, 0xF9, 0xC7, 0xE7, 0x1B, 0x9C, 0x6C, 0x71, 0xB1, 0x86, 0xCE, 0xEE, 0x66, 0x76, 0xC3, - 0x73, 0xB9, 0xDC, 0x76, 0x3B, 0x1D, 0xCE, 0xC3, 0xC3, 0x1B, 0x9C, 0x7E, 0x71, 0xF8, 0xE7, 0x63, - 0x9D, 0x8E, 0x77, 0x38, 0xD8, 0x63, 0x1C, 0x0F, 0x83, 0xE1, 0xDC, 0xE3, 0xB8, 0xE0, 0x00, 0x0E, - 0x1E, 0x3C, 0x78, 0x70, 0xE0, 0xF0, 0x78, 0x3C, 0x1C, 0x0E, 0x06, 0x00, 0x38, 0x0E, 0x03, 0x81, - 0xE0, 0x78, 0x1C, 0x1E, 0x0E, 0x0E, 0x0E, 0x0E, 0x06, 0x00, 0x00, 0x00, 0x1C, 0x03, 0x7B, 0x1E, - 0xDE, 0x3C, 0xF0, 0x7F, 0x8E, 0xFD, 0xF9, 0xEF, 0xEF, 0xDC, 0x7F, 0x83, 0xCF, 0x1E, 0xDE, 0x37, - 0xB0, 0x1E, 0x00, 0x30, 0x00, 0x60, 0xCE, 0x1D, 0xC3, 0xB8, 0x77, 0x0E, 0xE1, 0xDC, 0x3B, 0x87, - 0x70, 0xEE, 0x1D, 0xC3, 0xB8, 0x77, 0x0E, 0xE1, 0x80, 0x00, 0x00, 0x70, 0xFE, 0x3D, 0xC3, 0x90, - 0x00, 0x0F, 0x03, 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x76, 0x67, 0x0E, 0x70, 0xEE, 0x0F, 0xE0, 0xFC, - 0x07, 0x80, 0x70, 0x06, 0x00, 0x60, 0x00, 0x00, 0x00, 0x0E, 0x00, 0xF0, 0x0E, 0x00, 0x00, 0xFF, - 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x67, 0x80, 0x1F, 0x7F, 0xFF, 0xE1, 0xFF, 0xF8, 0x07, 0xFE, - 0x00, 0x07, 0xFE, 0x01, 0xFF, 0xF8, 0x7F, 0xFF, 0xE7, 0x80, 0x1F, 0x20, 0x00, 0x20, 0x00, 0x00, - 0x18, 0x07, 0x00, 0xC0, 0x38, 0x0E, 0x01, 0xC0, 0x70, 0x0E, 0x03, 0xC0, 0xFC, 0x19, 0xC7, 0x38, - 0xC3, 0x00, 0x00, 0x00, 0x00, 0x05, 0xC0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0xF0, 0x03, 0xC0, 0x07, - 0xE0, 0x01, 0x80, 0x18, 0x18, 0x7E, 0x7E, 0x7E, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x7E, 0x7E, - 0x7E, 0x18, 0x18, 0x7E, 0xFF, 0xFE, 0x00, 0x02, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, - 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x03, 0x80, 0x07, 0x00, - 0x0E, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x30, 0x00, 0x70, 0x00, 0x7B, 0xFF, 0xF8, 0xE3, - 0x8E, 0x38, 0xE3, 0x8E, 0x3F, 0xFF, 0x8E, 0x38, 0xE3, 0x8E, 0x38, 0xE3, 0xFF, 0xFF, 0xFB, 0xFF, - 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xFF, 0xFC, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1F, 0xFF, 0xFE, - 0x0F, 0x00, 0x78, 0x1F, 0xE0, 0xFF, 0x0F, 0xF8, 0xFF, 0xCF, 0x1E, 0x70, 0xF7, 0x07, 0x38, 0x3B, - 0x83, 0x98, 0x1C, 0x01, 0xC0, 0x0E, 0x01, 0xE0, 0x0E, 0x01, 0xE0, 0x0F, 0x03, 0xE0, 0x1F, 0x01, - 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x00, 0xE0, 0x07, 0x00, 0x70, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x0F, 0x00, 0x70, 0x07, 0x80, 0x3C, 0x03, 0xC0, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x0F, - 0x01, 0x8F, 0xF0, 0x73, 0xFE, 0x1D, 0xE3, 0xC7, 0x70, 0x71, 0xDC, 0x1C, 0x70, 0x07, 0x1C, 0x03, - 0xC7, 0x01, 0xE1, 0xC1, 0xF0, 0x70, 0x78, 0x1C, 0x1C, 0x07, 0x0E, 0x01, 0xC3, 0x80, 0x70, 0x00, - 0x00, 0x00, 0x00, 0x0F, 0x01, 0xC3, 0xC0, 0xF0, 0xF0, 0x1C, 0x00, 0x00, 0x60, 0x3C, 0x1C, 0x3F, - 0xC7, 0x1F, 0xF9, 0xC7, 0x0E, 0x71, 0xC1, 0xDC, 0x60, 0x77, 0x00, 0x1D, 0xC0, 0x0E, 0x70, 0x0F, - 0x9C, 0x07, 0xC7, 0x01, 0xC1, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x70, 0x38, 0x3C, 0x0F, 0x07, 0x03, 0x80, 0x80, 0x40, 0xFF, 0xFF, 0xFF, 0xE0, 0x70, 0x38, 0x1C, - 0x0E, 0x06, 0x03, 0x01, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x01, 0x80, 0x00, - 0xFE, 0x0F, 0xF8, 0xFF, 0xEE, 0xFE, 0xEF, 0xFE, 0xFF, 0xEF, 0xFE, 0xFF, 0xEF, 0xFE, 0xFE, 0xEF, - 0xEE, 0xFC, 0xE6, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x00, 0x0F, 0xF3, 0xFF, - 0x7F, 0xF7, 0xF7, 0xFF, 0x7F, 0xF7, 0xFF, 0x77, 0xF7, 0x7F, 0x73, 0xFF, 0x1F, 0xF0, 0x7F, 0x0F, - 0xFC, 0xFF, 0xEE, 0x7F, 0xE7, 0xFE, 0x7F, 0xE7, 0xFE, 0x7F, 0xE7, 0xFF, 0xFE, 0xFF, 0xC0, 0x0C, - 0x01, 0x81, 0x30, 0x76, 0xEF, 0xFC, 0x7E, 0x0F, 0xC7, 0xFE, 0xED, 0xC1, 0x80, 0x30, 0x00, 0x00, - 0x77, 0x9C, 0x40, 0x00, 0x00, 0x00, 0x1C, 0xE7, 0x1C, 0xE7, 0x18, 0xC0, 0x07, 0xFE, 0x01, 0xFF, - 0xF8, 0x7F, 0xFF, 0xE7, 0x80, 0x1E, 0x60, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x80, 0x01, 0xF0, 0x03, 0xFC, 0x01, 0xFE, 0x00, 0xF7, 0x00, 0x73, 0x80, 0x39, 0xC0, + 0x1C, 0xE0, 0x0E, 0x70, 0x07, 0x38, 0x03, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80, 0x39, + 0xC0, 0x1C, 0xE0, 0x1C, 0x78, 0x0E, 0x1E, 0x1F, 0x07, 0xFF, 0x01, 0xFF, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x07, 0x80, 0x01, 0xC0, 0x00, 0x40, 0x00, 0x00, 0x0C, 0x00, 0x70, 0x01, 0xD8, + 0x1F, 0xE0, 0x7F, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, + 0x71, 0xC1, 0xC7, 0x8F, 0x1F, 0xFC, 0x3F, 0xF0, 0x10, 0x00, 0x00, 0x01, 0xC0, 0x07, 0x00, 0x1C, + 0x00, 0x20, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x00, 0x03, 0x00, 0x19, + 0xC0, 0x1C, 0xF0, 0x1E, 0x38, 0x0E, 0x1E, 0x0E, 0x07, 0x0F, 0x01, 0xC7, 0x00, 0xF7, 0x00, 0x3B, + 0x80, 0x0F, 0x80, 0x07, 0xC0, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, + 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00, 0x70, 0x01, 0xC0, 0x00, 0x03, 0x80, + 0xE7, 0x03, 0x9C, 0x0E, 0x70, 0x70, 0xE1, 0xC3, 0x87, 0x0F, 0x38, 0x1C, 0xE0, 0x73, 0x00, 0xFC, + 0x03, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xC0, 0x07, 0x00, 0x38, 0x03, 0xE0, 0x1F, 0x00, + 0x30, 0x00, 0x60, 0x03, 0x38, 0x03, 0x9E, 0x03, 0xC7, 0x01, 0xC3, 0xC1, 0xC0, 0xE1, 0xE0, 0x38, + 0xE0, 0x1E, 0xE0, 0x07, 0x70, 0x01, 0xF0, 0x00, 0xF8, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, + 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x07, 0x00, 0x03, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x39, 0xC0, 0xE7, 0x03, 0x9C, 0x1C, 0x38, + 0x70, 0xE1, 0xC3, 0xCE, 0x07, 0x38, 0x1C, 0xC0, 0x3F, 0x00, 0xFC, 0x03, 0xE0, 0x07, 0x80, 0x1E, + 0x00, 0x70, 0x01, 0xC0, 0x0E, 0x70, 0xFB, 0xC7, 0xC7, 0x0C, 0x08, 0x03, 0xC0, 0x01, 0xF0, 0x00, + 0x30, 0x00, 0x38, 0x00, 0x00, 0x03, 0x00, 0x19, 0xC0, 0x1C, 0xF0, 0x1E, 0x38, 0x0E, 0x1E, 0x0E, + 0x07, 0x0F, 0x01, 0xC7, 0x00, 0xF7, 0x00, 0x3B, 0x80, 0x0F, 0x80, 0x07, 0xC0, 0x01, 0xC0, 0x00, + 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x07, 0x80, + 0x1E, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x03, 0x80, 0xE7, 0x03, 0x9C, 0x0E, 0x70, 0x70, 0xE1, 0xC3, + 0x87, 0x0F, 0x38, 0x1C, 0xE0, 0x73, 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0x78, 0x01, + 0xC0, 0x07, 0x00, 0x38, 0x03, 0xE0, 0x1F, 0x00, 0x30, 0x00, 0x03, 0x30, 0x03, 0xF8, 0x01, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x19, 0xC0, 0x1C, 0xF0, 0x1E, 0x38, 0x0E, 0x1E, 0x0E, 0x07, + 0x0F, 0x01, 0xC7, 0x00, 0xF7, 0x00, 0x3B, 0x80, 0x0F, 0x80, 0x07, 0xC0, 0x01, 0xC0, 0x00, 0xE0, + 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x0E, 0x60, 0x7F, + 0x81, 0xFC, 0x00, 0x00, 0x00, 0x03, 0x80, 0xE7, 0x03, 0x9C, 0x0E, 0x70, 0x70, 0xE1, 0xC3, 0x87, + 0x0F, 0x38, 0x1C, 0xE0, 0x73, 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xC0, + 0x07, 0x00, 0x38, 0x03, 0xE0, 0x1F, 0x00, 0x30, 0x00, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, + 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, + 0x03, 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, 0xDF, + 0x7D, 0xF7, 0xDF, 0x7D, 0xF7, 0xDF, 0x7D, 0xC0, 0x7F, 0xFB, 0xFF, 0xC0, 0x00, 0xFF, 0xF7, 0xFF, + 0x80, 0x00, 0x11, 0xCC, 0xE7, 0x39, 0xCE, 0x33, 0x9C, 0xE7, 0x31, 0x8C, 0x77, 0x76, 0xEE, 0xEC, + 0x63, 0x9C, 0xE3, 0x18, 0xE7, 0x11, 0x1D, 0xCC, 0xEE, 0x77, 0x33, 0xB9, 0xDC, 0xEE, 0x33, 0x39, + 0xDD, 0xCE, 0xE7, 0x73, 0x39, 0x98, 0xCC, 0x73, 0x3B, 0x9D, 0xCC, 0xEE, 0x67, 0x33, 0xB9, 0x9C, + 0x67, 0x3B, 0x9D, 0xCE, 0x63, 0x39, 0x9C, 0xEE, 0x73, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, + 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0xC0, + 0x30, 0xFF, 0xFF, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x8F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, + 0x0C, 0x03, 0x00, 0xC0, 0x00, 0x1F, 0x9F, 0xCF, 0xF7, 0xFB, 0xFD, 0xFE, 0x7E, 0x1E, 0x00, 0x01, + 0x83, 0xC7, 0xCF, 0xFF, 0xBE, 0x70, 0x80, 0x03, 0xBC, 0xE2, 0x00, 0x00, 0x0E, 0x1F, 0xC7, 0xB8, + 0x72, 0x00, 0x00, 0x00, 0x1C, 0x38, 0xFF, 0x1E, 0x3D, 0xC3, 0x8F, 0x20, 0x00, 0x00, 0x03, 0xBC, + 0xE2, 0x00, 0x3C, 0x01, 0x80, 0x07, 0xE0, 0x38, 0x00, 0x7F, 0x07, 0x00, 0x0E, 0x70, 0x70, 0x00, + 0xE3, 0x0E, 0x00, 0x0E, 0x71, 0xC0, 0x00, 0xE7, 0x1C, 0x00, 0x07, 0xE3, 0x80, 0x00, 0x3E, 0x70, + 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x1C, 0x7C, 0x3E, 0x01, 0xCF, 0xE7, 0xF0, + 0x38, 0xE6, 0x67, 0x07, 0x0C, 0x7E, 0x30, 0x60, 0xC7, 0xE3, 0x0E, 0x0C, 0x7E, 0x31, 0xC0, 0xFE, + 0x7F, 0x38, 0x07, 0xE7, 0xE0, 0x00, 0x38, 0x1C, 0x3C, 0x01, 0x80, 0x00, 0x03, 0xF0, 0x1C, 0x00, + 0x00, 0x1F, 0xC1, 0xC0, 0x00, 0x01, 0xCE, 0x0E, 0x00, 0x00, 0x0E, 0x30, 0xE0, 0x00, 0x00, 0x73, + 0x8E, 0x00, 0x00, 0x03, 0x9C, 0x70, 0x00, 0x00, 0x0F, 0xC7, 0x00, 0x00, 0x00, 0x3E, 0x70, 0x00, + 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x03, 0x8F, 0x87, 0xC7, 0xE0, + 0x1C, 0xFE, 0x7F, 0x3F, 0x81, 0xC7, 0x33, 0x3B, 0x9C, 0x1C, 0x31, 0xF8, 0xDC, 0xE0, 0xC1, 0x8F, + 0xC6, 0xE7, 0x0E, 0x0C, 0x7E, 0x37, 0x38, 0xE0, 0x7F, 0x3F, 0x9F, 0xCE, 0x01, 0xF9, 0xF8, 0xFC, + 0x00, 0x07, 0x03, 0x81, 0x80, 0x6E, 0xEE, 0xEE, 0xCC, 0x63, 0x71, 0xF8, 0xFC, 0xEE, 0x77, 0x3B, + 0x1D, 0x8C, 0x63, 0x0F, 0x8E, 0x7E, 0x39, 0xF9, 0xC7, 0xE7, 0x1B, 0x9C, 0x6C, 0x71, 0xB1, 0x86, + 0xCE, 0xEE, 0x66, 0x76, 0xC3, 0x73, 0xB9, 0xDC, 0x76, 0x3B, 0x1D, 0xCE, 0xC3, 0xC3, 0x1B, 0x9C, + 0x7E, 0x71, 0xF8, 0xE7, 0x63, 0x9D, 0x8E, 0x77, 0x38, 0xD8, 0x63, 0x1C, 0x0F, 0x83, 0xE1, 0xDC, + 0xE3, 0xB8, 0xE0, 0x00, 0x0E, 0x1E, 0x3C, 0x78, 0x70, 0xE0, 0xF0, 0x78, 0x3C, 0x1C, 0x0E, 0x06, + 0x00, 0x38, 0x0E, 0x03, 0x81, 0xE0, 0x78, 0x1C, 0x1E, 0x0E, 0x0E, 0x0E, 0x0E, 0x06, 0x00, 0x00, + 0x00, 0x1C, 0x03, 0x7B, 0x1E, 0xDE, 0x3C, 0xF0, 0x7F, 0x8E, 0xFD, 0xF9, 0xEF, 0xEF, 0xDC, 0x7F, + 0x83, 0xCF, 0x1E, 0xDE, 0x37, 0xB0, 0x1E, 0x00, 0x30, 0x00, 0x60, 0xCE, 0x1D, 0xC3, 0xB8, 0x77, + 0x0E, 0xE1, 0xDC, 0x3B, 0x87, 0x70, 0xEE, 0x1D, 0xC3, 0xB8, 0x77, 0x0E, 0xE1, 0x80, 0x00, 0x00, + 0x70, 0xFE, 0x3D, 0xC3, 0x90, 0x00, 0x0F, 0x03, 0xFC, 0x3F, 0xE7, 0x0E, 0x70, 0x76, 0x67, 0x0E, + 0x70, 0xEE, 0x0F, 0xE0, 0xFC, 0x07, 0x80, 0x70, 0x06, 0x00, 0x60, 0x00, 0x00, 0x00, 0x0E, 0x00, + 0xF0, 0x0E, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x67, 0x80, 0x1F, 0x7F, 0xFF, + 0xE1, 0xFF, 0xF8, 0x07, 0xFE, 0x00, 0x07, 0xFE, 0x01, 0xFF, 0xF8, 0x7F, 0xFF, 0xE7, 0x80, 0x1F, + 0x20, 0x00, 0x20, 0x00, 0x00, 0x18, 0x07, 0x00, 0xC0, 0x38, 0x0E, 0x01, 0xC0, 0x70, 0x0E, 0x03, + 0xC0, 0xFC, 0x19, 0xC7, 0x38, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x05, 0xC0, 0x07, 0xE0, 0x07, 0xE0, + 0x0F, 0xF0, 0x03, 0xC0, 0x07, 0xE0, 0x01, 0x80, 0x18, 0x18, 0x7E, 0x7E, 0x7E, 0x7E, 0xFF, 0xFF, + 0xFF, 0xFF, 0x7E, 0x7E, 0x7E, 0x7E, 0x18, 0x18, 0x7E, 0xFF, 0xFE, 0x00, 0x02, 0x00, 0x06, 0x00, + 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x00, 0xE0, 0x01, 0xC0, 0x03, + 0x80, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x30, 0x00, 0x70, + 0x00, 0x7B, 0xFF, 0xF8, 0xE3, 0x8E, 0x38, 0xE3, 0x8E, 0x3F, 0xFF, 0x8E, 0x38, 0xE3, 0x8E, 0x38, + 0xE3, 0xFF, 0xFF, 0xFB, 0xFF, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xFF, 0xFC, 0x71, 0xC7, 0x1C, + 0x71, 0xC7, 0x1F, 0xFF, 0xFE, 0x0F, 0x00, 0x78, 0x1F, 0xE0, 0xFF, 0x0F, 0xF8, 0xFF, 0xCF, 0x1E, + 0x70, 0xF7, 0x07, 0x38, 0x3B, 0x83, 0x98, 0x1C, 0x01, 0xC0, 0x0E, 0x01, 0xE0, 0x0E, 0x01, 0xE0, + 0x0F, 0x03, 0xE0, 0x1F, 0x01, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x00, 0xE0, 0x07, 0x00, 0x70, 0x03, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x70, 0x07, 0x80, 0x3C, 0x03, 0xC0, 0x1C, + 0x00, 0x00, 0x00, 0x00, 0x0F, 0x01, 0x8F, 0xF0, 0x73, 0xFE, 0x1D, 0xE3, 0xC7, 0x70, 0x71, 0xDC, + 0x1C, 0x70, 0x07, 0x1C, 0x03, 0xC7, 0x01, 0xE1, 0xC1, 0xF0, 0x70, 0x78, 0x1C, 0x1C, 0x07, 0x0E, + 0x01, 0xC3, 0x80, 0x70, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x01, 0xC3, 0xC0, 0xF0, 0xF0, 0x1C, 0x00, + 0x00, 0x60, 0x3C, 0x1C, 0x3F, 0xC7, 0x1F, 0xF9, 0xC7, 0x0E, 0x71, 0xC1, 0xDC, 0x60, 0x77, 0x00, + 0x1D, 0xC0, 0x0E, 0x70, 0x0F, 0x9C, 0x07, 0xC7, 0x01, 0xC1, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x38, 0x3C, 0x0F, 0x07, 0x03, 0x80, 0x80, 0x40, 0xFF, 0xFF, + 0xFF, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x06, 0x03, 0x01, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, + 0x07, 0x03, 0x01, 0x80, 0x00, 0xFE, 0x0F, 0xF8, 0xFF, 0xEE, 0xFE, 0xEF, 0xFE, 0xFF, 0xEF, 0xFE, + 0xFF, 0xEF, 0xFE, 0xFE, 0xEF, 0xEE, 0xFC, 0xE6, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, + 0xE0, 0x00, 0x0F, 0xF3, 0xFF, 0x7F, 0xF7, 0xF7, 0xFF, 0x7F, 0xF7, 0xFF, 0x77, 0xF7, 0x7F, 0x73, + 0xFF, 0x1F, 0xF0, 0x7F, 0x0F, 0xFC, 0xFF, 0xEE, 0x7F, 0xE7, 0xFE, 0x7F, 0xE7, 0xFE, 0x7F, 0xE7, + 0xFF, 0xFE, 0xFF, 0xC0, 0x0C, 0x01, 0x81, 0x30, 0x76, 0xEF, 0xFC, 0x7E, 0x0F, 0xC7, 0xFE, 0xED, + 0xC1, 0x80, 0x30, 0x00, 0x00, 0x77, 0x9C, 0x40, 0x00, 0x00, 0x00, 0x1C, 0xE7, 0x1C, 0xE7, 0x18, + 0xC0, 0x07, 0xFE, 0x01, 0xFF, 0xF8, 0x7F, 0xFF, 0xE7, 0x80, 0x1E, 0x60, 0x00, 0x60, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, - 0x67, 0x80, 0x1E, 0x7F, 0xFF, 0xE1, 0xFF, 0xF8, 0x07, 0xFE, 0x00, 0x04, 0x01, 0xC1, 0x39, 0x3F, - 0xE7, 0xFC, 0x7E, 0x0F, 0xE7, 0xFE, 0x6E, 0xC1, 0xC0, 0x38, 0x00, 0x00, 0x00, 0x08, 0x03, 0x82, - 0x72, 0x7F, 0xCF, 0xF8, 0xFC, 0x1F, 0xCF, 0xFC, 0xDD, 0x83, 0x80, 0x70, 0x00, 0x00, 0x00, 0x06, - 0x00, 0x0E, 0x00, 0x0C, 0x70, 0x1C, 0xF8, 0x38, 0xF8, 0x70, 0x70, 0x70, 0x00, 0xE0, 0x01, 0xC0, - 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x07, 0x0E, 0x0E, 0x1F, 0x1C, 0x1F, 0x1C, 0x1F, 0x38, 0x0E, - 0x70, 0x00, 0x60, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xFF, 0xC6, 0x67, 0xFF, 0x00, 0xFE, 0x00, 0x38, - 0x03, 0xFC, 0x01, 0xFF, 0xF8, 0x3F, 0xFF, 0xC7, 0xE0, 0x7F, 0x70, 0x00, 0x60, 0x06, 0x06, 0xD9, - 0xFF, 0x1F, 0xCF, 0xFF, 0xFF, 0xDF, 0xF9, 0xFE, 0x7F, 0xC5, 0xD8, 0x18, 0x00, 0x63, 0x0C, 0x6E, - 0x39, 0xCE, 0xE3, 0x9C, 0xEE, 0x71, 0xCE, 0xE7, 0x18, 0xEE, 0x71, 0x8C, 0xC7, 0x18, 0xCC, 0x61, - 0x8C, 0x3E, 0x1F, 0xCF, 0x3B, 0x8E, 0xE1, 0xF8, 0x7E, 0x3B, 0x8E, 0x7F, 0x9F, 0xC0, 0xC0, 0x5F, - 0xFF, 0xFF, 0xFF, 0x80, 0x0F, 0x03, 0xC1, 0xF0, 0xFC, 0x77, 0x1D, 0xCF, 0xFB, 0xFF, 0xFF, 0x81, - 0xC0, 0x00, 0x7F, 0x3F, 0x98, 0x1F, 0xCF, 0xF7, 0xBC, 0x0F, 0xC7, 0xFF, 0xBF, 0x86, 0x00, 0x3E, - 0x3F, 0xBC, 0xFD, 0xCF, 0xF7, 0xFF, 0x8F, 0xC7, 0xFF, 0xBF, 0x87, 0x00, 0x7F, 0xBF, 0xC0, 0xE0, - 0xE0, 0x70, 0x70, 0x38, 0x38, 0x38, 0x1C, 0x00, 0x00, 0x7E, 0x7F, 0xB8, 0xFC, 0x77, 0xF3, 0xFB, - 0x8F, 0xC7, 0xFF, 0xBF, 0x87, 0x00, 0x7E, 0x7F, 0xB8, 0xFC, 0x7E, 0x7F, 0xFC, 0xFF, 0xC7, 0xFF, - 0x3F, 0x06, 0x00, 0x1C, 0x0E, 0x1F, 0xDF, 0xEF, 0xF0, 0xE0, 0x70, 0x7F, 0x7F, 0xBF, 0xC0, 0xFF, - 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0x39, 0x9C, 0xE6, 0x73, 0x9C, 0xE3, 0x9C, 0xE3, 0x80, 0x63, 0x9C, - 0x73, 0x9C, 0xE7, 0x39, 0xCC, 0xE6, 0x00, 0xCE, 0x7F, 0xBF, 0xDC, 0x7E, 0x3F, 0x1F, 0x8F, 0xC7, - 0x00, 0x00, 0x3E, 0x1F, 0xCF, 0x3B, 0x8E, 0xE1, 0xF8, 0x7E, 0x3B, 0x8E, 0x7F, 0x9F, 0xC0, 0xC0, - 0x7F, 0xFF, 0x73, 0x9C, 0xE7, 0x39, 0xC0, 0x7E, 0x7F, 0xB8, 0xF8, 0x70, 0x70, 0x70, 0xF0, 0xF0, - 0xFF, 0xFF, 0xC0, 0x00, 0x3E, 0x3F, 0xB8, 0xE0, 0x71, 0xF0, 0xF8, 0x0F, 0xC7, 0xFF, 0xBF, 0x87, - 0x00, 0x0F, 0x03, 0xC1, 0xF0, 0xFC, 0x77, 0x1D, 0xCF, 0xFB, 0xFF, 0xFF, 0x81, 0xC0, 0x00, 0x7F, - 0x3F, 0x98, 0x1F, 0xCF, 0xF7, 0xBC, 0x0F, 0xC7, 0xFF, 0xBF, 0x86, 0x00, 0x3E, 0x3F, 0xBC, 0xFD, - 0xCF, 0xF7, 0xFF, 0x8F, 0xC7, 0xFF, 0xBF, 0x87, 0x00, 0x7F, 0xBF, 0xC0, 0xE0, 0xE0, 0x70, 0x70, - 0x38, 0x38, 0x38, 0x1C, 0x00, 0x00, 0x7E, 0x7F, 0xB8, 0xFC, 0x77, 0xF3, 0xFB, 0x8F, 0xC7, 0xFF, - 0xBF, 0x87, 0x00, 0x7E, 0x7F, 0xB8, 0xFC, 0x7E, 0x7F, 0xFC, 0xFF, 0xC7, 0xFF, 0x3F, 0x06, 0x00, - 0x1C, 0x0E, 0x1F, 0xDF, 0xEF, 0xF0, 0xE0, 0x70, 0x7F, 0x7F, 0xBF, 0xC0, 0xFF, 0xFF, 0x00, 0xFF, - 0xFF, 0x00, 0x39, 0x9C, 0xE6, 0x73, 0x9C, 0xE3, 0x9C, 0xE3, 0x80, 0x63, 0x9C, 0x73, 0x9C, 0xE7, - 0x39, 0xCC, 0xE6, 0x00, 0x1E, 0x3F, 0x9D, 0xE3, 0xF7, 0xFB, 0x9D, 0xFE, 0xFF, 0x18, 0x00, 0x1E, - 0x0F, 0xC7, 0xB9, 0xFE, 0x7F, 0xDC, 0x07, 0xF8, 0xFE, 0x0C, 0x00, 0x1E, 0x0F, 0xE7, 0xF9, 0xC7, - 0x61, 0xDC, 0x77, 0xF8, 0xFC, 0x0C, 0x00, 0x61, 0x9C, 0xE3, 0xF0, 0xF8, 0x1E, 0x0F, 0xC7, 0xF1, - 0xCE, 0x00, 0x00, 0x3E, 0x3F, 0xCF, 0x78, 0x0E, 0xFF, 0xBF, 0xEF, 0xFB, 0x8E, 0x7F, 0x9F, 0xC1, - 0xC0, 0xE0, 0x70, 0x3B, 0x9F, 0xEF, 0xF7, 0x1F, 0x8F, 0xC7, 0xE3, 0xF1, 0xC0, 0x00, 0xE0, 0x70, - 0x39, 0xDD, 0xEF, 0xE7, 0xE3, 0xF1, 0xFC, 0xEE, 0x73, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, - 0xCC, 0x77, 0xFF, 0xFF, 0xFF, 0xCE, 0x7E, 0x73, 0xF3, 0x9F, 0x9C, 0xFC, 0xE7, 0x00, 0x00, 0xCE, - 0x7F, 0xBF, 0xDC, 0x7E, 0x3F, 0x1F, 0x8F, 0xC7, 0x00, 0x00, 0xEE, 0x7F, 0xBF, 0xFC, 0x7E, 0x3F, - 0x1F, 0xFF, 0xFE, 0xEC, 0x70, 0x38, 0x00, 0x1E, 0x3F, 0x9D, 0xEF, 0x83, 0xF1, 0x7D, 0xFE, 0xFE, - 0x1C, 0x00, 0x01, 0xCF, 0xFF, 0xF9, 0xC7, 0x1C, 0x7C, 0xF1, 0x80, 0x0F, 0x80, 0x3F, 0xC0, 0x7D, - 0xE0, 0x70, 0xE0, 0x70, 0x70, 0x70, 0x00, 0x73, 0xFE, 0x73, 0xFF, 0x73, 0xFE, 0x73, 0x80, 0x7B, - 0x80, 0x3F, 0xFC, 0x1F, 0xFE, 0x07, 0xFC, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0xFE, 0x03, - 0xFF, 0x00, 0x00, 0x00, 0x64, 0x00, 0x6C, 0x03, 0xEC, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0xDE, 0x79, - 0x9F, 0x71, 0xB7, 0xF1, 0xB7, 0xE3, 0x30, 0xE3, 0x60, 0xE3, 0x60, 0xE2, 0x60, 0xE6, 0xC0, 0xE6, - 0xC0, 0x76, 0xC7, 0x7C, 0x87, 0x7D, 0x8F, 0x3F, 0xBE, 0x1F, 0xFC, 0x1F, 0xF8, 0x1B, 0xC0, 0x33, - 0x00, 0x12, 0x00, 0x03, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x1E, 0x78, 0x0F, 0x70, 0x07, 0xF0, - 0x07, 0xE0, 0x00, 0xE1, 0x9C, 0xE1, 0xFE, 0xE1, 0xFC, 0xE1, 0xE0, 0xE1, 0xC0, 0x71, 0xC7, 0x71, - 0xC7, 0x79, 0xCE, 0x3F, 0xFE, 0x1F, 0xFC, 0x0F, 0xF8, 0x01, 0xC0, 0x0F, 0xFC, 0x3F, 0xFC, 0x7F, - 0xF8, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x3F, 0xF8, 0x7F, 0xF0, - 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x3F, 0xE0, 0x7F, 0xC0, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0x03, - 0x80, 0x7F, 0x07, 0xFC, 0x78, 0xF3, 0x83, 0x9C, 0x1C, 0xE0, 0x1F, 0xF8, 0xFF, 0xC7, 0xFE, 0x0E, - 0x01, 0xFF, 0x8F, 0xFC, 0x1C, 0x00, 0xE0, 0x07, 0x07, 0x70, 0x3F, 0xFF, 0xFF, 0xFC, 0x00, 0x38, - 0x00, 0x07, 0x01, 0x9E, 0xFE, 0x7F, 0xFF, 0xEF, 0xFF, 0xFD, 0xE1, 0xE3, 0xFC, 0x38, 0x3F, 0x07, - 0x07, 0xE1, 0xE0, 0xFC, 0x3C, 0x1F, 0x8F, 0x83, 0xF1, 0xF0, 0x7E, 0x3E, 0x0F, 0xCF, 0xC1, 0xF9, - 0xF8, 0x3F, 0x77, 0x07, 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x01, 0x87, 0x80, 0x70, 0xF8, 0x0E, - 0x1F, 0x01, 0xC3, 0xF0, 0x38, 0x7E, 0x07, 0x0E, 0xE0, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFC, 0xE3, 0xCE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0x7F, 0x0E, 0x07, 0xE1, 0xC0, 0xFC, 0x38, - 0x0F, 0x87, 0x00, 0xF0, 0xE0, 0x1E, 0x00, 0xFE, 0x00, 0x07, 0xFE, 0x00, 0x3F, 0xFC, 0x71, 0xC1, - 0xE3, 0x8E, 0x07, 0x9C, 0x70, 0x1F, 0xFF, 0x80, 0xFF, 0xFC, 0x07, 0xFF, 0xE0, 0x79, 0xC7, 0x07, - 0x8E, 0x3F, 0xFC, 0x71, 0xFF, 0xC3, 0x8F, 0xF0, 0x1C, 0x70, 0x00, 0xE3, 0x80, 0x07, 0x1C, 0x00, - 0x38, 0xE0, 0x01, 0xE7, 0x00, 0x07, 0xF8, 0x00, 0x3E, 0x00, 0x00, 0x20, 0xFE, 0x00, 0x00, 0x7F, - 0xF0, 0x00, 0x3F, 0xFC, 0x00, 0x1C, 0x1E, 0x00, 0x0E, 0x03, 0x80, 0x07, 0x01, 0xC3, 0xE3, 0x80, - 0xE7, 0xF9, 0xC0, 0x73, 0xFE, 0xE0, 0x7B, 0x83, 0xFF, 0xF9, 0xC0, 0x3F, 0xF8, 0xF0, 0x1F, 0xF8, - 0x3F, 0x8E, 0x1C, 0x0F, 0xF7, 0x0F, 0x00, 0xFB, 0x83, 0x80, 0x0F, 0xC1, 0xE7, 0x07, 0xE0, 0x73, - 0x87, 0xF0, 0x3D, 0xFF, 0xB8, 0x0E, 0x7F, 0x80, 0x00, 0x07, 0x00, 0x60, 0x18, 0x06, 0xE0, 0x3C, - 0x0E, 0x70, 0x3C, 0x0E, 0x70, 0x7C, 0x0E, 0x70, 0x7C, 0x0E, 0x70, 0x7E, 0x1C, 0x38, 0x7E, 0x1C, - 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFE, 0x38, 0xE7, 0x38, 0x1C, 0xE7, 0x38, 0xFF, 0xFF, 0xFE, 0xFF, - 0xFF, 0xFE, 0x1F, 0xC3, 0xF0, 0x0F, 0x83, 0xF0, 0x0F, 0x81, 0xF0, 0x0F, 0x81, 0xE0, 0x07, 0x81, - 0xE0, 0x07, 0x01, 0xE0, 0xFF, 0xC3, 0x7F, 0xF3, 0xBF, 0xFD, 0xDC, 0x0E, 0xEE, 0x67, 0x77, 0x73, - 0xBB, 0xB9, 0xDD, 0xDC, 0xEE, 0xEE, 0x77, 0x77, 0x3B, 0xBB, 0x81, 0xDD, 0xC1, 0xEE, 0xFF, 0xF7, - 0x7F, 0xF0, 0x00, 0x30, 0x00, 0x70, 0x0F, 0xF0, 0x1F, 0xE0, 0x03, 0x83, 0xE7, 0x0F, 0xEE, 0x3F, - 0xFC, 0x70, 0xF9, 0xC0, 0xF3, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x1E, 0x78, 0x3C, - 0x78, 0xF8, 0xFF, 0xF0, 0xFE, 0xE0, 0x30, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFE, 0x7F, 0xFC, 0x00, - 0x00, 0x00, 0xF0, 0x03, 0xFC, 0x07, 0xFE, 0x0F, 0x0E, 0x0E, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x7F, - 0xF8, 0xFF, 0xF8, 0x38, 0x00, 0x7F, 0xE0, 0x7F, 0xE0, 0xFF, 0xE0, 0x1C, 0x00, 0x1C, 0x00, 0x0E, - 0x00, 0x0F, 0x8E, 0x07, 0xFE, 0x03, 0xFC, 0x00, 0x70, 0x18, 0x03, 0x87, 0x01, 0xE1, 0xC0, 0xF0, - 0x70, 0x78, 0x1C, 0x3C, 0x07, 0x1E, 0x01, 0xCF, 0x00, 0x77, 0x80, 0xFF, 0xFF, 0xBF, 0xFF, 0xEF, - 0xFF, 0xF8, 0x7F, 0xC0, 0x1E, 0x70, 0x07, 0x0E, 0x01, 0xC3, 0xC0, 0x70, 0x78, 0x1C, 0x0E, 0x07, - 0x01, 0xC1, 0xC0, 0x78, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x7F, - 0x01, 0xFC, 0x1F, 0xC0, 0xFE, 0x83, 0xFF, 0x01, 0xF8, 0x1F, 0x80, 0xFE, 0x03, 0x78, 0x01, 0xE0, - 0x07, 0x80, 0x1E, 0x00, 0x78, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x01, 0xFC, - 0x00, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0x03, 0xFF, 0xF0, 0x00, 0x01, 0xF8, 0x7C, 0x00, 0x00, 0x1C, - 0x0E, 0x00, 0x00, 0x0E, 0x03, 0x81, 0xF0, 0x07, 0x01, 0xC3, 0xFC, 0x03, 0x80, 0xE3, 0xFF, 0x01, - 0xC0, 0x39, 0xC3, 0xC0, 0xE0, 0x1D, 0xC0, 0xE0, 0x70, 0x0E, 0xE0, 0x70, 0x38, 0x07, 0x70, 0x1C, - 0xFC, 0x07, 0x38, 0x0E, 0xFE, 0x03, 0x9C, 0x07, 0xFF, 0x01, 0xCE, 0x07, 0x73, 0xC1, 0xE7, 0x03, - 0xB9, 0xF1, 0xE3, 0xC3, 0xDF, 0xFF, 0xE1, 0xFF, 0xC7, 0xCF, 0xE0, 0xFF, 0xC0, 0xC1, 0xC0, 0x73, - 0x80, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x80, 0x3F, 0xF0, 0x3F, 0xF8, 0x38, 0x3C, 0x7F, 0xFF, - 0xFF, 0xFF, 0x7F, 0xFF, 0x38, 0x0E, 0xFF, 0xFF, 0xFF, 0xFF, 0x38, 0x1C, 0x38, 0x7C, 0x3F, 0xF8, - 0x3F, 0xF0, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, 0xE0, - 0x00, 0xF8, 0x01, 0xFF, 0x01, 0xFF, 0xC1, 0xEE, 0xF1, 0xE7, 0x3C, 0xE3, 0x8E, 0xF1, 0xC7, 0x70, - 0xE0, 0x38, 0x70, 0x1C, 0x3F, 0xEE, 0x1F, 0xF7, 0x0F, 0xFB, 0x87, 0x1C, 0xE3, 0x8E, 0x71, 0xC7, - 0x3C, 0xE7, 0x8F, 0xFF, 0x83, 0xFF, 0x80, 0xFF, 0x80, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x00, 0x01, - 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x00, 0x7C, 0x00, 0x77, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x1C, 0x70, - 0x7F, 0xFF, 0x3F, 0xFF, 0x87, 0x07, 0x03, 0x83, 0x87, 0xFF, 0xF3, 0xFF, 0xF9, 0xFF, 0xFC, 0xF0, - 0x1E, 0x70, 0x07, 0x38, 0x03, 0xBC, 0x00, 0xE0, 0x07, 0x80, 0x7F, 0x83, 0xFF, 0x1E, 0x1C, 0x70, - 0x70, 0xC1, 0xC0, 0x07, 0x3F, 0xFF, 0xFF, 0xFC, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xC0, - 0x07, 0x03, 0x9C, 0x0E, 0x78, 0xF8, 0xFF, 0xC1, 0xFE, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x03, - 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3D, 0xDE, 0x79, 0xCF, 0x71, 0xC7, 0xF1, 0xC7, 0xE1, 0xC0, 0xE1, - 0xC0, 0xE1, 0xC0, 0xE1, 0xC0, 0xE1, 0xC0, 0xE1, 0xC0, 0x71, 0xC7, 0x71, 0xC7, 0x79, 0xCF, 0x3F, - 0xFE, 0x1F, 0xFC, 0x0F, 0xF8, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xC0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, - 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x00, 0x7F, 0xFB, 0xFF, - 0xFF, 0xFE, 0x07, 0x80, 0x1C, 0x3F, 0xFF, 0xFF, 0xEF, 0xFE, 0x01, 0xC3, 0xFE, 0x3F, 0xE0, 0xFC, - 0x03, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0xF0, 0x18, 0x01, 0xC0, 0x0E, - 0x60, 0x7F, 0x03, 0xF8, 0x3F, 0x03, 0xE2, 0x1F, 0xF0, 0x3F, 0x83, 0xF8, 0x3F, 0x01, 0xF0, 0x7B, - 0x87, 0x9C, 0x38, 0xE1, 0xC7, 0x1E, 0x39, 0xE1, 0xFE, 0x0F, 0xE0, 0x38, 0x00, 0x01, 0x80, 0x03, - 0xC0, 0x03, 0xC0, 0x07, 0xE0, 0x1F, 0xF8, 0x3F, 0xFC, 0x3B, 0xDC, 0x73, 0xDE, 0x73, 0xCE, 0x73, - 0xCE, 0xE3, 0xCF, 0xE3, 0xC7, 0xE3, 0xC7, 0xE3, 0xC7, 0x0F, 0xE0, 0x0F, 0xFE, 0x07, 0xFF, 0x83, - 0x81, 0xE1, 0xC0, 0x70, 0xE0, 0x38, 0x70, 0x1C, 0x38, 0x0E, 0x1C, 0x07, 0x3F, 0xFF, 0xBF, 0xFF, - 0x8F, 0xFF, 0x01, 0xC0, 0x03, 0xFE, 0x03, 0xFF, 0x00, 0xFF, 0x80, 0x1C, 0x00, 0x0E, 0x00, 0x07, - 0x00, 0x00, 0x06, 0x60, 0x06, 0xE0, 0x07, 0xE0, 0x0F, 0xF0, 0x1F, 0xF8, 0x3E, 0xFC, 0x7E, 0xFE, - 0x76, 0xEE, 0x76, 0xEE, 0xE6, 0xEF, 0xE6, 0x67, 0xE0, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, - 0x38, 0x00, 0x1F, 0x00, 0xFF, 0xFE, 0xFF, 0xFE, 0x3B, 0x01, 0xD8, 0x3F, 0xC1, 0xFF, 0x8F, 0xFF, - 0x70, 0x7B, 0x81, 0xDC, 0x07, 0xE0, 0x3F, 0x03, 0xBF, 0xFD, 0xFF, 0x8F, 0xFF, 0x70, 0x3F, 0x80, - 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x3F, 0xFF, 0xEF, 0xFE, 0x1D, 0x80, 0xEC, 0x02, 0x60, 0x01, - 0x80, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, - 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, - 0x00, 0xF0, 0x00, 0x0C, 0x00, 0x00, 0x80, 0x00, 0xE0, 0x00, 0xF8, 0x00, 0xFE, 0x00, 0xFF, 0x80, - 0xEF, 0xE0, 0xF7, 0x78, 0xF3, 0x9E, 0xF1, 0xC7, 0x30, 0xE1, 0x80, 0x70, 0x00, 0x38, 0x00, 0x1C, - 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x00, 0x60, - 0x00, 0x0E, 0x00, 0x01, 0xE0, 0x00, 0x1E, 0x00, 0x01, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0xE7, 0xFF, - 0xFE, 0xFF, 0xFF, 0xC0, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x70, 0x00, - 0x1C, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, - 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x18, 0x70, 0xDE, 0x38, 0xE7, 0x9C, 0xF1, - 0xEE, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, 0x07, 0xF0, 0x01, 0xF0, 0x00, 0x70, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x67, 0x80, 0x1E, 0x7F, 0xFF, 0xE1, 0xFF, 0xF8, 0x07, 0xFE, 0x00, + 0x04, 0x01, 0xC1, 0x39, 0x3F, 0xE7, 0xFC, 0x7E, 0x0F, 0xE7, 0xFE, 0x6E, 0xC1, 0xC0, 0x38, 0x00, + 0x00, 0x00, 0x08, 0x03, 0x82, 0x72, 0x7F, 0xCF, 0xF8, 0xFC, 0x1F, 0xCF, 0xFC, 0xDD, 0x83, 0x80, + 0x70, 0x00, 0x00, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x0C, 0x70, 0x1C, 0xF8, 0x38, 0xF8, 0x70, 0x70, + 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x07, 0x0E, 0x0E, 0x1F, 0x1C, + 0x1F, 0x1C, 0x1F, 0x38, 0x0E, 0x70, 0x00, 0x60, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xFF, 0xC6, 0x67, + 0xFF, 0x00, 0xFE, 0x00, 0x38, 0x03, 0xFC, 0x01, 0xFF, 0xF8, 0x3F, 0xFF, 0xC7, 0xE0, 0x7F, 0x70, + 0x00, 0x60, 0x06, 0x06, 0xD9, 0xFF, 0x1F, 0xCF, 0xFF, 0xFF, 0xDF, 0xF9, 0xFE, 0x7F, 0xC5, 0xD8, + 0x18, 0x00, 0x63, 0x0C, 0x6E, 0x39, 0xCE, 0xE3, 0x9C, 0xEE, 0x71, 0xCE, 0xE7, 0x18, 0xEE, 0x71, + 0x8C, 0xC7, 0x18, 0xCC, 0x61, 0x8C, 0x3E, 0x1F, 0xCF, 0x3B, 0x8E, 0xE1, 0xF8, 0x7E, 0x3B, 0x8E, + 0x7F, 0x9F, 0xC0, 0xC0, 0x5F, 0xFF, 0xFF, 0xFF, 0x80, 0x0F, 0x03, 0xC1, 0xF0, 0xFC, 0x77, 0x1D, + 0xCF, 0xFB, 0xFF, 0xFF, 0x81, 0xC0, 0x00, 0x7F, 0x3F, 0x98, 0x1F, 0xCF, 0xF7, 0xBC, 0x0F, 0xC7, + 0xFF, 0xBF, 0x86, 0x00, 0x3E, 0x3F, 0xBC, 0xFD, 0xCF, 0xF7, 0xFF, 0x8F, 0xC7, 0xFF, 0xBF, 0x87, + 0x00, 0x7F, 0xBF, 0xC0, 0xE0, 0xE0, 0x70, 0x70, 0x38, 0x38, 0x38, 0x1C, 0x00, 0x00, 0x7E, 0x7F, + 0xB8, 0xFC, 0x77, 0xF3, 0xFB, 0x8F, 0xC7, 0xFF, 0xBF, 0x87, 0x00, 0x7E, 0x7F, 0xB8, 0xFC, 0x7E, + 0x7F, 0xFC, 0xFF, 0xC7, 0xFF, 0x3F, 0x06, 0x00, 0x1C, 0x0E, 0x1F, 0xDF, 0xEF, 0xF0, 0xE0, 0x70, + 0x7F, 0x7F, 0xBF, 0xC0, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0x39, 0x9C, 0xE6, 0x73, 0x9C, 0xE3, + 0x9C, 0xE3, 0x80, 0x63, 0x9C, 0x73, 0x9C, 0xE7, 0x39, 0xCC, 0xE6, 0x00, 0xCE, 0x7F, 0xBF, 0xDC, + 0x7E, 0x3F, 0x1F, 0x8F, 0xC7, 0x00, 0x00, 0x3E, 0x1F, 0xCF, 0x3B, 0x8E, 0xE1, 0xF8, 0x7E, 0x3B, + 0x8E, 0x7F, 0x9F, 0xC0, 0xC0, 0x7F, 0xFF, 0x73, 0x9C, 0xE7, 0x39, 0xC0, 0x7E, 0x7F, 0xB8, 0xF8, + 0x70, 0x70, 0x70, 0xF0, 0xF0, 0xFF, 0xFF, 0xC0, 0x00, 0x3E, 0x3F, 0xB8, 0xE0, 0x71, 0xF0, 0xF8, + 0x0F, 0xC7, 0xFF, 0xBF, 0x87, 0x00, 0x0F, 0x03, 0xC1, 0xF0, 0xFC, 0x77, 0x1D, 0xCF, 0xFB, 0xFF, + 0xFF, 0x81, 0xC0, 0x00, 0x7F, 0x3F, 0x98, 0x1F, 0xCF, 0xF7, 0xBC, 0x0F, 0xC7, 0xFF, 0xBF, 0x86, + 0x00, 0x3E, 0x3F, 0xBC, 0xFD, 0xCF, 0xF7, 0xFF, 0x8F, 0xC7, 0xFF, 0xBF, 0x87, 0x00, 0x7F, 0xBF, + 0xC0, 0xE0, 0xE0, 0x70, 0x70, 0x38, 0x38, 0x38, 0x1C, 0x00, 0x00, 0x7E, 0x7F, 0xB8, 0xFC, 0x77, + 0xF3, 0xFB, 0x8F, 0xC7, 0xFF, 0xBF, 0x87, 0x00, 0x7E, 0x7F, 0xB8, 0xFC, 0x7E, 0x7F, 0xFC, 0xFF, + 0xC7, 0xFF, 0x3F, 0x06, 0x00, 0x1C, 0x0E, 0x1F, 0xDF, 0xEF, 0xF0, 0xE0, 0x70, 0x7F, 0x7F, 0xBF, + 0xC0, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0x39, 0x9C, 0xE6, 0x73, 0x9C, 0xE3, 0x9C, 0xE3, 0x80, + 0x63, 0x9C, 0x73, 0x9C, 0xE7, 0x39, 0xCC, 0xE6, 0x00, 0x1E, 0x3F, 0x9D, 0xE3, 0xF7, 0xFB, 0x9D, + 0xFE, 0xFF, 0x18, 0x00, 0x1E, 0x0F, 0xC7, 0xB9, 0xFE, 0x7F, 0xDC, 0x07, 0xF8, 0xFE, 0x0C, 0x00, + 0x1E, 0x0F, 0xE7, 0xF9, 0xC7, 0x61, 0xDC, 0x77, 0xF8, 0xFC, 0x0C, 0x00, 0x61, 0x9C, 0xE3, 0xF0, + 0xF8, 0x1E, 0x0F, 0xC7, 0xF1, 0xCE, 0x00, 0x00, 0x3E, 0x3F, 0xCF, 0x78, 0x0E, 0xFF, 0xBF, 0xEF, + 0xFB, 0x8E, 0x7F, 0x9F, 0xC1, 0xC0, 0xE0, 0x70, 0x3B, 0x9F, 0xEF, 0xF7, 0x1F, 0x8F, 0xC7, 0xE3, + 0xF1, 0xC0, 0x00, 0xE0, 0x70, 0x39, 0xDD, 0xEF, 0xE7, 0xE3, 0xF1, 0xFC, 0xEE, 0x73, 0x80, 0x00, + 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0xCC, 0x77, 0xFF, 0xFF, 0xFF, 0xCE, 0x7E, 0x73, 0xF3, 0x9F, 0x9C, + 0xFC, 0xE7, 0x00, 0x00, 0xCE, 0x7F, 0xBF, 0xDC, 0x7E, 0x3F, 0x1F, 0x8F, 0xC7, 0x00, 0x00, 0xEE, + 0x7F, 0xBF, 0xFC, 0x7E, 0x3F, 0x1F, 0xFF, 0xFE, 0xEC, 0x70, 0x38, 0x00, 0x1E, 0x3F, 0x9D, 0xEF, + 0x83, 0xF1, 0x7D, 0xFE, 0xFE, 0x1C, 0x00, 0x01, 0xCF, 0xFF, 0xF9, 0xC7, 0x1C, 0x7C, 0xF1, 0x80, + 0x0F, 0x80, 0x3F, 0xC0, 0x7D, 0xE0, 0x70, 0xE0, 0x70, 0x70, 0x70, 0x00, 0x73, 0xFE, 0x73, 0xFF, + 0x73, 0xFE, 0x73, 0x80, 0x7B, 0x80, 0x3F, 0xFC, 0x1F, 0xFE, 0x07, 0xFC, 0x03, 0x80, 0x03, 0x80, + 0x03, 0x80, 0x03, 0xFE, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x64, 0x00, 0x6C, 0x03, 0xEC, 0x0F, 0xF8, + 0x1F, 0xFC, 0x3C, 0xDE, 0x79, 0x9F, 0x71, 0xB7, 0xF1, 0xB7, 0xE3, 0x30, 0xE3, 0x60, 0xE3, 0x60, + 0xE2, 0x60, 0xE6, 0xC0, 0xE6, 0xC0, 0x76, 0xC7, 0x7C, 0x87, 0x7D, 0x8F, 0x3F, 0xBE, 0x1F, 0xFC, + 0x1F, 0xF8, 0x1B, 0xC0, 0x33, 0x00, 0x12, 0x00, 0x03, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x1E, + 0x78, 0x0F, 0x70, 0x07, 0xF0, 0x07, 0xE0, 0x00, 0xE1, 0x9C, 0xE1, 0xFE, 0xE1, 0xFC, 0xE1, 0xE0, + 0xE1, 0xC0, 0x71, 0xC7, 0x71, 0xC7, 0x79, 0xCE, 0x3F, 0xFE, 0x1F, 0xFC, 0x0F, 0xF8, 0x01, 0xC0, + 0x0F, 0xFC, 0x3F, 0xFC, 0x7F, 0xF8, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, + 0x00, 0x3F, 0xF8, 0x7F, 0xF0, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x3F, 0xE0, 0x7F, 0xC0, 0x1C, 0x00, + 0x38, 0x00, 0x70, 0x00, 0x03, 0x80, 0x7F, 0x07, 0xFC, 0x78, 0xF3, 0x83, 0x9C, 0x1C, 0xE0, 0x1F, + 0xF8, 0xFF, 0xC7, 0xFE, 0x0E, 0x01, 0xFF, 0x8F, 0xFC, 0x1C, 0x00, 0xE0, 0x07, 0x07, 0x70, 0x3F, + 0xFF, 0xFF, 0xFC, 0x00, 0x38, 0x00, 0x07, 0x01, 0x9E, 0xFE, 0x7F, 0xFF, 0xEF, 0xFF, 0xFD, 0xE1, + 0xE3, 0xFC, 0x38, 0x3F, 0x07, 0x07, 0xE1, 0xE0, 0xFC, 0x3C, 0x1F, 0x8F, 0x83, 0xF1, 0xF0, 0x7E, + 0x3E, 0x0F, 0xCF, 0xC1, 0xF9, 0xF8, 0x3F, 0x77, 0x07, 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x01, + 0x87, 0x80, 0x70, 0xF8, 0x0E, 0x1F, 0x01, 0xC3, 0xF0, 0x38, 0x7E, 0x07, 0x0E, 0xE0, 0xE7, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xE3, 0xCE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFE, 0x70, 0x7F, 0x0E, + 0x07, 0xE1, 0xC0, 0xFC, 0x38, 0x0F, 0x87, 0x00, 0xF0, 0xE0, 0x1E, 0x00, 0xFE, 0x00, 0x07, 0xFE, + 0x00, 0x3F, 0xFC, 0x71, 0xC1, 0xE3, 0x8E, 0x07, 0x9C, 0x70, 0x1F, 0xFF, 0x80, 0xFF, 0xFC, 0x07, + 0xFF, 0xE0, 0x79, 0xC7, 0x07, 0x8E, 0x3F, 0xFC, 0x71, 0xFF, 0xC3, 0x8F, 0xF0, 0x1C, 0x70, 0x00, + 0xE3, 0x80, 0x07, 0x1C, 0x00, 0x38, 0xE0, 0x01, 0xE7, 0x00, 0x07, 0xF8, 0x00, 0x3E, 0x00, 0x00, + 0x20, 0xFE, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x3F, 0xFC, 0x00, 0x1C, 0x1E, 0x00, 0x0E, 0x03, 0x80, + 0x07, 0x01, 0xC3, 0xE3, 0x80, 0xE7, 0xF9, 0xC0, 0x73, 0xFE, 0xE0, 0x7B, 0x83, 0xFF, 0xF9, 0xC0, + 0x3F, 0xF8, 0xF0, 0x1F, 0xF8, 0x3F, 0x8E, 0x1C, 0x0F, 0xF7, 0x0F, 0x00, 0xFB, 0x83, 0x80, 0x0F, + 0xC1, 0xE7, 0x07, 0xE0, 0x73, 0x87, 0xF0, 0x3D, 0xFF, 0xB8, 0x0E, 0x7F, 0x80, 0x00, 0x07, 0x00, + 0x60, 0x18, 0x06, 0xE0, 0x3C, 0x0E, 0x70, 0x3C, 0x0E, 0x70, 0x7C, 0x0E, 0x70, 0x7C, 0x0E, 0x70, + 0x7E, 0x1C, 0x38, 0x7E, 0x1C, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFE, 0x38, 0xE7, 0x38, 0x1C, 0xE7, + 0x38, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFE, 0x1F, 0xC3, 0xF0, 0x0F, 0x83, 0xF0, 0x0F, 0x81, 0xF0, + 0x0F, 0x81, 0xE0, 0x07, 0x81, 0xE0, 0x07, 0x01, 0xE0, 0xFF, 0xC3, 0x7F, 0xF3, 0xBF, 0xFD, 0xDC, + 0x0E, 0xEE, 0x67, 0x77, 0x73, 0xBB, 0xB9, 0xDD, 0xDC, 0xEE, 0xEE, 0x77, 0x77, 0x3B, 0xBB, 0x81, + 0xDD, 0xC1, 0xEE, 0xFF, 0xF7, 0x7F, 0xF0, 0x00, 0x30, 0x00, 0x70, 0x0F, 0xF0, 0x1F, 0xE0, 0x03, + 0x83, 0xE7, 0x0F, 0xEE, 0x3F, 0xFC, 0x70, 0xF9, 0xC0, 0xF3, 0x80, 0xE7, 0x01, 0xCE, 0x03, 0x9C, + 0x07, 0x38, 0x1E, 0x78, 0x3C, 0x78, 0xF8, 0xFF, 0xF0, 0xFE, 0xE0, 0x30, 0x00, 0x00, 0x00, 0x00, + 0x3F, 0xFE, 0x7F, 0xFC, 0x00, 0x00, 0x00, 0xF0, 0x03, 0xFC, 0x07, 0xFE, 0x0F, 0x0E, 0x0E, 0x00, + 0x1C, 0x00, 0x1C, 0x00, 0x7F, 0xF8, 0xFF, 0xF8, 0x38, 0x00, 0x7F, 0xE0, 0x7F, 0xE0, 0xFF, 0xE0, + 0x1C, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x0F, 0x8E, 0x07, 0xFE, 0x03, 0xFC, 0x00, 0x70, 0x18, 0x03, + 0x87, 0x01, 0xE1, 0xC0, 0xF0, 0x70, 0x78, 0x1C, 0x3C, 0x07, 0x1E, 0x01, 0xCF, 0x00, 0x77, 0x80, + 0xFF, 0xFF, 0xBF, 0xFF, 0xEF, 0xFF, 0xF8, 0x7F, 0xC0, 0x1E, 0x70, 0x07, 0x0E, 0x01, 0xC3, 0xC0, + 0x70, 0x78, 0x1C, 0x0E, 0x07, 0x01, 0xC1, 0xC0, 0x78, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, + 0x07, 0x80, 0x1E, 0x00, 0x7F, 0x01, 0xFC, 0x1F, 0xC0, 0xFE, 0x83, 0xFF, 0x01, 0xF8, 0x1F, 0x80, + 0xFE, 0x03, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, + 0x80, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x07, 0xFF, 0xC0, 0x00, 0x03, 0xFF, 0xF0, 0x00, 0x01, + 0xF8, 0x7C, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x0E, 0x03, 0x81, 0xF0, 0x07, 0x01, 0xC3, 0xFC, + 0x03, 0x80, 0xE3, 0xFF, 0x01, 0xC0, 0x39, 0xC3, 0xC0, 0xE0, 0x1D, 0xC0, 0xE0, 0x70, 0x0E, 0xE0, + 0x70, 0x38, 0x07, 0x70, 0x1C, 0xFC, 0x07, 0x38, 0x0E, 0xFE, 0x03, 0x9C, 0x07, 0xFF, 0x01, 0xCE, + 0x07, 0x73, 0xC1, 0xE7, 0x03, 0xB9, 0xF1, 0xE3, 0xC3, 0xDF, 0xFF, 0xE1, 0xFF, 0xC7, 0xCF, 0xE0, + 0xFF, 0xC0, 0xC1, 0xC0, 0x73, 0x80, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x80, 0x3F, 0xF0, 0x3F, + 0xF8, 0x38, 0x3C, 0x7F, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x38, 0x0E, 0xFF, 0xFF, 0xFF, 0xFF, 0x38, + 0x1C, 0x38, 0x7C, 0x3F, 0xF8, 0x3F, 0xF0, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, + 0x00, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x01, 0xFF, 0x01, 0xFF, 0xC1, 0xEE, 0xF1, 0xE7, 0x3C, + 0xE3, 0x8E, 0xF1, 0xC7, 0x70, 0xE0, 0x38, 0x70, 0x1C, 0x3F, 0xEE, 0x1F, 0xF7, 0x0F, 0xFB, 0x87, + 0x1C, 0xE3, 0x8E, 0x71, 0xC7, 0x3C, 0xE7, 0x8F, 0xFF, 0x83, 0xFF, 0x80, 0xFF, 0x80, 0x0E, 0x00, + 0x07, 0x00, 0x03, 0x00, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x00, 0x7C, 0x00, 0x77, 0x00, 0x3B, + 0x80, 0x1D, 0xC0, 0x1C, 0x70, 0x7F, 0xFF, 0x3F, 0xFF, 0x87, 0x07, 0x03, 0x83, 0x87, 0xFF, 0xF3, + 0xFF, 0xF9, 0xFF, 0xFC, 0xF0, 0x1E, 0x70, 0x07, 0x38, 0x03, 0xBC, 0x00, 0xE0, 0x07, 0x80, 0x7F, + 0x83, 0xFF, 0x1E, 0x1C, 0x70, 0x70, 0xC1, 0xC0, 0x07, 0x3F, 0xFF, 0xFF, 0xFC, 0x00, 0x0F, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFD, 0xC0, 0x07, 0x03, 0x9C, 0x0E, 0x78, 0xF8, 0xFF, 0xC1, 0xFE, 0x01, 0xC0, + 0x01, 0xC0, 0x01, 0xC0, 0x03, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3D, 0xDE, 0x79, 0xCF, 0x71, 0xC7, + 0xF1, 0xC7, 0xE1, 0xC0, 0xE1, 0xC0, 0xE1, 0xC0, 0xE1, 0xC0, 0xE1, 0xC0, 0xE1, 0xC0, 0x71, 0xC7, + 0x71, 0xC7, 0x79, 0xCF, 0x3F, 0xFE, 0x1F, 0xFC, 0x0F, 0xF8, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x07, 0x80, + 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, + 0x78, 0x00, 0x7F, 0xFB, 0xFF, 0xFF, 0xFE, 0x07, 0x80, 0x1C, 0x3F, 0xFF, 0xFF, 0xEF, 0xFE, 0x01, + 0xC3, 0xFE, 0x3F, 0xE0, 0xFC, 0x03, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, + 0xF0, 0x18, 0x01, 0xC0, 0x0E, 0x60, 0x7F, 0x03, 0xF8, 0x3F, 0x03, 0xE2, 0x1F, 0xF0, 0x3F, 0x83, + 0xF8, 0x3F, 0x01, 0xF0, 0x7B, 0x87, 0x9C, 0x38, 0xE1, 0xC7, 0x1E, 0x39, 0xE1, 0xFE, 0x0F, 0xE0, + 0x38, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x03, 0xC0, 0x07, 0xE0, 0x1F, 0xF8, 0x3F, 0xFC, 0x3B, 0xDC, + 0x73, 0xDE, 0x73, 0xCE, 0x73, 0xCE, 0xE3, 0xCF, 0xE3, 0xC7, 0xE3, 0xC7, 0xE3, 0xC7, 0x0F, 0xE0, + 0x0F, 0xFE, 0x07, 0xFF, 0x83, 0x81, 0xE1, 0xC0, 0x70, 0xE0, 0x38, 0x70, 0x1C, 0x38, 0x0E, 0x1C, + 0x07, 0x3F, 0xFF, 0xBF, 0xFF, 0x8F, 0xFF, 0x01, 0xC0, 0x03, 0xFE, 0x03, 0xFF, 0x00, 0xFF, 0x80, + 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x00, 0x06, 0x60, 0x06, 0xE0, 0x07, 0xE0, 0x0F, 0xF0, 0x1F, + 0xF8, 0x3E, 0xFC, 0x7E, 0xFE, 0x76, 0xEE, 0x76, 0xEE, 0xE6, 0xEF, 0xE6, 0x67, 0xE0, 0x00, 0x70, + 0x00, 0x70, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1F, 0x00, 0xFF, 0xFE, 0xFF, 0xFE, 0x3B, 0x01, 0xD8, + 0x3F, 0xC1, 0xFF, 0x8F, 0xFF, 0x70, 0x7B, 0x81, 0xDC, 0x07, 0xE0, 0x3F, 0x03, 0xBF, 0xFD, 0xFF, + 0x8F, 0xFF, 0x70, 0x3F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x3F, 0xFF, 0xEF, 0xFE, 0x1D, + 0x80, 0xEC, 0x02, 0x60, 0x01, 0x80, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, + 0x78, 0x00, 0x1E, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, + 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0C, 0x00, 0x00, 0x80, 0x00, 0xE0, 0x00, 0xF8, + 0x00, 0xFE, 0x00, 0xFF, 0x80, 0xEF, 0xE0, 0xF7, 0x78, 0xF3, 0x9E, 0xF1, 0xC7, 0x30, 0xE1, 0x80, + 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, + 0x00, 0x70, 0x00, 0x00, 0x60, 0x00, 0x0E, 0x00, 0x01, 0xE0, 0x00, 0x1E, 0x00, 0x01, 0xE0, 0x00, + 0x0E, 0x00, 0x00, 0xE7, 0xFF, 0xFE, 0xFF, 0xFF, 0xC0, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x07, 0x00, + 0x01, 0xC0, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x70, 0x00, + 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x18, 0x70, + 0xDE, 0x38, 0xE7, 0x9C, 0xF1, 0xEE, 0xF0, 0x7F, 0xF0, 0x1F, 0xF0, 0x07, 0xF0, 0x01, 0xF0, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x01, 0x80, 0x0C, 0x00, 0x1E, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x1E, + 0x00, 0x0F, 0x01, 0xE0, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0xF1, 0xE0, 0x00, 0x03, 0xDF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xC0, 0x00, 0x07, 0x8F, 0x00, 0x00, 0x78, 0x3C, 0x00, 0x07, 0x80, + 0xF0, 0x00, 0x78, 0x03, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x78, 0x00, 0x30, 0x01, 0x80, 0x00, 0x00, + 0x00, 0xE0, 0x00, 0xF8, 0x00, 0xFE, 0x00, 0xFF, 0x80, 0xFF, 0xE0, 0xF7, 0x78, 0xF3, 0x9E, 0xF1, + 0xC7, 0x30, 0xE1, 0x80, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, + 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x06, 0x1C, 0x37, 0x8E, 0x39, 0xE7, 0x3C, 0x7B, + 0xBC, 0x1F, 0xFC, 0x07, 0xFC, 0x01, 0xFC, 0x00, 0x7C, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x7F, 0xF0, + 0xFF, 0xF1, 0xFF, 0xC3, 0xE0, 0x07, 0xE0, 0x0D, 0xE0, 0x19, 0xE0, 0x31, 0xE0, 0x61, 0xE0, 0xC1, + 0xE1, 0x81, 0xE3, 0x01, 0xE0, 0x01, 0xE0, 0x01, 0x80, 0x00, 0x00, 0x1F, 0xFC, 0x3F, 0xFC, 0x7F, + 0xF8, 0x03, 0xF0, 0x0F, 0xE0, 0x3D, 0xC0, 0xF3, 0x83, 0xC7, 0x0F, 0x0E, 0x3C, 0x1C, 0xF0, 0x3B, + 0xC0, 0x77, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xE0, 0x01, 0xE0, 0x31, 0xE0, 0x71, + 0xE0, 0xE1, 0xE1, 0xC1, 0xE3, 0x81, 0xE7, 0x01, 0xEE, 0x01, 0xFC, 0x01, 0xF8, 0x01, 0xF1, 0xFF, + 0xE3, 0xFF, 0xC0, 0x00, 0x0C, 0x00, 0x3D, 0x80, 0xF3, 0x03, 0xC6, 0x0F, 0x0C, 0x3C, 0x18, 0xF0, + 0x33, 0xC0, 0x6F, 0x00, 0xDC, 0x01, 0xF0, 0x03, 0xC0, 0x07, 0xFF, 0x8F, 0xFF, 0x00, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x1F, 0x80, 0x30, 0x00, 0xE0, 0x3C, 0x00, 0x70, 0x3C, 0x00, + 0x1C, 0x3C, 0x00, 0x0E, 0x3C, 0x00, 0x0E, 0x3C, 0x00, 0x07, 0x3C, 0x00, 0x0F, 0xBF, 0xFF, 0xFF, + 0x9F, 0xFF, 0xFF, 0x87, 0x80, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x07, 0x80, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1F, 0x80, 0x00, + 0x1F, 0x80, 0x00, 0x1E, 0x00, 0x30, 0x0E, 0x00, 0x1C, 0x07, 0x00, 0x0F, 0x03, 0x80, 0x03, 0xC1, + 0xC0, 0x00, 0xF0, 0xF0, 0x00, 0x3C, 0x3C, 0x00, 0x0F, 0x0F, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xE0, + 0x00, 0x00, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xE0, 0x00, + 0x00, 0xE0, 0x00, 0x00, 0x60, 0x00, 0x04, 0x00, 0x00, 0x60, 0x00, 0x07, 0x00, 0x00, 0x78, 0x00, + 0x07, 0xFF, 0xFF, 0x7F, 0xFF, 0xFD, 0xFF, 0xFF, 0xC7, 0x80, 0x06, 0x1C, 0x00, 0x30, 0x60, 0x01, + 0x81, 0x00, 0x0C, 0x00, 0x00, 0x60, 0x00, 0x03, 0x00, 0x00, 0x18, 0x00, 0x00, 0xC0, 0x00, 0x06, + 0x00, 0x00, 0x30, 0x00, 0x01, 0x80, 0x00, 0x0C, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xF0, + 0x00, 0x07, 0xCF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFB, 0xFF, 0xFF, 0xDC, 0x00, 0x7C, 0xE0, 0x03, 0xC7, + 0x00, 0x1C, 0x38, 0x00, 0xC1, 0xC0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x1C, + 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x18, 0x00, 0xC0, 0x06, 0x00, + 0x30, 0x01, 0x80, 0x0C, 0x00, 0x61, 0xC3, 0x1C, 0x19, 0xC0, 0xDC, 0x07, 0xFF, 0xFF, 0xFF, 0xBC, + 0x00, 0xF0, 0x03, 0xC0, 0x0C, 0x00, 0x40, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x0C, 0x1C, 0x00, 0x70, 0xE0, 0x03, 0xC7, 0x00, 0x1F, + 0x3F, 0xFF, 0xFD, 0xFF, 0xFF, 0xE0, 0x00, 0x3E, 0x00, 0x01, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x7F, 0xF0, 0x7F, 0xF8, 0x3F, 0xFC, 0x00, 0x06, 0x00, 0x03, 0x00, 0x01, 0x80, + 0x00, 0xC0, 0x00, 0x60, 0x00, 0x30, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x03, 0x00, 0x01, + 0x80, 0x0F, 0xFC, 0x03, 0xFC, 0x00, 0xFC, 0x00, 0x3C, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x01, 0x80, 0x00, 0x0C, 0x00, 0x00, 0x60, 0x00, 0x03, 0x00, 0x00, 0x18, 0x30, 0x00, + 0xC3, 0x80, 0x06, 0x3C, 0x00, 0x33, 0xE0, 0x01, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x00, + 0x1E, 0x00, 0x00, 0x70, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x07, 0xFE, + 0x00, 0x78, 0xF0, 0x07, 0x01, 0xC0, 0x70, 0x07, 0x03, 0x00, 0x38, 0x18, 0x00, 0xC1, 0xC0, 0x06, + 0x0E, 0x00, 0x32, 0x72, 0x00, 0x3B, 0xB8, 0x00, 0xFF, 0x80, 0x03, 0xF8, 0x00, 0x0F, 0x80, 0x00, + 0x38, 0x00, 0x00, 0x80, 0x00, 0x0F, 0xE0, 0x00, 0xFF, 0xC0, 0x0F, 0x9F, 0x00, 0xF0, 0x1C, 0x07, + 0x00, 0x60, 0x70, 0x03, 0x83, 0x80, 0x1C, 0x1C, 0x00, 0x60, 0xC0, 0x03, 0x00, 0x01, 0x99, 0x80, + 0x0E, 0xDC, 0x00, 0x7F, 0xE0, 0x01, 0xFE, 0x00, 0x07, 0xE0, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x0C, 0x00, 0x03, 0x80, 0x00, 0xF0, 0x00, 0x3E, 0x00, 0x0F, 0xF8, 0x01, 0xFF, 0xC0, + 0x1F, 0x7C, 0x01, 0xE3, 0xC0, 0x1C, 0x38, 0x01, 0x83, 0x80, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, + 0xF8, 0x00, 0x1B, 0x00, 0x07, 0x70, 0x00, 0xEE, 0x00, 0x1C, 0xE0, 0x07, 0x1E, 0x01, 0xC1, 0xF9, + 0xF8, 0x1F, 0xFC, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x01, 0xC0, 0x00, 0x3C, 0x00, + 0x07, 0xC0, 0x07, 0xFC, 0x03, 0xFF, 0x80, 0xFB, 0xE0, 0x3C, 0x78, 0x0F, 0x0E, 0x01, 0xC1, 0x80, + 0x38, 0x00, 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x00, 0x1B, 0x00, 0x07, 0x70, 0x00, 0xEE, 0x00, + 0x1C, 0xE0, 0x07, 0x1E, 0x01, 0xC1, 0xF9, 0xF8, 0x1F, 0xFC, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x80, 0x00, 0x38, 0x00, 0x03, 0x80, 0x00, 0x39, 0xFF, 0xFF, 0xBF, 0xFF, 0xF0, 0x00, 0x1C, + 0x00, 0x07, 0x01, 0xC1, 0xC0, 0x78, 0x30, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFB, 0xC0, 0x00, 0x3C, 0x00, 0x03, 0xC0, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xC0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xC0, 0x00, + 0x3C, 0x00, 0x03, 0xC1, 0x80, 0x38, 0x38, 0x00, 0x03, 0x80, 0x00, 0x39, 0xFF, 0xFF, 0xBF, 0xFF, + 0xF0, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, + 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xE0, 0x00, 0x7F, 0xFF, 0xDF, 0xFF, 0xFF, 0x80, 0x00, + 0xF0, 0x00, 0x0F, 0xFF, 0xFC, 0xFF, 0xFF, 0x8F, 0xFF, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, + 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x0E, 0x00, 0x01, 0xE0, 0x00, 0x1E, 0x00, + 0x01, 0xE1, 0xFF, 0xFE, 0x3F, 0xFF, 0xE0, 0x00, 0x1E, 0x00, 0x01, 0xDF, 0xFF, 0xF3, 0xFF, 0xFC, + 0x7F, 0xFF, 0x00, 0x01, 0xC0, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x0C, 0x00, 0x1E, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x1E, 0x00, 0x0F, 0x01, 0xE0, 0x00, - 0x3C, 0x1E, 0x00, 0x00, 0xF1, 0xE0, 0x00, 0x03, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, - 0xC0, 0x00, 0x07, 0x8F, 0x00, 0x00, 0x78, 0x3C, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x78, 0x03, 0xC0, - 0x07, 0x80, 0x0F, 0x00, 0x78, 0x00, 0x30, 0x01, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xF8, 0x00, - 0xFE, 0x00, 0xFF, 0x80, 0xFF, 0xE0, 0xF7, 0x78, 0xF3, 0x9E, 0xF1, 0xC7, 0x30, 0xE1, 0x80, 0x70, - 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, - 0x70, 0x00, 0x38, 0x06, 0x1C, 0x37, 0x8E, 0x39, 0xE7, 0x3C, 0x7B, 0xBC, 0x1F, 0xFC, 0x07, 0xFC, - 0x01, 0xFC, 0x00, 0x7C, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x7F, 0xF0, 0xFF, 0xF1, 0xFF, 0xC3, 0xE0, - 0x07, 0xE0, 0x0D, 0xE0, 0x19, 0xE0, 0x31, 0xE0, 0x61, 0xE0, 0xC1, 0xE1, 0x81, 0xE3, 0x01, 0xE0, - 0x01, 0xE0, 0x01, 0x80, 0x00, 0x00, 0x1F, 0xFC, 0x3F, 0xFC, 0x7F, 0xF8, 0x03, 0xF0, 0x0F, 0xE0, - 0x3D, 0xC0, 0xF3, 0x83, 0xC7, 0x0F, 0x0E, 0x3C, 0x1C, 0xF0, 0x3B, 0xC0, 0x77, 0x00, 0x0C, 0x00, - 0x00, 0x00, 0x00, 0x20, 0x00, 0xE0, 0x01, 0xE0, 0x31, 0xE0, 0x71, 0xE0, 0xE1, 0xE1, 0xC1, 0xE3, - 0x81, 0xE7, 0x01, 0xEE, 0x01, 0xFC, 0x01, 0xF8, 0x01, 0xF1, 0xFF, 0xE3, 0xFF, 0xC0, 0x00, 0x0C, - 0x00, 0x3D, 0x80, 0xF3, 0x03, 0xC6, 0x0F, 0x0C, 0x3C, 0x18, 0xF0, 0x33, 0xC0, 0x6F, 0x00, 0xDC, - 0x01, 0xF0, 0x03, 0xC0, 0x07, 0xFF, 0x8F, 0xFF, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x3E, 0x00, - 0x00, 0x1F, 0x80, 0x30, 0x00, 0xE0, 0x3C, 0x00, 0x70, 0x3C, 0x00, 0x1C, 0x3C, 0x00, 0x0E, 0x3C, - 0x00, 0x0E, 0x3C, 0x00, 0x07, 0x3C, 0x00, 0x0F, 0xBF, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x87, 0x80, - 0x00, 0x01, 0xE0, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x07, 0x80, 0x00, 0x01, 0xE0, - 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1F, 0x80, 0x00, 0x1F, 0x80, 0x00, 0x1E, 0x00, - 0x30, 0x0E, 0x00, 0x1C, 0x07, 0x00, 0x0F, 0x03, 0x80, 0x03, 0xC1, 0xC0, 0x00, 0xF0, 0xF0, 0x00, - 0x3C, 0x3C, 0x00, 0x0F, 0x0F, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x01, - 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x60, - 0x00, 0x04, 0x00, 0x00, 0x60, 0x00, 0x07, 0x00, 0x00, 0x78, 0x00, 0x07, 0xFF, 0xFF, 0x7F, 0xFF, - 0xFD, 0xFF, 0xFF, 0xC7, 0x80, 0x06, 0x1C, 0x00, 0x30, 0x60, 0x01, 0x81, 0x00, 0x0C, 0x00, 0x00, - 0x60, 0x00, 0x03, 0x00, 0x00, 0x18, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x30, 0x00, 0x01, - 0x80, 0x00, 0x0C, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xF0, 0x00, 0x07, 0xCF, 0xFF, 0xFF, - 0x7F, 0xFF, 0xFB, 0xFF, 0xFF, 0xDC, 0x00, 0x7C, 0xE0, 0x03, 0xC7, 0x00, 0x1C, 0x38, 0x00, 0xC1, - 0xC0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x1C, 0x00, 0x00, 0xE0, 0x00, 0x07, - 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x18, 0x00, 0xC0, 0x06, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, - 0x61, 0xC3, 0x1C, 0x19, 0xC0, 0xDC, 0x07, 0xFF, 0xFF, 0xFF, 0xBC, 0x00, 0xF0, 0x03, 0xC0, 0x0C, - 0x00, 0x40, 0x00, 0x07, 0x00, 0x00, 0x38, 0x00, 0x01, 0xC0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, - 0x03, 0x80, 0x0C, 0x1C, 0x00, 0x70, 0xE0, 0x03, 0xC7, 0x00, 0x1F, 0x3F, 0xFF, 0xFD, 0xFF, 0xFF, - 0xE0, 0x00, 0x3E, 0x00, 0x01, 0xE0, 0x00, 0x0E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x7F, 0xF0, - 0x7F, 0xF8, 0x3F, 0xFC, 0x00, 0x06, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xC0, 0x00, 0x60, 0x00, - 0x30, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x03, 0x00, 0x01, 0x80, 0x0F, 0xFC, 0x03, 0xFC, - 0x00, 0xFC, 0x00, 0x3C, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x01, 0x80, 0x00, - 0x0C, 0x00, 0x00, 0x60, 0x00, 0x03, 0x00, 0x00, 0x18, 0x30, 0x00, 0xC3, 0x80, 0x06, 0x3C, 0x00, - 0x33, 0xE0, 0x01, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x00, 0x1E, 0x00, 0x00, 0x70, 0x00, - 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x07, 0xFE, 0x00, 0x78, 0xF0, 0x07, 0x01, - 0xC0, 0x70, 0x07, 0x03, 0x00, 0x38, 0x18, 0x00, 0xC1, 0xC0, 0x06, 0x0E, 0x00, 0x32, 0x72, 0x00, - 0x3B, 0xB8, 0x00, 0xFF, 0x80, 0x03, 0xF8, 0x00, 0x0F, 0x80, 0x00, 0x38, 0x00, 0x00, 0x80, 0x00, - 0x0F, 0xE0, 0x00, 0xFF, 0xC0, 0x0F, 0x9F, 0x00, 0xF0, 0x1C, 0x07, 0x00, 0x60, 0x70, 0x03, 0x83, - 0x80, 0x1C, 0x1C, 0x00, 0x60, 0xC0, 0x03, 0x00, 0x01, 0x99, 0x80, 0x0E, 0xDC, 0x00, 0x7F, 0xE0, - 0x01, 0xFE, 0x00, 0x07, 0xE0, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x03, - 0x80, 0x00, 0xF0, 0x00, 0x3E, 0x00, 0x0F, 0xF8, 0x01, 0xFF, 0xC0, 0x1F, 0x7C, 0x01, 0xE3, 0xC0, - 0x1C, 0x38, 0x01, 0x83, 0x80, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF8, 0x00, 0x1B, 0x00, 0x07, - 0x70, 0x00, 0xEE, 0x00, 0x1C, 0xE0, 0x07, 0x1E, 0x01, 0xC1, 0xF9, 0xF8, 0x1F, 0xFC, 0x00, 0xFF, - 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x01, 0xC0, 0x00, 0x3C, 0x00, 0x07, 0xC0, 0x07, 0xFC, 0x03, - 0xFF, 0x80, 0xFB, 0xE0, 0x3C, 0x78, 0x0F, 0x0E, 0x01, 0xC1, 0x80, 0x38, 0x00, 0x0E, 0x00, 0x01, - 0xC0, 0x00, 0x38, 0x00, 0x1B, 0x00, 0x07, 0x70, 0x00, 0xEE, 0x00, 0x1C, 0xE0, 0x07, 0x1E, 0x01, - 0xC1, 0xF9, 0xF8, 0x1F, 0xFC, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x38, 0x00, - 0x03, 0x80, 0x00, 0x39, 0xFF, 0xFF, 0xBF, 0xFF, 0xF0, 0x00, 0x1C, 0x00, 0x07, 0x01, 0xC1, 0xC0, - 0x78, 0x30, 0x1E, 0x00, 0x07, 0x80, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xC0, 0x00, 0x3C, 0x00, - 0x03, 0xC0, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x78, 0x00, 0x1E, - 0x00, 0x07, 0x80, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xC0, 0x00, 0x3C, 0x00, 0x03, 0xC1, 0x80, - 0x38, 0x38, 0x00, 0x03, 0x80, 0x00, 0x39, 0xFF, 0xFF, 0xBF, 0xFF, 0xF0, 0x00, 0x1C, 0x00, 0x07, - 0x00, 0x01, 0xC0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, - 0x80, 0x01, 0xE0, 0x00, 0x7F, 0xFF, 0xDF, 0xFF, 0xFF, 0x80, 0x00, 0xF0, 0x00, 0x0F, 0xFF, 0xFC, - 0xFF, 0xFF, 0x8F, 0xFF, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x60, 0x00, 0x0E, 0x00, 0x01, 0xE0, 0x00, 0x1E, 0x00, 0x01, 0xE1, 0xFF, 0xFE, 0x3F, - 0xFF, 0xE0, 0x00, 0x1E, 0x00, 0x01, 0xDF, 0xFF, 0xF3, 0xFF, 0xFC, 0x7F, 0xFF, 0x00, 0x01, 0xC0, - 0x00, 0x70, 0x00, 0x1C, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x0C, 0x00, 0x1E, 0x00, - 0xF0, 0x01, 0xE0, 0x03, 0xC0, 0x1E, 0x00, 0x0F, 0x01, 0xE0, 0x00, 0x3C, 0x1F, 0xFF, 0xFF, 0xF1, - 0xFF, 0xFF, 0xFF, 0xDE, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x7B, 0xFF, 0xFF, 0xFF, 0x8F, 0xFF, - 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, 0x80, 0xF0, 0x00, 0x78, 0x03, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x78, - 0x00, 0x30, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0xC0, 0x7C, 0x1F, 0xC7, 0xFD, 0xFF, - 0xC3, 0x80, 0x70, 0x7F, 0xCF, 0xF9, 0xFF, 0x07, 0x07, 0xFC, 0xFF, 0x9F, 0xF0, 0x70, 0x0E, 0x01, - 0xC0, 0x38, 0x00, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0xFF, 0x9F, 0xF0, 0x70, 0x0E, 0x0F, - 0xF9, 0xFF, 0x07, 0x00, 0xE0, 0x1C, 0x3F, 0xFB, 0xFE, 0x3F, 0x83, 0xE0, 0x38, 0x02, 0x00, 0xC0, - 0x00, 0x07, 0x06, 0x00, 0x38, 0x70, 0x01, 0xC7, 0x80, 0x0E, 0x7C, 0x00, 0x77, 0xFF, 0xFF, 0xBF, - 0xFF, 0xFC, 0xF8, 0x00, 0xE3, 0xC0, 0x07, 0x0E, 0x00, 0x38, 0x30, 0x01, 0x80, 0x00, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x40, 0xC0, 0x03, 0x06, 0x00, 0x1C, 0x30, 0x01, 0xF1, 0xFF, 0xFF, 0xCF, 0xFF, - 0xFE, 0x60, 0x01, 0xE3, 0x00, 0x0E, 0x18, 0x00, 0x60, 0xC0, 0x02, 0x06, 0x00, 0x00, 0x30, 0x00, - 0x20, 0x00, 0x01, 0xC0, 0x00, 0x0F, 0x80, 0x00, 0x7F, 0x00, 0x03, 0xCE, 0x00, 0x1E, 0x1C, 0x00, - 0xF0, 0x38, 0x07, 0x80, 0x70, 0x3C, 0x00, 0xE1, 0xFC, 0x0F, 0xCF, 0xF0, 0x3F, 0x81, 0xC0, 0xC0, - 0x07, 0x03, 0x00, 0x1C, 0x0C, 0x00, 0x70, 0x30, 0x01, 0xC0, 0xC0, 0x07, 0x03, 0x00, 0x1C, 0x0C, - 0x00, 0x7F, 0xF0, 0x01, 0xFF, 0xC0, 0x00, 0x20, 0x00, 0x01, 0xC0, 0x00, 0x0F, 0x80, 0x00, 0x7F, - 0x00, 0x03, 0xCE, 0x00, 0x1E, 0x1C, 0x00, 0xF0, 0x38, 0x07, 0x80, 0x70, 0x3C, 0x00, 0xE1, 0xFC, - 0x0F, 0xCF, 0xF0, 0x3F, 0x81, 0xC0, 0xC0, 0x07, 0x03, 0x00, 0x1C, 0x0C, 0x00, 0x7F, 0xF0, 0x01, - 0xFF, 0xC0, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF0, 0x01, 0xFF, 0xC0, 0x07, 0x03, 0x00, - 0x1C, 0x0C, 0x00, 0x7F, 0xF0, 0x01, 0xFF, 0xC0, 0x1F, 0x01, 0xFE, 0x0F, 0xF8, 0x43, 0xE0, 0x07, - 0x00, 0x38, 0x3C, 0xE3, 0xFF, 0x3F, 0xFB, 0xC3, 0xDC, 0x0E, 0xE0, 0x77, 0x03, 0xB8, 0x1D, 0xC0, - 0xEE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x06, 0x00, 0x00, 0x00, 0x1F, 0xCC, 0x7F, 0xF9, 0xFF, - 0xE7, 0x83, 0xCE, 0x0F, 0xB8, 0x3B, 0xF0, 0xE7, 0xE3, 0x8F, 0xCE, 0x1D, 0xF8, 0x73, 0xE1, 0xE7, - 0xC7, 0x8F, 0xFF, 0x3B, 0xF8, 0x61, 0xC0, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xF8, 0x00, 0x7C, 0x00, - 0x7E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x1C, 0xE0, 0x0E, 0x38, 0x07, 0x1C, 0x07, 0x0F, 0x03, 0x83, - 0x81, 0xC1, 0xC1, 0xC0, 0x70, 0xE0, 0x38, 0xF0, 0x1C, 0x70, 0x07, 0x3F, 0xFF, 0xBF, 0xFF, 0xC0, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, - 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, - 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x70, 0x7F, 0xFC, 0xFF, 0xF9, 0xFF, 0xF3, 0xC0, 0x03, 0xC0, - 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x80, 0x07, 0x80, 0x07, 0x80, 0x07, 0x80, 0x07, 0x00, 0x1E, 0x00, - 0x78, 0x00, 0xE0, 0x03, 0x80, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x07, 0xFF, 0xEF, 0xFF, - 0xDF, 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x60, 0x01, 0xC0, - 0x07, 0x00, 0x0C, 0x00, 0x38, 0x00, 0xE0, 0x01, 0x80, 0x06, 0x00, 0x1C, 0x00, 0x70, 0x00, 0xC0, - 0x03, 0x80, 0x0E, 0x00, 0x18, 0x00, 0x60, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x78, 0x00, 0xE0, - 0x01, 0xC0, 0x07, 0x80, 0x0E, 0x00, 0x1C, 0x00, 0x70, 0xF0, 0xE3, 0xF3, 0xC3, 0xE7, 0x01, 0xCE, - 0x01, 0xFC, 0x03, 0xF0, 0x07, 0xE0, 0x07, 0x80, 0x0F, 0x00, 0x0E, 0x00, 0x1F, 0x03, 0xC3, 0xFC, - 0x7F, 0x9F, 0xF7, 0xFF, 0xC3, 0xF8, 0x7E, 0x0F, 0x83, 0xF0, 0x3C, 0x1F, 0x83, 0xE0, 0xFE, 0x7F, - 0xCF, 0x7F, 0xDF, 0xF1, 0xFC, 0x3F, 0x03, 0x00, 0x60, 0x01, 0x81, 0xF0, 0xFC, 0x38, 0x1C, 0x07, - 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, - 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC1, 0xF0, 0xFC, 0x3E, 0x00, 0x03, 0xBC, 0xE2, 0x00, 0x00, 0x00, - 0x00, 0xEF, 0x38, 0x80, 0x00, 0x00, 0xF8, 0x77, 0xF1, 0xDD, 0xFE, 0x61, 0xF8, 0x03, 0xC3, 0xE1, - 0x9F, 0xC7, 0x7F, 0xFD, 0x8F, 0xE6, 0x1F, 0x00, 0x01, 0xC0, 0x1C, 0x01, 0x8F, 0xFF, 0xFF, 0xF7, - 0xFF, 0x07, 0x00, 0x60, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xC0, 0x18, 0x03, 0x80, 0x00, 0x30, 0x1F, - 0x0F, 0xF7, 0xF8, 0xFC, 0x0F, 0xE0, 0x3F, 0xC0, 0x7F, 0x01, 0xF0, 0x03, 0x00, 0x0F, 0xFF, 0xFF, - 0xF7, 0xFF, 0xE0, 0x0F, 0xC0, 0xFF, 0x01, 0xFE, 0x03, 0xF0, 0x7F, 0x3F, 0xEF, 0xF0, 0xF8, 0x0C, - 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF7, 0xFF, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, - 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0x07, 0xF0, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0x80, 0x00, - 0xF8, 0x03, 0xC0, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x00, 0x70, 0x1C, 0x00, 0x00, 0x07, 0x07, 0x00, - 0x18, 0x00, 0xF0, 0xE0, 0x03, 0x80, 0x0E, 0x38, 0x00, 0x70, 0x00, 0xE7, 0x00, 0x0E, 0x00, 0x1D, - 0xC0, 0x01, 0xC0, 0x03, 0xB8, 0x00, 0x38, 0x00, 0x37, 0x00, 0x07, 0x00, 0x07, 0xE0, 0x00, 0xE0, - 0x00, 0xFC, 0x0F, 0xFF, 0xF8, 0x1F, 0x81, 0xFF, 0xFF, 0x03, 0xF0, 0x00, 0x70, 0x00, 0x7E, 0x00, - 0x0E, 0x00, 0x0F, 0xC0, 0x01, 0xC0, 0x01, 0x98, 0x00, 0x38, 0x00, 0x73, 0x80, 0x07, 0x00, 0x0E, - 0x70, 0x00, 0xE0, 0x03, 0xC7, 0x00, 0x1C, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, - 0x07, 0x80, 0xE0, 0x00, 0x01, 0xE0, 0x1E, 0x00, 0x00, 0x78, 0x01, 0xF0, 0x00, 0x1E, 0x00, 0x1F, - 0x80, 0x0F, 0x80, 0x00, 0xFF, 0xBF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0xF8, 0x00, - 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x7F, - 0x07, 0xF0, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0x80, 0x00, 0xF8, 0x03, 0xC0, 0x00, 0x07, 0x80, - 0xF0, 0x00, 0x00, 0x70, 0x1C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0x00, 0xF0, 0xE0, 0x00, 0x00, - 0x0E, 0x38, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x1D, 0xC0, 0x00, 0x00, 0x03, 0xB8, 0x00, - 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x07, 0xE0, 0x0F, 0xFE, 0x00, 0xFC, 0x03, 0xFF, 0xC0, 0x1F, - 0x80, 0x7F, 0xF8, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x00, - 0x01, 0x98, 0x00, 0x00, 0x00, 0x73, 0x80, 0x00, 0x00, 0x0E, 0x70, 0x00, 0x00, 0x03, 0xC7, 0x00, - 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, 0x80, 0xE0, 0x00, 0x01, 0xE0, - 0x1E, 0x00, 0x00, 0x78, 0x01, 0xF0, 0x00, 0x1E, 0x00, 0x1F, 0x80, 0x0F, 0x80, 0x00, 0xFF, 0xBF, - 0xE0, 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, - 0x03, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0x07, 0xF0, 0x00, 0x3F, 0x00, 0x1F, - 0x80, 0x0F, 0x80, 0x00, 0xF8, 0x03, 0xC0, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x00, 0x70, 0x1C, 0x00, - 0x00, 0x07, 0x07, 0x00, 0x00, 0x00, 0xF0, 0xE0, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x00, 0xE7, - 0x00, 0xC0, 0x60, 0x1D, 0xC0, 0x1C, 0x1C, 0x03, 0xB8, 0x03, 0xC7, 0x80, 0x37, 0x00, 0x3D, 0xE0, - 0x07, 0xE0, 0x03, 0xF8, 0x00, 0xFC, 0x00, 0x3E, 0x00, 0x1F, 0x80, 0x07, 0xC0, 0x03, 0xF0, 0x01, - 0xFC, 0x00, 0x7E, 0x00, 0x7B, 0xC0, 0x0F, 0xC0, 0x1E, 0x3C, 0x01, 0x98, 0x03, 0x83, 0x80, 0x73, - 0x80, 0x60, 0x20, 0x0E, 0x70, 0x00, 0x00, 0x03, 0xC7, 0x00, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, - 0x1E, 0x0E, 0x00, 0x00, 0x07, 0x80, 0xE0, 0x00, 0x01, 0xE0, 0x1E, 0x00, 0x00, 0x78, 0x01, 0xF0, - 0x00, 0x1E, 0x00, 0x1F, 0x80, 0x0F, 0x80, 0x00, 0xFF, 0xBF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, 0x00, - 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x01, 0xFF, - 0xFF, 0x00, 0x00, 0x7F, 0x07, 0xF0, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0x80, 0x00, 0xF8, 0x03, - 0xC0, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x00, 0x70, 0x1C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0x00, - 0xF0, 0xE0, 0x00, 0x08, 0x0E, 0x38, 0x00, 0x03, 0x80, 0xE7, 0x00, 0x00, 0xF0, 0x1D, 0xC0, 0x00, - 0x3C, 0x03, 0xB8, 0x00, 0x0F, 0x00, 0x37, 0x00, 0x01, 0xC0, 0x07, 0xE0, 0x00, 0x70, 0x00, 0xFC, - 0x00, 0x1E, 0x00, 0x1F, 0x80, 0x07, 0x80, 0x03, 0xF0, 0x01, 0xE0, 0x00, 0x7E, 0x00, 0x78, 0x00, - 0x0F, 0xC0, 0x0E, 0x00, 0x01, 0x98, 0x03, 0x80, 0x00, 0x73, 0x80, 0xF0, 0x00, 0x0E, 0x70, 0x1C, - 0x00, 0x03, 0xC7, 0x00, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, 0x80, - 0xE0, 0x00, 0x01, 0xE0, 0x1E, 0x00, 0x00, 0x78, 0x01, 0xF0, 0x00, 0x1E, 0x00, 0x1F, 0x80, 0x0F, - 0x80, 0x00, 0xFF, 0xBF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x1F, - 0x30, 0xFB, 0xC6, 0x0C, 0x70, 0x03, 0x80, 0x1C, 0x03, 0xFF, 0xDF, 0xFE, 0x38, 0x31, 0xC1, 0x8E, - 0x0C, 0x70, 0x63, 0x83, 0x1C, 0x18, 0xE0, 0xC7, 0x06, 0x38, 0x31, 0xC1, 0x8E, 0x0C, 0x1F, 0x38, - 0x3E, 0x70, 0x60, 0xE1, 0xC1, 0xC3, 0x83, 0x87, 0x07, 0x3F, 0xCE, 0x7F, 0x9C, 0x38, 0x38, 0x70, - 0x70, 0xE0, 0xE1, 0xC1, 0xC3, 0x83, 0x87, 0x07, 0x0E, 0x0E, 0x1C, 0x1C, 0x38, 0x38, 0x70, 0x7C, - 0xE0, 0xF8, + 0x3C, 0x1F, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xFF, 0xDE, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x7B, + 0xFF, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, 0x80, 0xF0, 0x00, 0x78, 0x03, 0xC0, + 0x07, 0x80, 0x0F, 0x00, 0x78, 0x00, 0x30, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0xC0, + 0x7C, 0x1F, 0xC7, 0xFD, 0xFF, 0xC3, 0x80, 0x70, 0x7F, 0xCF, 0xF9, 0xFF, 0x07, 0x07, 0xFC, 0xFF, + 0x9F, 0xF0, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x00, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0xFF, + 0x9F, 0xF0, 0x70, 0x0E, 0x0F, 0xF9, 0xFF, 0x07, 0x00, 0xE0, 0x1C, 0x3F, 0xFB, 0xFE, 0x3F, 0x83, + 0xE0, 0x38, 0x02, 0x00, 0xC0, 0x00, 0x07, 0x06, 0x00, 0x38, 0x70, 0x01, 0xC7, 0x80, 0x0E, 0x7C, + 0x00, 0x77, 0xFF, 0xFF, 0xBF, 0xFF, 0xFC, 0xF8, 0x00, 0xE3, 0xC0, 0x07, 0x0E, 0x00, 0x38, 0x30, + 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x40, 0xC0, 0x03, 0x06, 0x00, 0x1C, 0x30, 0x01, + 0xF1, 0xFF, 0xFF, 0xCF, 0xFF, 0xFE, 0x60, 0x01, 0xE3, 0x00, 0x0E, 0x18, 0x00, 0x60, 0xC0, 0x02, + 0x06, 0x00, 0x00, 0x30, 0x00, 0x20, 0x00, 0x01, 0xC0, 0x00, 0x0F, 0x80, 0x00, 0x7F, 0x00, 0x03, + 0xCE, 0x00, 0x1E, 0x1C, 0x00, 0xF0, 0x38, 0x07, 0x80, 0x70, 0x3C, 0x00, 0xE1, 0xFC, 0x0F, 0xCF, + 0xF0, 0x3F, 0x81, 0xC0, 0xC0, 0x07, 0x03, 0x00, 0x1C, 0x0C, 0x00, 0x70, 0x30, 0x01, 0xC0, 0xC0, + 0x07, 0x03, 0x00, 0x1C, 0x0C, 0x00, 0x7F, 0xF0, 0x01, 0xFF, 0xC0, 0x00, 0x20, 0x00, 0x01, 0xC0, + 0x00, 0x0F, 0x80, 0x00, 0x7F, 0x00, 0x03, 0xCE, 0x00, 0x1E, 0x1C, 0x00, 0xF0, 0x38, 0x07, 0x80, + 0x70, 0x3C, 0x00, 0xE1, 0xFC, 0x0F, 0xCF, 0xF0, 0x3F, 0x81, 0xC0, 0xC0, 0x07, 0x03, 0x00, 0x1C, + 0x0C, 0x00, 0x7F, 0xF0, 0x01, 0xFF, 0xC0, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF0, 0x01, + 0xFF, 0xC0, 0x07, 0x03, 0x00, 0x1C, 0x0C, 0x00, 0x7F, 0xF0, 0x01, 0xFF, 0xC0, 0x1F, 0x01, 0xFE, + 0x0F, 0xF8, 0x43, 0xE0, 0x07, 0x00, 0x38, 0x3C, 0xE3, 0xFF, 0x3F, 0xFB, 0xC3, 0xDC, 0x0E, 0xE0, + 0x77, 0x03, 0xB8, 0x1D, 0xC0, 0xEE, 0x0E, 0x78, 0xF1, 0xFF, 0x07, 0xF0, 0x06, 0x00, 0x00, 0x00, + 0x1F, 0xCC, 0x7F, 0xF9, 0xFF, 0xE7, 0x83, 0xCE, 0x0F, 0xB8, 0x3B, 0xF0, 0xE7, 0xE3, 0x8F, 0xCE, + 0x1D, 0xF8, 0x73, 0xE1, 0xE7, 0xC7, 0x8F, 0xFF, 0x3B, 0xF8, 0x61, 0xC0, 0x01, 0xC0, 0x00, 0xE0, + 0x00, 0xF8, 0x00, 0x7C, 0x00, 0x7E, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x1C, 0xE0, 0x0E, 0x38, 0x07, + 0x1C, 0x07, 0x0F, 0x03, 0x83, 0x81, 0xC1, 0xC1, 0xC0, 0x70, 0xE0, 0x38, 0xF0, 0x1C, 0x70, 0x07, + 0x3F, 0xFF, 0xBF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, + 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, + 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x70, 0x7F, 0xFC, 0xFF, 0xF9, + 0xFF, 0xF3, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x80, 0x07, 0x80, 0x07, 0x80, 0x07, + 0x80, 0x07, 0x00, 0x1E, 0x00, 0x78, 0x00, 0xE0, 0x03, 0x80, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, + 0xC0, 0x07, 0xFF, 0xEF, 0xFF, 0xDF, 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x0E, 0x00, + 0x18, 0x00, 0x60, 0x01, 0xC0, 0x07, 0x00, 0x0C, 0x00, 0x38, 0x00, 0xE0, 0x01, 0x80, 0x06, 0x00, + 0x1C, 0x00, 0x70, 0x00, 0xC0, 0x03, 0x80, 0x0E, 0x00, 0x18, 0x00, 0x60, 0x00, 0x00, 0x0E, 0x00, + 0x1C, 0x00, 0x78, 0x00, 0xE0, 0x01, 0xC0, 0x07, 0x80, 0x0E, 0x00, 0x1C, 0x00, 0x70, 0xF0, 0xE3, + 0xF3, 0xC3, 0xE7, 0x01, 0xCE, 0x01, 0xFC, 0x03, 0xF0, 0x07, 0xE0, 0x07, 0x80, 0x0F, 0x00, 0x0E, + 0x00, 0x1F, 0x03, 0xC3, 0xFC, 0x7F, 0x9F, 0xF7, 0xFF, 0xC3, 0xF8, 0x7E, 0x0F, 0x83, 0xF0, 0x3C, + 0x1F, 0x83, 0xE0, 0xFE, 0x7F, 0xCF, 0x7F, 0xDF, 0xF1, 0xFC, 0x3F, 0x03, 0x00, 0x60, 0x01, 0x81, + 0xF0, 0xFC, 0x38, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, + 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC1, 0xF0, 0xFC, 0x3E, 0x00, 0x03, + 0xBC, 0xE2, 0x00, 0x00, 0x00, 0x00, 0xEF, 0x38, 0x80, 0x00, 0x00, 0xF8, 0x77, 0xF1, 0xDD, 0xFE, + 0x61, 0xF8, 0x03, 0xC3, 0xE1, 0x9F, 0xC7, 0x7F, 0xFD, 0x8F, 0xE6, 0x1F, 0x00, 0x01, 0xC0, 0x1C, + 0x01, 0x8F, 0xFF, 0xFF, 0xF7, 0xFF, 0x07, 0x00, 0x60, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xC0, 0x18, + 0x03, 0x80, 0x00, 0x30, 0x1F, 0x0F, 0xF7, 0xF8, 0xFC, 0x0F, 0xE0, 0x3F, 0xC0, 0x7F, 0x01, 0xF0, + 0x03, 0x00, 0x0F, 0xFF, 0xFF, 0xF7, 0xFF, 0xE0, 0x0F, 0xC0, 0xFF, 0x01, 0xFE, 0x03, 0xF0, 0x7F, + 0x3F, 0xEF, 0xF0, 0xF8, 0x0C, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF7, 0xFF, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0x07, 0xF0, 0x00, 0x3F, 0x00, + 0x1F, 0x80, 0x0F, 0x80, 0x00, 0xF8, 0x03, 0xC0, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x00, 0x70, 0x1C, + 0x00, 0x00, 0x07, 0x07, 0x00, 0x18, 0x00, 0xF0, 0xE0, 0x03, 0x80, 0x0E, 0x38, 0x00, 0x70, 0x00, + 0xE7, 0x00, 0x0E, 0x00, 0x1D, 0xC0, 0x01, 0xC0, 0x03, 0xB8, 0x00, 0x38, 0x00, 0x37, 0x00, 0x07, + 0x00, 0x07, 0xE0, 0x00, 0xE0, 0x00, 0xFC, 0x0F, 0xFF, 0xF8, 0x1F, 0x81, 0xFF, 0xFF, 0x03, 0xF0, + 0x00, 0x70, 0x00, 0x7E, 0x00, 0x0E, 0x00, 0x0F, 0xC0, 0x01, 0xC0, 0x01, 0x98, 0x00, 0x38, 0x00, + 0x73, 0x80, 0x07, 0x00, 0x0E, 0x70, 0x00, 0xE0, 0x03, 0xC7, 0x00, 0x1C, 0x00, 0x70, 0xF0, 0x00, + 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, 0x80, 0xE0, 0x00, 0x01, 0xE0, 0x1E, 0x00, 0x00, 0x78, 0x01, + 0xF0, 0x00, 0x1E, 0x00, 0x1F, 0x80, 0x0F, 0x80, 0x00, 0xFF, 0xBF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, + 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x01, + 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0x07, 0xF0, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0x80, 0x00, 0xF8, + 0x03, 0xC0, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x00, 0x70, 0x1C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, + 0x00, 0xF0, 0xE0, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x1D, 0xC0, + 0x00, 0x00, 0x03, 0xB8, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x07, 0xE0, 0x0F, 0xFE, 0x00, + 0xFC, 0x03, 0xFF, 0xC0, 0x1F, 0x80, 0x7F, 0xF8, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, + 0x00, 0x0F, 0xC0, 0x00, 0x00, 0x01, 0x98, 0x00, 0x00, 0x00, 0x73, 0x80, 0x00, 0x00, 0x0E, 0x70, + 0x00, 0x00, 0x03, 0xC7, 0x00, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, + 0x80, 0xE0, 0x00, 0x01, 0xE0, 0x1E, 0x00, 0x00, 0x78, 0x01, 0xF0, 0x00, 0x1E, 0x00, 0x1F, 0x80, + 0x0F, 0x80, 0x00, 0xFF, 0xBF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, + 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0x07, + 0xF0, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0x80, 0x00, 0xF8, 0x03, 0xC0, 0x00, 0x07, 0x80, 0xF0, + 0x00, 0x00, 0x70, 0x1C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0x00, 0xF0, 0xE0, 0x00, 0x00, 0x0E, + 0x38, 0x00, 0x00, 0x00, 0xE7, 0x00, 0xC0, 0x60, 0x1D, 0xC0, 0x1C, 0x1C, 0x03, 0xB8, 0x03, 0xC7, + 0x80, 0x37, 0x00, 0x3D, 0xE0, 0x07, 0xE0, 0x03, 0xF8, 0x00, 0xFC, 0x00, 0x3E, 0x00, 0x1F, 0x80, + 0x07, 0xC0, 0x03, 0xF0, 0x01, 0xFC, 0x00, 0x7E, 0x00, 0x7B, 0xC0, 0x0F, 0xC0, 0x1E, 0x3C, 0x01, + 0x98, 0x03, 0x83, 0x80, 0x73, 0x80, 0x60, 0x20, 0x0E, 0x70, 0x00, 0x00, 0x03, 0xC7, 0x00, 0x00, + 0x00, 0x70, 0xF0, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, 0x80, 0xE0, 0x00, 0x01, 0xE0, 0x1E, + 0x00, 0x00, 0x78, 0x01, 0xF0, 0x00, 0x1E, 0x00, 0x1F, 0x80, 0x0F, 0x80, 0x00, 0xFF, 0xBF, 0xE0, + 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x03, + 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0x07, 0xF0, 0x00, 0x3F, 0x00, 0x1F, 0x80, + 0x0F, 0x80, 0x00, 0xF8, 0x03, 0xC0, 0x00, 0x07, 0x80, 0xF0, 0x00, 0x00, 0x70, 0x1C, 0x00, 0x00, + 0x07, 0x07, 0x00, 0x00, 0x00, 0xF0, 0xE0, 0x00, 0x08, 0x0E, 0x38, 0x00, 0x03, 0x80, 0xE7, 0x00, + 0x00, 0xF0, 0x1D, 0xC0, 0x00, 0x3C, 0x03, 0xB8, 0x00, 0x0F, 0x00, 0x37, 0x00, 0x01, 0xC0, 0x07, + 0xE0, 0x00, 0x70, 0x00, 0xFC, 0x00, 0x1E, 0x00, 0x1F, 0x80, 0x07, 0x80, 0x03, 0xF0, 0x01, 0xE0, + 0x00, 0x7E, 0x00, 0x78, 0x00, 0x0F, 0xC0, 0x0E, 0x00, 0x01, 0x98, 0x03, 0x80, 0x00, 0x73, 0x80, + 0xF0, 0x00, 0x0E, 0x70, 0x1C, 0x00, 0x03, 0xC7, 0x00, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x1E, + 0x0E, 0x00, 0x00, 0x07, 0x80, 0xE0, 0x00, 0x01, 0xE0, 0x1E, 0x00, 0x00, 0x78, 0x01, 0xF0, 0x00, + 0x1E, 0x00, 0x1F, 0x80, 0x0F, 0x80, 0x00, 0xFF, 0xBF, 0xE0, 0x00, 0x07, 0xFF, 0xF0, 0x00, 0x00, + 0x3F, 0xF8, 0x00, 0x00, 0x1F, 0x30, 0xFB, 0xC6, 0x0C, 0x70, 0x03, 0x80, 0x1C, 0x03, 0xFF, 0xDF, + 0xFE, 0x38, 0x31, 0xC1, 0x8E, 0x0C, 0x70, 0x63, 0x83, 0x1C, 0x18, 0xE0, 0xC7, 0x06, 0x38, 0x31, + 0xC1, 0x8E, 0x0C, 0x1F, 0x38, 0x3E, 0x70, 0x60, 0xE1, 0xC1, 0xC3, 0x83, 0x87, 0x07, 0x3F, 0xCE, + 0x7F, 0x9C, 0x38, 0x38, 0x70, 0x70, 0xE0, 0xE1, 0xC1, 0xC3, 0x83, 0x87, 0x07, 0x0E, 0x0E, 0x1C, + 0x1C, 0x38, 0x38, 0x70, 0x7C, 0xE0, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x60, 0x00, 0x0E, + 0x70, 0x00, 0x07, 0x60, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xF0, 0x00, 0x07, 0x78, + 0x00, 0x1E, 0x7F, 0xFF, 0xFE, 0x1F, 0xFF, 0xF8, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x76, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x76, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x18, 0x00, 0x60, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x18, 0x00, 0x00, 0x01, 0xC0, 0x00, + 0x06, 0x0E, 0x00, 0x00, 0x78, 0x78, 0x00, 0x0F, 0x81, 0xE0, 0x03, 0xFF, 0x0F, 0xFF, 0xFF, 0x7F, + 0x1F, 0xFF, 0xE1, 0xF8, 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, + 0xE0, 0x00, 0x00, 0x77, 0x00, 0x00, 0x01, 0xB0, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x18, 0x38, 0x70, 0xE0, 0xC1, 0x83, 0x0E, 0xFD, 0xF0, 0x00, + 0x0D, 0xDF, 0xB7, 0x38, 0x70, 0x60, 0x00, 0x01, 0xC0, 0x30, 0x0E, 0x01, 0xC0, 0x7C, 0x3F, 0xFF, + 0xBF, 0x00, 0x00, 0x00, 0xD8, 0x3B, 0x83, 0x60, 0x38, 0x07, 0x00, 0x40, 0x00, 0x30, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3B, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x7E, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x60, 0x00, 0x0E, 0x70, 0x00, 0x07, 0x60, 0x00, 0x07, + 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xF0, 0x00, 0x07, 0x78, 0x00, 0x1E, 0x7F, 0xFF, 0xFE, 0x1F, + 0xFF, 0xF8, 0x07, 0xFF, 0x80, 0x00, 0x30, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x0F, 0x80, 0x00, + 0x00, 0x7E, 0x00, 0x00, 0x03, 0xB0, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x01, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x30, 0x70, 0x00, 0x03, 0xC3, 0xC0, 0x00, 0x7C, 0x0F, 0x00, 0x1F, 0xF8, 0x7F, + 0xFF, 0xFB, 0xF8, 0xFF, 0xFF, 0x0F, 0xC1, 0xFF, 0x80, 0x00, 0x60, 0xC1, 0xF3, 0xF7, 0x7F, 0xFF, + 0x00, 0x00, 0x00, 0xE1, 0xC3, 0x83, 0x87, 0x0E, 0x1C, 0x7B, 0xE7, 0x80, 0x00, 0x18, 0x03, 0x00, + 0x7C, 0x0F, 0xC1, 0xD8, 0x7F, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x38, 0x06, 0x01, 0xC0, 0x38, + 0x0F, 0x87, 0xFF, 0xF7, 0xE0, 0x00, 0x3C, 0x00, 0x7F, 0xC0, 0x7F, 0xFE, 0x01, 0xFE, 0x01, 0xFE, + 0x07, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0x38, 0x00, 0x70, 0x00, 0x73, 0x70, 0xE7, 0xF0, 0xE3, 0x70, + 0xE1, 0xC0, 0xE1, 0xC0, 0xE0, 0xC0, 0xF0, 0x00, 0x78, 0x00, 0x3F, 0xFE, 0x1F, 0xFF, 0x07, 0xFC, + 0x3C, 0x00, 0x07, 0xFC, 0x00, 0x7F, 0xFE, 0x00, 0x1F, 0xE0, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x0F, + 0x9C, 0x01, 0xE0, 0xE0, 0x38, 0x0F, 0x07, 0x00, 0x7E, 0x73, 0x73, 0xEE, 0x7F, 0x00, 0xE3, 0x70, + 0x0E, 0x1C, 0x00, 0xE1, 0xC0, 0x0E, 0x0C, 0x00, 0xF0, 0x00, 0x07, 0x80, 0x00, 0x3F, 0xFE, 0x01, + 0xFF, 0xF0, 0x07, 0xFC, 0x00, 0x7F, 0x01, 0xFF, 0x03, 0xFE, 0x00, 0x3F, 0x00, 0xFC, 0x0F, 0xE1, + 0xFC, 0x3F, 0xC0, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x03, 0xF0, 0x0F, 0xC0, 0x37, 0x00, 0x30, 0x01, + 0xC0, 0x03, 0x00, 0x7F, 0x00, 0x1F, 0xF0, 0x03, 0xFE, 0x00, 0x03, 0xF0, 0x00, 0xFC, 0x00, 0xFE, + 0x01, 0xFF, 0xC3, 0xFC, 0x7E, 0xFC, 0x0F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x1D, 0xC0, + 0x03, 0x60, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x02, 0x00, 0x00, 0x18, 0x03, 0x00, 0x7C, 0x0F, 0xC1, + 0xD8, 0x7F, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x03, 0xC0, 0x38, 0x07, 0x80, 0x70, 0x07, + 0x00, 0xE0, 0x1C, 0x03, 0xB8, 0xF7, 0xFC, 0xFF, 0x01, 0x00, 0x18, 0x00, 0x30, 0x00, 0x7C, 0x00, + 0xFC, 0x01, 0xDC, 0x07, 0xF0, 0x0F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x38, + 0x00, 0x38, 0x00, 0x78, 0x00, 0x70, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0xC3, 0x8F, 0xC7, + 0xFF, 0xEF, 0xF3, 0xC1, 0x00, 0x00, 0x04, 0x07, 0x03, 0x83, 0x63, 0xB9, 0xD8, 0x00, 0x00, 0x02, + 0x03, 0x81, 0xC0, 0x60, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x83, 0x83, 0xC3, 0xCF, 0xC7, 0xC1, 0x80, + 0x0C, 0x00, 0x70, 0x03, 0x80, 0x76, 0x03, 0xF0, 0x1D, 0x80, 0x00, 0x00, 0x00, 0x02, 0x00, 0x38, + 0x01, 0xC0, 0x06, 0x00, 0x38, 0x01, 0xC0, 0x0F, 0x00, 0x7E, 0x03, 0xF0, 0x38, 0x03, 0xC0, 0x3C, + 0x0F, 0xC0, 0x7C, 0x01, 0x80, 0x00, 0x18, 0x06, 0x01, 0xF0, 0x7E, 0x1D, 0x8F, 0xE3, 0xF0, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x1C, 0x07, 0x00, 0xC0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x1C, 0x0F, + 0x07, 0x8F, 0xC3, 0xE0, 0x60, 0x00, 0x18, 0x00, 0xC0, 0x07, 0xC0, 0x3F, 0x01, 0xD8, 0x1F, 0xC0, + 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0E, 0x00, 0x70, 0x01, 0x80, 0x0E, 0x00, 0x70, + 0x03, 0xC0, 0x1F, 0x80, 0xFC, 0x0E, 0x00, 0xF0, 0x0F, 0x03, 0xF0, 0x1F, 0x00, 0x60, 0x00, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x78, 0x00, 0x03, 0xF0, 0x00, 0x1F, 0x80, 0x00, 0x7C, 0x00, 0x01, 0xE0, + 0x00, 0x03, 0x80, 0x00, 0x07, 0x80, 0x00, 0x07, 0x80, 0xC0, 0x03, 0x81, 0xC0, 0x03, 0x83, 0x00, + 0x03, 0x8E, 0x00, 0x07, 0x1C, 0x00, 0x07, 0x3C, 0x00, 0x0E, 0x3C, 0x00, 0x3C, 0x7F, 0xFF, 0xF0, + 0x3F, 0xFF, 0xC0, 0x1F, 0xFC, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x0F, 0xC0, + 0x00, 0x0F, 0xC0, 0x00, 0x07, 0xC0, 0x00, 0x03, 0xC0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x3C, 0x00, + 0x00, 0x07, 0x80, 0x18, 0x00, 0xF0, 0x07, 0x00, 0x1E, 0x01, 0x80, 0x03, 0xC0, 0xE0, 0x00, 0x70, + 0x38, 0x00, 0x0E, 0x0F, 0x00, 0x03, 0xC1, 0xE0, 0x03, 0xF8, 0x7F, 0xFF, 0xFF, 0xC7, 0xFF, 0xF9, + 0xF0, 0x7F, 0xF0, 0x00, 0x00, 0x40, 0x3C, 0x1F, 0x8F, 0xC7, 0xE0, 0xF0, 0x3C, 0x03, 0xC0, 0x3C, + 0x03, 0xC0, 0x3C, 0x03, 0x80, 0x38, 0x07, 0x00, 0xE0, 0x3C, 0xFF, 0x1F, 0xC3, 0x00, 0x00, 0x00, + 0x40, 0x0F, 0x80, 0x7E, 0x07, 0xE0, 0x7E, 0x01, 0xE0, 0x0E, 0x00, 0x1C, 0x00, 0x78, 0x00, 0x70, + 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x0E, 0x00, 0x3C, 0x01, 0xFC, 0xFF, 0xFF, 0xF9, 0xFC, 0x00, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF0, 0x00, 0x03, 0xE0, 0x00, 0x1F, 0x60, 0x00, 0xFB, 0xC0, + 0x01, 0xDF, 0x80, 0x02, 0xFC, 0x00, 0x03, 0xE0, 0x00, 0x0F, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x3C, + 0x00, 0x00, 0x3C, 0x06, 0x00, 0x1C, 0x0E, 0x00, 0x1C, 0x18, 0x00, 0x1C, 0x70, 0x00, 0x38, 0xE0, + 0x00, 0x39, 0xE0, 0x00, 0x71, 0xE0, 0x01, 0xE3, 0xFF, 0xFF, 0x81, 0xFF, 0xFE, 0x00, 0xFF, 0xE0, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x0F, 0xB0, 0x00, 0x0F, + 0xBC, 0x00, 0x03, 0xBF, 0x00, 0x00, 0xBF, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x03, + 0x80, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x1E, 0x00, 0x60, 0x03, 0xC0, 0x1C, 0x00, 0x78, 0x06, 0x00, + 0x0F, 0x03, 0x80, 0x01, 0xC0, 0xE0, 0x00, 0x38, 0x3C, 0x00, 0x0F, 0x07, 0x80, 0x0F, 0xE1, 0xFF, + 0xFF, 0xFF, 0x1F, 0xFF, 0xE7, 0xC1, 0xFF, 0xC0, 0x00, 0x00, 0x80, 0x70, 0x3E, 0x1F, 0x27, 0x9F, + 0xEF, 0xF7, 0xE3, 0xF0, 0x78, 0x1E, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x1E, 0x01, 0xC0, 0x1C, 0x03, + 0x80, 0x70, 0x1E, 0x7F, 0x8F, 0xE1, 0x80, 0x00, 0x00, 0x80, 0x0E, 0x00, 0xF8, 0x0F, 0x90, 0x7B, + 0xE3, 0xDF, 0x8D, 0xF8, 0x1F, 0x80, 0x78, 0x03, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x38, + 0x00, 0x70, 0x00, 0xE0, 0x03, 0x80, 0x0F, 0x00, 0x7F, 0x3F, 0xFF, 0xFE, 0x7F, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x20, 0x07, 0x70, 0x07, 0x70, 0x07, 0x60, 0x07, 0xE0, 0x07, 0xE0, + 0x07, 0x70, 0x07, 0x78, 0x1E, 0x7F, 0xFC, 0x3F, 0xF8, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xE0, + 0x00, 0x07, 0x02, 0x00, 0x70, 0x70, 0x07, 0x07, 0x00, 0x78, 0x60, 0x07, 0xFE, 0x00, 0x7F, 0xE0, + 0x07, 0x07, 0x00, 0x70, 0x70, 0x0E, 0x07, 0xFF, 0xE0, 0x3F, 0xFC, 0x00, 0xFF, 0x00, 0x08, 0x07, + 0x81, 0xE0, 0xFC, 0x3B, 0x9C, 0x66, 0x1F, 0x87, 0xE1, 0xDC, 0xE7, 0xF8, 0xFC, 0x04, 0x00, 0xFC, + 0x07, 0xF8, 0x3C, 0xE0, 0xE1, 0x83, 0x87, 0x0E, 0x1E, 0x18, 0x3E, 0x60, 0x78, 0x00, 0x00, 0x00, + 0xE3, 0x8E, 0x1C, 0x71, 0xC7, 0x3F, 0xEF, 0x04, 0x30, 0x87, 0x0C, 0x38, 0x03, 0xC0, 0x3E, 0x3D, + 0xF3, 0xE3, 0xBE, 0x0D, 0xC0, 0x7C, 0x03, 0xE0, 0x0F, 0x00, 0x38, 0x01, 0x80, 0x01, 0x00, 0x03, + 0x80, 0x03, 0xC0, 0x03, 0xF0, 0x07, 0xF8, 0x0E, 0x7C, 0x0E, 0x3E, 0x0E, 0x7E, 0x0E, 0x77, 0x07, + 0xF7, 0x07, 0xE7, 0xFF, 0xFF, 0xFF, 0xFE, 0x18, 0x18, 0x00, 0x60, 0x00, 0xF8, 0x00, 0xFE, 0x00, + 0x77, 0x00, 0x73, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0C, 0xF0, 0x4F, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, + 0xE1, 0xC1, 0x80, 0x70, 0xC0, 0x3C, 0x60, 0x0F, 0xF0, 0x03, 0xF8, 0x00, 0xF0, 0x00, 0x08, 0x00, + 0x38, 0x00, 0xF0, 0x07, 0xE0, 0x3F, 0xC0, 0xE7, 0x83, 0x1F, 0x0C, 0x7E, 0x39, 0xF8, 0xFE, 0x71, + 0xF1, 0xFF, 0xFE, 0xFF, 0xF8, 0x01, 0x80, 0x03, 0x80, 0x1F, 0x00, 0xFC, 0x07, 0x38, 0x1C, 0xE0, + 0xE3, 0x83, 0x8C, 0x0E, 0x70, 0x3F, 0xC3, 0xFF, 0xFF, 0xFF, 0xCE, 0x1C, 0x38, 0x70, 0xF1, 0xC1, + 0xFE, 0x03, 0xF8, 0x03, 0x80, 0x03, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x7F, 0x00, + 0x01, 0xCE, 0x00, 0x07, 0x8C, 0x00, 0x1E, 0x10, 0x00, 0x38, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xF0, + 0x00, 0x0D, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xF0, 0x3F, 0xFE, 0x00, 0x07, 0xFE, 0x7F, 0xFD, 0xFF, + 0xF7, 0x80, 0x0C, 0x00, 0x1C, 0x00, 0x3F, 0x80, 0x1F, 0xFF, 0x0F, 0xFE, 0x0C, 0x00, 0x00, 0x78, + 0x00, 0x00, 0xC0, 0x00, 0x01, 0x80, 0x00, 0x01, 0xE0, 0x00, 0x07, 0xC0, 0x00, 0x0C, 0xC0, 0x00, + 0x03, 0xC0, 0x00, 0x07, 0xC0, 0x00, 0x1F, 0xC0, 0x00, 0x73, 0x80, 0x01, 0xE3, 0x00, 0x07, 0x84, + 0x00, 0x0E, 0x00, 0x00, 0x38, 0x00, 0x00, 0x3C, 0x00, 0x03, 0x7F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, + 0x0F, 0xFF, 0x80, 0x0C, 0x00, 0x78, 0x00, 0xC0, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xC0, 0x0C, 0x00, + 0x03, 0xFF, 0x3F, 0xFE, 0xFF, 0xFB, 0xC0, 0x06, 0x00, 0x0E, 0x00, 0x1F, 0xC0, 0x0F, 0xFF, 0x87, + 0xFF, 0x00, 0x03, 0xC0, 0x01, 0xFC, 0x00, 0x7F, 0x00, 0x1E, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x08, + 0x1C, 0x03, 0x83, 0xE0, 0x70, 0x3F, 0x0C, 0x01, 0xFB, 0x80, 0x07, 0x70, 0x00, 0xE7, 0x00, 0x3C, + 0xF0, 0x0F, 0x1F, 0xFF, 0xC1, 0xFF, 0xF0, 0x0F, 0xF0, 0x00, 0x20, 0x00, 0x07, 0x00, 0xC0, 0x70, + 0x0F, 0x86, 0x00, 0xFF, 0xE0, 0x03, 0xFE, 0x00, 0x1C, 0x70, 0x01, 0xC7, 0x80, 0x78, 0x3F, 0xFF, + 0x81, 0xFF, 0xE0, 0x0F, 0xF8, 0x00, 0x00, 0x18, 0x38, 0x70, 0xE0, 0xC1, 0x83, 0x0E, 0xFD, 0xF0, + 0x00, 0x0D, 0xDF, 0xB7, 0x00, 0x01, 0xC0, 0x30, 0x0E, 0x01, 0xC0, 0x7C, 0x3F, 0xFF, 0xBF, 0x00, + 0x00, 0x00, 0xD8, 0x3B, 0x83, 0x60, 0x1F, 0x0F, 0xC7, 0xA1, 0xC0, 0x70, 0x1C, 0x03, 0xF8, 0x7F, + 0xFF, 0x3F, 0x04, 0x00, 0x38, 0x7F, 0xD3, 0xC0, 0x00, 0x00, 0xE0, 0x70, 0x38, 0x0C, 0x06, 0x03, + 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x38, 0x1F, 0xF1, 0x3C, + 0x00, 0x00, 0x00, 0x38, 0x07, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xE0, 0x1C, 0x03, + 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xF0, 0x1F, 0xC1, 0xF8, 0x00, 0x33, 0xCC, 0x30, + 0xFB, 0xEC, 0x00, 0x71, 0xC7, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0E, 0x38, 0xE3, 0x8E, 0x30, + 0x78, 0x30, 0x18, 0x0F, 0x87, 0xC3, 0x00, 0x00, 0x70, 0x38, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, + 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x78, 0x3F, 0x8F, 0xC0, 0x00, 0x0C, 0x1E, + 0x0C, 0x06, 0x03, 0xE1, 0xF0, 0xC0, 0x38, 0x3F, 0x3F, 0x9C, 0xFC, 0x7E, 0x3F, 0x1D, 0xFE, 0xFF, + 0x0F, 0x83, 0x87, 0xDF, 0xCF, 0xC7, 0x00, 0x0C, 0x01, 0xE0, 0x0C, 0x00, 0x60, 0x03, 0xE0, 0x1F, + 0x00, 0xC0, 0x00, 0x00, 0x1C, 0x01, 0xF8, 0x1F, 0xC0, 0xE7, 0x0E, 0x38, 0x71, 0xC3, 0xCE, 0x0F, + 0xFE, 0x7F, 0xF0, 0x3F, 0x01, 0xC0, 0x3E, 0x0F, 0xE0, 0x7E, 0x03, 0x80, 0x00, 0xE3, 0x8E, 0x18, + 0x61, 0x86, 0x18, 0x61, 0x86, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC0, 0x0C, 0xF3, 0x0C, 0x1E, 0xFB, + 0x00, 0xE0, 0xE0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, + 0x78, 0x7F, 0x3F, 0x00, 0x30, 0xF0, 0xC0, 0xC0, 0x78, 0xF8, 0xC0, 0x0E, 0x00, 0x03, 0xC0, 0x78, + 0x60, 0x3F, 0x8C, 0x0F, 0xE1, 0xF3, 0xC0, 0x3E, 0x70, 0x06, 0x1C, 0x01, 0x03, 0x80, 0x70, 0x7C, + 0x0E, 0x07, 0xE1, 0x80, 0x3F, 0x70, 0x00, 0xEE, 0x00, 0x1C, 0xE0, 0x07, 0x9E, 0x01, 0xE3, 0xFF, + 0xF8, 0x3F, 0xFE, 0x01, 0xFE, 0x00, 0x0E, 0x00, 0x01, 0xE0, 0x00, 0x18, 0x00, 0x01, 0x80, 0x00, + 0x1F, 0x00, 0x01, 0xF0, 0x00, 0x18, 0x00, 0x02, 0x00, 0x00, 0x70, 0x0C, 0x07, 0x00, 0xF8, 0x60, + 0x0F, 0xFE, 0x00, 0x3F, 0xE0, 0x01, 0xC7, 0x00, 0x1C, 0x78, 0x07, 0x83, 0xFF, 0xF8, 0x1F, 0xFE, + 0x00, 0xFF, 0x80, 0x31, 0xEE, 0x38, 0x7F, 0xF6, 0x00, 0x38, 0xE3, 0x87, 0x1C, 0x71, 0xCF, 0xFB, + 0xC0, 0x00, 0x18, 0x1E, 0x06, 0x01, 0x80, 0x3C, 0x1F, 0x06, 0x00, 0x00, 0x1C, 0x07, 0x01, 0xC0, + 0x70, 0x3E, 0x3F, 0xFF, 0x7C, 0x00, 0xEE, 0xE6, 0x66, 0x66, 0x66, 0x67, 0x77, 0x77, 0x77, 0xE0, + 0xE0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x78, 0x7F, + 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x60, 0x00, 0x0E, 0x70, 0x00, 0x07, 0x60, 0x00, + 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xF0, 0x00, 0x07, 0x78, 0x00, 0x1E, 0x7F, 0xFF, 0xFE, + 0x1F, 0xFF, 0xF8, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x1C, 0x00, 0x00, 0x18, 0x00, 0x60, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x18, 0x00, 0x00, 0x01, + 0xC0, 0x00, 0x06, 0x0E, 0x00, 0x00, 0x78, 0x78, 0x00, 0x0F, 0x81, 0xE0, 0x03, 0xFF, 0x0F, 0xFF, + 0xFF, 0x7F, 0x1F, 0xFF, 0xE1, 0xF8, 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x80, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE3, 0x8E, 0x1C, + 0x71, 0xC7, 0x3F, 0xEF, 0x00, 0x01, 0xC7, 0x1C, 0x00, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x0F, 0x8F, + 0xFF, 0xDF, 0x00, 0x00, 0x01, 0x80, 0x70, 0x18, 0x00, 0x3B, 0x0F, 0xE3, 0xB0, 0x00, 0x00, 0x02, + 0x01, 0xE0, 0x78, 0x3F, 0x0E, 0xE7, 0x19, 0x87, 0xE1, 0xF8, 0x77, 0x39, 0xFE, 0x3F, 0x00, 0x1D, + 0x80, 0x7E, 0x01, 0xD8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x60, 0x0F, 0xC0, 0x7F, + 0x03, 0xFC, 0x1C, 0x70, 0x61, 0xC3, 0x87, 0x07, 0xFC, 0x1F, 0xF0, 0x3F, 0xE0, 0x03, 0xF0, 0x07, + 0xC0, 0x00, 0x00, 0x76, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, 0x0E, 0x60, 0x00, + 0x0E, 0x70, 0x00, 0x07, 0x60, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xF0, 0x00, 0x07, + 0x78, 0x00, 0x1E, 0x7F, 0xFF, 0xFE, 0x1F, 0xFF, 0xF8, 0x07, 0xFF, 0x80, 0x00, 0x76, 0x00, 0x00, + 0x03, 0xF8, 0x00, 0x00, 0x1D, 0x80, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x01, 0x80, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0xE0, 0x00, 0x07, 0x87, 0x80, 0x00, 0xF8, + 0x1E, 0x00, 0x3F, 0xF0, 0xFF, 0xFF, 0xF7, 0xF1, 0xFF, 0xFE, 0x1F, 0x83, 0xFF, 0x00, 0x00, 0xED, + 0xFF, 0xB0, 0x00, 0x00, 0x0E, 0x1C, 0x38, 0x38, 0x70, 0xE1, 0xC7, 0xBE, 0x78, 0x00, 0x33, 0x07, + 0xE0, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x60, 0x1C, 0x03, 0x80, 0xF8, 0x7F, 0xFF, 0x7E, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x76, 0x00, 0x00, 0x7E, + 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, 0x0E, 0x60, 0x00, 0x0E, 0x70, 0x00, 0x07, 0x60, 0x00, 0x07, + 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xF0, 0x00, 0x07, 0x78, 0x00, 0x1E, 0x7F, 0xFF, 0xFE, 0x1F, + 0xFF, 0xF8, 0x07, 0xFF, 0x80, 0x00, 0x18, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x00, 0xEC, 0x00, 0x00, 0x07, 0xF0, 0x00, 0x00, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, + 0x00, 0x00, 0x70, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0xC1, 0xC0, 0x00, 0x0F, + 0x0F, 0x00, 0x01, 0xF0, 0x3C, 0x00, 0x7F, 0xE1, 0xFF, 0xFF, 0xEF, 0xE3, 0xFF, 0xFC, 0x3F, 0x07, + 0xFE, 0x00, 0x00, 0x30, 0x70, 0xE7, 0x6F, 0xFD, 0x80, 0x00, 0x00, 0x70, 0xE1, 0xC1, 0xC3, 0x87, + 0x0E, 0x3D, 0xF3, 0xC0, 0x00, 0x0C, 0x03, 0x80, 0x30, 0x19, 0x83, 0xF0, 0x7E, 0x00, 0x00, 0x00, + 0x00, 0x01, 0xC0, 0x30, 0x0E, 0x01, 0xC0, 0x7C, 0x3F, 0xFF, 0xBF, 0x00, 0x00, 0x3C, 0x00, 0xFF, + 0x01, 0xFF, 0xF0, 0x0F, 0xF0, 0x1F, 0xE0, 0xFC, 0x03, 0xC0, 0x0F, 0x00, 0x38, 0x00, 0xE0, 0x01, + 0xC0, 0x07, 0x04, 0x0E, 0x1C, 0x1C, 0x38, 0x38, 0x20, 0x70, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xFF, + 0xF8, 0xFF, 0xF0, 0x7F, 0x80, 0x3C, 0x00, 0x1F, 0xE0, 0x07, 0xFF, 0xC0, 0x07, 0xF8, 0x01, 0xFE, + 0x01, 0xFC, 0x00, 0xF3, 0x00, 0x78, 0xE0, 0x38, 0x3C, 0x1C, 0x07, 0xF7, 0x00, 0xFF, 0x82, 0x00, + 0xE1, 0xC0, 0x38, 0x70, 0x0E, 0x08, 0x03, 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x03, 0xFF, 0xE0, + 0x7F, 0xF8, 0x07, 0xF8, 0x00, 0x7F, 0x01, 0xFF, 0x03, 0xFE, 0x00, 0x3F, 0x00, 0xFC, 0x0F, 0xE1, + 0xFC, 0x3F, 0xC0, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x80, 0x0C, 0x00, 0x7F, 0x00, + 0x1F, 0xF0, 0x03, 0xFE, 0x00, 0x03, 0xF0, 0x00, 0xFC, 0x00, 0xFE, 0x01, 0xFF, 0xC3, 0xFC, 0x7E, + 0xFC, 0x0F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x3C, 0x00, + 0xFF, 0x01, 0xFF, 0xF0, 0x0F, 0xF0, 0x1F, 0xE0, 0xFC, 0x03, 0xC0, 0x0F, 0x00, 0x38, 0x00, 0xE0, + 0x01, 0xC0, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xF0, 0x00, 0xF0, 0x00, + 0xFF, 0xF8, 0xFF, 0xF0, 0x7F, 0x80, 0x3C, 0x00, 0x1F, 0xE0, 0x07, 0xFF, 0xC0, 0x07, 0xF8, 0x01, + 0xFE, 0x01, 0xFC, 0x00, 0xF3, 0x00, 0x78, 0xE0, 0x38, 0x3C, 0x1C, 0x07, 0xF7, 0x00, 0xFF, 0x80, + 0x00, 0xE0, 0x00, 0x38, 0x00, 0x0E, 0x00, 0x03, 0x80, 0x00, 0xF0, 0x00, 0x1E, 0x00, 0x03, 0xFF, + 0xE0, 0x7F, 0xF8, 0x07, 0xF8, 0x00, 0x7F, 0x01, 0xFF, 0x03, 0xFE, 0x00, 0x3F, 0x00, 0xFC, 0x0F, + 0xE1, 0xFC, 0x3F, 0xC0, 0xFC, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x1F, 0xF0, 0x03, 0xFE, 0x00, 0x03, + 0xF0, 0x00, 0xFC, 0x00, 0xFE, 0x01, 0xFF, 0xC3, 0xFC, 0x7E, 0xFC, 0x0F, 0x80, 0x00, 0x00, 0x01, + 0x80, 0x07, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x1F, 0xE0, 0x3F, 0xFE, 0x01, 0xFE, + 0x03, 0xFC, 0x1F, 0x80, 0x78, 0x01, 0xE0, 0x07, 0x00, 0x1C, 0x00, 0x38, 0x00, 0xE0, 0x01, 0xC0, + 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1F, 0xFF, 0x1F, 0xFE, 0x0F, 0xF0, + 0x01, 0x80, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x07, 0xF8, 0x01, + 0xFF, 0xF0, 0x01, 0xFE, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x3C, 0xC0, 0x1E, 0x38, 0x0E, 0x0F, 0x07, + 0x01, 0xFD, 0xC0, 0x3F, 0xE0, 0x00, 0x38, 0x00, 0x0E, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x00, 0x3C, + 0x00, 0x07, 0x80, 0x00, 0xFF, 0xF8, 0x1F, 0xFE, 0x01, 0xFE, 0x00, 0x18, 0x00, 0x70, 0x01, 0x80, + 0x00, 0x00, 0x00, 0x01, 0xFC, 0x07, 0xFC, 0x0F, 0xF8, 0x00, 0xFC, 0x03, 0xF0, 0x3F, 0x87, 0xF0, + 0xFF, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x18, 0x00, 0x0E, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x1F, 0xC0, 0x07, 0xFC, 0x00, 0xFF, 0x80, 0x00, 0xFC, 0x00, 0x3F, 0x00, 0x3F, 0x80, 0x7F, + 0xF0, 0xFF, 0x1F, 0xBF, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x00, 0xF0, 0x0E, 0x01, 0xE0, 0x1C, 0x01, + 0xC0, 0x38, 0x07, 0x00, 0xEE, 0x3D, 0xFF, 0x3F, 0xC0, 0x40, 0x03, 0x00, 0x0E, 0x00, 0x0E, 0x00, + 0x1E, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xF0, 0xE3, 0xF1, 0xFF, 0xFB, 0xFC, + 0xF0, 0x40, 0x00, 0x0C, 0x01, 0xC0, 0x38, 0x00, 0x00, 0x00, 0x06, 0x01, 0xE0, 0x1C, 0x03, 0xC0, + 0x38, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xDC, 0x7B, 0xFE, 0x7F, 0x80, 0x80, 0x0E, 0x00, 0x1C, 0x00, + 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x01, 0xC0, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x80, 0x03, + 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x1C, 0x7E, 0x3F, 0xFF, 0x7F, 0x9E, 0x08, 0x00, 0x02, 0x03, + 0x81, 0xC0, 0x60, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x83, 0x83, 0xC3, 0xCF, 0xC7, 0xC1, 0x80, 0x02, + 0x00, 0x38, 0x01, 0xC0, 0x06, 0x00, 0x38, 0x01, 0xC0, 0x0F, 0x00, 0x7E, 0x03, 0xF0, 0x38, 0x03, + 0xC0, 0x3C, 0x0F, 0xC0, 0x7C, 0x01, 0x80, 0x00, 0x0E, 0x07, 0x03, 0x80, 0x00, 0x00, 0x10, 0x1C, + 0x0E, 0x03, 0x01, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x1C, 0x1E, 0x1E, 0x7E, 0x3E, 0x0C, 0x00, 0x0C, + 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1C, 0x00, 0xE0, 0x03, 0x00, 0x1C, 0x00, + 0xE0, 0x07, 0x80, 0x3F, 0x01, 0xF8, 0x1C, 0x01, 0xE0, 0x1E, 0x07, 0xE0, 0x3E, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x70, 0x00, + 0xE0, 0xE3, 0x80, 0x03, 0x87, 0x0C, 0x80, 0x1C, 0x38, 0x6E, 0x00, 0xE1, 0xC3, 0x70, 0x07, 0x9E, + 0x3B, 0x00, 0x3F, 0xFF, 0xF8, 0x01, 0xFF, 0xFD, 0xC0, 0x0E, 0x00, 0x07, 0x00, 0x70, 0x00, 0x3C, + 0x07, 0x00, 0x00, 0xFF, 0xF8, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x03, 0x83, 0x8E, 0x00, 0x00, 0xE1, 0xC3, 0x02, 0x00, + 0x70, 0xE1, 0x83, 0x80, 0x38, 0x71, 0xE1, 0xC0, 0x1E, 0x78, 0xF8, 0xC0, 0x0F, 0xFF, 0xFF, 0xE0, + 0x07, 0xFF, 0xE7, 0xF0, 0x03, 0x80, 0x00, 0x1C, 0x01, 0xC0, 0x00, 0x0F, 0x01, 0xC0, 0x00, 0x03, + 0xFF, 0xE0, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x38, 0x00, 0x07, 0x00, 0x00, 0xE1, 0xC3, 0x8C, 0x38, 0x61, 0xC7, 0x0C, 0x39, 0xC3, 0x87, 0x3C, + 0x79, 0xDF, 0xFF, 0xFB, 0xFF, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x01, 0x80, 0x70, + 0xE3, 0x80, 0xE1, 0x87, 0x01, 0xC3, 0x0E, 0x07, 0x0E, 0x1C, 0x0F, 0x1E, 0x7C, 0x7F, 0xFF, 0xFF, + 0xFF, 0xDF, 0xBE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1B, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x01, 0xC0, 0x03, 0x83, 0x8E, 0x00, 0x0E, + 0x1C, 0x32, 0x00, 0x70, 0xE1, 0xB8, 0x03, 0x87, 0x0D, 0xC0, 0x1E, 0x78, 0xEC, 0x00, 0xFF, 0xFF, + 0xE0, 0x07, 0xFF, 0xF7, 0x00, 0x38, 0x00, 0x1C, 0x01, 0xC0, 0x00, 0xF0, 0x1C, 0x00, 0x03, 0xFF, + 0xE0, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0xC0, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, + 0x01, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0x03, 0x83, 0x8E, 0x00, 0x00, 0xE1, 0xC3, 0x02, 0x00, 0x70, 0xE1, 0x83, + 0x80, 0x38, 0x71, 0xE1, 0xC0, 0x1E, 0x78, 0xF8, 0xC0, 0x0F, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xE7, + 0xF0, 0x03, 0x80, 0x00, 0x1C, 0x01, 0xC0, 0x00, 0x0F, 0x01, 0xC0, 0x00, 0x03, 0xFF, 0xE0, 0x00, + 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x3F, 0xC0, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x01, 0xC0, + 0x00, 0x6E, 0x00, 0x1F, 0xC0, 0x01, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x01, 0xC0, 0x00, + 0x38, 0x70, 0xE3, 0x0E, 0x18, 0x71, 0xC3, 0x0E, 0x70, 0xE1, 0xCF, 0x1E, 0x77, 0xFF, 0xFE, 0xFF, + 0xDF, 0x80, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0x00, 0x01, 0xC0, 0x00, 0x06, 0xE0, 0x00, + 0x1F, 0xC0, 0x00, 0x1B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x01, + 0x80, 0x70, 0xE3, 0x80, 0xE1, 0x87, 0x01, 0xC3, 0x0E, 0x07, 0x0E, 0x1C, 0x0F, 0x1E, 0x7C, 0x7F, + 0xFF, 0xFF, 0xFF, 0xDF, 0xBE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x01, 0xFC, + 0x00, 0x00, 0x03, 0xFE, 0x00, 0x0E, 0x0F, 0x8F, 0x00, 0x07, 0x1E, 0x07, 0x20, 0x07, 0x1C, 0x07, + 0x70, 0x07, 0x38, 0x07, 0x70, 0x07, 0xF0, 0x1E, 0x60, 0x07, 0xFF, 0xFC, 0xE0, 0x07, 0xFF, 0xF8, + 0xE0, 0x07, 0x1F, 0x80, 0x70, 0x07, 0x00, 0x00, 0x78, 0x0E, 0x00, 0x00, 0x3F, 0xFE, 0x00, 0x00, + 0x1F, 0xFC, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1F, + 0xC0, 0x00, 0x00, 0x03, 0xFE, 0x00, 0x00, 0xE0, 0xF8, 0xF0, 0x00, 0x07, 0x1E, 0x07, 0x02, 0x00, + 0x71, 0xC0, 0x70, 0x70, 0x07, 0x38, 0x07, 0x07, 0x00, 0x7F, 0x01, 0xF0, 0x60, 0x07, 0xFF, 0xFF, + 0xEE, 0x00, 0x7F, 0xFF, 0xFE, 0xE0, 0x07, 0x1F, 0x80, 0x07, 0x00, 0x70, 0x00, 0x00, 0x78, 0x0E, + 0x00, 0x00, 0x03, 0xFF, 0xE0, 0x00, 0x00, 0x1F, 0xFC, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x1F, 0xC0, 0x00, 0xFF, 0x87, 0x07, 0x8F, 0x1C, 0x38, 0x1C, 0x71, 0xC0, + 0x73, 0xCE, 0x01, 0xDF, 0x70, 0x1E, 0xFF, 0xFF, 0xF3, 0x9F, 0xFF, 0x80, 0x1F, 0xF0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x03, 0xF8, 0x00, 0x03, 0xFE, 0x03, 0x83, 0xC7, 0x81, 0xC3, 0x81, 0xC0, 0xE3, + 0x80, 0xE0, 0xF3, 0x80, 0x70, 0xFB, 0x80, 0xF8, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xC0, 0x7F, + 0xC0, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x00, + 0x03, 0xFE, 0x00, 0x0E, 0x0F, 0x8F, 0x00, 0x07, 0x1E, 0x07, 0x20, 0x07, 0x1C, 0x07, 0x70, 0x07, + 0x38, 0x07, 0x70, 0x07, 0xF0, 0x1E, 0x60, 0x07, 0xFF, 0xFC, 0xE0, 0x07, 0xFF, 0xF8, 0xE0, 0x07, + 0x1F, 0x80, 0x70, 0x07, 0x00, 0x00, 0x78, 0x0E, 0x00, 0x00, 0x3F, 0xFE, 0x00, 0x00, 0x1F, 0xFC, + 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0x80, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x0E, 0x0F, 0x8F, 0x00, 0x00, + 0x71, 0xE0, 0x70, 0x20, 0x07, 0x1C, 0x07, 0x07, 0x00, 0x73, 0x80, 0x70, 0x70, 0x07, 0xF0, 0x1F, + 0x06, 0x00, 0x7F, 0xFF, 0xFE, 0xE0, 0x07, 0xFF, 0xFF, 0xEE, 0x00, 0x71, 0xF8, 0x00, 0x70, 0x07, + 0x00, 0x00, 0x07, 0x80, 0xE0, 0x00, 0x00, 0x3F, 0xFE, 0x00, 0x00, 0x01, 0xFF, 0xC0, 0x00, 0x00, + 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x7F, 0x00, 0x03, 0xFE, 0x1C, 0x1E, 0x3C, 0x70, 0xE0, + 0x71, 0xC7, 0x01, 0xCF, 0x38, 0x07, 0x7D, 0xC0, 0x7B, 0xFF, 0xFF, 0xCE, 0x7F, 0xFE, 0x00, 0x7F, + 0xC0, 0x00, 0x01, 0x80, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x1F, 0xC0, 0x00, 0x1F, 0xF0, 0x1C, 0x1E, 0x3C, 0x0E, 0x1C, 0x0E, + 0x07, 0x1C, 0x07, 0x07, 0x9C, 0x03, 0x87, 0xDC, 0x07, 0xC7, 0xFF, 0xFF, 0xFF, 0x9F, 0xFF, 0xFE, + 0x03, 0xFE, 0x00, 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x18, 0x00, 0x03, 0x00, 0x00, 0x60, 0x00, 0x0C, + 0x00, 0x01, 0x80, 0x00, 0x30, 0x00, 0x07, 0x07, 0x00, 0xE7, 0xF0, 0x1D, 0xFF, 0x03, 0xF8, 0xF0, + 0x7C, 0x0E, 0x0F, 0x01, 0xC1, 0xC0, 0x38, 0x70, 0x1E, 0xFF, 0xFF, 0x9F, 0xFF, 0xE0, 0x7F, 0xC0, + 0x00, 0x0E, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x18, 0x00, 0x00, 0x30, 0x00, 0x00, 0x60, 0x00, 0x00, + 0xC0, 0x00, 0x01, 0x80, 0x00, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x0E, 0x7F, 0x00, 0x1D, 0xFF, + 0x00, 0x3F, 0x8F, 0x00, 0x7C, 0x0E, 0x00, 0xF0, 0x1C, 0x01, 0xC0, 0x38, 0x07, 0x01, 0xF0, 0xFF, + 0xFF, 0xFD, 0xFF, 0xFF, 0xF8, 0x7F, 0xC0, 0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, + 0x30, 0x00, 0x30, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x30, 0x38, 0xFE, 0x3B, 0xFE, 0x3F, 0xC7, + 0x3F, 0x07, 0x3E, 0x07, 0x3C, 0x07, 0x78, 0x7E, 0xFF, 0xFC, 0xFF, 0xF0, 0xF0, 0x00, 0x30, 0x00, + 0x06, 0x00, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x03, 0x00, 0x00, 0x60, 0x00, 0x0E, 0x00, 0x01, 0xC0, + 0x00, 0x38, 0x30, 0x07, 0x1F, 0xC0, 0xEF, 0xF8, 0x1F, 0xE3, 0x83, 0xF0, 0x70, 0x7C, 0x0E, 0x0F, + 0x01, 0xC3, 0xC3, 0xF8, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xC0, 0x00, 0x00, 0x0E, 0x00, 0x01, 0xC0, + 0x00, 0x18, 0x00, 0x03, 0x03, 0x00, 0x60, 0x70, 0x0C, 0x0C, 0x01, 0x80, 0x00, 0x30, 0x00, 0x07, + 0x07, 0x00, 0xE7, 0xF0, 0x1D, 0xFF, 0x03, 0xF8, 0xF0, 0x7C, 0x0E, 0x0F, 0x01, 0xC1, 0xC0, 0x38, + 0x70, 0x1E, 0xFF, 0xFF, 0x9F, 0xFF, 0xE0, 0x7F, 0xC0, 0x00, 0x0E, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x30, 0x60, 0x00, 0x61, 0xC0, 0x00, 0xC1, 0x80, 0x01, 0x80, 0x00, 0x03, 0x00, + 0x00, 0x07, 0x07, 0x00, 0x0E, 0x7F, 0x00, 0x1D, 0xFF, 0x00, 0x3F, 0x8F, 0x00, 0x7C, 0x0E, 0x00, + 0xF0, 0x1C, 0x01, 0xC0, 0x38, 0x07, 0x01, 0xF0, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xF8, 0x7F, 0xC0, + 0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x60, 0x30, 0x70, 0x30, 0x60, 0x38, 0x00, 0x38, + 0x00, 0x38, 0x30, 0x38, 0xFE, 0x3B, 0xFE, 0x3F, 0xC7, 0x3F, 0x07, 0x3E, 0x07, 0x3C, 0x07, 0x78, + 0x7E, 0xFF, 0xFC, 0xFF, 0xF0, 0xF0, 0x00, 0x30, 0x00, 0x06, 0x00, 0x00, 0xC0, 0x00, 0x18, 0x30, + 0x03, 0x07, 0x00, 0x60, 0xC0, 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x30, 0x07, 0x1F, 0xC0, 0xEF, + 0xF8, 0x1F, 0xE3, 0x83, 0xF0, 0x70, 0x7C, 0x0E, 0x0F, 0x01, 0xC3, 0xC3, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFE, 0xFF, 0xC0, 0x00, 0x00, 0x0F, 0x00, 0x7F, 0x01, 0xFE, 0x03, 0x80, 0x06, 0x00, 0x0C, 0x00, + 0x1C, 0x10, 0x3F, 0xF0, 0x3F, 0xE0, 0x3E, 0x00, 0xF0, 0x03, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x38, + 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x03, 0xC0, 0x07, 0xFF, 0xC7, 0xFF, 0xC3, 0xFE, + 0x00, 0x03, 0x80, 0x3F, 0x80, 0xFF, 0x83, 0xC7, 0x07, 0x86, 0x07, 0x9C, 0x07, 0xF0, 0x07, 0xE0, + 0x0F, 0xE0, 0x3F, 0xFC, 0xF3, 0xFB, 0xC0, 0x0F, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, + 0x01, 0xE0, 0x11, 0xFF, 0xF1, 0xFF, 0xE1, 0xFF, 0x00, 0x0F, 0xC1, 0xFC, 0x3E, 0x83, 0x80, 0x30, + 0x03, 0x00, 0x38, 0x03, 0xE7, 0x1F, 0xFF, 0xFE, 0xFF, 0x00, 0x00, 0x03, 0x80, 0x3F, 0x80, 0xFF, + 0x83, 0xC7, 0x07, 0x06, 0x07, 0x9C, 0x07, 0xF8, 0x07, 0xE0, 0x1F, 0xE1, 0xFF, 0xFB, 0xE3, 0xF8, + 0x00, 0x00, 0x06, 0x00, 0x1E, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x01, 0xE0, 0x0F, 0xE0, 0x3F, + 0xC0, 0x70, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x82, 0x07, 0xFE, 0x07, 0xFC, 0x07, 0xC0, 0x1E, 0x00, + 0x78, 0x00, 0xE0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0x78, + 0x00, 0xFF, 0xF8, 0xFF, 0xF8, 0x7F, 0xC0, 0x03, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x70, 0x07, 0xF0, 0x1F, 0xF0, 0x78, 0xE0, 0xF0, 0xC0, 0xF3, 0x80, 0xFE, 0x00, 0xFC, 0x01, + 0xFC, 0x07, 0xFF, 0x9E, 0x7F, 0x78, 0x01, 0xE0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, + 0x3C, 0x02, 0x3F, 0xFE, 0x3F, 0xFC, 0x3F, 0xE0, 0x03, 0x00, 0x70, 0x07, 0x00, 0x00, 0x00, 0x00, + 0xFC, 0x1F, 0xC3, 0xE8, 0x38, 0x03, 0x00, 0x30, 0x03, 0x80, 0x3E, 0x71, 0xFF, 0xFF, 0xEF, 0xF0, + 0x00, 0x00, 0x03, 0x00, 0x0E, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x07, 0xF0, 0x1F, + 0xF0, 0x78, 0xE0, 0xE0, 0xC0, 0xF3, 0x80, 0xFF, 0x00, 0xFC, 0x03, 0xFC, 0x3F, 0xFF, 0x7C, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x78, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x7C, 0x00, 0x00, 0xFE, 0x00, 0x01, 0xCE, 0x60, 0x01, 0xC7, + 0x70, 0x01, 0xC7, 0x60, 0x01, 0xC7, 0xE0, 0x01, 0xFF, 0xE0, 0x00, 0xFF, 0xF0, 0x00, 0x07, 0x78, + 0x00, 0x0E, 0x7F, 0xFF, 0xFE, 0x1F, 0xFF, 0xFC, 0x07, 0xFF, 0xE0, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x70, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0xFC, 0x0C, 0x00, 0x07, 0xF0, 0x70, 0x00, 0x73, 0xC3, 0x00, 0x03, 0x8E, 0x38, + 0x00, 0x1C, 0x71, 0xC0, 0x00, 0xE3, 0x8F, 0x00, 0x07, 0xB8, 0x3C, 0x00, 0x1F, 0xC1, 0xFF, 0xFF, + 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0x07, 0xFF, 0x80, 0x00, 0x0E, 0x01, 0xC0, 0x38, 0x00, 0x00, 0x00, + 0x1E, 0x07, 0xE1, 0xFE, 0x39, 0xC6, 0x39, 0xC3, 0x1C, 0x63, 0xFC, 0x3F, 0x80, 0x70, 0x3C, 0xFF, + 0x9F, 0xC0, 0x00, 0x00, 0x06, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x01, 0xF8, + 0x07, 0xF0, 0x39, 0xE0, 0xE3, 0x83, 0x0E, 0x0E, 0x38, 0x3D, 0xC0, 0x7F, 0x0F, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x1B, 0x80, 0x07, 0xF0, 0x00, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x07, 0xE0, 0x00, 0xFE, 0x00, 0x39, 0xC0, 0x07, 0x19, 0xC0, 0xC3, 0xB8, 0x1C, 0x77, 0x03, + 0xFF, 0xC0, 0x3F, 0xB8, 0x01, 0xF7, 0x00, 0x0E, 0x70, 0x03, 0xCF, 0x00, 0xF0, 0xFF, 0xFC, 0x0F, + 0xFF, 0x00, 0x7F, 0x80, 0x00, 0x1D, 0x80, 0x00, 0x7F, 0x00, 0x01, 0xD8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xF8, 0x00, 0x07, 0xF0, 0x00, 0x39, 0xC0, 0x00, 0xE7, 0x03, + 0x03, 0x0E, 0x1C, 0x0E, 0x38, 0x70, 0x3F, 0xFD, 0x80, 0x7F, 0xFE, 0x00, 0x3F, 0x38, 0x00, 0x70, + 0x70, 0x03, 0xC1, 0xE0, 0x1E, 0x03, 0xFF, 0xF0, 0x0F, 0xFF, 0x80, 0x0F, 0xF8, 0x00, 0x1B, 0x07, + 0x70, 0x6E, 0x00, 0x00, 0x00, 0x1E, 0x07, 0xE1, 0xFE, 0x39, 0xC6, 0x39, 0xC3, 0x1C, 0x63, 0xFC, + 0x3F, 0x80, 0x70, 0x3C, 0xFF, 0x9F, 0xC0, 0x00, 0x00, 0x1D, 0x80, 0x7E, 0x01, 0xD8, 0x00, 0x00, + 0x00, 0x00, 0x1C, 0x01, 0xF8, 0x07, 0xF0, 0x39, 0xE0, 0xE3, 0x83, 0x0E, 0x0E, 0x38, 0x3D, 0xC0, + 0x7F, 0x0F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x01, 0x80, 0x00, 0x0E, 0x00, + 0x00, 0x70, 0x00, 0x83, 0x80, 0x1E, 0x1C, 0x00, 0xE0, 0xE0, 0x0E, 0x07, 0x00, 0x38, 0x3B, 0x00, + 0xC1, 0xDC, 0x0F, 0x0E, 0xC1, 0xF0, 0x7E, 0x06, 0x03, 0xF0, 0x00, 0x1F, 0xC0, 0x00, 0xEF, 0x00, + 0x1E, 0x7F, 0xFF, 0xF0, 0xFF, 0xFF, 0x01, 0xFF, 0x80, 0x00, 0x00, 0x30, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x07, 0x00, 0x00, 0x83, 0x80, 0x01, 0xE1, 0xC0, 0x00, 0xE0, 0xE0, 0x00, + 0xE0, 0x70, 0x00, 0x38, 0x38, 0x30, 0x0C, 0x1C, 0x1C, 0x0F, 0x0E, 0x0C, 0x1F, 0x07, 0x0E, 0x06, + 0x03, 0x87, 0x00, 0x01, 0xC3, 0xC0, 0x00, 0xE0, 0xF0, 0x01, 0xF8, 0x7F, 0xFF, 0xFF, 0x8F, 0xFF, + 0xF7, 0xC1, 0xFF, 0xC0, 0x00, 0x00, 0x40, 0x3C, 0x1F, 0x8F, 0xC7, 0xE0, 0xF0, 0x3C, 0x03, 0xC0, + 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0x80, 0x38, 0x07, 0x00, 0xE0, 0x3C, 0xFF, 0x1F, 0xC3, 0x00, 0x00, + 0x00, 0x40, 0x0F, 0x80, 0x7E, 0x07, 0xE0, 0x7E, 0x01, 0xE0, 0x0E, 0x00, 0x1C, 0x00, 0x78, 0x00, + 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x0E, 0x00, 0x3C, 0x01, 0xFC, 0xFF, 0xFF, 0xF9, 0xFC, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, + 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x20, 0x07, 0x70, 0x07, + 0x70, 0x07, 0x60, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0x70, 0x0E, 0x78, 0x1E, 0x7F, 0xFC, 0x3F, 0xF8, + 0x0F, 0xF0, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x07, 0x00, 0x00, + 0x70, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x07, 0x00, 0x00, 0x70, + 0x00, 0x07, 0x02, 0x00, 0x70, 0x70, 0x07, 0x07, 0x00, 0x78, 0x60, 0x07, 0xFE, 0x00, 0x7F, 0xE0, + 0x07, 0x07, 0x00, 0xF0, 0x78, 0x1E, 0x03, 0xFF, 0xC0, 0x1F, 0xF8, 0x00, 0xFE, 0x00, 0x38, 0xE3, + 0x8E, 0x38, 0xE3, 0x8E, 0x38, 0xE3, 0x8E, 0x18, 0x61, 0x8E, 0xFB, 0xC0, 0x00, 0x38, 0x1C, 0x0E, + 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x01, 0x80, 0xC0, 0x70, 0x7C, 0xFF, + 0xFB, 0xC0, 0x00, 0x07, 0x81, 0xFC, 0x3F, 0xE3, 0x8E, 0x30, 0x60, 0x06, 0x0F, 0xE3, 0xFE, 0x7F, + 0xEF, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x06, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, + 0x00, 0x00, 0x00, 0x07, 0xC0, 0x0F, 0xE0, 0x1F, 0xE0, 0x3C, 0x70, 0x78, 0x70, 0x7C, 0x30, 0xFE, + 0x38, 0xEF, 0xFF, 0xE7, 0xFF, 0xE1, 0xC0, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x60, + 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x07, 0xC0, 0xFE, 0x0F, 0xE1, 0xC7, 0x18, + 0x73, 0x87, 0x7E, 0x7F, 0xFE, 0xE7, 0xE0, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x0F, 0xE0, 0x0E, 0xE0, + 0x1C, 0x70, 0x38, 0x70, 0x3C, 0x38, 0x7E, 0x3C, 0xFF, 0xFF, 0xE7, 0xFF, 0x01, 0xC0, 0x01, 0x80, + 0x01, 0xC0, 0x01, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x20, 0x07, + 0x70, 0x07, 0x70, 0x07, 0x60, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0x70, 0x07, 0x78, 0x1E, 0x7F, 0xFC, + 0x3F, 0xF8, 0x0F, 0xF0, 0x01, 0x80, 0x00, 0x1C, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x20, 0x07, 0x07, 0x00, 0x70, 0x70, 0x07, 0x86, + 0x00, 0x7F, 0xE0, 0x07, 0xFE, 0x00, 0x70, 0x70, 0x07, 0x07, 0x00, 0xE0, 0x7F, 0xFE, 0x03, 0xFF, + 0xC0, 0x0F, 0xF0, 0x00, 0x30, 0xE3, 0x00, 0x00, 0x03, 0x8E, 0x38, 0x71, 0xC7, 0x1C, 0xFF, 0xBC, + 0x00, 0x18, 0x0E, 0x01, 0x80, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x03, 0xE3, 0xFF, + 0xF7, 0xC0, 0x00, 0x08, 0x07, 0x81, 0xE0, 0xFC, 0x3B, 0x9C, 0x66, 0x1F, 0x87, 0xE1, 0xDC, 0xE7, + 0xF8, 0xFC, 0x01, 0x80, 0x06, 0x00, 0xFC, 0x07, 0xF0, 0x3F, 0xC1, 0xC7, 0x06, 0x1C, 0x38, 0x70, + 0x7F, 0xC1, 0xFF, 0x03, 0xFE, 0x00, 0x3F, 0x00, 0x7C, 0x00, 0x00, 0x08, 0x00, 0x38, 0x00, 0xF0, + 0x07, 0xE0, 0x3F, 0xC0, 0xE7, 0x83, 0x1F, 0x0C, 0x7E, 0x39, 0xF8, 0xFE, 0x71, 0xF1, 0xFF, 0xFE, + 0xFF, 0xF8, 0x01, 0x80, 0x03, 0x80, 0x1F, 0x00, 0xFC, 0x07, 0x38, 0x1C, 0xE0, 0xE3, 0x83, 0x8C, + 0x0E, 0x70, 0x3F, 0xC3, 0xFF, 0xFF, 0xFF, 0xCE, 0x1C, 0x38, 0x70, 0xF1, 0xC1, 0xFE, 0x03, 0xF8, + 0x03, 0x80, 0x1C, 0x1F, 0x9F, 0xCE, 0x7E, 0x3F, 0x1F, 0x8E, 0xFF, 0x7F, 0x87, 0xC1, 0xC3, 0xEF, + 0xE7, 0xE3, 0x80, 0x1C, 0x01, 0xF8, 0x1F, 0xC0, 0xE7, 0x0E, 0x38, 0x71, 0xC3, 0xCE, 0x0F, 0xFE, + 0x7F, 0xF0, 0x3F, 0x01, 0xC0, 0x3E, 0x0F, 0xE0, 0x7E, 0x03, 0x80, 0x00, 0x00, 0x03, 0xC0, 0x01, + 0xFC, 0x00, 0x7F, 0x00, 0x1E, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x08, 0x1C, 0x03, 0x83, 0xE0, 0x70, + 0x3F, 0x0C, 0x01, 0xFB, 0x80, 0x07, 0x70, 0x00, 0xE7, 0x00, 0x3C, 0xF0, 0x0F, 0x1F, 0xFF, 0xC1, + 0xFF, 0xF0, 0x0F, 0xF0, 0x00, 0x20, 0x00, 0x07, 0x00, 0xC0, 0x70, 0x0F, 0x86, 0x00, 0xFF, 0xE0, + 0x03, 0xFE, 0x00, 0x1C, 0x70, 0x01, 0xC7, 0x80, 0x78, 0x3F, 0xFF, 0x81, 0xFF, 0xE0, 0x0F, 0xF8, + 0x00, 0x00, 0x03, 0xC0, 0x01, 0xFC, 0x00, 0x7F, 0x00, 0x1E, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x08, + 0x1C, 0x03, 0x83, 0xE0, 0x70, 0x3F, 0x0C, 0x01, 0xFB, 0x80, 0x07, 0x70, 0x00, 0xE7, 0x00, 0x3C, + 0xF0, 0x0F, 0x1F, 0xFF, 0xC1, 0xFF, 0xF0, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xB8, + 0x00, 0x3F, 0x00, 0x06, 0xE0, 0x00, 0x20, 0x00, 0x07, 0x00, 0xC0, 0x70, 0x0F, 0x86, 0x00, 0xFF, + 0xE0, 0x03, 0xFE, 0x00, 0x1C, 0x70, 0x01, 0xC7, 0x80, 0x78, 0x3F, 0xFF, 0x81, 0xFF, 0xE0, 0x0F, + 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0x00, 0x07, 0xF0, 0x00, 0x76, 0x00, 0x00, 0x18, + 0x38, 0x70, 0xE0, 0xC1, 0x83, 0x0E, 0xFD, 0xF0, 0x00, 0x0D, 0xDF, 0xB7, 0x00, 0x01, 0xC0, 0x30, + 0x0E, 0x01, 0xC0, 0x7C, 0x3F, 0xFF, 0xBF, 0x00, 0x00, 0x00, 0xD8, 0x3B, 0x83, 0x60, 0x78, 0x01, + 0xFF, 0x3B, 0x3C, 0x70, 0x00, 0xE0, 0x81, 0xC3, 0x83, 0x87, 0xC7, 0x07, 0xCE, 0x03, 0xDC, 0x03, + 0xF8, 0x03, 0xE0, 0x01, 0xC0, 0x07, 0xC0, 0x1F, 0xC0, 0x7B, 0x81, 0xE3, 0x07, 0x8E, 0x1F, 0xFC, + 0x3F, 0xF0, 0x38, 0x00, 0x0F, 0xF3, 0x81, 0x3E, 0x70, 0x00, 0x0E, 0x00, 0x01, 0xC0, 0x30, 0x38, + 0x07, 0x07, 0x00, 0x70, 0xE0, 0x07, 0x1C, 0x00, 0x73, 0x80, 0x07, 0x70, 0x00, 0x7E, 0x00, 0x0F, + 0xC0, 0x00, 0xF8, 0x00, 0x0F, 0x00, 0x01, 0xF0, 0x01, 0xFE, 0x03, 0xFD, 0xF8, 0x7F, 0x1F, 0x00, + 0x00, 0x00, 0x38, 0x01, 0xE0, 0x06, 0x00, 0x18, 0x00, 0x7C, 0x39, 0xF0, 0xE6, 0x03, 0x84, 0x0E, + 0x38, 0x38, 0xF8, 0xE1, 0xF3, 0x81, 0xEE, 0x03, 0xF8, 0x07, 0xC0, 0x07, 0x00, 0x3E, 0x01, 0xFC, + 0x0F, 0x70, 0x78, 0xC3, 0xC7, 0x1F, 0xFC, 0x7F, 0xE0, 0x30, 0x00, 0x78, 0x00, 0x30, 0x00, 0x18, + 0x00, 0x07, 0x87, 0x07, 0xC3, 0x83, 0x01, 0xC0, 0x00, 0xE0, 0x60, 0x70, 0x38, 0x38, 0x0E, 0x1C, + 0x03, 0x8E, 0x00, 0xE7, 0x00, 0x3B, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x01, 0xF0, 0x00, 0x78, 0x00, + 0x3E, 0x00, 0xFF, 0x07, 0xFB, 0xF3, 0xF8, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x0E, 0x00, 0xE4, 0x0E, + 0xE0, 0xEF, 0x8E, 0x7C, 0xE1, 0xEE, 0x0F, 0xE0, 0x7C, 0x01, 0xC0, 0x3E, 0x07, 0xF0, 0xF7, 0x1E, + 0x33, 0xC7, 0x7F, 0xF7, 0xFE, 0x00, 0x03, 0x80, 0x78, 0x06, 0x00, 0x60, 0x07, 0xC0, 0x7C, 0x06, + 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0xC0, 0xE0, 0xE0, 0xE0, 0x70, 0xE0, 0x38, + 0xE0, 0x1C, 0xE0, 0x0E, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x03, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x0F, + 0xF0, 0xFF, 0x7E, 0xFE, 0x3E, 0x00, 0x00, 0x0C, 0x00, 0x1E, 0x00, 0x38, 0x00, 0x38, 0x00, 0x1F, + 0x00, 0x3F, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x0E, 0x00, 0xE4, 0x0E, 0xE0, 0xEF, 0x8E, 0x7C, 0xE1, + 0xEE, 0x0F, 0xE0, 0x7C, 0x01, 0xC0, 0x3E, 0x07, 0xF0, 0xF7, 0x1E, 0x33, 0xC7, 0x7F, 0xF7, 0xFE, + 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0xC0, 0xE0, 0xE0, 0xE0, 0x70, 0xE0, 0x38, 0xE0, + 0x1C, 0xE0, 0x0E, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x03, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x0F, 0xF0, + 0xFF, 0x7E, 0xFE, 0x3E, 0x00, 0x00, }; static const EpdGlyph inter_12_regularGlyphs[] = { @@ -2623,656 +3047,851 @@ static const EpdGlyph inter_12_regularGlyphs[] = { { 8, 5, 0, -3, 20, 5, 14301 }, // U+030B { 8, 4, 0, 1, 19, 4, 14306 }, // U+030C { 9, 4, 0, 1, 19, 5, 14310 }, // U+030F - { 5, 8, 0, 4, 13, 5, 14315 }, // U+0313 - { 5, 8, 0, 4, 13, 5, 14320 }, // U+0315 - { 4, 6, 0, -5, 18, 3, 14325 }, // U+031B - { 4, 4, 94, 1, -2, 2, 14328 }, // U+0323 - { 4, 6, 0, -10, -1, 3, 14330 }, // U+0326 - { 6, 7, 0, 1, 1, 6, 14333 }, // U+0327 - { 5, 7, 0, 1, 1, 5, 14339 }, // U+0328 - { 8, 4, 0, 1, -2, 4, 14344 }, // U+032C - { 11, 17, 0, -13, 15, 24, 14348 }, // U+0337 - { 15, 20, 0, -15, 19, 38, 14372 }, // U+0338 - { 8, 4, 0, -11, 19, 4, 14410 }, // U+0342 - { 4, 6, 0, 0, 16, 3, 14414 }, // U+0343 - { 9, 6, 0, -12, 20, 7, 14417 }, // U+0346 - { 8, 6, 0, -12, -3, 6, 14424 }, // U+0347 - { 7, 6, 0, -10, 0, 6, 14430 }, // U+0348 - { 6, 6, 0, -10, -1, 5, 14436 }, // U+0349 - { 9, 6, 0, -12, 20, 7, 14441 }, // U+034A - { 10, 10, 0, -13, 23, 13, 14448 }, // U+034B - { 10, 6, 0, -12, 21, 8, 14461 }, // U+034C - { 11, 6, 0, -13, -1, 9, 14469 }, // U+034D - { 7, 6, 0, -11, -1, 6, 14478 }, // U+034E - { 0, 0, 0, 0, 0, 0, 14484 }, // U+034F - { 10, 10, 0, -12, 25, 13, 14484 }, // U+0350 - { 3, 6, 0, -8, 19, 3, 14497 }, // U+0351 - { 8, 7, 0, -11, 21, 7, 14500 }, // U+0352 - { 7, 7, 0, -10, 0, 7, 14507 }, // U+0353 - { 7, 7, 0, -10, 0, 7, 14514 }, // U+0354 - { 7, 7, 0, -9, 0, 7, 14521 }, // U+0355 - { 14, 7, 0, -12, 0, 13, 14528 }, // U+0356 - { 4, 7, 0, 0, 21, 4, 14541 }, // U+0357 - { 4, 4, 0, 1, 19, 2, 14545 }, // U+0358 - { 6, 7, 0, -10, 0, 6, 14547 }, // U+0359 - { 10, 6, 0, -12, 0, 8, 14553 }, // U+035A - { 6, 7, 0, -10, 21, 6, 14561 }, // U+035B - { 18, 5, 0, -9, 0, 12, 14567 }, // U+035C - { 18, 5, 0, -9, 19, 12, 14579 }, // U+035D - { 16, 3, 0, -15, 19, 6, 14591 }, // U+035E - { 16, 3, 0, -15, -2, 6, 14597 }, // U+035F - { 24, 4, 0, -12, 19, 12, 14603 }, // U+0360 - { 18, 5, 0, -9, 19, 12, 14615 }, // U+0361 - { 20, 6, 0, -9, -1, 15, 14627 }, // U+0362 - { 7, 8, 0, -10, 22, 7, 14642 }, // U+0363 - { 7, 8, 0, -10, 22, 7, 14649 }, // U+0364 - { 2, 10, 0, -8, 24, 3, 14656 }, // U+0365 - { 8, 8, 0, -11, 22, 8, 14659 }, // U+0366 - { 6, 8, 0, -10, 22, 6, 14667 }, // U+0367 - { 7, 8, 0, -11, 22, 7, 14673 }, // U+0368 - { 7, 11, 0, -11, 25, 10, 14680 }, // U+0369 - { 6, 11, 0, -10, 25, 9, 14690 }, // U+036A - { 10, 8, 0, -12, 22, 10, 14699 }, // U+036B - { 5, 8, 0, -9, 22, 5, 14709 }, // U+036C - { 5, 10, 0, 0, 24, 7, 14714 }, // U+036D - { 8, 8, 0, -11, 22, 8, 14721 }, // U+036E - { 7, 8, 0, -10, 22, 7, 14729 }, // U+036F - { 12, 24, 238, 2, 24, 36, 14736 }, // U+0400 - { 12, 24, 238, 2, 24, 36, 14772 }, // U+0401 - { 19, 21, 334, 1, 19, 50, 14808 }, // U+0402 - { 12, 24, 228, 2, 24, 36, 14858 }, // U+0403 - { 16, 20, 291, 1, 19, 40, 14894 }, // U+0404 - { 14, 20, 254, 1, 19, 35, 14934 }, // U+0405 - { 3, 19, 104, 2, 19, 8, 14969 }, // U+0406 - { 9, 24, 104, -1, 24, 27, 14977 }, // U+0407 - { 11, 20, 225, 1, 19, 28, 15004 }, // U+0408 - { 26, 19, 442, 1, 19, 62, 15032 }, // U+0409 - { 25, 19, 447, 2, 19, 60, 15094 }, // U+040A - { 20, 19, 351, 1, 19, 48, 15154 }, // U+040B - { 14, 24, 265, 2, 24, 42, 15202 }, // U+040C - { 15, 24, 297, 2, 24, 45, 15244 }, // U+040D - { 15, 24, 248, 0, 24, 45, 15289 }, // U+040E - { 14, 23, 286, 2, 19, 41, 15334 }, // U+040F - { 17, 19, 273, 0, 19, 41, 15375 }, // U+0410 - { 13, 19, 255, 2, 19, 31, 15416 }, // U+0411 - { 13, 19, 260, 2, 19, 31, 15447 }, // U+0412 - { 12, 19, 228, 2, 19, 29, 15478 }, // U+0413 - { 19, 23, 323, 1, 19, 55, 15507 }, // U+0414 - { 12, 19, 238, 2, 19, 29, 15562 }, // U+0415 - { 24, 19, 380, 0, 19, 57, 15591 }, // U+0416 - { 13, 20, 245, 1, 19, 33, 15648 }, // U+0417 - { 15, 19, 297, 2, 19, 36, 15681 }, // U+0418 - { 15, 24, 297, 2, 24, 45, 15717 }, // U+0419 - { 14, 19, 265, 2, 19, 34, 15762 }, // U+041A - { 16, 19, 296, 1, 19, 38, 15796 }, // U+041B - { 19, 19, 357, 2, 19, 46, 15834 }, // U+041C - { 15, 19, 294, 2, 19, 36, 15880 }, // U+041D - { 17, 20, 304, 1, 19, 43, 15916 }, // U+041E - { 15, 19, 294, 2, 19, 36, 15959 }, // U+041F - { 13, 19, 253, 2, 19, 31, 15995 }, // U+0420 - { 16, 20, 291, 1, 19, 40, 16026 }, // U+0421 - { 14, 19, 255, 1, 19, 34, 16066 }, // U+0422 - { 15, 19, 248, 0, 19, 36, 16100 }, // U+0423 - { 19, 22, 330, 1, 20, 53, 16136 }, // U+0424 - { 17, 19, 270, 0, 19, 41, 16189 }, // U+0425 - { 16, 23, 286, 2, 19, 46, 16230 }, // U+0426 - { 15, 19, 277, 1, 19, 36, 16276 }, // U+0427 - { 20, 19, 380, 2, 19, 48, 16312 }, // U+0428 - { 22, 23, 385, 2, 19, 64, 16360 }, // U+0429 - { 19, 19, 323, 1, 19, 46, 16424 }, // U+042A - { 18, 19, 347, 2, 19, 43, 16470 }, // U+042B - { 13, 19, 257, 2, 19, 31, 16513 }, // U+042C - { 16, 20, 291, 1, 19, 40, 16544 }, // U+042D - { 22, 20, 404, 2, 19, 55, 16584 }, // U+042E - { 14, 19, 256, 0, 19, 34, 16639 }, // U+042F - { 11, 15, 221, 1, 14, 21, 16673 }, // U+0430 - { 13, 20, 233, 1, 19, 33, 16694 }, // U+0431 - { 12, 14, 222, 1, 14, 21, 16727 }, // U+0432 - { 10, 14, 173, 1, 14, 18, 16748 }, // U+0433 - { 15, 18, 244, 0, 14, 34, 16766 }, // U+0434 - { 13, 15, 229, 1, 14, 25, 16800 }, // U+0435 - { 20, 14, 321, 0, 14, 35, 16825 }, // U+0436 - { 11, 15, 195, 1, 14, 21, 16860 }, // U+0437 - { 12, 14, 233, 1, 14, 21, 16881 }, // U+0438 - { 12, 19, 233, 1, 19, 29, 16902 }, // U+0439 - { 13, 14, 213, 1, 14, 23, 16931 }, // U+043A - { 13, 14, 228, 0, 14, 23, 16954 }, // U+043B - { 17, 14, 304, 1, 14, 30, 16977 }, // U+043C - { 12, 14, 232, 1, 14, 21, 17007 }, // U+043D - { 13, 15, 235, 1, 14, 25, 17028 }, // U+043E - { 12, 14, 232, 1, 14, 21, 17053 }, // U+043F - { 13, 20, 241, 1, 14, 33, 17074 }, // U+0440 - { 12, 15, 224, 1, 14, 23, 17107 }, // U+0441 - { 12, 14, 186, 0, 14, 21, 17130 }, // U+0442 - { 14, 20, 220, 0, 14, 35, 17151 }, // U+0443 - { 15, 25, 271, 1, 19, 47, 17186 }, // U+0444 - { 13, 14, 215, 0, 14, 23, 17233 }, // U+0445 - { 14, 18, 236, 1, 14, 32, 17256 }, // U+0446 - { 12, 14, 228, 1, 14, 21, 17288 }, // U+0447 - { 18, 14, 326, 1, 14, 32, 17309 }, // U+0448 - { 20, 18, 331, 1, 14, 45, 17341 }, // U+0449 - { 15, 14, 255, 0, 14, 27, 17386 }, // U+044A - { 16, 14, 290, 1, 14, 28, 17413 }, // U+044B - { 12, 14, 220, 1, 14, 21, 17441 }, // U+044C - { 12, 15, 224, 1, 14, 23, 17462 }, // U+044D - { 19, 15, 332, 1, 14, 36, 17485 }, // U+044E - { 12, 14, 219, 0, 14, 21, 17521 }, // U+044F - { 13, 20, 229, 1, 19, 33, 17542 }, // U+0450 - { 13, 20, 229, 1, 19, 33, 17575 }, // U+0451 - { 13, 25, 233, 0, 19, 41, 17608 }, // U+0452 - { 10, 19, 173, 1, 19, 24, 17649 }, // U+0453 - { 12, 15, 224, 1, 14, 23, 17673 }, // U+0454 - { 11, 15, 206, 1, 14, 21, 17696 }, // U+0455 - { 4, 19, 94, 1, 19, 10, 17717 }, // U+0456 - { 9, 19, 94, -2, 19, 22, 17727 }, // U+0457 - { 6, 25, 94, -1, 19, 19, 17749 }, // U+0458 - { 22, 14, 367, 0, 14, 39, 17768 }, // U+0459 - { 21, 14, 356, 1, 14, 37, 17807 }, // U+045A - { 13, 19, 233, 0, 19, 31, 17844 }, // U+045B - { 13, 19, 213, 1, 19, 31, 17875 }, // U+045C - { 12, 19, 233, 1, 19, 29, 17906 }, // U+045D - { 14, 25, 220, 0, 19, 44, 17935 }, // U+045E - { 12, 18, 232, 1, 14, 27, 17979 }, // U+045F - { 19, 20, 355, 2, 19, 48, 18006 }, // U+0460 - { 18, 15, 319, 1, 14, 34, 18054 }, // U+0461 - { 16, 19, 274, 0, 19, 38, 18088 }, // U+0462 - { 15, 19, 243, 0, 19, 36, 18126 }, // U+0463 - { 21, 20, 379, 2, 19, 53, 18162 }, // U+0464 - { 18, 15, 309, 1, 14, 34, 18215 }, // U+0465 - { 17, 19, 273, 0, 19, 41, 18249 }, // U+0466 - { 14, 14, 226, 0, 14, 25, 18290 }, // U+0467 - { 21, 19, 372, 2, 19, 50, 18315 }, // U+0468 - { 18, 14, 308, 1, 14, 32, 18365 }, // U+0469 - { 19, 19, 338, 1, 19, 46, 18397 }, // U+046A - { 16, 14, 277, 1, 14, 28, 18443 }, // U+046B - { 25, 19, 455, 2, 19, 60, 18471 }, // U+046C - { 22, 14, 381, 1, 14, 39, 18531 }, // U+046D - { 12, 30, 215, 1, 24, 45, 18570 }, // U+046E - { 10, 25, 205, 1, 19, 32, 18615 }, // U+046F - { 18, 19, 349, 2, 19, 43, 18647 }, // U+0470 - { 17, 23, 308, 1, 17, 49, 18690 }, // U+0471 - { 17, 20, 304, 1, 19, 43, 18739 }, // U+0472 - { 13, 15, 235, 1, 14, 25, 18782 }, // U+0473 - { 17, 19, 279, 0, 19, 41, 18807 }, // U+0474 - { 15, 14, 234, 0, 14, 27, 18848 }, // U+0475 - { 17, 24, 279, 0, 24, 51, 18875 }, // U+0476 - { 15, 19, 234, 0, 19, 36, 18926 }, // U+0477 - { 32, 25, 525, 1, 19, 100, 18962 }, // U+0478 - { 27, 20, 448, 1, 14, 68, 19062 }, // U+0479 - { 16, 19, 283, 1, 19, 38, 19130 }, // U+0480 - { 12, 19, 222, 1, 14, 29, 19168 }, // U+0481 - { 14, 17, 250, 1, 17, 30, 19197 }, // U+0482 - { 10, 5, 0, -12, 19, 7, 19227 }, // U+0483 - { 11, 4, 0, -12, 19, 6, 19234 }, // U+0484 - { 4, 5, 0, -9, 20, 3, 19240 }, // U+0485 - { 4, 5, 0, -8, 20, 3, 19243 }, // U+0486 - { 11, 4, 0, -12, 19, 6, 19246 }, // U+0487 - { 25, 22, 0, -19, 18, 69, 19252 }, // U+0488 - { 24, 25, 0, -19, 19, 75, 19321 }, // U+0489 - { 17, 28, 297, 2, 24, 60, 19396 }, // U+048A - { 15, 23, 233, 1, 19, 44, 19456 }, // U+048B - { 16, 19, 274, 0, 19, 38, 19500 }, // U+048C - { 15, 17, 243, 0, 17, 32, 19538 }, // U+048D - { 13, 19, 251, 2, 19, 31, 19570 }, // U+048E - { 13, 20, 241, 1, 14, 33, 19601 }, // U+048F - { 12, 22, 228, 2, 22, 33, 19634 }, // U+0490 - { 10, 17, 173, 1, 17, 22, 19667 }, // U+0491 - { 15, 19, 228, -1, 19, 36, 19689 }, // U+0492 - { 11, 14, 173, 0, 14, 20, 19725 }, // U+0493 - { 13, 21, 258, 2, 19, 35, 19745 }, // U+0494 - { 12, 18, 214, 1, 14, 27, 19780 }, // U+0495 - { 25, 23, 388, 0, 19, 72, 19807 }, // U+0496 - { 21, 18, 328, 0, 14, 48, 19879 }, // U+0497 - { 13, 25, 245, 1, 19, 41, 19927 }, // U+0498 - { 11, 20, 195, 1, 14, 28, 19968 }, // U+0499 - { 16, 23, 280, 2, 19, 46, 19996 }, // U+049A - { 13, 18, 222, 1, 14, 30, 20042 }, // U+049B - { 14, 19, 263, 2, 19, 34, 20072 }, // U+049C - { 14, 14, 231, 1, 14, 25, 20106 }, // U+049D - { 20, 19, 332, 1, 19, 48, 20131 }, // U+04A0 - { 16, 14, 249, 0, 14, 28, 20179 }, // U+04A1 - { 17, 23, 300, 2, 19, 49, 20207 }, // U+04A2 - { 14, 18, 238, 1, 14, 32, 20256 }, // U+04A3 - { 22, 19, 391, 2, 19, 53, 20288 }, // U+04A4 - { 16, 14, 270, 1, 14, 28, 20341 }, // U+04A5 - { 25, 21, 440, 2, 19, 66, 20369 }, // U+04A6 - { 21, 18, 366, 1, 14, 48, 20435 }, // U+04A7 - { 23, 20, 391, 1, 19, 58, 20483 }, // U+04A8 - { 18, 15, 309, 1, 14, 34, 20541 }, // U+04A9 - { 16, 25, 291, 1, 19, 50, 20575 }, // U+04AA - { 12, 20, 224, 1, 14, 30, 20625 }, // U+04AB - { 14, 23, 255, 1, 19, 41, 20655 }, // U+04AC - { 12, 18, 186, 0, 14, 27, 20696 }, // U+04AD - { 17, 19, 268, 0, 19, 41, 20723 }, // U+04AE - { 14, 20, 220, 0, 14, 35, 20764 }, // U+04AF - { 17, 19, 268, 0, 19, 41, 20799 }, // U+04B0 - { 14, 20, 220, 0, 14, 35, 20840 }, // U+04B1 - { 18, 23, 277, 0, 19, 52, 20875 }, // U+04B2 - { 14, 18, 224, 0, 14, 32, 20927 }, // U+04B3 - { 22, 23, 364, 1, 19, 64, 20959 }, // U+04B4 - { 17, 18, 262, 0, 14, 39, 21023 }, // U+04B5 - { 16, 23, 282, 1, 19, 46, 21062 }, // U+04B6 - { 14, 18, 233, 1, 14, 32, 21108 }, // U+04B7 - { 15, 19, 277, 1, 19, 36, 21140 }, // U+04B8 - { 12, 14, 228, 1, 14, 21, 21176 }, // U+04B9 - { 14, 19, 277, 2, 19, 34, 21197 }, // U+04BA - { 12, 19, 233, 1, 19, 29, 21231 }, // U+04BB - { 20, 20, 335, 0, 19, 50, 21260 }, // U+04BC - { 17, 15, 283, 0, 14, 32, 21310 }, // U+04BD - { 20, 23, 335, 0, 19, 58, 21342 }, // U+04BE - { 17, 20, 283, 0, 14, 43, 21400 }, // U+04BF - { 3, 19, 104, 2, 19, 8, 21443 }, // U+04C0 - { 24, 24, 380, 0, 24, 72, 21451 }, // U+04C1 - { 20, 19, 321, 0, 19, 48, 21523 }, // U+04C2 - { 14, 23, 270, 2, 19, 41, 21571 }, // U+04C3 - { 13, 18, 232, 1, 14, 30, 21612 }, // U+04C4 - { 18, 23, 296, 1, 19, 52, 21642 }, // U+04C5 - { 15, 18, 228, 0, 14, 34, 21694 }, // U+04C6 - { 15, 25, 294, 2, 19, 47, 21728 }, // U+04C7 - { 12, 20, 232, 1, 14, 30, 21775 }, // U+04C8 - { 17, 23, 294, 2, 19, 49, 21805 }, // U+04C9 - { 15, 18, 232, 1, 14, 34, 21854 }, // U+04CA - { 16, 23, 282, 1, 19, 46, 21888 }, // U+04CB - { 14, 18, 233, 1, 14, 32, 21934 }, // U+04CC - { 21, 23, 357, 2, 19, 61, 21966 }, // U+04CD - { 19, 18, 304, 1, 14, 43, 22027 }, // U+04CE - { 4, 19, 94, 1, 19, 10, 22070 }, // U+04CF - { 17, 24, 273, 0, 24, 51, 22080 }, // U+04D0 - { 11, 20, 221, 1, 19, 28, 22131 }, // U+04D1 - { 17, 24, 273, 0, 24, 51, 22159 }, // U+04D2 - { 11, 20, 221, 1, 19, 28, 22210 }, // U+04D3 - { 24, 19, 395, 0, 19, 57, 22238 }, // U+04D4 - { 21, 15, 363, 1, 14, 40, 22295 }, // U+04D5 - { 12, 24, 238, 2, 24, 36, 22335 }, // U+04D6 - { 13, 20, 229, 1, 19, 33, 22371 }, // U+04D7 - { 17, 20, 297, 1, 19, 43, 22404 }, // U+04D8 - { 13, 15, 229, 1, 14, 25, 22447 }, // U+04D9 - { 17, 25, 297, 1, 24, 54, 22472 }, // U+04DA - { 13, 20, 229, 1, 19, 33, 22526 }, // U+04DB - { 24, 24, 380, 0, 24, 72, 22559 }, // U+04DC - { 20, 19, 321, 0, 19, 48, 22631 }, // U+04DD - { 13, 25, 245, 1, 24, 41, 22679 }, // U+04DE - { 11, 20, 195, 1, 19, 28, 22720 }, // U+04DF - { 13, 20, 246, 1, 19, 33, 22748 }, // U+04E0 - { 13, 20, 246, 1, 14, 33, 22781 }, // U+04E1 - { 15, 23, 297, 2, 23, 44, 22814 }, // U+04E2 - { 12, 18, 233, 1, 18, 27, 22858 }, // U+04E3 - { 15, 24, 297, 2, 24, 45, 22885 }, // U+04E4 - { 12, 19, 233, 1, 19, 29, 22930 }, // U+04E5 - { 17, 25, 304, 1, 24, 54, 22959 }, // U+04E6 - { 13, 20, 235, 1, 19, 33, 23013 }, // U+04E7 - { 17, 20, 304, 1, 19, 43, 23046 }, // U+04E8 - { 13, 15, 235, 1, 14, 25, 23089 }, // U+04E9 - { 17, 25, 304, 1, 24, 54, 23114 }, // U+04EA - { 13, 20, 235, 1, 19, 33, 23168 }, // U+04EB - { 16, 25, 291, 1, 24, 50, 23201 }, // U+04EC - { 12, 20, 224, 1, 19, 30, 23251 }, // U+04ED - { 15, 23, 248, 0, 23, 44, 23281 }, // U+04EE - { 14, 24, 220, 0, 18, 42, 23325 }, // U+04EF - { 15, 24, 248, 0, 24, 45, 23367 }, // U+04F0 - { 14, 25, 220, 0, 19, 44, 23412 }, // U+04F1 - { 15, 24, 248, 0, 24, 45, 23456 }, // U+04F2 - { 14, 25, 220, 0, 19, 44, 23501 }, // U+04F3 - { 15, 24, 277, 1, 24, 45, 23545 }, // U+04F4 - { 12, 19, 228, 1, 19, 29, 23590 }, // U+04F5 - { 12, 23, 228, 2, 19, 35, 23619 }, // U+04F6 - { 10, 18, 173, 1, 14, 23, 23654 }, // U+04F7 - { 18, 24, 347, 2, 24, 54, 23677 }, // U+04F8 - { 16, 19, 290, 1, 19, 38, 23731 }, // U+04F9 - { 14, 25, 235, 0, 19, 44, 23769 }, // U+04FA - { 11, 20, 173, 0, 14, 28, 23813 }, // U+04FB - { 18, 25, 279, 0, 19, 57, 23841 }, // U+04FC - { 13, 20, 205, 0, 14, 33, 23898 }, // U+04FD - { 17, 19, 270, 0, 19, 41, 23931 }, // U+04FE - { 13, 14, 215, 0, 14, 23, 23972 }, // U+04FF - { 13, 15, 251, 1, 15, 25, 23995 }, // U+05D0 - { 12, 15, 221, 1, 15, 23, 24020 }, // U+05D1 - { 8, 15, 168, 1, 15, 15, 24043 }, // U+05D2 - { 13, 15, 211, 0, 15, 25, 24058 }, // U+05D3 - { 12, 15, 255, 2, 15, 23, 24083 }, // U+05D4 - { 3, 15, 104, 2, 15, 6, 24106 }, // U+05D5 - { 8, 15, 152, 1, 15, 15, 24112 }, // U+05D6 - { 13, 15, 259, 2, 15, 25, 24127 }, // U+05D7 - { 14, 15, 258, 1, 15, 27, 24152 }, // U+05D8 - { 4, 8, 96, 1, 15, 4, 24179 }, // U+05D9 - { 11, 20, 207, 0, 15, 28, 24183 }, // U+05DA - { 11, 15, 212, 1, 15, 21, 24211 }, // U+05DB - { 12, 19, 190, 0, 19, 29, 24232 }, // U+05DC - { 13, 15, 259, 2, 15, 25, 24261 }, // U+05DD - { 14, 15, 262, 1, 15, 27, 24286 }, // U+05DE - { 3, 20, 104, 2, 15, 8, 24313 }, // U+05DF - { 6, 15, 139, 1, 15, 12, 24321 }, // U+05E0 - { 14, 15, 257, 1, 15, 27, 24333 }, // U+05E1 - { 14, 16, 237, 0, 15, 28, 24360 }, // U+05E2 - { 13, 20, 252, 1, 15, 33, 24388 }, // U+05E3 - { 13, 15, 249, 2, 15, 25, 24421 }, // U+05E4 - { 12, 20, 206, 0, 15, 30, 24446 }, // U+05E5 - { 12, 15, 217, 0, 15, 23, 24476 }, // U+05E6 - { 13, 20, 243, 2, 15, 33, 24499 }, // U+05E7 - { 12, 15, 210, 0, 15, 23, 24532 }, // U+05E8 - { 16, 15, 301, 1, 15, 30, 24555 }, // U+05E9 - { 14, 15, 266, 1, 15, 27, 24585 }, // U+05EA - { 17, 25, 273, 0, 19, 54, 24612 }, // U+1EA0 - { 11, 20, 221, 1, 14, 28, 24666 }, // U+1EA1 - { 17, 24, 273, 0, 24, 51, 24694 }, // U+1EA2 - { 11, 20, 221, 1, 19, 28, 24745 }, // U+1EA3 - { 17, 28, 273, 0, 28, 60, 24773 }, // U+1EA4 - { 12, 24, 221, 1, 23, 36, 24833 }, // U+1EA5 - { 17, 28, 273, 0, 28, 60, 24869 }, // U+1EA6 - { 11, 24, 221, 1, 23, 33, 24929 }, // U+1EA7 - { 17, 28, 273, 0, 28, 60, 24962 }, // U+1EA8 - { 11, 24, 221, 1, 23, 33, 25022 }, // U+1EA9 - { 17, 28, 273, 0, 28, 60, 25055 }, // U+1EAA - { 11, 24, 221, 1, 23, 33, 25115 }, // U+1EAB - { 17, 30, 273, 0, 24, 64, 25148 }, // U+1EAC - { 11, 25, 221, 1, 19, 35, 25212 }, // U+1EAD - { 17, 28, 273, 0, 28, 60, 25247 }, // U+1EAE - { 11, 24, 221, 1, 23, 33, 25307 }, // U+1EAF - { 17, 28, 273, 0, 28, 60, 25340 }, // U+1EB0 - { 11, 24, 221, 1, 23, 33, 25400 }, // U+1EB1 - { 17, 28, 273, 0, 28, 60, 25433 }, // U+1EB2 - { 11, 24, 221, 1, 23, 33, 25493 }, // U+1EB3 - { 17, 28, 273, 0, 28, 60, 25526 }, // U+1EB4 - { 11, 24, 221, 1, 23, 33, 25586 }, // U+1EB5 - { 17, 30, 273, 0, 24, 64, 25619 }, // U+1EB6 - { 11, 25, 221, 1, 19, 35, 25683 }, // U+1EB7 - { 12, 25, 238, 2, 19, 38, 25718 }, // U+1EB8 - { 13, 20, 229, 1, 14, 33, 25756 }, // U+1EB9 - { 12, 24, 238, 2, 24, 36, 25789 }, // U+1EBA - { 13, 20, 229, 1, 19, 33, 25825 }, // U+1EBB - { 12, 24, 238, 2, 24, 36, 25858 }, // U+1EBC - { 13, 20, 229, 1, 19, 33, 25894 }, // U+1EBD - { 12, 28, 238, 2, 28, 42, 25927 }, // U+1EBE - { 13, 24, 229, 1, 23, 39, 25969 }, // U+1EBF - { 12, 28, 238, 2, 28, 42, 26008 }, // U+1EC0 - { 13, 24, 229, 1, 23, 39, 26050 }, // U+1EC1 - { 12, 28, 238, 2, 28, 42, 26089 }, // U+1EC2 - { 13, 24, 229, 1, 23, 39, 26131 }, // U+1EC3 - { 12, 28, 238, 2, 28, 42, 26170 }, // U+1EC4 - { 13, 24, 229, 1, 23, 39, 26212 }, // U+1EC5 - { 12, 30, 238, 2, 24, 45, 26251 }, // U+1EC6 - { 13, 25, 229, 1, 19, 41, 26296 }, // U+1EC7 - { 5, 24, 104, 1, 24, 15, 26337 }, // U+1EC8 - { 5, 19, 94, 0, 19, 12, 26352 }, // U+1EC9 - { 4, 25, 104, 1, 19, 13, 26364 }, // U+1ECA - { 4, 25, 94, 1, 19, 13, 26377 }, // U+1ECB - { 17, 25, 304, 1, 19, 54, 26390 }, // U+1ECC - { 13, 20, 235, 1, 14, 33, 26444 }, // U+1ECD - { 17, 25, 304, 1, 24, 54, 26477 }, // U+1ECE - { 13, 20, 235, 1, 19, 33, 26531 }, // U+1ECF - { 17, 29, 304, 1, 28, 62, 26564 }, // U+1ED0 - { 13, 24, 235, 1, 23, 39, 26626 }, // U+1ED1 - { 17, 29, 304, 1, 28, 62, 26665 }, // U+1ED2 - { 13, 24, 235, 1, 23, 39, 26727 }, // U+1ED3 - { 17, 29, 304, 1, 28, 62, 26766 }, // U+1ED4 - { 13, 24, 235, 1, 23, 39, 26828 }, // U+1ED5 - { 17, 29, 304, 1, 28, 62, 26867 }, // U+1ED6 - { 13, 24, 235, 1, 23, 39, 26929 }, // U+1ED7 - { 17, 30, 304, 1, 24, 64, 26968 }, // U+1ED8 - { 13, 25, 235, 1, 19, 41, 27032 }, // U+1ED9 - { 17, 25, 304, 1, 24, 54, 27073 }, // U+1EDA - { 13, 20, 234, 1, 19, 33, 27127 }, // U+1EDB - { 17, 25, 304, 1, 24, 54, 27160 }, // U+1EDC - { 13, 20, 234, 1, 19, 33, 27214 }, // U+1EDD - { 17, 25, 304, 1, 24, 54, 27247 }, // U+1EDE - { 13, 20, 234, 1, 19, 33, 27301 }, // U+1EDF - { 17, 25, 304, 1, 24, 54, 27334 }, // U+1EE0 - { 13, 20, 234, 1, 19, 33, 27388 }, // U+1EE1 - { 17, 27, 304, 1, 21, 58, 27421 }, // U+1EE2 - { 13, 23, 234, 1, 17, 38, 27479 }, // U+1EE3 - { 15, 25, 294, 2, 19, 47, 27517 }, // U+1EE4 - { 12, 20, 233, 1, 14, 30, 27564 }, // U+1EE5 - { 15, 25, 294, 2, 24, 47, 27594 }, // U+1EE6 - { 12, 20, 233, 1, 19, 30, 27641 }, // U+1EE7 - { 17, 25, 293, 2, 24, 54, 27671 }, // U+1EE8 - { 14, 20, 246, 1, 19, 35, 27725 }, // U+1EE9 - { 17, 25, 293, 2, 24, 54, 27760 }, // U+1EEA - { 14, 20, 246, 1, 19, 35, 27814 }, // U+1EEB - { 17, 25, 293, 2, 24, 54, 27849 }, // U+1EEC - { 14, 20, 246, 1, 19, 35, 27903 }, // U+1EED - { 17, 25, 293, 2, 24, 54, 27938 }, // U+1EEE - { 14, 20, 246, 1, 19, 35, 27992 }, // U+1EEF - { 17, 27, 293, 2, 21, 58, 28027 }, // U+1EF0 - { 14, 23, 246, 1, 17, 41, 28085 }, // U+1EF1 - { 17, 24, 268, 0, 24, 51, 28126 }, // U+1EF2 - { 14, 25, 220, 0, 19, 44, 28177 }, // U+1EF3 - { 17, 25, 268, 0, 19, 54, 28221 }, // U+1EF4 - { 14, 20, 220, 0, 14, 35, 28275 }, // U+1EF5 - { 17, 24, 268, 0, 24, 51, 28310 }, // U+1EF6 - { 14, 25, 220, 0, 19, 44, 28361 }, // U+1EF7 - { 17, 24, 268, 0, 24, 51, 28405 }, // U+1EF8 - { 14, 25, 220, 0, 19, 44, 28456 }, // U+1EF9 - { 0, 0, 218, 0, 0, 0, 28500 }, // U+2000 - { 0, 0, 436, 0, 0, 0, 28500 }, // U+2001 - { 0, 0, 200, 0, 0, 0, 28500 }, // U+2002 - { 0, 0, 400, 0, 0, 0, 28500 }, // U+2003 - { 0, 0, 134, 0, 0, 0, 28500 }, // U+2004 - { 0, 0, 100, 0, 0, 0, 28500 }, // U+2005 - { 0, 0, 66, 0, 0, 0, 28500 }, // U+2006 - { 0, 0, 259, 0, 0, 0, 28500 }, // U+2007 - { 0, 0, 109, 0, 0, 0, 28500 }, // U+2008 - { 0, 0, 70, 0, 0, 0, 28500 }, // U+2009 - { 0, 0, 34, 0, 0, 0, 28500 }, // U+200A - { 0, 0, 0, 0, 0, 0, 28500 }, // U+200B - { 0, 0, 0, 0, 0, 0, 28500 }, // U+200C - { 0, 0, 0, 0, 0, 0, 28500 }, // U+200D - { 0, 0, 0, 0, 0, 0, 28500 }, // U+200E - { 0, 0, 0, 0, 0, 0, 28500 }, // U+200F - { 9, 3, 141, 0, 9, 4, 28500 }, // U+2010 - { 9, 3, 141, 0, 9, 4, 28504 }, // U+2011 - { 14, 3, 259, 1, 10, 6, 28508 }, // U+2012 - { 13, 3, 200, 0, 9, 5, 28514 }, // U+2013 - { 25, 3, 400, 0, 9, 10, 28519 }, // U+2014 - { 25, 3, 400, 0, 9, 10, 28529 }, // U+2015 - { 6, 31, 188, 3, 25, 24, 28539 }, // U+2016 - { 13, 6, 178, -1, 0, 10, 28563 }, // U+2017 - { 5, 8, 98, 1, 19, 5, 28573 }, // U+2018 - { 5, 8, 98, 1, 19, 5, 28578 }, // U+2019 - { 4, 8, 91, 1, 3, 4, 28583 }, // U+201A - { 5, 8, 98, 0, 19, 5, 28587 }, // U+201B - { 9, 8, 167, 1, 19, 9, 28592 }, // U+201C - { 9, 8, 167, 1, 19, 9, 28601 }, // U+201D - { 9, 8, 160, 1, 3, 9, 28610 }, // U+201E - { 9, 8, 167, 0, 19, 9, 28619 }, // U+201F - { 10, 15, 193, 1, 19, 19, 28628 }, // U+2020 - { 10, 19, 193, 1, 19, 24, 28647 }, // U+2021 - { 9, 9, 220, 2, 12, 11, 28671 }, // U+2022 - { 7, 9, 220, 4, 12, 8, 28682 }, // U+2023 - { 5, 5, 109, 1, 4, 4, 28690 }, // U+2024 - { 11, 5, 218, 1, 4, 7, 28694 }, // U+2025 - { 18, 5, 328, 1, 4, 12, 28701 }, // U+2026 - { 5, 5, 109, 1, 11, 4, 28713 }, // U+2027 - { 0, 0, 0, 0, 0, 0, 28717 }, // U+202A - { 0, 0, 0, 0, 0, 0, 28717 }, // U+202B - { 0, 0, 0, 0, 0, 0, 28717 }, // U+202C - { 0, 0, 0, 0, 0, 0, 28717 }, // U+202D - { 0, 0, 0, 0, 0, 0, 28717 }, // U+202E - { 0, 0, 70, 0, 0, 0, 28717 }, // U+202F - { 28, 20, 520, 2, 19, 70, 28717 }, // U+2030 - { 37, 20, 660, 2, 19, 93, 28787 }, // U+2031 - { 4, 8, 86, 1, 19, 4, 28880 }, // U+2032 - { 9, 8, 171, 1, 19, 9, 28884 }, // U+2033 - { 14, 8, 256, 1, 19, 14, 28893 }, // U+2034 - { 4, 8, 86, 1, 19, 4, 28907 }, // U+2035 - { 9, 8, 171, 1, 19, 9, 28911 }, // U+2036 - { 14, 8, 256, 1, 19, 14, 28920 }, // U+2037 - { 10, 6, 179, 1, -1, 8, 28934 }, // U+2038 - { 8, 13, 148, 1, 14, 13, 28942 }, // U+2039 - { 9, 13, 148, 0, 14, 15, 28955 }, // U+203A - { 14, 15, 252, 1, 16, 27, 28970 }, // U+203B - { 11, 20, 218, 1, 19, 28, 28997 }, // U+203C - { 12, 20, 201, 0, 19, 30, 29025 }, // U+203D - { 12, 3, 213, 1, 21, 5, 29055 }, // U+203E - { 20, 5, 324, 0, -1, 13, 29060 }, // U+203F - { 20, 5, 324, 0, 19, 13, 29073 }, // U+2040 - { 11, 15, 178, 0, 8, 21, 29086 }, // U+2041 - { 16, 16, 286, 1, 17, 32, 29107 }, // U+2042 - { 8, 3, 220, 3, 9, 3, 29139 }, // U+2043 - { 16, 19, 71, -6, 19, 38, 29142 }, // U+2044 - { 6, 24, 140, 2, 20, 18, 29180 }, // U+2045 - { 6, 24, 140, 1, 20, 18, 29198 }, // U+2046 - { 25, 20, 411, 0, 19, 63, 29216 }, // U+2047 - { 18, 20, 315, 0, 19, 45, 29279 }, // U+2048 - { 18, 20, 315, 1, 19, 45, 29324 }, // U+2049 - { 9, 20, 173, 1, 14, 23, 29369 }, // U+204A - { 12, 19, 238, 2, 19, 29, 29392 }, // U+204B - { 12, 11, 234, 1, 13, 17, 29421 }, // U+204C - { 12, 11, 234, 1, 13, 17, 29438 }, // U+204D - { 11, 12, 197, 1, 9, 17, 29455 }, // U+204E - { 5, 18, 114, 1, 13, 12, 29472 }, // U+204F - { 20, 25, 322, 0, 19, 63, 29484 }, // U+2050 - { 11, 25, 213, 2, 19, 35, 29547 }, // U+2051 - { 16, 19, 285, 1, 19, 38, 29582 }, // U+2052 - { 16, 6, 287, 1, 12, 12, 29620 }, // U+2053 - { 20, 5, 324, 0, -1, 13, 29632 }, // U+2054 - { 11, 11, 216, 1, 12, 16, 29645 }, // U+2055 - { 20, 8, 341, 1, 19, 20, 29661 }, // U+2057 - { 0, 0, 110, 0, 0, 0, 29681 }, // U+205F - { 0, 0, 0, 0, 0, 0, 29681 }, // U+2060 - { 0, 0, 0, 0, 0, 0, 29681 }, // U+2061 - { 0, 0, 0, 0, 0, 0, 29681 }, // U+2062 - { 0, 0, 0, 0, 0, 0, 29681 }, // U+2063 - { 0, 0, 0, 0, 0, 0, 29681 }, // U+2064 - { 0, 0, 0, 0, 0, 0, 29681 }, // U+2066 - { 0, 0, 0, 0, 0, 0, 29681 }, // U+2067 - { 0, 0, 0, 0, 0, 0, 29681 }, // U+2068 - { 0, 0, 0, 0, 0, 0, 29681 }, // U+2069 - { 0, 0, 0, 0, 0, 0, 29681 }, // U+206A - { 0, 0, 0, 0, 0, 0, 29681 }, // U+206B - { 0, 0, 0, 0, 0, 0, 29681 }, // U+206C - { 0, 0, 0, 0, 0, 0, 29681 }, // U+206D - { 0, 0, 0, 0, 0, 0, 29681 }, // U+206E - { 0, 0, 0, 0, 0, 0, 29681 }, // U+206F - { 10, 11, 180, 1, 21, 14, 29681 }, // U+2070 - { 3, 12, 77, 1, 22, 5, 29695 }, // U+2071 - { 10, 11, 179, 1, 21, 14, 29700 }, // U+2074 - { 9, 11, 171, 1, 21, 13, 29714 }, // U+2075 - { 9, 11, 173, 1, 21, 13, 29727 }, // U+2076 - { 9, 11, 158, 0, 21, 13, 29740 }, // U+2077 - { 9, 11, 175, 1, 21, 13, 29753 }, // U+2078 - { 9, 11, 173, 1, 21, 13, 29766 }, // U+2079 - { 9, 7, 171, 1, 19, 8, 29779 }, // U+207A - { 9, 3, 171, 1, 17, 4, 29787 }, // U+207B - { 8, 6, 171, 1, 18, 6, 29791 }, // U+207C - { 5, 13, 104, 1, 22, 9, 29797 }, // U+207D - { 5, 13, 104, 0, 22, 9, 29806 }, // U+207E - { 9, 9, 169, 1, 19, 11, 29815 }, // U+207F - { 10, 11, 180, 1, 6, 14, 29826 }, // U+2080 - { 5, 11, 118, 1, 6, 7, 29840 }, // U+2081 - { 9, 11, 172, 1, 6, 13, 29847 }, // U+2082 - { 9, 11, 174, 1, 6, 13, 29860 }, // U+2083 - { 10, 11, 179, 1, 6, 14, 29873 }, // U+2084 - { 9, 11, 171, 1, 6, 13, 29887 }, // U+2085 - { 9, 11, 173, 1, 6, 13, 29900 }, // U+2086 - { 9, 11, 158, 0, 6, 13, 29913 }, // U+2087 - { 9, 11, 175, 1, 6, 13, 29926 }, // U+2088 - { 9, 11, 173, 1, 6, 13, 29939 }, // U+2089 - { 9, 7, 171, 1, 4, 8, 29952 }, // U+208A - { 9, 3, 171, 1, 2, 4, 29960 }, // U+208B - { 8, 6, 171, 1, 3, 6, 29964 }, // U+208C - { 5, 13, 104, 1, 7, 9, 29970 }, // U+208D - { 5, 13, 104, 0, 7, 9, 29979 }, // U+208E - { 9, 9, 157, 0, 4, 11, 29988 }, // U+2090 - { 10, 9, 162, 0, 4, 12, 29999 }, // U+2091 - { 10, 9, 166, 0, 4, 12, 30011 }, // U+2092 - { 10, 9, 156, 0, 4, 12, 30023 }, // U+2093 - { 10, 11, 177, 1, 5, 14, 30035 }, // U+2094 - { 9, 11, 169, 1, 6, 13, 30049 }, // U+2095 - { 9, 11, 155, 1, 6, 13, 30062 }, // U+2096 - { 3, 11, 77, 1, 6, 5, 30075 }, // U+2097 - { 13, 9, 244, 1, 4, 15, 30080 }, // U+2098 - { 9, 9, 169, 1, 4, 11, 30095 }, // U+2099 - { 9, 11, 170, 1, 4, 13, 30106 }, // U+209A - { 9, 9, 151, 0, 4, 11, 30119 }, // U+209B - { 6, 11, 98, 0, 6, 9, 30130 }, // U+209C - { 16, 20, 288, 1, 19, 40, 30139 }, // U+20A0 - { 16, 24, 291, 1, 21, 48, 30179 }, // U+20A1 - { 16, 20, 289, 1, 19, 40, 30227 }, // U+20A2 - { 15, 19, 240, -1, 19, 36, 30267 }, // U+20A3 - { 13, 19, 242, 1, 19, 31, 30303 }, // U+20A4 - { 19, 18, 340, 1, 16, 43, 30334 }, // U+20A5 - { 19, 19, 305, 0, 19, 46, 30377 }, // U+20A6 - { 21, 20, 376, 2, 19, 53, 30423 }, // U+20A7 - { 25, 20, 444, 2, 19, 63, 30476 }, // U+20A8 - { 24, 19, 379, 0, 19, 57, 30539 }, // U+20A9 - { 17, 14, 323, 2, 14, 30, 30596 }, // U+20AA - { 15, 25, 241, 1, 19, 47, 30626 }, // U+20AB - { 16, 20, 264, 0, 19, 40, 30673 }, // U+20AC - { 18, 19, 281, 0, 19, 43, 30713 }, // U+20AD - { 14, 19, 255, 1, 19, 34, 30756 }, // U+20AE - { 33, 27, 550, 1, 21, 112, 30790 }, // U+20AF - { 16, 19, 263, 0, 19, 38, 30902 }, // U+20B1 - { 17, 24, 297, 1, 21, 51, 30940 }, // U+20B2 - { 17, 19, 273, 0, 19, 41, 30991 }, // U+20B3 - { 14, 20, 250, 1, 19, 35, 31032 }, // U+20B4 - { 16, 24, 291, 1, 21, 48, 31067 }, // U+20B5 - { 14, 19, 255, 1, 19, 34, 31115 }, // U+20B8 - { 13, 19, 214, 0, 19, 31, 31149 }, // U+20B9 - { 13, 20, 235, 1, 19, 33, 31180 }, // U+20BA - { 16, 14, 288, 1, 14, 28, 31213 }, // U+20BC - { 17, 19, 277, 0, 19, 41, 31241 }, // U+20BD - { 16, 19, 287, 1, 19, 38, 31282 }, // U+20BE - { 13, 24, 260, 2, 21, 39, 31320 }, // U+20BF - { 19, 16, 379, 2, 16, 38, 31359 }, // U+2190 - { 17, 19, 331, 2, 19, 41, 31397 }, // U+2191 - { 19, 16, 379, 3, 16, 38, 31438 }, // U+2192 - { 17, 20, 331, 2, 19, 43, 31476 }, // U+2193 - { 29, 16, 534, 2, 16, 58, 31519 }, // U+2194 - { 17, 30, 331, 2, 23, 64, 31577 }, // U+2195 - { 15, 15, 306, 2, 14, 29, 31641 }, // U+2196 - { 15, 15, 306, 2, 14, 29, 31670 }, // U+2197 - { 15, 14, 306, 2, 14, 27, 31699 }, // U+2198 - { 15, 14, 306, 2, 14, 27, 31726 }, // U+2199 - { 25, 19, 455, 2, 19, 60, 31753 }, // U+21A9 - { 25, 19, 455, 2, 19, 60, 31813 }, // U+21AA - { 21, 19, 392, 2, 19, 50, 31873 }, // U+21B0 - { 21, 19, 392, 2, 19, 50, 31923 }, // U+21B1 - { 13, 17, 328, 3, 17, 28, 31973 }, // U+21B2 - { 21, 17, 392, 2, 19, 45, 32001 }, // U+21B3 - { 17, 20, 330, 2, 19, 43, 32046 }, // U+21B4 - { 21, 17, 392, 2, 19, 45, 32089 }, // U+21B5 - { 21, 16, 328, 0, 17, 42, 32134 }, // U+21B6 - { 21, 16, 328, 0, 17, 42, 32176 }, // U+21B7 - { 19, 23, 379, 2, 23, 55, 32218 }, // U+21BA - { 19, 23, 379, 2, 23, 55, 32273 }, // U+21BB - { 19, 20, 328, 1, 19, 48, 32328 }, // U+21C4 - { 19, 20, 328, 1, 19, 48, 32376 }, // U+21C6 - { 19, 17, 379, 2, 16, 41, 32424 }, // U+21D0 - { 19, 17, 379, 3, 16, 41, 32465 }, // U+21D2 - { 29, 17, 534, 2, 16, 62, 32506 }, // U+21D4 - { 11, 19, 240, 2, 19, 27, 32568 }, // U+21DE - { 11, 20, 240, 2, 19, 28, 32595 }, // U+21DF - { 21, 12, 403, 2, 15, 32, 32623 }, // U+21E4 - { 21, 12, 403, 2, 15, 32, 32655 }, // U+21E5 - { 22, 20, 409, 2, 20, 55, 32687 }, // U+21E7 - { 22, 24, 409, 2, 20, 66, 32742 }, // U+21EA - { 13, 20, 250, 1, 19, 33, 32808 }, // U+2202 - { 15, 16, 271, 1, 15, 30, 32841 }, // U+2205 - { 17, 19, 269, 0, 19, 41, 32871 }, // U+2206 - { 14, 22, 286, 2, 19, 39, 32912 }, // U+220F - { 15, 23, 251, 0, 19, 44, 32951 }, // U+2211 - { 12, 3, 262, 2, 9, 5, 32995 }, // U+2212 - { 15, 17, 59, -6, 17, 32, 33000 }, // U+2215 - { 15, 19, 249, 0, 19, 36, 33032 }, // U+221A - { 21, 11, 371, 1, 13, 29, 33068 }, // U+221E - { 10, 26, 144, -1, 20, 33, 33097 }, // U+222B - { 5, 15, 109, 1, 17, 10, 33130 }, // U+2236 - { 14, 11, 262, 1, 13, 20, 33140 }, // U+2248 - { 12, 14, 262, 2, 14, 21, 33160 }, // U+2260 - { 12, 14, 262, 2, 14, 21, 33181 }, // U+2264 - { 12, 14, 262, 2, 14, 21, 33202 }, // U+2265 - { 35, 35, 556, 0, 27, 154, 33223 }, // U+2295 - { 35, 35, 556, 0, 27, 154, 33377 }, // U+2296 - { 35, 35, 556, 0, 27, 154, 33531 }, // U+2297 - { 35, 35, 556, 0, 27, 154, 33685 }, // U+2298 - { 13, 19, 227, 0, 19, 31, 33839 }, // U+FB01 - { 15, 19, 238, 0, 19, 36, 33870 }, // U+FB02 + { 4, 7, 0, -2, 18, 4, 14315 }, // U+0312 + { 5, 8, 0, 4, 13, 5, 14319 }, // U+0313 + { 5, 8, 0, 4, 13, 5, 14324 }, // U+0315 + { 4, 6, 0, -5, 18, 3, 14329 }, // U+031B + { 4, 4, 94, 1, -2, 2, 14332 }, // U+0323 + { 4, 6, 0, -10, -1, 3, 14334 }, // U+0326 + { 6, 7, 0, 1, 1, 6, 14337 }, // U+0327 + { 5, 7, 0, 1, 1, 5, 14343 }, // U+0328 + { 8, 4, 0, 1, -2, 4, 14348 }, // U+032C + { 11, 17, 0, -13, 15, 24, 14352 }, // U+0337 + { 15, 20, 0, -15, 19, 38, 14376 }, // U+0338 + { 8, 4, 0, -11, 19, 4, 14414 }, // U+0342 + { 4, 6, 0, 0, 16, 3, 14418 }, // U+0343 + { 9, 6, 0, -12, 20, 7, 14421 }, // U+0346 + { 8, 6, 0, -12, -3, 6, 14428 }, // U+0347 + { 7, 6, 0, -10, 0, 6, 14434 }, // U+0348 + { 6, 6, 0, -10, -1, 5, 14440 }, // U+0349 + { 9, 6, 0, -12, 20, 7, 14445 }, // U+034A + { 10, 10, 0, -13, 23, 13, 14452 }, // U+034B + { 10, 6, 0, -12, 21, 8, 14465 }, // U+034C + { 11, 6, 0, -13, -1, 9, 14473 }, // U+034D + { 7, 6, 0, -11, -1, 6, 14482 }, // U+034E + { 0, 0, 0, 0, 0, 0, 14488 }, // U+034F + { 10, 10, 0, -12, 25, 13, 14488 }, // U+0350 + { 3, 6, 0, -8, 19, 3, 14501 }, // U+0351 + { 8, 7, 0, -11, 21, 7, 14504 }, // U+0352 + { 7, 7, 0, -10, 0, 7, 14511 }, // U+0353 + { 7, 7, 0, -10, 0, 7, 14518 }, // U+0354 + { 7, 7, 0, -9, 0, 7, 14525 }, // U+0355 + { 14, 7, 0, -12, 0, 13, 14532 }, // U+0356 + { 4, 7, 0, 0, 21, 4, 14545 }, // U+0357 + { 4, 4, 0, 1, 19, 2, 14549 }, // U+0358 + { 6, 7, 0, -10, 0, 6, 14551 }, // U+0359 + { 10, 6, 0, -12, 0, 8, 14557 }, // U+035A + { 6, 7, 0, -10, 21, 6, 14565 }, // U+035B + { 18, 5, 0, -9, 0, 12, 14571 }, // U+035C + { 18, 5, 0, -9, 19, 12, 14583 }, // U+035D + { 16, 3, 0, -15, 19, 6, 14595 }, // U+035E + { 16, 3, 0, -15, -2, 6, 14601 }, // U+035F + { 24, 4, 0, -12, 19, 12, 14607 }, // U+0360 + { 18, 5, 0, -9, 19, 12, 14619 }, // U+0361 + { 20, 6, 0, -9, -1, 15, 14631 }, // U+0362 + { 7, 8, 0, -10, 22, 7, 14646 }, // U+0363 + { 7, 8, 0, -10, 22, 7, 14653 }, // U+0364 + { 2, 10, 0, -8, 24, 3, 14660 }, // U+0365 + { 8, 8, 0, -11, 22, 8, 14663 }, // U+0366 + { 6, 8, 0, -10, 22, 6, 14671 }, // U+0367 + { 7, 8, 0, -11, 22, 7, 14677 }, // U+0368 + { 7, 11, 0, -11, 25, 10, 14684 }, // U+0369 + { 6, 11, 0, -10, 25, 9, 14694 }, // U+036A + { 10, 8, 0, -12, 22, 10, 14703 }, // U+036B + { 5, 8, 0, -9, 22, 5, 14713 }, // U+036C + { 5, 10, 0, 0, 24, 7, 14718 }, // U+036D + { 8, 8, 0, -11, 22, 8, 14725 }, // U+036E + { 7, 8, 0, -10, 22, 7, 14733 }, // U+036F + { 12, 24, 238, 2, 24, 36, 14740 }, // U+0400 + { 12, 24, 238, 2, 24, 36, 14776 }, // U+0401 + { 19, 21, 334, 1, 19, 50, 14812 }, // U+0402 + { 12, 24, 228, 2, 24, 36, 14862 }, // U+0403 + { 16, 20, 291, 1, 19, 40, 14898 }, // U+0404 + { 14, 20, 254, 1, 19, 35, 14938 }, // U+0405 + { 3, 19, 104, 2, 19, 8, 14973 }, // U+0406 + { 9, 24, 104, -1, 24, 27, 14981 }, // U+0407 + { 11, 20, 225, 1, 19, 28, 15008 }, // U+0408 + { 26, 19, 442, 1, 19, 62, 15036 }, // U+0409 + { 25, 19, 447, 2, 19, 60, 15098 }, // U+040A + { 20, 19, 351, 1, 19, 48, 15158 }, // U+040B + { 14, 24, 265, 2, 24, 42, 15206 }, // U+040C + { 15, 24, 297, 2, 24, 45, 15248 }, // U+040D + { 15, 24, 248, 0, 24, 45, 15293 }, // U+040E + { 14, 23, 286, 2, 19, 41, 15338 }, // U+040F + { 17, 19, 273, 0, 19, 41, 15379 }, // U+0410 + { 13, 19, 255, 2, 19, 31, 15420 }, // U+0411 + { 13, 19, 260, 2, 19, 31, 15451 }, // U+0412 + { 12, 19, 228, 2, 19, 29, 15482 }, // U+0413 + { 19, 23, 323, 1, 19, 55, 15511 }, // U+0414 + { 12, 19, 238, 2, 19, 29, 15566 }, // U+0415 + { 24, 19, 380, 0, 19, 57, 15595 }, // U+0416 + { 13, 20, 245, 1, 19, 33, 15652 }, // U+0417 + { 15, 19, 297, 2, 19, 36, 15685 }, // U+0418 + { 15, 24, 297, 2, 24, 45, 15721 }, // U+0419 + { 14, 19, 265, 2, 19, 34, 15766 }, // U+041A + { 16, 19, 296, 1, 19, 38, 15800 }, // U+041B + { 19, 19, 357, 2, 19, 46, 15838 }, // U+041C + { 15, 19, 294, 2, 19, 36, 15884 }, // U+041D + { 17, 20, 304, 1, 19, 43, 15920 }, // U+041E + { 15, 19, 294, 2, 19, 36, 15963 }, // U+041F + { 13, 19, 253, 2, 19, 31, 15999 }, // U+0420 + { 16, 20, 291, 1, 19, 40, 16030 }, // U+0421 + { 14, 19, 255, 1, 19, 34, 16070 }, // U+0422 + { 15, 19, 248, 0, 19, 36, 16104 }, // U+0423 + { 19, 22, 330, 1, 20, 53, 16140 }, // U+0424 + { 17, 19, 270, 0, 19, 41, 16193 }, // U+0425 + { 16, 23, 286, 2, 19, 46, 16234 }, // U+0426 + { 15, 19, 277, 1, 19, 36, 16280 }, // U+0427 + { 20, 19, 380, 2, 19, 48, 16316 }, // U+0428 + { 22, 23, 385, 2, 19, 64, 16364 }, // U+0429 + { 19, 19, 323, 1, 19, 46, 16428 }, // U+042A + { 18, 19, 347, 2, 19, 43, 16474 }, // U+042B + { 13, 19, 257, 2, 19, 31, 16517 }, // U+042C + { 16, 20, 291, 1, 19, 40, 16548 }, // U+042D + { 22, 20, 404, 2, 19, 55, 16588 }, // U+042E + { 14, 19, 256, 0, 19, 34, 16643 }, // U+042F + { 11, 15, 221, 1, 14, 21, 16677 }, // U+0430 + { 13, 20, 233, 1, 19, 33, 16698 }, // U+0431 + { 12, 14, 222, 1, 14, 21, 16731 }, // U+0432 + { 10, 14, 173, 1, 14, 18, 16752 }, // U+0433 + { 15, 18, 244, 0, 14, 34, 16770 }, // U+0434 + { 13, 15, 229, 1, 14, 25, 16804 }, // U+0435 + { 20, 14, 321, 0, 14, 35, 16829 }, // U+0436 + { 11, 15, 195, 1, 14, 21, 16864 }, // U+0437 + { 12, 14, 233, 1, 14, 21, 16885 }, // U+0438 + { 12, 19, 233, 1, 19, 29, 16906 }, // U+0439 + { 13, 14, 213, 1, 14, 23, 16935 }, // U+043A + { 13, 14, 228, 0, 14, 23, 16958 }, // U+043B + { 17, 14, 304, 1, 14, 30, 16981 }, // U+043C + { 12, 14, 232, 1, 14, 21, 17011 }, // U+043D + { 13, 15, 235, 1, 14, 25, 17032 }, // U+043E + { 12, 14, 232, 1, 14, 21, 17057 }, // U+043F + { 13, 20, 241, 1, 14, 33, 17078 }, // U+0440 + { 12, 15, 224, 1, 14, 23, 17111 }, // U+0441 + { 12, 14, 186, 0, 14, 21, 17134 }, // U+0442 + { 14, 20, 220, 0, 14, 35, 17155 }, // U+0443 + { 15, 25, 271, 1, 19, 47, 17190 }, // U+0444 + { 13, 14, 215, 0, 14, 23, 17237 }, // U+0445 + { 14, 18, 236, 1, 14, 32, 17260 }, // U+0446 + { 12, 14, 228, 1, 14, 21, 17292 }, // U+0447 + { 18, 14, 326, 1, 14, 32, 17313 }, // U+0448 + { 20, 18, 331, 1, 14, 45, 17345 }, // U+0449 + { 15, 14, 255, 0, 14, 27, 17390 }, // U+044A + { 16, 14, 290, 1, 14, 28, 17417 }, // U+044B + { 12, 14, 220, 1, 14, 21, 17445 }, // U+044C + { 12, 15, 224, 1, 14, 23, 17466 }, // U+044D + { 19, 15, 332, 1, 14, 36, 17489 }, // U+044E + { 12, 14, 219, 0, 14, 21, 17525 }, // U+044F + { 13, 20, 229, 1, 19, 33, 17546 }, // U+0450 + { 13, 20, 229, 1, 19, 33, 17579 }, // U+0451 + { 13, 25, 233, 0, 19, 41, 17612 }, // U+0452 + { 10, 19, 173, 1, 19, 24, 17653 }, // U+0453 + { 12, 15, 224, 1, 14, 23, 17677 }, // U+0454 + { 11, 15, 206, 1, 14, 21, 17700 }, // U+0455 + { 4, 19, 94, 1, 19, 10, 17721 }, // U+0456 + { 9, 19, 94, -2, 19, 22, 17731 }, // U+0457 + { 6, 25, 94, -1, 19, 19, 17753 }, // U+0458 + { 22, 14, 367, 0, 14, 39, 17772 }, // U+0459 + { 21, 14, 356, 1, 14, 37, 17811 }, // U+045A + { 13, 19, 233, 0, 19, 31, 17848 }, // U+045B + { 13, 19, 213, 1, 19, 31, 17879 }, // U+045C + { 12, 19, 233, 1, 19, 29, 17910 }, // U+045D + { 14, 25, 220, 0, 19, 44, 17939 }, // U+045E + { 12, 18, 232, 1, 14, 27, 17983 }, // U+045F + { 19, 20, 355, 2, 19, 48, 18010 }, // U+0460 + { 18, 15, 319, 1, 14, 34, 18058 }, // U+0461 + { 16, 19, 274, 0, 19, 38, 18092 }, // U+0462 + { 15, 19, 243, 0, 19, 36, 18130 }, // U+0463 + { 21, 20, 379, 2, 19, 53, 18166 }, // U+0464 + { 18, 15, 309, 1, 14, 34, 18219 }, // U+0465 + { 17, 19, 273, 0, 19, 41, 18253 }, // U+0466 + { 14, 14, 226, 0, 14, 25, 18294 }, // U+0467 + { 21, 19, 372, 2, 19, 50, 18319 }, // U+0468 + { 18, 14, 308, 1, 14, 32, 18369 }, // U+0469 + { 19, 19, 338, 1, 19, 46, 18401 }, // U+046A + { 16, 14, 277, 1, 14, 28, 18447 }, // U+046B + { 25, 19, 455, 2, 19, 60, 18475 }, // U+046C + { 22, 14, 381, 1, 14, 39, 18535 }, // U+046D + { 12, 30, 215, 1, 24, 45, 18574 }, // U+046E + { 10, 25, 205, 1, 19, 32, 18619 }, // U+046F + { 18, 19, 349, 2, 19, 43, 18651 }, // U+0470 + { 17, 23, 308, 1, 17, 49, 18694 }, // U+0471 + { 17, 20, 304, 1, 19, 43, 18743 }, // U+0472 + { 13, 15, 235, 1, 14, 25, 18786 }, // U+0473 + { 17, 19, 279, 0, 19, 41, 18811 }, // U+0474 + { 15, 14, 234, 0, 14, 27, 18852 }, // U+0475 + { 17, 24, 279, 0, 24, 51, 18879 }, // U+0476 + { 15, 19, 234, 0, 19, 36, 18930 }, // U+0477 + { 32, 25, 525, 1, 19, 100, 18966 }, // U+0478 + { 27, 20, 448, 1, 14, 68, 19066 }, // U+0479 + { 16, 19, 283, 1, 19, 38, 19134 }, // U+0480 + { 12, 19, 222, 1, 14, 29, 19172 }, // U+0481 + { 14, 17, 250, 1, 17, 30, 19201 }, // U+0482 + { 10, 5, 0, -12, 19, 7, 19231 }, // U+0483 + { 11, 4, 0, -12, 19, 6, 19238 }, // U+0484 + { 4, 5, 0, -9, 20, 3, 19244 }, // U+0485 + { 4, 5, 0, -8, 20, 3, 19247 }, // U+0486 + { 11, 4, 0, -12, 19, 6, 19250 }, // U+0487 + { 25, 22, 0, -19, 18, 69, 19256 }, // U+0488 + { 24, 25, 0, -19, 19, 75, 19325 }, // U+0489 + { 17, 28, 297, 2, 24, 60, 19400 }, // U+048A + { 15, 23, 233, 1, 19, 44, 19460 }, // U+048B + { 16, 19, 274, 0, 19, 38, 19504 }, // U+048C + { 15, 17, 243, 0, 17, 32, 19542 }, // U+048D + { 13, 19, 251, 2, 19, 31, 19574 }, // U+048E + { 13, 20, 241, 1, 14, 33, 19605 }, // U+048F + { 12, 22, 228, 2, 22, 33, 19638 }, // U+0490 + { 10, 17, 173, 1, 17, 22, 19671 }, // U+0491 + { 15, 19, 228, -1, 19, 36, 19693 }, // U+0492 + { 11, 14, 173, 0, 14, 20, 19729 }, // U+0493 + { 13, 21, 258, 2, 19, 35, 19749 }, // U+0494 + { 12, 18, 214, 1, 14, 27, 19784 }, // U+0495 + { 25, 23, 388, 0, 19, 72, 19811 }, // U+0496 + { 21, 18, 328, 0, 14, 48, 19883 }, // U+0497 + { 13, 25, 245, 1, 19, 41, 19931 }, // U+0498 + { 11, 20, 195, 1, 14, 28, 19972 }, // U+0499 + { 16, 23, 280, 2, 19, 46, 20000 }, // U+049A + { 13, 18, 222, 1, 14, 30, 20046 }, // U+049B + { 14, 19, 263, 2, 19, 34, 20076 }, // U+049C + { 14, 14, 231, 1, 14, 25, 20110 }, // U+049D + { 20, 19, 332, 1, 19, 48, 20135 }, // U+04A0 + { 16, 14, 249, 0, 14, 28, 20183 }, // U+04A1 + { 17, 23, 300, 2, 19, 49, 20211 }, // U+04A2 + { 14, 18, 238, 1, 14, 32, 20260 }, // U+04A3 + { 22, 19, 391, 2, 19, 53, 20292 }, // U+04A4 + { 16, 14, 270, 1, 14, 28, 20345 }, // U+04A5 + { 25, 21, 440, 2, 19, 66, 20373 }, // U+04A6 + { 21, 18, 366, 1, 14, 48, 20439 }, // U+04A7 + { 23, 20, 391, 1, 19, 58, 20487 }, // U+04A8 + { 18, 15, 309, 1, 14, 34, 20545 }, // U+04A9 + { 16, 25, 291, 1, 19, 50, 20579 }, // U+04AA + { 12, 20, 224, 1, 14, 30, 20629 }, // U+04AB + { 14, 23, 255, 1, 19, 41, 20659 }, // U+04AC + { 12, 18, 186, 0, 14, 27, 20700 }, // U+04AD + { 17, 19, 268, 0, 19, 41, 20727 }, // U+04AE + { 14, 20, 220, 0, 14, 35, 20768 }, // U+04AF + { 17, 19, 268, 0, 19, 41, 20803 }, // U+04B0 + { 14, 20, 220, 0, 14, 35, 20844 }, // U+04B1 + { 18, 23, 277, 0, 19, 52, 20879 }, // U+04B2 + { 14, 18, 224, 0, 14, 32, 20931 }, // U+04B3 + { 22, 23, 364, 1, 19, 64, 20963 }, // U+04B4 + { 17, 18, 262, 0, 14, 39, 21027 }, // U+04B5 + { 16, 23, 282, 1, 19, 46, 21066 }, // U+04B6 + { 14, 18, 233, 1, 14, 32, 21112 }, // U+04B7 + { 15, 19, 277, 1, 19, 36, 21144 }, // U+04B8 + { 12, 14, 228, 1, 14, 21, 21180 }, // U+04B9 + { 14, 19, 277, 2, 19, 34, 21201 }, // U+04BA + { 12, 19, 233, 1, 19, 29, 21235 }, // U+04BB + { 20, 20, 335, 0, 19, 50, 21264 }, // U+04BC + { 17, 15, 283, 0, 14, 32, 21314 }, // U+04BD + { 20, 23, 335, 0, 19, 58, 21346 }, // U+04BE + { 17, 20, 283, 0, 14, 43, 21404 }, // U+04BF + { 3, 19, 104, 2, 19, 8, 21447 }, // U+04C0 + { 24, 24, 380, 0, 24, 72, 21455 }, // U+04C1 + { 20, 19, 321, 0, 19, 48, 21527 }, // U+04C2 + { 14, 23, 270, 2, 19, 41, 21575 }, // U+04C3 + { 13, 18, 232, 1, 14, 30, 21616 }, // U+04C4 + { 18, 23, 296, 1, 19, 52, 21646 }, // U+04C5 + { 15, 18, 228, 0, 14, 34, 21698 }, // U+04C6 + { 15, 25, 294, 2, 19, 47, 21732 }, // U+04C7 + { 12, 20, 232, 1, 14, 30, 21779 }, // U+04C8 + { 17, 23, 294, 2, 19, 49, 21809 }, // U+04C9 + { 15, 18, 232, 1, 14, 34, 21858 }, // U+04CA + { 16, 23, 282, 1, 19, 46, 21892 }, // U+04CB + { 14, 18, 233, 1, 14, 32, 21938 }, // U+04CC + { 21, 23, 357, 2, 19, 61, 21970 }, // U+04CD + { 19, 18, 304, 1, 14, 43, 22031 }, // U+04CE + { 4, 19, 94, 1, 19, 10, 22074 }, // U+04CF + { 17, 24, 273, 0, 24, 51, 22084 }, // U+04D0 + { 11, 20, 221, 1, 19, 28, 22135 }, // U+04D1 + { 17, 24, 273, 0, 24, 51, 22163 }, // U+04D2 + { 11, 20, 221, 1, 19, 28, 22214 }, // U+04D3 + { 24, 19, 395, 0, 19, 57, 22242 }, // U+04D4 + { 21, 15, 363, 1, 14, 40, 22299 }, // U+04D5 + { 12, 24, 238, 2, 24, 36, 22339 }, // U+04D6 + { 13, 20, 229, 1, 19, 33, 22375 }, // U+04D7 + { 17, 20, 297, 1, 19, 43, 22408 }, // U+04D8 + { 13, 15, 229, 1, 14, 25, 22451 }, // U+04D9 + { 17, 25, 297, 1, 24, 54, 22476 }, // U+04DA + { 13, 20, 229, 1, 19, 33, 22530 }, // U+04DB + { 24, 24, 380, 0, 24, 72, 22563 }, // U+04DC + { 20, 19, 321, 0, 19, 48, 22635 }, // U+04DD + { 13, 25, 245, 1, 24, 41, 22683 }, // U+04DE + { 11, 20, 195, 1, 19, 28, 22724 }, // U+04DF + { 13, 20, 246, 1, 19, 33, 22752 }, // U+04E0 + { 13, 20, 246, 1, 14, 33, 22785 }, // U+04E1 + { 15, 23, 297, 2, 23, 44, 22818 }, // U+04E2 + { 12, 18, 233, 1, 18, 27, 22862 }, // U+04E3 + { 15, 24, 297, 2, 24, 45, 22889 }, // U+04E4 + { 12, 19, 233, 1, 19, 29, 22934 }, // U+04E5 + { 17, 25, 304, 1, 24, 54, 22963 }, // U+04E6 + { 13, 20, 235, 1, 19, 33, 23017 }, // U+04E7 + { 17, 20, 304, 1, 19, 43, 23050 }, // U+04E8 + { 13, 15, 235, 1, 14, 25, 23093 }, // U+04E9 + { 17, 25, 304, 1, 24, 54, 23118 }, // U+04EA + { 13, 20, 235, 1, 19, 33, 23172 }, // U+04EB + { 16, 25, 291, 1, 24, 50, 23205 }, // U+04EC + { 12, 20, 224, 1, 19, 30, 23255 }, // U+04ED + { 15, 23, 248, 0, 23, 44, 23285 }, // U+04EE + { 14, 24, 220, 0, 18, 42, 23329 }, // U+04EF + { 15, 24, 248, 0, 24, 45, 23371 }, // U+04F0 + { 14, 25, 220, 0, 19, 44, 23416 }, // U+04F1 + { 15, 24, 248, 0, 24, 45, 23460 }, // U+04F2 + { 14, 25, 220, 0, 19, 44, 23505 }, // U+04F3 + { 15, 24, 277, 1, 24, 45, 23549 }, // U+04F4 + { 12, 19, 228, 1, 19, 29, 23594 }, // U+04F5 + { 12, 23, 228, 2, 19, 35, 23623 }, // U+04F6 + { 10, 18, 173, 1, 14, 23, 23658 }, // U+04F7 + { 18, 24, 347, 2, 24, 54, 23681 }, // U+04F8 + { 16, 19, 290, 1, 19, 38, 23735 }, // U+04F9 + { 14, 25, 235, 0, 19, 44, 23773 }, // U+04FA + { 11, 20, 173, 0, 14, 28, 23817 }, // U+04FB + { 18, 25, 279, 0, 19, 57, 23845 }, // U+04FC + { 13, 20, 205, 0, 14, 33, 23902 }, // U+04FD + { 17, 19, 270, 0, 19, 41, 23935 }, // U+04FE + { 13, 14, 215, 0, 14, 23, 23976 }, // U+04FF + { 13, 15, 251, 1, 15, 25, 23999 }, // U+05D0 + { 12, 15, 221, 1, 15, 23, 24024 }, // U+05D1 + { 8, 15, 168, 1, 15, 15, 24047 }, // U+05D2 + { 13, 15, 211, 0, 15, 25, 24062 }, // U+05D3 + { 12, 15, 255, 2, 15, 23, 24087 }, // U+05D4 + { 3, 15, 104, 2, 15, 6, 24110 }, // U+05D5 + { 8, 15, 152, 1, 15, 15, 24116 }, // U+05D6 + { 13, 15, 259, 2, 15, 25, 24131 }, // U+05D7 + { 14, 15, 258, 1, 15, 27, 24156 }, // U+05D8 + { 4, 8, 96, 1, 15, 4, 24183 }, // U+05D9 + { 11, 20, 207, 0, 15, 28, 24187 }, // U+05DA + { 11, 15, 212, 1, 15, 21, 24215 }, // U+05DB + { 12, 19, 190, 0, 19, 29, 24236 }, // U+05DC + { 13, 15, 259, 2, 15, 25, 24265 }, // U+05DD + { 14, 15, 262, 1, 15, 27, 24290 }, // U+05DE + { 3, 20, 104, 2, 15, 8, 24317 }, // U+05DF + { 6, 15, 139, 1, 15, 12, 24325 }, // U+05E0 + { 14, 15, 257, 1, 15, 27, 24337 }, // U+05E1 + { 14, 16, 237, 0, 15, 28, 24364 }, // U+05E2 + { 13, 20, 252, 1, 15, 33, 24392 }, // U+05E3 + { 13, 15, 249, 2, 15, 25, 24425 }, // U+05E4 + { 12, 20, 206, 0, 15, 30, 24450 }, // U+05E5 + { 12, 15, 217, 0, 15, 23, 24480 }, // U+05E6 + { 13, 20, 243, 2, 15, 33, 24503 }, // U+05E7 + { 12, 15, 210, 0, 15, 23, 24536 }, // U+05E8 + { 16, 15, 301, 1, 15, 30, 24559 }, // U+05E9 + { 14, 15, 266, 1, 15, 27, 24589 }, // U+05EA + { 4, 7, 131, 2, 6, 4, 24616 }, // U+060C + { 5, 14, 138, 1, 13, 9, 24620 }, // U+061B + { 11, 20, 174, 0, 19, 28, 24629 }, // U+061F + { 10, 11, 164, 0, 9, 14, 24657 }, // U+0621 + { 8, 3, 108, 0, 2, 3, 24671 }, // U+0640 + { 5, 5, 229, 5, 10, 4, 24674 }, // U+0660 + { 5, 19, 229, 5, 18, 12, 24678 }, // U+0661 + { 11, 19, 229, 2, 18, 27, 24690 }, // U+0662 + { 14, 19, 229, 0, 18, 34, 24717 }, // U+0663 + { 11, 19, 229, 2, 18, 27, 24751 }, // U+0664 + { 11, 14, 229, 2, 13, 20, 24778 }, // U+0665 + { 12, 19, 229, 1, 18, 29, 24798 }, // U+0666 + { 14, 19, 229, 0, 18, 34, 24827 }, // U+0667 + { 14, 19, 229, 0, 18, 34, 24861 }, // U+0668 + { 11, 18, 229, 2, 17, 25, 24895 }, // U+0669 + { 16, 14, 274, 0, 8, 28, 24920 }, // U+06BA + { 6, 2, 120, 1, 2, 2, 24948 }, // U+06D4 + { 5, 5, 229, 5, 10, 4, 24950 }, // U+06F0 + { 5, 19, 229, 5, 18, 12, 24954 }, // U+06F1 + { 11, 19, 229, 2, 18, 27, 24966 }, // U+06F2 + { 14, 19, 229, 0, 18, 34, 24993 }, // U+06F3 + { 11, 17, 229, 2, 17, 24, 25027 }, // U+06F4 + { 12, 17, 229, 1, 17, 26, 25051 }, // U+06F5 + { 10, 18, 229, 2, 17, 23, 25077 }, // U+06F6 + { 14, 19, 229, 0, 18, 34, 25100 }, // U+06F7 + { 14, 19, 229, 0, 18, 34, 25134 }, // U+06F8 + { 11, 18, 229, 2, 17, 25, 25168 }, // U+06F9 + { 17, 25, 273, 0, 19, 54, 25193 }, // U+1EA0 + { 11, 20, 221, 1, 14, 28, 25247 }, // U+1EA1 + { 17, 24, 273, 0, 24, 51, 25275 }, // U+1EA2 + { 11, 20, 221, 1, 19, 28, 25326 }, // U+1EA3 + { 17, 28, 273, 0, 28, 60, 25354 }, // U+1EA4 + { 12, 24, 221, 1, 23, 36, 25414 }, // U+1EA5 + { 17, 28, 273, 0, 28, 60, 25450 }, // U+1EA6 + { 11, 24, 221, 1, 23, 33, 25510 }, // U+1EA7 + { 17, 28, 273, 0, 28, 60, 25543 }, // U+1EA8 + { 11, 24, 221, 1, 23, 33, 25603 }, // U+1EA9 + { 17, 28, 273, 0, 28, 60, 25636 }, // U+1EAA + { 11, 24, 221, 1, 23, 33, 25696 }, // U+1EAB + { 17, 30, 273, 0, 24, 64, 25729 }, // U+1EAC + { 11, 25, 221, 1, 19, 35, 25793 }, // U+1EAD + { 17, 28, 273, 0, 28, 60, 25828 }, // U+1EAE + { 11, 24, 221, 1, 23, 33, 25888 }, // U+1EAF + { 17, 28, 273, 0, 28, 60, 25921 }, // U+1EB0 + { 11, 24, 221, 1, 23, 33, 25981 }, // U+1EB1 + { 17, 28, 273, 0, 28, 60, 26014 }, // U+1EB2 + { 11, 24, 221, 1, 23, 33, 26074 }, // U+1EB3 + { 17, 28, 273, 0, 28, 60, 26107 }, // U+1EB4 + { 11, 24, 221, 1, 23, 33, 26167 }, // U+1EB5 + { 17, 30, 273, 0, 24, 64, 26200 }, // U+1EB6 + { 11, 25, 221, 1, 19, 35, 26264 }, // U+1EB7 + { 12, 25, 238, 2, 19, 38, 26299 }, // U+1EB8 + { 13, 20, 229, 1, 14, 33, 26337 }, // U+1EB9 + { 12, 24, 238, 2, 24, 36, 26370 }, // U+1EBA + { 13, 20, 229, 1, 19, 33, 26406 }, // U+1EBB + { 12, 24, 238, 2, 24, 36, 26439 }, // U+1EBC + { 13, 20, 229, 1, 19, 33, 26475 }, // U+1EBD + { 12, 28, 238, 2, 28, 42, 26508 }, // U+1EBE + { 13, 24, 229, 1, 23, 39, 26550 }, // U+1EBF + { 12, 28, 238, 2, 28, 42, 26589 }, // U+1EC0 + { 13, 24, 229, 1, 23, 39, 26631 }, // U+1EC1 + { 12, 28, 238, 2, 28, 42, 26670 }, // U+1EC2 + { 13, 24, 229, 1, 23, 39, 26712 }, // U+1EC3 + { 12, 28, 238, 2, 28, 42, 26751 }, // U+1EC4 + { 13, 24, 229, 1, 23, 39, 26793 }, // U+1EC5 + { 12, 30, 238, 2, 24, 45, 26832 }, // U+1EC6 + { 13, 25, 229, 1, 19, 41, 26877 }, // U+1EC7 + { 5, 24, 104, 1, 24, 15, 26918 }, // U+1EC8 + { 5, 19, 94, 0, 19, 12, 26933 }, // U+1EC9 + { 4, 25, 104, 1, 19, 13, 26945 }, // U+1ECA + { 4, 25, 94, 1, 19, 13, 26958 }, // U+1ECB + { 17, 25, 304, 1, 19, 54, 26971 }, // U+1ECC + { 13, 20, 235, 1, 14, 33, 27025 }, // U+1ECD + { 17, 25, 304, 1, 24, 54, 27058 }, // U+1ECE + { 13, 20, 235, 1, 19, 33, 27112 }, // U+1ECF + { 17, 29, 304, 1, 28, 62, 27145 }, // U+1ED0 + { 13, 24, 235, 1, 23, 39, 27207 }, // U+1ED1 + { 17, 29, 304, 1, 28, 62, 27246 }, // U+1ED2 + { 13, 24, 235, 1, 23, 39, 27308 }, // U+1ED3 + { 17, 29, 304, 1, 28, 62, 27347 }, // U+1ED4 + { 13, 24, 235, 1, 23, 39, 27409 }, // U+1ED5 + { 17, 29, 304, 1, 28, 62, 27448 }, // U+1ED6 + { 13, 24, 235, 1, 23, 39, 27510 }, // U+1ED7 + { 17, 30, 304, 1, 24, 64, 27549 }, // U+1ED8 + { 13, 25, 235, 1, 19, 41, 27613 }, // U+1ED9 + { 17, 25, 304, 1, 24, 54, 27654 }, // U+1EDA + { 13, 20, 234, 1, 19, 33, 27708 }, // U+1EDB + { 17, 25, 304, 1, 24, 54, 27741 }, // U+1EDC + { 13, 20, 234, 1, 19, 33, 27795 }, // U+1EDD + { 17, 25, 304, 1, 24, 54, 27828 }, // U+1EDE + { 13, 20, 234, 1, 19, 33, 27882 }, // U+1EDF + { 17, 25, 304, 1, 24, 54, 27915 }, // U+1EE0 + { 13, 20, 234, 1, 19, 33, 27969 }, // U+1EE1 + { 17, 27, 304, 1, 21, 58, 28002 }, // U+1EE2 + { 13, 23, 234, 1, 17, 38, 28060 }, // U+1EE3 + { 15, 25, 294, 2, 19, 47, 28098 }, // U+1EE4 + { 12, 20, 233, 1, 14, 30, 28145 }, // U+1EE5 + { 15, 25, 294, 2, 24, 47, 28175 }, // U+1EE6 + { 12, 20, 233, 1, 19, 30, 28222 }, // U+1EE7 + { 17, 25, 293, 2, 24, 54, 28252 }, // U+1EE8 + { 14, 20, 246, 1, 19, 35, 28306 }, // U+1EE9 + { 17, 25, 293, 2, 24, 54, 28341 }, // U+1EEA + { 14, 20, 246, 1, 19, 35, 28395 }, // U+1EEB + { 17, 25, 293, 2, 24, 54, 28430 }, // U+1EEC + { 14, 20, 246, 1, 19, 35, 28484 }, // U+1EED + { 17, 25, 293, 2, 24, 54, 28519 }, // U+1EEE + { 14, 20, 246, 1, 19, 35, 28573 }, // U+1EEF + { 17, 27, 293, 2, 21, 58, 28608 }, // U+1EF0 + { 14, 23, 246, 1, 17, 41, 28666 }, // U+1EF1 + { 17, 24, 268, 0, 24, 51, 28707 }, // U+1EF2 + { 14, 25, 220, 0, 19, 44, 28758 }, // U+1EF3 + { 17, 25, 268, 0, 19, 54, 28802 }, // U+1EF4 + { 14, 20, 220, 0, 14, 35, 28856 }, // U+1EF5 + { 17, 24, 268, 0, 24, 51, 28891 }, // U+1EF6 + { 14, 25, 220, 0, 19, 44, 28942 }, // U+1EF7 + { 17, 24, 268, 0, 24, 51, 28986 }, // U+1EF8 + { 14, 25, 220, 0, 19, 44, 29037 }, // U+1EF9 + { 0, 0, 218, 0, 0, 0, 29081 }, // U+2000 + { 0, 0, 436, 0, 0, 0, 29081 }, // U+2001 + { 0, 0, 200, 0, 0, 0, 29081 }, // U+2002 + { 0, 0, 400, 0, 0, 0, 29081 }, // U+2003 + { 0, 0, 134, 0, 0, 0, 29081 }, // U+2004 + { 0, 0, 100, 0, 0, 0, 29081 }, // U+2005 + { 0, 0, 66, 0, 0, 0, 29081 }, // U+2006 + { 0, 0, 259, 0, 0, 0, 29081 }, // U+2007 + { 0, 0, 109, 0, 0, 0, 29081 }, // U+2008 + { 0, 0, 70, 0, 0, 0, 29081 }, // U+2009 + { 0, 0, 34, 0, 0, 0, 29081 }, // U+200A + { 0, 0, 0, 0, 0, 0, 29081 }, // U+200B + { 0, 0, 0, 0, 0, 0, 29081 }, // U+200C + { 0, 0, 0, 0, 0, 0, 29081 }, // U+200D + { 0, 0, 0, 0, 0, 0, 29081 }, // U+200E + { 0, 0, 0, 0, 0, 0, 29081 }, // U+200F + { 9, 3, 141, 0, 9, 4, 29081 }, // U+2010 + { 9, 3, 141, 0, 9, 4, 29085 }, // U+2011 + { 14, 3, 259, 1, 10, 6, 29089 }, // U+2012 + { 13, 3, 200, 0, 9, 5, 29095 }, // U+2013 + { 25, 3, 400, 0, 9, 10, 29100 }, // U+2014 + { 25, 3, 400, 0, 9, 10, 29110 }, // U+2015 + { 6, 31, 188, 3, 25, 24, 29120 }, // U+2016 + { 13, 6, 178, -1, 0, 10, 29144 }, // U+2017 + { 5, 8, 98, 1, 19, 5, 29154 }, // U+2018 + { 5, 8, 98, 1, 19, 5, 29159 }, // U+2019 + { 4, 8, 91, 1, 3, 4, 29164 }, // U+201A + { 5, 8, 98, 0, 19, 5, 29168 }, // U+201B + { 9, 8, 167, 1, 19, 9, 29173 }, // U+201C + { 9, 8, 167, 1, 19, 9, 29182 }, // U+201D + { 9, 8, 160, 1, 3, 9, 29191 }, // U+201E + { 9, 8, 167, 0, 19, 9, 29200 }, // U+201F + { 10, 15, 193, 1, 19, 19, 29209 }, // U+2020 + { 10, 19, 193, 1, 19, 24, 29228 }, // U+2021 + { 9, 9, 220, 2, 12, 11, 29252 }, // U+2022 + { 7, 9, 220, 4, 12, 8, 29263 }, // U+2023 + { 5, 5, 109, 1, 4, 4, 29271 }, // U+2024 + { 11, 5, 218, 1, 4, 7, 29275 }, // U+2025 + { 18, 5, 328, 1, 4, 12, 29282 }, // U+2026 + { 5, 5, 109, 1, 11, 4, 29294 }, // U+2027 + { 0, 0, 0, 0, 0, 0, 29298 }, // U+202A + { 0, 0, 0, 0, 0, 0, 29298 }, // U+202B + { 0, 0, 0, 0, 0, 0, 29298 }, // U+202C + { 0, 0, 0, 0, 0, 0, 29298 }, // U+202D + { 0, 0, 0, 0, 0, 0, 29298 }, // U+202E + { 0, 0, 70, 0, 0, 0, 29298 }, // U+202F + { 28, 20, 520, 2, 19, 70, 29298 }, // U+2030 + { 37, 20, 660, 2, 19, 93, 29368 }, // U+2031 + { 4, 8, 86, 1, 19, 4, 29461 }, // U+2032 + { 9, 8, 171, 1, 19, 9, 29465 }, // U+2033 + { 14, 8, 256, 1, 19, 14, 29474 }, // U+2034 + { 4, 8, 86, 1, 19, 4, 29488 }, // U+2035 + { 9, 8, 171, 1, 19, 9, 29492 }, // U+2036 + { 14, 8, 256, 1, 19, 14, 29501 }, // U+2037 + { 10, 6, 179, 1, -1, 8, 29515 }, // U+2038 + { 8, 13, 148, 1, 14, 13, 29523 }, // U+2039 + { 9, 13, 148, 0, 14, 15, 29536 }, // U+203A + { 14, 15, 252, 1, 16, 27, 29551 }, // U+203B + { 11, 20, 218, 1, 19, 28, 29578 }, // U+203C + { 12, 20, 201, 0, 19, 30, 29606 }, // U+203D + { 12, 3, 213, 1, 21, 5, 29636 }, // U+203E + { 20, 5, 324, 0, -1, 13, 29641 }, // U+203F + { 20, 5, 324, 0, 19, 13, 29654 }, // U+2040 + { 11, 15, 178, 0, 8, 21, 29667 }, // U+2041 + { 16, 16, 286, 1, 17, 32, 29688 }, // U+2042 + { 8, 3, 220, 3, 9, 3, 29720 }, // U+2043 + { 16, 19, 71, -6, 19, 38, 29723 }, // U+2044 + { 6, 24, 140, 2, 20, 18, 29761 }, // U+2045 + { 6, 24, 140, 1, 20, 18, 29779 }, // U+2046 + { 25, 20, 411, 0, 19, 63, 29797 }, // U+2047 + { 18, 20, 315, 0, 19, 45, 29860 }, // U+2048 + { 18, 20, 315, 1, 19, 45, 29905 }, // U+2049 + { 9, 20, 173, 1, 14, 23, 29950 }, // U+204A + { 12, 19, 238, 2, 19, 29, 29973 }, // U+204B + { 12, 11, 234, 1, 13, 17, 30002 }, // U+204C + { 12, 11, 234, 1, 13, 17, 30019 }, // U+204D + { 11, 12, 197, 1, 9, 17, 30036 }, // U+204E + { 5, 18, 114, 1, 13, 12, 30053 }, // U+204F + { 20, 25, 322, 0, 19, 63, 30065 }, // U+2050 + { 11, 25, 213, 2, 19, 35, 30128 }, // U+2051 + { 16, 19, 285, 1, 19, 38, 30163 }, // U+2052 + { 16, 6, 287, 1, 12, 12, 30201 }, // U+2053 + { 20, 5, 324, 0, -1, 13, 30213 }, // U+2054 + { 11, 11, 216, 1, 12, 16, 30226 }, // U+2055 + { 20, 8, 341, 1, 19, 20, 30242 }, // U+2057 + { 0, 0, 110, 0, 0, 0, 30262 }, // U+205F + { 0, 0, 0, 0, 0, 0, 30262 }, // U+2060 + { 0, 0, 0, 0, 0, 0, 30262 }, // U+2061 + { 0, 0, 0, 0, 0, 0, 30262 }, // U+2062 + { 0, 0, 0, 0, 0, 0, 30262 }, // U+2063 + { 0, 0, 0, 0, 0, 0, 30262 }, // U+2064 + { 0, 0, 0, 0, 0, 0, 30262 }, // U+2066 + { 0, 0, 0, 0, 0, 0, 30262 }, // U+2067 + { 0, 0, 0, 0, 0, 0, 30262 }, // U+2068 + { 0, 0, 0, 0, 0, 0, 30262 }, // U+2069 + { 0, 0, 0, 0, 0, 0, 30262 }, // U+206A + { 0, 0, 0, 0, 0, 0, 30262 }, // U+206B + { 0, 0, 0, 0, 0, 0, 30262 }, // U+206C + { 0, 0, 0, 0, 0, 0, 30262 }, // U+206D + { 0, 0, 0, 0, 0, 0, 30262 }, // U+206E + { 0, 0, 0, 0, 0, 0, 30262 }, // U+206F + { 10, 11, 180, 1, 21, 14, 30262 }, // U+2070 + { 3, 12, 77, 1, 22, 5, 30276 }, // U+2071 + { 10, 11, 179, 1, 21, 14, 30281 }, // U+2074 + { 9, 11, 171, 1, 21, 13, 30295 }, // U+2075 + { 9, 11, 173, 1, 21, 13, 30308 }, // U+2076 + { 9, 11, 158, 0, 21, 13, 30321 }, // U+2077 + { 9, 11, 175, 1, 21, 13, 30334 }, // U+2078 + { 9, 11, 173, 1, 21, 13, 30347 }, // U+2079 + { 9, 7, 171, 1, 19, 8, 30360 }, // U+207A + { 9, 3, 171, 1, 17, 4, 30368 }, // U+207B + { 8, 6, 171, 1, 18, 6, 30372 }, // U+207C + { 5, 13, 104, 1, 22, 9, 30378 }, // U+207D + { 5, 13, 104, 0, 22, 9, 30387 }, // U+207E + { 9, 9, 169, 1, 19, 11, 30396 }, // U+207F + { 10, 11, 180, 1, 6, 14, 30407 }, // U+2080 + { 5, 11, 118, 1, 6, 7, 30421 }, // U+2081 + { 9, 11, 172, 1, 6, 13, 30428 }, // U+2082 + { 9, 11, 174, 1, 6, 13, 30441 }, // U+2083 + { 10, 11, 179, 1, 6, 14, 30454 }, // U+2084 + { 9, 11, 171, 1, 6, 13, 30468 }, // U+2085 + { 9, 11, 173, 1, 6, 13, 30481 }, // U+2086 + { 9, 11, 158, 0, 6, 13, 30494 }, // U+2087 + { 9, 11, 175, 1, 6, 13, 30507 }, // U+2088 + { 9, 11, 173, 1, 6, 13, 30520 }, // U+2089 + { 9, 7, 171, 1, 4, 8, 30533 }, // U+208A + { 9, 3, 171, 1, 2, 4, 30541 }, // U+208B + { 8, 6, 171, 1, 3, 6, 30545 }, // U+208C + { 5, 13, 104, 1, 7, 9, 30551 }, // U+208D + { 5, 13, 104, 0, 7, 9, 30560 }, // U+208E + { 9, 9, 157, 0, 4, 11, 30569 }, // U+2090 + { 10, 9, 162, 0, 4, 12, 30580 }, // U+2091 + { 10, 9, 166, 0, 4, 12, 30592 }, // U+2092 + { 10, 9, 156, 0, 4, 12, 30604 }, // U+2093 + { 10, 11, 177, 1, 5, 14, 30616 }, // U+2094 + { 9, 11, 169, 1, 6, 13, 30630 }, // U+2095 + { 9, 11, 155, 1, 6, 13, 30643 }, // U+2096 + { 3, 11, 77, 1, 6, 5, 30656 }, // U+2097 + { 13, 9, 244, 1, 4, 15, 30661 }, // U+2098 + { 9, 9, 169, 1, 4, 11, 30676 }, // U+2099 + { 9, 11, 170, 1, 4, 13, 30687 }, // U+209A + { 9, 9, 151, 0, 4, 11, 30700 }, // U+209B + { 6, 11, 98, 0, 6, 9, 30711 }, // U+209C + { 16, 20, 288, 1, 19, 40, 30720 }, // U+20A0 + { 16, 24, 291, 1, 21, 48, 30760 }, // U+20A1 + { 16, 20, 289, 1, 19, 40, 30808 }, // U+20A2 + { 15, 19, 240, -1, 19, 36, 30848 }, // U+20A3 + { 13, 19, 242, 1, 19, 31, 30884 }, // U+20A4 + { 19, 18, 340, 1, 16, 43, 30915 }, // U+20A5 + { 19, 19, 305, 0, 19, 46, 30958 }, // U+20A6 + { 21, 20, 376, 2, 19, 53, 31004 }, // U+20A7 + { 25, 20, 444, 2, 19, 63, 31057 }, // U+20A8 + { 24, 19, 379, 0, 19, 57, 31120 }, // U+20A9 + { 17, 14, 323, 2, 14, 30, 31177 }, // U+20AA + { 15, 25, 241, 1, 19, 47, 31207 }, // U+20AB + { 16, 20, 264, 0, 19, 40, 31254 }, // U+20AC + { 18, 19, 281, 0, 19, 43, 31294 }, // U+20AD + { 14, 19, 255, 1, 19, 34, 31337 }, // U+20AE + { 33, 27, 550, 1, 21, 112, 31371 }, // U+20AF + { 16, 19, 263, 0, 19, 38, 31483 }, // U+20B1 + { 17, 24, 297, 1, 21, 51, 31521 }, // U+20B2 + { 17, 19, 273, 0, 19, 41, 31572 }, // U+20B3 + { 14, 20, 250, 1, 19, 35, 31613 }, // U+20B4 + { 16, 24, 291, 1, 21, 48, 31648 }, // U+20B5 + { 14, 19, 255, 1, 19, 34, 31696 }, // U+20B8 + { 13, 19, 214, 0, 19, 31, 31730 }, // U+20B9 + { 13, 20, 235, 1, 19, 33, 31761 }, // U+20BA + { 16, 14, 288, 1, 14, 28, 31794 }, // U+20BC + { 17, 19, 277, 0, 19, 41, 31822 }, // U+20BD + { 16, 19, 287, 1, 19, 38, 31863 }, // U+20BE + { 13, 24, 260, 2, 21, 39, 31901 }, // U+20BF + { 19, 16, 379, 2, 16, 38, 31940 }, // U+2190 + { 17, 19, 331, 2, 19, 41, 31978 }, // U+2191 + { 19, 16, 379, 3, 16, 38, 32019 }, // U+2192 + { 17, 20, 331, 2, 19, 43, 32057 }, // U+2193 + { 29, 16, 534, 2, 16, 58, 32100 }, // U+2194 + { 17, 30, 331, 2, 23, 64, 32158 }, // U+2195 + { 15, 15, 306, 2, 14, 29, 32222 }, // U+2196 + { 15, 15, 306, 2, 14, 29, 32251 }, // U+2197 + { 15, 14, 306, 2, 14, 27, 32280 }, // U+2198 + { 15, 14, 306, 2, 14, 27, 32307 }, // U+2199 + { 25, 19, 455, 2, 19, 60, 32334 }, // U+21A9 + { 25, 19, 455, 2, 19, 60, 32394 }, // U+21AA + { 21, 19, 392, 2, 19, 50, 32454 }, // U+21B0 + { 21, 19, 392, 2, 19, 50, 32504 }, // U+21B1 + { 13, 17, 328, 3, 17, 28, 32554 }, // U+21B2 + { 21, 17, 392, 2, 19, 45, 32582 }, // U+21B3 + { 17, 20, 330, 2, 19, 43, 32627 }, // U+21B4 + { 21, 17, 392, 2, 19, 45, 32670 }, // U+21B5 + { 21, 16, 328, 0, 17, 42, 32715 }, // U+21B6 + { 21, 16, 328, 0, 17, 42, 32757 }, // U+21B7 + { 19, 23, 379, 2, 23, 55, 32799 }, // U+21BA + { 19, 23, 379, 2, 23, 55, 32854 }, // U+21BB + { 19, 20, 328, 1, 19, 48, 32909 }, // U+21C4 + { 19, 20, 328, 1, 19, 48, 32957 }, // U+21C6 + { 19, 17, 379, 2, 16, 41, 33005 }, // U+21D0 + { 19, 17, 379, 3, 16, 41, 33046 }, // U+21D2 + { 29, 17, 534, 2, 16, 62, 33087 }, // U+21D4 + { 11, 19, 240, 2, 19, 27, 33149 }, // U+21DE + { 11, 20, 240, 2, 19, 28, 33176 }, // U+21DF + { 21, 12, 403, 2, 15, 32, 33204 }, // U+21E4 + { 21, 12, 403, 2, 15, 32, 33236 }, // U+21E5 + { 22, 20, 409, 2, 20, 55, 33268 }, // U+21E7 + { 22, 24, 409, 2, 20, 66, 33323 }, // U+21EA + { 13, 20, 250, 1, 19, 33, 33389 }, // U+2202 + { 15, 16, 271, 1, 15, 30, 33422 }, // U+2205 + { 17, 19, 269, 0, 19, 41, 33452 }, // U+2206 + { 14, 22, 286, 2, 19, 39, 33493 }, // U+220F + { 15, 23, 251, 0, 19, 44, 33532 }, // U+2211 + { 12, 3, 262, 2, 9, 5, 33576 }, // U+2212 + { 15, 17, 59, -6, 17, 32, 33581 }, // U+2215 + { 15, 19, 249, 0, 19, 36, 33613 }, // U+221A + { 21, 11, 371, 1, 13, 29, 33649 }, // U+221E + { 10, 26, 144, -1, 20, 33, 33678 }, // U+222B + { 5, 15, 109, 1, 17, 10, 33711 }, // U+2236 + { 14, 11, 262, 1, 13, 20, 33721 }, // U+2248 + { 12, 14, 262, 2, 14, 21, 33741 }, // U+2260 + { 12, 14, 262, 2, 14, 21, 33762 }, // U+2264 + { 12, 14, 262, 2, 14, 21, 33783 }, // U+2265 + { 35, 35, 556, 0, 27, 154, 33804 }, // U+2295 + { 35, 35, 556, 0, 27, 154, 33958 }, // U+2296 + { 35, 35, 556, 0, 27, 154, 34112 }, // U+2297 + { 35, 35, 556, 0, 27, 154, 34266 }, // U+2298 + { 13, 19, 227, 0, 19, 31, 34420 }, // U+FB01 + { 15, 19, 238, 0, 19, 36, 34451 }, // U+FB02 + { 24, 20, 397, 0, 11, 60, 34487 }, // U+FB56 + { 29, 18, 437, 0, 9, 66, 34547 }, // U+FB57 + { 7, 19, 120, -1, 11, 17, 34613 }, // U+FB58 + { 11, 16, 149, 0, 8, 22, 34630 }, // U+FB59 + { 24, 19, 397, 0, 18, 57, 34652 }, // U+FB66 + { 29, 19, 437, 0, 18, 69, 34709 }, // U+FB67 + { 7, 21, 108, 0, 20, 19, 34778 }, // U+FB68 + { 11, 18, 149, 0, 17, 25, 34797 }, // U+FB69 + { 16, 21, 264, 0, 11, 42, 34822 }, // U+FB7A + { 20, 21, 288, 0, 11, 53, 34864 }, // U+FB7B + { 14, 17, 236, 0, 9, 30, 34917 }, // U+FB7C + { 18, 17, 255, 0, 9, 39, 34947 }, // U+FB7D + { 11, 23, 191, 0, 22, 32, 34986 }, // U+FB88 + { 15, 23, 213, 0, 22, 44, 35018 }, // U+FB89 + { 9, 23, 147, -1, 17, 26, 35062 }, // U+FB8A + { 13, 23, 158, -1, 17, 38, 35088 }, // U+FB8B + { 10, 25, 147, -1, 19, 32, 35126 }, // U+FB8C + { 13, 25, 158, -1, 19, 41, 35158 }, // U+FB8D + { 23, 19, 351, 0, 18, 55, 35199 }, // U+FB8E + { 26, 19, 390, 0, 18, 62, 35254 }, // U+FB8F + { 11, 19, 165, 0, 18, 27, 35316 }, // U+FB90 + { 14, 19, 203, 0, 18, 34, 35343 }, // U+FB91 + { 23, 22, 351, 0, 21, 64, 35377 }, // U+FB92 + { 26, 22, 390, 0, 21, 72, 35441 }, // U+FB93 + { 11, 22, 165, 0, 21, 31, 35513 }, // U+FB94 + { 14, 22, 203, 0, 21, 39, 35544 }, // U+FB95 + { 16, 14, 274, 0, 8, 28, 35583 }, // U+FB9E + { 20, 14, 292, 0, 8, 35, 35611 }, // U+FB9F + { 10, 12, 162, 0, 12, 15, 35646 }, // U+FBA6 + { 14, 10, 192, 0, 9, 18, 35661 }, // U+FBA7 + { 6, 16, 108, 0, 11, 12, 35679 }, // U+FBA8 + { 13, 11, 175, 0, 4, 18, 35691 }, // U+FBA9 + { 16, 14, 269, 0, 13, 28, 35709 }, // U+FBAA + { 17, 17, 246, 0, 11, 37, 35737 }, // U+FBAB + { 14, 14, 230, 0, 13, 25, 35774 }, // U+FBAC + { 14, 17, 203, 0, 11, 30, 35799 }, // U+FBAD + { 23, 13, 377, 0, 12, 38, 35829 }, // U+FBAE + { 15, 9, 220, 0, 2, 17, 35867 }, // U+FBAF + { 23, 19, 377, 0, 18, 55, 35884 }, // U+FBB0 + { 15, 16, 220, 0, 9, 30, 35939 }, // U+FBB1 + { 19, 17, 304, 0, 11, 41, 35969 }, // U+FBFC + { 20, 11, 294, 0, 5, 28, 36010 }, // U+FBFD + { 7, 16, 120, -1, 11, 14, 36038 }, // U+FBFE + { 11, 13, 149, 0, 8, 18, 36052 }, // U+FBFF + { 10, 11, 164, 0, 9, 14, 36070 }, // U+FE80 + { 9, 21, 94, -2, 21, 24, 36084 }, // U+FE81 + { 11, 24, 116, -2, 23, 33, 36108 }, // U+FE82 + { 6, 24, 94, 0, 24, 18, 36141 }, // U+FE83 + { 9, 27, 116, 0, 26, 31, 36159 }, // U+FE84 + { 9, 22, 179, 1, 16, 25, 36190 }, // U+FE85 + { 13, 23, 189, 1, 17, 38, 36215 }, // U+FE86 + { 6, 26, 95, 1, 18, 20, 36253 }, // U+FE87 + { 8, 26, 116, 1, 18, 26, 36273 }, // U+FE88 + { 19, 18, 304, 0, 12, 43, 36299 }, // U+FE89 + { 20, 18, 294, 0, 12, 45, 36342 }, // U+FE8A + { 6, 19, 108, 0, 18, 15, 36387 }, // U+FE8B + { 10, 16, 137, 0, 15, 20, 36402 }, // U+FE8C + { 4, 18, 95, 1, 18, 9, 36422 }, // U+FE8D + { 8, 19, 116, 1, 18, 19, 36431 }, // U+FE8E + { 24, 17, 397, 0, 11, 51, 36450 }, // U+FE8F + { 29, 15, 437, 0, 9, 55, 36501 }, // U+FE90 + { 6, 16, 108, 0, 11, 12, 36556 }, // U+FE91 + { 10, 13, 137, 0, 8, 17, 36568 }, // U+FE92 + { 10, 17, 162, 0, 17, 22, 36585 }, // U+FE93 + { 14, 20, 194, 0, 19, 35, 36607 }, // U+FE94 + { 24, 14, 397, 0, 13, 42, 36642 }, // U+FE95 + { 29, 14, 437, 0, 13, 51, 36684 }, // U+FE96 + { 7, 17, 108, 0, 16, 15, 36735 }, // U+FE97 + { 11, 14, 149, 0, 13, 20, 36750 }, // U+FE98 + { 24, 17, 397, 0, 16, 51, 36770 }, // U+FE99 + { 29, 17, 437, 0, 16, 62, 36821 }, // U+FE9A + { 7, 20, 108, 0, 19, 18, 36883 }, // U+FE9B + { 11, 17, 149, 0, 16, 24, 36901 }, // U+FE9C + { 15, 21, 256, 0, 11, 40, 36925 }, // U+FE9D + { 18, 21, 260, 0, 11, 48, 36965 }, // U+FE9E + { 14, 14, 236, 0, 9, 25, 37013 }, // U+FE9F + { 18, 14, 255, 0, 9, 32, 37038 }, // U+FEA0 + { 15, 21, 256, 0, 11, 40, 37070 }, // U+FEA1 + { 18, 21, 260, 0, 11, 48, 37110 }, // U+FEA2 + { 14, 10, 236, 0, 9, 18, 37158 }, // U+FEA3 + { 18, 10, 255, 0, 9, 23, 37176 }, // U+FEA4 + { 15, 26, 256, 0, 16, 49, 37199 }, // U+FEA5 + { 18, 26, 260, 0, 16, 59, 37248 }, // U+FEA6 + { 14, 15, 236, 0, 14, 27, 37307 }, // U+FEA7 + { 18, 15, 255, 0, 14, 34, 37334 }, // U+FEA8 + { 11, 13, 191, 0, 12, 18, 37368 }, // U+FEA9 + { 15, 13, 213, 0, 12, 25, 37386 }, // U+FEAA + { 11, 18, 191, 0, 17, 25, 37411 }, // U+FEAB + { 15, 18, 213, 0, 17, 34, 37436 }, // U+FEAC + { 9, 15, 147, -1, 9, 17, 37470 }, // U+FEAD + { 13, 15, 158, -1, 9, 25, 37487 }, // U+FEAE + { 9, 20, 147, -1, 14, 23, 37512 }, // U+FEAF + { 13, 20, 158, -1, 14, 33, 37535 }, // U+FEB0 + { 29, 17, 484, 0, 11, 62, 37568 }, // U+FEB1 + { 33, 15, 508, 0, 9, 62, 37630 }, // U+FEB2 + { 19, 12, 314, 0, 11, 29, 37692 }, // U+FEB3 + { 23, 10, 340, 0, 9, 29, 37721 }, // U+FEB4 + { 29, 23, 484, 0, 17, 84, 37750 }, // U+FEB5 + { 33, 23, 508, 0, 17, 95, 37834 }, // U+FEB6 + { 19, 18, 314, 0, 17, 43, 37929 }, // U+FEB7 + { 23, 18, 340, 0, 17, 52, 37972 }, // U+FEB8 + { 32, 16, 522, 0, 10, 64, 38024 }, // U+FEB9 + { 36, 16, 541, 0, 10, 72, 38088 }, // U+FEBA + { 22, 11, 361, 0, 10, 31, 38160 }, // U+FEBB + { 25, 11, 380, 0, 10, 35, 38191 }, // U+FEBC + { 32, 21, 522, 0, 15, 84, 38226 }, // U+FEBD + { 36, 21, 541, 0, 15, 95, 38310 }, // U+FEBE + { 22, 16, 361, 0, 15, 44, 38405 }, // U+FEBF + { 25, 16, 380, 0, 15, 50, 38449 }, // U+FEC0 + { 19, 19, 315, 0, 18, 46, 38499 }, // U+FEC1 + { 23, 19, 332, 0, 18, 55, 38545 }, // U+FEC2 + { 16, 19, 271, 0, 18, 38, 38600 }, // U+FEC3 + { 19, 19, 284, 0, 18, 46, 38638 }, // U+FEC4 + { 19, 19, 315, 0, 18, 46, 38684 }, // U+FEC5 + { 23, 19, 332, 0, 18, 55, 38730 }, // U+FEC6 + { 16, 19, 271, 0, 18, 38, 38785 }, // U+FEC7 + { 19, 19, 284, 0, 18, 46, 38823 }, // U+FEC8 + { 15, 23, 203, 0, 13, 44, 38869 }, // U+FEC9 + { 15, 21, 212, 0, 11, 40, 38913 }, // U+FECA + { 12, 12, 201, 0, 11, 18, 38953 }, // U+FECB + { 15, 12, 208, 0, 11, 23, 38971 }, // U+FECC + { 15, 28, 203, 0, 18, 53, 38994 }, // U+FECD + { 15, 26, 212, 0, 16, 49, 39047 }, // U+FECE + { 12, 17, 201, 0, 16, 26, 39096 }, // U+FECF + { 15, 17, 208, 0, 16, 32, 39122 }, // U+FED0 + { 24, 19, 393, 0, 18, 57, 39154 }, // U+FED1 + { 29, 17, 439, 0, 16, 62, 39211 }, // U+FED2 + { 11, 19, 179, 0, 18, 27, 39273 }, // U+FED3 + { 14, 17, 198, 0, 16, 30, 39300 }, // U+FED4 + { 19, 21, 310, 0, 16, 50, 39330 }, // U+FED5 + { 22, 21, 323, 0, 14, 58, 39380 }, // U+FED6 + { 11, 19, 179, 0, 18, 27, 39438 }, // U+FED7 + { 14, 17, 198, 0, 16, 30, 39465 }, // U+FED8 + { 21, 19, 353, 0, 18, 50, 39495 }, // U+FED9 + { 25, 19, 369, 0, 18, 60, 39545 }, // U+FEDA + { 11, 19, 165, 0, 18, 27, 39605 }, // U+FEDB + { 14, 19, 203, 0, 18, 34, 39632 }, // U+FEDC + { 16, 24, 278, 0, 18, 48, 39666 }, // U+FEDD + { 20, 24, 292, 0, 18, 60, 39714 }, // U+FEDE + { 6, 19, 104, 0, 18, 15, 39774 }, // U+FEDF + { 9, 19, 120, 0, 18, 22, 39789 }, // U+FEE0 + { 12, 20, 194, 0, 10, 30, 39811 }, // U+FEE1 + { 16, 20, 225, 0, 10, 40, 39841 }, // U+FEE2 + { 12, 10, 210, 0, 9, 15, 39881 }, // U+FEE3 + { 16, 11, 231, 0, 10, 22, 39896 }, // U+FEE4 + { 16, 19, 274, 0, 13, 38, 39918 }, // U+FEE5 + { 20, 19, 292, 0, 13, 48, 39956 }, // U+FEE6 + { 6, 17, 108, 0, 16, 13, 40004 }, // U+FEE7 + { 10, 14, 137, 0, 13, 18, 40017 }, // U+FEE8 + { 10, 12, 162, 0, 12, 15, 40035 }, // U+FEE9 + { 14, 14, 194, 0, 13, 25, 40050 }, // U+FEEA + { 14, 14, 230, 0, 13, 25, 40075 }, // U+FEEB + { 14, 17, 203, 0, 11, 30, 40100 }, // U+FEEC + { 9, 15, 179, 1, 9, 17, 40130 }, // U+FEED + { 13, 15, 189, 1, 9, 25, 40147 }, // U+FEEE + { 19, 17, 304, 0, 11, 41, 40172 }, // U+FEEF + { 20, 11, 294, 0, 5, 28, 40213 }, // U+FEF0 + { 19, 22, 304, 0, 11, 53, 40241 }, // U+FEF1 + { 20, 16, 294, 0, 5, 40, 40294 }, // U+FEF2 + { 7, 16, 120, -1, 11, 14, 40334 }, // U+FEF3 + { 11, 13, 149, 0, 8, 18, 40348 }, // U+FEF4 + { 15, 19, 233, -2, 19, 36, 40366 }, // U+FEF5 + { 19, 20, 240, -2, 19, 48, 40402 }, // U+FEF6 + { 14, 22, 233, -1, 22, 39, 40450 }, // U+FEF7 + { 17, 23, 240, 0, 22, 49, 40489 }, // U+FEF8 + { 12, 26, 233, 1, 18, 39, 40538 }, // U+FEF9 + { 16, 26, 240, 1, 18, 52, 40577 }, // U+FEFA + { 12, 18, 233, 1, 18, 27, 40629 }, // U+FEFB + { 16, 19, 240, 1, 18, 38, 40656 }, // U+FEFC }; static const EpdUnicodeInterval inter_12_regularIntervals[] = { - { 0x0, 0x7F, 0x0 }, - { 0x80, 0xFF, 0x80 }, - { 0x100, 0x148, 0x100 }, + { 0x0, 0x148, 0x0 }, { 0x14A, 0x17F, 0x149 }, { 0x1A0, 0x1A1, 0x17F }, { 0x1AF, 0x1B0, 0x181 }, @@ -3280,59 +3899,75 @@ static const EpdUnicodeInterval inter_12_regularIntervals[] = { { 0x300, 0x304, 0x1DE }, { 0x306, 0x30C, 0x1E3 }, { 0x30F, 0x30F, 0x1EA }, - { 0x313, 0x313, 0x1EB }, - { 0x315, 0x315, 0x1EC }, - { 0x31B, 0x31B, 0x1ED }, - { 0x323, 0x323, 0x1EE }, - { 0x326, 0x328, 0x1EF }, - { 0x32C, 0x32C, 0x1F2 }, - { 0x337, 0x338, 0x1F3 }, - { 0x342, 0x343, 0x1F5 }, - { 0x346, 0x36F, 0x1F7 }, - { 0x400, 0x479, 0x221 }, - { 0x480, 0x49D, 0x29B }, - { 0x4A0, 0x4FF, 0x2B9 }, - { 0x5D0, 0x5EA, 0x319 }, - { 0x1EA0, 0x1EF9, 0x334 }, - { 0x2000, 0x2027, 0x38E }, - { 0x202A, 0x2055, 0x3B6 }, - { 0x2057, 0x2057, 0x3E2 }, - { 0x205F, 0x2064, 0x3E3 }, - { 0x2066, 0x206F, 0x3E9 }, - { 0x2070, 0x2071, 0x3F3 }, - { 0x2074, 0x208E, 0x3F5 }, - { 0x2090, 0x209C, 0x410 }, - { 0x20A0, 0x20AF, 0x41D }, - { 0x20B1, 0x20B5, 0x42D }, - { 0x20B8, 0x20BA, 0x432 }, - { 0x20BC, 0x20BF, 0x435 }, - { 0x2190, 0x2199, 0x439 }, - { 0x21A9, 0x21AA, 0x443 }, - { 0x21B0, 0x21B7, 0x445 }, - { 0x21BA, 0x21BB, 0x44D }, - { 0x21C4, 0x21C4, 0x44F }, - { 0x21C6, 0x21C6, 0x450 }, - { 0x21D0, 0x21D0, 0x451 }, - { 0x21D2, 0x21D2, 0x452 }, - { 0x21D4, 0x21D4, 0x453 }, - { 0x21DE, 0x21DF, 0x454 }, - { 0x21E4, 0x21E5, 0x456 }, - { 0x21E7, 0x21E7, 0x458 }, - { 0x21EA, 0x21EA, 0x459 }, - { 0x2202, 0x2202, 0x45A }, - { 0x2205, 0x2206, 0x45B }, - { 0x220F, 0x220F, 0x45D }, - { 0x2211, 0x2212, 0x45E }, - { 0x2215, 0x2215, 0x460 }, - { 0x221A, 0x221A, 0x461 }, - { 0x221E, 0x221E, 0x462 }, - { 0x222B, 0x222B, 0x463 }, - { 0x2236, 0x2236, 0x464 }, - { 0x2248, 0x2248, 0x465 }, - { 0x2260, 0x2260, 0x466 }, - { 0x2264, 0x2265, 0x467 }, - { 0x2295, 0x2298, 0x469 }, - { 0xFB01, 0xFB02, 0x46D }, + { 0x312, 0x313, 0x1EB }, + { 0x315, 0x315, 0x1ED }, + { 0x31B, 0x31B, 0x1EE }, + { 0x323, 0x323, 0x1EF }, + { 0x326, 0x328, 0x1F0 }, + { 0x32C, 0x32C, 0x1F3 }, + { 0x337, 0x338, 0x1F4 }, + { 0x342, 0x343, 0x1F6 }, + { 0x346, 0x36F, 0x1F8 }, + { 0x400, 0x479, 0x222 }, + { 0x480, 0x49D, 0x29C }, + { 0x4A0, 0x4FF, 0x2BA }, + { 0x5D0, 0x5EA, 0x31A }, + { 0x60C, 0x60C, 0x335 }, + { 0x61B, 0x61B, 0x336 }, + { 0x61F, 0x61F, 0x337 }, + { 0x621, 0x621, 0x338 }, + { 0x640, 0x640, 0x339 }, + { 0x660, 0x669, 0x33A }, + { 0x6BA, 0x6BA, 0x344 }, + { 0x6D4, 0x6D4, 0x345 }, + { 0x6F0, 0x6F9, 0x346 }, + { 0x1EA0, 0x1EF9, 0x350 }, + { 0x2000, 0x2027, 0x3AA }, + { 0x202A, 0x2055, 0x3D2 }, + { 0x2057, 0x2057, 0x3FE }, + { 0x205F, 0x2064, 0x3FF }, + { 0x2066, 0x2071, 0x405 }, + { 0x2074, 0x208E, 0x411 }, + { 0x2090, 0x209C, 0x42C }, + { 0x20A0, 0x20AF, 0x439 }, + { 0x20B1, 0x20B5, 0x449 }, + { 0x20B8, 0x20BA, 0x44E }, + { 0x20BC, 0x20BF, 0x451 }, + { 0x2190, 0x2199, 0x455 }, + { 0x21A9, 0x21AA, 0x45F }, + { 0x21B0, 0x21B7, 0x461 }, + { 0x21BA, 0x21BB, 0x469 }, + { 0x21C4, 0x21C4, 0x46B }, + { 0x21C6, 0x21C6, 0x46C }, + { 0x21D0, 0x21D0, 0x46D }, + { 0x21D2, 0x21D2, 0x46E }, + { 0x21D4, 0x21D4, 0x46F }, + { 0x21DE, 0x21DF, 0x470 }, + { 0x21E4, 0x21E5, 0x472 }, + { 0x21E7, 0x21E7, 0x474 }, + { 0x21EA, 0x21EA, 0x475 }, + { 0x2202, 0x2202, 0x476 }, + { 0x2205, 0x2206, 0x477 }, + { 0x220F, 0x220F, 0x479 }, + { 0x2211, 0x2212, 0x47A }, + { 0x2215, 0x2215, 0x47C }, + { 0x221A, 0x221A, 0x47D }, + { 0x221E, 0x221E, 0x47E }, + { 0x222B, 0x222B, 0x47F }, + { 0x2236, 0x2236, 0x480 }, + { 0x2248, 0x2248, 0x481 }, + { 0x2260, 0x2260, 0x482 }, + { 0x2264, 0x2265, 0x483 }, + { 0x2295, 0x2298, 0x485 }, + { 0xFB01, 0xFB02, 0x489 }, + { 0xFB56, 0xFB59, 0x48B }, + { 0xFB66, 0xFB69, 0x48F }, + { 0xFB7A, 0xFB7D, 0x493 }, + { 0xFB88, 0xFB95, 0x497 }, + { 0xFB9E, 0xFB9F, 0x4A5 }, + { 0xFBA6, 0xFBB1, 0x4A7 }, + { 0xFBFC, 0xFBFF, 0x4B3 }, + { 0xFE80, 0xFEFC, 0x4B7 }, }; static const EpdKernClassEntry inter_12_regularKernLeftClasses[] = { @@ -4100,6 +4735,10 @@ static const EpdKernClassEntry inter_12_regularKernLeftClasses[] = { { 0x2264, 20 }, // U+2264 { 0x2265, 20 }, // U+2265 { 0xFB02, 145 }, // U+FB02 + { 0xFB8C, 146 }, // U+FB8C + { 0xFEAD, 146 }, // U+FEAD + { 0xFEAE, 146 }, // U+FEAE + { 0xFEAF, 146 }, // U+FEAF }; static const EpdKernClassEntry inter_12_regularKernRightClasses[] = { @@ -4872,161 +5511,181 @@ static const EpdKernClassEntry inter_12_regularKernRightClasses[] = { { 0x2260, 24 }, // U+2260 { 0x2264, 24 }, // U+2264 { 0x2265, 24 }, // U+2265 + { 0xFB8E, 141 }, // U+FB8E + { 0xFBA6, 141 }, // U+FBA6 + { 0xFBAA, 141 }, // U+FBAA + { 0xFE8D, 142 }, // U+FE8D + { 0xFEA3, 141 }, // U+FEA3 + { 0xFEA9, 143 }, // U+FEA9 + { 0xFEB1, 143 }, // U+FEB1 + { 0xFEB3, 143 }, // U+FEB3 + { 0xFEB9, 143 }, // U+FEB9 + { 0xFEBB, 143 }, // U+FEBB + { 0xFEC1, 143 }, // U+FEC1 + { 0xFEC3, 143 }, // U+FEC3 + { 0xFECB, 142 }, // U+FECB + { 0xFEDB, 141 }, // U+FEDB + { 0xFEDF, 142 }, // U+FEDF + { 0xFEE1, 141 }, // U+FEE1 + { 0xFEE3, 141 }, // U+FEE3 + { 0xFEE9, 141 }, // U+FEE9 + { 0xFEEB, 141 }, // U+FEEB }; static const int8_t inter_12_regularKernMatrix[] = { - 0, 0, 0, 0, -13, 0, 0, 0, -33, 0, -33, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, -66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 18, 18, 0, 0, -3, -3, -3, 0, 0, 0, 0, 18, 0, 0, -4, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -27, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -25, -16, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, 0, 0, -16, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 11, -4, 0, 0, 0, 0, 0, 0, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -13, 0, 0, 0, -54, 0, -54, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, -4, -32, 0, -4, -38, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -4, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -29, 0, -20, -20, -15, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -43, -43, -29, 0, 0, 0, 0, 0, 0, 0, -58, -25, 0, 0, -57, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -36, 0, -9, 0, -6, -6, -8, -3, 0, 0, 0, -40, -18, 0, 0, -18, 0, -13, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, -9, -18, 0, -13, 0, 0, 0, -18, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -23, 0, -54, -54, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -10, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -5, 0, -14, -13, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 6, 0, 6, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -18, 0, -20, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 2, 0, 13, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 19, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -7, 0, -9, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -10, 0, -13, 0, -13, 0, 0, -10, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -10, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -2, 0, -11, 0, -11, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, -23, 0, 1, -18, 0, 2, 0, -50, 0, -50, 0, -6, 0, 0, -7, -23, -4, 8, -6, -4, -13, -36, 0, 0, 0, -43, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 2, -63, -18, -20, 0, 0, 0, -20, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, -20, 0, 0, 0, 36, -20, 0, 0, -18, 0, 0, 36, 36, 0, -20, 0, 0, 0, 0, 0, 0, 0, 36, -41, -41, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -18, 0, -27, -25, -20, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -18, -18, -18, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, -29, -29, -23, -36, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, -13, -13, -20, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 0, -13, -2, -16, -15, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -13, 0, -16, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -16, -40, 0, 0, 0, 0, - -32, 0, 0, 0, 0, 0, -32, -13, 0, -13, 0, 8, -2, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -25, -13, 5, 0, -13, 0, -35, -10, -30, -23, 0, -32, 0, -32, 0, 0, -2, 0, 2, 0, 0, 0, -9, 0, -27, -27, 0, 0, -13, 0, -29, -13, -13, 0, -28, -32, 0, 0, -20, -13, 0, -13, 0, 0, 0, -13, 0, 0, -2, 2, 2, 2, 0, -2, 0, -27, 0, 0, 0, 0, 0, 0, -2, -35, -35, -58, 0, 0, 2, 0, 0, 0, 0, 0, 0, -13, 0, -35, 0, 0, -9, -28, -9, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -52, -41, 0, 0, 0, -13, 0, -41, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -21, -21, -21, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 29, 29, 0, 0, 0, 0, 0, 0, 0, 0, 22, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -29, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -36, 0, -1, -27, 0, 0, 0, 0, 0, 0, 0, 0, -13, -9, -13, 0, 0, 0, -10, 0, 0, -13, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -13, 0, 0, 0, 0, -13, -13, -13, -9, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 0, 0, 46, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -48, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -12, 0, -9, 0, -2, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -12, -12, -12, 0, 0, 0, 9, 13, 0, 0, -2, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -5, 0, 0, 0, 0, -43, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -13, 0, -14, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 5, 0, 0, 0, 0, -13, -16, -25, 0, 0, -43, 0, 0, -20, -63, 0, -6, 0, 0, -13, 0, -61, 0, 0, 0, 0, 0, -13, 0, 0, -16, 5, 5, 5, 34, -16, -13, -16, 0, 0, 0, 34, 34, 0, -16, 0, 0, 0, -13, 0, 5, 34, 34, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, - -36, 0, 0, 0, 0, 0, -68, -34, 0, -54, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -11, 0, 0, -11, 0, -39, 0, -27, 0, 0, -23, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -50, 0, -38, -25, -86, 0, -32, -70, 0, 0, -35, -59, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -39, -39, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -39, 0, 0, -9, -60, -9, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, -34, -16, 0, -18, -18, -13, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -13, 0, 0, -18, -16, -18, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -38, -1, 0, 0, 0, 0, 0, 0, 0, 0, -2, -4, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -2, -2, -4, 0, 0, 0, 0, -4, 0, 0, -2, 0, 0, 0, 0, 0, -4, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, 0, 0, 0, -54, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -7, 0, 0, -10, 0, 0, 0, -2, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -6, 0, 0, 0, 18, -6, 0, 0, -2, 0, 0, 18, 18, 0, -6, -2, -2, -2, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -13, 0, 0, -29, -13, -29, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -35, 0, -13, -36, 0, 0, 0, 0, 0, 6, 0, 0, -23, -33, -35, -1, 0, 0, -26, -33, -33, -31, -29, -29, -28, -23, -29, 0, 0, -52, -29, 0, 0, 0, 0, -72, 0, -61, 0, -20, 0, 0, 0, -13, -49, -49, -55, 0, 0, 0, 36, -55, -55, -43, -33, 0, 0, 36, 36, 0, -35, 0, 0, 0, -47, 0, 0, 51, 36, -53, -53, 0, 0, -13, 0, 0, 0, 0, -33, -25, -33, -33, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, - 0, 0, 0, 0, -20, 0, 0, -20, -38, -20, -38, -19, -5, 0, 0, 0, -13, 0, 0, 0, 0, -25, -29, -25, 0, -27, -30, 0, -13, -38, 0, 0, 7, 0, 0, 0, 0, 0, -32, -21, -22, 0, 0, 0, -4, -18, 0, 0, 0, 0, 0, 0, -20, 0, 0, -23, -45, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, -16, -21, -37, -22, 0, 0, 0, 34, -40, 0, 0, -21, 0, 0, 34, 34, 0, -22, 0, 0, 0, 0, 0, 0, 16, 34, -49, -49, 0, 0, -13, 0, 0, 0, 0, 0, -6, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, 0, 0, -16, -38, 0, 0, 0, 0, 0, 0, 0, -53, -61, 0, 0, 0, 0, - 0, 0, 0, 0, -20, 0, 0, -20, -38, -20, -38, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -20, -29, -25, 0, -2, -23, 0, -2, -25, 0, 0, 0, 9, 0, 0, 0, 0, 0, -20, -20, 0, 0, 0, -12, -13, 0, 0, 0, 0, 0, 0, -20, 0, 0, -23, -20, 0, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, -20, 0, 0, -20, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -12, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, -35, -43, 0, 0, 0, 0, - 0, 0, -4, 0, 0, 0, 0, -15, 0, -15, 0, 0, -14, 0, 0, -8, -16, 0, 0, -8, 0, 0, -23, -20, 0, -16, 0, 0, -16, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -16, -35, 0, -3, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, -18, -23, -23, -11, 0, 0, 0, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, -11, -12, -12, -12, 0, 0, 0, 34, 0, 0, 0, 0, -8, -36, -13, -12, 0, 0, -25, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, - 0, 0, 0, 0, -25, 0, 0, -32, -13, -32, -13, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -15, -32, 0, -15, -13, 6, 0, 0, 0, 0, 0, 0, 0, 0, -31, -33, 0, -11, 0, -18, -27, 0, -16, 0, 0, 0, 0, -32, 0, 0, -36, -52, 0, -3, 0, 0, -16, 0, -52, 0, -25, 0, 0, 0, -34, -58, -49, -33, -11, -23, -11, 0, -33, -16, 0, -51, 0, 0, 0, 0, 0, -55, 6, 6, 6, -16, 0, -11, 34, 0, 0, 0, 0, 0, -15, 0, 6, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -52, 0, 0, 0, 0, 0, 0, 0, -60, -71, 0, 0, 0, -13, - 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, -13, -16, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 13, 34, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 11, -4, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -32, 0, 0, 0, 0, 0, -32, -13, 0, -13, 0, 8, -2, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -25, -13, 5, 0, -13, 0, -51, -10, -49, -46, 0, -32, 0, -32, 0, 0, -2, 0, 2, 0, 0, 0, -9, 0, -47, -27, 0, 0, -13, 0, -29, -13, -13, 0, -28, -32, 0, 0, -20, -13, 0, -13, 0, 0, 0, -13, 0, 0, -2, 2, 2, 2, 0, -2, 0, -47, 0, 0, 0, 0, 0, 0, -2, -51, -51, -51, 0, 0, 2, 0, 0, 0, 0, 0, 0, -13, 0, -51, 0, 0, -9, -28, -9, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -69, -63, 0, 0, 0, -13, 0, -63, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, - 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, -18, -43, 0, -18, -23, -18, 0, -18, -18, 0, 0, 0, 0, -16, 0, 11, -16, 0, -23, -16, -32, 0, 0, 0, 0, -27, 0, 0, 0, 0, 11, 38, 11, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 11, 0, 0, 0, -32, 0, 0, 11, 0, 0, 38, 0, -23, -23, -23, 0, 11, 11, 0, 0, 23, 23, 0, -18, -16, -43, -20, 18, 18, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, - -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, -20, -20, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -18, -10, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -36, 0, -22, -20, -11, -35, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -8, -7, -10, -6, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -8, 4, 0, 0, 0, 0, 0, 0, -36, -36, -36, 0, 0, 0, 14, 0, 0, 0, -11, 0, 0, 0, -36, 0, 0, 0, -5, 0, 0, -69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -35, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -11, -23, -11, -23, -11, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -25, 0, 0, 0, 0, 0, 4, 0, 0, -6, -6, -10, -15, 0, 0, 0, -9, -14, 0, 0, 7, 0, 0, -11, 0, 0, -16, -11, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, -9, -6, -6, -10, 0, 0, 0, 40, -10, 0, 0, -6, 0, 0, 40, 56, 0, -10, 0, 0, 0, 0, 0, 0, 18, 54, -28, -28, 0, 0, 0, 0, 0, 0, 0, -14, 0, -14, -9, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, -36, 0, 0, -9, -23, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 41, 28, 0, 0, 0, 0, 0, 0, 0, 0, 34, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, -20, -45, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -9, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -16, -25, -16, -25, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, -16, 4, 0, -23, -23, 0, 0, 0, 0, 0, -13, 0, 0, -1, -6, 6, 3, 5, 5, 0, 6, 0, 6, 3, 5, 0, -16, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, -1, -1, -6, 3, 3, 3, 11, -6, 0, 6, -1, 0, 4, 11, 11, 5, -6, -23, -23, -23, 0, 4, 3, 0, 11, 0, 0, 0, 0, 0, 0, -23, -9, -9, 6, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 6, -32, 0, 0, 0, -25, 6, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, - -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, -18, -13, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -29, -29, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 1, -1, 1, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -20, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -13, 0, -4, -4, 0, -6, 0, 0, 0, 3, -4, -8, 0, 4, 0, -4, -3, 0, 9, 7, 0, 0, -8, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, -4, 3, 3, -4, 0, 0, 0, 0, -4, 0, 9, 3, 0, 0, 0, 0, 4, -4, -13, -13, -13, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -3, 0, -3, -4, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -16, 3, 0, 0, 0, -4, 0, 3, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, -20, -50, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -16, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -13, -19, -13, -19, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -16, 0, 0, -27, 2, 0, -25, -13, 0, 0, 0, 0, -13, 0, 0, -32, -3, -8, 6, 0, 0, 0, -1, 6, 0, 0, 0, 0, 0, -13, 0, 0, -16, -13, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -3, -3, -8, 0, 0, 0, 0, -8, 0, 0, -3, 0, 2, 0, 0, 0, -8, -13, -38, -13, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, -13, -18, -18, 6, 0, 6, -1, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 3, -32, 0, 0, 0, -49, 3, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, - 0, 0, 0, 0, -13, 0, 0, 0, -29, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, -23, -20, 0, 0, 0, 0, 0, -13, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -7, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -32, 0, 0, 0, -29, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, -1, -10, 7, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, -9, 0, 0, -13, -9, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, -1, -1, -10, 0, 0, 0, 0, -10, 0, 0, -1, 0, 0, 0, 0, 0, -10, -25, -25, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -6, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -4, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -29, 0, -20, -20, -29, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -43, -43, -29, 0, 0, 0, 0, 0, 0, 0, -29, -25, 0, 0, -57, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -13, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -27, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -20, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, -2, -28, 0, -2, -28, 0, 0, 0, 0, -3, -3, 0, 0, 0, -4, -11, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, -11, 0, 0, 0, 0, -11, 0, 0, -4, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -2, 0, 0, 0, 0, 0, 0, 0, -4, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, -20, -50, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -36, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -4, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -61, 0, -36, -20, -33, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -61, -61, -61, 0, 0, 0, 0, 0, 0, 0, -33, -25, 0, 0, -61, 0, 0, 0, 0, 0, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -52, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -20, -18, 0, 0, -20, -13, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -52, 0, -25, -23, -16, -36, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -52, -52, -52, 0, 0, 0, 0, 0, -13, -13, -36, -18, 0, 0, -79, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -33, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -27, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -13, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 16, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -36, 0, -16, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -36, -36, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -41, 0, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, -16, -23, 0, -20, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -50, 0, 0, 0, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -21, -21, -21, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 44, 41, 0, 0, 0, 0, 0, 0, 0, 0, 29, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 28, 28, 11, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 41, 28, 41, 69, 0, 0, 0, 0, 0, 25, 69, 69, 41, 0, 36, 36, 36, 0, 55, 41, 20, 69, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, -20, -20, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -50, -50, -50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -18, -10, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -56, 0, -22, -20, -11, -35, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -8, -7, -10, -6, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -8, 4, 0, 0, 0, 0, 0, 0, -56, -56, -56, 0, 0, 0, 14, 0, 0, 0, -11, 0, 0, 0, -56, 0, 0, 0, -5, 0, 0, -69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -35, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -59, 0, -20, -20, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -59, -59, -59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -59, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -18, -10, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 13, 0, 0, 0, 0, 39, 23, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 16, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 16, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 7, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 35, 20, 0, 0, 0, 0, 0, 13, 20, 20, 11, 0, 16, 16, 16, 0, 13, 36, 18, 20, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 16, 16, 0, 0, 0, 0, 0, 16, 16, 16, 16, 0, 13, 13, 13, 0, 13, 13, 16, 16, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 44, 41, 11, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 44, 44, 44, 69, 0, 0, 0, 0, 0, 25, 69, 69, 41, 0, 36, 36, 36, 0, 55, 44, 43, 69, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 44, 44, 11, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 44, 44, 44, 69, 0, 0, 0, 0, 0, 25, 69, 69, 44, 0, 36, 36, 36, 0, 55, 44, 20, 69, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 44, 28, 11, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 44, 44, 44, 69, 0, 0, 0, 0, 0, 25, 69, 69, 41, 0, 36, 36, 36, 0, 55, 44, 20, 69, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 44, 41, 0, 0, 0, 0, 0, 0, 0, 0, 29, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -36, 0, 0, 0, 0, 0, -70, -54, 0, -54, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -11, 0, 0, -11, 0, -39, 0, -27, 0, 0, -23, 0, -70, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -52, 0, -38, -25, -54, 0, -32, -70, 0, 0, -35, -54, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -39, -39, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -39, 0, 0, -9, -60, -9, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, -34, -16, 0, -18, -18, -13, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 2, 0, 13, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 35, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -36, 0, 0, 0, 0, 0, -68, -50, 0, -50, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -11, 0, 0, -11, 0, -39, 0, -27, 0, 0, -23, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -47, 0, -38, -25, -50, 0, -32, -70, 0, 0, -35, -50, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -39, -39, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -39, 0, 0, -9, -60, -9, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, -34, -16, 0, -18, -18, -13, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 2, 0, 13, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 19, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -36, 0, 0, 0, 0, 0, -66, -34, 0, -54, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -11, 0, 0, -11, 0, -39, 0, -27, 0, 0, -45, 0, -66, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -50, 0, -38, -25, -86, 0, -32, -66, 0, 0, -35, -59, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -39, -39, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -39, 0, 0, -9, -60, -9, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, -34, -29, 0, -18, -18, -13, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 2, 0, 13, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 19, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -13, 0, 0, -43, -13, -43, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -35, 0, -13, -36, 0, 0, 0, 0, 0, 6, 0, 0, -23, -33, -35, -1, 0, 0, -26, -33, -33, -45, -29, -29, -56, -37, -43, 0, 0, -52, -43, 0, 0, 0, 0, -45, 0, -43, 0, -20, 0, 0, 0, -13, -49, -49, -55, 0, 0, 0, 36, -55, -45, -43, -33, 0, 0, 36, 36, 0, -35, 0, 0, 0, -45, 0, 0, 51, 36, -53, -53, 0, 0, -13, 0, 0, 0, 0, -33, -25, -33, -33, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, - 0, 0, 0, 0, -13, 0, 0, -43, -13, -43, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -35, 0, -13, -36, 0, 0, 0, 0, 0, 6, 0, 0, -23, -33, -35, -1, 0, 0, -26, -33, -33, -31, -29, -29, -28, -23, -43, 0, 0, -52, -43, 0, 0, 0, 0, -72, 0, -43, 0, -20, 0, 0, 0, -13, -49, -49, -55, 0, 0, 0, 36, -55, -55, -43, -33, 0, 0, 36, 36, 0, -35, 0, 0, 0, -47, 0, 0, 51, 36, -53, -53, 0, 0, -13, 0, 0, 0, 0, -33, -25, -33, -33, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, - 0, 0, 0, 0, -13, 0, 0, -29, -13, -29, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -58, 0, -13, -84, 0, 0, 0, 0, 0, 6, 0, 0, -23, -33, -35, -1, 0, 0, -26, -33, -33, -31, -29, -29, -28, -23, -29, 0, 0, -52, -29, 0, 0, 0, 0, -72, 0, -61, 0, -20, 0, 0, 0, -13, -49, -49, -55, 0, 0, 0, 36, -55, -55, -43, -33, 0, 0, 36, 36, 0, -35, 0, 0, 0, -47, 0, 0, 51, 36, -53, -53, 0, 0, -13, 0, 0, 0, 0, -33, -25, -33, -33, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -79, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -25, -12, 0, 0, -21, -21, -11, -21, -21, -21, -21, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, -21, -21, -25, 0, 0, 0, 0, -25, -21, -21, -21, 0, 0, 0, 0, 0, -25, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 12, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -14, 0, 6, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -6, -29, -29, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 41, 41, 0, 0, 0, 0, 0, 0, 0, 0, 47, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -67, 0, -22, -20, -11, -35, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -8, -7, -10, -6, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -8, 4, 0, 0, 0, 0, 0, 0, -67, -67, -67, 0, 0, 0, 14, 0, 0, 0, -11, 0, 0, 0, -67, 0, 0, 0, -5, 0, 0, -69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -35, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, -38, -38, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -16, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 29, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 41, 41, 0, 0, 0, 0, 0, 0, 0, 0, 34, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 41, 44, 11, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 41, 41, 41, 69, 0, 0, 0, 0, 0, 25, 69, 69, 44, 0, 36, 36, 36, 0, 55, 41, 38, 85, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -14, -14, 0, 0, 0, 0, 0, 11, 11, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -4, 0, 0, 0, 0, -58, 0, -58, 0, 0, -14, 0, 0, -8, -16, 0, 0, -8, 0, 0, -20, -20, 0, -16, 0, 0, -16, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -58, 0, 0, -36, -58, 0, -3, 0, 0, 0, 0, -58, 0, 0, 0, 0, 0, -18, -23, -23, -11, 0, 0, 0, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, -11, -12, -12, -12, 0, 0, 0, 34, 0, 0, 0, 0, -8, -36, -13, -12, 0, 0, -25, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 0, -13, -2, -16, -15, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -13, 0, -36, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -16, -40, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -11, 0, 0, 0, 0, 0, -11, -11, 6, -11, 6, 0, -4, 0, 0, 0, -9, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -5, -10, 0, 0, 0, 0, -10, 0, -8, -10, 0, 0, -11, 0, 0, 0, -11, 0, 0, -11, 0, 0, -10, -11, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, -5, 0, -8, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -6, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 6, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -16, -50, -16, -50, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, -16, 4, 0, -23, -23, 0, 0, 0, 0, 0, -13, 0, 0, -1, -6, 6, 3, 5, 5, 0, 6, 0, 6, 3, 5, 0, -16, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, -1, -1, -6, 3, 3, 3, 11, -6, 0, 6, -1, 0, 4, 11, 11, 5, -6, -23, -23, -23, 0, 4, 3, 0, 11, 0, 0, 0, 0, 0, 0, -23, -9, -9, 6, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 6, -61, 0, 0, 0, -50, 6, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, - 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, -40, 0, -11, -68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -11, -11, -9, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -40, -40, -40, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -40, 0, 0, -10, 0, -46, 0, -68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -68, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -25, 0, 0, -32, -13, -32, -13, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -15, -32, 0, -15, -13, 6, 0, 0, 0, 0, 0, 0, 0, 0, -31, -64, 0, -11, 0, -18, -27, 0, -16, 0, 0, 0, 0, -32, 0, 0, -36, -52, 0, -3, 0, 0, -16, 0, -52, 0, -25, 0, 0, 0, -34, -58, -49, -64, -11, -23, -11, 0, -64, -16, 0, -51, 0, 0, 0, 0, 0, -64, 6, 6, 6, -16, 0, -11, 34, 0, 0, 0, 0, 0, -15, 0, 6, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -52, 0, 0, 0, 0, 0, 0, 0, -60, -71, 0, 0, 0, -13, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -12, -12, 6, -12, 0, -12, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 2, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 41, 28, 0, 0, 0, 0, 0, 0, 0, 0, 29, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -36, 0, -22, -20, -11, -35, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -8, -7, -10, -6, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -8, 4, 0, 0, 0, 0, 0, 0, -36, -36, -36, 0, 0, 0, 28, 0, 0, 0, -11, 0, 0, 0, -36, 0, 0, 0, -5, 0, 0, -69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -35, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -25, 0, 0, -32, -27, -32, -27, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -15, -32, 0, -15, -13, 6, 0, 0, 0, 0, 0, 0, 0, 0, -31, -33, 0, -11, 0, -18, -27, 0, -16, 0, 0, 0, 0, -32, 0, 0, -36, -52, 0, -3, 0, 0, -16, 0, -52, 0, -25, 0, 0, 0, -34, -58, -49, -33, -11, -23, -11, 0, -33, -16, 0, -51, 0, 0, 0, 0, 0, -55, 6, 6, 6, -16, 0, -11, 34, 0, 0, 0, 0, 0, -15, 0, 6, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -27, 0, 0, 0, 0, 0, 0, 0, -60, -71, 0, 0, 0, -13, - 0, 0, 0, 0, -25, 0, 0, 0, -34, 0, -34, -38, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, -59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -25, -45, 0, 0, 0, 0, 0, -57, -34, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, -50, 0, 0, -27, 0, 0, -25, -40, -25, -40, -57, -18, 0, 0, 0, -38, 0, -25, -16, 0, -23, -38, -23, 0, -23, -55, 0, -23, -50, 0, 0, 0, 0, 0, 0, 0, 0, -16, -23, -25, 0, 0, 0, -8, -31, 6, -16, 6, 0, 0, 0, -43, 0, 0, -34, -25, 0, 0, 0, 0, -34, -4, -25, 0, 0, -27, 0, 0, 0, -23, -23, -25, 0, 0, 0, 0, -25, -16, 6, -23, 0, 0, 0, 0, 0, -25, 0, 0, 0, -16, 0, 0, 0, 0, -20, -36, 0, 0, -23, 0, 0, -48, -25, 6, 0, 6, -44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, -36, -25, 0, -43, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, 0, 0, 0, 0, 0, -20, -13, 0, 0, 0, 0, 0, 0, -38, -38, -38, 0, 0, 0, 0, 0, 20, 20, 0, 0, 0, -25, -59, 25, 25, -20, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, -50, -38, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 0, -16, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, - -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -36, 0, -9, 0, -6, -6, -8, -3, 0, 0, 0, -40, -18, 0, 0, -18, 0, -13, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, -9, -18, 0, -13, 0, 0, 0, -18, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -23, 0, -54, -73, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 5, -3, 0, -13, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, -16, 0, -50, 0, -53, -35, 0, -60, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 5, 0, -50, -50, -79, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -16, -63, 0, 0, 0, 0, 0, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -6, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -27, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -13, 0, 0, 0, -33, 0, -33, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, -66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 18, 18, 0, 0, -3, -3, -3, 0, 0, 0, 0, 18, 0, 0, -4, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -27, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -25, -16, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, 0, 0, -16, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 11, -4, 0, 0, 0, 0, 0, 0, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -13, 0, 0, 0, -54, 0, -54, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, -4, -32, 0, -4, -38, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -4, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -29, 0, -20, -20, -15, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -43, -43, -29, 0, 0, 0, 0, 0, 0, 0, -58, -25, 0, 0, -57, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -36, 0, -9, 0, -6, -6, -8, -3, 0, 0, 0, -40, -18, 0, 0, -18, 0, -13, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, -9, -18, 0, -13, 0, 0, 0, -18, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -23, 0, -54, -54, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -10, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -5, 0, -14, -13, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 6, 0, 6, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -18, 0, -20, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 2, 0, 13, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 19, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -7, 0, -9, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -10, 0, -13, 0, -13, 0, 0, -10, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -10, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -2, 0, -11, 0, -11, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -23, 0, 1, -18, 0, 2, 0, -50, 0, -50, 0, -6, 0, 0, -7, -23, -4, 8, -6, -4, -13, -36, 0, 0, 0, -43, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 2, -63, -18, -20, 0, 0, 0, -20, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, -20, 0, 0, 0, 36, -20, 0, 0, -18, 0, 0, 36, 36, 0, -20, 0, 0, 0, 0, 0, 0, 0, 36, -41, -41, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -18, 0, -27, -25, -20, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -18, -18, -18, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, -29, -29, -23, -36, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, -13, -13, -20, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 0, -13, -2, -16, -15, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -13, 0, -16, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -16, -40, 0, 0, 0, 0, 0, 0, 0, + -32, 0, 0, 0, 0, 0, -32, -13, 0, -13, 0, 8, -2, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -25, -13, 5, 0, -13, 0, -35, -10, -30, -23, 0, -32, 0, -32, 0, 0, -2, 0, 2, 0, 0, 0, -9, 0, -27, -27, 0, 0, -13, 0, -29, -13, -13, 0, -28, -32, 0, 0, -20, -13, 0, -13, 0, 0, 0, -13, 0, 0, -2, 2, 2, 2, 0, -2, 0, -27, 0, 0, 0, 0, 0, 0, -2, -35, -35, -58, 0, 0, 2, 0, 0, 0, 0, 0, 0, -13, 0, -35, 0, 0, -9, -28, -9, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -52, -41, 0, 0, 0, -13, 0, -41, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -21, -21, -21, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 0, 29, 29, 0, 0, 0, 0, 0, 0, 0, 0, 22, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -29, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -36, 0, -1, -27, 0, 0, 0, 0, 0, 0, 0, 0, -13, -9, -13, 0, 0, 0, -10, 0, 0, -13, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -13, 0, 0, 0, 0, -13, -13, -13, -9, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 0, 0, 46, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -48, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -12, 0, -9, 0, -2, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -12, -12, -12, 0, 0, 0, 9, 13, 0, 0, -2, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -5, 0, 0, 0, 0, -43, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -13, 0, -14, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 5, 0, 0, 0, 0, -13, -16, -25, 0, 0, -43, 0, 0, -20, -63, 0, -6, 0, 0, -13, 0, -61, 0, 0, 0, 0, 0, -13, 0, 0, -16, 5, 5, 5, 34, -16, -13, -16, 0, 0, 0, 34, 34, 0, -16, 0, 0, 0, -13, 0, 5, 34, 34, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, + -36, 0, 0, 0, 0, 0, -68, -34, 0, -54, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -11, 0, 0, -11, 0, -39, 0, -27, 0, 0, -23, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -50, 0, -38, -25, -86, 0, -32, -70, 0, 0, -35, -59, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -39, -39, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -39, 0, 0, -9, -60, -9, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, -34, -16, 0, -18, -18, -13, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -13, 0, 0, -18, -16, -18, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -38, -1, 0, 0, 0, 0, 0, 0, 0, 0, -2, -4, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -2, -2, -4, 0, 0, 0, 0, -4, 0, 0, -2, 0, 0, 0, 0, 0, -4, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -57, 0, 0, 0, -54, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -7, 0, 0, -10, 0, 0, 0, -2, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -6, 0, 0, 0, 18, -6, 0, 0, -2, 0, 0, 18, 18, 0, -6, -2, -2, -2, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -13, 0, 0, -29, -13, -29, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -35, 0, -13, -36, 0, 0, 0, 0, 0, 6, 0, 0, -23, -33, -35, -1, 0, 0, -26, -33, -33, -31, -29, -29, -28, -23, -29, 0, 0, -52, -29, 0, 0, 0, 0, -72, 0, -61, 0, -20, 0, 0, 0, -13, -49, -49, -55, 0, 0, 0, 36, -55, -55, -43, -33, 0, 0, 36, 36, 0, -35, 0, 0, 0, -47, 0, 0, 51, 36, -53, -53, 0, 0, -13, 0, 0, 0, 0, -33, -25, -33, -33, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -20, 0, 0, -20, -38, -20, -38, -19, -5, 0, 0, 0, -13, 0, 0, 0, 0, -25, -29, -25, 0, -27, -30, 0, -13, -38, 0, 0, 7, 0, 0, 0, 0, 0, -32, -21, -22, 0, 0, 0, -4, -18, 0, 0, 0, 0, 0, 0, -20, 0, 0, -23, -45, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, -16, -21, -37, -22, 0, 0, 0, 34, -40, 0, 0, -21, 0, 0, 34, 34, 0, -22, 0, 0, 0, 0, 0, 0, 16, 34, -49, -49, 0, 0, -13, 0, 0, 0, 0, 0, -6, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, 0, 0, -16, -38, 0, 0, 0, 0, 0, 0, 0, -53, -61, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -20, 0, 0, -20, -38, -20, -38, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -20, -29, -25, 0, -2, -23, 0, -2, -25, 0, 0, 0, 9, 0, 0, 0, 0, 0, -20, -20, 0, 0, 0, -12, -13, 0, 0, 0, 0, 0, 0, -20, 0, 0, -23, -20, 0, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, -20, 0, 0, -20, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -12, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, -35, -43, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -4, 0, 0, 0, 0, -15, 0, -15, 0, 0, -14, 0, 0, -8, -16, 0, 0, -8, 0, 0, -23, -20, 0, -16, 0, 0, -16, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -16, -35, 0, -3, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, -18, -23, -23, -11, 0, 0, 0, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, -11, -12, -12, -12, 0, 0, 0, 34, 0, 0, 0, 0, -8, -36, -13, -12, 0, 0, -25, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -25, 0, 0, -32, -13, -32, -13, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -15, -32, 0, -15, -13, 6, 0, 0, 0, 0, 0, 0, 0, 0, -31, -33, 0, -11, 0, -18, -27, 0, -16, 0, 0, 0, 0, -32, 0, 0, -36, -52, 0, -3, 0, 0, -16, 0, -52, 0, -25, 0, 0, 0, -34, -58, -49, -33, -11, -23, -11, 0, -33, -16, 0, -51, 0, 0, 0, 0, 0, -55, 6, 6, 6, -16, 0, -11, 34, 0, 0, 0, 0, 0, -15, 0, 6, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -52, 0, 0, 0, 0, 0, 0, 0, -60, -71, 0, 0, 0, -13, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, -13, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, -13, -16, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 13, 34, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 11, -4, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -32, 0, 0, 0, 0, 0, -32, -13, 0, -13, 0, 8, -2, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -25, -13, 5, 0, -13, 0, -51, -10, -49, -46, 0, -32, 0, -32, 0, 0, -2, 0, 2, 0, 0, 0, -9, 0, -47, -27, 0, 0, -13, 0, -29, -13, -13, 0, -28, -32, 0, 0, -20, -13, 0, -13, 0, 0, 0, -13, 0, 0, -2, 2, 2, 2, 0, -2, 0, -47, 0, 0, 0, 0, 0, 0, -2, -51, -51, -51, 0, 0, 2, 0, 0, 0, 0, 0, 0, -13, 0, -51, 0, 0, -9, -28, -9, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -69, -63, 0, 0, 0, -13, 0, -63, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, -18, -43, 0, -18, -23, -18, 0, -18, -18, 0, 0, 0, 0, -16, 0, 11, -16, 0, -23, -16, -32, 0, 0, 0, 0, -27, 0, 0, 0, 0, 11, 38, 11, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 11, 0, 0, 0, -32, 0, 0, 11, 0, 0, 38, 0, -23, -23, -23, 0, 11, 11, 0, 0, 23, 23, 0, -18, -16, -43, -20, 18, 18, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, + -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, -20, -20, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -30, -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -18, -10, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -36, 0, -22, -20, -11, -35, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -8, -7, -10, -6, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -8, 4, 0, 0, 0, 0, 0, 0, -36, -36, -36, 0, 0, 0, 14, 0, 0, 0, -11, 0, 0, 0, -36, 0, 0, 0, -5, 0, 0, -69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -35, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -11, -23, -11, -23, -11, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -25, 0, 0, 0, 0, 0, 4, 0, 0, -6, -6, -10, -15, 0, 0, 0, -9, -14, 0, 0, 7, 0, 0, -11, 0, 0, -16, -11, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, -9, -6, -6, -10, 0, 0, 0, 40, -10, 0, 0, -6, 0, 0, 40, 56, 0, -10, 0, 0, 0, 0, 0, 0, 18, 54, -28, -28, 0, 0, 0, 0, 0, 0, 0, -14, 0, -14, -9, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, -36, 0, 0, -9, -23, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 41, 28, 0, 0, 0, 0, 0, 0, 0, 0, 34, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, -20, -45, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -9, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -16, -25, -16, -25, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, -16, 4, 0, -23, -23, 0, 0, 0, 0, 0, -13, 0, 0, -1, -6, 6, 3, 5, 5, 0, 6, 0, 6, 3, 5, 0, -16, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, -1, -1, -6, 3, 3, 3, 11, -6, 0, 6, -1, 0, 4, 11, 11, 5, -6, -23, -23, -23, 0, 4, 3, 0, 11, 0, 0, 0, 0, 0, 0, -23, -9, -9, 6, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 6, -32, 0, 0, 0, -25, 6, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, + -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, -18, -13, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -29, -29, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 1, -1, 1, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -20, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -13, 0, -4, -4, 0, -6, 0, 0, 0, 3, -4, -8, 0, 4, 0, -4, -3, 0, 9, 7, 0, 0, -8, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, -4, 3, 3, -4, 0, 0, 0, 0, -4, 0, 9, 3, 0, 0, 0, 0, 4, -4, -13, -13, -13, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -3, 0, -3, -4, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -16, 3, 0, 0, 0, -4, 0, 3, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, -20, -50, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -16, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -13, -19, -13, -19, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -16, 0, 0, -27, 2, 0, -25, -13, 0, 0, 0, 0, -13, 0, 0, -32, -3, -8, 6, 0, 0, 0, -1, 6, 0, 0, 0, 0, 0, -13, 0, 0, -16, -13, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -3, -3, -8, 0, 0, 0, 0, -8, 0, 0, -3, 0, 2, 0, 0, 0, -8, -13, -38, -13, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, -13, -18, -18, 6, 0, 6, -1, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 3, -32, 0, 0, 0, -49, 3, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -13, 0, 0, 0, -29, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, -23, -20, 0, 0, 0, 0, 0, -13, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -7, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -32, 0, 0, 0, -29, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, -1, -10, 7, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, -9, 0, 0, -13, -9, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, -1, -1, -10, 0, 0, 0, 0, -10, 0, 0, -1, 0, 0, 0, 0, 0, -10, -25, -25, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -6, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -4, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -29, 0, -20, -20, -29, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -43, -43, -29, 0, 0, 0, 0, 0, 0, 0, -29, -25, 0, 0, -57, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -13, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -27, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -20, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, -2, -28, 0, -2, -28, 0, 0, 0, 0, -3, -3, 0, 0, 0, -4, -11, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, -11, 0, 0, 0, 0, -11, 0, 0, -4, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -2, 0, 0, 0, 0, 0, 0, 0, -4, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, -20, -50, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -36, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -4, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, -18, -61, 0, -36, -20, -33, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -61, -61, -61, 0, 0, 0, 0, 0, 0, 0, -33, -25, 0, 0, -61, 0, 0, 0, 0, 0, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -52, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -20, -18, 0, 0, -20, -13, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -52, 0, -25, -23, -16, -36, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, -52, -52, -52, 0, 0, 0, 0, 0, -13, -13, -36, -18, 0, 0, -79, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -33, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -27, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, -13, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 16, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -36, 0, -16, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, -36, -36, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -41, 0, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, -16, -23, 0, -20, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -23, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, -50, 0, 0, 0, -41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 13, 13, 0, 0, -21, -21, -21, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -32, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 44, 41, 0, 0, 0, 0, 0, 0, 0, 0, 29, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 28, 28, 11, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 41, 28, 41, 69, 0, 0, 0, 0, 0, 25, 69, 69, 41, 0, 36, 36, 36, 0, 55, 41, 20, 69, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, -20, -20, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -50, -50, -50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -18, -10, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -56, 0, -22, -20, -11, -35, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -8, -7, -10, -6, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -8, 4, 0, 0, 0, 0, 0, 0, -56, -56, -56, 0, 0, 0, 14, 0, 0, 0, -11, 0, 0, 0, -56, 0, 0, 0, -5, 0, 0, -69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -35, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -59, 0, -20, -20, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, -59, -59, -59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -59, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -18, -10, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 13, 0, 0, 0, 0, 39, 23, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 16, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 16, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 7, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 35, 20, 0, 0, 0, 0, 0, 13, 20, 20, 11, 0, 16, 16, 16, 0, 13, 36, 18, 20, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 16, 16, 0, 0, 0, 0, 0, 16, 16, 16, 16, 0, 13, 13, 13, 0, 13, 13, 16, 16, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 44, 41, 11, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 44, 44, 44, 69, 0, 0, 0, 0, 0, 25, 69, 69, 41, 0, 36, 36, 36, 0, 55, 44, 43, 69, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 44, 44, 11, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 44, 44, 44, 69, 0, 0, 0, 0, 0, 25, 69, 69, 44, 0, 36, 36, 36, 0, 55, 44, 20, 69, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 44, 28, 11, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 44, 44, 44, 69, 0, 0, 0, 0, 0, 25, 69, 69, 41, 0, 36, 36, 36, 0, 55, 44, 20, 69, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 44, 41, 0, 0, 0, 0, 0, 0, 0, 0, 29, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -36, 0, 0, 0, 0, 0, -70, -54, 0, -54, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -11, 0, 0, -11, 0, -39, 0, -27, 0, 0, -23, 0, -70, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -52, 0, -38, -25, -54, 0, -32, -70, 0, 0, -35, -54, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -39, -39, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -39, 0, 0, -9, -60, -9, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, -34, -16, 0, -18, -18, -13, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 2, 0, 13, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 35, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -36, 0, 0, 0, 0, 0, -68, -50, 0, -50, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -11, 0, 0, -11, 0, -39, 0, -27, 0, 0, -23, 0, -52, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -47, 0, -38, -25, -50, 0, -32, -70, 0, 0, -35, -50, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -39, -39, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -39, 0, 0, -9, -60, -9, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, -34, -16, 0, -18, -18, -13, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 2, 0, 13, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 19, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -36, 0, 0, 0, 0, 0, -66, -34, 0, -54, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -11, 0, 0, -11, 0, -39, 0, -27, 0, 0, -45, 0, -66, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -27, 0, 0, 0, -50, 0, -38, -25, -86, 0, -32, -66, 0, 0, -35, -59, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, -39, -39, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -39, 0, 0, -9, -60, -9, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, -34, -29, 0, -18, -18, -13, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 2, 0, 13, 0, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 19, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -13, 0, 0, -43, -13, -43, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -35, 0, -13, -36, 0, 0, 0, 0, 0, 6, 0, 0, -23, -33, -35, -1, 0, 0, -26, -33, -33, -45, -29, -29, -56, -37, -43, 0, 0, -52, -43, 0, 0, 0, 0, -45, 0, -43, 0, -20, 0, 0, 0, -13, -49, -49, -55, 0, 0, 0, 36, -55, -45, -43, -33, 0, 0, 36, 36, 0, -35, 0, 0, 0, -45, 0, 0, 51, 36, -53, -53, 0, 0, -13, 0, 0, 0, 0, -33, -25, -33, -33, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -13, 0, 0, -43, -13, -43, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -35, 0, -13, -36, 0, 0, 0, 0, 0, 6, 0, 0, -23, -33, -35, -1, 0, 0, -26, -33, -33, -31, -29, -29, -28, -23, -43, 0, 0, -52, -43, 0, 0, 0, 0, -72, 0, -43, 0, -20, 0, 0, 0, -13, -49, -49, -55, 0, 0, 0, 36, -55, -55, -43, -33, 0, 0, 36, 36, 0, -35, 0, 0, 0, -47, 0, 0, 51, 36, -53, -53, 0, 0, -13, 0, 0, 0, 0, -33, -25, -33, -33, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -13, 0, 0, -29, -13, -29, -13, -52, -13, 0, 0, 0, -27, 0, -32, 0, 0, -13, -32, -13, 0, -13, -58, 0, -13, -84, 0, 0, 0, 0, 0, 6, 0, 0, -23, -33, -35, -1, 0, 0, -26, -33, -33, -31, -29, -29, -28, -23, -29, 0, 0, -52, -29, 0, 0, 0, 0, -72, 0, -61, 0, -20, 0, 0, 0, -13, -49, -49, -55, 0, 0, 0, 36, -55, -55, -43, -33, 0, 0, 36, 36, 0, -35, 0, 0, 0, -47, 0, 0, 51, 36, -53, -53, 0, 0, -13, 0, 0, 0, 0, -33, -25, -33, -33, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, -48, 0, 0, -13, -47, 0, 0, 0, 0, 0, 0, 0, 0, -79, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -25, -12, 0, 0, -21, -21, -11, -21, -21, -21, -21, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, -21, -21, -25, 0, 0, 0, 0, -25, -21, -21, -21, 0, 0, 0, 0, 0, -25, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 12, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -14, 0, 6, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -6, -29, -29, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 41, 41, 0, 0, 0, 0, 0, 0, 0, 0, 47, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -67, 0, -22, -20, -11, -35, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -8, -7, -10, -6, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -8, 4, 0, 0, 0, 0, 0, 0, -67, -67, -67, 0, 0, 0, 14, 0, 0, 0, -11, 0, 0, 0, -67, 0, 0, 0, -5, 0, 0, -69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -35, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -16, 0, -16, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, -23, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, -38, -38, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -16, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 29, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 41, 41, 0, 0, 0, 0, 0, 0, 0, 0, 34, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 36, 0, 34, 0, 0, 0, 36, 0, 0, 0, 0, 16, 41, 44, 11, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 41, 41, 41, 69, 0, 0, 0, 0, 0, 25, 69, 69, 44, 0, 36, 36, 36, 0, 55, 41, 38, 85, 0, 0, 0, 0, 0, 0, 36, 0, 0, 20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -14, -14, 0, 0, 0, 0, 0, 11, 11, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -4, 0, 0, 0, 0, -58, 0, -58, 0, 0, -14, 0, 0, -8, -16, 0, 0, -8, 0, 0, -20, -20, 0, -16, 0, 0, -16, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -23, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -58, 0, 0, -36, -58, 0, -3, 0, 0, 0, 0, -58, 0, 0, 0, 0, 0, -18, -23, -23, -11, 0, 0, 0, 0, -11, 0, 0, -23, 0, 0, 0, 0, 0, -11, -12, -12, -12, 0, 0, 0, 34, 0, 0, 0, 0, -8, -36, -13, -12, 0, 0, -25, -32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 0, -13, -2, -16, -15, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -13, 0, -36, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -16, -40, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -11, 0, 0, 0, 0, 0, -11, -11, 6, -11, 6, 0, -4, 0, 0, 0, -9, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -5, -10, 0, 0, 0, 0, -10, 0, -8, -10, 0, 0, -11, 0, 0, 0, -11, 0, 0, -11, 0, 0, -10, -11, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, -5, 0, -8, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -6, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 6, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -16, -50, -16, -50, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, -16, 4, 0, -23, -23, 0, 0, 0, 0, 0, -13, 0, 0, -1, -6, 6, 3, 5, 5, 0, 6, 0, 6, 3, 5, 0, -16, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, -1, -1, -6, 3, 3, 3, 11, -6, 0, 6, -1, 0, 4, 11, 11, 5, -6, -23, -23, -23, 0, 4, 3, 0, 11, 0, 0, 0, 0, 0, 0, -23, -9, -9, 6, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 6, -61, 0, 0, 0, -50, 6, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, -40, 0, -11, -68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -11, -11, -9, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -40, -40, -40, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -40, 0, 0, -10, 0, -46, 0, -68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -68, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -25, 0, 0, -32, -13, -32, -13, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -15, -32, 0, -15, -13, 6, 0, 0, 0, 0, 0, 0, 0, 0, -31, -64, 0, -11, 0, -18, -27, 0, -16, 0, 0, 0, 0, -32, 0, 0, -36, -52, 0, -3, 0, 0, -16, 0, -52, 0, -25, 0, 0, 0, -34, -58, -49, -64, -11, -23, -11, 0, -64, -16, 0, -51, 0, 0, 0, 0, 0, -64, 6, 6, 6, -16, 0, -11, 34, 0, 0, 0, 0, 0, -15, 0, 6, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -52, 0, 0, 0, 0, 0, 0, 0, -60, -71, 0, 0, 0, -13, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -12, -12, 6, -12, 0, -12, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 2, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 41, 28, 0, 0, 0, 0, 0, 0, 0, 0, 29, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -9, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -36, 0, -22, -20, -11, -35, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, -8, -7, -10, -6, 0, 0, -13, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, -8, 4, 0, 0, 0, 0, 0, 0, -36, -36, -36, 0, 0, 0, 28, 0, 0, 0, -11, 0, 0, 0, -36, 0, 0, 0, -5, 0, 0, -69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -35, -25, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -25, 0, 0, -32, -27, -32, -27, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, -25, -41, -38, 0, -15, -32, 0, -15, -13, 6, 0, 0, 0, 0, 0, 0, 0, 0, -31, -33, 0, -11, 0, -18, -27, 0, -16, 0, 0, 0, 0, -32, 0, 0, -36, -52, 0, -3, 0, 0, -16, 0, -52, 0, -25, 0, 0, 0, -34, -58, -49, -33, -11, -23, -11, 0, -33, -16, 0, -51, 0, 0, 0, 0, 0, -55, 6, 6, 6, -16, 0, -11, 34, 0, 0, 0, 0, 0, -15, 0, 6, 0, 0, 0, -23, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, 0, 0, -16, -27, 0, 0, 0, 0, 0, 0, 0, -60, -71, 0, 0, 0, -13, 0, 0, 0, + 0, 0, 0, 0, -25, 0, 0, 0, -34, 0, -34, -38, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, -59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, -25, -45, 0, 0, 0, 0, 0, -57, -34, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -50, 0, 0, -27, 0, 0, -25, -40, -25, -40, -57, -18, 0, 0, 0, -38, 0, -25, -16, 0, -23, -38, -23, 0, -23, -55, 0, -23, -50, 0, 0, 0, 0, 0, 0, 0, 0, -16, -23, -25, 0, 0, 0, -8, -31, 6, -16, 6, 0, 0, 0, -43, 0, 0, -34, -25, 0, 0, 0, 0, -34, -4, -25, 0, 0, -27, 0, 0, 0, -23, -23, -25, 0, 0, 0, 0, -25, -16, 6, -23, 0, 0, 0, 0, 0, -25, 0, 0, 0, -16, 0, 0, 0, 0, -20, -36, 0, 0, -23, 0, 0, -48, -25, 6, 0, 6, -44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, -36, -25, 0, -43, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0, 0, 0, 0, 0, 0, 0, -20, -13, 0, 0, 0, 0, 0, 0, -38, -38, -38, 0, 0, 0, 0, 0, 20, 20, 0, 0, 0, -25, -59, 25, 25, -20, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -43, -50, -38, 0, 0, 0, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, -13, 0, -16, 0, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -43, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, + -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -36, 0, -9, 0, -6, -6, -8, -3, 0, 0, 0, -40, -18, 0, 0, -18, 0, -13, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, -13, 0, 0, 0, 0, 0, 0, 0, 0, -9, -18, 0, -13, 0, 0, 0, -18, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -23, 0, -54, -73, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 5, -3, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, -16, 0, -50, 0, -53, -35, 0, -60, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 5, 0, -50, -50, -79, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -16, -63, 0, 0, 0, 0, 0, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -6, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -27, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -10, -12, }; static constexpr EpdFontData inter_12_regular = { inter_12_regularBitmaps, inter_12_regularGlyphs, inter_12_regularIntervals, - 63, + 77, 30, 25, -7, @@ -5037,10 +5696,10 @@ static constexpr EpdFontData inter_12_regular = { inter_12_regularKernLeftClasses, inter_12_regularKernRightClasses, inter_12_regularKernMatrix, - 764, - 769, - 145, - 140, + 768, + 788, + 146, + 143, nullptr, 0, }; diff --git a/lib/EpdFont/builtinFonts/inter_8_regular.h b/lib/EpdFont/builtinFonts/inter_8_regular.h index d229e0763a..39edc5c35b 100644 --- a/lib/EpdFont/builtinFonts/inter_8_regular.h +++ b/lib/EpdFont/builtinFonts/inter_8_regular.h @@ -3,12 +3,12 @@ * name: inter_8_regular * size: 8 * mode: 1-bit - * Command used: fontconvert.py inter_8_regular 8 ../builtinFonts/source/Inter/Inter-Regular.ttf ../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-Regular.ttf --additional-intervals 0x05D0,0x05EA + * Command used: fontconvert.py inter_8_regular 8 ../builtinFonts/source/Inter/Inter-Regular.ttf ../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-Regular.ttf ../builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf --additional-intervals 0x05D0,0x05EA --additional-intervals 0x060C,0x060C --additional-intervals 0x061B,0x061B --additional-intervals 0x061F,0x061F --additional-intervals 0x0621,0x0621 --additional-intervals 0x0640,0x0640 --additional-intervals 0x0660,0x0669 --additional-intervals 0x06BA,0x06BA --additional-intervals 0x06D4,0x06D4 --additional-intervals 0x06F0,0x06F9 --additional-intervals 0xFB56,0xFB59 --additional-intervals 0xFB66,0xFB69 --additional-intervals 0xFB7A,0xFB7D --additional-intervals 0xFB88,0xFB95 --additional-intervals 0xFB9E,0xFB9F --additional-intervals 0xFBA6,0xFBB1 --additional-intervals 0xFBFC,0xFBFF --additional-intervals 0xFE80,0xFEFC */ #pragma once #include "EpdFontData.h" -static const uint8_t inter_8_regularBitmaps[16223] = { +static const uint8_t inter_8_regularBitmaps[19437] = { 0x5B, 0x6D, 0xB6, 0xC8, 0x7E, 0x00, 0x47, 0xF7, 0xBD, 0xAC, 0x00, 0x03, 0x30, 0x66, 0x0C, 0xC7, 0xFC, 0xFF, 0x8C, 0xC1, 0x98, 0xFF, 0xDF, 0xF8, 0x8C, 0x31, 0x86, 0x60, 0x0C, 0x03, 0x83, 0xF9, 0xFE, 0x6D, 0xDB, 0x07, 0xC0, 0xFC, 0x0F, 0x83, 0x76, 0xCD, 0xB3, 0x7F, 0xCF, 0xE0, 0xC0, 0x30, @@ -439,312 +439,330 @@ static const uint8_t inter_8_regularBitmaps[16223] = { 0x00, 0x30, 0x0C, 0x03, 0x00, 0xFE, 0x3F, 0x8C, 0x73, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0xDD, 0x80, 0x7F, 0x00, 0x39, 0xE6, 0xC0, 0x6D, 0xF5, 0x80, 0xF9, 0xE0, 0x4D, 0xF3, 0x80, 0x6D, 0x80, 0xCB, 0x7C, 0x80, 0x67, 0x60, 0x77, 0xF7, 0xF7, 0x00, 0x00, 0x79, 0xB3, 0x40, 0x6D, 0xF3, - 0x80, 0xDB, 0xF6, 0xC0, 0x0D, 0xBD, 0x80, 0x0D, 0xBD, 0x80, 0x66, 0x66, 0x6D, 0x80, 0x6F, 0xA0, - 0x4C, 0xEE, 0xE0, 0x26, 0xEF, 0x70, 0x6D, 0xF3, 0x80, 0x02, 0x06, 0x06, 0x0C, 0x0C, 0x18, 0x18, - 0x30, 0x30, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x18, 0x0C, 0x03, 0x01, 0x80, 0x40, 0x30, 0x18, - 0x06, 0x03, 0x01, 0x80, 0x60, 0x10, 0x00, 0x4F, 0xEC, 0x1F, 0x60, 0x7B, 0xFC, 0xF3, 0xFD, 0xEF, - 0xFE, 0x5E, 0xF6, 0xB0, 0x0F, 0x73, 0x00, 0x7F, 0xFF, 0x88, 0x10, 0x30, 0xFB, 0xF7, 0xC3, 0x06, - 0x00, 0x65, 0xFB, 0xFF, 0xE0, 0x00, 0x36, 0x7F, 0x66, 0x34, 0x03, 0xBD, 0xC6, 0x00, 0xC1, 0xF0, - 0xF8, 0xFF, 0x9C, 0x00, 0x00, 0x2F, 0x30, 0x79, 0xF7, 0xCC, 0x30, 0x53, 0xDC, 0xF0, 0x00, 0x0B, - 0xF8, 0xF0, 0x80, 0x87, 0x8F, 0xE8, 0x00, 0x83, 0x79, 0x8F, 0xFE, 0xF8, 0x48, 0xDD, 0xFC, 0x6D, - 0x80, 0x2F, 0xF7, 0x00, 0x7D, 0xFE, 0xDF, 0xE0, 0x66, 0x73, 0x20, 0x00, 0x0E, 0x0F, 0x7F, 0xC0, - 0xF0, 0x00, 0x0E, 0x0F, 0x7F, 0xC0, 0xF0, 0x7F, 0xDF, 0xF8, 0x00, 0x00, 0x7F, 0xDF, 0xF8, 0x00, - 0x00, 0x3C, 0x01, 0xFF, 0xFF, 0xC3, 0xFE, 0x00, 0x03, 0xFC, 0x70, 0xEC, 0x03, 0x00, 0x00, 0x03, - 0xBF, 0xFE, 0x00, 0x60, 0x06, 0x00, 0x67, 0x9F, 0xFF, 0xB8, 0x67, 0xBF, 0xEF, 0x38, 0xDF, 0xF4, - 0x67, 0xB7, 0xBF, 0x30, 0xD6, 0xF7, 0xBF, 0xB8, 0x67, 0xB5, 0xAF, 0x30, 0x10, 0xDF, 0xFD, 0xEF, - 0xEE, 0x86, 0x3D, 0xED, 0xEF, 0x72, 0xED, 0xFF, 0xDF, 0xBF, 0x72, 0x40, 0xFF, 0x6D, 0x00, 0x4E, - 0xE6, 0x66, 0x20, 0xD6, 0xDC, 0xE6, 0x10, 0x5B, 0xDC, 0xE7, 0xA4, 0x30, 0x38, 0x18, 0x7E, 0xFF, - 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0x26, 0x76, 0x26, 0x7E, 0xFF, - 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0x7F, 0x83, 0xFF, 0x07, 0xFC, - 0x03, 0x00, 0x0C, 0x00, 0x37, 0x80, 0xFF, 0x03, 0xCE, 0x0E, 0x18, 0x38, 0x60, 0xC1, 0x83, 0x0E, - 0x0C, 0xF0, 0x03, 0x80, 0x0C, 0x0C, 0x18, 0x7E, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, - 0xC0, 0xC0, 0xC0, 0xC0, 0x07, 0x01, 0xFC, 0x3F, 0xE7, 0x06, 0x60, 0x76, 0x00, 0x7F, 0x07, 0xF0, - 0x60, 0x06, 0x07, 0x70, 0x63, 0xFE, 0x1F, 0xC0, 0x20, 0x0E, 0x0F, 0xE7, 0xB9, 0x87, 0x60, 0x1E, - 0x03, 0xF0, 0x3E, 0x01, 0xD8, 0x36, 0x0D, 0xFF, 0x3F, 0x83, 0x00, 0x3F, 0xFF, 0xFF, 0xC0, 0x4F, - 0xB4, 0xC0, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x01, 0x03, 0x03, 0x03, 0x03, - 0x03, 0x03, 0x03, 0x43, 0xE3, 0x63, 0x7F, 0x3E, 0x08, 0x0F, 0xC0, 0x07, 0xF8, 0x01, 0xFE, 0x00, - 0x61, 0x80, 0x18, 0x60, 0x06, 0x1F, 0x81, 0x87, 0xF8, 0x61, 0x8F, 0x38, 0x60, 0xCE, 0x18, 0x33, - 0x06, 0x1D, 0xC1, 0xFE, 0xE0, 0x7F, 0x00, 0x00, 0x00, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0C, - 0x0C, 0x07, 0xFF, 0xE3, 0xFF, 0xFD, 0x81, 0x87, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x1F, - 0xFC, 0x0F, 0xF0, 0x7F, 0xC3, 0xFF, 0x87, 0xFC, 0x03, 0x00, 0x0C, 0x00, 0x37, 0x80, 0xFF, 0x83, - 0xCE, 0x0C, 0x1C, 0x30, 0x70, 0xC1, 0xC3, 0x07, 0x0C, 0x1C, 0x06, 0x03, 0x00, 0xC0, 0x02, 0xC1, - 0xB0, 0xEC, 0x73, 0x38, 0xCC, 0x3F, 0x0F, 0xC3, 0x38, 0xC6, 0x31, 0xCC, 0x3B, 0x07, 0x18, 0x07, - 0x00, 0xC0, 0x01, 0xC1, 0xF0, 0x7C, 0x3F, 0x1F, 0xC6, 0xF3, 0xBC, 0xCF, 0x73, 0xF8, 0xFC, 0x3F, - 0x0F, 0x83, 0x13, 0x07, 0xC1, 0xE1, 0x00, 0xE0, 0xD8, 0x77, 0x18, 0xCE, 0x33, 0x06, 0xC1, 0xF0, - 0x78, 0x0E, 0x03, 0x03, 0xC0, 0xE0, 0x00, 0xB0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, - 0xC0, 0xF0, 0x3C, 0x0F, 0xFF, 0xFF, 0xC3, 0x00, 0xC0, 0x30, 0x04, 0x01, 0xC0, 0x3C, 0x0F, 0x81, - 0xB0, 0x37, 0x0E, 0x61, 0x8C, 0x3F, 0xCF, 0xF9, 0x83, 0x30, 0x7E, 0x06, 0x7E, 0x7F, 0xBF, 0xD8, - 0x0C, 0x06, 0x03, 0xFD, 0xFF, 0xC1, 0xE0, 0xF0, 0x7F, 0xFF, 0xF0, 0x78, 0x7F, 0xBF, 0xD8, 0x3C, - 0x1E, 0x1F, 0xFD, 0xFF, 0xC1, 0xE0, 0xF0, 0x7F, 0xFF, 0xF0, 0x7E, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, - 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x0F, 0xE0, 0xFF, 0x87, 0xFC, 0x30, 0xE1, 0x87, 0x0C, - 0x38, 0x61, 0xC3, 0x0E, 0x18, 0x71, 0xC3, 0x8C, 0x1C, 0xFF, 0xFF, 0xFF, 0xF0, 0x0F, 0x80, 0x78, - 0x03, 0x7E, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0x40, 0x02, - 0x61, 0x86, 0x71, 0x8E, 0x39, 0x9C, 0x19, 0x98, 0x1D, 0xB8, 0x0F, 0xF0, 0x1F, 0xF0, 0x19, 0x98, - 0x39, 0x9C, 0x31, 0x8C, 0x61, 0x8E, 0xE1, 0x87, 0x0C, 0x0F, 0xC7, 0x39, 0x86, 0x01, 0x80, 0xE0, - 0xF0, 0x3E, 0x01, 0x90, 0x76, 0x1D, 0xFE, 0x3F, 0x03, 0x00, 0x00, 0x70, 0x7C, 0x1F, 0x0F, 0xC7, - 0xF1, 0xBC, 0xEF, 0x33, 0xDC, 0xFE, 0x3F, 0x0F, 0xC3, 0xE0, 0xC0, 0x13, 0x07, 0xC0, 0xE0, 0x01, - 0xC1, 0xF0, 0x7C, 0x3F, 0x1F, 0xC6, 0xF3, 0xBC, 0xCF, 0x73, 0xF8, 0xFC, 0x3F, 0x0F, 0x83, 0x00, - 0xB0, 0x6C, 0x3B, 0x1C, 0xCE, 0x33, 0x0F, 0xC3, 0xF0, 0xCE, 0x31, 0x8C, 0x73, 0x0E, 0xC1, 0xC0, - 0x0F, 0xC3, 0xFC, 0x7F, 0x8C, 0x31, 0x86, 0x30, 0xC6, 0x18, 0xC3, 0x38, 0x67, 0x0C, 0xC1, 0xB8, - 0x3E, 0x06, 0x40, 0x17, 0x01, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xFC, 0x6F, 0x63, 0x7B, 0xBB, 0xCD, - 0x9E, 0x6C, 0xF3, 0xC7, 0x8E, 0x3C, 0x71, 0x80, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3F, - 0xFF, 0xFF, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, 0x07, 0x01, 0xFC, 0x3F, 0xE7, 0x06, 0x60, - 0x76, 0x03, 0x60, 0x36, 0x03, 0x60, 0x36, 0x07, 0x70, 0x63, 0xFE, 0x1F, 0xC0, 0x20, 0x7F, 0xBF, - 0xFF, 0xFF, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, 0x70, - 0x7F, 0xBF, 0xF8, 0x3C, 0x1E, 0x0F, 0x0F, 0xFE, 0xFE, 0x60, 0x30, 0x18, 0x0C, 0x00, 0x07, 0x01, - 0xFC, 0x3F, 0xE7, 0x06, 0x60, 0x76, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x07, 0x70, 0x63, 0xFE, - 0x1F, 0xC0, 0x20, 0x7F, 0xBF, 0xF7, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, - 0xC0, 0x30, 0x0C, 0x00, 0x40, 0x38, 0x36, 0x1D, 0xC6, 0x33, 0x8C, 0xC1, 0xB0, 0x7C, 0x1E, 0x03, - 0x80, 0xC0, 0xF0, 0x38, 0x00, 0x03, 0x00, 0x3C, 0x07, 0xF8, 0x7F, 0xE7, 0x33, 0xB1, 0x8D, 0x8C, - 0x6C, 0x63, 0x63, 0x1B, 0x99, 0xCF, 0xFC, 0x3F, 0xC0, 0x30, 0x01, 0x80, 0x40, 0x4C, 0x1D, 0xC7, - 0x1C, 0xC1, 0xB8, 0x3E, 0x03, 0x80, 0x70, 0x1F, 0x07, 0x70, 0xC7, 0x38, 0x6E, 0x0E, 0x00, 0x98, - 0x1B, 0x03, 0x60, 0x6C, 0x0D, 0x81, 0xB0, 0x36, 0x06, 0xC0, 0xD8, 0x1B, 0x03, 0x7F, 0xFF, 0xFE, - 0x00, 0xC0, 0x18, 0x03, 0x00, 0xB0, 0x7C, 0x1F, 0x07, 0xC1, 0xF0, 0x7E, 0x3D, 0xFF, 0x3D, 0xC0, - 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x00, 0x0B, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, - 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x09, 0x86, 0x1B, - 0x0C, 0x36, 0x18, 0x6C, 0x30, 0xD8, 0x61, 0xB0, 0xC3, 0x61, 0x86, 0xC3, 0x0D, 0x86, 0x1B, 0x0C, - 0x37, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x78, 0x07, 0xE0, 0x1F, 0x00, 0x18, - 0x00, 0xC0, 0x07, 0xE0, 0x3F, 0xC1, 0x8F, 0x0C, 0x18, 0x60, 0xC3, 0x0E, 0x1F, 0xE0, 0xFF, 0x00, - 0x00, 0x16, 0x01, 0xB0, 0x0D, 0x80, 0x6C, 0x03, 0x7E, 0x1B, 0xFC, 0xD8, 0x76, 0xC1, 0xB6, 0x0D, - 0xB0, 0x6D, 0xFF, 0x6F, 0xF3, 0x00, 0x00, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xE3, 0xFD, 0x87, 0xC1, - 0xE0, 0xF0, 0x7F, 0xFF, 0xF0, 0x0E, 0x03, 0xF8, 0x7F, 0xC6, 0x0E, 0x60, 0x60, 0x06, 0x0F, 0xF0, - 0xFF, 0x00, 0x66, 0x06, 0x70, 0xE3, 0xFC, 0x1F, 0x80, 0x40, 0x00, 0xE1, 0x87, 0xF3, 0x1F, 0xF6, - 0x70, 0x7C, 0xC0, 0x79, 0x80, 0xFF, 0x01, 0xFE, 0x03, 0xCC, 0x07, 0x9C, 0x0F, 0x18, 0x36, 0x3F, - 0xEC, 0x3F, 0x80, 0x08, 0x00, 0x07, 0x8F, 0xF7, 0xFD, 0x87, 0x61, 0xD8, 0x77, 0x9C, 0xFF, 0x1F, - 0xCE, 0x77, 0x1D, 0xC7, 0xE1, 0xC0, 0x00, 0x3E, 0x7F, 0x63, 0x0F, 0x7F, 0x73, 0xE3, 0x7F, 0x7F, - 0x10, 0x01, 0x03, 0xCF, 0xCF, 0x06, 0xE3, 0xF9, 0xCF, 0xC3, 0xE1, 0xF0, 0xD8, 0x6E, 0xF3, 0xF0, - 0x60, 0xFE, 0xFE, 0xC6, 0xFE, 0xFE, 0xC7, 0xC3, 0xFF, 0xFE, 0xFF, 0xFC, 0x30, 0xC3, 0x0C, 0x30, - 0xC0, 0x3F, 0x8F, 0xE3, 0x18, 0xC6, 0x31, 0x8C, 0x67, 0x1B, 0xFF, 0xFF, 0xF0, 0x3C, 0x0F, 0x03, - 0x00, 0x1F, 0x9F, 0xCC, 0x37, 0xFF, 0xFF, 0x80, 0xC3, 0x7F, 0x9F, 0x83, 0x00, 0xE3, 0x3B, 0x99, - 0x8E, 0xD8, 0x3F, 0x80, 0xFC, 0x0F, 0xF0, 0xED, 0x8E, 0x66, 0xE3, 0x38, 0x00, 0x7E, 0x7E, 0x66, - 0x0E, 0x1E, 0x07, 0x43, 0x7F, 0x7E, 0x00, 0xC7, 0xC7, 0xCF, 0xDF, 0xDB, 0xF3, 0xF3, 0xE3, 0xC3, - 0x64, 0x7C, 0x3C, 0x00, 0xC7, 0xC7, 0xCF, 0xDF, 0xDB, 0xF3, 0xF3, 0xE3, 0xC3, 0xC7, 0xCE, 0xDC, - 0xF8, 0xF8, 0xF8, 0xDC, 0xCE, 0xC7, 0x3F, 0x9F, 0xCC, 0xE6, 0x73, 0x39, 0x9D, 0xCF, 0xC7, 0xE3, - 0x80, 0xE0, 0xFC, 0x3F, 0xC7, 0xF9, 0xFF, 0xB7, 0xB6, 0xF7, 0x9E, 0x73, 0xCE, 0x60, 0xC3, 0xC3, - 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0x00, 0x0F, 0xC7, 0xF9, 0x86, 0x61, 0xB8, 0x6E, 0x19, - 0x86, 0x7F, 0x8F, 0xC0, 0xC0, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x00, 0x7F, - 0x3F, 0xD8, 0x6C, 0x3E, 0x1F, 0x0F, 0x86, 0xFF, 0x7F, 0x32, 0x18, 0x0C, 0x06, 0x00, 0x00, 0x1F, - 0x9F, 0xEC, 0x36, 0x07, 0x03, 0x80, 0xC3, 0x7F, 0x9F, 0x83, 0x00, 0xFF, 0xFF, 0x18, 0x18, 0x18, - 0x18, 0x18, 0x18, 0x18, 0xE1, 0xB1, 0xD8, 0xCE, 0x63, 0x61, 0xB0, 0xF8, 0x38, 0x1C, 0x0E, 0x06, - 0x0F, 0x06, 0x00, 0x04, 0x00, 0xC0, 0x18, 0x07, 0x03, 0xF8, 0xFF, 0x99, 0xBB, 0x33, 0xE6, 0x7C, - 0xCD, 0x9B, 0xBF, 0xE3, 0xF8, 0x1C, 0x01, 0x80, 0x30, 0x06, 0x00, 0x63, 0x3B, 0x8D, 0x83, 0x81, - 0xC1, 0xE0, 0xD8, 0xEE, 0xE3, 0x80, 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0xFF, 0xFF, - 0x80, 0xC0, 0x60, 0x30, 0xC7, 0xC7, 0xC7, 0xC7, 0xFF, 0x7F, 0x1F, 0x07, 0x07, 0xC6, 0x3C, 0x63, - 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x3F, 0xFF, 0xFF, 0xF0, 0xC6, 0x36, 0x31, 0xB1, 0x8D, - 0x8C, 0x6C, 0x63, 0x63, 0x1B, 0x18, 0xDF, 0xFF, 0xFF, 0xF8, 0x00, 0xC0, 0x06, 0x00, 0x30, 0xF8, - 0x3E, 0x03, 0x80, 0xFE, 0x3F, 0xCE, 0x33, 0x8C, 0xFF, 0x3F, 0x80, 0xC0, 0xF0, 0x3C, 0x0F, 0xF3, - 0xFE, 0xF1, 0xFC, 0x7F, 0xFB, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xFC, 0xFE, 0xC7, 0xC7, 0xFE, 0xFE, - 0x00, 0x1F, 0x1F, 0xCC, 0x71, 0xF8, 0xFC, 0x7E, 0xC7, 0x77, 0x1F, 0x02, 0x00, 0x00, 0x06, 0x3F, - 0x33, 0xFD, 0x98, 0x6F, 0xC3, 0x7E, 0x1B, 0xF0, 0xD9, 0x86, 0xCF, 0xF6, 0x3F, 0x00, 0x60, 0x7F, - 0x7F, 0xE3, 0xE3, 0x7F, 0x3F, 0x73, 0x63, 0xE3, 0x18, 0x0C, 0x03, 0x00, 0x03, 0xF3, 0xF9, 0x86, - 0xFF, 0xFF, 0xF0, 0x18, 0x6F, 0xF3, 0xF0, 0x60, 0x32, 0x1B, 0x8C, 0x80, 0x03, 0xF3, 0xF9, 0x86, - 0xFF, 0xFF, 0xF0, 0x18, 0x6F, 0xF3, 0xF0, 0x60, 0x00, 0x30, 0x3F, 0x0C, 0x07, 0xF3, 0xF9, 0x8E, - 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x18, 0x0C, 0x06, 0x0F, 0x07, 0x00, 0x18, 0x63, 0x00, 0xFF, - 0xFC, 0x30, 0xC3, 0x0C, 0x30, 0xC0, 0x00, 0x1F, 0x9F, 0xEC, 0x37, 0xC7, 0xF3, 0xF0, 0xC3, 0x7F, - 0x9F, 0x83, 0x00, 0x00, 0x3E, 0x7F, 0x63, 0x78, 0x3E, 0x0F, 0x63, 0x77, 0x7E, 0x18, 0x6D, 0x86, - 0xDB, 0x6D, 0xB6, 0xCB, 0xFC, 0xC0, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0x33, 0x30, 0x33, - 0x33, 0x33, 0x33, 0x33, 0x3F, 0x40, 0x3F, 0x80, 0x7F, 0x00, 0xC6, 0x01, 0x8F, 0xE3, 0x1F, 0xC6, - 0x31, 0xCC, 0x61, 0xF0, 0xFF, 0xE1, 0xFC, 0xC3, 0x03, 0x0C, 0x0C, 0x30, 0x3F, 0xF8, 0xFF, 0xFB, - 0x0C, 0xEC, 0x31, 0xB0, 0xFE, 0xC3, 0xF0, 0x00, 0x30, 0x3F, 0x1F, 0x87, 0xF3, 0xF9, 0x8E, 0xC3, - 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x18, 0x0C, 0x18, 0x18, 0x00, 0xC7, 0xCE, 0xDC, 0xF8, 0xF8, 0xF8, - 0xDC, 0xCE, 0xC7, 0x30, 0x30, 0x18, 0x00, 0xC7, 0xC7, 0xCF, 0xDF, 0xDB, 0xF3, 0xF3, 0xE3, 0xC3, - 0x32, 0x1F, 0x07, 0x00, 0x0E, 0x1B, 0x1D, 0x8C, 0xE6, 0x36, 0x1B, 0x0F, 0x83, 0x81, 0xC0, 0xE0, - 0x60, 0xF0, 0x60, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0x18, 0x18, 0x18, - 0x02, 0x16, 0x39, 0xF1, 0xCF, 0x8E, 0x7C, 0x73, 0xE3, 0x9F, 0x1C, 0xF8, 0xE7, 0xC7, 0x3E, 0x39, - 0xF1, 0xCF, 0xFF, 0xE7, 0xFF, 0x08, 0x00, 0xC6, 0x6C, 0x66, 0xC6, 0x6C, 0x66, 0xC6, 0x6C, 0x66, - 0xC6, 0x6F, 0xFE, 0x7B, 0xE0, 0x08, 0x00, 0x06, 0x03, 0xF8, 0x7F, 0x03, 0x00, 0x7E, 0x0F, 0xF1, - 0x8F, 0x30, 0x66, 0x0C, 0xC3, 0x9F, 0xE3, 0xF8, 0x00, 0x0C, 0x03, 0x00, 0xC0, 0xFE, 0x3F, 0x03, - 0x00, 0xFC, 0x3F, 0x8C, 0x73, 0x1C, 0xFE, 0x3F, 0x80, 0x00, 0xC3, 0x0F, 0xCC, 0x7F, 0xB3, 0x87, - 0xCC, 0x0F, 0x70, 0x0F, 0xFC, 0x3F, 0xF0, 0xDC, 0x03, 0x30, 0x3C, 0xE1, 0xF1, 0xFE, 0xC3, 0xF0, - 0x01, 0x00, 0x00, 0x0C, 0x7C, 0xCF, 0xED, 0xC6, 0xFF, 0x0F, 0xF8, 0xFF, 0x0D, 0xC6, 0xCE, 0xEC, - 0x7C, 0x01, 0x00, 0x04, 0x01, 0xC0, 0x3C, 0x0F, 0x81, 0xB0, 0x37, 0x0E, 0x61, 0xFC, 0x3F, 0xCF, - 0xF9, 0x9B, 0x33, 0x7E, 0x66, 0x1C, 0x07, 0x01, 0xE0, 0xD8, 0x37, 0x1F, 0xC7, 0xF1, 0xBE, 0xED, - 0x80, 0x00, 0x81, 0x83, 0x83, 0x07, 0x86, 0x0F, 0x0C, 0x36, 0x18, 0x6E, 0x31, 0xCC, 0x7F, 0xFC, - 0xFF, 0xF9, 0xFF, 0xF3, 0x33, 0x76, 0x66, 0x6D, 0xCC, 0xC0, 0xC3, 0x0C, 0x78, 0xC7, 0x8C, 0x7C, - 0xFE, 0xCF, 0xFC, 0xDF, 0xED, 0xB6, 0xDB, 0x70, 0x3F, 0xC7, 0xFE, 0x3F, 0xC3, 0x9C, 0x19, 0x80, - 0xF8, 0x3F, 0xC7, 0xFE, 0xE6, 0x7C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x30, 0x3F, 0xC7, 0xF0, 0x6E, - 0x0F, 0x83, 0xFC, 0xFF, 0xD9, 0x9F, 0x33, 0xE6, 0x60, 0x01, 0xFE, 0x61, 0xFF, 0xB0, 0x7F, 0x98, - 0x39, 0xCC, 0x0C, 0xC6, 0x07, 0xC3, 0xFF, 0xF9, 0xFF, 0xFE, 0xC7, 0x33, 0xE3, 0x18, 0xF1, 0x8C, - 0x78, 0xC6, 0x3C, 0x63, 0x18, 0xC7, 0xF9, 0x87, 0xF3, 0x0E, 0xC6, 0x0F, 0x8F, 0xFF, 0x9F, 0xFF, - 0xB3, 0x33, 0x66, 0x66, 0xCC, 0xCC, 0x33, 0x0F, 0x03, 0x06, 0x07, 0xE3, 0xF8, 0x0C, 0x06, 0x07, - 0x1F, 0x0F, 0xC0, 0x60, 0x30, 0x18, 0x7C, 0xFC, 0x78, 0x30, 0x1C, 0x06, 0x00, 0x66, 0x3C, 0x18, - 0x00, 0x7E, 0x7E, 0x07, 0x1E, 0x3E, 0x3E, 0x03, 0x3E, 0x7E, 0xE0, 0xE0, 0x70, 0x30, 0x02, 0x16, - 0x31, 0xF1, 0x8F, 0x8C, 0x7C, 0x63, 0x63, 0x1B, 0x98, 0xCE, 0xCE, 0x3F, 0xE0, 0xFC, 0x01, 0x80, - 0x0C, 0x00, 0x60, 0x00, 0x04, 0x01, 0xC0, 0x38, 0x67, 0x3C, 0xE7, 0x9C, 0xF3, 0x9E, 0x73, 0xCE, - 0x7D, 0xDD, 0xFF, 0x1F, 0xC0, 0xE0, 0x1C, 0x03, 0x80, 0x60, 0x07, 0x01, 0xFC, 0x3F, 0xE7, 0x06, - 0x60, 0x76, 0x03, 0x7F, 0xF7, 0xFF, 0x60, 0x36, 0x07, 0x70, 0x63, 0xFE, 0x1F, 0xC0, 0x20, 0x00, - 0x0F, 0xC7, 0xF9, 0x86, 0x79, 0xBF, 0xEE, 0x79, 0x86, 0x7F, 0x8F, 0xC0, 0xC0, 0x40, 0x2E, 0x07, - 0x60, 0xE6, 0x0C, 0x70, 0xC3, 0x1C, 0x31, 0x83, 0x98, 0x1B, 0x81, 0xB0, 0x0F, 0x00, 0xE0, 0x0E, - 0x00, 0xE1, 0xD8, 0xE6, 0x31, 0xDC, 0x36, 0x0D, 0x83, 0xE0, 0x70, 0x1C, 0x00, 0x36, 0x03, 0xF0, - 0x1B, 0x04, 0x02, 0xE0, 0x76, 0x0E, 0x60, 0xC7, 0x0C, 0x31, 0xC3, 0x18, 0x39, 0x81, 0xB8, 0x1B, - 0x00, 0xF0, 0x0E, 0x00, 0xE0, 0x6C, 0x1B, 0x03, 0x60, 0x00, 0xE1, 0xD8, 0xE6, 0x31, 0xDC, 0x36, - 0x0D, 0x83, 0xE0, 0x70, 0x1C, 0x00, 0x07, 0x00, 0x00, 0x7F, 0x00, 0x03, 0xFE, 0x00, 0x1C, 0x18, - 0x00, 0x60, 0x76, 0x1D, 0x80, 0xDC, 0x66, 0x03, 0x31, 0x98, 0x0C, 0xCE, 0x60, 0x33, 0xB1, 0x81, - 0xC6, 0xC7, 0x06, 0x1E, 0x0F, 0xF8, 0x38, 0x1F, 0xC0, 0xE0, 0x08, 0x03, 0x00, 0x00, 0x1C, 0x00, - 0x00, 0xE0, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0xE6, 0x19, 0xFE, 0xC3, 0x30, 0xDC, 0xE6, 0x19, - 0x99, 0xC3, 0x33, 0x38, 0x63, 0xE3, 0x0C, 0x78, 0x7F, 0x8F, 0x07, 0xE0, 0xC0, 0x30, 0x18, 0x00, - 0x07, 0x00, 0x03, 0xC0, 0x00, 0x70, 0x00, 0x07, 0x03, 0xF8, 0xF7, 0x38, 0x36, 0x06, 0xC0, 0x18, - 0x03, 0x00, 0x60, 0x6E, 0x1C, 0xFF, 0x8F, 0xB0, 0x06, 0x00, 0x1F, 0x9F, 0xCC, 0x36, 0x07, 0x01, - 0x80, 0xC0, 0x78, 0x1E, 0x03, 0x01, 0x80, 0xC0, 0x01, 0x86, 0xE1, 0xF0, 0x3C, 0x07, 0xD3, 0x37, - 0xC0, 0xF0, 0x3E, 0x0D, 0x87, 0x00, 0x05, 0xFF, 0xF6, 0x00, 0x1D, 0xFF, 0x98, 0x59, 0x80, 0xDB, - 0x40, 0xF1, 0xFE, 0x38, 0x01, 0xC0, 0x01, 0xA0, 0x0E, 0x03, 0x87, 0x83, 0xC0, 0x00, 0x00, 0x00, - 0x01, 0xC0, 0x0D, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x03, 0x07, 0x83, 0xC2, 0x81, 0x20, - 0x0E, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0x19, 0x84, 0x18, 0x1C, 0x08, 0x18, - 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x30, 0x03, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x18, 0x38, 0x8C, - 0x01, 0x8C, 0x01, 0x80, 0x01, 0x80, 0x13, 0x01, 0xF0, 0x0E, 0x00, 0x04, 0xC1, 0xCC, 0x1C, 0xC3, - 0xCC, 0x7C, 0xC6, 0xCC, 0xEC, 0xCC, 0xCD, 0xCC, 0xF8, 0xCF, 0x0C, 0xF0, 0xFE, 0x0F, 0x00, 0x60, - 0x06, 0x00, 0x60, 0x64, 0x1F, 0x03, 0xC0, 0x00, 0xC7, 0x31, 0xCC, 0xF3, 0x7C, 0xDB, 0x3C, 0xCF, - 0x33, 0x8E, 0xC3, 0x80, 0x60, 0x38, 0x0C, 0x00, 0x06, 0x03, 0xF8, 0x7F, 0x03, 0x00, 0x7E, 0x0F, - 0xF1, 0x8F, 0x30, 0x66, 0x0C, 0xC3, 0x9F, 0xE3, 0xF8, 0x20, 0x0C, 0x03, 0x03, 0xF8, 0xFC, 0x0C, - 0x03, 0xF0, 0xFE, 0x31, 0xCC, 0x73, 0xF8, 0xFE, 0x70, 0x7F, 0xBF, 0xF8, 0x3C, 0x1E, 0x2F, 0x1F, - 0xFE, 0xFF, 0x61, 0xB0, 0x18, 0x0C, 0x00, 0x00, 0x7F, 0x3F, 0xD8, 0x6C, 0x3E, 0x1F, 0x0F, 0x9E, - 0xFF, 0x7F, 0xB2, 0xD8, 0x0C, 0x06, 0x00, 0x03, 0x03, 0x7F, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, - 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x04, 0x30, 0xFF, 0xFF, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0x1F, - 0x8F, 0xF3, 0xFC, 0xC0, 0x30, 0x0C, 0x07, 0xF1, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x00, - 0x7E, 0xFD, 0x83, 0x0F, 0xDF, 0x98, 0x30, 0x60, 0x7E, 0x7F, 0xBF, 0xD8, 0x0C, 0x06, 0xF3, 0xFD, - 0xE7, 0xE1, 0xE0, 0xF0, 0x78, 0x7D, 0xF0, 0xF0, 0xFC, 0xFC, 0xC0, 0xC0, 0xFC, 0xFE, 0xC6, 0xC6, - 0xC6, 0x06, 0x3E, 0x38, 0x40, 0x02, 0x30, 0xC3, 0x1C, 0x63, 0x87, 0x33, 0x81, 0x99, 0x80, 0xED, - 0xC0, 0x3F, 0xC0, 0x3F, 0xE0, 0x19, 0x98, 0x1C, 0xCE, 0x0C, 0x63, 0x0C, 0x31, 0xEE, 0x18, 0x70, - 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, 0xE3, 0x39, 0xCC, 0xC3, 0xB6, 0x07, 0xF0, 0x0F, 0xC0, 0x7F, - 0x83, 0xB6, 0x1C, 0xCF, 0xE3, 0x3C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x0C, 0x0F, 0xC7, 0x39, 0x86, - 0x01, 0x80, 0xE0, 0xF0, 0x3E, 0x01, 0x90, 0x76, 0x1D, 0xFE, 0x3F, 0x03, 0x80, 0xF0, 0x3C, 0x0E, - 0x00, 0x00, 0x7E, 0x7E, 0x66, 0x0E, 0x1E, 0x07, 0x43, 0x7F, 0x7E, 0x18, 0x1C, 0x1C, 0x18, 0x00, - 0x98, 0x33, 0x0E, 0x63, 0x8C, 0xE1, 0x98, 0x3F, 0x07, 0xE0, 0xCE, 0x18, 0xC3, 0x1C, 0x61, 0xEC, - 0x1E, 0x00, 0xC0, 0x18, 0x03, 0xC7, 0x67, 0x37, 0x1F, 0x0F, 0x87, 0xC3, 0x71, 0x9E, 0xC7, 0x81, - 0xC0, 0xE0, 0x30, 0x00, 0xB0, 0x6C, 0x3B, 0x0C, 0xDF, 0x37, 0x8F, 0xE3, 0xF8, 0xDE, 0x37, 0xCC, - 0x3B, 0x06, 0xC1, 0xC0, 0xC3, 0x63, 0xBD, 0x9F, 0x8F, 0xC7, 0xF3, 0xD9, 0x8E, 0xC3, 0x80, 0x78, - 0x0B, 0xF0, 0xE7, 0xC3, 0x03, 0x1C, 0x0C, 0xE0, 0x37, 0x00, 0xF8, 0x03, 0xF0, 0x0C, 0xC0, 0x31, - 0x80, 0xC7, 0x03, 0x0E, 0x0C, 0x18, 0xF9, 0xDF, 0x30, 0xEE, 0x1F, 0x83, 0xE0, 0x7E, 0x0E, 0xE1, - 0xCC, 0x38, 0xC0, 0x00, 0x0C, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xFF, 0xCF, 0xFC, 0xC0, - 0xCC, 0x0C, 0xC0, 0xCC, 0x0E, 0xC0, 0xE0, 0x06, 0x00, 0x60, 0x06, 0xC3, 0x61, 0xB0, 0xDF, 0xEF, - 0xF6, 0x1B, 0x0D, 0x87, 0xC3, 0x80, 0xC0, 0x60, 0x30, 0x00, 0xFD, 0x81, 0xFF, 0x03, 0xF6, 0x06, - 0x0C, 0x0C, 0x18, 0x18, 0x3F, 0xF0, 0x7F, 0xE0, 0xC0, 0xC1, 0x81, 0x83, 0x03, 0x06, 0x06, 0x0C, - 0x0C, 0x00, 0xC3, 0xF0, 0xFC, 0x33, 0xFC, 0xFF, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x00, 0x7F, 0x80, - 0x7F, 0xE0, 0x3F, 0xF0, 0x18, 0x18, 0x0C, 0x0C, 0x06, 0x06, 0xE3, 0x03, 0xFD, 0x81, 0xCE, 0xC0, - 0xC3, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x7C, 0x0D, 0xF0, 0x00, 0xF0, 0xFF, 0x03, 0xFC, 0x0C, - 0x30, 0x30, 0xF0, 0xC3, 0xF3, 0x0D, 0xEC, 0x31, 0xF0, 0xC3, 0xC3, 0x0C, 0x00, 0xE0, 0x07, 0x80, - 0x08, 0x04, 0x00, 0x1E, 0x00, 0x3C, 0xF8, 0x71, 0xFC, 0x61, 0x8E, 0x63, 0x86, 0x63, 0x86, 0x63, - 0x86, 0x61, 0x8E, 0x61, 0xCC, 0x70, 0xFC, 0x3F, 0xFF, 0x1F, 0xFF, 0x03, 0x04, 0x00, 0x01, 0xC0, - 0x1E, 0xF8, 0xCF, 0xC6, 0x63, 0x73, 0x1B, 0x98, 0xCC, 0xEC, 0x7F, 0xF1, 0xFF, 0xC1, 0x88, 0x07, - 0x01, 0xFC, 0x3F, 0xE7, 0x06, 0x60, 0x76, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x07, 0x70, 0x63, - 0xFE, 0x1F, 0xC0, 0x70, 0x03, 0x80, 0x78, 0x07, 0x00, 0x00, 0x1F, 0x9F, 0xEC, 0x36, 0x07, 0x03, - 0x80, 0xC3, 0x7F, 0x9F, 0x83, 0x01, 0xC0, 0xE0, 0x60, 0x7F, 0xBF, 0xF7, 0xFC, 0x30, 0x0C, 0x03, - 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x0F, 0x01, 0xC0, 0x70, 0x0C, 0xFF, 0xFF, 0x18, - 0x18, 0x18, 0x18, 0x18, 0x1C, 0x1C, 0x0C, 0x0C, 0x0C, 0x40, 0x5C, 0x1D, 0xC3, 0x18, 0xE3, 0xB8, - 0x36, 0x07, 0xC0, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0xE1, 0xB1, 0xD8, 0xCE, 0x63, - 0x61, 0xB0, 0xF8, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x80, 0xC0, 0x40, 0x5C, 0x1D, 0xC3, 0x18, 0xE3, - 0xB8, 0x36, 0x07, 0xC0, 0x70, 0x0E, 0x07, 0xF0, 0xFE, 0x07, 0x00, 0xE0, 0xE1, 0xB1, 0xD8, 0xCE, - 0x63, 0x61, 0xB0, 0xF8, 0x38, 0x1C, 0x1F, 0x8F, 0x83, 0x80, 0xC0, 0x40, 0x46, 0x0E, 0x71, 0xC3, - 0x98, 0x1B, 0x81, 0xF0, 0x0E, 0x00, 0xE0, 0x1F, 0x03, 0xB8, 0x31, 0xC7, 0x0F, 0xE0, 0xF0, 0x03, - 0x00, 0x30, 0x03, 0x63, 0x1D, 0xC3, 0x60, 0x70, 0x1C, 0x0F, 0x03, 0x61, 0xDE, 0xE3, 0xC0, 0x70, - 0x1C, 0x03, 0x7F, 0x81, 0xFF, 0x99, 0xFF, 0x30, 0x60, 0x60, 0xC0, 0xC1, 0x81, 0x83, 0x03, 0x06, - 0x06, 0x0C, 0x0C, 0x18, 0x18, 0x30, 0x30, 0x7F, 0xF0, 0xFF, 0xE0, 0x00, 0xC0, 0x01, 0x80, 0x03, - 0xFC, 0xDF, 0x98, 0xC3, 0x18, 0x63, 0x0C, 0x61, 0x8C, 0x31, 0xFF, 0x3F, 0xE0, 0x0C, 0x01, 0x80, - 0x30, 0x00, 0x98, 0x3B, 0x07, 0x60, 0xEC, 0x1D, 0x83, 0xB8, 0xF3, 0xFE, 0x3D, 0xC0, 0x38, 0x07, - 0x00, 0xE0, 0x1E, 0x01, 0xC0, 0x38, 0x03, 0xC7, 0x63, 0xB1, 0xD8, 0xEF, 0xF3, 0xF8, 0x7C, 0x0F, - 0x07, 0x80, 0xC0, 0x60, 0x30, 0x00, 0xB0, 0x7C, 0x1F, 0x07, 0xC1, 0xF3, 0x7E, 0xFD, 0xFF, 0x3D, - 0xC3, 0x70, 0xDC, 0x07, 0x01, 0xC0, 0xC7, 0xC7, 0xC7, 0xDF, 0xFF, 0x7F, 0x1F, 0x1F, 0x1F, 0x00, - 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x37, 0xCF, 0xFB, 0x86, 0xC1, 0xF0, 0x7C, 0x1F, 0x07, 0xC1, 0xC0, - 0x00, 0xC0, 0xC0, 0xC0, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x01, 0x80, 0x7F, - 0x07, 0xFC, 0x38, 0x7F, 0x81, 0xFC, 0x0F, 0xFF, 0xEF, 0xFF, 0x38, 0x00, 0xC0, 0xC7, 0x0E, 0x1F, - 0xE0, 0x7E, 0x00, 0xC0, 0x00, 0x00, 0xFC, 0x1F, 0xED, 0x86, 0xFF, 0xE7, 0xFE, 0x18, 0x01, 0x86, - 0x1F, 0xE0, 0xFC, 0x03, 0x00, 0x01, 0x80, 0x7F, 0x07, 0xFC, 0x38, 0x7F, 0x81, 0xFC, 0x0F, 0xFF, - 0xEF, 0xFF, 0x38, 0x00, 0xC0, 0xC7, 0x0E, 0x1F, 0xE0, 0x7E, 0x00, 0xC0, 0x06, 0x00, 0x30, 0x00, - 0x00, 0xFC, 0x1F, 0xED, 0x86, 0xFF, 0xE7, 0xFE, 0x18, 0x01, 0x86, 0x1F, 0xE0, 0xFC, 0x03, 0x00, - 0x38, 0x03, 0x80, 0x30, 0x3F, 0xFF, 0xFF, 0xC0, 0x06, 0x60, 0x07, 0xC0, 0x03, 0xC0, 0x40, 0x02, - 0x61, 0x86, 0x71, 0x8E, 0x39, 0x9C, 0x19, 0x98, 0x1D, 0xB8, 0x0F, 0xF0, 0x1F, 0xF0, 0x19, 0x98, - 0x39, 0x9C, 0x31, 0x8C, 0x61, 0x8E, 0xE1, 0x87, 0x0C, 0x80, 0x7C, 0x01, 0xC0, 0x00, 0x0E, 0x33, - 0xB9, 0x98, 0xED, 0x83, 0xF8, 0x0F, 0xC0, 0xFF, 0x0E, 0xD8, 0xE6, 0x6E, 0x33, 0x80, 0x00, 0xB0, - 0x6C, 0x33, 0x1C, 0xCE, 0x37, 0x0F, 0xE3, 0xFC, 0xC3, 0xB0, 0x6C, 0x1F, 0x07, 0xC1, 0x80, 0xE0, - 0x70, 0x18, 0xC7, 0xCE, 0xDC, 0xF8, 0xFE, 0xFF, 0xC3, 0xC3, 0xC3, 0x0F, 0x0E, 0x00, 0x0F, 0xC0, - 0xFF, 0x07, 0xF8, 0x30, 0xC1, 0x86, 0x0C, 0x30, 0x61, 0x83, 0x0C, 0x38, 0x61, 0xC3, 0x0C, 0x18, - 0xE0, 0xFE, 0x07, 0x80, 0x18, 0x00, 0xC0, 0x06, 0x3F, 0x8F, 0xE3, 0x38, 0xCE, 0x33, 0x8C, 0xE7, - 0x3B, 0x8F, 0xE3, 0xC0, 0x70, 0x18, 0x06, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3F, 0xFF, - 0xFF, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x0F, 0x03, 0x80, 0xC3, 0xC3, 0xC3, - 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0x03, 0x03, 0x0F, 0x0E, 0x00, 0x0C, 0x0C, 0xC0, 0xCC, 0x0C, - 0xC0, 0xCC, 0x0C, 0xFF, 0xCF, 0xFC, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0F, 0xC0, 0xF0, 0x06, 0x00, - 0x60, 0x06, 0xC3, 0x30, 0xCC, 0x33, 0xFC, 0xFF, 0x30, 0xCC, 0x33, 0x0E, 0xC3, 0x80, 0x60, 0x38, - 0x0C, 0x00, 0x98, 0x3B, 0x07, 0x60, 0xEC, 0x1D, 0x83, 0xB8, 0xF3, 0xFE, 0x3D, 0xC0, 0x38, 0x07, - 0x00, 0xE0, 0x1E, 0x01, 0xC0, 0x38, 0x03, 0xC7, 0x63, 0xB1, 0xD8, 0xEF, 0xF3, 0xF8, 0x7C, 0x0F, - 0x07, 0x80, 0xC0, 0x60, 0x30, 0x40, 0x11, 0xC0, 0x73, 0xC1, 0xE7, 0x83, 0xCF, 0x07, 0x9F, 0x1B, - 0x36, 0x36, 0x6E, 0xEC, 0xCD, 0x99, 0x9B, 0x33, 0x3C, 0x66, 0x38, 0xEC, 0x71, 0xC0, 0x01, 0x80, - 0x07, 0x00, 0x0C, 0xE0, 0xE7, 0x0F, 0x3C, 0x79, 0xE7, 0xCF, 0xB6, 0x6D, 0xB3, 0x79, 0x99, 0xCE, - 0xCE, 0x70, 0x01, 0x80, 0x1C, 0x00, 0xC0, 0x3F, 0xFF, 0xFF, 0xC0, 0x19, 0x03, 0xE0, 0x38, 0x02, - 0x00, 0xE0, 0x1E, 0x07, 0xC0, 0xD8, 0x1B, 0x87, 0x30, 0xC6, 0x1F, 0xE7, 0xFC, 0xC1, 0x98, 0x3F, - 0x03, 0x36, 0x3E, 0x1C, 0x00, 0x3E, 0x7F, 0x63, 0x0F, 0x7F, 0x73, 0xE3, 0x7F, 0x7F, 0x10, 0x19, - 0x03, 0x70, 0x64, 0x02, 0x00, 0xE0, 0x1E, 0x07, 0xC0, 0xD8, 0x1B, 0x87, 0x30, 0xC6, 0x1F, 0xE7, - 0xFC, 0xC1, 0x98, 0x3F, 0x03, 0x36, 0x76, 0x36, 0x00, 0x3E, 0x7F, 0x63, 0x0F, 0x7F, 0x73, 0xE3, - 0x7F, 0x7F, 0x10, 0x01, 0xFE, 0x07, 0xFF, 0x06, 0xFE, 0x0E, 0xC0, 0x0C, 0xC0, 0x0C, 0xC0, 0x18, - 0xFE, 0x18, 0xFE, 0x3F, 0xC0, 0x3F, 0xC0, 0x70, 0xC0, 0x60, 0xFF, 0xE0, 0xFF, 0x00, 0x00, 0x7D, - 0xF1, 0xFF, 0xF3, 0x1C, 0x61, 0xFF, 0xEF, 0xFF, 0xDC, 0xC0, 0x71, 0xC6, 0x7F, 0xDC, 0xFD, 0xF8, - 0x60, 0x80, 0x26, 0x3E, 0x1C, 0x7E, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, - 0xFF, 0xFF, 0x32, 0x1F, 0x07, 0x80, 0x03, 0xF3, 0xF9, 0x86, 0xFF, 0xFF, 0xF0, 0x18, 0x6F, 0xF3, - 0xF0, 0x60, 0x06, 0x01, 0xF8, 0x3F, 0xC7, 0x0E, 0x60, 0x60, 0x07, 0x7F, 0xF7, 0xFF, 0x60, 0x76, - 0x06, 0x70, 0xE3, 0xFC, 0x1F, 0x80, 0x60, 0x08, 0x1F, 0x9F, 0xCC, 0x30, 0x1F, 0xFF, 0xFE, 0xC3, - 0x7F, 0x1F, 0x00, 0x00, 0x09, 0x81, 0xD8, 0x09, 0x80, 0x60, 0x1F, 0x83, 0xFC, 0x70, 0xE6, 0x06, - 0x00, 0x77, 0xFF, 0x7F, 0xF6, 0x07, 0x60, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, 0x00, 0x32, 0x1B, - 0x8C, 0x81, 0x03, 0xF3, 0xF9, 0x86, 0x03, 0xFF, 0xFF, 0xD8, 0x6F, 0xE3, 0xE0, 0x00, 0x06, 0x40, - 0x06, 0xE0, 0x06, 0x40, 0x40, 0x02, 0x61, 0x86, 0x71, 0x8E, 0x39, 0x9C, 0x19, 0x98, 0x1D, 0xB8, - 0x0F, 0xF0, 0x1F, 0xF0, 0x19, 0x98, 0x39, 0x9C, 0x31, 0x8C, 0x61, 0x8E, 0xE1, 0x87, 0x0C, 0x80, - 0x6E, 0x03, 0x20, 0x00, 0x0E, 0x33, 0xB9, 0x98, 0xED, 0x83, 0xF8, 0x0F, 0xC0, 0xFF, 0x0E, 0xD8, - 0xE6, 0x6E, 0x33, 0x80, 0x13, 0x0E, 0xC1, 0x30, 0x30, 0x3F, 0x1C, 0xE6, 0x18, 0x06, 0x03, 0x83, - 0xC0, 0xF8, 0x06, 0x41, 0xD8, 0x77, 0xF8, 0xFC, 0x0C, 0x00, 0x26, 0x7E, 0x26, 0x00, 0x7E, 0x7E, - 0x66, 0x0E, 0x1E, 0x07, 0x43, 0x7F, 0x7E, 0x00, 0x3F, 0x1F, 0xE3, 0xF8, 0x1C, 0x0E, 0x07, 0x01, - 0xF0, 0x1E, 0x01, 0xD0, 0x76, 0x1D, 0xFE, 0x3F, 0x03, 0x00, 0x3F, 0x1F, 0xE3, 0xF8, 0x1C, 0x0E, - 0x07, 0x01, 0xF0, 0x1E, 0x01, 0xD0, 0x76, 0x1D, 0xFE, 0x3F, 0x03, 0x00, 0x1F, 0x07, 0x80, 0x07, - 0x07, 0xC1, 0xF0, 0xFC, 0x7F, 0x1B, 0xCE, 0xF3, 0x3D, 0xCF, 0xE3, 0xF0, 0xFC, 0x3E, 0x0C, 0x7E, - 0x3C, 0x00, 0xC7, 0xC7, 0xCF, 0xDF, 0xDB, 0xF3, 0xF3, 0xE3, 0xC3, 0x13, 0x0E, 0xC1, 0x30, 0x01, - 0xC1, 0xF0, 0x7C, 0x3F, 0x1F, 0xC6, 0xF3, 0xBC, 0xCF, 0x73, 0xF8, 0xFC, 0x3F, 0x0F, 0x83, 0x64, - 0x6E, 0x64, 0x00, 0xC7, 0xC7, 0xCF, 0xDF, 0xDB, 0xF3, 0xF3, 0xE3, 0xC3, 0x09, 0x81, 0xD8, 0x09, - 0x80, 0x70, 0x1F, 0xC3, 0xFE, 0x70, 0x66, 0x07, 0x60, 0x36, 0x03, 0x60, 0x36, 0x03, 0x60, 0x77, - 0x06, 0x3F, 0xE1, 0xFC, 0x02, 0x00, 0x32, 0x0D, 0xC3, 0x20, 0x00, 0x3F, 0x1F, 0xE6, 0x19, 0x86, - 0xE1, 0xB8, 0x66, 0x19, 0xFE, 0x3F, 0x03, 0x00, 0x07, 0x01, 0xFC, 0x3F, 0xE7, 0x06, 0x60, 0x76, - 0x03, 0x7F, 0xF7, 0xFF, 0x60, 0x36, 0x07, 0x70, 0x63, 0xFE, 0x1F, 0xC0, 0x20, 0x00, 0x0F, 0xC7, - 0xF9, 0x86, 0x79, 0xBF, 0xEE, 0x79, 0x86, 0x7F, 0x8F, 0xC0, 0xC0, 0x09, 0x81, 0xD8, 0x09, 0x80, - 0x70, 0x1F, 0xC3, 0xFE, 0x70, 0x66, 0x07, 0x60, 0x37, 0xFF, 0x7F, 0xF6, 0x03, 0x60, 0x77, 0x06, - 0x3F, 0xE1, 0xFC, 0x02, 0x00, 0x32, 0x0D, 0xC3, 0x20, 0x00, 0x3F, 0x1F, 0xE6, 0x19, 0xE6, 0xFF, - 0xB9, 0xE6, 0x19, 0xFE, 0x3F, 0x03, 0x00, 0x19, 0x01, 0xB8, 0x19, 0x00, 0xE0, 0x3F, 0x87, 0xFC, - 0x60, 0xE6, 0x06, 0x00, 0x60, 0xFF, 0x0F, 0xF0, 0x06, 0x60, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x04, - 0x00, 0x36, 0x3B, 0x0D, 0x80, 0x03, 0xE3, 0xF9, 0x8E, 0x3F, 0x1F, 0x8F, 0xD8, 0xEE, 0xE3, 0xE0, - 0x40, 0x1F, 0x07, 0x84, 0x03, 0x83, 0x61, 0xDC, 0x63, 0x38, 0xCC, 0x1B, 0x07, 0xC1, 0xE0, 0x38, - 0x0C, 0x0F, 0x03, 0x80, 0x3E, 0x0F, 0x00, 0x1C, 0x36, 0x3B, 0x19, 0xCC, 0x6C, 0x36, 0x1F, 0x07, - 0x03, 0x81, 0xC0, 0xC1, 0xE0, 0xC0, 0x13, 0x0E, 0xC1, 0x31, 0x00, 0xE0, 0xD8, 0x77, 0x18, 0xCE, - 0x33, 0x06, 0xC1, 0xF0, 0x78, 0x0E, 0x03, 0x03, 0xC0, 0xE0, 0x32, 0x1B, 0x8D, 0x80, 0x0E, 0x1B, - 0x1D, 0x8C, 0xE6, 0x36, 0x1B, 0x0F, 0x83, 0x81, 0xC0, 0xE0, 0x60, 0xF0, 0x60, 0x00, 0x1B, 0x06, - 0xC3, 0x61, 0x00, 0xE0, 0xD8, 0x77, 0x18, 0xCE, 0x33, 0x06, 0xC1, 0xF0, 0x78, 0x0E, 0x03, 0x03, - 0xC0, 0xE0, 0x1B, 0x1B, 0x0D, 0x80, 0x0E, 0x1B, 0x1D, 0x8C, 0xE6, 0x36, 0x1B, 0x0F, 0x83, 0x81, - 0xC0, 0xE0, 0x60, 0xF0, 0x60, 0x00, 0x32, 0x0D, 0xC3, 0x20, 0x02, 0xC1, 0xF0, 0x7C, 0x1F, 0x07, - 0xC1, 0xF8, 0xF7, 0xFC, 0xF7, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x64, 0x6E, 0x64, 0x00, 0xC7, 0xC7, - 0xC7, 0xC7, 0xFF, 0x7F, 0x1F, 0x07, 0x07, 0x7E, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, - 0xC0, 0xC0, 0xE0, 0xE0, 0x60, 0x60, 0x60, 0xFF, 0xFC, 0x30, 0xC3, 0x0C, 0x38, 0xE1, 0x86, 0x18, - 0x09, 0x80, 0xFC, 0x06, 0x60, 0x00, 0x2C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x06, 0xFC, 0x37, 0xF9, - 0xB0, 0xED, 0x83, 0x6C, 0x1B, 0x60, 0xDB, 0xFE, 0xDF, 0xE6, 0x13, 0x0F, 0xC3, 0x30, 0x00, 0xC0, - 0xF0, 0x3C, 0x0F, 0xF3, 0xFE, 0xF1, 0xFC, 0x7F, 0xFB, 0xFE, 0xC0, 0x3F, 0x9F, 0xE7, 0xF9, 0xC0, - 0x70, 0x1C, 0x0F, 0xE3, 0xF8, 0x70, 0x1C, 0x07, 0x01, 0xE0, 0x78, 0x06, 0x01, 0x81, 0xC0, 0x60, - 0x00, 0x7E, 0xFD, 0x83, 0x0F, 0xDF, 0x98, 0x38, 0x70, 0x60, 0xC7, 0x8E, 0x00, 0x40, 0x46, 0x0E, - 0x71, 0xC3, 0x98, 0x1B, 0x81, 0xF0, 0x0E, 0x00, 0xE0, 0x1F, 0x03, 0xB8, 0x31, 0xC7, 0x0F, 0xE0, - 0xF0, 0x03, 0x00, 0x30, 0x0F, 0x00, 0xE0, 0xE3, 0x33, 0x8D, 0x87, 0x81, 0xC1, 0xE0, 0xF8, 0xCC, - 0xE3, 0x01, 0x80, 0xC1, 0xE0, 0xE0, 0x40, 0x4C, 0x1D, 0xC7, 0x1C, 0xC1, 0xB8, 0x3E, 0x1F, 0xF3, - 0xFE, 0x1F, 0x07, 0x70, 0xC7, 0x38, 0x6E, 0x0E, 0x63, 0x3B, 0x8D, 0x8F, 0xEF, 0xF1, 0xE0, 0xD8, - 0xEE, 0xE3, 0x80, 0xE3, 0xB1, 0xDC, 0xE7, 0x77, 0xF7, 0xFB, 0x39, 0x8C, 0xC7, 0x61, 0xC0, 0xFE, - 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x7F, 0xC0, 0x78, 0x61, 0xC3, 0x0C, 0x30, - 0xC7, 0x3F, 0xB0, 0xFF, 0x83, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x00, 0xFF, - 0x01, 0x80, 0x60, 0x30, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xC0, 0xFF, 0xFF, 0xF0, 0xFC, 0x43, - 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xE0, 0xFF, 0x61, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, - 0xE0, 0xC0, 0xCF, 0x61, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0x77, 0x3F, 0x80, 0xFF, 0xC0, - 0xFC, 0x0E, 0x07, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFE, 0x07, 0x80, - 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x0F, 0x7F, 0x00, 0xC0, 0xC0, 0xC0, 0xFF, 0x03, 0x07, 0x06, - 0x0E, 0x0C, 0x0C, 0x1C, 0x18, 0x18, 0xFF, 0x61, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, - 0xFF, 0xC0, 0x3F, 0x8E, 0x73, 0x0C, 0xC3, 0x30, 0xDC, 0x36, 0x0D, 0x83, 0x60, 0xD9, 0xF0, 0xFF, - 0xFF, 0xFF, 0xC0, 0xF1, 0xC6, 0x31, 0x8C, 0x63, 0x1F, 0xC0, 0x7F, 0x86, 0x79, 0x83, 0x30, 0x66, - 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x3B, 0x83, 0xE0, 0xE1, 0xB0, 0xD8, 0x66, 0x33, 0x19, 0xCC, 0x66, - 0x36, 0x7E, 0x7C, 0x00, 0xFE, 0x67, 0xB0, 0xD8, 0x7C, 0x1F, 0x8C, 0x06, 0x03, 0x01, 0x80, 0xC0, - 0x60, 0x30, 0x18, 0xFE, 0x63, 0xB0, 0xF8, 0x3C, 0x1F, 0xCC, 0x06, 0x07, 0x07, 0x7F, 0x00, 0xC3, - 0xE3, 0x63, 0x63, 0x73, 0x37, 0x3E, 0x38, 0x18, 0x18, 0x1C, 0x0C, 0x0C, 0xE3, 0x39, 0x8C, 0xC7, - 0x61, 0xF0, 0x70, 0x38, 0x0C, 0x03, 0x3F, 0x80, 0xFF, 0x80, 0xC0, 0x60, 0x60, 0x36, 0x3B, 0x19, - 0x8C, 0xCE, 0x66, 0x30, 0x18, 0x0C, 0x00, 0xFE, 0x06, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, - 0x03, 0xCC, 0x79, 0x8F, 0x31, 0xE6, 0x3C, 0xC7, 0xF8, 0xF0, 0x1F, 0x06, 0x71, 0xC7, 0xF0, 0xFF, - 0x19, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xC3, 0x61, 0xF0, 0xC0, 0x04, 0x01, 0xC0, 0x3C, 0x0F, + 0x80, 0xDB, 0xF6, 0xC0, 0x26, 0x6E, 0x40, 0x0D, 0xBD, 0x80, 0x0D, 0xBD, 0x80, 0x66, 0x66, 0x6D, + 0x80, 0x6F, 0xA0, 0x4C, 0xEE, 0xE0, 0x26, 0xEF, 0x70, 0x6D, 0xF3, 0x80, 0x02, 0x06, 0x06, 0x0C, + 0x0C, 0x18, 0x18, 0x30, 0x30, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x18, 0x0C, 0x03, 0x01, 0x80, + 0x40, 0x30, 0x18, 0x06, 0x03, 0x01, 0x80, 0x60, 0x10, 0x00, 0x4F, 0xEC, 0x1F, 0x60, 0x7B, 0xFC, + 0xF3, 0xFD, 0xEF, 0xFE, 0x5E, 0xF6, 0xB0, 0x0F, 0x73, 0x00, 0x7F, 0xFF, 0x88, 0x10, 0x30, 0xFB, + 0xF7, 0xC3, 0x06, 0x00, 0x65, 0xFB, 0xFF, 0xE0, 0x00, 0x36, 0x7F, 0x66, 0x34, 0x03, 0xBD, 0xC6, + 0x00, 0xC1, 0xF0, 0xF8, 0xFF, 0x9C, 0x00, 0x00, 0x2F, 0x30, 0x79, 0xF7, 0xCC, 0x30, 0x53, 0xDC, + 0xF0, 0x00, 0x0B, 0xF8, 0xF0, 0x80, 0x87, 0x8F, 0xE8, 0x00, 0x83, 0x79, 0x8F, 0xFE, 0xF8, 0x48, + 0xDD, 0xFC, 0x6D, 0x80, 0x2F, 0xF7, 0x00, 0x7D, 0xFE, 0xDF, 0xE0, 0x66, 0x73, 0x20, 0x00, 0x0E, + 0x0F, 0x7F, 0xC0, 0xF0, 0x00, 0x0E, 0x0F, 0x7F, 0xC0, 0xF0, 0x7F, 0xDF, 0xF8, 0x00, 0x00, 0x7F, + 0xDF, 0xF8, 0x00, 0x00, 0x3C, 0x01, 0xFF, 0xFF, 0xC3, 0xFE, 0x00, 0x03, 0xFC, 0x70, 0xEC, 0x03, + 0x00, 0x00, 0x03, 0xBF, 0xFE, 0x00, 0x60, 0x06, 0x00, 0x67, 0x9F, 0xFF, 0xB8, 0x67, 0xBF, 0xEF, + 0x38, 0xDF, 0xF4, 0x67, 0xB7, 0xBF, 0x30, 0xD6, 0xF7, 0xBF, 0xB8, 0x67, 0xB5, 0xAF, 0x30, 0x10, + 0xDF, 0xFD, 0xEF, 0xEE, 0x86, 0x3D, 0xED, 0xEF, 0x72, 0xED, 0xFF, 0xDF, 0xBF, 0x72, 0x40, 0xFF, + 0x6D, 0x00, 0x4E, 0xE6, 0x66, 0x20, 0xD6, 0xDC, 0xE6, 0x10, 0x5B, 0xDC, 0xE7, 0xA4, 0x30, 0x38, + 0x18, 0x7E, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0x26, 0x76, + 0x26, 0x7E, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0x7F, 0x83, + 0xFF, 0x07, 0xFC, 0x03, 0x00, 0x0C, 0x00, 0x37, 0x80, 0xFF, 0x03, 0xCE, 0x0E, 0x18, 0x38, 0x60, + 0xC1, 0x83, 0x0E, 0x0C, 0xF0, 0x03, 0x80, 0x0C, 0x0C, 0x18, 0x7E, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, + 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x07, 0x01, 0xFC, 0x3F, 0xE7, 0x06, 0x60, 0x76, 0x00, + 0x7F, 0x07, 0xF0, 0x60, 0x06, 0x07, 0x70, 0x63, 0xFE, 0x1F, 0xC0, 0x20, 0x0E, 0x0F, 0xE7, 0xB9, + 0x87, 0x60, 0x1E, 0x03, 0xF0, 0x3E, 0x01, 0xD8, 0x36, 0x0D, 0xFF, 0x3F, 0x83, 0x00, 0x3F, 0xFF, + 0xFF, 0xC0, 0x4F, 0xB4, 0xC0, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x01, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x43, 0xE3, 0x63, 0x7F, 0x3E, 0x08, 0x0F, 0xC0, 0x07, 0xF8, + 0x01, 0xFE, 0x00, 0x61, 0x80, 0x18, 0x60, 0x06, 0x1F, 0x81, 0x87, 0xF8, 0x61, 0x8F, 0x38, 0x60, + 0xCE, 0x18, 0x33, 0x06, 0x1D, 0xC1, 0xFE, 0xE0, 0x7F, 0x00, 0x00, 0x00, 0x60, 0x60, 0x30, 0x30, + 0x18, 0x18, 0x0C, 0x0C, 0x07, 0xFF, 0xE3, 0xFF, 0xFD, 0x81, 0x87, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, + 0x30, 0x78, 0x1F, 0xFC, 0x0F, 0xF0, 0x7F, 0xC3, 0xFF, 0x87, 0xFC, 0x03, 0x00, 0x0C, 0x00, 0x37, + 0x80, 0xFF, 0x83, 0xCE, 0x0C, 0x1C, 0x30, 0x70, 0xC1, 0xC3, 0x07, 0x0C, 0x1C, 0x06, 0x03, 0x00, + 0xC0, 0x02, 0xC1, 0xB0, 0xEC, 0x73, 0x38, 0xCC, 0x3F, 0x0F, 0xC3, 0x38, 0xC6, 0x31, 0xCC, 0x3B, + 0x07, 0x18, 0x07, 0x00, 0xC0, 0x01, 0xC1, 0xF0, 0x7C, 0x3F, 0x1F, 0xC6, 0xF3, 0xBC, 0xCF, 0x73, + 0xF8, 0xFC, 0x3F, 0x0F, 0x83, 0x13, 0x07, 0xC1, 0xE1, 0x00, 0xE0, 0xD8, 0x77, 0x18, 0xCE, 0x33, + 0x06, 0xC1, 0xF0, 0x78, 0x0E, 0x03, 0x03, 0xC0, 0xE0, 0x00, 0xB0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, + 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0xFF, 0xFF, 0xC3, 0x00, 0xC0, 0x30, 0x04, 0x01, 0xC0, + 0x3C, 0x0F, 0x81, 0xB0, 0x37, 0x0E, 0x61, 0x8C, 0x3F, 0xCF, 0xF9, 0x83, 0x30, 0x7E, 0x06, 0x7E, + 0x7F, 0xBF, 0xD8, 0x0C, 0x06, 0x03, 0xFD, 0xFF, 0xC1, 0xE0, 0xF0, 0x7F, 0xFF, 0xF0, 0x78, 0x7F, + 0xBF, 0xD8, 0x3C, 0x1E, 0x1F, 0xFD, 0xFF, 0xC1, 0xE0, 0xF0, 0x7F, 0xFF, 0xF0, 0x7E, 0xFF, 0xFF, + 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x0F, 0xE0, 0xFF, 0x87, 0xFC, 0x30, + 0xE1, 0x87, 0x0C, 0x38, 0x61, 0xC3, 0x0E, 0x18, 0x71, 0xC3, 0x8C, 0x1C, 0xFF, 0xFF, 0xFF, 0xF0, + 0x0F, 0x80, 0x78, 0x03, 0x7E, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, + 0xFF, 0x40, 0x02, 0x61, 0x86, 0x71, 0x8E, 0x39, 0x9C, 0x19, 0x98, 0x1D, 0xB8, 0x0F, 0xF0, 0x1F, + 0xF0, 0x19, 0x98, 0x39, 0x9C, 0x31, 0x8C, 0x61, 0x8E, 0xE1, 0x87, 0x0C, 0x0F, 0xC7, 0x39, 0x86, + 0x01, 0x80, 0xE0, 0xF0, 0x3E, 0x01, 0x90, 0x76, 0x1D, 0xFE, 0x3F, 0x03, 0x00, 0x00, 0x70, 0x7C, + 0x1F, 0x0F, 0xC7, 0xF1, 0xBC, 0xEF, 0x33, 0xDC, 0xFE, 0x3F, 0x0F, 0xC3, 0xE0, 0xC0, 0x13, 0x07, + 0xC0, 0xE0, 0x01, 0xC1, 0xF0, 0x7C, 0x3F, 0x1F, 0xC6, 0xF3, 0xBC, 0xCF, 0x73, 0xF8, 0xFC, 0x3F, + 0x0F, 0x83, 0x00, 0xB0, 0x6C, 0x3B, 0x1C, 0xCE, 0x33, 0x0F, 0xC3, 0xF0, 0xCE, 0x31, 0x8C, 0x73, + 0x0E, 0xC1, 0xC0, 0x0F, 0xC3, 0xFC, 0x7F, 0x8C, 0x31, 0x86, 0x30, 0xC6, 0x18, 0xC3, 0x38, 0x67, + 0x0C, 0xC1, 0xB8, 0x3E, 0x06, 0x40, 0x17, 0x01, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xFC, 0x6F, 0x63, + 0x7B, 0xBB, 0xCD, 0x9E, 0x6C, 0xF3, 0xC7, 0x8E, 0x3C, 0x71, 0x80, 0x00, 0x30, 0x3C, 0x0F, 0x03, + 0xC0, 0xF0, 0x3F, 0xFF, 0xFF, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, 0x07, 0x01, 0xFC, 0x3F, + 0xE7, 0x06, 0x60, 0x76, 0x03, 0x60, 0x36, 0x03, 0x60, 0x36, 0x07, 0x70, 0x63, 0xFE, 0x1F, 0xC0, + 0x20, 0x7F, 0xBF, 0xFF, 0xFF, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, + 0xC0, 0xC0, 0x70, 0x7F, 0xBF, 0xF8, 0x3C, 0x1E, 0x0F, 0x0F, 0xFE, 0xFE, 0x60, 0x30, 0x18, 0x0C, + 0x00, 0x07, 0x01, 0xFC, 0x3F, 0xE7, 0x06, 0x60, 0x76, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x07, + 0x70, 0x63, 0xFE, 0x1F, 0xC0, 0x20, 0x7F, 0xBF, 0xF7, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, + 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, 0x40, 0x38, 0x36, 0x1D, 0xC6, 0x33, 0x8C, 0xC1, 0xB0, + 0x7C, 0x1E, 0x03, 0x80, 0xC0, 0xF0, 0x38, 0x00, 0x03, 0x00, 0x3C, 0x07, 0xF8, 0x7F, 0xE7, 0x33, + 0xB1, 0x8D, 0x8C, 0x6C, 0x63, 0x63, 0x1B, 0x99, 0xCF, 0xFC, 0x3F, 0xC0, 0x30, 0x01, 0x80, 0x40, + 0x4C, 0x1D, 0xC7, 0x1C, 0xC1, 0xB8, 0x3E, 0x03, 0x80, 0x70, 0x1F, 0x07, 0x70, 0xC7, 0x38, 0x6E, + 0x0E, 0x00, 0x98, 0x1B, 0x03, 0x60, 0x6C, 0x0D, 0x81, 0xB0, 0x36, 0x06, 0xC0, 0xD8, 0x1B, 0x03, + 0x7F, 0xFF, 0xFE, 0x00, 0xC0, 0x18, 0x03, 0x00, 0xB0, 0x7C, 0x1F, 0x07, 0xC1, 0xF0, 0x7E, 0x3D, + 0xFF, 0x3D, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x00, 0x0B, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, + 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, + 0x09, 0x86, 0x1B, 0x0C, 0x36, 0x18, 0x6C, 0x30, 0xD8, 0x61, 0xB0, 0xC3, 0x61, 0x86, 0xC3, 0x0D, + 0x86, 0x1B, 0x0C, 0x37, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x78, 0x07, 0xE0, + 0x1F, 0x00, 0x18, 0x00, 0xC0, 0x07, 0xE0, 0x3F, 0xC1, 0x8F, 0x0C, 0x18, 0x60, 0xC3, 0x0E, 0x1F, + 0xE0, 0xFF, 0x00, 0x00, 0x16, 0x01, 0xB0, 0x0D, 0x80, 0x6C, 0x03, 0x7E, 0x1B, 0xFC, 0xD8, 0x76, + 0xC1, 0xB6, 0x0D, 0xB0, 0x6D, 0xFF, 0x6F, 0xF3, 0x00, 0x00, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xE3, + 0xFD, 0x87, 0xC1, 0xE0, 0xF0, 0x7F, 0xFF, 0xF0, 0x0E, 0x03, 0xF8, 0x7F, 0xC6, 0x0E, 0x60, 0x60, + 0x06, 0x0F, 0xF0, 0xFF, 0x00, 0x66, 0x06, 0x70, 0xE3, 0xFC, 0x1F, 0x80, 0x40, 0x00, 0xE1, 0x87, + 0xF3, 0x1F, 0xF6, 0x70, 0x7C, 0xC0, 0x79, 0x80, 0xFF, 0x01, 0xFE, 0x03, 0xCC, 0x07, 0x9C, 0x0F, + 0x18, 0x36, 0x3F, 0xEC, 0x3F, 0x80, 0x08, 0x00, 0x07, 0x8F, 0xF7, 0xFD, 0x87, 0x61, 0xD8, 0x77, + 0x9C, 0xFF, 0x1F, 0xCE, 0x77, 0x1D, 0xC7, 0xE1, 0xC0, 0x00, 0x3E, 0x7F, 0x63, 0x0F, 0x7F, 0x73, + 0xE3, 0x7F, 0x7F, 0x10, 0x01, 0x03, 0xCF, 0xCF, 0x06, 0xE3, 0xF9, 0xCF, 0xC3, 0xE1, 0xF0, 0xD8, + 0x6E, 0xF3, 0xF0, 0x60, 0xFE, 0xFE, 0xC6, 0xFE, 0xFE, 0xC7, 0xC3, 0xFF, 0xFE, 0xFF, 0xFC, 0x30, + 0xC3, 0x0C, 0x30, 0xC0, 0x3F, 0x8F, 0xE3, 0x18, 0xC6, 0x31, 0x8C, 0x67, 0x1B, 0xFF, 0xFF, 0xF0, + 0x3C, 0x0F, 0x03, 0x00, 0x1F, 0x9F, 0xCC, 0x37, 0xFF, 0xFF, 0x80, 0xC3, 0x7F, 0x9F, 0x83, 0x00, + 0xE3, 0x3B, 0x99, 0x8E, 0xD8, 0x3F, 0x80, 0xFC, 0x0F, 0xF0, 0xED, 0x8E, 0x66, 0xE3, 0x38, 0x00, + 0x7E, 0x7E, 0x66, 0x0E, 0x1E, 0x07, 0x43, 0x7F, 0x7E, 0x00, 0xC7, 0xC7, 0xCF, 0xDF, 0xDB, 0xF3, + 0xF3, 0xE3, 0xC3, 0x64, 0x7C, 0x3C, 0x00, 0xC7, 0xC7, 0xCF, 0xDF, 0xDB, 0xF3, 0xF3, 0xE3, 0xC3, + 0xC7, 0xCE, 0xDC, 0xF8, 0xF8, 0xF8, 0xDC, 0xCE, 0xC7, 0x3F, 0x9F, 0xCC, 0xE6, 0x73, 0x39, 0x9D, + 0xCF, 0xC7, 0xE3, 0x80, 0xE0, 0xFC, 0x3F, 0xC7, 0xF9, 0xFF, 0xB7, 0xB6, 0xF7, 0x9E, 0x73, 0xCE, + 0x60, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0x00, 0x0F, 0xC7, 0xF9, 0x86, 0x61, + 0xB8, 0x6E, 0x19, 0x86, 0x7F, 0x8F, 0xC0, 0xC0, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, + 0xC3, 0x00, 0x7F, 0x3F, 0xD8, 0x6C, 0x3E, 0x1F, 0x0F, 0x86, 0xFF, 0x7F, 0x32, 0x18, 0x0C, 0x06, + 0x00, 0x00, 0x1F, 0x9F, 0xEC, 0x36, 0x07, 0x03, 0x80, 0xC3, 0x7F, 0x9F, 0x83, 0x00, 0xFF, 0xFF, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xE1, 0xB1, 0xD8, 0xCE, 0x63, 0x61, 0xB0, 0xF8, 0x38, + 0x1C, 0x0E, 0x06, 0x0F, 0x06, 0x00, 0x04, 0x00, 0xC0, 0x18, 0x07, 0x03, 0xF8, 0xFF, 0x99, 0xBB, + 0x33, 0xE6, 0x7C, 0xCD, 0x9B, 0xBF, 0xE3, 0xF8, 0x1C, 0x01, 0x80, 0x30, 0x06, 0x00, 0x63, 0x3B, + 0x8D, 0x83, 0x81, 0xC1, 0xE0, 0xD8, 0xEE, 0xE3, 0x80, 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, + 0x0D, 0xFF, 0xFF, 0x80, 0xC0, 0x60, 0x30, 0xC7, 0xC7, 0xC7, 0xC7, 0xFF, 0x7F, 0x1F, 0x07, 0x07, + 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x3F, 0xFF, 0xFF, 0xF0, 0xC6, 0x36, + 0x31, 0xB1, 0x8D, 0x8C, 0x6C, 0x63, 0x63, 0x1B, 0x18, 0xDF, 0xFF, 0xFF, 0xF8, 0x00, 0xC0, 0x06, + 0x00, 0x30, 0xF8, 0x3E, 0x03, 0x80, 0xFE, 0x3F, 0xCE, 0x33, 0x8C, 0xFF, 0x3F, 0x80, 0xC0, 0xF0, + 0x3C, 0x0F, 0xF3, 0xFE, 0xF1, 0xFC, 0x7F, 0xFB, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xFC, 0xFE, 0xC7, + 0xC7, 0xFE, 0xFE, 0x00, 0x1F, 0x1F, 0xCC, 0x71, 0xF8, 0xFC, 0x7E, 0xC7, 0x77, 0x1F, 0x02, 0x00, + 0x00, 0x06, 0x3F, 0x33, 0xFD, 0x98, 0x6F, 0xC3, 0x7E, 0x1B, 0xF0, 0xD9, 0x86, 0xCF, 0xF6, 0x3F, + 0x00, 0x60, 0x7F, 0x7F, 0xE3, 0xE3, 0x7F, 0x3F, 0x73, 0x63, 0xE3, 0x18, 0x0C, 0x03, 0x00, 0x03, + 0xF3, 0xF9, 0x86, 0xFF, 0xFF, 0xF0, 0x18, 0x6F, 0xF3, 0xF0, 0x60, 0x32, 0x1B, 0x8C, 0x80, 0x03, + 0xF3, 0xF9, 0x86, 0xFF, 0xFF, 0xF0, 0x18, 0x6F, 0xF3, 0xF0, 0x60, 0x00, 0x30, 0x3F, 0x0C, 0x07, + 0xF3, 0xF9, 0x8E, 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x18, 0x0C, 0x06, 0x0F, 0x07, 0x00, 0x18, + 0x63, 0x00, 0xFF, 0xFC, 0x30, 0xC3, 0x0C, 0x30, 0xC0, 0x00, 0x1F, 0x9F, 0xEC, 0x37, 0xC7, 0xF3, + 0xF0, 0xC3, 0x7F, 0x9F, 0x83, 0x00, 0x00, 0x3E, 0x7F, 0x63, 0x78, 0x3E, 0x0F, 0x63, 0x77, 0x7E, + 0x18, 0x6D, 0x86, 0xDB, 0x6D, 0xB6, 0xCB, 0xFC, 0xC0, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, + 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x40, 0x3F, 0x80, 0x7F, 0x00, 0xC6, 0x01, 0x8F, + 0xE3, 0x1F, 0xC6, 0x31, 0xCC, 0x61, 0xF0, 0xFF, 0xE1, 0xFC, 0xC3, 0x03, 0x0C, 0x0C, 0x30, 0x3F, + 0xF8, 0xFF, 0xFB, 0x0C, 0xEC, 0x31, 0xB0, 0xFE, 0xC3, 0xF0, 0x00, 0x30, 0x3F, 0x1F, 0x87, 0xF3, + 0xF9, 0x8E, 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x18, 0x0C, 0x18, 0x18, 0x00, 0xC7, 0xCE, 0xDC, + 0xF8, 0xF8, 0xF8, 0xDC, 0xCE, 0xC7, 0x30, 0x30, 0x18, 0x00, 0xC7, 0xC7, 0xCF, 0xDF, 0xDB, 0xF3, + 0xF3, 0xE3, 0xC3, 0x32, 0x1F, 0x07, 0x00, 0x0E, 0x1B, 0x1D, 0x8C, 0xE6, 0x36, 0x1B, 0x0F, 0x83, + 0x81, 0xC0, 0xE0, 0x60, 0xF0, 0x60, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, + 0x18, 0x18, 0x18, 0x02, 0x16, 0x39, 0xF1, 0xCF, 0x8E, 0x7C, 0x73, 0xE3, 0x9F, 0x1C, 0xF8, 0xE7, + 0xC7, 0x3E, 0x39, 0xF1, 0xCF, 0xFF, 0xE7, 0xFF, 0x08, 0x00, 0xC6, 0x6C, 0x66, 0xC6, 0x6C, 0x66, + 0xC6, 0x6C, 0x66, 0xC6, 0x6F, 0xFE, 0x7B, 0xE0, 0x08, 0x00, 0x06, 0x03, 0xF8, 0x7F, 0x03, 0x00, + 0x7E, 0x0F, 0xF1, 0x8F, 0x30, 0x66, 0x0C, 0xC3, 0x9F, 0xE3, 0xF8, 0x00, 0x0C, 0x03, 0x00, 0xC0, + 0xFE, 0x3F, 0x03, 0x00, 0xFC, 0x3F, 0x8C, 0x73, 0x1C, 0xFE, 0x3F, 0x80, 0x00, 0xC3, 0x0F, 0xCC, + 0x7F, 0xB3, 0x87, 0xCC, 0x0F, 0x70, 0x0F, 0xFC, 0x3F, 0xF0, 0xDC, 0x03, 0x30, 0x3C, 0xE1, 0xF1, + 0xFE, 0xC3, 0xF0, 0x01, 0x00, 0x00, 0x0C, 0x7C, 0xCF, 0xED, 0xC6, 0xFF, 0x0F, 0xF8, 0xFF, 0x0D, + 0xC6, 0xCE, 0xEC, 0x7C, 0x01, 0x00, 0x04, 0x01, 0xC0, 0x3C, 0x0F, 0x81, 0xB0, 0x37, 0x0E, 0x61, + 0xFC, 0x3F, 0xCF, 0xF9, 0x9B, 0x33, 0x7E, 0x66, 0x1C, 0x07, 0x01, 0xE0, 0xD8, 0x37, 0x1F, 0xC7, + 0xF1, 0xBE, 0xED, 0x80, 0x00, 0x81, 0x83, 0x83, 0x07, 0x86, 0x0F, 0x0C, 0x36, 0x18, 0x6E, 0x31, + 0xCC, 0x7F, 0xFC, 0xFF, 0xF9, 0xFF, 0xF3, 0x33, 0x76, 0x66, 0x6D, 0xCC, 0xC0, 0xC3, 0x0C, 0x78, + 0xC7, 0x8C, 0x7C, 0xFE, 0xCF, 0xFC, 0xDF, 0xED, 0xB6, 0xDB, 0x70, 0x3F, 0xC7, 0xFE, 0x3F, 0xC3, + 0x9C, 0x19, 0x80, 0xF8, 0x3F, 0xC7, 0xFE, 0xE6, 0x7C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x30, 0x3F, + 0xC7, 0xF0, 0x6E, 0x0F, 0x83, 0xFC, 0xFF, 0xD9, 0x9F, 0x33, 0xE6, 0x60, 0x01, 0xFE, 0x61, 0xFF, + 0xB0, 0x7F, 0x98, 0x39, 0xCC, 0x0C, 0xC6, 0x07, 0xC3, 0xFF, 0xF9, 0xFF, 0xFE, 0xC7, 0x33, 0xE3, + 0x18, 0xF1, 0x8C, 0x78, 0xC6, 0x3C, 0x63, 0x18, 0xC7, 0xF9, 0x87, 0xF3, 0x0E, 0xC6, 0x0F, 0x8F, + 0xFF, 0x9F, 0xFF, 0xB3, 0x33, 0x66, 0x66, 0xCC, 0xCC, 0x33, 0x0F, 0x03, 0x06, 0x07, 0xE3, 0xF8, + 0x0C, 0x06, 0x07, 0x1F, 0x0F, 0xC0, 0x60, 0x30, 0x18, 0x7C, 0xFC, 0x78, 0x30, 0x1C, 0x06, 0x00, + 0x66, 0x3C, 0x18, 0x00, 0x7E, 0x7E, 0x07, 0x1E, 0x3E, 0x3E, 0x03, 0x3E, 0x7E, 0xE0, 0xE0, 0x70, + 0x30, 0x02, 0x16, 0x31, 0xF1, 0x8F, 0x8C, 0x7C, 0x63, 0x63, 0x1B, 0x98, 0xCE, 0xCE, 0x3F, 0xE0, + 0xFC, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x00, 0x04, 0x01, 0xC0, 0x38, 0x67, 0x3C, 0xE7, 0x9C, 0xF3, + 0x9E, 0x73, 0xCE, 0x7D, 0xDD, 0xFF, 0x1F, 0xC0, 0xE0, 0x1C, 0x03, 0x80, 0x60, 0x07, 0x01, 0xFC, + 0x3F, 0xE7, 0x06, 0x60, 0x76, 0x03, 0x7F, 0xF7, 0xFF, 0x60, 0x36, 0x07, 0x70, 0x63, 0xFE, 0x1F, + 0xC0, 0x20, 0x00, 0x0F, 0xC7, 0xF9, 0x86, 0x79, 0xBF, 0xEE, 0x79, 0x86, 0x7F, 0x8F, 0xC0, 0xC0, + 0x40, 0x2E, 0x07, 0x60, 0xE6, 0x0C, 0x70, 0xC3, 0x1C, 0x31, 0x83, 0x98, 0x1B, 0x81, 0xB0, 0x0F, + 0x00, 0xE0, 0x0E, 0x00, 0xE1, 0xD8, 0xE6, 0x31, 0xDC, 0x36, 0x0D, 0x83, 0xE0, 0x70, 0x1C, 0x00, + 0x36, 0x03, 0xF0, 0x1B, 0x04, 0x02, 0xE0, 0x76, 0x0E, 0x60, 0xC7, 0x0C, 0x31, 0xC3, 0x18, 0x39, + 0x81, 0xB8, 0x1B, 0x00, 0xF0, 0x0E, 0x00, 0xE0, 0x6C, 0x1B, 0x03, 0x60, 0x00, 0xE1, 0xD8, 0xE6, + 0x31, 0xDC, 0x36, 0x0D, 0x83, 0xE0, 0x70, 0x1C, 0x00, 0x07, 0x00, 0x00, 0x7F, 0x00, 0x03, 0xFE, + 0x00, 0x1C, 0x18, 0x00, 0x60, 0x76, 0x1D, 0x80, 0xDC, 0x66, 0x03, 0x31, 0x98, 0x0C, 0xCE, 0x60, + 0x33, 0xB1, 0x81, 0xC6, 0xC7, 0x06, 0x1E, 0x0F, 0xF8, 0x38, 0x1F, 0xC0, 0xE0, 0x08, 0x03, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0xE0, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0xE6, 0x19, 0xFE, 0xC3, 0x30, + 0xDC, 0xE6, 0x19, 0x99, 0xC3, 0x33, 0x38, 0x63, 0xE3, 0x0C, 0x78, 0x7F, 0x8F, 0x07, 0xE0, 0xC0, + 0x30, 0x18, 0x00, 0x07, 0x00, 0x03, 0xC0, 0x00, 0x70, 0x00, 0x07, 0x03, 0xF8, 0xF7, 0x38, 0x36, + 0x06, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x6E, 0x1C, 0xFF, 0x8F, 0xB0, 0x06, 0x00, 0x1F, 0x9F, 0xCC, + 0x36, 0x07, 0x01, 0x80, 0xC0, 0x78, 0x1E, 0x03, 0x01, 0x80, 0xC0, 0x01, 0x86, 0xE1, 0xF0, 0x3C, + 0x07, 0xD3, 0x37, 0xC0, 0xF0, 0x3E, 0x0D, 0x87, 0x00, 0x05, 0xFF, 0xF6, 0x00, 0x1D, 0xFF, 0x98, + 0x59, 0x80, 0xDB, 0x40, 0xF1, 0xFE, 0x38, 0x01, 0xC0, 0x01, 0xA0, 0x0E, 0x03, 0x87, 0x83, 0xC0, + 0x00, 0x00, 0x00, 0x01, 0xC0, 0x0D, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x03, 0x07, 0x83, + 0xC2, 0x81, 0x20, 0x0E, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0x19, 0x84, 0x18, + 0x1C, 0x08, 0x18, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x30, 0x03, 0x00, 0x00, 0x00, 0x00, 0x1C, + 0x18, 0x38, 0x8C, 0x01, 0x8C, 0x01, 0x80, 0x01, 0x80, 0x13, 0x01, 0xF0, 0x0E, 0x00, 0x04, 0xC1, + 0xCC, 0x1C, 0xC3, 0xCC, 0x7C, 0xC6, 0xCC, 0xEC, 0xCC, 0xCD, 0xCC, 0xF8, 0xCF, 0x0C, 0xF0, 0xFE, + 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x64, 0x1F, 0x03, 0xC0, 0x00, 0xC7, 0x31, 0xCC, 0xF3, 0x7C, + 0xDB, 0x3C, 0xCF, 0x33, 0x8E, 0xC3, 0x80, 0x60, 0x38, 0x0C, 0x00, 0x06, 0x03, 0xF8, 0x7F, 0x03, + 0x00, 0x7E, 0x0F, 0xF1, 0x8F, 0x30, 0x66, 0x0C, 0xC3, 0x9F, 0xE3, 0xF8, 0x20, 0x0C, 0x03, 0x03, + 0xF8, 0xFC, 0x0C, 0x03, 0xF0, 0xFE, 0x31, 0xCC, 0x73, 0xF8, 0xFE, 0x70, 0x7F, 0xBF, 0xF8, 0x3C, + 0x1E, 0x2F, 0x1F, 0xFE, 0xFF, 0x61, 0xB0, 0x18, 0x0C, 0x00, 0x00, 0x7F, 0x3F, 0xD8, 0x6C, 0x3E, + 0x1F, 0x0F, 0x9E, 0xFF, 0x7F, 0xB2, 0xD8, 0x0C, 0x06, 0x00, 0x03, 0x03, 0x7F, 0xFF, 0xFF, 0xC0, + 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x04, 0x30, 0xFF, 0xFF, 0x0C, 0x30, 0xC3, + 0x0C, 0x30, 0x1F, 0x8F, 0xF3, 0xFC, 0xC0, 0x30, 0x0C, 0x07, 0xF1, 0xFC, 0x30, 0x0C, 0x03, 0x00, + 0xC0, 0x30, 0x00, 0x7E, 0xFD, 0x83, 0x0F, 0xDF, 0x98, 0x30, 0x60, 0x7E, 0x7F, 0xBF, 0xD8, 0x0C, + 0x06, 0xF3, 0xFD, 0xE7, 0xE1, 0xE0, 0xF0, 0x78, 0x7D, 0xF0, 0xF0, 0xFC, 0xFC, 0xC0, 0xC0, 0xFC, + 0xFE, 0xC6, 0xC6, 0xC6, 0x06, 0x3E, 0x38, 0x40, 0x02, 0x30, 0xC3, 0x1C, 0x63, 0x87, 0x33, 0x81, + 0x99, 0x80, 0xED, 0xC0, 0x3F, 0xC0, 0x3F, 0xE0, 0x19, 0x98, 0x1C, 0xCE, 0x0C, 0x63, 0x0C, 0x31, + 0xEE, 0x18, 0x70, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x06, 0xE3, 0x39, 0xCC, 0xC3, 0xB6, 0x07, 0xF0, + 0x0F, 0xC0, 0x7F, 0x83, 0xB6, 0x1C, 0xCF, 0xE3, 0x3C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x0C, 0x0F, + 0xC7, 0x39, 0x86, 0x01, 0x80, 0xE0, 0xF0, 0x3E, 0x01, 0x90, 0x76, 0x1D, 0xFE, 0x3F, 0x03, 0x80, + 0xF0, 0x3C, 0x0E, 0x00, 0x00, 0x7E, 0x7E, 0x66, 0x0E, 0x1E, 0x07, 0x43, 0x7F, 0x7E, 0x18, 0x1C, + 0x1C, 0x18, 0x00, 0x98, 0x33, 0x0E, 0x63, 0x8C, 0xE1, 0x98, 0x3F, 0x07, 0xE0, 0xCE, 0x18, 0xC3, + 0x1C, 0x61, 0xEC, 0x1E, 0x00, 0xC0, 0x18, 0x03, 0xC7, 0x67, 0x37, 0x1F, 0x0F, 0x87, 0xC3, 0x71, + 0x9E, 0xC7, 0x81, 0xC0, 0xE0, 0x30, 0x00, 0xB0, 0x6C, 0x3B, 0x0C, 0xDF, 0x37, 0x8F, 0xE3, 0xF8, + 0xDE, 0x37, 0xCC, 0x3B, 0x06, 0xC1, 0xC0, 0xC3, 0x63, 0xBD, 0x9F, 0x8F, 0xC7, 0xF3, 0xD9, 0x8E, + 0xC3, 0x80, 0x78, 0x0B, 0xF0, 0xE7, 0xC3, 0x03, 0x1C, 0x0C, 0xE0, 0x37, 0x00, 0xF8, 0x03, 0xF0, + 0x0C, 0xC0, 0x31, 0x80, 0xC7, 0x03, 0x0E, 0x0C, 0x18, 0xF9, 0xDF, 0x30, 0xEE, 0x1F, 0x83, 0xE0, + 0x7E, 0x0E, 0xE1, 0xCC, 0x38, 0xC0, 0x00, 0x0C, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xFF, + 0xCF, 0xFC, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0E, 0xC0, 0xE0, 0x06, 0x00, 0x60, 0x06, 0xC3, 0x61, + 0xB0, 0xDF, 0xEF, 0xF6, 0x1B, 0x0D, 0x87, 0xC3, 0x80, 0xC0, 0x60, 0x30, 0x00, 0xFD, 0x81, 0xFF, + 0x03, 0xF6, 0x06, 0x0C, 0x0C, 0x18, 0x18, 0x3F, 0xF0, 0x7F, 0xE0, 0xC0, 0xC1, 0x81, 0x83, 0x03, + 0x06, 0x06, 0x0C, 0x0C, 0x00, 0xC3, 0xF0, 0xFC, 0x33, 0xFC, 0xFF, 0x30, 0xCC, 0x33, 0x0C, 0xC3, + 0x00, 0x7F, 0x80, 0x7F, 0xE0, 0x3F, 0xF0, 0x18, 0x18, 0x0C, 0x0C, 0x06, 0x06, 0xE3, 0x03, 0xFD, + 0x81, 0xCE, 0xC0, 0xC3, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x7C, 0x0D, 0xF0, 0x00, 0xF0, 0xFF, + 0x03, 0xFC, 0x0C, 0x30, 0x30, 0xF0, 0xC3, 0xF3, 0x0D, 0xEC, 0x31, 0xF0, 0xC3, 0xC3, 0x0C, 0x00, + 0xE0, 0x07, 0x80, 0x08, 0x04, 0x00, 0x1E, 0x00, 0x3C, 0xF8, 0x71, 0xFC, 0x61, 0x8E, 0x63, 0x86, + 0x63, 0x86, 0x63, 0x86, 0x61, 0x8E, 0x61, 0xCC, 0x70, 0xFC, 0x3F, 0xFF, 0x1F, 0xFF, 0x03, 0x04, + 0x00, 0x01, 0xC0, 0x1E, 0xF8, 0xCF, 0xC6, 0x63, 0x73, 0x1B, 0x98, 0xCC, 0xEC, 0x7F, 0xF1, 0xFF, + 0xC1, 0x88, 0x07, 0x01, 0xFC, 0x3F, 0xE7, 0x06, 0x60, 0x76, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, + 0x07, 0x70, 0x63, 0xFE, 0x1F, 0xC0, 0x70, 0x03, 0x80, 0x78, 0x07, 0x00, 0x00, 0x1F, 0x9F, 0xEC, + 0x36, 0x07, 0x03, 0x80, 0xC3, 0x7F, 0x9F, 0x83, 0x01, 0xC0, 0xE0, 0x60, 0x7F, 0xBF, 0xF7, 0xFC, + 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x0F, 0x01, 0xC0, 0x70, 0x0C, + 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1C, 0x1C, 0x0C, 0x0C, 0x0C, 0x40, 0x5C, 0x1D, 0xC3, + 0x18, 0xE3, 0xB8, 0x36, 0x07, 0xC0, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0xE1, 0xB1, + 0xD8, 0xCE, 0x63, 0x61, 0xB0, 0xF8, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x80, 0xC0, 0x40, 0x5C, 0x1D, + 0xC3, 0x18, 0xE3, 0xB8, 0x36, 0x07, 0xC0, 0x70, 0x0E, 0x07, 0xF0, 0xFE, 0x07, 0x00, 0xE0, 0xE1, + 0xB1, 0xD8, 0xCE, 0x63, 0x61, 0xB0, 0xF8, 0x38, 0x1C, 0x1F, 0x8F, 0x83, 0x80, 0xC0, 0x40, 0x46, + 0x0E, 0x71, 0xC3, 0x98, 0x1B, 0x81, 0xF0, 0x0E, 0x00, 0xE0, 0x1F, 0x03, 0xB8, 0x31, 0xC7, 0x0F, + 0xE0, 0xF0, 0x03, 0x00, 0x30, 0x03, 0x63, 0x1D, 0xC3, 0x60, 0x70, 0x1C, 0x0F, 0x03, 0x61, 0xDE, + 0xE3, 0xC0, 0x70, 0x1C, 0x03, 0x7F, 0x81, 0xFF, 0x99, 0xFF, 0x30, 0x60, 0x60, 0xC0, 0xC1, 0x81, + 0x83, 0x03, 0x06, 0x06, 0x0C, 0x0C, 0x18, 0x18, 0x30, 0x30, 0x7F, 0xF0, 0xFF, 0xE0, 0x00, 0xC0, + 0x01, 0x80, 0x03, 0xFC, 0xDF, 0x98, 0xC3, 0x18, 0x63, 0x0C, 0x61, 0x8C, 0x31, 0xFF, 0x3F, 0xE0, + 0x0C, 0x01, 0x80, 0x30, 0x00, 0x98, 0x3B, 0x07, 0x60, 0xEC, 0x1D, 0x83, 0xB8, 0xF3, 0xFE, 0x3D, + 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1E, 0x01, 0xC0, 0x38, 0x03, 0xC7, 0x63, 0xB1, 0xD8, 0xEF, 0xF3, + 0xF8, 0x7C, 0x0F, 0x07, 0x80, 0xC0, 0x60, 0x30, 0x00, 0xB0, 0x7C, 0x1F, 0x07, 0xC1, 0xF3, 0x7E, + 0xFD, 0xFF, 0x3D, 0xC3, 0x70, 0xDC, 0x07, 0x01, 0xC0, 0xC7, 0xC7, 0xC7, 0xDF, 0xFF, 0x7F, 0x1F, + 0x1F, 0x1F, 0x00, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x37, 0xCF, 0xFB, 0x86, 0xC1, 0xF0, 0x7C, 0x1F, + 0x07, 0xC1, 0xC0, 0x00, 0xC0, 0xC0, 0xC0, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, + 0x01, 0x80, 0x7F, 0x07, 0xFC, 0x38, 0x7F, 0x81, 0xFC, 0x0F, 0xFF, 0xEF, 0xFF, 0x38, 0x00, 0xC0, + 0xC7, 0x0E, 0x1F, 0xE0, 0x7E, 0x00, 0xC0, 0x00, 0x00, 0xFC, 0x1F, 0xED, 0x86, 0xFF, 0xE7, 0xFE, + 0x18, 0x01, 0x86, 0x1F, 0xE0, 0xFC, 0x03, 0x00, 0x01, 0x80, 0x7F, 0x07, 0xFC, 0x38, 0x7F, 0x81, + 0xFC, 0x0F, 0xFF, 0xEF, 0xFF, 0x38, 0x00, 0xC0, 0xC7, 0x0E, 0x1F, 0xE0, 0x7E, 0x00, 0xC0, 0x06, + 0x00, 0x30, 0x00, 0x00, 0xFC, 0x1F, 0xED, 0x86, 0xFF, 0xE7, 0xFE, 0x18, 0x01, 0x86, 0x1F, 0xE0, + 0xFC, 0x03, 0x00, 0x38, 0x03, 0x80, 0x30, 0x3F, 0xFF, 0xFF, 0xC0, 0x06, 0x60, 0x07, 0xC0, 0x03, + 0xC0, 0x40, 0x02, 0x61, 0x86, 0x71, 0x8E, 0x39, 0x9C, 0x19, 0x98, 0x1D, 0xB8, 0x0F, 0xF0, 0x1F, + 0xF0, 0x19, 0x98, 0x39, 0x9C, 0x31, 0x8C, 0x61, 0x8E, 0xE1, 0x87, 0x0C, 0x80, 0x7C, 0x01, 0xC0, + 0x00, 0x0E, 0x33, 0xB9, 0x98, 0xED, 0x83, 0xF8, 0x0F, 0xC0, 0xFF, 0x0E, 0xD8, 0xE6, 0x6E, 0x33, + 0x80, 0x00, 0xB0, 0x6C, 0x33, 0x1C, 0xCE, 0x37, 0x0F, 0xE3, 0xFC, 0xC3, 0xB0, 0x6C, 0x1F, 0x07, + 0xC1, 0x80, 0xE0, 0x70, 0x18, 0xC7, 0xCE, 0xDC, 0xF8, 0xFE, 0xFF, 0xC3, 0xC3, 0xC3, 0x0F, 0x0E, + 0x00, 0x0F, 0xC0, 0xFF, 0x07, 0xF8, 0x30, 0xC1, 0x86, 0x0C, 0x30, 0x61, 0x83, 0x0C, 0x38, 0x61, + 0xC3, 0x0C, 0x18, 0xE0, 0xFE, 0x07, 0x80, 0x18, 0x00, 0xC0, 0x06, 0x3F, 0x8F, 0xE3, 0x38, 0xCE, + 0x33, 0x8C, 0xE7, 0x3B, 0x8F, 0xE3, 0xC0, 0x70, 0x18, 0x06, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, + 0xF0, 0x3F, 0xFF, 0xFF, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x0F, 0x03, 0x80, + 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0x03, 0x03, 0x0F, 0x0E, 0x00, 0x0C, 0x0C, + 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xFF, 0xCF, 0xFC, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0F, 0xC0, + 0xF0, 0x06, 0x00, 0x60, 0x06, 0xC3, 0x30, 0xCC, 0x33, 0xFC, 0xFF, 0x30, 0xCC, 0x33, 0x0E, 0xC3, + 0x80, 0x60, 0x38, 0x0C, 0x00, 0x98, 0x3B, 0x07, 0x60, 0xEC, 0x1D, 0x83, 0xB8, 0xF3, 0xFE, 0x3D, + 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1E, 0x01, 0xC0, 0x38, 0x03, 0xC7, 0x63, 0xB1, 0xD8, 0xEF, 0xF3, + 0xF8, 0x7C, 0x0F, 0x07, 0x80, 0xC0, 0x60, 0x30, 0x40, 0x11, 0xC0, 0x73, 0xC1, 0xE7, 0x83, 0xCF, + 0x07, 0x9F, 0x1B, 0x36, 0x36, 0x6E, 0xEC, 0xCD, 0x99, 0x9B, 0x33, 0x3C, 0x66, 0x38, 0xEC, 0x71, + 0xC0, 0x01, 0x80, 0x07, 0x00, 0x0C, 0xE0, 0xE7, 0x0F, 0x3C, 0x79, 0xE7, 0xCF, 0xB6, 0x6D, 0xB3, + 0x79, 0x99, 0xCE, 0xCE, 0x70, 0x01, 0x80, 0x1C, 0x00, 0xC0, 0x3F, 0xFF, 0xFF, 0xC0, 0x19, 0x03, + 0xE0, 0x38, 0x02, 0x00, 0xE0, 0x1E, 0x07, 0xC0, 0xD8, 0x1B, 0x87, 0x30, 0xC6, 0x1F, 0xE7, 0xFC, + 0xC1, 0x98, 0x3F, 0x03, 0x36, 0x3E, 0x1C, 0x00, 0x3E, 0x7F, 0x63, 0x0F, 0x7F, 0x73, 0xE3, 0x7F, + 0x7F, 0x10, 0x19, 0x03, 0x70, 0x64, 0x02, 0x00, 0xE0, 0x1E, 0x07, 0xC0, 0xD8, 0x1B, 0x87, 0x30, + 0xC6, 0x1F, 0xE7, 0xFC, 0xC1, 0x98, 0x3F, 0x03, 0x36, 0x76, 0x36, 0x00, 0x3E, 0x7F, 0x63, 0x0F, + 0x7F, 0x73, 0xE3, 0x7F, 0x7F, 0x10, 0x01, 0xFE, 0x07, 0xFF, 0x06, 0xFE, 0x0E, 0xC0, 0x0C, 0xC0, + 0x0C, 0xC0, 0x18, 0xFE, 0x18, 0xFE, 0x3F, 0xC0, 0x3F, 0xC0, 0x70, 0xC0, 0x60, 0xFF, 0xE0, 0xFF, + 0x00, 0x00, 0x7D, 0xF1, 0xFF, 0xF3, 0x1C, 0x61, 0xFF, 0xEF, 0xFF, 0xDC, 0xC0, 0x71, 0xC6, 0x7F, + 0xDC, 0xFD, 0xF8, 0x60, 0x80, 0x26, 0x3E, 0x1C, 0x7E, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, + 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, 0x32, 0x1F, 0x07, 0x80, 0x03, 0xF3, 0xF9, 0x86, 0xFF, 0xFF, 0xF0, + 0x18, 0x6F, 0xF3, 0xF0, 0x60, 0x06, 0x01, 0xF8, 0x3F, 0xC7, 0x0E, 0x60, 0x60, 0x07, 0x7F, 0xF7, + 0xFF, 0x60, 0x76, 0x06, 0x70, 0xE3, 0xFC, 0x1F, 0x80, 0x60, 0x08, 0x1F, 0x9F, 0xCC, 0x30, 0x1F, + 0xFF, 0xFE, 0xC3, 0x7F, 0x1F, 0x00, 0x00, 0x09, 0x81, 0xD8, 0x09, 0x80, 0x60, 0x1F, 0x83, 0xFC, + 0x70, 0xE6, 0x06, 0x00, 0x77, 0xFF, 0x7F, 0xF6, 0x07, 0x60, 0x67, 0x0E, 0x3F, 0xC1, 0xF8, 0x06, + 0x00, 0x32, 0x1B, 0x8C, 0x81, 0x03, 0xF3, 0xF9, 0x86, 0x03, 0xFF, 0xFF, 0xD8, 0x6F, 0xE3, 0xE0, + 0x00, 0x06, 0x40, 0x06, 0xE0, 0x06, 0x40, 0x40, 0x02, 0x61, 0x86, 0x71, 0x8E, 0x39, 0x9C, 0x19, + 0x98, 0x1D, 0xB8, 0x0F, 0xF0, 0x1F, 0xF0, 0x19, 0x98, 0x39, 0x9C, 0x31, 0x8C, 0x61, 0x8E, 0xE1, + 0x87, 0x0C, 0x80, 0x6E, 0x03, 0x20, 0x00, 0x0E, 0x33, 0xB9, 0x98, 0xED, 0x83, 0xF8, 0x0F, 0xC0, + 0xFF, 0x0E, 0xD8, 0xE6, 0x6E, 0x33, 0x80, 0x13, 0x0E, 0xC1, 0x30, 0x30, 0x3F, 0x1C, 0xE6, 0x18, + 0x06, 0x03, 0x83, 0xC0, 0xF8, 0x06, 0x41, 0xD8, 0x77, 0xF8, 0xFC, 0x0C, 0x00, 0x26, 0x7E, 0x26, + 0x00, 0x7E, 0x7E, 0x66, 0x0E, 0x1E, 0x07, 0x43, 0x7F, 0x7E, 0x00, 0x3F, 0x1F, 0xE3, 0xF8, 0x1C, + 0x0E, 0x07, 0x01, 0xF0, 0x1E, 0x01, 0xD0, 0x76, 0x1D, 0xFE, 0x3F, 0x03, 0x00, 0x3F, 0x1F, 0xE3, + 0xF8, 0x1C, 0x0E, 0x07, 0x01, 0xF0, 0x1E, 0x01, 0xD0, 0x76, 0x1D, 0xFE, 0x3F, 0x03, 0x00, 0x1F, + 0x07, 0x80, 0x07, 0x07, 0xC1, 0xF0, 0xFC, 0x7F, 0x1B, 0xCE, 0xF3, 0x3D, 0xCF, 0xE3, 0xF0, 0xFC, + 0x3E, 0x0C, 0x7E, 0x3C, 0x00, 0xC7, 0xC7, 0xCF, 0xDF, 0xDB, 0xF3, 0xF3, 0xE3, 0xC3, 0x13, 0x0E, + 0xC1, 0x30, 0x01, 0xC1, 0xF0, 0x7C, 0x3F, 0x1F, 0xC6, 0xF3, 0xBC, 0xCF, 0x73, 0xF8, 0xFC, 0x3F, + 0x0F, 0x83, 0x64, 0x6E, 0x64, 0x00, 0xC7, 0xC7, 0xCF, 0xDF, 0xDB, 0xF3, 0xF3, 0xE3, 0xC3, 0x09, + 0x81, 0xD8, 0x09, 0x80, 0x70, 0x1F, 0xC3, 0xFE, 0x70, 0x66, 0x07, 0x60, 0x36, 0x03, 0x60, 0x36, + 0x03, 0x60, 0x77, 0x06, 0x3F, 0xE1, 0xFC, 0x02, 0x00, 0x32, 0x0D, 0xC3, 0x20, 0x00, 0x3F, 0x1F, + 0xE6, 0x19, 0x86, 0xE1, 0xB8, 0x66, 0x19, 0xFE, 0x3F, 0x03, 0x00, 0x07, 0x01, 0xFC, 0x3F, 0xE7, + 0x06, 0x60, 0x76, 0x03, 0x7F, 0xF7, 0xFF, 0x60, 0x36, 0x07, 0x70, 0x63, 0xFE, 0x1F, 0xC0, 0x20, + 0x00, 0x0F, 0xC7, 0xF9, 0x86, 0x79, 0xBF, 0xEE, 0x79, 0x86, 0x7F, 0x8F, 0xC0, 0xC0, 0x09, 0x81, + 0xD8, 0x09, 0x80, 0x70, 0x1F, 0xC3, 0xFE, 0x70, 0x66, 0x07, 0x60, 0x37, 0xFF, 0x7F, 0xF6, 0x03, + 0x60, 0x77, 0x06, 0x3F, 0xE1, 0xFC, 0x02, 0x00, 0x32, 0x0D, 0xC3, 0x20, 0x00, 0x3F, 0x1F, 0xE6, + 0x19, 0xE6, 0xFF, 0xB9, 0xE6, 0x19, 0xFE, 0x3F, 0x03, 0x00, 0x19, 0x01, 0xB8, 0x19, 0x00, 0xE0, + 0x3F, 0x87, 0xFC, 0x60, 0xE6, 0x06, 0x00, 0x60, 0xFF, 0x0F, 0xF0, 0x06, 0x60, 0x67, 0x0E, 0x3F, + 0xC1, 0xF8, 0x04, 0x00, 0x36, 0x3B, 0x0D, 0x80, 0x03, 0xE3, 0xF9, 0x8E, 0x3F, 0x1F, 0x8F, 0xD8, + 0xEE, 0xE3, 0xE0, 0x40, 0x1F, 0x07, 0x84, 0x03, 0x83, 0x61, 0xDC, 0x63, 0x38, 0xCC, 0x1B, 0x07, + 0xC1, 0xE0, 0x38, 0x0C, 0x0F, 0x03, 0x80, 0x3E, 0x0F, 0x00, 0x1C, 0x36, 0x3B, 0x19, 0xCC, 0x6C, + 0x36, 0x1F, 0x07, 0x03, 0x81, 0xC0, 0xC1, 0xE0, 0xC0, 0x13, 0x0E, 0xC1, 0x31, 0x00, 0xE0, 0xD8, + 0x77, 0x18, 0xCE, 0x33, 0x06, 0xC1, 0xF0, 0x78, 0x0E, 0x03, 0x03, 0xC0, 0xE0, 0x32, 0x1B, 0x8D, + 0x80, 0x0E, 0x1B, 0x1D, 0x8C, 0xE6, 0x36, 0x1B, 0x0F, 0x83, 0x81, 0xC0, 0xE0, 0x60, 0xF0, 0x60, + 0x00, 0x1B, 0x06, 0xC3, 0x61, 0x00, 0xE0, 0xD8, 0x77, 0x18, 0xCE, 0x33, 0x06, 0xC1, 0xF0, 0x78, + 0x0E, 0x03, 0x03, 0xC0, 0xE0, 0x1B, 0x1B, 0x0D, 0x80, 0x0E, 0x1B, 0x1D, 0x8C, 0xE6, 0x36, 0x1B, + 0x0F, 0x83, 0x81, 0xC0, 0xE0, 0x60, 0xF0, 0x60, 0x00, 0x32, 0x0D, 0xC3, 0x20, 0x02, 0xC1, 0xF0, + 0x7C, 0x1F, 0x07, 0xC1, 0xF8, 0xF7, 0xFC, 0xF7, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x64, 0x6E, 0x64, + 0x00, 0xC7, 0xC7, 0xC7, 0xC7, 0xFF, 0x7F, 0x1F, 0x07, 0x07, 0x7E, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, + 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xE0, 0xE0, 0x60, 0x60, 0x60, 0xFF, 0xFC, 0x30, 0xC3, 0x0C, 0x38, + 0xE1, 0x86, 0x18, 0x09, 0x80, 0xFC, 0x06, 0x60, 0x00, 0x2C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x06, + 0xFC, 0x37, 0xF9, 0xB0, 0xED, 0x83, 0x6C, 0x1B, 0x60, 0xDB, 0xFE, 0xDF, 0xE6, 0x13, 0x0F, 0xC3, + 0x30, 0x00, 0xC0, 0xF0, 0x3C, 0x0F, 0xF3, 0xFE, 0xF1, 0xFC, 0x7F, 0xFB, 0xFE, 0xC0, 0x3F, 0x9F, + 0xE7, 0xF9, 0xC0, 0x70, 0x1C, 0x0F, 0xE3, 0xF8, 0x70, 0x1C, 0x07, 0x01, 0xE0, 0x78, 0x06, 0x01, + 0x81, 0xC0, 0x60, 0x00, 0x7E, 0xFD, 0x83, 0x0F, 0xDF, 0x98, 0x38, 0x70, 0x60, 0xC7, 0x8E, 0x00, + 0x40, 0x46, 0x0E, 0x71, 0xC3, 0x98, 0x1B, 0x81, 0xF0, 0x0E, 0x00, 0xE0, 0x1F, 0x03, 0xB8, 0x31, + 0xC7, 0x0F, 0xE0, 0xF0, 0x03, 0x00, 0x30, 0x0F, 0x00, 0xE0, 0xE3, 0x33, 0x8D, 0x87, 0x81, 0xC1, + 0xE0, 0xF8, 0xCC, 0xE3, 0x01, 0x80, 0xC1, 0xE0, 0xE0, 0x40, 0x4C, 0x1D, 0xC7, 0x1C, 0xC1, 0xB8, + 0x3E, 0x1F, 0xF3, 0xFE, 0x1F, 0x07, 0x70, 0xC7, 0x38, 0x6E, 0x0E, 0x63, 0x3B, 0x8D, 0x8F, 0xEF, + 0xF1, 0xE0, 0xD8, 0xEE, 0xE3, 0x80, 0xE3, 0xB1, 0xDC, 0xE7, 0x77, 0xF7, 0xFB, 0x39, 0x8C, 0xC7, + 0x61, 0xC0, 0xFE, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x7F, 0xC0, 0x78, 0x61, + 0xC3, 0x0C, 0x30, 0xC7, 0x3F, 0xB0, 0xFF, 0x83, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, + 0x03, 0x00, 0xFF, 0x01, 0x80, 0x60, 0x30, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xC0, 0xFF, 0xFF, + 0xF0, 0xFC, 0x43, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xE0, 0xFF, 0x61, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, + 0x07, 0x83, 0xC1, 0xE0, 0xC0, 0xCF, 0x61, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0x77, 0x3F, + 0x80, 0xFF, 0xC0, 0xFC, 0x0E, 0x07, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0xFE, 0x07, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x0F, 0x7F, 0x00, 0xC0, 0xC0, 0xC0, 0xFF, + 0x03, 0x07, 0x06, 0x0E, 0x0C, 0x0C, 0x1C, 0x18, 0x18, 0xFF, 0x61, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, + 0x07, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x8E, 0x73, 0x0C, 0xC3, 0x30, 0xDC, 0x36, 0x0D, 0x83, 0x60, + 0xD9, 0xF0, 0xFF, 0xFF, 0xFF, 0xC0, 0xF1, 0xC6, 0x31, 0x8C, 0x63, 0x1F, 0xC0, 0x7F, 0x86, 0x79, + 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x3B, 0x83, 0xE0, 0xE1, 0xB0, 0xD8, 0x66, 0x33, + 0x19, 0xCC, 0x66, 0x36, 0x7E, 0x7C, 0x00, 0xFE, 0x67, 0xB0, 0xD8, 0x7C, 0x1F, 0x8C, 0x06, 0x03, + 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0xFE, 0x63, 0xB0, 0xF8, 0x3C, 0x1F, 0xCC, 0x06, 0x07, 0x07, + 0x7F, 0x00, 0xC3, 0xE3, 0x63, 0x63, 0x73, 0x37, 0x3E, 0x38, 0x18, 0x18, 0x1C, 0x0C, 0x0C, 0xE3, + 0x39, 0x8C, 0xC7, 0x61, 0xF0, 0x70, 0x38, 0x0C, 0x03, 0x3F, 0x80, 0xFF, 0x80, 0xC0, 0x60, 0x60, + 0x36, 0x3B, 0x19, 0x8C, 0xCE, 0x66, 0x30, 0x18, 0x0C, 0x00, 0xFE, 0x06, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0xCC, 0x79, 0x8F, 0x31, 0xE6, 0x3C, 0xC7, 0xF8, 0xF0, 0x1F, 0x06, 0x71, + 0xC7, 0xF0, 0xFF, 0x19, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xC3, 0x61, 0xF0, 0xC0, 0x6F, 0xF0, + 0x2D, 0xFC, 0x07, 0xE8, 0x00, 0xFF, 0xFE, 0x0C, 0x1C, 0x1C, 0x1C, 0x18, 0x30, 0x00, 0xC1, 0x80, + 0x00, 0x38, 0xF1, 0x83, 0x07, 0xDF, 0xBC, 0x00, 0xFB, 0xF0, 0x00, 0x0E, 0xE4, 0x4C, 0xE6, 0x66, + 0x66, 0x66, 0x67, 0x00, 0x42, 0xC6, 0xFE, 0x7E, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x70, 0x70, + 0x00, 0x40, 0xF6, 0xDB, 0x6F, 0xF7, 0xFB, 0x01, 0x80, 0xC0, 0x70, 0x18, 0x0C, 0x06, 0x00, 0x00, + 0x08, 0x38, 0x70, 0xE0, 0xC0, 0xF8, 0x78, 0x70, 0xE0, 0xC0, 0xFE, 0x7E, 0x00, 0x38, 0x7C, 0x66, + 0xC6, 0xC6, 0xC6, 0xC6, 0xFE, 0x7C, 0x00, 0x80, 0xFE, 0x7E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x03, 0x00, 0x40, 0xB0, 0x66, 0x19, 0x8E, 0x73, 0x0C, 0xC3, 0x60, 0xF8, 0x1E, 0x07, + 0x01, 0xC0, 0x30, 0x00, 0x00, 0x0C, 0x03, 0x01, 0xC0, 0x78, 0x1E, 0x0F, 0x83, 0x70, 0xCC, 0x63, + 0x18, 0xEE, 0x1B, 0x06, 0x00, 0x00, 0x30, 0x7C, 0xEC, 0xCC, 0xCE, 0xFE, 0x7E, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x00, 0x00, 0x00, 0x18, 0x03, 0xB0, 0x3C, 0x07, 0x80, 0xF0, 0x1F, 0x06, 0x7F, 0xC7, + 0xE0, 0x7F, 0xC0, 0x0E, 0xE4, 0x4C, 0xE6, 0x66, 0x66, 0x66, 0x67, 0x00, 0x42, 0xC6, 0xFE, 0x7E, + 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x70, 0x70, 0x00, 0x40, 0xF6, 0xDB, 0x6F, 0xF7, 0xFB, 0x01, + 0x80, 0xC0, 0x70, 0x18, 0x0C, 0x06, 0x00, 0x00, 0x4C, 0xDE, 0xD8, 0x7E, 0x7E, 0x60, 0x60, 0x60, + 0x60, 0x60, 0x60, 0x60, 0x00, 0x38, 0x18, 0x3C, 0x7E, 0x66, 0xC6, 0xC6, 0xC3, 0xD3, 0xFE, 0x7E, + 0x1C, 0x79, 0xC3, 0x06, 0x07, 0xC7, 0x8C, 0x30, 0xE1, 0x83, 0x00, 0x00, 0x40, 0xB0, 0x66, 0x19, + 0x8E, 0x73, 0x0C, 0xC3, 0x60, 0xF8, 0x1E, 0x07, 0x01, 0xC0, 0x30, 0x00, 0x00, 0x0C, 0x03, 0x01, + 0xC0, 0x78, 0x1E, 0x0F, 0x83, 0x70, 0xCC, 0x63, 0x18, 0xEE, 0x1B, 0x06, 0x00, 0x00, 0x30, 0x7C, + 0xEC, 0xCC, 0xCE, 0xFE, 0x7E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x00, 0x04, 0x01, 0xC0, 0x3C, 0x0F, 0x81, 0xB0, 0x37, 0x0E, 0x61, 0x8C, 0x3F, 0xCF, 0xF9, 0x83, 0x30, 0x7E, 0x06, 0x00, 0x01, 0x80, 0x70, 0x06, 0x00, 0x00, 0x3E, 0x7F, 0x63, 0x0F, 0x7F, 0x73, 0xE3, 0x7F, 0x7F, 0x10, 0x08, 0x1C, 0x08, 0x0E, 0x01, 0xC0, 0x18, 0x02, 0x00, 0xE0, 0x1E, 0x07, 0xC0, 0xD8, 0x1B, 0x87, 0x30, 0xC6, @@ -1022,7 +1040,190 @@ static const uint8_t inter_8_regularBitmaps[16223] = { 0x0E, 0x00, 0xF0, 0x38, 0x01, 0xF0, 0xE0, 0x03, 0x61, 0x80, 0x0C, 0xE0, 0x00, 0x38, 0xE0, 0x00, 0x60, 0xE0, 0x01, 0xC0, 0xE0, 0x0F, 0x00, 0xFC, 0xF8, 0x00, 0xFF, 0xE0, 0x00, 0x3E, 0x00, 0x3F, 0x99, 0xCC, 0x06, 0x0F, 0xF9, 0x9C, 0xCE, 0x67, 0x33, 0x99, 0xCC, 0xE6, 0x73, 0x38, 0x3D, 0x8C, - 0x63, 0x18, 0xC6, 0xFD, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xC0, + 0x63, 0x18, 0xC6, 0xFD, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xC0, 0x00, + 0x06, 0x40, 0x07, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xF8, 0x3F, 0x7F, 0xFE, 0x0F, 0xE0, 0x03, + 0xC0, 0x03, 0xC0, 0x01, 0x80, 0x01, 0x80, 0x40, 0x00, 0x0C, 0x00, 0x00, 0xC0, 0x03, 0x0C, 0x00, + 0x78, 0xF8, 0x7F, 0xE7, 0xFF, 0x9E, 0x0F, 0xC0, 0x00, 0x2C, 0x00, 0x07, 0xC0, 0x00, 0x18, 0x00, + 0x01, 0x80, 0x00, 0x18, 0xC6, 0x31, 0xBD, 0xC0, 0x7B, 0xCC, 0x60, 0x10, 0x30, 0x30, 0xFE, 0xFE, + 0x00, 0x78, 0x78, 0x30, 0x30, 0x01, 0x00, 0x01, 0xC0, 0x01, 0xE0, 0x03, 0xE0, 0x03, 0xC0, 0x00, + 0x06, 0x40, 0x07, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xF8, 0x3F, 0x7F, 0xFE, 0x0F, 0xE0, 0x01, + 0x00, 0x00, 0x1C, 0x00, 0x01, 0xE0, 0x00, 0x3E, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x40, 0x00, + 0x0C, 0x00, 0x00, 0xC0, 0x03, 0x0C, 0x00, 0x78, 0xF8, 0x7F, 0xE7, 0xFF, 0x9E, 0x0F, 0xC0, 0x00, + 0x43, 0x9F, 0xFF, 0x00, 0x0C, 0x71, 0x8C, 0x6F, 0x70, 0x00, 0x20, 0x38, 0x3C, 0x7C, 0x78, 0x00, + 0x00, 0x10, 0x30, 0x30, 0xFE, 0xFE, 0x00, 0xFC, 0x1F, 0xF8, 0x1F, 0x0F, 0x83, 0x80, 0xE0, 0x3B, + 0xC6, 0x78, 0xC6, 0x18, 0xC3, 0x80, 0x38, 0x23, 0xFE, 0x3F, 0x00, 0xFC, 0x07, 0xFE, 0x01, 0xF0, + 0x3F, 0x03, 0x8C, 0x38, 0x7F, 0xBD, 0xF9, 0xE0, 0xC6, 0x06, 0x30, 0x38, 0x00, 0xE0, 0x83, 0xFE, + 0x0F, 0xC0, 0x7C, 0x1F, 0xC0, 0x78, 0x3E, 0xFE, 0x3E, 0x00, 0x00, 0x58, 0x3E, 0x03, 0x00, 0xC0, + 0x7C, 0x07, 0xF0, 0x07, 0x80, 0xF8, 0xFF, 0xEF, 0x8F, 0x00, 0x03, 0x40, 0x3E, 0x01, 0x80, 0x18, + 0x00, 0x30, 0x38, 0x3C, 0x7C, 0x7C, 0x00, 0x08, 0x0C, 0x0E, 0x06, 0x06, 0x07, 0x7E, 0xFE, 0x00, + 0x30, 0x0F, 0x03, 0xC1, 0xF0, 0x7C, 0x00, 0x00, 0x80, 0x30, 0x0E, 0x01, 0x80, 0x60, 0x1C, 0x7F, + 0xFF, 0xF0, 0x00, 0x18, 0x30, 0xF1, 0xE0, 0x01, 0x03, 0x06, 0x0C, 0x18, 0x30, 0xE3, 0x9E, 0x18, + 0x00, 0x18, 0x0C, 0x1F, 0x05, 0x80, 0x00, 0x40, 0x30, 0x18, 0x0E, 0x07, 0x83, 0xE3, 0x83, 0x87, + 0x81, 0x80, 0x30, 0x78, 0xF9, 0xF3, 0xC0, 0x00, 0x04, 0x0C, 0x18, 0x30, 0x60, 0xC3, 0x8E, 0x78, + 0x60, 0x30, 0x1E, 0x0F, 0x07, 0x87, 0xC0, 0x00, 0x00, 0x10, 0x0C, 0x06, 0x03, 0x81, 0xE0, 0xF8, + 0xE0, 0xE1, 0xE0, 0x60, 0x00, 0x00, 0x06, 0x00, 0x3C, 0x01, 0xE0, 0x07, 0x00, 0x0C, 0x00, 0x1E, + 0x10, 0x0E, 0x60, 0x0C, 0xC0, 0x0D, 0x80, 0x1B, 0xE0, 0xF3, 0xFF, 0xC0, 0xFC, 0x00, 0x00, 0x06, + 0x00, 0x07, 0x80, 0x07, 0xC0, 0x03, 0xC0, 0x00, 0xC0, 0x00, 0x38, 0x04, 0x03, 0x03, 0x00, 0x60, + 0xC0, 0x1C, 0x30, 0x03, 0x8F, 0x87, 0xF9, 0xFF, 0xEE, 0x0F, 0xC0, 0x00, 0x06, 0x1E, 0x7C, 0xF0, + 0xE0, 0x70, 0x38, 0x1C, 0x0C, 0x0C, 0xFC, 0xF8, 0x00, 0x06, 0x07, 0x87, 0xC3, 0xC0, 0xE0, 0x1C, + 0x03, 0x80, 0x60, 0x0C, 0x03, 0x8F, 0xFB, 0xEF, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x78, 0x01, 0xD8, + 0x07, 0xF0, 0x0F, 0x80, 0x1C, 0x00, 0x30, 0x00, 0x78, 0x40, 0x39, 0x80, 0x33, 0x00, 0x36, 0x00, + 0x6F, 0x83, 0xCF, 0xFF, 0x03, 0xF0, 0x00, 0x00, 0x0C, 0x00, 0x0F, 0x00, 0x07, 0x60, 0x03, 0xF8, + 0x00, 0xFC, 0x00, 0x3C, 0x00, 0x0C, 0x00, 0x03, 0x80, 0x40, 0x30, 0x30, 0x06, 0x0C, 0x01, 0xC3, + 0x00, 0x38, 0xF8, 0x7F, 0x9F, 0xFE, 0xE0, 0xFC, 0x00, 0x0C, 0x1C, 0x7E, 0xFE, 0xFC, 0xF0, 0xE0, + 0x70, 0x38, 0x1C, 0x0C, 0x0C, 0xFC, 0xF8, 0x00, 0x0C, 0x07, 0x07, 0xE3, 0xF8, 0xFC, 0x3C, 0x0E, + 0x01, 0xC0, 0x38, 0x06, 0x00, 0xC0, 0x38, 0xFF, 0xBE, 0xF0, 0x00, 0x00, 0x00, 0x18, 0x03, 0xB0, + 0x3C, 0x07, 0x80, 0xF0, 0x1F, 0x06, 0x7F, 0xC7, 0xE0, 0x00, 0xC0, 0x07, 0x18, 0x19, 0x80, 0xFC, + 0x07, 0xE0, 0x33, 0x83, 0x0F, 0xF8, 0x3F, 0x80, 0x30, 0x71, 0xF3, 0x6C, 0x78, 0xFF, 0x3E, 0x38, + 0x3E, 0x3B, 0x19, 0xCC, 0x7E, 0x1C, 0x00, 0x67, 0x33, 0x3F, 0xE2, 0x66, 0x00, 0x20, 0x71, 0xB9, + 0xC5, 0xC3, 0xC1, 0xE0, 0x60, 0x10, 0x00, 0x01, 0x80, 0x3C, 0x0F, 0xC1, 0xBC, 0x37, 0x87, 0xDF, + 0xFF, 0xFF, 0xC4, 0x00, 0x07, 0x00, 0xF8, 0x0D, 0x81, 0x98, 0x1B, 0x8F, 0xFE, 0xFF, 0xE1, 0x8C, + 0x1C, 0xC0, 0xF8, 0x07, 0x80, 0x00, 0x1C, 0x0F, 0x0F, 0xC6, 0x73, 0x7D, 0xF7, 0xFF, 0xFF, 0x81, + 0x00, 0x1E, 0x0F, 0x83, 0x61, 0xD8, 0x6E, 0x3F, 0xEF, 0xFD, 0x8C, 0x73, 0x0F, 0xC1, 0xE0, 0x0C, + 0x00, 0x1E, 0x00, 0x3E, 0x00, 0x73, 0x00, 0x60, 0x00, 0xC0, 0x00, 0xF8, 0x1E, 0x7F, 0xFE, 0x0F, + 0xF0, 0x03, 0xCF, 0xFF, 0xF3, 0x00, 0xE0, 0x1F, 0xF1, 0xFC, 0x30, 0x00, 0x70, 0x00, 0x68, 0x00, + 0x78, 0x00, 0x2C, 0x00, 0x1E, 0x00, 0x3E, 0x00, 0x73, 0x00, 0x60, 0x00, 0xC0, 0x00, 0xF8, 0x1E, + 0x7F, 0xFE, 0x0F, 0xF0, 0x30, 0x1C, 0x06, 0x81, 0xE0, 0x23, 0xCF, 0xFF, 0xF3, 0x00, 0xE0, 0x1F, + 0xF1, 0xFC, 0x00, 0xF8, 0x0F, 0x80, 0xE0, 0x06, 0x06, 0x38, 0x60, 0xF3, 0x01, 0xD8, 0x06, 0xE0, + 0x73, 0xFF, 0x0F, 0xE0, 0x60, 0x83, 0x03, 0xEC, 0x07, 0xB0, 0x0C, 0xE0, 0x71, 0xFF, 0x83, 0xF8, + 0x00, 0x18, 0xC6, 0x31, 0xBD, 0xC0, 0x7B, 0xC0, 0x10, 0x30, 0x30, 0xFE, 0xFE, 0x00, 0x78, 0x78, + 0x00, 0x38, 0xF1, 0x83, 0x07, 0xDF, 0xBC, 0x00, 0xFB, 0xF0, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, + 0x0C, 0x30, 0xC0, 0xF9, 0xF8, 0x01, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x87, + 0x80, 0x6E, 0xEF, 0x86, 0x66, 0x66, 0x66, 0x66, 0x66, 0x63, 0x8E, 0x3C, 0x81, 0x86, 0x18, 0x61, + 0x86, 0x18, 0x61, 0x86, 0x1E, 0x3C, 0x00, 0x18, 0x70, 0xE1, 0xE2, 0x07, 0x1F, 0x37, 0x66, 0xFD, + 0xF8, 0xF1, 0xDF, 0x1C, 0x00, 0x18, 0x1C, 0x0E, 0x07, 0x82, 0x01, 0xC1, 0xF0, 0xDC, 0x66, 0x3F, + 0xDF, 0xE1, 0xC1, 0xC7, 0xC1, 0xC0, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC0, 0x39, 0x8E, + 0x72, 0x00, 0x61, 0x86, 0x18, 0x61, 0x86, 0x18, 0x61, 0x87, 0x8F, 0x01, 0xC7, 0x1C, 0x71, 0x00, + 0x38, 0x01, 0x87, 0xCE, 0x7C, 0x77, 0x02, 0x30, 0x31, 0xC3, 0x07, 0x98, 0x0E, 0xC0, 0x37, 0x03, + 0x9F, 0xF8, 0x7F, 0x00, 0x38, 0x00, 0xC0, 0x03, 0x80, 0x0E, 0x00, 0x20, 0x01, 0x82, 0x0C, 0x0F, + 0xB0, 0x1E, 0xC0, 0x33, 0x81, 0xC7, 0xFE, 0x0F, 0xE0, 0x6E, 0xDF, 0xC6, 0x73, 0x33, 0xFE, 0x00, + 0x70, 0xC1, 0xC3, 0x84, 0x06, 0x0C, 0x18, 0xFD, 0xFC, 0x00, 0xFF, 0xFF, 0xFF, 0xC6, 0x31, 0x8C, + 0x63, 0x18, 0xC6, 0x3C, 0xF0, 0x00, 0x00, 0x06, 0x40, 0x07, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, + 0xF8, 0x3F, 0x7F, 0xFE, 0x0F, 0xE0, 0x01, 0x80, 0x01, 0x80, 0x00, 0x00, 0x40, 0x00, 0x0C, 0x00, + 0x00, 0xC0, 0x03, 0x0C, 0x00, 0x78, 0xF8, 0x7F, 0xE7, 0xFF, 0x9E, 0x0F, 0xC0, 0x00, 0x18, 0x00, + 0x01, 0x80, 0x00, 0x00, 0x00, 0x67, 0x33, 0x3F, 0xE0, 0x66, 0x00, 0x30, 0x60, 0xC7, 0xEF, 0xE0, + 0x0C, 0x18, 0x00, 0x00, 0xF9, 0xA0, 0x03, 0x07, 0x1F, 0x36, 0xC7, 0x8F, 0xF3, 0xE0, 0x00, 0x1E, + 0x0F, 0x00, 0x00, 0xC0, 0x60, 0xF8, 0xFC, 0xCE, 0x63, 0x3F, 0x87, 0xF0, 0x78, 0x00, 0x00, 0x00, + 0x03, 0xC0, 0x03, 0xC6, 0x40, 0x07, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xF8, 0x3F, 0x7F, 0xFE, + 0x0F, 0xE0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x03, 0xC0, 0x04, 0x00, 0x00, 0xC0, 0x00, 0x0C, 0x00, + 0x30, 0xC0, 0x07, 0x8F, 0x87, 0xFE, 0x7F, 0xF9, 0xE0, 0xFC, 0x00, 0x07, 0xBC, 0x06, 0x38, 0xC6, + 0x37, 0xB8, 0x00, 0x00, 0x7C, 0x68, 0x00, 0x10, 0x30, 0x30, 0xFE, 0xFE, 0x00, 0x01, 0x80, 0x01, + 0x80, 0x03, 0xC0, 0x03, 0xC6, 0x40, 0x07, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xF8, 0x3F, 0x7F, + 0xFE, 0x0F, 0xE0, 0x01, 0x80, 0x00, 0x18, 0x00, 0x03, 0xC0, 0x00, 0x3C, 0x00, 0x40, 0x00, 0x0C, + 0x00, 0x00, 0xC0, 0x03, 0x0C, 0x00, 0x78, 0xF8, 0x7F, 0xE7, 0xFF, 0x9E, 0x0F, 0xC0, 0x00, 0x63, + 0x3D, 0xE0, 0x31, 0xC6, 0x31, 0xBD, 0xC0, 0x00, 0x30, 0x30, 0x7C, 0x68, 0x00, 0x10, 0x30, 0x30, + 0xFE, 0xFE, 0x00, 0xFC, 0x3F, 0xF0, 0x7C, 0x78, 0x38, 0x1C, 0x0E, 0x03, 0x18, 0xC6, 0x30, 0x0C, + 0x03, 0xC3, 0x7F, 0xC7, 0xE0, 0xFC, 0x0F, 0xFC, 0x07, 0xC1, 0xF0, 0x3B, 0x87, 0x1E, 0xE1, 0xFC, + 0x60, 0xC6, 0x0C, 0x00, 0xC0, 0x0F, 0x0C, 0x7F, 0xC1, 0xF8, 0x7C, 0x1F, 0xC0, 0x78, 0x3E, 0xFE, + 0x3E, 0x00, 0x00, 0x20, 0x0C, 0x00, 0x00, 0x7C, 0x07, 0xF0, 0x07, 0x80, 0xF8, 0xFF, 0xEF, 0x8F, + 0x00, 0x00, 0x80, 0x18, 0x00, 0x00, 0xFC, 0x3F, 0xF0, 0x7C, 0x78, 0x38, 0x1C, 0x0E, 0x03, 0x00, + 0xC0, 0x30, 0x0C, 0x03, 0xC3, 0x7F, 0xC7, 0xE0, 0xFC, 0x0F, 0xFC, 0x07, 0xC1, 0xF0, 0x3B, 0x87, + 0x1E, 0xE1, 0xFC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0x0C, 0x7F, 0xC1, 0xF8, 0x7C, 0x1F, 0xC0, + 0x78, 0x3E, 0xFE, 0x3E, 0x00, 0x00, 0x7C, 0x07, 0xF0, 0x07, 0x80, 0xF8, 0xFF, 0xEF, 0x8F, 0x00, + 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0xFC, 0x3F, 0xF0, 0x7C, 0x78, 0x38, 0x1C, 0x0E, 0x03, 0x00, + 0xC0, 0x30, 0x0C, 0x03, 0xC3, 0x7F, 0xC7, 0xE0, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xFC, 0x0F, + 0xFC, 0x07, 0xC1, 0xF0, 0x3B, 0x87, 0x1E, 0xE1, 0xFC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0x0C, + 0x7F, 0xC1, 0xF8, 0x00, 0x0C, 0x03, 0x00, 0x00, 0x7C, 0x1F, 0xC0, 0x78, 0x3E, 0xFE, 0x3E, 0x00, + 0x00, 0x00, 0x07, 0x00, 0x20, 0x00, 0x00, 0x7C, 0x07, 0xF0, 0x07, 0x80, 0xF8, 0xFF, 0xEF, 0x8F, + 0x00, 0x00, 0x08, 0x0C, 0x0E, 0x06, 0x06, 0x07, 0x7E, 0xFE, 0x00, 0x08, 0x03, 0x00, 0xE0, 0x18, + 0x06, 0x01, 0xC7, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x38, 0x10, 0x00, 0x08, 0x0C, 0x0E, 0x06, 0x06, + 0x07, 0x7E, 0xFE, 0x00, 0x00, 0x06, 0x01, 0x80, 0x00, 0x08, 0x03, 0x00, 0xE0, 0x18, 0x06, 0x01, + 0xC7, 0xFF, 0xFF, 0x00, 0x00, 0x08, 0x18, 0x30, 0x60, 0xC1, 0x87, 0x1C, 0xF0, 0xC0, 0x08, 0x06, + 0x03, 0x01, 0xC0, 0xF0, 0x7C, 0x70, 0x70, 0xF0, 0x30, 0x00, 0x00, 0x30, 0x60, 0x00, 0x81, 0x83, + 0x06, 0x0C, 0x18, 0x71, 0xCF, 0x0C, 0x00, 0x00, 0x0C, 0x06, 0x00, 0x00, 0x80, 0x60, 0x30, 0x1C, + 0x0F, 0x07, 0xC7, 0x07, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x60, 0x00, 0x06, 0x00, 0xC2, 0x60, 0x0E, + 0x63, 0x60, 0x66, 0x3C, 0x07, 0xFE, 0xC0, 0x7F, 0xEC, 0x06, 0x00, 0xE0, 0xC0, 0x07, 0xFC, 0x00, + 0x3F, 0x80, 0x00, 0x00, 0x00, 0x60, 0x03, 0x09, 0x80, 0x0E, 0x63, 0x18, 0x19, 0x8C, 0xC0, 0x7F, + 0xFF, 0x01, 0xFF, 0xFC, 0x06, 0x00, 0x38, 0x30, 0x00, 0x7F, 0xC0, 0x00, 0xFE, 0x00, 0x00, 0x00, + 0x30, 0x01, 0x8C, 0x4C, 0x66, 0x73, 0x31, 0xFF, 0xFB, 0xFF, 0xC0, 0x00, 0x00, 0x30, 0x62, 0x20, + 0xCC, 0x61, 0x98, 0xCF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x60, 0x00, + 0x1F, 0x00, 0x00, 0xB0, 0x00, 0x00, 0x60, 0x00, 0x06, 0x00, 0xC2, 0x60, 0x0E, 0x63, 0x60, 0x66, + 0x3C, 0x07, 0xFE, 0xC0, 0x7F, 0xEC, 0x06, 0x00, 0xE0, 0xC0, 0x07, 0xFC, 0x00, 0x3F, 0x80, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x18, 0x00, 0x01, 0xF0, 0x00, 0x02, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x80, 0x0C, 0x26, 0x00, 0x39, 0x8C, 0x60, 0x66, 0x33, 0x01, 0xFF, 0xFC, 0x07, 0xFF, 0xF0, 0x18, + 0x00, 0xE0, 0xC0, 0x01, 0xFF, 0x00, 0x03, 0xF8, 0x00, 0x00, 0x03, 0x00, 0x18, 0x01, 0xE0, 0x0F, + 0x00, 0x03, 0x00, 0x18, 0xC4, 0xC6, 0x67, 0x33, 0x1F, 0xFF, 0xBF, 0xFC, 0x00, 0x00, 0x03, 0x00, + 0x06, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x06, 0x0C, 0x44, 0x19, 0x8C, 0x33, 0x19, 0xFF, + 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x07, 0xE0, 0x0C, 0x3F, 0x80, 0x39, 0xC6, + 0x60, 0x6C, 0x1B, 0x01, 0xFF, 0xEC, 0x07, 0xFF, 0x30, 0x19, 0xE0, 0xE0, 0xC0, 0x01, 0xFF, 0x00, + 0x03, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x01, 0xF8, 0x00, 0xC3, 0xF8, 0x00, 0xE7, 0x18, + 0x60, 0x6C, 0x18, 0xC0, 0x7F, 0xFE, 0xC0, 0x7F, 0xFF, 0xC0, 0x67, 0x80, 0xE0, 0xC0, 0x00, 0x7F, + 0xC0, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x10, 0x01, 0xF8, 0xC7, 0xF1, 0x9C, 0x33, 0x70, 0xFF, 0xC7, + 0xBF, 0xFE, 0x03, 0xE0, 0x00, 0x10, 0x00, 0x7E, 0x0C, 0x7F, 0x06, 0x70, 0xC3, 0x70, 0xE7, 0xF1, + 0xFB, 0xFF, 0xFE, 0x0F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x07, 0xE0, 0x0C, 0x3F, 0x80, 0x39, 0xC6, 0x60, 0x6C, 0x1B, 0x01, 0xFF, + 0xEC, 0x07, 0xFF, 0x30, 0x19, 0xE0, 0xE0, 0xC0, 0x01, 0xFF, 0x00, 0x03, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x01, + 0xF8, 0x00, 0xC3, 0xF8, 0x00, 0xE7, 0x18, 0x60, 0x6C, 0x18, 0xC0, 0x7F, 0xFE, 0xC0, 0x7F, 0xFF, + 0xC0, 0x67, 0x80, 0xE0, 0xC0, 0x00, 0x7F, 0xC0, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x60, + 0x00, 0xC0, 0x00, 0x80, 0x0F, 0xC6, 0x3F, 0x8C, 0xE1, 0x9B, 0x87, 0xFE, 0x3D, 0xFF, 0xF0, 0x1F, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x0C, 0x00, 0x02, 0x00, 0x0F, 0xC1, 0x8F, 0xE0, 0xCE, 0x18, + 0x6E, 0x1C, 0xFE, 0x3F, 0x7F, 0xFF, 0xC1, 0xF0, 0x00, 0x18, 0x00, 0xC0, 0x06, 0x00, 0x30, 0x01, + 0x80, 0x0C, 0x20, 0x6F, 0xC3, 0xFF, 0x1E, 0x18, 0xE1, 0xFF, 0xFD, 0xFF, 0xC1, 0xF0, 0x00, 0x18, + 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x08, 0x06, 0xFC, 0x0F, 0xFC, 0x1E, 0x18, + 0x38, 0x73, 0xFF, 0xF7, 0xFF, 0xF1, 0xF0, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC2, + 0x19, 0xF3, 0xFF, 0x78, 0x6E, 0x1F, 0xFF, 0x7F, 0xC0, 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xC0, + 0x06, 0x00, 0x30, 0x81, 0x9F, 0x0F, 0xFC, 0x78, 0x63, 0x87, 0x3F, 0xFD, 0xFF, 0xF0, 0x00, 0x00, + 0x18, 0x00, 0xC0, 0x06, 0x38, 0x30, 0x81, 0x80, 0x0C, 0x20, 0x6F, 0xC3, 0xFF, 0x1E, 0x18, 0xE1, + 0xFF, 0xFD, 0xFF, 0xC1, 0xF0, 0x00, 0x18, 0x00, 0x30, 0x00, 0x63, 0x00, 0xC4, 0x01, 0x80, 0x03, + 0x08, 0x06, 0xFC, 0x0F, 0xFC, 0x1E, 0x18, 0x38, 0x73, 0xFF, 0xF7, 0xFF, 0xF1, 0xF0, 0x00, 0x60, + 0x0C, 0x01, 0x80, 0x31, 0x86, 0x30, 0xC2, 0x19, 0xF3, 0xFF, 0x78, 0x6E, 0x1F, 0xFF, 0x7F, 0xC0, + 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xC6, 0x06, 0x30, 0x30, 0x81, 0x9F, 0x0F, 0xFC, 0x78, 0x63, + 0x87, 0x3F, 0xFD, 0xFF, 0xF0, 0x00, 0x00, 0x18, 0x1F, 0x0E, 0x03, 0x00, 0xE2, 0x1F, 0x83, 0xE1, + 0xC0, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x3C, 0x27, 0xFC, 0x7E, 0x3E, 0x1F, 0xC6, 0x31, 0xD8, + 0x3E, 0x0F, 0xE7, 0x7F, 0x80, 0xC0, 0x30, 0x0C, 0x03, 0x86, 0x7F, 0x8F, 0xC0, 0x0C, 0x3E, 0x70, + 0x60, 0x60, 0x73, 0xFF, 0xFC, 0x00, 0x3E, 0x1F, 0xC6, 0x31, 0xD8, 0x3E, 0x3F, 0xEF, 0xFC, 0x00, + 0x00, 0x06, 0x01, 0x80, 0x00, 0x18, 0x1F, 0x0E, 0x03, 0x00, 0xE2, 0x1F, 0x83, 0xE1, 0xC0, 0xE0, + 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x3C, 0x27, 0xFC, 0x7E, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x3E, 0x1F, + 0xC6, 0x31, 0xD8, 0x3E, 0x0F, 0xE7, 0x7F, 0x80, 0xC0, 0x30, 0x0C, 0x03, 0x86, 0x7F, 0x8F, 0xC0, + 0x00, 0x1C, 0x08, 0x00, 0x0C, 0x3E, 0x70, 0x60, 0x60, 0x73, 0xFF, 0xFC, 0x00, 0x00, 0x07, 0x00, + 0x80, 0x00, 0x3E, 0x1F, 0xC6, 0x31, 0xD8, 0x3E, 0x3F, 0xEF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x1E, 0x00, 0x3F, 0x40, 0x33, 0xC0, 0x33, 0xC0, 0x3F, + 0xC0, 0x1F, 0xF8, 0x0F, 0x7F, 0xFE, 0x0F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x00, 0x0F, 0x04, 0x00, 0xF8, 0xC0, 0x19, 0x8C, 0x01, 0x98, 0xC0, 0x1D, 0x8F, 0x83, + 0xFE, 0x7F, 0xFF, 0xE0, 0xFE, 0x00, 0x00, 0x30, 0x60, 0x01, 0x87, 0x9F, 0xB3, 0x66, 0xFC, 0xFF, + 0xEF, 0x80, 0x00, 0x00, 0x06, 0x01, 0x80, 0x00, 0x3C, 0x1F, 0x86, 0x61, 0x98, 0x76, 0x3F, 0xEF, + 0xF8, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x78, 0x00, 0x00, 0x0E, 0x00, 0xF8, 0x0E, 0xCC, 0x66, 0xE3, + 0xFE, 0x0F, 0xB0, 0x0D, 0xC0, 0xE7, 0xFE, 0x1F, 0xE0, 0x10, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x02, + 0xC0, 0x00, 0x00, 0x0E, 0x00, 0x3E, 0x00, 0x6C, 0x31, 0x98, 0x63, 0xFD, 0x83, 0xFF, 0x00, 0xC7, + 0x01, 0x87, 0x0E, 0x0F, 0xF8, 0x07, 0xC0, 0x00, 0x00, 0xF8, 0xB0, 0x01, 0x87, 0x9F, 0xB3, 0x66, + 0xFC, 0xFF, 0xEF, 0x80, 0x00, 0x00, 0x0F, 0x03, 0xC0, 0x00, 0x3C, 0x1F, 0x86, 0x61, 0x98, 0x76, + 0x3F, 0xEF, 0xF8, 0x00, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x63, 0x03, 0x8C, 0x0C, 0x34, 0x18, + 0xF0, 0xE3, 0xC3, 0x0F, 0x00, 0x3F, 0x87, 0xDF, 0xFE, 0x0F, 0xC0, 0x00, 0x0C, 0x00, 0x06, 0x00, + 0x03, 0x00, 0x31, 0x80, 0x38, 0xC0, 0x18, 0x61, 0x06, 0x31, 0x87, 0x18, 0xC3, 0x0C, 0x60, 0x06, + 0x3E, 0x1F, 0xCF, 0xFF, 0xE0, 0xFC, 0x00, 0x06, 0x1E, 0x7C, 0xF0, 0xE0, 0x70, 0x38, 0x1C, 0x0C, + 0x0C, 0xFC, 0xF8, 0x00, 0x06, 0x07, 0x87, 0xC3, 0xC0, 0xE0, 0x1C, 0x03, 0x80, 0x60, 0x0C, 0x03, + 0x8F, 0xFB, 0xEF, 0x00, 0x00, 0x00, 0xE0, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, + 0x00, 0x6C, 0x0F, 0x01, 0xE0, 0x3C, 0x0D, 0xC1, 0x9F, 0xE1, 0xF8, 0x00, 0xE0, 0x03, 0x00, 0x18, + 0x00, 0xC0, 0x06, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x63, 0x03, 0x30, 0x1F, 0x80, 0xFC, 0x0E, + 0x70, 0x61, 0xFF, 0x07, 0xE0, 0x66, 0x66, 0x77, 0x73, 0x33, 0xFE, 0x00, 0x61, 0x87, 0x1C, 0x71, + 0xC3, 0x0C, 0x30, 0xCF, 0xFF, 0x00, 0x1C, 0x3E, 0x76, 0x63, 0x1F, 0x7F, 0xE6, 0xC0, 0xC0, 0xC0, + 0xC0, 0xC0, 0x60, 0x60, 0x00, 0x03, 0xC0, 0xFC, 0x39, 0x8F, 0x31, 0xFF, 0xB7, 0xF6, 0x30, 0xC0, + 0x18, 0x03, 0x00, 0x70, 0x06, 0x00, 0xC0, 0x00, 0x1E, 0x3F, 0x33, 0x73, 0xFF, 0xFE, 0x00, 0x00, + 0x03, 0xC0, 0xFC, 0x19, 0x87, 0x39, 0xF7, 0xB7, 0xF8, 0x30, 0x00, 0x01, 0xC0, 0x10, 0x00, 0x00, + 0x0C, 0x01, 0xD8, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x83, 0x3F, 0xE3, 0xF0, 0x00, 0x00, 0x70, 0x01, + 0x00, 0x00, 0x00, 0x0C, 0x00, 0x71, 0x81, 0x98, 0x0F, 0xC0, 0x7E, 0x03, 0x38, 0x30, 0xFF, 0x83, + 0xF8, 0x00, 0x06, 0x60, 0x67, 0x33, 0x3F, 0xE0, 0x00, 0xE0, 0x80, 0x03, 0x06, 0x0C, 0x7E, 0xFE, + 0x00, 0x30, 0x71, 0xF3, 0x6C, 0x78, 0xFF, 0x3E, 0x0C, 0x06, 0x0F, 0x8F, 0xCC, 0xE6, 0x33, 0xF8, + 0x7F, 0x07, 0x80, 0x00, 0x00, 0x1C, 0x0F, 0x0F, 0xC6, 0x73, 0x7D, 0xF7, 0xFF, 0xFF, 0x81, 0x00, + 0x1E, 0x0F, 0x83, 0x61, 0xD8, 0x6E, 0x3F, 0xEF, 0xFD, 0x8C, 0x73, 0x0F, 0xC1, 0xE0, 0x38, 0xF9, + 0xBB, 0x37, 0xEF, 0xC7, 0x8E, 0xF8, 0xE0, 0x38, 0x3E, 0x1B, 0x8C, 0xC7, 0xFB, 0xFC, 0x38, 0x38, + 0xF8, 0x38, 0x00, 0x00, 0xF8, 0x0F, 0x80, 0xE0, 0x06, 0x06, 0x38, 0x60, 0xF3, 0x01, 0xD8, 0x06, + 0xE0, 0x73, 0xFF, 0x0F, 0xE0, 0x60, 0x83, 0x03, 0xEC, 0x07, 0xB0, 0x0C, 0xE0, 0x71, 0xFF, 0x83, + 0xF8, 0x00, 0x00, 0xF8, 0x0F, 0x80, 0xE0, 0x06, 0x06, 0x38, 0x60, 0xF3, 0x01, 0xD8, 0x06, 0xE0, + 0x73, 0xFF, 0x0F, 0xE0, 0x00, 0x00, 0xF0, 0x07, 0x80, 0x00, 0x00, 0x60, 0x83, 0x03, 0xEC, 0x07, + 0xB0, 0x0C, 0xE0, 0x71, 0xFF, 0x83, 0xF8, 0x00, 0x00, 0x0A, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x18, + 0xC6, 0x31, 0xBD, 0xC0, 0x7B, 0xC0, 0x10, 0x30, 0x30, 0xFE, 0xFE, 0x00, 0x78, 0x78, 0x00, 0x7C, + 0x0F, 0x98, 0x03, 0x08, 0x61, 0xCC, 0x1D, 0x81, 0xF0, 0x1E, 0x01, 0xC0, 0x7C, 0x1D, 0x87, 0xF0, + 0xFC, 0xFC, 0x0F, 0xD8, 0x01, 0x82, 0x18, 0x31, 0x83, 0x98, 0x1D, 0x80, 0xF8, 0x07, 0x80, 0x38, + 0x03, 0x83, 0xFF, 0x3E, 0xF0, 0x00, 0x70, 0x18, 0x07, 0x01, 0xC6, 0x41, 0x88, 0x63, 0x98, 0x76, + 0x0F, 0x81, 0xE0, 0x38, 0x1F, 0x0E, 0xC7, 0xF1, 0xF8, 0x60, 0x1C, 0x03, 0x80, 0x79, 0x88, 0x30, + 0x86, 0x18, 0xC3, 0x98, 0x3B, 0x03, 0xE0, 0x3C, 0x03, 0x80, 0x70, 0xFF, 0xDF, 0x78, 0x00, 0x03, + 0x01, 0x90, 0xCE, 0x63, 0xB0, 0xF8, 0x3C, 0x0E, 0x0F, 0x8E, 0xCF, 0xE7, 0xE0, 0x01, 0x81, 0xC0, + 0xF0, 0x78, 0x20, 0x00, 0x03, 0x00, 0x61, 0x0C, 0x31, 0x87, 0x30, 0x76, 0x07, 0xC0, 0x78, 0x07, + 0x00, 0xE1, 0xFF, 0xBE, 0xF0, 0x00, 0x30, 0x0E, 0x01, 0xA0, 0x3C, 0x06, 0x00, 0x03, 0x01, 0x90, + 0xCE, 0x63, 0xB0, 0xF8, 0x3C, 0x0E, 0x0F, 0x8E, 0xCF, 0xE7, 0xE0, 0x03, 0x00, 0x61, 0x0C, 0x31, + 0x87, 0x30, 0x76, 0x07, 0xC0, 0x78, 0x07, 0x00, 0xE1, 0xFF, 0xBE, 0xF0, 0x00, }; static const EpdGlyph inter_8_regularGlyphs[] = { @@ -1517,656 +1718,851 @@ static const EpdGlyph inter_8_regularGlyphs[] = { { 7, 4, 0, -3, 14, 4, 6874 }, // U+030B { 6, 3, 0, 0, 13, 3, 6878 }, // U+030C { 6, 3, 0, 1, 13, 3, 6881 }, // U+030F - { 3, 6, 0, 3, 9, 3, 6884 }, // U+0313 - { 3, 6, 0, 3, 9, 3, 6887 }, // U+0315 - { 4, 4, 0, -4, 12, 2, 6890 }, // U+031B - { 3, 3, 62, 0, -1, 2, 6892 }, // U+0323 - { 3, 4, 0, -7, -1, 2, 6894 }, // U+0326 - { 4, 5, 0, 1, 1, 3, 6896 }, // U+0327 - { 4, 5, 0, 0, 1, 3, 6899 }, // U+0328 - { 6, 3, 0, 0, -1, 3, 6902 }, // U+032C - { 8, 12, 0, -9, 10, 12, 6905 }, // U+0337 - { 10, 14, 0, -10, 13, 18, 6917 }, // U+0338 - { 5, 3, 0, -7, 13, 2, 6935 }, // U+0342 - { 3, 5, 0, 0, 11, 2, 6937 }, // U+0343 - { 6, 4, 0, -8, 14, 3, 6939 }, // U+0346 - { 6, 4, 0, -8, -2, 3, 6942 }, // U+0347 - { 5, 4, 0, -7, 0, 3, 6945 }, // U+0348 - { 4, 5, 0, -7, 0, 3, 6948 }, // U+0349 - { 6, 4, 0, -8, 13, 3, 6951 }, // U+034A - { 7, 7, 0, -9, 16, 7, 6954 }, // U+034B - { 7, 4, 0, -8, 14, 4, 6961 }, // U+034C - { 8, 5, 0, -9, 0, 5, 6965 }, // U+034D - { 5, 5, 0, -7, 0, 4, 6970 }, // U+034E - { 0, 0, 0, 0, 0, 0, 6974 }, // U+034F - { 7, 7, 0, -8, 17, 7, 6974 }, // U+0350 - { 3, 4, 0, -6, 13, 2, 6981 }, // U+0351 - { 6, 5, 0, -8, 14, 4, 6983 }, // U+0352 - { 5, 5, 0, -7, 0, 4, 6987 }, // U+0353 - { 5, 5, 0, -7, 0, 4, 6991 }, // U+0354 - { 5, 5, 0, -6, 0, 4, 6995 }, // U+0355 - { 9, 5, 0, -8, 0, 6, 6999 }, // U+0356 - { 3, 5, 0, 0, 14, 2, 7005 }, // U+0357 - { 3, 3, 0, 0, 13, 2, 7007 }, // U+0358 - { 4, 5, 0, -7, 0, 3, 7009 }, // U+0359 - { 7, 4, 0, -8, 0, 4, 7012 }, // U+035A - { 4, 5, 0, -7, 14, 3, 7016 }, // U+035B - { 12, 4, 0, -6, 0, 6, 7019 }, // U+035C - { 12, 4, 0, -6, 13, 6, 7025 }, // U+035D - { 11, 3, 0, -10, 13, 5, 7031 }, // U+035E - { 11, 3, 0, -10, -1, 5, 7036 }, // U+035F - { 16, 3, 0, -8, 13, 6, 7041 }, // U+0360 - { 12, 4, 0, -6, 13, 6, 7047 }, // U+0361 - { 13, 5, 0, -6, 0, 9, 7053 }, // U+0362 - { 5, 6, 0, -7, 15, 4, 7062 }, // U+0363 - { 5, 6, 0, -7, 15, 4, 7066 }, // U+0364 - { 2, 7, 0, -6, 16, 2, 7070 }, // U+0365 - { 5, 6, 0, -7, 15, 4, 7072 }, // U+0366 - { 5, 6, 0, -7, 15, 4, 7076 }, // U+0367 - { 5, 6, 0, -7, 15, 4, 7080 }, // U+0368 - { 5, 8, 0, -7, 17, 5, 7084 }, // U+0369 - { 5, 8, 0, -7, 17, 5, 7089 }, // U+036A - { 7, 6, 0, -8, 15, 6, 7094 }, // U+036B - { 3, 6, 0, -6, 15, 3, 7100 }, // U+036C - { 4, 7, 0, 0, 16, 4, 7103 }, // U+036D - { 5, 6, 0, -7, 15, 4, 7107 }, // U+036E - { 5, 6, 0, -7, 15, 4, 7111 }, // U+036F - { 8, 16, 159, 1, 16, 16, 7115 }, // U+0400 - { 8, 16, 159, 1, 16, 16, 7131 }, // U+0401 - { 14, 14, 222, 0, 13, 25, 7147 }, // U+0402 - { 8, 16, 152, 1, 16, 16, 7172 }, // U+0403 - { 12, 14, 194, 0, 13, 21, 7188 }, // U+0404 - { 10, 14, 170, 0, 13, 18, 7209 }, // U+0405 - { 2, 13, 70, 1, 13, 4, 7227 }, // U+0406 - { 6, 16, 70, -1, 16, 12, 7231 }, // U+0407 - { 8, 14, 150, 0, 13, 14, 7243 }, // U+0408 - { 18, 13, 295, 0, 13, 30, 7257 }, // U+0409 - { 17, 13, 298, 1, 13, 28, 7287 }, // U+040A - { 14, 13, 234, 0, 13, 23, 7315 }, // U+040B - { 10, 16, 177, 1, 16, 20, 7338 }, // U+040C - { 10, 16, 198, 1, 16, 20, 7358 }, // U+040D - { 10, 16, 165, 0, 16, 20, 7378 }, // U+040E - { 10, 16, 191, 1, 13, 20, 7398 }, // U+040F - { 11, 13, 182, 0, 13, 18, 7418 }, // U+0410 - { 9, 13, 170, 1, 13, 15, 7436 }, // U+0411 - { 9, 13, 174, 1, 13, 15, 7451 }, // U+0412 - { 8, 13, 152, 1, 13, 13, 7466 }, // U+0413 - { 13, 16, 215, 0, 13, 26, 7479 }, // U+0414 - { 8, 13, 159, 1, 13, 13, 7505 }, // U+0415 - { 16, 13, 254, 0, 13, 26, 7518 }, // U+0416 - { 10, 14, 164, 0, 13, 18, 7544 }, // U+0417 - { 10, 13, 198, 1, 13, 17, 7562 }, // U+0418 - { 10, 16, 198, 1, 16, 20, 7579 }, // U+0419 - { 10, 13, 177, 1, 13, 17, 7599 }, // U+041A - { 11, 13, 197, 0, 13, 18, 7616 }, // U+041B - { 13, 13, 238, 1, 13, 22, 7634 }, // U+041C - { 10, 13, 196, 1, 13, 17, 7656 }, // U+041D - { 12, 14, 203, 0, 13, 21, 7673 }, // U+041E - { 10, 13, 196, 1, 13, 17, 7694 }, // U+041F - { 9, 13, 169, 1, 13, 15, 7711 }, // U+0420 - { 12, 14, 194, 0, 13, 21, 7726 }, // U+0421 - { 10, 13, 170, 0, 13, 17, 7747 }, // U+0422 - { 10, 13, 165, 0, 13, 17, 7764 }, // U+0423 - { 13, 14, 220, 0, 13, 23, 7781 }, // U+0424 - { 11, 13, 180, 0, 13, 18, 7804 }, // U+0425 - { 11, 16, 190, 1, 13, 22, 7822 }, // U+0426 - { 10, 13, 185, 1, 13, 17, 7844 }, // U+0427 - { 14, 13, 253, 1, 13, 23, 7861 }, // U+0428 - { 15, 16, 257, 1, 13, 30, 7884 }, // U+0429 - { 13, 13, 216, 0, 13, 22, 7914 }, // U+042A - { 13, 13, 232, 1, 13, 22, 7936 }, // U+042B - { 9, 13, 171, 1, 13, 15, 7958 }, // U+042C - { 12, 14, 194, 0, 13, 21, 7973 }, // U+042D - { 15, 14, 270, 1, 13, 27, 7994 }, // U+042E - { 10, 13, 171, 0, 13, 17, 8021 }, // U+042F - { 8, 11, 147, 0, 10, 11, 8038 }, // U+0430 - { 9, 14, 156, 0, 13, 16, 8049 }, // U+0431 - { 8, 9, 148, 1, 9, 9, 8065 }, // U+0432 - { 6, 9, 116, 1, 9, 7, 8074 }, // U+0433 - { 10, 12, 162, 0, 9, 15, 8081 }, // U+0434 - { 9, 11, 153, 0, 10, 13, 8096 }, // U+0435 - { 13, 9, 214, 0, 9, 15, 8109 }, // U+0436 - { 8, 11, 130, 0, 10, 11, 8124 }, // U+0437 - { 8, 9, 156, 1, 9, 9, 8135 }, // U+0438 - { 8, 13, 156, 1, 13, 13, 8144 }, // U+0439 - { 8, 9, 142, 1, 9, 9, 8157 }, // U+043A - { 9, 9, 152, 0, 9, 11, 8166 }, // U+043B - { 11, 9, 203, 1, 9, 13, 8177 }, // U+043C - { 8, 9, 155, 1, 9, 9, 8190 }, // U+043D - { 10, 11, 157, 0, 10, 14, 8199 }, // U+043E - { 8, 9, 155, 1, 9, 9, 8213 }, // U+043F - { 9, 14, 161, 1, 10, 16, 8222 }, // U+0440 - { 9, 11, 150, 0, 10, 13, 8238 }, // U+0441 - { 8, 9, 124, 0, 9, 9, 8251 }, // U+0442 - { 9, 13, 147, 0, 9, 15, 8260 }, // U+0443 - { 11, 17, 181, 0, 13, 24, 8275 }, // U+0444 - { 9, 9, 143, 0, 9, 11, 8299 }, // U+0445 - { 9, 12, 157, 1, 9, 14, 8310 }, // U+0446 - { 8, 9, 152, 1, 9, 9, 8324 }, // U+0447 - { 12, 9, 217, 1, 9, 14, 8333 }, // U+0448 - { 13, 12, 221, 1, 9, 20, 8347 }, // U+0449 - { 10, 9, 170, 0, 9, 12, 8367 }, // U+044A - { 10, 9, 193, 1, 9, 12, 8379 }, // U+044B - { 8, 9, 147, 1, 9, 9, 8391 }, // U+044C - { 9, 11, 150, 0, 10, 13, 8400 }, // U+044D - { 13, 11, 221, 1, 10, 18, 8413 }, // U+044E - { 8, 9, 146, 0, 9, 9, 8431 }, // U+044F - { 9, 14, 153, 0, 13, 16, 8440 }, // U+0450 - { 9, 14, 153, 0, 13, 16, 8456 }, // U+0451 - { 9, 17, 155, 0, 13, 20, 8472 }, // U+0452 - { 6, 13, 116, 1, 13, 10, 8492 }, // U+0453 - { 9, 11, 150, 0, 10, 13, 8502 }, // U+0454 - { 8, 11, 138, 0, 10, 11, 8515 }, // U+0455 - { 3, 13, 63, 0, 13, 5, 8526 }, // U+0456 - { 6, 13, 63, -1, 13, 10, 8531 }, // U+0457 - { 4, 17, 63, -1, 13, 9, 8541 }, // U+0458 - { 15, 9, 245, 0, 9, 17, 8550 }, // U+0459 - { 14, 9, 238, 1, 9, 16, 8567 }, // U+045A - { 9, 13, 155, 0, 13, 15, 8583 }, // U+045B - { 8, 13, 142, 1, 13, 13, 8598 }, // U+045C - { 8, 13, 156, 1, 13, 13, 8611 }, // U+045D - { 9, 17, 147, 0, 13, 20, 8624 }, // U+045E - { 8, 12, 155, 1, 9, 12, 8644 }, // U+045F - { 13, 14, 237, 1, 13, 23, 8656 }, // U+0460 - { 12, 10, 213, 1, 9, 15, 8679 }, // U+0461 - { 11, 13, 183, 0, 13, 18, 8694 }, // U+0462 - { 10, 13, 162, 0, 13, 17, 8712 }, // U+0463 - { 14, 14, 253, 1, 13, 25, 8729 }, // U+0464 - { 12, 11, 206, 1, 10, 17, 8754 }, // U+0465 - { 11, 13, 182, 0, 13, 18, 8771 }, // U+0466 - { 10, 9, 151, 0, 9, 12, 8789 }, // U+0467 - { 15, 13, 248, 1, 13, 25, 8801 }, // U+0468 - { 12, 9, 205, 1, 9, 14, 8826 }, // U+0469 - { 12, 13, 225, 1, 13, 20, 8840 }, // U+046A - { 11, 9, 185, 0, 9, 13, 8860 }, // U+046B - { 17, 13, 304, 1, 13, 28, 8873 }, // U+046C - { 15, 9, 254, 1, 9, 17, 8901 }, // U+046D - { 9, 20, 143, 0, 16, 23, 8918 }, // U+046E - { 8, 17, 137, 0, 13, 17, 8941 }, // U+046F - { 13, 13, 233, 1, 13, 22, 8958 }, // U+0470 - { 11, 16, 205, 1, 12, 22, 8980 }, // U+0471 - { 12, 14, 203, 0, 13, 21, 9002 }, // U+0472 - { 10, 11, 157, 0, 10, 14, 9023 }, // U+0473 - { 12, 13, 186, 0, 13, 20, 9037 }, // U+0474 - { 10, 9, 156, 0, 9, 12, 9057 }, // U+0475 - { 12, 16, 186, 0, 16, 24, 9069 }, // U+0476 - { 10, 13, 156, 0, 13, 17, 9093 }, // U+0477 - { 22, 17, 350, 0, 13, 47, 9110 }, // U+0478 - { 19, 14, 299, 0, 10, 34, 9157 }, // U+0479 - { 11, 13, 189, 0, 13, 18, 9191 }, // U+0480 - { 9, 13, 148, 0, 10, 15, 9209 }, // U+0481 - { 10, 11, 167, 0, 11, 14, 9224 }, // U+0482 - { 7, 4, 0, -8, 13, 4, 9238 }, // U+0483 - { 7, 3, 0, -8, 13, 3, 9242 }, // U+0484 - { 3, 4, 0, -6, 14, 2, 9245 }, // U+0485 - { 3, 4, 0, -5, 14, 2, 9247 }, // U+0486 - { 7, 3, 0, -8, 13, 3, 9249 }, // U+0487 - { 17, 15, 0, -13, 12, 32, 9252 }, // U+0488 - { 16, 17, 0, -13, 13, 34, 9284 }, // U+0489 - { 12, 19, 198, 1, 16, 29, 9318 }, // U+048A - { 10, 16, 156, 1, 13, 20, 9347 }, // U+048B - { 11, 13, 183, 0, 13, 18, 9367 }, // U+048C - { 10, 12, 162, 0, 12, 15, 9385 }, // U+048D - { 9, 13, 168, 1, 13, 15, 9400 }, // U+048E - { 9, 14, 161, 1, 10, 16, 9415 }, // U+048F - { 8, 15, 152, 1, 15, 15, 9431 }, // U+0490 - { 6, 12, 116, 1, 12, 9, 9446 }, // U+0491 - { 10, 13, 152, -1, 13, 17, 9455 }, // U+0492 - { 7, 9, 116, 0, 9, 8, 9472 }, // U+0493 - { 9, 14, 172, 1, 13, 16, 9480 }, // U+0494 - { 8, 12, 143, 1, 9, 12, 9496 }, // U+0495 - { 17, 16, 258, 0, 13, 34, 9508 }, // U+0496 - { 14, 12, 219, 0, 9, 21, 9542 }, // U+0497 - { 10, 17, 164, 0, 13, 22, 9563 }, // U+0498 - { 8, 14, 130, 0, 10, 14, 9585 }, // U+0499 - { 11, 16, 187, 1, 13, 22, 9599 }, // U+049A - { 9, 12, 148, 1, 9, 14, 9621 }, // U+049B - { 10, 13, 176, 1, 13, 17, 9635 }, // U+049C - { 9, 9, 154, 1, 9, 11, 9652 }, // U+049D - { 14, 13, 221, 0, 13, 23, 9663 }, // U+04A0 - { 11, 9, 166, 0, 9, 13, 9686 }, // U+04A1 - { 12, 16, 200, 1, 13, 24, 9699 }, // U+04A2 - { 9, 12, 158, 1, 9, 14, 9723 }, // U+04A3 - { 15, 13, 260, 1, 13, 25, 9737 }, // U+04A4 - { 10, 9, 180, 1, 9, 12, 9762 }, // U+04A5 - { 17, 14, 294, 1, 13, 30, 9774 }, // U+04A6 - { 14, 12, 244, 1, 9, 21, 9804 }, // U+04A7 - { 16, 14, 261, 0, 13, 28, 9825 }, // U+04A8 - { 13, 11, 206, 0, 10, 18, 9853 }, // U+04A9 - { 12, 17, 194, 0, 13, 26, 9871 }, // U+04AA - { 9, 14, 150, 0, 10, 16, 9897 }, // U+04AB - { 10, 16, 170, 0, 13, 20, 9913 }, // U+04AC - { 8, 12, 124, 0, 9, 12, 9933 }, // U+04AD - { 11, 13, 179, 0, 13, 18, 9945 }, // U+04AE - { 9, 13, 147, 0, 9, 15, 9963 }, // U+04AF - { 11, 13, 179, 0, 13, 18, 9978 }, // U+04B0 - { 9, 13, 147, 0, 9, 15, 9996 }, // U+04B1 - { 12, 16, 185, 0, 13, 24, 10011 }, // U+04B2 - { 10, 12, 149, 0, 9, 15, 10035 }, // U+04B3 - { 15, 16, 243, 0, 13, 30, 10050 }, // U+04B4 - { 11, 12, 175, 0, 9, 17, 10080 }, // U+04B5 - { 11, 16, 188, 1, 13, 22, 10097 }, // U+04B6 - { 9, 12, 156, 1, 9, 14, 10119 }, // U+04B7 - { 10, 13, 185, 1, 13, 17, 10133 }, // U+04B8 - { 8, 9, 152, 1, 9, 9, 10150 }, // U+04B9 - { 10, 13, 185, 1, 13, 17, 10159 }, // U+04BA - { 8, 13, 155, 1, 13, 13, 10176 }, // U+04BB - { 13, 14, 223, 0, 13, 23, 10189 }, // U+04BC - { 12, 11, 188, 0, 10, 17, 10212 }, // U+04BD - { 13, 16, 223, 0, 13, 26, 10229 }, // U+04BE - { 12, 14, 188, 0, 10, 21, 10255 }, // U+04BF - { 2, 13, 70, 1, 13, 4, 10276 }, // U+04C0 - { 16, 16, 254, 0, 16, 32, 10280 }, // U+04C1 - { 13, 13, 214, 0, 13, 22, 10312 }, // U+04C2 - { 10, 16, 180, 1, 13, 20, 10334 }, // U+04C3 - { 8, 12, 154, 1, 9, 12, 10354 }, // U+04C4 - { 13, 16, 197, 0, 13, 26, 10366 }, // U+04C5 - { 10, 12, 152, 0, 9, 15, 10392 }, // U+04C6 - { 10, 17, 196, 1, 13, 22, 10407 }, // U+04C7 - { 8, 13, 155, 1, 9, 13, 10429 }, // U+04C8 - { 12, 16, 196, 1, 13, 24, 10442 }, // U+04C9 - { 10, 12, 155, 1, 9, 15, 10466 }, // U+04CA - { 11, 16, 188, 1, 13, 22, 10481 }, // U+04CB - { 9, 12, 156, 1, 9, 14, 10503 }, // U+04CC - { 15, 16, 238, 1, 13, 30, 10517 }, // U+04CD - { 13, 12, 203, 1, 9, 20, 10547 }, // U+04CE - { 2, 13, 63, 1, 13, 4, 10567 }, // U+04CF - { 11, 16, 182, 0, 16, 22, 10571 }, // U+04D0 - { 8, 14, 147, 0, 13, 14, 10593 }, // U+04D1 - { 11, 16, 182, 0, 16, 22, 10607 }, // U+04D2 - { 8, 14, 147, 0, 13, 14, 10629 }, // U+04D3 - { 16, 13, 263, 0, 13, 26, 10643 }, // U+04D4 - { 15, 11, 242, 0, 10, 21, 10669 }, // U+04D5 - { 8, 16, 159, 1, 16, 16, 10690 }, // U+04D6 - { 9, 14, 153, 0, 13, 16, 10706 }, // U+04D7 - { 12, 14, 198, 0, 13, 21, 10722 }, // U+04D8 - { 9, 11, 153, 0, 10, 13, 10743 }, // U+04D9 - { 12, 17, 198, 0, 16, 26, 10756 }, // U+04DA - { 9, 14, 153, 0, 13, 16, 10782 }, // U+04DB - { 16, 16, 254, 0, 16, 32, 10798 }, // U+04DC - { 13, 13, 214, 0, 13, 22, 10830 }, // U+04DD - { 10, 17, 164, 0, 16, 22, 10852 }, // U+04DE - { 8, 14, 130, 0, 13, 14, 10874 }, // U+04DF - { 10, 14, 164, 0, 13, 18, 10888 }, // U+04E0 - { 10, 14, 164, 0, 10, 18, 10906 }, // U+04E1 - { 10, 15, 198, 1, 15, 19, 10924 }, // U+04E2 - { 8, 12, 156, 1, 12, 12, 10943 }, // U+04E3 - { 10, 16, 198, 1, 16, 20, 10955 }, // U+04E4 - { 8, 13, 156, 1, 13, 13, 10975 }, // U+04E5 - { 12, 17, 203, 0, 16, 26, 10988 }, // U+04E6 - { 10, 14, 157, 0, 13, 18, 11014 }, // U+04E7 - { 12, 14, 203, 0, 13, 21, 11032 }, // U+04E8 - { 10, 11, 157, 0, 10, 14, 11053 }, // U+04E9 - { 12, 17, 203, 0, 16, 26, 11067 }, // U+04EA - { 10, 14, 157, 0, 13, 18, 11093 }, // U+04EB - { 12, 17, 194, 0, 16, 26, 11111 }, // U+04EC - { 9, 14, 150, 0, 13, 16, 11137 }, // U+04ED - { 10, 15, 165, 0, 15, 19, 11153 }, // U+04EE - { 9, 16, 147, 0, 12, 18, 11172 }, // U+04EF - { 10, 16, 165, 0, 16, 20, 11190 }, // U+04F0 - { 9, 17, 147, 0, 13, 20, 11210 }, // U+04F1 - { 10, 16, 165, 0, 16, 20, 11230 }, // U+04F2 - { 9, 17, 147, 0, 13, 20, 11250 }, // U+04F3 - { 10, 16, 185, 1, 16, 20, 11270 }, // U+04F4 - { 8, 13, 152, 1, 13, 13, 11290 }, // U+04F5 - { 8, 16, 152, 1, 13, 16, 11303 }, // U+04F6 - { 6, 12, 116, 1, 9, 9, 11319 }, // U+04F7 - { 13, 16, 232, 1, 16, 26, 11328 }, // U+04F8 - { 10, 13, 193, 1, 13, 17, 11354 }, // U+04F9 - { 10, 17, 157, 0, 13, 22, 11371 }, // U+04FA - { 7, 13, 116, 0, 9, 12, 11393 }, // U+04FB - { 12, 17, 186, 0, 13, 26, 11405 }, // U+04FC - { 9, 13, 137, 0, 9, 15, 11431 }, // U+04FD - { 11, 13, 180, 0, 13, 18, 11446 }, // U+04FE - { 9, 9, 143, 0, 9, 11, 11464 }, // U+04FF - { 9, 10, 168, 1, 10, 12, 11475 }, // U+05D0 - { 9, 10, 147, 0, 10, 12, 11487 }, // U+05D1 - { 6, 10, 112, 0, 10, 8, 11499 }, // U+05D2 - { 9, 10, 141, 0, 10, 12, 11507 }, // U+05D3 - { 9, 10, 170, 1, 10, 12, 11519 }, // U+05D4 - { 2, 10, 69, 1, 10, 3, 11531 }, // U+05D5 - { 6, 10, 101, 0, 10, 8, 11534 }, // U+05D6 - { 9, 10, 173, 1, 10, 12, 11542 }, // U+05D7 - { 9, 10, 172, 1, 10, 12, 11554 }, // U+05D8 - { 2, 5, 64, 1, 10, 2, 11566 }, // U+05D9 - { 8, 13, 138, 0, 10, 13, 11568 }, // U+05DA - { 9, 10, 142, 0, 10, 12, 11581 }, // U+05DB - { 8, 13, 126, 0, 13, 13, 11593 }, // U+05DC - { 9, 10, 173, 1, 10, 12, 11606 }, // U+05DD - { 10, 10, 174, 0, 10, 13, 11618 }, // U+05DE - { 2, 13, 69, 1, 10, 4, 11631 }, // U+05DF - { 5, 10, 93, 0, 10, 7, 11635 }, // U+05E0 - { 11, 10, 171, 0, 10, 14, 11642 }, // U+05E1 - { 9, 10, 158, 0, 10, 12, 11656 }, // U+05E2 - { 9, 13, 168, 1, 10, 15, 11668 }, // U+05E3 - { 9, 10, 166, 1, 10, 12, 11683 }, // U+05E4 - { 8, 13, 137, 0, 10, 13, 11695 }, // U+05E5 - { 9, 10, 145, 0, 10, 12, 11708 }, // U+05E6 - { 9, 13, 162, 1, 10, 15, 11720 }, // U+05E7 - { 8, 10, 140, 0, 10, 10, 11735 }, // U+05E8 - { 11, 10, 201, 1, 10, 14, 11745 }, // U+05E9 - { 9, 10, 177, 1, 10, 12, 11759 }, // U+05EA - { 11, 17, 182, 0, 13, 24, 11771 }, // U+1EA0 - { 8, 14, 147, 0, 10, 14, 11795 }, // U+1EA1 - { 11, 16, 182, 0, 16, 22, 11809 }, // U+1EA2 - { 8, 14, 147, 0, 13, 14, 11831 }, // U+1EA3 - { 11, 19, 182, 0, 19, 27, 11845 }, // U+1EA4 - { 9, 17, 147, 0, 16, 20, 11872 }, // U+1EA5 - { 11, 19, 182, 0, 19, 27, 11892 }, // U+1EA6 - { 8, 17, 147, 0, 16, 17, 11919 }, // U+1EA7 - { 11, 19, 182, 0, 19, 27, 11936 }, // U+1EA8 - { 8, 17, 147, 0, 16, 17, 11963 }, // U+1EA9 - { 11, 18, 182, 0, 18, 25, 11980 }, // U+1EAA - { 8, 16, 147, 0, 15, 16, 12005 }, // U+1EAB - { 11, 20, 182, 0, 16, 28, 12021 }, // U+1EAC - { 8, 17, 147, 0, 13, 17, 12049 }, // U+1EAD - { 11, 19, 182, 0, 19, 27, 12066 }, // U+1EAE - { 8, 17, 147, 0, 16, 17, 12093 }, // U+1EAF - { 11, 19, 182, 0, 19, 27, 12110 }, // U+1EB0 - { 8, 17, 147, 0, 16, 17, 12137 }, // U+1EB1 - { 11, 19, 182, 0, 19, 27, 12154 }, // U+1EB2 - { 8, 17, 147, 0, 16, 17, 12181 }, // U+1EB3 - { 11, 18, 182, 0, 18, 25, 12198 }, // U+1EB4 - { 8, 16, 147, 0, 15, 16, 12223 }, // U+1EB5 - { 11, 20, 182, 0, 16, 28, 12239 }, // U+1EB6 - { 8, 17, 147, 0, 13, 17, 12267 }, // U+1EB7 - { 8, 17, 159, 1, 13, 17, 12284 }, // U+1EB8 - { 9, 14, 153, 0, 10, 16, 12301 }, // U+1EB9 - { 8, 16, 159, 1, 16, 16, 12317 }, // U+1EBA - { 9, 14, 153, 0, 13, 16, 12333 }, // U+1EBB - { 8, 16, 159, 1, 16, 16, 12349 }, // U+1EBC - { 9, 14, 153, 0, 13, 16, 12365 }, // U+1EBD - { 9, 19, 159, 1, 19, 22, 12381 }, // U+1EBE - { 9, 17, 153, 0, 16, 20, 12403 }, // U+1EBF - { 8, 19, 159, 1, 19, 19, 12423 }, // U+1EC0 - { 9, 17, 153, 0, 16, 20, 12442 }, // U+1EC1 - { 8, 19, 159, 1, 19, 19, 12462 }, // U+1EC2 - { 9, 17, 153, 0, 16, 20, 12481 }, // U+1EC3 - { 8, 18, 159, 1, 18, 18, 12501 }, // U+1EC4 - { 9, 16, 153, 0, 15, 18, 12519 }, // U+1EC5 - { 8, 20, 159, 1, 16, 20, 12537 }, // U+1EC6 - { 9, 17, 153, 0, 13, 20, 12557 }, // U+1EC7 - { 4, 16, 70, 0, 16, 8, 12577 }, // U+1EC8 - { 4, 13, 63, 0, 13, 7, 12585 }, // U+1EC9 - { 3, 17, 70, 1, 13, 7, 12592 }, // U+1ECA - { 3, 17, 63, 0, 13, 7, 12599 }, // U+1ECB - { 12, 17, 203, 0, 13, 26, 12606 }, // U+1ECC - { 10, 14, 157, 0, 10, 18, 12632 }, // U+1ECD - { 12, 17, 203, 0, 16, 26, 12650 }, // U+1ECE - { 10, 14, 157, 0, 13, 18, 12676 }, // U+1ECF - { 12, 20, 203, 0, 19, 30, 12694 }, // U+1ED0 - { 10, 17, 157, 0, 16, 22, 12724 }, // U+1ED1 - { 12, 20, 203, 0, 19, 30, 12746 }, // U+1ED2 - { 10, 17, 157, 0, 16, 22, 12776 }, // U+1ED3 - { 12, 20, 203, 0, 19, 30, 12798 }, // U+1ED4 - { 10, 17, 157, 0, 16, 22, 12828 }, // U+1ED5 - { 12, 19, 203, 0, 18, 29, 12850 }, // U+1ED6 - { 10, 16, 157, 0, 15, 20, 12879 }, // U+1ED7 - { 12, 20, 203, 0, 16, 30, 12899 }, // U+1ED8 - { 10, 17, 157, 0, 13, 22, 12929 }, // U+1ED9 - { 12, 17, 202, 0, 16, 26, 12951 }, // U+1EDA - { 10, 14, 156, 0, 13, 18, 12977 }, // U+1EDB - { 12, 17, 202, 0, 16, 26, 12995 }, // U+1EDC - { 10, 14, 156, 0, 13, 18, 13021 }, // U+1EDD - { 12, 17, 202, 0, 16, 26, 13039 }, // U+1EDE - { 10, 14, 156, 0, 13, 18, 13065 }, // U+1EDF - { 12, 17, 202, 0, 16, 26, 13083 }, // U+1EE0 - { 10, 14, 156, 0, 13, 18, 13109 }, // U+1EE1 - { 12, 18, 202, 0, 14, 27, 13127 }, // U+1EE2 - { 10, 15, 156, 0, 11, 19, 13154 }, // U+1EE3 - { 10, 17, 196, 1, 13, 22, 13173 }, // U+1EE4 - { 8, 13, 155, 1, 9, 13, 13195 }, // U+1EE5 - { 10, 17, 196, 1, 16, 22, 13208 }, // U+1EE6 - { 8, 14, 155, 1, 13, 14, 13230 }, // U+1EE7 - { 12, 17, 196, 1, 16, 26, 13244 }, // U+1EE8 - { 9, 14, 164, 1, 13, 16, 13270 }, // U+1EE9 - { 12, 17, 196, 1, 16, 26, 13286 }, // U+1EEA - { 9, 14, 164, 1, 13, 16, 13312 }, // U+1EEB - { 12, 17, 196, 1, 16, 26, 13328 }, // U+1EEC - { 9, 14, 164, 1, 13, 16, 13354 }, // U+1EED - { 12, 17, 196, 1, 16, 26, 13370 }, // U+1EEE - { 9, 14, 164, 1, 13, 16, 13396 }, // U+1EEF - { 12, 18, 196, 1, 14, 27, 13412 }, // U+1EF0 - { 9, 15, 164, 1, 11, 17, 13439 }, // U+1EF1 - { 11, 16, 179, 0, 16, 22, 13456 }, // U+1EF2 - { 9, 17, 147, 0, 13, 20, 13478 }, // U+1EF3 - { 11, 17, 179, 0, 13, 24, 13498 }, // U+1EF4 - { 9, 13, 147, 0, 9, 15, 13522 }, // U+1EF5 - { 11, 16, 179, 0, 16, 22, 13537 }, // U+1EF6 - { 9, 17, 147, 0, 13, 20, 13559 }, // U+1EF7 - { 11, 16, 179, 0, 16, 22, 13579 }, // U+1EF8 - { 9, 17, 147, 0, 13, 20, 13601 }, // U+1EF9 - { 0, 0, 146, 0, 0, 0, 13621 }, // U+2000 - { 0, 0, 291, 0, 0, 0, 13621 }, // U+2001 - { 0, 0, 133, 0, 0, 0, 13621 }, // U+2002 - { 0, 0, 267, 0, 0, 0, 13621 }, // U+2003 - { 0, 0, 89, 0, 0, 0, 13621 }, // U+2004 - { 0, 0, 67, 0, 0, 0, 13621 }, // U+2005 - { 0, 0, 44, 0, 0, 0, 13621 }, // U+2006 - { 0, 0, 173, 0, 0, 0, 13621 }, // U+2007 - { 0, 0, 73, 0, 0, 0, 13621 }, // U+2008 - { 0, 0, 47, 0, 0, 0, 13621 }, // U+2009 - { 0, 0, 22, 0, 0, 0, 13621 }, // U+200A - { 0, 0, 0, 0, 0, 0, 13621 }, // U+200B - { 0, 0, 0, 0, 0, 0, 13621 }, // U+200C - { 0, 0, 0, 0, 0, 0, 13621 }, // U+200D - { 0, 0, 0, 0, 0, 0, 13621 }, // U+200E - { 0, 0, 0, 0, 0, 0, 13621 }, // U+200F - { 6, 2, 94, 0, 6, 2, 13621 }, // U+2010 - { 6, 2, 94, 0, 6, 2, 13623 }, // U+2011 - { 10, 3, 173, 0, 7, 4, 13625 }, // U+2012 - { 9, 2, 133, 0, 6, 3, 13629 }, // U+2013 - { 17, 2, 267, 0, 6, 5, 13632 }, // U+2014 - { 17, 2, 267, 0, 6, 5, 13637 }, // U+2015 - { 4, 21, 126, 2, 17, 11, 13642 }, // U+2016 - { 9, 4, 119, -1, 0, 5, 13653 }, // U+2017 - { 3, 6, 65, 1, 13, 3, 13658 }, // U+2018 - { 3, 6, 65, 1, 13, 3, 13661 }, // U+2019 - { 4, 6, 60, 0, 3, 3, 13664 }, // U+201A - { 3, 6, 65, 0, 13, 3, 13667 }, // U+201B - { 6, 6, 111, 1, 13, 5, 13670 }, // U+201C - { 6, 6, 111, 1, 13, 5, 13675 }, // U+201D - { 7, 6, 107, 0, 3, 6, 13680 }, // U+201E - { 6, 6, 111, 0, 13, 5, 13686 }, // U+201F - { 8, 10, 129, 0, 13, 10, 13691 }, // U+2020 - { 8, 13, 129, 0, 13, 13, 13701 }, // U+2021 - { 7, 6, 146, 1, 8, 6, 13714 }, // U+2022 - { 6, 6, 146, 2, 8, 5, 13720 }, // U+2023 - { 3, 4, 73, 1, 3, 2, 13725 }, // U+2024 - { 7, 4, 146, 1, 3, 4, 13727 }, // U+2025 - { 12, 4, 218, 1, 3, 6, 13731 }, // U+2026 - { 3, 4, 73, 1, 8, 2, 13737 }, // U+2027 - { 0, 0, 0, 0, 0, 0, 13739 }, // U+202A - { 0, 0, 0, 0, 0, 0, 13739 }, // U+202B - { 0, 0, 0, 0, 0, 0, 13739 }, // U+202C - { 0, 0, 0, 0, 0, 0, 13739 }, // U+202D - { 0, 0, 0, 0, 0, 0, 13739 }, // U+202E - { 0, 0, 47, 0, 0, 0, 13739 }, // U+202F - { 19, 14, 347, 1, 13, 34, 13739 }, // U+2030 - { 25, 14, 440, 1, 13, 44, 13773 }, // U+2031 - { 3, 6, 57, 0, 13, 3, 13817 }, // U+2032 - { 7, 6, 114, 0, 13, 6, 13820 }, // U+2033 - { 10, 6, 171, 0, 13, 8, 13826 }, // U+2034 - { 3, 6, 57, 0, 13, 3, 13834 }, // U+2035 - { 7, 6, 114, 0, 13, 6, 13837 }, // U+2036 - { 10, 6, 171, 0, 13, 8, 13843 }, // U+2037 - { 7, 5, 119, 0, 0, 5, 13851 }, // U+2038 - { 6, 8, 99, 0, 9, 6, 13856 }, // U+2039 - { 6, 8, 99, 0, 9, 6, 13862 }, // U+203A - { 10, 10, 168, 0, 11, 13, 13868 }, // U+203B - { 7, 14, 145, 1, 13, 13, 13881 }, // U+203C - { 8, 14, 134, 0, 13, 14, 13894 }, // U+203D - { 9, 2, 142, 0, 14, 3, 13908 }, // U+203E - { 13, 4, 216, 0, 0, 7, 13911 }, // U+203F - { 13, 4, 216, 0, 13, 7, 13918 }, // U+2040 - { 7, 10, 119, 0, 5, 9, 13925 }, // U+2041 - { 10, 10, 191, 1, 11, 13, 13934 }, // U+2042 - { 5, 2, 146, 2, 6, 2, 13947 }, // U+2043 - { 11, 13, 47, -4, 13, 18, 13949 }, // U+2044 - { 4, 16, 93, 1, 13, 8, 13967 }, // U+2045 - { 5, 16, 93, 0, 13, 10, 13975 }, // U+2046 - { 17, 14, 274, 0, 13, 30, 13985 }, // U+2047 - { 12, 14, 210, 0, 13, 21, 14015 }, // U+2048 - { 12, 14, 210, 1, 13, 21, 14036 }, // U+2049 - { 7, 13, 116, 0, 9, 12, 14057 }, // U+204A - { 9, 13, 158, 1, 13, 15, 14069 }, // U+204B - { 8, 8, 156, 1, 9, 8, 14084 }, // U+204C - { 8, 8, 156, 1, 9, 8, 14092 }, // U+204D - { 8, 8, 132, 0, 7, 8, 14100 }, // U+204E - { 3, 12, 76, 1, 9, 5, 14108 }, // U+204F - { 13, 17, 215, 0, 13, 28, 14113 }, // U+2050 - { 8, 16, 142, 1, 13, 16, 14141 }, // U+2051 - { 11, 13, 190, 0, 13, 18, 14157 }, // U+2052 - { 10, 4, 191, 1, 8, 5, 14175 }, // U+2053 - { 13, 4, 216, 0, 0, 7, 14180 }, // U+2054 - { 7, 8, 144, 1, 8, 7, 14187 }, // U+2055 - { 14, 6, 228, 0, 13, 11, 14194 }, // U+2057 - { 0, 0, 73, 0, 0, 0, 14205 }, // U+205F - { 0, 0, 0, 0, 0, 0, 14205 }, // U+2060 - { 0, 0, 0, 0, 0, 0, 14205 }, // U+2061 - { 0, 0, 0, 0, 0, 0, 14205 }, // U+2062 - { 0, 0, 0, 0, 0, 0, 14205 }, // U+2063 - { 0, 0, 0, 0, 0, 0, 14205 }, // U+2064 - { 0, 0, 0, 0, 0, 0, 14205 }, // U+2066 - { 0, 0, 0, 0, 0, 0, 14205 }, // U+2067 - { 0, 0, 0, 0, 0, 0, 14205 }, // U+2068 - { 0, 0, 0, 0, 0, 0, 14205 }, // U+2069 - { 0, 0, 0, 0, 0, 0, 14205 }, // U+206A - { 0, 0, 0, 0, 0, 0, 14205 }, // U+206B - { 0, 0, 0, 0, 0, 0, 14205 }, // U+206C - { 0, 0, 0, 0, 0, 0, 14205 }, // U+206D - { 0, 0, 0, 0, 0, 0, 14205 }, // U+206E - { 0, 0, 0, 0, 0, 0, 14205 }, // U+206F - { 7, 7, 120, 0, 14, 7, 14205 }, // U+2070 - { 3, 8, 51, 0, 15, 3, 14212 }, // U+2071 - { 7, 7, 120, 0, 14, 7, 14215 }, // U+2074 - { 7, 7, 114, 0, 14, 7, 14222 }, // U+2075 - { 7, 7, 116, 0, 14, 7, 14229 }, // U+2076 - { 6, 7, 105, 0, 14, 6, 14236 }, // U+2077 - { 7, 7, 117, 0, 14, 7, 14242 }, // U+2078 - { 7, 7, 116, 0, 14, 7, 14249 }, // U+2079 - { 6, 5, 114, 1, 13, 4, 14256 }, // U+207A - { 5, 2, 114, 1, 11, 2, 14260 }, // U+207B - { 5, 4, 114, 1, 12, 3, 14262 }, // U+207C - { 3, 9, 69, 1, 15, 4, 14265 }, // U+207D - { 4, 9, 69, 0, 15, 5, 14269 }, // U+207E - { 7, 6, 113, 0, 13, 6, 14274 }, // U+207F - { 7, 7, 120, 0, 4, 7, 14280 }, // U+2080 - { 4, 7, 79, 0, 4, 4, 14287 }, // U+2081 - { 7, 7, 114, 0, 4, 7, 14291 }, // U+2082 - { 7, 7, 116, 0, 4, 7, 14298 }, // U+2083 - { 7, 7, 120, 0, 4, 7, 14305 }, // U+2084 - { 7, 7, 114, 0, 4, 7, 14312 }, // U+2085 - { 7, 7, 116, 0, 4, 7, 14319 }, // U+2086 - { 6, 7, 105, 0, 4, 6, 14326 }, // U+2087 - { 7, 7, 117, 0, 4, 7, 14332 }, // U+2088 - { 7, 7, 116, 0, 4, 7, 14339 }, // U+2089 - { 6, 5, 114, 1, 3, 4, 14346 }, // U+208A - { 5, 2, 114, 1, 1, 2, 14350 }, // U+208B - { 5, 4, 114, 1, 2, 3, 14352 }, // U+208C - { 3, 9, 69, 1, 5, 4, 14355 }, // U+208D - { 4, 9, 69, 0, 5, 5, 14359 }, // U+208E - { 6, 6, 104, 0, 3, 5, 14364 }, // U+2090 - { 7, 6, 108, 0, 3, 6, 14369 }, // U+2091 - { 7, 6, 111, 0, 3, 6, 14375 }, // U+2092 - { 7, 6, 104, 0, 3, 6, 14381 }, // U+2093 - { 7, 8, 118, 0, 4, 7, 14387 }, // U+2094 - { 7, 7, 113, 0, 4, 7, 14394 }, // U+2095 - { 7, 7, 103, 0, 4, 7, 14401 }, // U+2096 - { 3, 7, 51, 0, 4, 3, 14408 }, // U+2097 - { 10, 6, 162, 0, 3, 8, 14411 }, // U+2098 - { 7, 6, 113, 0, 3, 6, 14419 }, // U+2099 - { 7, 8, 113, 0, 3, 7, 14425 }, // U+209A - { 6, 6, 101, 0, 3, 5, 14432 }, // U+209B - { 4, 7, 65, 0, 4, 4, 14437 }, // U+209C - { 10, 14, 192, 1, 13, 18, 14441 }, // U+20A0 - { 12, 16, 194, 0, 14, 24, 14459 }, // U+20A1 - { 12, 14, 193, 0, 13, 21, 14483 }, // U+20A2 - { 11, 13, 160, -1, 13, 18, 14504 }, // U+20A3 - { 10, 13, 161, 0, 13, 17, 14522 }, // U+20A4 - { 12, 13, 227, 1, 11, 20, 14539 }, // U+20A5 - { 13, 13, 203, 0, 13, 22, 14559 }, // U+20A6 - { 15, 14, 251, 1, 13, 27, 14581 }, // U+20A7 - { 17, 14, 296, 1, 13, 30, 14608 }, // U+20A8 - { 16, 13, 252, 0, 13, 26, 14638 }, // U+20A9 - { 12, 10, 215, 1, 10, 15, 14664 }, // U+20AA - { 10, 17, 161, 0, 13, 22, 14679 }, // U+20AB - { 11, 14, 176, 0, 13, 20, 14701 }, // U+20AC - { 12, 13, 187, 0, 13, 20, 14721 }, // U+20AD - { 10, 13, 170, 0, 13, 17, 14741 }, // U+20AE - { 23, 18, 367, 0, 14, 52, 14758 }, // U+20AF - { 11, 13, 176, 0, 13, 18, 14810 }, // U+20B1 - { 12, 16, 198, 0, 14, 24, 14828 }, // U+20B2 - { 11, 13, 182, 0, 13, 18, 14852 }, // U+20B3 - { 10, 14, 167, 0, 13, 18, 14870 }, // U+20B4 - { 12, 16, 194, 0, 14, 24, 14888 }, // U+20B5 - { 10, 13, 170, 0, 13, 17, 14912 }, // U+20B8 - { 9, 13, 142, 0, 13, 15, 14929 }, // U+20B9 - { 9, 14, 156, 0, 13, 16, 14944 }, // U+20BA - { 10, 10, 192, 1, 10, 13, 14960 }, // U+20BC - { 11, 13, 185, 0, 13, 18, 14973 }, // U+20BD - { 10, 13, 191, 1, 13, 17, 14991 }, // U+20BE - { 9, 16, 174, 1, 14, 18, 15008 }, // U+20BF - { 13, 11, 253, 1, 11, 18, 15026 }, // U+2190 - { 12, 13, 221, 1, 13, 20, 15044 }, // U+2191 - { 13, 11, 253, 2, 11, 18, 15064 }, // U+2192 - { 12, 14, 221, 1, 13, 21, 15082 }, // U+2193 - { 20, 11, 356, 1, 11, 28, 15103 }, // U+2194 - { 12, 20, 221, 1, 15, 30, 15131 }, // U+2195 - { 10, 10, 204, 1, 9, 13, 15161 }, // U+2196 - { 10, 10, 204, 1, 9, 13, 15174 }, // U+2197 - { 10, 10, 204, 1, 10, 13, 15187 }, // U+2198 - { 10, 10, 204, 1, 10, 13, 15200 }, // U+2199 - { 17, 13, 303, 1, 13, 28, 15213 }, // U+21A9 - { 17, 13, 303, 1, 13, 28, 15241 }, // U+21AA - { 14, 13, 261, 1, 13, 23, 15269 }, // U+21B0 - { 14, 13, 261, 1, 13, 23, 15292 }, // U+21B1 - { 9, 12, 219, 2, 12, 14, 15315 }, // U+21B2 - { 14, 12, 261, 1, 13, 21, 15329 }, // U+21B3 - { 12, 14, 220, 1, 13, 21, 15350 }, // U+21B4 - { 14, 12, 261, 1, 13, 21, 15371 }, // U+21B5 - { 14, 11, 219, 0, 11, 20, 15392 }, // U+21B6 - { 14, 11, 219, 0, 11, 20, 15412 }, // U+21B7 - { 13, 15, 253, 1, 15, 25, 15432 }, // U+21BA - { 13, 15, 253, 1, 15, 25, 15457 }, // U+21BB - { 13, 13, 219, 0, 12, 22, 15482 }, // U+21C4 - { 13, 13, 219, 0, 12, 22, 15504 }, // U+21C6 - { 13, 12, 253, 1, 11, 20, 15526 }, // U+21D0 - { 13, 12, 253, 2, 11, 20, 15546 }, // U+21D2 - { 20, 12, 356, 1, 11, 30, 15566 }, // U+21D4 - { 8, 13, 160, 1, 13, 13, 15596 }, // U+21DE - { 8, 14, 160, 1, 13, 14, 15609 }, // U+21DF - { 15, 8, 269, 1, 10, 15, 15623 }, // U+21E4 - { 15, 8, 269, 1, 10, 15, 15638 }, // U+21E5 - { 15, 14, 273, 1, 14, 27, 15653 }, // U+21E7 - { 15, 17, 273, 1, 14, 32, 15680 }, // U+21EA - { 9, 14, 167, 1, 13, 16, 15712 }, // U+2202 - { 11, 11, 181, 0, 10, 16, 15728 }, // U+2205 - { 11, 13, 179, 0, 13, 18, 15744 }, // U+2206 - { 10, 15, 191, 1, 13, 19, 15762 }, // U+220F - { 10, 16, 167, 0, 13, 20, 15781 }, // U+2211 - { 9, 2, 175, 1, 6, 3, 15801 }, // U+2212 - { 10, 12, 39, -4, 12, 15, 15804 }, // U+2215 - { 10, 13, 166, 0, 13, 17, 15819 }, // U+221A - { 15, 8, 247, 0, 9, 15, 15836 }, // U+221E - { 7, 17, 96, -1, 13, 15, 15851 }, // U+222B - { 3, 10, 73, 1, 11, 4, 15866 }, // U+2236 - { 9, 8, 175, 1, 9, 9, 15870 }, // U+2248 - { 9, 10, 175, 1, 10, 12, 15879 }, // U+2260 - { 9, 10, 175, 1, 10, 12, 15891 }, // U+2264 - { 9, 10, 175, 1, 10, 12, 15903 }, // U+2265 - { 23, 24, 371, 0, 18, 69, 15915 }, // U+2295 - { 23, 24, 371, 0, 18, 69, 15984 }, // U+2296 - { 23, 24, 371, 0, 18, 69, 16053 }, // U+2297 - { 23, 24, 371, 0, 18, 69, 16122 }, // U+2298 - { 9, 13, 151, 0, 13, 15, 16191 }, // U+FB01 - { 10, 13, 159, 0, 13, 17, 16206 }, // U+FB02 + { 4, 5, 0, -2, 12, 3, 6884 }, // U+0312 + { 3, 6, 0, 3, 9, 3, 6887 }, // U+0313 + { 3, 6, 0, 3, 9, 3, 6890 }, // U+0315 + { 4, 4, 0, -4, 12, 2, 6893 }, // U+031B + { 3, 3, 62, 0, -1, 2, 6895 }, // U+0323 + { 3, 4, 0, -7, -1, 2, 6897 }, // U+0326 + { 4, 5, 0, 1, 1, 3, 6899 }, // U+0327 + { 4, 5, 0, 0, 1, 3, 6902 }, // U+0328 + { 6, 3, 0, 0, -1, 3, 6905 }, // U+032C + { 8, 12, 0, -9, 10, 12, 6908 }, // U+0337 + { 10, 14, 0, -10, 13, 18, 6920 }, // U+0338 + { 5, 3, 0, -7, 13, 2, 6938 }, // U+0342 + { 3, 5, 0, 0, 11, 2, 6940 }, // U+0343 + { 6, 4, 0, -8, 14, 3, 6942 }, // U+0346 + { 6, 4, 0, -8, -2, 3, 6945 }, // U+0347 + { 5, 4, 0, -7, 0, 3, 6948 }, // U+0348 + { 4, 5, 0, -7, 0, 3, 6951 }, // U+0349 + { 6, 4, 0, -8, 13, 3, 6954 }, // U+034A + { 7, 7, 0, -9, 16, 7, 6957 }, // U+034B + { 7, 4, 0, -8, 14, 4, 6964 }, // U+034C + { 8, 5, 0, -9, 0, 5, 6968 }, // U+034D + { 5, 5, 0, -7, 0, 4, 6973 }, // U+034E + { 0, 0, 0, 0, 0, 0, 6977 }, // U+034F + { 7, 7, 0, -8, 17, 7, 6977 }, // U+0350 + { 3, 4, 0, -6, 13, 2, 6984 }, // U+0351 + { 6, 5, 0, -8, 14, 4, 6986 }, // U+0352 + { 5, 5, 0, -7, 0, 4, 6990 }, // U+0353 + { 5, 5, 0, -7, 0, 4, 6994 }, // U+0354 + { 5, 5, 0, -6, 0, 4, 6998 }, // U+0355 + { 9, 5, 0, -8, 0, 6, 7002 }, // U+0356 + { 3, 5, 0, 0, 14, 2, 7008 }, // U+0357 + { 3, 3, 0, 0, 13, 2, 7010 }, // U+0358 + { 4, 5, 0, -7, 0, 3, 7012 }, // U+0359 + { 7, 4, 0, -8, 0, 4, 7015 }, // U+035A + { 4, 5, 0, -7, 14, 3, 7019 }, // U+035B + { 12, 4, 0, -6, 0, 6, 7022 }, // U+035C + { 12, 4, 0, -6, 13, 6, 7028 }, // U+035D + { 11, 3, 0, -10, 13, 5, 7034 }, // U+035E + { 11, 3, 0, -10, -1, 5, 7039 }, // U+035F + { 16, 3, 0, -8, 13, 6, 7044 }, // U+0360 + { 12, 4, 0, -6, 13, 6, 7050 }, // U+0361 + { 13, 5, 0, -6, 0, 9, 7056 }, // U+0362 + { 5, 6, 0, -7, 15, 4, 7065 }, // U+0363 + { 5, 6, 0, -7, 15, 4, 7069 }, // U+0364 + { 2, 7, 0, -6, 16, 2, 7073 }, // U+0365 + { 5, 6, 0, -7, 15, 4, 7075 }, // U+0366 + { 5, 6, 0, -7, 15, 4, 7079 }, // U+0367 + { 5, 6, 0, -7, 15, 4, 7083 }, // U+0368 + { 5, 8, 0, -7, 17, 5, 7087 }, // U+0369 + { 5, 8, 0, -7, 17, 5, 7092 }, // U+036A + { 7, 6, 0, -8, 15, 6, 7097 }, // U+036B + { 3, 6, 0, -6, 15, 3, 7103 }, // U+036C + { 4, 7, 0, 0, 16, 4, 7106 }, // U+036D + { 5, 6, 0, -7, 15, 4, 7110 }, // U+036E + { 5, 6, 0, -7, 15, 4, 7114 }, // U+036F + { 8, 16, 159, 1, 16, 16, 7118 }, // U+0400 + { 8, 16, 159, 1, 16, 16, 7134 }, // U+0401 + { 14, 14, 222, 0, 13, 25, 7150 }, // U+0402 + { 8, 16, 152, 1, 16, 16, 7175 }, // U+0403 + { 12, 14, 194, 0, 13, 21, 7191 }, // U+0404 + { 10, 14, 170, 0, 13, 18, 7212 }, // U+0405 + { 2, 13, 70, 1, 13, 4, 7230 }, // U+0406 + { 6, 16, 70, -1, 16, 12, 7234 }, // U+0407 + { 8, 14, 150, 0, 13, 14, 7246 }, // U+0408 + { 18, 13, 295, 0, 13, 30, 7260 }, // U+0409 + { 17, 13, 298, 1, 13, 28, 7290 }, // U+040A + { 14, 13, 234, 0, 13, 23, 7318 }, // U+040B + { 10, 16, 177, 1, 16, 20, 7341 }, // U+040C + { 10, 16, 198, 1, 16, 20, 7361 }, // U+040D + { 10, 16, 165, 0, 16, 20, 7381 }, // U+040E + { 10, 16, 191, 1, 13, 20, 7401 }, // U+040F + { 11, 13, 182, 0, 13, 18, 7421 }, // U+0410 + { 9, 13, 170, 1, 13, 15, 7439 }, // U+0411 + { 9, 13, 174, 1, 13, 15, 7454 }, // U+0412 + { 8, 13, 152, 1, 13, 13, 7469 }, // U+0413 + { 13, 16, 215, 0, 13, 26, 7482 }, // U+0414 + { 8, 13, 159, 1, 13, 13, 7508 }, // U+0415 + { 16, 13, 254, 0, 13, 26, 7521 }, // U+0416 + { 10, 14, 164, 0, 13, 18, 7547 }, // U+0417 + { 10, 13, 198, 1, 13, 17, 7565 }, // U+0418 + { 10, 16, 198, 1, 16, 20, 7582 }, // U+0419 + { 10, 13, 177, 1, 13, 17, 7602 }, // U+041A + { 11, 13, 197, 0, 13, 18, 7619 }, // U+041B + { 13, 13, 238, 1, 13, 22, 7637 }, // U+041C + { 10, 13, 196, 1, 13, 17, 7659 }, // U+041D + { 12, 14, 203, 0, 13, 21, 7676 }, // U+041E + { 10, 13, 196, 1, 13, 17, 7697 }, // U+041F + { 9, 13, 169, 1, 13, 15, 7714 }, // U+0420 + { 12, 14, 194, 0, 13, 21, 7729 }, // U+0421 + { 10, 13, 170, 0, 13, 17, 7750 }, // U+0422 + { 10, 13, 165, 0, 13, 17, 7767 }, // U+0423 + { 13, 14, 220, 0, 13, 23, 7784 }, // U+0424 + { 11, 13, 180, 0, 13, 18, 7807 }, // U+0425 + { 11, 16, 190, 1, 13, 22, 7825 }, // U+0426 + { 10, 13, 185, 1, 13, 17, 7847 }, // U+0427 + { 14, 13, 253, 1, 13, 23, 7864 }, // U+0428 + { 15, 16, 257, 1, 13, 30, 7887 }, // U+0429 + { 13, 13, 216, 0, 13, 22, 7917 }, // U+042A + { 13, 13, 232, 1, 13, 22, 7939 }, // U+042B + { 9, 13, 171, 1, 13, 15, 7961 }, // U+042C + { 12, 14, 194, 0, 13, 21, 7976 }, // U+042D + { 15, 14, 270, 1, 13, 27, 7997 }, // U+042E + { 10, 13, 171, 0, 13, 17, 8024 }, // U+042F + { 8, 11, 147, 0, 10, 11, 8041 }, // U+0430 + { 9, 14, 156, 0, 13, 16, 8052 }, // U+0431 + { 8, 9, 148, 1, 9, 9, 8068 }, // U+0432 + { 6, 9, 116, 1, 9, 7, 8077 }, // U+0433 + { 10, 12, 162, 0, 9, 15, 8084 }, // U+0434 + { 9, 11, 153, 0, 10, 13, 8099 }, // U+0435 + { 13, 9, 214, 0, 9, 15, 8112 }, // U+0436 + { 8, 11, 130, 0, 10, 11, 8127 }, // U+0437 + { 8, 9, 156, 1, 9, 9, 8138 }, // U+0438 + { 8, 13, 156, 1, 13, 13, 8147 }, // U+0439 + { 8, 9, 142, 1, 9, 9, 8160 }, // U+043A + { 9, 9, 152, 0, 9, 11, 8169 }, // U+043B + { 11, 9, 203, 1, 9, 13, 8180 }, // U+043C + { 8, 9, 155, 1, 9, 9, 8193 }, // U+043D + { 10, 11, 157, 0, 10, 14, 8202 }, // U+043E + { 8, 9, 155, 1, 9, 9, 8216 }, // U+043F + { 9, 14, 161, 1, 10, 16, 8225 }, // U+0440 + { 9, 11, 150, 0, 10, 13, 8241 }, // U+0441 + { 8, 9, 124, 0, 9, 9, 8254 }, // U+0442 + { 9, 13, 147, 0, 9, 15, 8263 }, // U+0443 + { 11, 17, 181, 0, 13, 24, 8278 }, // U+0444 + { 9, 9, 143, 0, 9, 11, 8302 }, // U+0445 + { 9, 12, 157, 1, 9, 14, 8313 }, // U+0446 + { 8, 9, 152, 1, 9, 9, 8327 }, // U+0447 + { 12, 9, 217, 1, 9, 14, 8336 }, // U+0448 + { 13, 12, 221, 1, 9, 20, 8350 }, // U+0449 + { 10, 9, 170, 0, 9, 12, 8370 }, // U+044A + { 10, 9, 193, 1, 9, 12, 8382 }, // U+044B + { 8, 9, 147, 1, 9, 9, 8394 }, // U+044C + { 9, 11, 150, 0, 10, 13, 8403 }, // U+044D + { 13, 11, 221, 1, 10, 18, 8416 }, // U+044E + { 8, 9, 146, 0, 9, 9, 8434 }, // U+044F + { 9, 14, 153, 0, 13, 16, 8443 }, // U+0450 + { 9, 14, 153, 0, 13, 16, 8459 }, // U+0451 + { 9, 17, 155, 0, 13, 20, 8475 }, // U+0452 + { 6, 13, 116, 1, 13, 10, 8495 }, // U+0453 + { 9, 11, 150, 0, 10, 13, 8505 }, // U+0454 + { 8, 11, 138, 0, 10, 11, 8518 }, // U+0455 + { 3, 13, 63, 0, 13, 5, 8529 }, // U+0456 + { 6, 13, 63, -1, 13, 10, 8534 }, // U+0457 + { 4, 17, 63, -1, 13, 9, 8544 }, // U+0458 + { 15, 9, 245, 0, 9, 17, 8553 }, // U+0459 + { 14, 9, 238, 1, 9, 16, 8570 }, // U+045A + { 9, 13, 155, 0, 13, 15, 8586 }, // U+045B + { 8, 13, 142, 1, 13, 13, 8601 }, // U+045C + { 8, 13, 156, 1, 13, 13, 8614 }, // U+045D + { 9, 17, 147, 0, 13, 20, 8627 }, // U+045E + { 8, 12, 155, 1, 9, 12, 8647 }, // U+045F + { 13, 14, 237, 1, 13, 23, 8659 }, // U+0460 + { 12, 10, 213, 1, 9, 15, 8682 }, // U+0461 + { 11, 13, 183, 0, 13, 18, 8697 }, // U+0462 + { 10, 13, 162, 0, 13, 17, 8715 }, // U+0463 + { 14, 14, 253, 1, 13, 25, 8732 }, // U+0464 + { 12, 11, 206, 1, 10, 17, 8757 }, // U+0465 + { 11, 13, 182, 0, 13, 18, 8774 }, // U+0466 + { 10, 9, 151, 0, 9, 12, 8792 }, // U+0467 + { 15, 13, 248, 1, 13, 25, 8804 }, // U+0468 + { 12, 9, 205, 1, 9, 14, 8829 }, // U+0469 + { 12, 13, 225, 1, 13, 20, 8843 }, // U+046A + { 11, 9, 185, 0, 9, 13, 8863 }, // U+046B + { 17, 13, 304, 1, 13, 28, 8876 }, // U+046C + { 15, 9, 254, 1, 9, 17, 8904 }, // U+046D + { 9, 20, 143, 0, 16, 23, 8921 }, // U+046E + { 8, 17, 137, 0, 13, 17, 8944 }, // U+046F + { 13, 13, 233, 1, 13, 22, 8961 }, // U+0470 + { 11, 16, 205, 1, 12, 22, 8983 }, // U+0471 + { 12, 14, 203, 0, 13, 21, 9005 }, // U+0472 + { 10, 11, 157, 0, 10, 14, 9026 }, // U+0473 + { 12, 13, 186, 0, 13, 20, 9040 }, // U+0474 + { 10, 9, 156, 0, 9, 12, 9060 }, // U+0475 + { 12, 16, 186, 0, 16, 24, 9072 }, // U+0476 + { 10, 13, 156, 0, 13, 17, 9096 }, // U+0477 + { 22, 17, 350, 0, 13, 47, 9113 }, // U+0478 + { 19, 14, 299, 0, 10, 34, 9160 }, // U+0479 + { 11, 13, 189, 0, 13, 18, 9194 }, // U+0480 + { 9, 13, 148, 0, 10, 15, 9212 }, // U+0481 + { 10, 11, 167, 0, 11, 14, 9227 }, // U+0482 + { 7, 4, 0, -8, 13, 4, 9241 }, // U+0483 + { 7, 3, 0, -8, 13, 3, 9245 }, // U+0484 + { 3, 4, 0, -6, 14, 2, 9248 }, // U+0485 + { 3, 4, 0, -5, 14, 2, 9250 }, // U+0486 + { 7, 3, 0, -8, 13, 3, 9252 }, // U+0487 + { 17, 15, 0, -13, 12, 32, 9255 }, // U+0488 + { 16, 17, 0, -13, 13, 34, 9287 }, // U+0489 + { 12, 19, 198, 1, 16, 29, 9321 }, // U+048A + { 10, 16, 156, 1, 13, 20, 9350 }, // U+048B + { 11, 13, 183, 0, 13, 18, 9370 }, // U+048C + { 10, 12, 162, 0, 12, 15, 9388 }, // U+048D + { 9, 13, 168, 1, 13, 15, 9403 }, // U+048E + { 9, 14, 161, 1, 10, 16, 9418 }, // U+048F + { 8, 15, 152, 1, 15, 15, 9434 }, // U+0490 + { 6, 12, 116, 1, 12, 9, 9449 }, // U+0491 + { 10, 13, 152, -1, 13, 17, 9458 }, // U+0492 + { 7, 9, 116, 0, 9, 8, 9475 }, // U+0493 + { 9, 14, 172, 1, 13, 16, 9483 }, // U+0494 + { 8, 12, 143, 1, 9, 12, 9499 }, // U+0495 + { 17, 16, 258, 0, 13, 34, 9511 }, // U+0496 + { 14, 12, 219, 0, 9, 21, 9545 }, // U+0497 + { 10, 17, 164, 0, 13, 22, 9566 }, // U+0498 + { 8, 14, 130, 0, 10, 14, 9588 }, // U+0499 + { 11, 16, 187, 1, 13, 22, 9602 }, // U+049A + { 9, 12, 148, 1, 9, 14, 9624 }, // U+049B + { 10, 13, 176, 1, 13, 17, 9638 }, // U+049C + { 9, 9, 154, 1, 9, 11, 9655 }, // U+049D + { 14, 13, 221, 0, 13, 23, 9666 }, // U+04A0 + { 11, 9, 166, 0, 9, 13, 9689 }, // U+04A1 + { 12, 16, 200, 1, 13, 24, 9702 }, // U+04A2 + { 9, 12, 158, 1, 9, 14, 9726 }, // U+04A3 + { 15, 13, 260, 1, 13, 25, 9740 }, // U+04A4 + { 10, 9, 180, 1, 9, 12, 9765 }, // U+04A5 + { 17, 14, 294, 1, 13, 30, 9777 }, // U+04A6 + { 14, 12, 244, 1, 9, 21, 9807 }, // U+04A7 + { 16, 14, 261, 0, 13, 28, 9828 }, // U+04A8 + { 13, 11, 206, 0, 10, 18, 9856 }, // U+04A9 + { 12, 17, 194, 0, 13, 26, 9874 }, // U+04AA + { 9, 14, 150, 0, 10, 16, 9900 }, // U+04AB + { 10, 16, 170, 0, 13, 20, 9916 }, // U+04AC + { 8, 12, 124, 0, 9, 12, 9936 }, // U+04AD + { 11, 13, 179, 0, 13, 18, 9948 }, // U+04AE + { 9, 13, 147, 0, 9, 15, 9966 }, // U+04AF + { 11, 13, 179, 0, 13, 18, 9981 }, // U+04B0 + { 9, 13, 147, 0, 9, 15, 9999 }, // U+04B1 + { 12, 16, 185, 0, 13, 24, 10014 }, // U+04B2 + { 10, 12, 149, 0, 9, 15, 10038 }, // U+04B3 + { 15, 16, 243, 0, 13, 30, 10053 }, // U+04B4 + { 11, 12, 175, 0, 9, 17, 10083 }, // U+04B5 + { 11, 16, 188, 1, 13, 22, 10100 }, // U+04B6 + { 9, 12, 156, 1, 9, 14, 10122 }, // U+04B7 + { 10, 13, 185, 1, 13, 17, 10136 }, // U+04B8 + { 8, 9, 152, 1, 9, 9, 10153 }, // U+04B9 + { 10, 13, 185, 1, 13, 17, 10162 }, // U+04BA + { 8, 13, 155, 1, 13, 13, 10179 }, // U+04BB + { 13, 14, 223, 0, 13, 23, 10192 }, // U+04BC + { 12, 11, 188, 0, 10, 17, 10215 }, // U+04BD + { 13, 16, 223, 0, 13, 26, 10232 }, // U+04BE + { 12, 14, 188, 0, 10, 21, 10258 }, // U+04BF + { 2, 13, 70, 1, 13, 4, 10279 }, // U+04C0 + { 16, 16, 254, 0, 16, 32, 10283 }, // U+04C1 + { 13, 13, 214, 0, 13, 22, 10315 }, // U+04C2 + { 10, 16, 180, 1, 13, 20, 10337 }, // U+04C3 + { 8, 12, 154, 1, 9, 12, 10357 }, // U+04C4 + { 13, 16, 197, 0, 13, 26, 10369 }, // U+04C5 + { 10, 12, 152, 0, 9, 15, 10395 }, // U+04C6 + { 10, 17, 196, 1, 13, 22, 10410 }, // U+04C7 + { 8, 13, 155, 1, 9, 13, 10432 }, // U+04C8 + { 12, 16, 196, 1, 13, 24, 10445 }, // U+04C9 + { 10, 12, 155, 1, 9, 15, 10469 }, // U+04CA + { 11, 16, 188, 1, 13, 22, 10484 }, // U+04CB + { 9, 12, 156, 1, 9, 14, 10506 }, // U+04CC + { 15, 16, 238, 1, 13, 30, 10520 }, // U+04CD + { 13, 12, 203, 1, 9, 20, 10550 }, // U+04CE + { 2, 13, 63, 1, 13, 4, 10570 }, // U+04CF + { 11, 16, 182, 0, 16, 22, 10574 }, // U+04D0 + { 8, 14, 147, 0, 13, 14, 10596 }, // U+04D1 + { 11, 16, 182, 0, 16, 22, 10610 }, // U+04D2 + { 8, 14, 147, 0, 13, 14, 10632 }, // U+04D3 + { 16, 13, 263, 0, 13, 26, 10646 }, // U+04D4 + { 15, 11, 242, 0, 10, 21, 10672 }, // U+04D5 + { 8, 16, 159, 1, 16, 16, 10693 }, // U+04D6 + { 9, 14, 153, 0, 13, 16, 10709 }, // U+04D7 + { 12, 14, 198, 0, 13, 21, 10725 }, // U+04D8 + { 9, 11, 153, 0, 10, 13, 10746 }, // U+04D9 + { 12, 17, 198, 0, 16, 26, 10759 }, // U+04DA + { 9, 14, 153, 0, 13, 16, 10785 }, // U+04DB + { 16, 16, 254, 0, 16, 32, 10801 }, // U+04DC + { 13, 13, 214, 0, 13, 22, 10833 }, // U+04DD + { 10, 17, 164, 0, 16, 22, 10855 }, // U+04DE + { 8, 14, 130, 0, 13, 14, 10877 }, // U+04DF + { 10, 14, 164, 0, 13, 18, 10891 }, // U+04E0 + { 10, 14, 164, 0, 10, 18, 10909 }, // U+04E1 + { 10, 15, 198, 1, 15, 19, 10927 }, // U+04E2 + { 8, 12, 156, 1, 12, 12, 10946 }, // U+04E3 + { 10, 16, 198, 1, 16, 20, 10958 }, // U+04E4 + { 8, 13, 156, 1, 13, 13, 10978 }, // U+04E5 + { 12, 17, 203, 0, 16, 26, 10991 }, // U+04E6 + { 10, 14, 157, 0, 13, 18, 11017 }, // U+04E7 + { 12, 14, 203, 0, 13, 21, 11035 }, // U+04E8 + { 10, 11, 157, 0, 10, 14, 11056 }, // U+04E9 + { 12, 17, 203, 0, 16, 26, 11070 }, // U+04EA + { 10, 14, 157, 0, 13, 18, 11096 }, // U+04EB + { 12, 17, 194, 0, 16, 26, 11114 }, // U+04EC + { 9, 14, 150, 0, 13, 16, 11140 }, // U+04ED + { 10, 15, 165, 0, 15, 19, 11156 }, // U+04EE + { 9, 16, 147, 0, 12, 18, 11175 }, // U+04EF + { 10, 16, 165, 0, 16, 20, 11193 }, // U+04F0 + { 9, 17, 147, 0, 13, 20, 11213 }, // U+04F1 + { 10, 16, 165, 0, 16, 20, 11233 }, // U+04F2 + { 9, 17, 147, 0, 13, 20, 11253 }, // U+04F3 + { 10, 16, 185, 1, 16, 20, 11273 }, // U+04F4 + { 8, 13, 152, 1, 13, 13, 11293 }, // U+04F5 + { 8, 16, 152, 1, 13, 16, 11306 }, // U+04F6 + { 6, 12, 116, 1, 9, 9, 11322 }, // U+04F7 + { 13, 16, 232, 1, 16, 26, 11331 }, // U+04F8 + { 10, 13, 193, 1, 13, 17, 11357 }, // U+04F9 + { 10, 17, 157, 0, 13, 22, 11374 }, // U+04FA + { 7, 13, 116, 0, 9, 12, 11396 }, // U+04FB + { 12, 17, 186, 0, 13, 26, 11408 }, // U+04FC + { 9, 13, 137, 0, 9, 15, 11434 }, // U+04FD + { 11, 13, 180, 0, 13, 18, 11449 }, // U+04FE + { 9, 9, 143, 0, 9, 11, 11467 }, // U+04FF + { 9, 10, 168, 1, 10, 12, 11478 }, // U+05D0 + { 9, 10, 147, 0, 10, 12, 11490 }, // U+05D1 + { 6, 10, 112, 0, 10, 8, 11502 }, // U+05D2 + { 9, 10, 141, 0, 10, 12, 11510 }, // U+05D3 + { 9, 10, 170, 1, 10, 12, 11522 }, // U+05D4 + { 2, 10, 69, 1, 10, 3, 11534 }, // U+05D5 + { 6, 10, 101, 0, 10, 8, 11537 }, // U+05D6 + { 9, 10, 173, 1, 10, 12, 11545 }, // U+05D7 + { 9, 10, 172, 1, 10, 12, 11557 }, // U+05D8 + { 2, 5, 64, 1, 10, 2, 11569 }, // U+05D9 + { 8, 13, 138, 0, 10, 13, 11571 }, // U+05DA + { 9, 10, 142, 0, 10, 12, 11584 }, // U+05DB + { 8, 13, 126, 0, 13, 13, 11596 }, // U+05DC + { 9, 10, 173, 1, 10, 12, 11609 }, // U+05DD + { 10, 10, 174, 0, 10, 13, 11621 }, // U+05DE + { 2, 13, 69, 1, 10, 4, 11634 }, // U+05DF + { 5, 10, 93, 0, 10, 7, 11638 }, // U+05E0 + { 11, 10, 171, 0, 10, 14, 11645 }, // U+05E1 + { 9, 10, 158, 0, 10, 12, 11659 }, // U+05E2 + { 9, 13, 168, 1, 10, 15, 11671 }, // U+05E3 + { 9, 10, 166, 1, 10, 12, 11686 }, // U+05E4 + { 8, 13, 137, 0, 10, 13, 11698 }, // U+05E5 + { 9, 10, 145, 0, 10, 12, 11711 }, // U+05E6 + { 9, 13, 162, 1, 10, 15, 11723 }, // U+05E7 + { 8, 10, 140, 0, 10, 10, 11738 }, // U+05E8 + { 11, 10, 201, 1, 10, 14, 11748 }, // U+05E9 + { 9, 10, 177, 1, 10, 12, 11762 }, // U+05EA + { 3, 5, 87, 1, 4, 2, 11774 }, // U+060C + { 3, 10, 92, 1, 9, 4, 11776 }, // U+061B + { 7, 14, 116, 0, 13, 13, 11780 }, // U+061F + { 7, 7, 110, 0, 6, 7, 11793 }, // U+0621 + { 6, 3, 72, 0, 2, 3, 11800 }, // U+0640 + { 4, 4, 153, 3, 7, 2, 11803 }, // U+0660 + { 4, 13, 153, 3, 12, 7, 11805 }, // U+0661 + { 8, 13, 153, 1, 12, 13, 11812 }, // U+0662 + { 9, 13, 153, 0, 12, 15, 11825 }, // U+0663 + { 8, 13, 153, 1, 12, 13, 11840 }, // U+0664 + { 8, 10, 153, 1, 9, 10, 11853 }, // U+0665 + { 8, 13, 153, 1, 12, 13, 11863 }, // U+0666 + { 10, 13, 153, 0, 12, 17, 11876 }, // U+0667 + { 10, 13, 153, 0, 12, 17, 11893 }, // U+0668 + { 8, 13, 153, 1, 12, 13, 11910 }, // U+0669 + { 11, 10, 183, 0, 6, 14, 11923 }, // U+06BA + { 5, 2, 80, 0, 2, 2, 11937 }, // U+06D4 + { 4, 4, 153, 3, 7, 2, 11939 }, // U+06F0 + { 4, 13, 153, 3, 12, 7, 11941 }, // U+06F1 + { 8, 13, 153, 1, 12, 13, 11948 }, // U+06F2 + { 9, 13, 153, 0, 12, 15, 11961 }, // U+06F3 + { 8, 12, 153, 1, 12, 12, 11976 }, // U+06F4 + { 8, 12, 153, 1, 12, 12, 11988 }, // U+06F5 + { 7, 13, 153, 1, 12, 12, 12000 }, // U+06F6 + { 10, 13, 153, 0, 12, 17, 12012 }, // U+06F7 + { 10, 13, 153, 0, 12, 17, 12029 }, // U+06F8 + { 8, 13, 153, 1, 12, 13, 12046 }, // U+06F9 + { 11, 17, 182, 0, 13, 24, 12059 }, // U+1EA0 + { 8, 14, 147, 0, 10, 14, 12083 }, // U+1EA1 + { 11, 16, 182, 0, 16, 22, 12097 }, // U+1EA2 + { 8, 14, 147, 0, 13, 14, 12119 }, // U+1EA3 + { 11, 19, 182, 0, 19, 27, 12133 }, // U+1EA4 + { 9, 17, 147, 0, 16, 20, 12160 }, // U+1EA5 + { 11, 19, 182, 0, 19, 27, 12180 }, // U+1EA6 + { 8, 17, 147, 0, 16, 17, 12207 }, // U+1EA7 + { 11, 19, 182, 0, 19, 27, 12224 }, // U+1EA8 + { 8, 17, 147, 0, 16, 17, 12251 }, // U+1EA9 + { 11, 18, 182, 0, 18, 25, 12268 }, // U+1EAA + { 8, 16, 147, 0, 15, 16, 12293 }, // U+1EAB + { 11, 20, 182, 0, 16, 28, 12309 }, // U+1EAC + { 8, 17, 147, 0, 13, 17, 12337 }, // U+1EAD + { 11, 19, 182, 0, 19, 27, 12354 }, // U+1EAE + { 8, 17, 147, 0, 16, 17, 12381 }, // U+1EAF + { 11, 19, 182, 0, 19, 27, 12398 }, // U+1EB0 + { 8, 17, 147, 0, 16, 17, 12425 }, // U+1EB1 + { 11, 19, 182, 0, 19, 27, 12442 }, // U+1EB2 + { 8, 17, 147, 0, 16, 17, 12469 }, // U+1EB3 + { 11, 18, 182, 0, 18, 25, 12486 }, // U+1EB4 + { 8, 16, 147, 0, 15, 16, 12511 }, // U+1EB5 + { 11, 20, 182, 0, 16, 28, 12527 }, // U+1EB6 + { 8, 17, 147, 0, 13, 17, 12555 }, // U+1EB7 + { 8, 17, 159, 1, 13, 17, 12572 }, // U+1EB8 + { 9, 14, 153, 0, 10, 16, 12589 }, // U+1EB9 + { 8, 16, 159, 1, 16, 16, 12605 }, // U+1EBA + { 9, 14, 153, 0, 13, 16, 12621 }, // U+1EBB + { 8, 16, 159, 1, 16, 16, 12637 }, // U+1EBC + { 9, 14, 153, 0, 13, 16, 12653 }, // U+1EBD + { 9, 19, 159, 1, 19, 22, 12669 }, // U+1EBE + { 9, 17, 153, 0, 16, 20, 12691 }, // U+1EBF + { 8, 19, 159, 1, 19, 19, 12711 }, // U+1EC0 + { 9, 17, 153, 0, 16, 20, 12730 }, // U+1EC1 + { 8, 19, 159, 1, 19, 19, 12750 }, // U+1EC2 + { 9, 17, 153, 0, 16, 20, 12769 }, // U+1EC3 + { 8, 18, 159, 1, 18, 18, 12789 }, // U+1EC4 + { 9, 16, 153, 0, 15, 18, 12807 }, // U+1EC5 + { 8, 20, 159, 1, 16, 20, 12825 }, // U+1EC6 + { 9, 17, 153, 0, 13, 20, 12845 }, // U+1EC7 + { 4, 16, 70, 0, 16, 8, 12865 }, // U+1EC8 + { 4, 13, 63, 0, 13, 7, 12873 }, // U+1EC9 + { 3, 17, 70, 1, 13, 7, 12880 }, // U+1ECA + { 3, 17, 63, 0, 13, 7, 12887 }, // U+1ECB + { 12, 17, 203, 0, 13, 26, 12894 }, // U+1ECC + { 10, 14, 157, 0, 10, 18, 12920 }, // U+1ECD + { 12, 17, 203, 0, 16, 26, 12938 }, // U+1ECE + { 10, 14, 157, 0, 13, 18, 12964 }, // U+1ECF + { 12, 20, 203, 0, 19, 30, 12982 }, // U+1ED0 + { 10, 17, 157, 0, 16, 22, 13012 }, // U+1ED1 + { 12, 20, 203, 0, 19, 30, 13034 }, // U+1ED2 + { 10, 17, 157, 0, 16, 22, 13064 }, // U+1ED3 + { 12, 20, 203, 0, 19, 30, 13086 }, // U+1ED4 + { 10, 17, 157, 0, 16, 22, 13116 }, // U+1ED5 + { 12, 19, 203, 0, 18, 29, 13138 }, // U+1ED6 + { 10, 16, 157, 0, 15, 20, 13167 }, // U+1ED7 + { 12, 20, 203, 0, 16, 30, 13187 }, // U+1ED8 + { 10, 17, 157, 0, 13, 22, 13217 }, // U+1ED9 + { 12, 17, 202, 0, 16, 26, 13239 }, // U+1EDA + { 10, 14, 156, 0, 13, 18, 13265 }, // U+1EDB + { 12, 17, 202, 0, 16, 26, 13283 }, // U+1EDC + { 10, 14, 156, 0, 13, 18, 13309 }, // U+1EDD + { 12, 17, 202, 0, 16, 26, 13327 }, // U+1EDE + { 10, 14, 156, 0, 13, 18, 13353 }, // U+1EDF + { 12, 17, 202, 0, 16, 26, 13371 }, // U+1EE0 + { 10, 14, 156, 0, 13, 18, 13397 }, // U+1EE1 + { 12, 18, 202, 0, 14, 27, 13415 }, // U+1EE2 + { 10, 15, 156, 0, 11, 19, 13442 }, // U+1EE3 + { 10, 17, 196, 1, 13, 22, 13461 }, // U+1EE4 + { 8, 13, 155, 1, 9, 13, 13483 }, // U+1EE5 + { 10, 17, 196, 1, 16, 22, 13496 }, // U+1EE6 + { 8, 14, 155, 1, 13, 14, 13518 }, // U+1EE7 + { 12, 17, 196, 1, 16, 26, 13532 }, // U+1EE8 + { 9, 14, 164, 1, 13, 16, 13558 }, // U+1EE9 + { 12, 17, 196, 1, 16, 26, 13574 }, // U+1EEA + { 9, 14, 164, 1, 13, 16, 13600 }, // U+1EEB + { 12, 17, 196, 1, 16, 26, 13616 }, // U+1EEC + { 9, 14, 164, 1, 13, 16, 13642 }, // U+1EED + { 12, 17, 196, 1, 16, 26, 13658 }, // U+1EEE + { 9, 14, 164, 1, 13, 16, 13684 }, // U+1EEF + { 12, 18, 196, 1, 14, 27, 13700 }, // U+1EF0 + { 9, 15, 164, 1, 11, 17, 13727 }, // U+1EF1 + { 11, 16, 179, 0, 16, 22, 13744 }, // U+1EF2 + { 9, 17, 147, 0, 13, 20, 13766 }, // U+1EF3 + { 11, 17, 179, 0, 13, 24, 13786 }, // U+1EF4 + { 9, 13, 147, 0, 9, 15, 13810 }, // U+1EF5 + { 11, 16, 179, 0, 16, 22, 13825 }, // U+1EF6 + { 9, 17, 147, 0, 13, 20, 13847 }, // U+1EF7 + { 11, 16, 179, 0, 16, 22, 13867 }, // U+1EF8 + { 9, 17, 147, 0, 13, 20, 13889 }, // U+1EF9 + { 0, 0, 146, 0, 0, 0, 13909 }, // U+2000 + { 0, 0, 291, 0, 0, 0, 13909 }, // U+2001 + { 0, 0, 133, 0, 0, 0, 13909 }, // U+2002 + { 0, 0, 267, 0, 0, 0, 13909 }, // U+2003 + { 0, 0, 89, 0, 0, 0, 13909 }, // U+2004 + { 0, 0, 67, 0, 0, 0, 13909 }, // U+2005 + { 0, 0, 44, 0, 0, 0, 13909 }, // U+2006 + { 0, 0, 173, 0, 0, 0, 13909 }, // U+2007 + { 0, 0, 73, 0, 0, 0, 13909 }, // U+2008 + { 0, 0, 47, 0, 0, 0, 13909 }, // U+2009 + { 0, 0, 22, 0, 0, 0, 13909 }, // U+200A + { 0, 0, 0, 0, 0, 0, 13909 }, // U+200B + { 0, 0, 0, 0, 0, 0, 13909 }, // U+200C + { 0, 0, 0, 0, 0, 0, 13909 }, // U+200D + { 0, 0, 0, 0, 0, 0, 13909 }, // U+200E + { 0, 0, 0, 0, 0, 0, 13909 }, // U+200F + { 6, 2, 94, 0, 6, 2, 13909 }, // U+2010 + { 6, 2, 94, 0, 6, 2, 13911 }, // U+2011 + { 10, 3, 173, 0, 7, 4, 13913 }, // U+2012 + { 9, 2, 133, 0, 6, 3, 13917 }, // U+2013 + { 17, 2, 267, 0, 6, 5, 13920 }, // U+2014 + { 17, 2, 267, 0, 6, 5, 13925 }, // U+2015 + { 4, 21, 126, 2, 17, 11, 13930 }, // U+2016 + { 9, 4, 119, -1, 0, 5, 13941 }, // U+2017 + { 3, 6, 65, 1, 13, 3, 13946 }, // U+2018 + { 3, 6, 65, 1, 13, 3, 13949 }, // U+2019 + { 4, 6, 60, 0, 3, 3, 13952 }, // U+201A + { 3, 6, 65, 0, 13, 3, 13955 }, // U+201B + { 6, 6, 111, 1, 13, 5, 13958 }, // U+201C + { 6, 6, 111, 1, 13, 5, 13963 }, // U+201D + { 7, 6, 107, 0, 3, 6, 13968 }, // U+201E + { 6, 6, 111, 0, 13, 5, 13974 }, // U+201F + { 8, 10, 129, 0, 13, 10, 13979 }, // U+2020 + { 8, 13, 129, 0, 13, 13, 13989 }, // U+2021 + { 7, 6, 146, 1, 8, 6, 14002 }, // U+2022 + { 6, 6, 146, 2, 8, 5, 14008 }, // U+2023 + { 3, 4, 73, 1, 3, 2, 14013 }, // U+2024 + { 7, 4, 146, 1, 3, 4, 14015 }, // U+2025 + { 12, 4, 218, 1, 3, 6, 14019 }, // U+2026 + { 3, 4, 73, 1, 8, 2, 14025 }, // U+2027 + { 0, 0, 0, 0, 0, 0, 14027 }, // U+202A + { 0, 0, 0, 0, 0, 0, 14027 }, // U+202B + { 0, 0, 0, 0, 0, 0, 14027 }, // U+202C + { 0, 0, 0, 0, 0, 0, 14027 }, // U+202D + { 0, 0, 0, 0, 0, 0, 14027 }, // U+202E + { 0, 0, 47, 0, 0, 0, 14027 }, // U+202F + { 19, 14, 347, 1, 13, 34, 14027 }, // U+2030 + { 25, 14, 440, 1, 13, 44, 14061 }, // U+2031 + { 3, 6, 57, 0, 13, 3, 14105 }, // U+2032 + { 7, 6, 114, 0, 13, 6, 14108 }, // U+2033 + { 10, 6, 171, 0, 13, 8, 14114 }, // U+2034 + { 3, 6, 57, 0, 13, 3, 14122 }, // U+2035 + { 7, 6, 114, 0, 13, 6, 14125 }, // U+2036 + { 10, 6, 171, 0, 13, 8, 14131 }, // U+2037 + { 7, 5, 119, 0, 0, 5, 14139 }, // U+2038 + { 6, 8, 99, 0, 9, 6, 14144 }, // U+2039 + { 6, 8, 99, 0, 9, 6, 14150 }, // U+203A + { 10, 10, 168, 0, 11, 13, 14156 }, // U+203B + { 7, 14, 145, 1, 13, 13, 14169 }, // U+203C + { 8, 14, 134, 0, 13, 14, 14182 }, // U+203D + { 9, 2, 142, 0, 14, 3, 14196 }, // U+203E + { 13, 4, 216, 0, 0, 7, 14199 }, // U+203F + { 13, 4, 216, 0, 13, 7, 14206 }, // U+2040 + { 7, 10, 119, 0, 5, 9, 14213 }, // U+2041 + { 10, 10, 191, 1, 11, 13, 14222 }, // U+2042 + { 5, 2, 146, 2, 6, 2, 14235 }, // U+2043 + { 11, 13, 47, -4, 13, 18, 14237 }, // U+2044 + { 4, 16, 93, 1, 13, 8, 14255 }, // U+2045 + { 5, 16, 93, 0, 13, 10, 14263 }, // U+2046 + { 17, 14, 274, 0, 13, 30, 14273 }, // U+2047 + { 12, 14, 210, 0, 13, 21, 14303 }, // U+2048 + { 12, 14, 210, 1, 13, 21, 14324 }, // U+2049 + { 7, 13, 116, 0, 9, 12, 14345 }, // U+204A + { 9, 13, 158, 1, 13, 15, 14357 }, // U+204B + { 8, 8, 156, 1, 9, 8, 14372 }, // U+204C + { 8, 8, 156, 1, 9, 8, 14380 }, // U+204D + { 8, 8, 132, 0, 7, 8, 14388 }, // U+204E + { 3, 12, 76, 1, 9, 5, 14396 }, // U+204F + { 13, 17, 215, 0, 13, 28, 14401 }, // U+2050 + { 8, 16, 142, 1, 13, 16, 14429 }, // U+2051 + { 11, 13, 190, 0, 13, 18, 14445 }, // U+2052 + { 10, 4, 191, 1, 8, 5, 14463 }, // U+2053 + { 13, 4, 216, 0, 0, 7, 14468 }, // U+2054 + { 7, 8, 144, 1, 8, 7, 14475 }, // U+2055 + { 14, 6, 228, 0, 13, 11, 14482 }, // U+2057 + { 0, 0, 73, 0, 0, 0, 14493 }, // U+205F + { 0, 0, 0, 0, 0, 0, 14493 }, // U+2060 + { 0, 0, 0, 0, 0, 0, 14493 }, // U+2061 + { 0, 0, 0, 0, 0, 0, 14493 }, // U+2062 + { 0, 0, 0, 0, 0, 0, 14493 }, // U+2063 + { 0, 0, 0, 0, 0, 0, 14493 }, // U+2064 + { 0, 0, 0, 0, 0, 0, 14493 }, // U+2066 + { 0, 0, 0, 0, 0, 0, 14493 }, // U+2067 + { 0, 0, 0, 0, 0, 0, 14493 }, // U+2068 + { 0, 0, 0, 0, 0, 0, 14493 }, // U+2069 + { 0, 0, 0, 0, 0, 0, 14493 }, // U+206A + { 0, 0, 0, 0, 0, 0, 14493 }, // U+206B + { 0, 0, 0, 0, 0, 0, 14493 }, // U+206C + { 0, 0, 0, 0, 0, 0, 14493 }, // U+206D + { 0, 0, 0, 0, 0, 0, 14493 }, // U+206E + { 0, 0, 0, 0, 0, 0, 14493 }, // U+206F + { 7, 7, 120, 0, 14, 7, 14493 }, // U+2070 + { 3, 8, 51, 0, 15, 3, 14500 }, // U+2071 + { 7, 7, 120, 0, 14, 7, 14503 }, // U+2074 + { 7, 7, 114, 0, 14, 7, 14510 }, // U+2075 + { 7, 7, 116, 0, 14, 7, 14517 }, // U+2076 + { 6, 7, 105, 0, 14, 6, 14524 }, // U+2077 + { 7, 7, 117, 0, 14, 7, 14530 }, // U+2078 + { 7, 7, 116, 0, 14, 7, 14537 }, // U+2079 + { 6, 5, 114, 1, 13, 4, 14544 }, // U+207A + { 5, 2, 114, 1, 11, 2, 14548 }, // U+207B + { 5, 4, 114, 1, 12, 3, 14550 }, // U+207C + { 3, 9, 69, 1, 15, 4, 14553 }, // U+207D + { 4, 9, 69, 0, 15, 5, 14557 }, // U+207E + { 7, 6, 113, 0, 13, 6, 14562 }, // U+207F + { 7, 7, 120, 0, 4, 7, 14568 }, // U+2080 + { 4, 7, 79, 0, 4, 4, 14575 }, // U+2081 + { 7, 7, 114, 0, 4, 7, 14579 }, // U+2082 + { 7, 7, 116, 0, 4, 7, 14586 }, // U+2083 + { 7, 7, 120, 0, 4, 7, 14593 }, // U+2084 + { 7, 7, 114, 0, 4, 7, 14600 }, // U+2085 + { 7, 7, 116, 0, 4, 7, 14607 }, // U+2086 + { 6, 7, 105, 0, 4, 6, 14614 }, // U+2087 + { 7, 7, 117, 0, 4, 7, 14620 }, // U+2088 + { 7, 7, 116, 0, 4, 7, 14627 }, // U+2089 + { 6, 5, 114, 1, 3, 4, 14634 }, // U+208A + { 5, 2, 114, 1, 1, 2, 14638 }, // U+208B + { 5, 4, 114, 1, 2, 3, 14640 }, // U+208C + { 3, 9, 69, 1, 5, 4, 14643 }, // U+208D + { 4, 9, 69, 0, 5, 5, 14647 }, // U+208E + { 6, 6, 104, 0, 3, 5, 14652 }, // U+2090 + { 7, 6, 108, 0, 3, 6, 14657 }, // U+2091 + { 7, 6, 111, 0, 3, 6, 14663 }, // U+2092 + { 7, 6, 104, 0, 3, 6, 14669 }, // U+2093 + { 7, 8, 118, 0, 4, 7, 14675 }, // U+2094 + { 7, 7, 113, 0, 4, 7, 14682 }, // U+2095 + { 7, 7, 103, 0, 4, 7, 14689 }, // U+2096 + { 3, 7, 51, 0, 4, 3, 14696 }, // U+2097 + { 10, 6, 162, 0, 3, 8, 14699 }, // U+2098 + { 7, 6, 113, 0, 3, 6, 14707 }, // U+2099 + { 7, 8, 113, 0, 3, 7, 14713 }, // U+209A + { 6, 6, 101, 0, 3, 5, 14720 }, // U+209B + { 4, 7, 65, 0, 4, 4, 14725 }, // U+209C + { 10, 14, 192, 1, 13, 18, 14729 }, // U+20A0 + { 12, 16, 194, 0, 14, 24, 14747 }, // U+20A1 + { 12, 14, 193, 0, 13, 21, 14771 }, // U+20A2 + { 11, 13, 160, -1, 13, 18, 14792 }, // U+20A3 + { 10, 13, 161, 0, 13, 17, 14810 }, // U+20A4 + { 12, 13, 227, 1, 11, 20, 14827 }, // U+20A5 + { 13, 13, 203, 0, 13, 22, 14847 }, // U+20A6 + { 15, 14, 251, 1, 13, 27, 14869 }, // U+20A7 + { 17, 14, 296, 1, 13, 30, 14896 }, // U+20A8 + { 16, 13, 252, 0, 13, 26, 14926 }, // U+20A9 + { 12, 10, 215, 1, 10, 15, 14952 }, // U+20AA + { 10, 17, 161, 0, 13, 22, 14967 }, // U+20AB + { 11, 14, 176, 0, 13, 20, 14989 }, // U+20AC + { 12, 13, 187, 0, 13, 20, 15009 }, // U+20AD + { 10, 13, 170, 0, 13, 17, 15029 }, // U+20AE + { 23, 18, 367, 0, 14, 52, 15046 }, // U+20AF + { 11, 13, 176, 0, 13, 18, 15098 }, // U+20B1 + { 12, 16, 198, 0, 14, 24, 15116 }, // U+20B2 + { 11, 13, 182, 0, 13, 18, 15140 }, // U+20B3 + { 10, 14, 167, 0, 13, 18, 15158 }, // U+20B4 + { 12, 16, 194, 0, 14, 24, 15176 }, // U+20B5 + { 10, 13, 170, 0, 13, 17, 15200 }, // U+20B8 + { 9, 13, 142, 0, 13, 15, 15217 }, // U+20B9 + { 9, 14, 156, 0, 13, 16, 15232 }, // U+20BA + { 10, 10, 192, 1, 10, 13, 15248 }, // U+20BC + { 11, 13, 185, 0, 13, 18, 15261 }, // U+20BD + { 10, 13, 191, 1, 13, 17, 15279 }, // U+20BE + { 9, 16, 174, 1, 14, 18, 15296 }, // U+20BF + { 13, 11, 253, 1, 11, 18, 15314 }, // U+2190 + { 12, 13, 221, 1, 13, 20, 15332 }, // U+2191 + { 13, 11, 253, 2, 11, 18, 15352 }, // U+2192 + { 12, 14, 221, 1, 13, 21, 15370 }, // U+2193 + { 20, 11, 356, 1, 11, 28, 15391 }, // U+2194 + { 12, 20, 221, 1, 15, 30, 15419 }, // U+2195 + { 10, 10, 204, 1, 9, 13, 15449 }, // U+2196 + { 10, 10, 204, 1, 9, 13, 15462 }, // U+2197 + { 10, 10, 204, 1, 10, 13, 15475 }, // U+2198 + { 10, 10, 204, 1, 10, 13, 15488 }, // U+2199 + { 17, 13, 303, 1, 13, 28, 15501 }, // U+21A9 + { 17, 13, 303, 1, 13, 28, 15529 }, // U+21AA + { 14, 13, 261, 1, 13, 23, 15557 }, // U+21B0 + { 14, 13, 261, 1, 13, 23, 15580 }, // U+21B1 + { 9, 12, 219, 2, 12, 14, 15603 }, // U+21B2 + { 14, 12, 261, 1, 13, 21, 15617 }, // U+21B3 + { 12, 14, 220, 1, 13, 21, 15638 }, // U+21B4 + { 14, 12, 261, 1, 13, 21, 15659 }, // U+21B5 + { 14, 11, 219, 0, 11, 20, 15680 }, // U+21B6 + { 14, 11, 219, 0, 11, 20, 15700 }, // U+21B7 + { 13, 15, 253, 1, 15, 25, 15720 }, // U+21BA + { 13, 15, 253, 1, 15, 25, 15745 }, // U+21BB + { 13, 13, 219, 0, 12, 22, 15770 }, // U+21C4 + { 13, 13, 219, 0, 12, 22, 15792 }, // U+21C6 + { 13, 12, 253, 1, 11, 20, 15814 }, // U+21D0 + { 13, 12, 253, 2, 11, 20, 15834 }, // U+21D2 + { 20, 12, 356, 1, 11, 30, 15854 }, // U+21D4 + { 8, 13, 160, 1, 13, 13, 15884 }, // U+21DE + { 8, 14, 160, 1, 13, 14, 15897 }, // U+21DF + { 15, 8, 269, 1, 10, 15, 15911 }, // U+21E4 + { 15, 8, 269, 1, 10, 15, 15926 }, // U+21E5 + { 15, 14, 273, 1, 14, 27, 15941 }, // U+21E7 + { 15, 17, 273, 1, 14, 32, 15968 }, // U+21EA + { 9, 14, 167, 1, 13, 16, 16000 }, // U+2202 + { 11, 11, 181, 0, 10, 16, 16016 }, // U+2205 + { 11, 13, 179, 0, 13, 18, 16032 }, // U+2206 + { 10, 15, 191, 1, 13, 19, 16050 }, // U+220F + { 10, 16, 167, 0, 13, 20, 16069 }, // U+2211 + { 9, 2, 175, 1, 6, 3, 16089 }, // U+2212 + { 10, 12, 39, -4, 12, 15, 16092 }, // U+2215 + { 10, 13, 166, 0, 13, 17, 16107 }, // U+221A + { 15, 8, 247, 0, 9, 15, 16124 }, // U+221E + { 7, 17, 96, -1, 13, 15, 16139 }, // U+222B + { 3, 10, 73, 1, 11, 4, 16154 }, // U+2236 + { 9, 8, 175, 1, 9, 9, 16158 }, // U+2248 + { 9, 10, 175, 1, 10, 12, 16167 }, // U+2260 + { 9, 10, 175, 1, 10, 12, 16179 }, // U+2264 + { 9, 10, 175, 1, 10, 12, 16191 }, // U+2265 + { 23, 24, 371, 0, 18, 69, 16203 }, // U+2295 + { 23, 24, 371, 0, 18, 69, 16272 }, // U+2296 + { 23, 24, 371, 0, 18, 69, 16341 }, // U+2297 + { 23, 24, 371, 0, 18, 69, 16410 }, // U+2298 + { 9, 13, 151, 0, 13, 15, 16479 }, // U+FB01 + { 10, 13, 159, 0, 13, 17, 16494 }, // U+FB02 + { 16, 12, 265, 0, 7, 24, 16511 }, // U+FB56 + { 20, 11, 292, 0, 6, 28, 16535 }, // U+FB57 + { 5, 12, 80, -1, 7, 8, 16563 }, // U+FB58 + { 8, 10, 99, 0, 5, 10, 16571 }, // U+FB59 + { 16, 13, 265, 0, 12, 26, 16581 }, // U+FB66 + { 20, 13, 292, 0, 12, 33, 16607 }, // U+FB67 + { 5, 15, 72, 0, 14, 10, 16640 }, // U+FB68 + { 8, 13, 99, 0, 12, 13, 16650 }, // U+FB69 + { 11, 14, 176, 0, 7, 20, 16663 }, // U+FB7A + { 13, 14, 192, 0, 7, 23, 16683 }, // U+FB7B + { 10, 11, 157, 0, 6, 14, 16706 }, // U+FB7C + { 12, 11, 170, 0, 6, 17, 16720 }, // U+FB7D + { 8, 15, 127, 0, 14, 15, 16737 }, // U+FB88 + { 10, 15, 142, 0, 14, 19, 16752 }, // U+FB89 + { 7, 15, 98, -1, 11, 14, 16771 }, // U+FB8A + { 9, 15, 106, -1, 11, 17, 16785 }, // U+FB8B + { 7, 17, 98, -1, 13, 15, 16802 }, // U+FB8C + { 9, 17, 106, -1, 13, 20, 16817 }, // U+FB8D + { 15, 13, 234, 0, 12, 25, 16837 }, // U+FB8E + { 18, 13, 260, 0, 12, 30, 16862 }, // U+FB8F + { 8, 13, 110, 0, 12, 13, 16892 }, // U+FB90 + { 10, 13, 136, 0, 12, 17, 16905 }, // U+FB91 + { 15, 15, 234, 0, 14, 29, 16922 }, // U+FB92 + { 18, 15, 260, 0, 14, 34, 16951 }, // U+FB93 + { 8, 15, 110, 0, 14, 15, 16985 }, // U+FB94 + { 10, 15, 136, 0, 14, 19, 17000 }, // U+FB95 + { 11, 10, 183, 0, 6, 14, 17019 }, // U+FB9E + { 13, 9, 195, 0, 5, 15, 17033 }, // U+FB9F + { 7, 8, 108, 0, 8, 7, 17048 }, // U+FBA6 + { 9, 7, 128, 0, 6, 8, 17055 }, // U+FBA7 + { 4, 11, 72, 0, 7, 6, 17063 }, // U+FBA8 + { 9, 8, 117, 0, 3, 9, 17069 }, // U+FBA9 + { 11, 10, 179, 0, 9, 14, 17078 }, // U+FBAA + { 12, 11, 164, 0, 7, 17, 17092 }, // U+FBAB + { 9, 10, 153, 0, 9, 12, 17109 }, // U+FBAC + { 10, 11, 135, 0, 7, 14, 17121 }, // U+FBAD + { 16, 9, 251, 0, 8, 18, 17135 }, // U+FBAE + { 10, 7, 146, 0, 2, 9, 17153 }, // U+FBAF + { 16, 13, 251, 0, 12, 26, 17162 }, // U+FBB0 + { 10, 11, 146, 0, 6, 14, 17188 }, // U+FBB1 + { 13, 11, 203, 0, 7, 18, 17202 }, // U+FBFC + { 14, 7, 196, 0, 3, 13, 17220 }, // U+FBFD + { 5, 11, 80, -1, 7, 7, 17233 }, // U+FBFE + { 8, 9, 99, 0, 5, 9, 17240 }, // U+FBFF + { 7, 7, 110, 0, 6, 7, 17249 }, // U+FE80 + { 6, 14, 63, -1, 14, 11, 17256 }, // U+FE81 + { 7, 16, 78, -1, 15, 14, 17267 }, // U+FE82 + { 4, 16, 63, 0, 16, 8, 17281 }, // U+FE83 + { 6, 18, 78, 0, 17, 14, 17289 }, // U+FE84 + { 7, 15, 119, 0, 11, 14, 17303 }, // U+FE85 + { 9, 15, 126, 0, 11, 17, 17317 }, // U+FE86 + { 5, 18, 63, 0, 12, 12, 17334 }, // U+FE87 + { 6, 18, 78, 0, 12, 14, 17346 }, // U+FE88 + { 13, 12, 203, 0, 8, 20, 17360 }, // U+FE89 + { 14, 12, 196, 0, 8, 21, 17380 }, // U+FE8A + { 4, 13, 72, 0, 12, 7, 17401 }, // U+FE8B + { 7, 11, 91, 0, 10, 10, 17408 }, // U+FE8C + { 2, 12, 63, 1, 12, 3, 17418 }, // U+FE8D + { 5, 13, 78, 1, 12, 9, 17421 }, // U+FE8E + { 16, 11, 265, 0, 7, 22, 17430 }, // U+FE8F + { 20, 10, 292, 0, 6, 25, 17452 }, // U+FE90 + { 4, 11, 72, 0, 7, 6, 17477 }, // U+FE91 + { 7, 9, 91, 0, 5, 8, 17483 }, // U+FE92 + { 7, 12, 108, 0, 12, 11, 17491 }, // U+FE93 + { 9, 14, 130, 0, 13, 16, 17502 }, // U+FE94 + { 16, 10, 265, 0, 9, 20, 17518 }, // U+FE95 + { 20, 10, 292, 0, 9, 25, 17538 }, // U+FE96 + { 5, 12, 72, 0, 11, 8, 17563 }, // U+FE97 + { 8, 10, 99, 0, 9, 10, 17571 }, // U+FE98 + { 16, 11, 265, 0, 10, 22, 17581 }, // U+FE99 + { 20, 11, 292, 0, 10, 28, 17603 }, // U+FE9A + { 5, 13, 72, 0, 12, 9, 17631 }, // U+FE9B + { 8, 11, 99, 0, 10, 11, 17640 }, // U+FE9C + { 10, 14, 170, 0, 7, 18, 17651 }, // U+FE9D + { 12, 14, 173, 0, 7, 21, 17669 }, // U+FE9E + { 10, 10, 157, 0, 6, 13, 17690 }, // U+FE9F + { 12, 10, 170, 0, 6, 15, 17703 }, // U+FEA0 + { 10, 14, 170, 0, 7, 18, 17718 }, // U+FEA1 + { 12, 14, 173, 0, 7, 21, 17736 }, // U+FEA2 + { 10, 7, 157, 0, 6, 9, 17757 }, // U+FEA3 + { 12, 7, 170, 0, 6, 11, 17766 }, // U+FEA4 + { 10, 18, 170, 0, 11, 23, 17777 }, // U+FEA5 + { 12, 18, 173, 0, 11, 27, 17800 }, // U+FEA6 + { 10, 11, 157, 0, 10, 14, 17827 }, // U+FEA7 + { 12, 11, 170, 0, 10, 17, 17841 }, // U+FEA8 + { 8, 9, 127, 0, 8, 9, 17858 }, // U+FEA9 + { 10, 9, 142, 0, 8, 12, 17867 }, // U+FEAA + { 8, 13, 127, 0, 12, 13, 17879 }, // U+FEAB + { 10, 13, 142, 0, 12, 17, 17892 }, // U+FEAC + { 7, 10, 98, -1, 6, 9, 17909 }, // U+FEAD + { 9, 10, 106, -1, 6, 12, 17918 }, // U+FEAE + { 7, 14, 98, -1, 10, 13, 17930 }, // U+FEAF + { 9, 14, 106, -1, 10, 16, 17943 }, // U+FEB0 + { 20, 11, 323, 0, 7, 28, 17959 }, // U+FEB1 + { 22, 10, 339, 0, 6, 28, 17987 }, // U+FEB2 + { 13, 8, 209, 0, 7, 13, 18015 }, // U+FEB3 + { 15, 7, 227, 0, 6, 14, 18028 }, // U+FEB4 + { 20, 15, 323, 0, 11, 38, 18042 }, // U+FEB5 + { 22, 15, 339, 0, 11, 42, 18080 }, // U+FEB6 + { 13, 12, 209, 0, 11, 20, 18122 }, // U+FEB7 + { 15, 12, 227, 0, 11, 23, 18142 }, // U+FEB8 + { 22, 11, 348, 0, 7, 31, 18165 }, // U+FEB9 + { 24, 11, 361, 0, 7, 33, 18196 }, // U+FEBA + { 15, 8, 241, 0, 7, 15, 18229 }, // U+FEBB + { 17, 8, 253, 0, 7, 17, 18244 }, // U+FEBC + { 22, 15, 348, 0, 11, 42, 18261 }, // U+FEBD + { 24, 15, 361, 0, 11, 45, 18303 }, // U+FEBE + { 15, 11, 241, 0, 10, 21, 18348 }, // U+FEBF + { 17, 11, 253, 0, 10, 24, 18369 }, // U+FEC0 + { 13, 13, 210, 0, 12, 22, 18393 }, // U+FEC1 + { 15, 13, 222, 0, 12, 25, 18415 }, // U+FEC2 + { 11, 13, 181, 0, 12, 18, 18440 }, // U+FEC3 + { 13, 13, 189, 0, 12, 22, 18458 }, // U+FEC4 + { 13, 13, 210, 0, 12, 22, 18480 }, // U+FEC5 + { 15, 13, 222, 0, 12, 25, 18502 }, // U+FEC6 + { 11, 13, 181, 0, 12, 18, 18527 }, // U+FEC7 + { 13, 13, 189, 0, 12, 22, 18545 }, // U+FEC8 + { 10, 16, 135, 0, 9, 20, 18567 }, // U+FEC9 + { 10, 14, 142, 0, 7, 18, 18587 }, // U+FECA + { 8, 9, 134, 0, 8, 9, 18605 }, // U+FECB + { 10, 8, 139, 0, 7, 10, 18614 }, // U+FECC + { 10, 20, 135, 0, 13, 25, 18624 }, // U+FECD + { 10, 18, 142, 0, 11, 23, 18649 }, // U+FECE + { 8, 13, 134, 0, 12, 13, 18672 }, // U+FECF + { 10, 12, 139, 0, 11, 15, 18685 }, // U+FED0 + { 16, 14, 262, 0, 13, 28, 18700 }, // U+FED1 + { 20, 12, 293, 0, 11, 30, 18728 }, // U+FED2 + { 7, 14, 119, 0, 13, 13, 18758 }, // U+FED3 + { 10, 12, 132, 0, 11, 15, 18771 }, // U+FED4 + { 13, 15, 206, 0, 11, 25, 18786 }, // U+FED5 + { 15, 15, 216, 0, 10, 29, 18811 }, // U+FED6 + { 7, 14, 119, 0, 13, 13, 18840 }, // U+FED7 + { 10, 12, 132, 0, 11, 15, 18853 }, // U+FED8 + { 14, 13, 235, 0, 12, 23, 18868 }, // U+FED9 + { 17, 13, 246, 0, 12, 28, 18891 }, // U+FEDA + { 8, 13, 110, 0, 12, 13, 18919 }, // U+FEDB + { 10, 13, 136, 0, 12, 17, 18932 }, // U+FEDC + { 11, 16, 185, 0, 12, 22, 18949 }, // U+FEDD + { 13, 16, 195, 0, 12, 26, 18971 }, // U+FEDE + { 4, 13, 69, 0, 12, 7, 18997 }, // U+FEDF + { 6, 13, 80, 0, 12, 10, 19004 }, // U+FEE0 + { 8, 14, 129, 0, 7, 14, 19014 }, // U+FEE1 + { 11, 14, 150, 0, 7, 20, 19028 }, // U+FEE2 + { 8, 7, 140, 0, 6, 7, 19048 }, // U+FEE3 + { 11, 8, 154, 0, 7, 11, 19055 }, // U+FEE4 + { 11, 13, 183, 0, 9, 18, 19066 }, // U+FEE5 + { 13, 13, 195, 0, 9, 22, 19084 }, // U+FEE6 + { 4, 12, 72, 0, 11, 6, 19106 }, // U+FEE7 + { 7, 10, 91, 0, 9, 9, 19112 }, // U+FEE8 + { 7, 8, 108, 0, 8, 7, 19121 }, // U+FEE9 + { 9, 10, 130, 0, 9, 12, 19128 }, // U+FEEA + { 9, 10, 153, 0, 9, 12, 19140 }, // U+FEEB + { 10, 11, 135, 0, 7, 14, 19152 }, // U+FEEC + { 7, 10, 119, 0, 6, 9, 19166 }, // U+FEED + { 9, 10, 126, 0, 6, 12, 19175 }, // U+FEEE + { 13, 11, 203, 0, 7, 18, 19187 }, // U+FEEF + { 14, 7, 196, 0, 3, 13, 19205 }, // U+FEF0 + { 13, 15, 203, 0, 7, 25, 19218 }, // U+FEF1 + { 14, 11, 196, 0, 3, 20, 19243 }, // U+FEF2 + { 5, 11, 80, -1, 7, 7, 19263 }, // U+FEF3 + { 8, 9, 99, 0, 5, 9, 19270 }, // U+FEF4 + { 11, 13, 155, -2, 13, 18, 19279 }, // U+FEF5 + { 12, 14, 160, -1, 13, 21, 19297 }, // U+FEF6 + { 10, 15, 155, -1, 15, 19, 19318 }, // U+FEF7 + { 11, 16, 160, 0, 15, 22, 19337 }, // U+FEF8 + { 9, 18, 155, 0, 12, 21, 19359 }, // U+FEF9 + { 11, 18, 160, 0, 12, 25, 19380 }, // U+FEFA + { 9, 12, 155, 0, 12, 14, 19405 }, // U+FEFB + { 11, 13, 160, 0, 12, 18, 19419 }, // U+FEFC }; static const EpdUnicodeInterval inter_8_regularIntervals[] = { - { 0x0, 0x7F, 0x0 }, - { 0x80, 0xFF, 0x80 }, - { 0x100, 0x148, 0x100 }, + { 0x0, 0x148, 0x0 }, { 0x14A, 0x17F, 0x149 }, { 0x1A0, 0x1A1, 0x17F }, { 0x1AF, 0x1B0, 0x181 }, @@ -2174,59 +2570,75 @@ static const EpdUnicodeInterval inter_8_regularIntervals[] = { { 0x300, 0x304, 0x1DE }, { 0x306, 0x30C, 0x1E3 }, { 0x30F, 0x30F, 0x1EA }, - { 0x313, 0x313, 0x1EB }, - { 0x315, 0x315, 0x1EC }, - { 0x31B, 0x31B, 0x1ED }, - { 0x323, 0x323, 0x1EE }, - { 0x326, 0x328, 0x1EF }, - { 0x32C, 0x32C, 0x1F2 }, - { 0x337, 0x338, 0x1F3 }, - { 0x342, 0x343, 0x1F5 }, - { 0x346, 0x36F, 0x1F7 }, - { 0x400, 0x479, 0x221 }, - { 0x480, 0x49D, 0x29B }, - { 0x4A0, 0x4FF, 0x2B9 }, - { 0x5D0, 0x5EA, 0x319 }, - { 0x1EA0, 0x1EF9, 0x334 }, - { 0x2000, 0x2027, 0x38E }, - { 0x202A, 0x2055, 0x3B6 }, - { 0x2057, 0x2057, 0x3E2 }, - { 0x205F, 0x2064, 0x3E3 }, - { 0x2066, 0x206F, 0x3E9 }, - { 0x2070, 0x2071, 0x3F3 }, - { 0x2074, 0x208E, 0x3F5 }, - { 0x2090, 0x209C, 0x410 }, - { 0x20A0, 0x20AF, 0x41D }, - { 0x20B1, 0x20B5, 0x42D }, - { 0x20B8, 0x20BA, 0x432 }, - { 0x20BC, 0x20BF, 0x435 }, - { 0x2190, 0x2199, 0x439 }, - { 0x21A9, 0x21AA, 0x443 }, - { 0x21B0, 0x21B7, 0x445 }, - { 0x21BA, 0x21BB, 0x44D }, - { 0x21C4, 0x21C4, 0x44F }, - { 0x21C6, 0x21C6, 0x450 }, - { 0x21D0, 0x21D0, 0x451 }, - { 0x21D2, 0x21D2, 0x452 }, - { 0x21D4, 0x21D4, 0x453 }, - { 0x21DE, 0x21DF, 0x454 }, - { 0x21E4, 0x21E5, 0x456 }, - { 0x21E7, 0x21E7, 0x458 }, - { 0x21EA, 0x21EA, 0x459 }, - { 0x2202, 0x2202, 0x45A }, - { 0x2205, 0x2206, 0x45B }, - { 0x220F, 0x220F, 0x45D }, - { 0x2211, 0x2212, 0x45E }, - { 0x2215, 0x2215, 0x460 }, - { 0x221A, 0x221A, 0x461 }, - { 0x221E, 0x221E, 0x462 }, - { 0x222B, 0x222B, 0x463 }, - { 0x2236, 0x2236, 0x464 }, - { 0x2248, 0x2248, 0x465 }, - { 0x2260, 0x2260, 0x466 }, - { 0x2264, 0x2265, 0x467 }, - { 0x2295, 0x2298, 0x469 }, - { 0xFB01, 0xFB02, 0x46D }, + { 0x312, 0x313, 0x1EB }, + { 0x315, 0x315, 0x1ED }, + { 0x31B, 0x31B, 0x1EE }, + { 0x323, 0x323, 0x1EF }, + { 0x326, 0x328, 0x1F0 }, + { 0x32C, 0x32C, 0x1F3 }, + { 0x337, 0x338, 0x1F4 }, + { 0x342, 0x343, 0x1F6 }, + { 0x346, 0x36F, 0x1F8 }, + { 0x400, 0x479, 0x222 }, + { 0x480, 0x49D, 0x29C }, + { 0x4A0, 0x4FF, 0x2BA }, + { 0x5D0, 0x5EA, 0x31A }, + { 0x60C, 0x60C, 0x335 }, + { 0x61B, 0x61B, 0x336 }, + { 0x61F, 0x61F, 0x337 }, + { 0x621, 0x621, 0x338 }, + { 0x640, 0x640, 0x339 }, + { 0x660, 0x669, 0x33A }, + { 0x6BA, 0x6BA, 0x344 }, + { 0x6D4, 0x6D4, 0x345 }, + { 0x6F0, 0x6F9, 0x346 }, + { 0x1EA0, 0x1EF9, 0x350 }, + { 0x2000, 0x2027, 0x3AA }, + { 0x202A, 0x2055, 0x3D2 }, + { 0x2057, 0x2057, 0x3FE }, + { 0x205F, 0x2064, 0x3FF }, + { 0x2066, 0x2071, 0x405 }, + { 0x2074, 0x208E, 0x411 }, + { 0x2090, 0x209C, 0x42C }, + { 0x20A0, 0x20AF, 0x439 }, + { 0x20B1, 0x20B5, 0x449 }, + { 0x20B8, 0x20BA, 0x44E }, + { 0x20BC, 0x20BF, 0x451 }, + { 0x2190, 0x2199, 0x455 }, + { 0x21A9, 0x21AA, 0x45F }, + { 0x21B0, 0x21B7, 0x461 }, + { 0x21BA, 0x21BB, 0x469 }, + { 0x21C4, 0x21C4, 0x46B }, + { 0x21C6, 0x21C6, 0x46C }, + { 0x21D0, 0x21D0, 0x46D }, + { 0x21D2, 0x21D2, 0x46E }, + { 0x21D4, 0x21D4, 0x46F }, + { 0x21DE, 0x21DF, 0x470 }, + { 0x21E4, 0x21E5, 0x472 }, + { 0x21E7, 0x21E7, 0x474 }, + { 0x21EA, 0x21EA, 0x475 }, + { 0x2202, 0x2202, 0x476 }, + { 0x2205, 0x2206, 0x477 }, + { 0x220F, 0x220F, 0x479 }, + { 0x2211, 0x2212, 0x47A }, + { 0x2215, 0x2215, 0x47C }, + { 0x221A, 0x221A, 0x47D }, + { 0x221E, 0x221E, 0x47E }, + { 0x222B, 0x222B, 0x47F }, + { 0x2236, 0x2236, 0x480 }, + { 0x2248, 0x2248, 0x481 }, + { 0x2260, 0x2260, 0x482 }, + { 0x2264, 0x2265, 0x483 }, + { 0x2295, 0x2298, 0x485 }, + { 0xFB01, 0xFB02, 0x489 }, + { 0xFB56, 0xFB59, 0x48B }, + { 0xFB66, 0xFB69, 0x48F }, + { 0xFB7A, 0xFB7D, 0x493 }, + { 0xFB88, 0xFB95, 0x497 }, + { 0xFB9E, 0xFB9F, 0x4A5 }, + { 0xFBA6, 0xFBB1, 0x4A7 }, + { 0xFBFC, 0xFBFF, 0x4B3 }, + { 0xFE80, 0xFEFC, 0x4B7 }, }; static const EpdKernClassEntry inter_8_regularKernLeftClasses[] = { @@ -2994,6 +3406,10 @@ static const EpdKernClassEntry inter_8_regularKernLeftClasses[] = { { 0x2264, 20 }, // U+2264 { 0x2265, 20 }, // U+2265 { 0xFB02, 145 }, // U+FB02 + { 0xFB8C, 146 }, // U+FB8C + { 0xFEAD, 146 }, // U+FEAD + { 0xFEAE, 146 }, // U+FEAE + { 0xFEAF, 146 }, // U+FEAF }; static const EpdKernClassEntry inter_8_regularKernRightClasses[] = { @@ -3766,161 +4182,181 @@ static const EpdKernClassEntry inter_8_regularKernRightClasses[] = { { 0x2260, 24 }, // U+2260 { 0x2264, 24 }, // U+2264 { 0x2265, 24 }, // U+2265 + { 0xFB8E, 141 }, // U+FB8E + { 0xFBA6, 141 }, // U+FBA6 + { 0xFBAA, 141 }, // U+FBAA + { 0xFE8D, 142 }, // U+FE8D + { 0xFEA3, 141 }, // U+FEA3 + { 0xFEA9, 143 }, // U+FEA9 + { 0xFEB1, 143 }, // U+FEB1 + { 0xFEB3, 143 }, // U+FEB3 + { 0xFEB9, 143 }, // U+FEB9 + { 0xFEBB, 143 }, // U+FEBB + { 0xFEC1, 143 }, // U+FEC1 + { 0xFEC3, 143 }, // U+FEC3 + { 0xFECB, 142 }, // U+FECB + { 0xFEDB, 141 }, // U+FEDB + { 0xFEDF, 142 }, // U+FEDF + { 0xFEE1, 141 }, // U+FEE1 + { 0xFEE3, 141 }, // U+FEE3 + { 0xFEE9, 141 }, // U+FEE9 + { 0xFEEB, 141 }, // U+FEEB }; static const int8_t inter_8_regularKernMatrix[] = { - 0, 0, 0, 0, -9, 0, 0, 0, -22, 0, -22, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, -44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -12, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 12, 12, 0, 0, -2, -2, -2, 0, 0, 0, 0, 12, 0, 0, -3, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -18, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -17, -11, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, 0, 0, 0, 0, -11, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -15, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 8, -3, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -9, 0, 0, 0, -36, 0, -36, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -2, -21, 0, -2, -26, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -3, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -12, -20, 0, -14, -14, -10, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -29, -29, -20, 0, 0, 0, 0, 0, 0, 0, -39, -17, 0, 0, -38, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -24, 0, -6, 0, -4, -4, -5, -2, 0, 0, 0, -26, -12, 0, 0, -12, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, 0, -6, -12, 0, -9, 0, 0, 0, -12, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -15, 0, -36, -36, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -4, 0, -9, -9, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -12, 0, -14, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 1, 0, 9, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 13, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -5, 0, -6, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -7, 0, -9, 0, -9, 0, 0, -6, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -7, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, -7, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, -15, 0, 1, -12, 0, 1, 0, -33, 0, -33, 0, -4, 0, 0, -4, -15, -3, 5, -4, -3, -9, -24, 0, 0, 0, -29, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 1, -42, -12, -14, 0, 0, 0, -14, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -12, -14, 0, 0, 0, 24, -14, 0, 0, -12, 0, 0, 24, 24, 0, -14, 0, 0, 0, 0, 0, 0, 0, 24, -28, -28, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -12, 0, -18, -17, -14, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -12, -12, -12, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, -20, -20, -15, -24, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -21, -21, 0, 0, 0, 0, 0, -9, -9, -14, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -9, 0, -9, -1, -11, -10, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -9, 0, -11, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, -10, -27, 0, 0, 0, 0, - -21, 0, 0, 0, 0, 0, -21, -9, 0, -9, 0, 5, -1, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -17, -9, 4, 0, -9, 0, -23, -6, -20, -15, 0, -22, 0, -21, 0, 0, -1, 0, 1, 0, 0, 0, -6, 0, -18, -18, 0, 0, -9, 0, -20, -9, -9, 0, -19, -21, 0, 0, -13, -9, 0, -9, 0, 0, 0, -9, 0, 0, -1, 1, 1, 1, 0, -1, 0, -18, 0, 0, 0, 0, 0, 0, -1, -23, -23, -39, 0, 0, 1, 0, 0, 0, 0, 0, 0, -9, 0, -23, 0, 0, -6, -19, -6, 0, -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, -35, -27, 0, 0, 0, -9, 0, -27, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, -14, -14, -14, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 15, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -20, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -24, 0, -1, -18, 0, 0, 0, 0, 0, 0, 0, 0, -9, -6, -9, 0, 0, 0, -7, 0, 0, -9, -9, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -6, -6, -9, 0, 0, 0, 0, -9, -9, -9, -6, 0, 0, 0, 0, 0, -9, 0, 0, 0, -9, 0, 0, 31, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, -8, 0, -6, 0, -1, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, -8, -8, -8, 0, 0, 0, 6, 9, 0, 0, -1, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -3, 0, 0, 0, 0, -29, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -9, 0, -9, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 3, 0, 0, 0, 0, -9, -11, -17, 0, 0, -29, 0, 0, -14, -42, 0, -4, 0, 0, -9, 0, -41, 0, 0, 0, 0, 0, -9, 0, 0, -11, 3, 3, 3, 23, -11, -9, -11, 0, 0, 0, 23, 23, 0, -11, 0, 0, 0, -9, 0, 3, 23, 23, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, - -24, 0, 0, 0, 0, 0, -45, -23, 0, -36, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -8, 0, 0, -8, 0, -26, 0, -18, 0, 0, -15, 0, -35, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -18, 0, 0, 0, -33, 0, -26, -17, -57, 0, -21, -47, 0, 0, -23, -39, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -26, 0, 0, -6, -40, -6, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -23, -10, 0, -12, -12, -9, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -9, 0, 0, -12, -11, -12, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, -26, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -3, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -1, -1, -3, 0, 0, 0, 0, -3, 0, 0, -1, 0, 0, 0, 0, 0, -3, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -4, 0, 0, -6, 0, 0, 0, -2, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -4, 0, 0, 0, 12, -4, 0, 0, -2, 0, 0, 12, 12, 0, -4, -1, -1, -1, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -9, 0, 0, -20, -9, -20, -9, -35, -9, 0, 0, 0, -18, 0, -21, 0, 0, -9, -21, -9, 0, -9, -23, 0, -9, -24, 0, 0, 0, 0, 0, 4, 0, 0, -15, -22, -23, -1, 0, 0, -17, -22, -22, -21, -20, -19, -19, -15, -20, 0, 0, -35, -20, 0, 0, 0, 0, -48, 0, -41, 0, -14, 0, 0, 0, -9, -32, -32, -37, 0, 0, 0, 24, -37, -37, -29, -22, 0, 0, 24, 24, 0, -23, 0, 0, 0, -31, 0, 0, 34, 24, -35, -35, 0, 0, -9, 0, 0, 0, 0, -22, -17, -22, -22, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, -32, 0, 0, -9, -32, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, - 0, 0, 0, 0, -14, 0, 0, -14, -26, -14, -26, -13, -4, 0, 0, 0, -9, 0, 0, 0, 0, -17, -20, -17, 0, -18, -20, 0, -9, -26, 0, 0, 4, 0, 0, 0, 0, 0, -21, -14, -15, 0, 0, 0, -2, -12, 0, 0, 0, 0, 0, 0, -14, 0, 0, -15, -30, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, -11, -14, -25, -15, 0, 0, 0, 23, -27, 0, 0, -14, 0, 0, 23, 23, 0, -15, 0, 0, 0, 0, 0, 0, 11, 23, -32, -32, 0, 0, -9, 0, 0, 0, 0, 0, -4, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, -11, -26, 0, 0, 0, 0, 0, 0, 0, -35, -41, 0, 0, 0, 0, - 0, 0, 0, 0, -14, 0, 0, -14, -26, -14, -26, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -14, -20, -17, 0, -1, -15, 0, -1, -17, 0, 0, 0, 6, 0, 0, 0, 0, 0, -14, -14, 0, 0, 0, -8, -9, 0, 0, 0, 0, 0, 0, -14, 0, 0, -15, -14, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -14, -14, -14, 0, 0, 0, 0, -14, 0, 0, -14, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -8, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, -24, -28, 0, 0, 0, 0, - 0, 0, -3, 0, 0, 0, 0, -10, 0, -10, 0, 0, -9, 0, 0, -5, -11, 0, 0, -5, 0, 0, -15, -14, 0, -11, 0, 0, -11, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -15, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, -11, -24, 0, -2, 0, 0, 0, 0, -22, 0, 0, 0, 0, 0, -12, -15, -15, -8, 0, 0, 0, 0, -8, 0, 0, -15, 0, 0, 0, 0, 0, -8, -8, -8, -8, 0, 0, 0, 23, 0, 0, 0, 0, -5, -24, -9, -8, 0, 0, -17, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, - 0, 0, 0, 0, -17, 0, 0, -21, -9, -21, -9, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, -17, -27, -26, 0, -10, -22, 0, -10, -9, 4, 0, 0, 0, 0, 0, 0, 0, 0, -20, -22, 0, -8, 0, -12, -18, 0, -11, 0, 0, 0, 0, -21, 0, 0, -24, -35, 0, -2, 0, 0, -11, 0, -35, 0, -17, 0, 0, 0, -23, -39, -33, -22, -8, -15, -8, 0, -22, -11, 0, -34, 0, 0, 0, 0, 0, -37, 4, 4, 4, -11, 0, -8, 23, 0, 0, 0, 0, 0, -10, 0, 4, 0, 0, 0, -15, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -11, -35, 0, 0, 0, 0, 0, 0, 0, -40, -47, 0, 0, 0, -9, - 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, -9, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -9, -11, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 9, 23, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 8, -3, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -21, 0, 0, 0, 0, 0, -21, -9, 0, -9, 0, 5, -1, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -17, -9, 4, 0, -9, 0, -34, -6, -33, -30, 0, -22, 0, -21, 0, 0, -1, 0, 1, 0, 0, 0, -6, 0, -32, -18, 0, 0, -9, 0, -20, -9, -9, 0, -19, -21, 0, 0, -13, -9, 0, -9, 0, 0, 0, -9, 0, 0, -1, 1, 1, 1, 0, -1, 0, -32, 0, 0, 0, 0, 0, 0, -1, -34, -34, -34, 0, 0, 1, 0, 0, 0, 0, 0, 0, -9, 0, -34, 0, 0, -6, -19, -6, 0, -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, -46, -42, 0, 0, 0, -9, 0, -42, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, - 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, -12, -29, 0, -12, -15, -12, 0, -12, -12, 0, 0, 0, 0, -11, 0, 7, -11, 0, -15, -11, -21, 0, 0, 0, 0, -18, 0, 0, 0, 0, 7, 26, 7, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, -21, 0, 0, 7, 0, 0, 26, 0, -15, -15, -15, 0, 7, 7, 0, 0, 15, 15, 0, -12, -11, -29, -14, 12, 12, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, - -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, -14, -14, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -12, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -6, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -24, 0, -15, -14, -8, -23, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, -5, -5, -6, -4, 0, 0, -9, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, -5, 2, 0, 0, 0, 0, 0, 0, -24, -24, -24, 0, 0, 0, 9, 0, 0, 0, -8, 0, 0, 0, -24, 0, 0, 0, -3, 0, 0, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -17, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -8, -15, -8, -15, -8, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -17, 0, 0, 0, 0, 0, 3, 0, 0, -4, -4, -7, -10, 0, 0, 0, -6, -9, 0, 0, 5, 0, 0, -8, 0, 0, -11, -8, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, -6, -4, -4, -7, 0, 0, 0, 27, -7, 0, 0, -4, 0, 0, 27, 38, 0, -7, 0, 0, 0, 0, 0, 0, 12, 36, -18, -18, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, -6, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -24, 0, 0, -6, -15, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -14, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 28, 19, 0, 0, 0, 0, 0, 0, 0, 0, 23, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, -14, -30, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -6, -15, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -11, -17, -11, -17, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 3, 0, -15, -15, 0, 0, 0, 0, 0, -9, 0, 0, -1, -4, 4, 2, 3, 3, 0, 4, 0, 4, 2, 3, 0, -11, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -1, -1, -4, 2, 2, 2, 7, -4, 0, 4, -1, 0, 3, 7, 7, 3, -4, -15, -15, -15, 0, 3, 2, 0, 7, 0, 0, 0, 0, 0, 0, -15, -6, -6, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 4, -21, 0, 0, 0, -17, 4, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, - -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, -12, -9, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 1, -1, 1, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -14, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -5, 0, -5, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, -9, 0, -3, -3, 0, -4, 0, 0, 0, 2, -3, -6, 0, 3, 0, -3, -2, 0, 6, 4, 0, 0, -5, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, -3, 2, 2, -3, 0, 0, 0, 0, -3, 0, 6, 2, 0, 0, 0, 0, 3, -3, -9, -9, -9, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -2, 0, -2, -3, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -11, 2, 0, 0, 0, -3, 0, 2, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -15, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -14, -33, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -11, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -9, -13, -9, -13, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -11, 0, 0, -18, 1, 0, -17, -9, 0, 0, 0, 0, -9, 0, 0, -21, -2, -5, 4, 0, 0, 0, -1, 4, 0, 0, 0, 0, 0, -9, 0, 0, -11, -9, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, -2, -2, -5, 0, 0, 0, 0, -5, 0, 0, -2, 0, 1, 0, 0, 0, -5, -9, -26, -9, 0, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, -9, -12, -12, 4, 0, 4, -1, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 2, -21, 0, 0, 0, -33, 2, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, - 0, 0, 0, 0, -9, 0, 0, 0, -20, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, -15, -14, 0, 0, 0, 0, 0, -9, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, -5, -14, -14, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -21, 0, 0, 0, -20, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -6, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, -1, -6, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, -6, 0, 0, -9, -6, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, -1, -1, -6, 0, 0, 0, 0, -6, 0, 0, -1, 0, 0, 0, 0, 0, -6, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, -4, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -3, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -12, -20, 0, -14, -14, -19, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -29, -29, -20, 0, 0, 0, 0, 0, 0, 0, -19, -17, 0, 0, -38, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -9, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -14, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -14, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -1, -19, 0, -1, -19, 0, 0, 0, 0, -2, -2, 0, 0, 0, -3, -8, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -3, -8, 0, 0, 0, 0, -8, 0, 0, -3, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -1, 0, 0, 0, 0, 0, 0, 0, -3, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -15, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -14, -33, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -24, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -3, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -12, -41, 0, -24, -14, -22, -35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -41, -41, -41, 0, 0, 0, 0, 0, 0, 0, -22, -17, 0, 0, -41, 0, 0, 0, 0, 0, 0, -35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -35, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -14, -12, 0, 0, -14, -9, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -35, 0, -17, -15, -11, -24, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -35, -35, -35, 0, 0, 0, 0, 0, -9, -9, -24, -12, 0, 0, -53, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -22, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 11, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -24, 0, -11, 0, -12, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, -24, -24, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -11, -15, 0, -14, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -33, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, -14, -14, -14, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -21, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 29, 28, 0, 0, 0, 0, 0, 0, 0, 0, 20, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 24, 0, 23, 0, 0, 0, 24, 0, 0, 0, 0, 11, 19, 18, 7, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 28, 19, 28, 46, 0, 0, 0, 0, 0, 17, 46, 46, 27, 0, 24, 24, 24, 0, 36, 28, 14, 46, 0, 0, 0, 0, 0, 0, 24, 0, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, -14, -14, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -33, -33, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -12, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -6, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -37, 0, -15, -14, -8, -23, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, -5, -5, -6, -4, 0, 0, -9, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, -5, 2, 0, 0, 0, 0, 0, 0, -37, -37, -37, 0, 0, 0, 9, 0, 0, 0, -8, 0, 0, 0, -37, 0, 0, 0, -3, 0, 0, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -17, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, -14, -14, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -40, -40, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -12, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 9, 0, 0, 0, 0, 26, 15, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 11, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 11, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 4, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 23, 13, 0, 0, 0, 0, 0, 9, 13, 13, 7, 0, 11, 11, 11, 0, 9, 24, 12, 13, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 11, 11, 0, 0, 0, 0, 0, 11, 11, 11, 11, 0, 9, 9, 9, 0, 9, 9, 11, 11, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 24, 0, 23, 0, 0, 0, 24, 0, 0, 0, 0, 11, 29, 27, 7, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 29, 29, 29, 46, 0, 0, 0, 0, 0, 17, 46, 46, 27, 0, 24, 24, 24, 0, 36, 29, 29, 46, 0, 0, 0, 0, 0, 0, 24, 0, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 24, 0, 23, 0, 0, 0, 24, 0, 0, 0, 0, 11, 29, 29, 7, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 29, 29, 29, 46, 0, 0, 0, 0, 0, 17, 46, 46, 29, 0, 24, 24, 24, 0, 36, 29, 14, 46, 0, 0, 0, 0, 0, 0, 24, 0, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 24, 0, 23, 0, 0, 0, 24, 0, 0, 0, 0, 11, 29, 18, 7, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 29, 29, 29, 46, 0, 0, 0, 0, 0, 17, 46, 46, 27, 0, 24, 24, 24, 0, 36, 29, 14, 46, 0, 0, 0, 0, 0, 0, 24, 0, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 29, 28, 0, 0, 0, 0, 0, 0, 0, 0, 20, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -24, 0, 0, 0, 0, 0, -47, -36, 0, -36, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -8, 0, 0, -8, 0, -26, 0, -18, 0, 0, -15, 0, -47, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -18, 0, 0, 0, -35, 0, -26, -17, -36, 0, -21, -47, 0, 0, -23, -36, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -26, 0, 0, -6, -40, -6, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -23, -10, 0, -12, -12, -9, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 1, 0, 9, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 23, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -24, 0, 0, 0, 0, 0, -45, -33, 0, -33, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -8, 0, 0, -8, 0, -26, 0, -18, 0, 0, -15, 0, -35, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -18, 0, 0, 0, -32, 0, -26, -17, -33, 0, -21, -47, 0, 0, -23, -33, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -26, 0, 0, -6, -40, -6, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -23, -10, 0, -12, -12, -9, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 1, 0, 9, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 13, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -24, 0, 0, 0, 0, 0, -44, -23, 0, -36, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -8, 0, 0, -8, 0, -26, 0, -18, 0, 0, -30, 0, -44, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -18, 0, 0, 0, -33, 0, -26, -17, -57, 0, -21, -44, 0, 0, -23, -39, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -26, 0, 0, -6, -40, -6, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -23, -19, 0, -12, -12, -9, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 1, 0, 9, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 13, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -9, 0, 0, -29, -9, -29, -9, -35, -9, 0, 0, 0, -18, 0, -21, 0, 0, -9, -21, -9, 0, -9, -23, 0, -9, -24, 0, 0, 0, 0, 0, 4, 0, 0, -15, -22, -23, -1, 0, 0, -17, -22, -22, -30, -20, -19, -38, -24, -29, 0, 0, -35, -29, 0, 0, 0, 0, -30, 0, -29, 0, -14, 0, 0, 0, -9, -32, -32, -37, 0, 0, 0, 24, -37, -30, -29, -22, 0, 0, 24, 24, 0, -23, 0, 0, 0, -30, 0, 0, 34, 24, -35, -35, 0, 0, -9, 0, 0, 0, 0, -22, -17, -22, -22, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, -32, 0, 0, -9, -32, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, - 0, 0, 0, 0, -9, 0, 0, -29, -9, -29, -9, -35, -9, 0, 0, 0, -18, 0, -21, 0, 0, -9, -21, -9, 0, -9, -23, 0, -9, -24, 0, 0, 0, 0, 0, 4, 0, 0, -15, -22, -23, -1, 0, 0, -17, -22, -22, -21, -20, -19, -19, -15, -29, 0, 0, -35, -29, 0, 0, 0, 0, -48, 0, -29, 0, -14, 0, 0, 0, -9, -32, -32, -37, 0, 0, 0, 24, -37, -37, -29, -22, 0, 0, 24, 24, 0, -23, 0, 0, 0, -31, 0, 0, 34, 24, -35, -35, 0, 0, -9, 0, 0, 0, 0, -22, -17, -22, -22, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, -32, 0, 0, -9, -32, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, - 0, 0, 0, 0, -9, 0, 0, -20, -9, -20, -9, -35, -9, 0, 0, 0, -18, 0, -21, 0, 0, -9, -21, -9, 0, -9, -39, 0, -9, -56, 0, 0, 0, 0, 0, 4, 0, 0, -15, -22, -23, -1, 0, 0, -17, -22, -22, -21, -20, -19, -19, -15, -20, 0, 0, -35, -20, 0, 0, 0, 0, -48, 0, -41, 0, -14, 0, 0, 0, -9, -32, -32, -37, 0, 0, 0, 24, -37, -37, -29, -22, 0, 0, 24, 24, 0, -23, 0, 0, 0, -31, 0, 0, 34, 24, -35, -35, 0, 0, -9, 0, 0, 0, 0, -22, -17, -22, -22, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, -32, 0, 0, -9, -32, 0, 0, 0, 0, 0, 0, 0, 0, -53, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -17, -8, 0, 0, -14, -14, -8, -14, -14, -14, -14, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, -14, -14, -17, 0, 0, 0, 0, -17, -14, -14, -14, 0, 0, 0, 0, 0, -17, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 8, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, -10, 0, 4, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, -4, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 28, 28, 0, 0, 0, 0, 0, 0, 0, 0, 31, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -6, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -45, 0, -15, -14, -8, -23, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, -5, -5, -6, -4, 0, 0, -9, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, -5, 2, 0, 0, 0, 0, 0, 0, -45, -45, -45, 0, 0, 0, 9, 0, 0, 0, -8, 0, 0, 0, -45, 0, 0, 0, -3, 0, 0, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -17, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, -15, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -11, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 20, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 28, 28, 0, 0, 0, 0, 0, 0, 0, 0, 23, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 24, 0, 23, 0, 0, 0, 24, 0, 0, 0, 0, 11, 28, 29, 7, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 28, 28, 28, 46, 0, 0, 0, 0, 0, 17, 46, 46, 29, 0, 24, 24, 24, 0, 36, 28, 26, 57, 0, 0, 0, 0, 0, 0, 24, 0, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -3, 0, 0, 0, 0, -39, 0, -39, 0, 0, -9, 0, 0, -5, -11, 0, 0, -5, 0, 0, -14, -14, 0, -11, 0, 0, -11, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -15, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, 0, 0, -24, -39, 0, -2, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, -12, -15, -15, -8, 0, 0, 0, 0, -8, 0, 0, -15, 0, 0, 0, 0, 0, -8, -8, -8, -8, 0, 0, 0, 23, 0, 0, 0, 0, -5, -24, -9, -8, 0, 0, -17, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -9, 0, -9, -1, -11, -10, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -9, 0, -24, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, -10, -27, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -7, 0, 0, 0, 0, 0, -7, -7, 4, -7, 4, 0, -3, 0, 0, 0, -6, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -4, -7, 0, 0, 0, 0, -7, 0, -5, -7, 0, 0, -7, 0, 0, 0, -7, 0, 0, -7, 0, 0, -7, -7, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, -5, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -4, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 4, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -11, -33, -11, -33, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 3, 0, -15, -15, 0, 0, 0, 0, 0, -9, 0, 0, -1, -4, 4, 2, 3, 3, 0, 4, 0, 4, 2, 3, 0, -11, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -1, -1, -4, 2, 2, 2, 7, -4, 0, 4, -1, 0, 3, 7, 7, 3, -4, -15, -15, -15, 0, 3, 2, 0, 7, 0, 0, 0, 0, 0, 0, -15, -6, -6, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 4, -41, 0, 0, 0, -33, 4, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, - 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, -26, 0, -8, -45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -8, -8, -6, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -26, 0, 0, -7, 0, -31, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -17, 0, 0, -21, -9, -21, -9, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, -17, -27, -26, 0, -10, -22, 0, -10, -9, 4, 0, 0, 0, 0, 0, 0, 0, 0, -20, -43, 0, -8, 0, -12, -18, 0, -11, 0, 0, 0, 0, -21, 0, 0, -24, -35, 0, -2, 0, 0, -11, 0, -35, 0, -17, 0, 0, 0, -23, -39, -33, -43, -8, -15, -8, 0, -43, -11, 0, -34, 0, 0, 0, 0, 0, -43, 4, 4, 4, -11, 0, -8, 23, 0, 0, 0, 0, 0, -10, 0, 4, 0, 0, 0, -15, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -11, -35, 0, 0, 0, 0, 0, 0, 0, -40, -47, 0, 0, 0, -9, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -8, -8, 4, -8, 0, -8, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 28, 19, 0, 0, 0, 0, 0, 0, 0, 0, 20, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -6, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -24, 0, -15, -14, -8, -23, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, -5, -5, -6, -4, 0, 0, -9, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, -5, 2, 0, 0, 0, 0, 0, 0, -24, -24, -24, 0, 0, 0, 19, 0, 0, 0, -8, 0, 0, 0, -24, 0, 0, 0, -3, 0, 0, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -17, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -17, 0, 0, -21, -18, -21, -18, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, -17, -27, -26, 0, -10, -22, 0, -10, -9, 4, 0, 0, 0, 0, 0, 0, 0, 0, -20, -22, 0, -8, 0, -12, -18, 0, -11, 0, 0, 0, 0, -21, 0, 0, -24, -35, 0, -2, 0, 0, -11, 0, -35, 0, -17, 0, 0, 0, -23, -39, -33, -22, -8, -15, -8, 0, -22, -11, 0, -34, 0, 0, 0, 0, 0, -37, 4, 4, 4, -11, 0, -8, 23, 0, 0, 0, 0, 0, -10, 0, 4, 0, 0, 0, -15, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -11, -18, 0, 0, 0, 0, 0, 0, 0, -40, -47, 0, 0, 0, -9, - 0, 0, 0, 0, -17, 0, 0, 0, -23, 0, -23, -26, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -17, -30, 0, 0, 0, 0, 0, -38, -23, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, -33, 0, 0, -18, 0, 0, -17, -26, -17, -26, -38, -12, 0, 0, 0, -26, 0, -17, -11, 0, -15, -26, -15, 0, -15, -36, 0, -15, -33, 0, 0, 0, 0, 0, 0, 0, 0, -11, -15, -17, 0, 0, 0, -6, -20, 4, -11, 4, 0, 0, 0, -29, 0, 0, -23, -17, 0, 0, 0, 0, -23, -3, -17, 0, 0, -18, 0, 0, 0, -15, -15, -17, 0, 0, 0, 0, -17, -11, 4, -15, 0, 0, 0, 0, 0, -17, 0, 0, 0, -11, 0, 0, 0, 0, -14, -24, 0, 0, -15, 0, 0, -32, -17, 4, 0, 4, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, -24, -17, 0, -29, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, 0, 0, 0, 0, 0, 0, 0, -14, -9, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 14, 14, 0, 0, 0, -17, -39, 17, 17, -14, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -33, -26, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -9, 0, -11, 0, -12, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, - -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -24, 0, -6, 0, -4, -4, -5, -2, 0, 0, 0, -26, -12, 0, 0, -12, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, 0, -6, -12, 0, -9, 0, 0, 0, -12, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -15, 0, -36, -48, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 3, -2, 0, -9, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, -10, 0, -33, 0, -35, -24, 0, -40, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 4, 0, -33, -33, -53, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -11, -42, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -4, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -9, 0, 0, 0, -22, 0, -22, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, -44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -12, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 12, 12, 0, 0, -2, -2, -2, 0, 0, 0, 0, 12, 0, 0, -3, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -18, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -17, -11, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, 0, 0, 0, 0, -11, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -15, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 8, -3, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -9, 0, 0, 0, -36, 0, -36, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -2, -21, 0, -2, -26, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -3, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -12, -20, 0, -14, -14, -10, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -29, -29, -20, 0, 0, 0, 0, 0, 0, 0, -39, -17, 0, 0, -38, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -24, 0, -6, 0, -4, -4, -5, -2, 0, 0, 0, -26, -12, 0, 0, -12, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, 0, -6, -12, 0, -9, 0, 0, 0, -12, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -15, 0, -36, -36, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -7, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -4, 0, -9, -9, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -12, 0, -14, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 1, 0, 9, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 13, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -5, 0, -6, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -7, 0, -9, 0, -9, 0, 0, -6, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -7, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -1, 0, -7, 0, -7, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -15, 0, 1, -12, 0, 1, 0, -33, 0, -33, 0, -4, 0, 0, -4, -15, -3, 5, -4, -3, -9, -24, 0, 0, 0, -29, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 1, -42, -12, -14, 0, 0, 0, -14, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -12, -14, 0, 0, 0, 24, -14, 0, 0, -12, 0, 0, 24, 24, 0, -14, 0, 0, 0, 0, 0, 0, 0, 24, -28, -28, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -12, 0, -18, -17, -14, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -12, -12, -12, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, -20, -20, -15, -24, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -21, -21, 0, 0, 0, 0, 0, -9, -9, -14, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -9, 0, -9, -1, -11, -10, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -9, 0, -11, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, -10, -27, 0, 0, 0, 0, 0, 0, 0, + -21, 0, 0, 0, 0, 0, -21, -9, 0, -9, 0, 5, -1, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -17, -9, 4, 0, -9, 0, -23, -6, -20, -15, 0, -22, 0, -21, 0, 0, -1, 0, 1, 0, 0, 0, -6, 0, -18, -18, 0, 0, -9, 0, -20, -9, -9, 0, -19, -21, 0, 0, -13, -9, 0, -9, 0, 0, 0, -9, 0, 0, -1, 1, 1, 1, 0, -1, 0, -18, 0, 0, 0, 0, 0, 0, -1, -23, -23, -39, 0, 0, 1, 0, 0, 0, 0, 0, 0, -9, 0, -23, 0, 0, -6, -19, -6, 0, -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, -35, -27, 0, 0, 0, -9, 0, -27, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, -14, -14, -14, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 15, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -20, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -24, 0, -1, -18, 0, 0, 0, 0, 0, 0, 0, 0, -9, -6, -9, 0, 0, 0, -7, 0, 0, -9, -9, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -6, -6, -9, 0, 0, 0, 0, -9, -9, -9, -6, 0, 0, 0, 0, 0, -9, 0, 0, 0, -9, 0, 0, 31, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, -8, 0, -6, 0, -1, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, -8, -8, -8, 0, 0, 0, 6, 9, 0, 0, -1, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -3, 0, 0, 0, 0, -29, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -9, 0, -9, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 3, 0, 0, 0, 0, -9, -11, -17, 0, 0, -29, 0, 0, -14, -42, 0, -4, 0, 0, -9, 0, -41, 0, 0, 0, 0, 0, -9, 0, 0, -11, 3, 3, 3, 23, -11, -9, -11, 0, 0, 0, 23, 23, 0, -11, 0, 0, 0, -9, 0, 3, 23, 23, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, + -24, 0, 0, 0, 0, 0, -45, -23, 0, -36, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -8, 0, 0, -8, 0, -26, 0, -18, 0, 0, -15, 0, -35, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -18, 0, 0, 0, -33, 0, -26, -17, -57, 0, -21, -47, 0, 0, -23, -39, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -26, 0, 0, -6, -40, -6, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -23, -10, 0, -12, -12, -9, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -9, 0, 0, -12, -11, -12, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, -26, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -3, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, -1, -1, -3, 0, 0, 0, 0, -3, 0, 0, -1, 0, 0, 0, 0, 0, -3, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -38, 0, 0, 0, -36, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -4, 0, 0, -6, 0, 0, 0, -2, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -4, 0, 0, 0, 12, -4, 0, 0, -2, 0, 0, 12, 12, 0, -4, -1, -1, -1, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -9, 0, 0, -20, -9, -20, -9, -35, -9, 0, 0, 0, -18, 0, -21, 0, 0, -9, -21, -9, 0, -9, -23, 0, -9, -24, 0, 0, 0, 0, 0, 4, 0, 0, -15, -22, -23, -1, 0, 0, -17, -22, -22, -21, -20, -19, -19, -15, -20, 0, 0, -35, -20, 0, 0, 0, 0, -48, 0, -41, 0, -14, 0, 0, 0, -9, -32, -32, -37, 0, 0, 0, 24, -37, -37, -29, -22, 0, 0, 24, 24, 0, -23, 0, 0, 0, -31, 0, 0, 34, 24, -35, -35, 0, 0, -9, 0, 0, 0, 0, -22, -17, -22, -22, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, -32, 0, 0, -9, -32, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -14, 0, 0, -14, -26, -14, -26, -13, -4, 0, 0, 0, -9, 0, 0, 0, 0, -17, -20, -17, 0, -18, -20, 0, -9, -26, 0, 0, 4, 0, 0, 0, 0, 0, -21, -14, -15, 0, 0, 0, -2, -12, 0, 0, 0, 0, 0, 0, -14, 0, 0, -15, -30, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, -11, -14, -25, -15, 0, 0, 0, 23, -27, 0, 0, -14, 0, 0, 23, 23, 0, -15, 0, 0, 0, 0, 0, 0, 11, 23, -32, -32, 0, 0, -9, 0, 0, 0, 0, 0, -4, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, 0, 0, -11, -26, 0, 0, 0, 0, 0, 0, 0, -35, -41, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -14, 0, 0, -14, -26, -14, -26, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -14, -20, -17, 0, -1, -15, 0, -1, -17, 0, 0, 0, 6, 0, 0, 0, 0, 0, -14, -14, 0, 0, 0, -8, -9, 0, 0, 0, 0, 0, 0, -14, 0, 0, -15, -14, 0, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, -14, -14, -14, 0, 0, 0, 0, -14, 0, 0, -14, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -8, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, -24, -28, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -3, 0, 0, 0, 0, -10, 0, -10, 0, 0, -9, 0, 0, -5, -11, 0, 0, -5, 0, 0, -15, -14, 0, -11, 0, 0, -11, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -15, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, 0, -11, -24, 0, -2, 0, 0, 0, 0, -22, 0, 0, 0, 0, 0, -12, -15, -15, -8, 0, 0, 0, 0, -8, 0, 0, -15, 0, 0, 0, 0, 0, -8, -8, -8, -8, 0, 0, 0, 23, 0, 0, 0, 0, -5, -24, -9, -8, 0, 0, -17, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -17, 0, 0, -21, -9, -21, -9, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, -17, -27, -26, 0, -10, -22, 0, -10, -9, 4, 0, 0, 0, 0, 0, 0, 0, 0, -20, -22, 0, -8, 0, -12, -18, 0, -11, 0, 0, 0, 0, -21, 0, 0, -24, -35, 0, -2, 0, 0, -11, 0, -35, 0, -17, 0, 0, 0, -23, -39, -33, -22, -8, -15, -8, 0, -22, -11, 0, -34, 0, 0, 0, 0, 0, -37, 4, 4, 4, -11, 0, -8, 23, 0, 0, 0, 0, 0, -10, 0, 4, 0, 0, 0, -15, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -11, -35, 0, 0, 0, 0, 0, 0, 0, -40, -47, 0, 0, 0, -9, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, -9, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -9, -11, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 9, 23, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 8, -3, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -21, 0, 0, 0, 0, 0, -21, -9, 0, -9, 0, 5, -1, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -17, -9, 4, 0, -9, 0, -34, -6, -33, -30, 0, -22, 0, -21, 0, 0, -1, 0, 1, 0, 0, 0, -6, 0, -32, -18, 0, 0, -9, 0, -20, -9, -9, 0, -19, -21, 0, 0, -13, -9, 0, -9, 0, 0, 0, -9, 0, 0, -1, 1, 1, 1, 0, -1, 0, -32, 0, 0, 0, 0, 0, 0, -1, -34, -34, -34, 0, 0, 1, 0, 0, 0, 0, 0, 0, -9, 0, -34, 0, 0, -6, -19, -6, 0, -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, -46, -42, 0, 0, 0, -9, 0, -42, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, -12, -29, 0, -12, -15, -12, 0, -12, -12, 0, 0, 0, 0, -11, 0, 7, -11, 0, -15, -11, -21, 0, 0, 0, 0, -18, 0, 0, 0, 0, 7, 26, 7, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, -21, 0, 0, 7, 0, 0, 26, 0, -15, -15, -15, 0, 7, 7, 0, 0, 15, 15, 0, -12, -11, -29, -14, 12, 12, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, + -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, -14, -14, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -12, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -6, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -24, 0, -15, -14, -8, -23, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, -5, -5, -6, -4, 0, 0, -9, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, -5, 2, 0, 0, 0, 0, 0, 0, -24, -24, -24, 0, 0, 0, 9, 0, 0, 0, -8, 0, 0, 0, -24, 0, 0, 0, -3, 0, 0, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -17, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -8, -15, -8, -15, -8, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -17, 0, 0, 0, 0, 0, 3, 0, 0, -4, -4, -7, -10, 0, 0, 0, -6, -9, 0, 0, 5, 0, 0, -8, 0, 0, -11, -8, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, -6, -4, -4, -7, 0, 0, 0, 27, -7, 0, 0, -4, 0, 0, 27, 38, 0, -7, 0, 0, 0, 0, 0, 0, 12, 36, -18, -18, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, -6, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, -24, 0, 0, -6, -15, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -14, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 28, 19, 0, 0, 0, 0, 0, 0, 0, 0, 23, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, -14, -30, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -6, -15, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -11, -17, -11, -17, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 3, 0, -15, -15, 0, 0, 0, 0, 0, -9, 0, 0, -1, -4, 4, 2, 3, 3, 0, 4, 0, 4, 2, 3, 0, -11, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -1, -1, -4, 2, 2, 2, 7, -4, 0, 4, -1, 0, 3, 7, 7, 3, -4, -15, -15, -15, 0, 3, 2, 0, 7, 0, 0, 0, 0, 0, 0, -15, -6, -6, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 4, -21, 0, 0, 0, -17, 4, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, + -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, -12, -9, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 1, -1, 1, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -14, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -5, 0, -5, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, -9, 0, -3, -3, 0, -4, 0, 0, 0, 2, -3, -6, 0, 3, 0, -3, -2, 0, 6, 4, 0, 0, -5, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, -3, 2, 2, -3, 0, 0, 0, 0, -3, 0, 6, 2, 0, 0, 0, 0, 3, -3, -9, -9, -9, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -2, 0, -2, -3, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -11, 2, 0, 0, 0, -3, 0, 2, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -15, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -14, -33, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -11, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -9, -13, -9, -13, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -11, 0, 0, -18, 1, 0, -17, -9, 0, 0, 0, 0, -9, 0, 0, -21, -2, -5, 4, 0, 0, 0, -1, 4, 0, 0, 0, 0, 0, -9, 0, 0, -11, -9, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, -2, -2, -5, 0, 0, 0, 0, -5, 0, 0, -2, 0, 1, 0, 0, 0, -5, -9, -26, -9, 0, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, -9, -12, -12, 4, 0, 4, -1, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 2, -21, 0, 0, 0, -33, 2, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -9, 0, 0, 0, -20, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, -15, -14, 0, 0, 0, 0, 0, -9, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, -5, -14, -14, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -21, 0, 0, 0, -20, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -6, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, -1, -6, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, -6, 0, 0, -9, -6, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, -1, -1, -6, 0, 0, 0, 0, -6, 0, 0, -1, 0, 0, 0, 0, 0, -6, -17, -17, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, -4, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -3, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -12, -20, 0, -14, -14, -19, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -29, -29, -20, 0, 0, 0, 0, 0, 0, 0, -19, -17, 0, 0, -38, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -9, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -14, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -14, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, -1, -19, 0, -1, -19, 0, 0, 0, 0, -2, -2, 0, 0, 0, -3, -8, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -3, -8, 0, 0, 0, 0, -8, 0, 0, -3, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -1, 0, 0, 0, 0, 0, 0, 0, -3, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, -15, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -14, -33, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -24, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -3, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, -12, -41, 0, -24, -14, -22, -35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -41, -41, -41, 0, 0, 0, 0, 0, 0, 0, -22, -17, 0, 0, -41, 0, 0, 0, 0, 0, 0, -35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -35, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -14, -12, 0, 0, -14, -9, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -35, 0, -17, -15, -11, -24, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -35, -35, -35, 0, 0, 0, 0, 0, -9, -9, -24, -12, 0, 0, -53, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -22, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -18, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, -9, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 11, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -24, 0, -11, 0, -12, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, -24, -24, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -27, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -11, -15, 0, -14, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -15, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, -33, 0, 0, 0, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, -14, -14, -14, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, -21, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 29, 28, 0, 0, 0, 0, 0, 0, 0, 0, 20, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 24, 0, 23, 0, 0, 0, 24, 0, 0, 0, 0, 11, 19, 18, 7, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 28, 19, 28, 46, 0, 0, 0, 0, 0, 17, 46, 46, 27, 0, 24, 24, 24, 0, 36, 28, 14, 46, 0, 0, 0, 0, 0, 0, 24, 0, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, -14, -14, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -33, -33, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -12, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -6, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -37, 0, -15, -14, -8, -23, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, -5, -5, -6, -4, 0, 0, -9, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, -5, 2, 0, 0, 0, 0, 0, 0, -37, -37, -37, 0, 0, 0, 9, 0, 0, 0, -8, 0, 0, 0, -37, 0, 0, 0, -3, 0, 0, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -17, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, -14, -14, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, -40, -40, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -12, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 9, 0, 0, 0, 0, 26, 15, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 11, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 11, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 4, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 23, 13, 0, 0, 0, 0, 0, 9, 13, 13, 7, 0, 11, 11, 11, 0, 9, 24, 12, 13, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 11, 11, 0, 0, 0, 0, 0, 11, 11, 11, 11, 0, 9, 9, 9, 0, 9, 9, 11, 11, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 24, 0, 23, 0, 0, 0, 24, 0, 0, 0, 0, 11, 29, 27, 7, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 29, 29, 29, 46, 0, 0, 0, 0, 0, 17, 46, 46, 27, 0, 24, 24, 24, 0, 36, 29, 29, 46, 0, 0, 0, 0, 0, 0, 24, 0, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 24, 0, 23, 0, 0, 0, 24, 0, 0, 0, 0, 11, 29, 29, 7, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 29, 29, 29, 46, 0, 0, 0, 0, 0, 17, 46, 46, 29, 0, 24, 24, 24, 0, 36, 29, 14, 46, 0, 0, 0, 0, 0, 0, 24, 0, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 24, 0, 23, 0, 0, 0, 24, 0, 0, 0, 0, 11, 29, 18, 7, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 29, 29, 29, 46, 0, 0, 0, 0, 0, 17, 46, 46, 27, 0, 24, 24, 24, 0, 36, 29, 14, 46, 0, 0, 0, 0, 0, 0, 24, 0, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 29, 28, 0, 0, 0, 0, 0, 0, 0, 0, 20, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -24, 0, 0, 0, 0, 0, -47, -36, 0, -36, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -8, 0, 0, -8, 0, -26, 0, -18, 0, 0, -15, 0, -47, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -18, 0, 0, 0, -35, 0, -26, -17, -36, 0, -21, -47, 0, 0, -23, -36, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -26, 0, 0, -6, -40, -6, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -23, -10, 0, -12, -12, -9, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 1, 0, 9, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 23, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -24, 0, 0, 0, 0, 0, -45, -33, 0, -33, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -8, 0, 0, -8, 0, -26, 0, -18, 0, 0, -15, 0, -35, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -18, 0, 0, 0, -32, 0, -26, -17, -33, 0, -21, -47, 0, 0, -23, -33, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -26, 0, 0, -6, -40, -6, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -23, -10, 0, -12, -12, -9, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 1, 0, 9, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 13, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -24, 0, 0, 0, 0, 0, -44, -23, 0, -36, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -8, 0, 0, -8, 0, -26, 0, -18, 0, 0, -30, 0, -44, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -18, 0, 0, 0, -33, 0, -26, -17, -57, 0, -21, -44, 0, 0, -23, -39, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -26, 0, 0, -6, -40, -6, 0, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -23, -19, 0, -12, -12, -9, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 1, 0, 9, 0, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 13, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -9, 0, 0, -29, -9, -29, -9, -35, -9, 0, 0, 0, -18, 0, -21, 0, 0, -9, -21, -9, 0, -9, -23, 0, -9, -24, 0, 0, 0, 0, 0, 4, 0, 0, -15, -22, -23, -1, 0, 0, -17, -22, -22, -30, -20, -19, -38, -24, -29, 0, 0, -35, -29, 0, 0, 0, 0, -30, 0, -29, 0, -14, 0, 0, 0, -9, -32, -32, -37, 0, 0, 0, 24, -37, -30, -29, -22, 0, 0, 24, 24, 0, -23, 0, 0, 0, -30, 0, 0, 34, 24, -35, -35, 0, 0, -9, 0, 0, 0, 0, -22, -17, -22, -22, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, -32, 0, 0, -9, -32, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -9, 0, 0, -29, -9, -29, -9, -35, -9, 0, 0, 0, -18, 0, -21, 0, 0, -9, -21, -9, 0, -9, -23, 0, -9, -24, 0, 0, 0, 0, 0, 4, 0, 0, -15, -22, -23, -1, 0, 0, -17, -22, -22, -21, -20, -19, -19, -15, -29, 0, 0, -35, -29, 0, 0, 0, 0, -48, 0, -29, 0, -14, 0, 0, 0, -9, -32, -32, -37, 0, 0, 0, 24, -37, -37, -29, -22, 0, 0, 24, 24, 0, -23, 0, 0, 0, -31, 0, 0, 34, 24, -35, -35, 0, 0, -9, 0, 0, 0, 0, -22, -17, -22, -22, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, -32, 0, 0, -9, -32, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -9, 0, 0, -20, -9, -20, -9, -35, -9, 0, 0, 0, -18, 0, -21, 0, 0, -9, -21, -9, 0, -9, -39, 0, -9, -56, 0, 0, 0, 0, 0, 4, 0, 0, -15, -22, -23, -1, 0, 0, -17, -22, -22, -21, -20, -19, -19, -15, -20, 0, 0, -35, -20, 0, 0, 0, 0, -48, 0, -41, 0, -14, 0, 0, 0, -9, -32, -32, -37, 0, 0, 0, 24, -37, -37, -29, -22, 0, 0, 24, 24, 0, -23, 0, 0, 0, -31, 0, 0, 34, 24, -35, -35, 0, 0, -9, 0, 0, 0, 0, -22, -17, -22, -22, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, -32, 0, 0, -9, -32, 0, 0, 0, 0, 0, 0, 0, 0, -53, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -17, -8, 0, 0, -14, -14, -8, -14, -14, -14, -14, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, -14, -14, -17, 0, 0, 0, 0, -17, -14, -14, -14, 0, 0, 0, 0, 0, -17, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -8, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 8, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, -10, 0, 4, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, -4, -20, -20, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 28, 28, 0, 0, 0, 0, 0, 0, 0, 0, 31, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -6, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -45, 0, -15, -14, -8, -23, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, -5, -5, -6, -4, 0, 0, -9, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, -5, 2, 0, 0, 0, 0, 0, 0, -45, -45, -45, 0, 0, 0, 9, 0, 0, 0, -8, 0, 0, 0, -45, 0, 0, 0, -3, 0, 0, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -17, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -11, 0, -11, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, -15, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -11, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 20, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 28, 28, 0, 0, 0, 0, 0, 0, 0, 0, 23, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 24, 0, 23, 0, 0, 0, 24, 0, 0, 0, 0, 11, 28, 29, 7, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 28, 28, 28, 46, 0, 0, 0, 0, 0, 17, 46, 46, 29, 0, 24, 24, 24, 0, 36, 28, 26, 57, 0, 0, 0, 0, 0, 0, 24, 0, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -3, 0, 0, 0, 0, -39, 0, -39, 0, 0, -9, 0, 0, -5, -11, 0, 0, -5, 0, 0, -14, -14, 0, -11, 0, 0, -11, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -15, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, 0, 0, -24, -39, 0, -2, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, -12, -15, -15, -8, 0, 0, 0, 0, -8, 0, 0, -15, 0, 0, 0, 0, 0, -8, -8, -8, -8, 0, 0, 0, 23, 0, 0, 0, 0, -5, -24, -9, -8, 0, 0, -17, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -9, 0, -9, -1, -11, -10, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -9, 0, -24, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, -10, -27, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -7, 0, 0, 0, 0, 0, -7, -7, 4, -7, 4, 0, -3, 0, 0, 0, -6, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -4, -7, 0, 0, 0, 0, -7, 0, -5, -7, 0, 0, -7, 0, 0, 0, -7, 0, 0, -7, 0, 0, -7, -7, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, -5, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -4, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 4, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -11, -33, -11, -33, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -11, 3, 0, -15, -15, 0, 0, 0, 0, 0, -9, 0, 0, -1, -4, 4, 2, 3, 3, 0, 4, 0, 4, 2, 3, 0, -11, 0, 0, 0, -20, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -1, -1, -4, 2, 2, 2, 7, -4, 0, 4, -1, 0, 3, 7, 7, 3, -4, -15, -15, -15, 0, 3, 2, 0, 7, 0, 0, 0, 0, 0, 0, -15, -6, -6, 4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 4, -41, 0, 0, 0, -33, 4, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, -26, 0, -8, -45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -8, -8, -6, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -26, 0, 0, -7, 0, -31, 0, -45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -45, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -17, 0, 0, -21, -9, -21, -9, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, -17, -27, -26, 0, -10, -22, 0, -10, -9, 4, 0, 0, 0, 0, 0, 0, 0, 0, -20, -43, 0, -8, 0, -12, -18, 0, -11, 0, 0, 0, 0, -21, 0, 0, -24, -35, 0, -2, 0, 0, -11, 0, -35, 0, -17, 0, 0, 0, -23, -39, -33, -43, -8, -15, -8, 0, -43, -11, 0, -34, 0, 0, 0, 0, 0, -43, 4, 4, 4, -11, 0, -8, 23, 0, 0, 0, 0, 0, -10, 0, 4, 0, 0, 0, -15, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -11, -35, 0, 0, 0, 0, 0, 0, 0, -40, -47, 0, 0, 0, -9, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -8, -8, 4, -8, 0, -8, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 28, 19, 0, 0, 0, 0, 0, 0, 0, 0, 20, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -6, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, -24, 0, -15, -14, -8, -23, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, -5, -5, -6, -4, 0, 0, -9, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, -5, 2, 0, 0, 0, 0, 0, 0, -24, -24, -24, 0, 0, 0, 19, 0, 0, 0, -8, 0, 0, 0, -24, 0, 0, 0, -3, 0, 0, -46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -17, -6, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -17, 0, 0, -21, -18, -21, -18, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, -17, -27, -26, 0, -10, -22, 0, -10, -9, 4, 0, 0, 0, 0, 0, 0, 0, 0, -20, -22, 0, -8, 0, -12, -18, 0, -11, 0, 0, 0, 0, -21, 0, 0, -24, -35, 0, -2, 0, 0, -11, 0, -35, 0, -17, 0, 0, 0, -23, -39, -33, -22, -8, -15, -8, 0, -22, -11, 0, -34, 0, 0, 0, 0, 0, -37, 4, 4, 4, -11, 0, -8, 23, 0, 0, 0, 0, 0, -10, 0, 4, 0, 0, 0, -15, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, 0, 0, -11, -18, 0, 0, 0, 0, 0, 0, 0, -40, -47, 0, 0, 0, -9, 0, 0, 0, + 0, 0, 0, 0, -17, 0, 0, 0, -23, 0, -23, -26, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, -17, -30, 0, 0, 0, 0, 0, -38, -23, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, 0, 0, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -33, 0, 0, -18, 0, 0, -17, -26, -17, -26, -38, -12, 0, 0, 0, -26, 0, -17, -11, 0, -15, -26, -15, 0, -15, -36, 0, -15, -33, 0, 0, 0, 0, 0, 0, 0, 0, -11, -15, -17, 0, 0, 0, -6, -20, 4, -11, 4, 0, 0, 0, -29, 0, 0, -23, -17, 0, 0, 0, 0, -23, -3, -17, 0, 0, -18, 0, 0, 0, -15, -15, -17, 0, 0, 0, 0, -17, -11, 4, -15, 0, 0, 0, 0, 0, -17, 0, 0, 0, -11, 0, 0, 0, 0, -14, -24, 0, 0, -15, 0, 0, -32, -17, 4, 0, 4, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, 0, -24, -17, 0, -29, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, 0, 0, 0, 0, 0, 0, 0, -14, -9, 0, 0, 0, 0, 0, 0, -26, -26, -26, 0, 0, 0, 0, 0, 14, 14, 0, 0, 0, -17, -39, 17, 17, -14, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -29, -33, -26, 0, 0, 0, 0, 0, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, -9, 0, -11, 0, -12, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, + -22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -24, 0, -6, 0, -4, -4, -5, -2, 0, 0, 0, -26, -12, 0, 0, -12, 0, -9, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -9, -9, 0, 0, 0, 0, 0, 0, 0, 0, -6, -12, 0, -9, 0, 0, 0, -12, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -15, 0, -36, -48, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 3, -2, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, -10, 0, -33, 0, -35, -24, 0, -40, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 4, 0, -33, -33, -53, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -11, -42, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -4, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, -18, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -7, -8, }; static constexpr EpdFontData inter_8_regular = { inter_8_regularBitmaps, inter_8_regularGlyphs, inter_8_regularIntervals, - 63, + 77, 20, 17, -5, @@ -3931,10 +4367,10 @@ static constexpr EpdFontData inter_8_regular = { inter_8_regularKernLeftClasses, inter_8_regularKernRightClasses, inter_8_regularKernMatrix, - 764, - 769, - 145, - 140, + 768, + 788, + 146, + 143, nullptr, 0, }; diff --git a/lib/EpdFont/builtinFonts/source/.gitignore b/lib/EpdFont/builtinFonts/source/.gitignore index c212e7e90c..bf88ca9033 100644 --- a/lib/EpdFont/builtinFonts/source/.gitignore +++ b/lib/EpdFont/builtinFonts/source/.gitignore @@ -6,6 +6,8 @@ !NotoSerif/** !NotoSans/ !NotoSans/** +!NotoSansArabic/ +!NotoSansArabic/** !NotoSansHebrew/ !NotoSansHebrew/** !OpenDyslexic/ diff --git a/lib/EpdFont/builtinFonts/source/NotoSansArabic/NotoSansArabic-Bold.ttf b/lib/EpdFont/builtinFonts/source/NotoSansArabic/NotoSansArabic-Bold.ttf new file mode 100644 index 0000000000..5e44da88fc Binary files /dev/null and b/lib/EpdFont/builtinFonts/source/NotoSansArabic/NotoSansArabic-Bold.ttf differ diff --git a/lib/EpdFont/builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf b/lib/EpdFont/builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf new file mode 100644 index 0000000000..3279938e11 Binary files /dev/null and b/lib/EpdFont/builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf differ diff --git a/lib/EpdFont/builtinFonts/source/NotoSansArabic/OFL.txt b/lib/EpdFont/builtinFonts/source/NotoSansArabic/OFL.txt new file mode 100644 index 0000000000..788fd321fa --- /dev/null +++ b/lib/EpdFont/builtinFonts/source/NotoSansArabic/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/lib/EpdFont/scripts/build-dictionary-fonts.py b/lib/EpdFont/scripts/build-dictionary-fonts.py new file mode 100755 index 0000000000..8523e57c00 --- /dev/null +++ b/lib/EpdFont/scripts/build-dictionary-fonts.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Build dictionary SD-card fonts with broad IPA coverage. + +This is a dictionary-specific wrapper around ``build-sd-fonts.py``. It keeps +the family/style/size catalog in ``sd-fonts.yaml`` while replacing each +family's interval list with the reading ranges plus IPA and combining-mark +ranges needed by dictionary definitions. The shared builder supplies +CrossInk's built-in intervals and fallback stack, with bundled Noto Sans +remaining the final fallback for every style. + +The default output is the sibling crossink-fonts repository: +``../crossink-fonts/dictionary-fonts``. + +Usage: + python3 build-dictionary-fonts.py + python3 build-dictionary-fonts.py --clean --jobs 2 + python3 build-dictionary-fonts.py --only GentiumBookPlus,ChareInk + python3 build-dictionary-fonts.py --output-dir /tmp/dictionary-fonts +""" + +from __future__ import annotations + +import argparse +import copy +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +import yaml + + +SCRIPT_DIR = Path(__file__).resolve().parent +SHARED_BUILDER = SCRIPT_DIR / "build-sd-fonts.py" +DEFAULT_CONFIG = SCRIPT_DIR / "sd-fonts.yaml" +PROJECT_ROOT = SCRIPT_DIR.parents[2] +DEFAULT_OUTPUT = PROJECT_ROOT.parent / "crossink-fonts" / "dictionary-fonts" +FALLBACK_FONT = PROJECT_ROOT / "lib/EpdFont/builtinFonts/source/NotoSans/NotoSans-Regular.ttf" + +# ``build-sd-fonts.py`` appends its ``builtin`` preset to this list. The +# reading preset supplies the same broad reader ranges used by the built-in +# fonts, while these explicit ranges add dictionary IPA and mark coverage. +DICTIONARY_INTERVALS = ( + "reading,(0x0250-0x02FF),(0x1D00-0x1DBF)," + "(0x1DC0-0x1DFF),(0x20D0-0x20FF),(0xFE20-0xFE2F)" +) + + +def load_dictionary_config(config_path: Path) -> tuple[dict, list[dict]]: + """Load the catalog and replace family intervals for dictionary output.""" + with config_path.open() as config_file: + config = yaml.safe_load(config_file) + + if not isinstance(config, dict) or not config.get("families"): + raise ValueError(f"No families defined in {config_path}") + + dictionary_config = copy.deepcopy(config) + for family in dictionary_config["families"]: + family["intervals"] = DICTIONARY_INTERVALS + return dictionary_config, dictionary_config["families"] + + +def write_temporary_config(config: dict) -> Path: + """Write a temporary transformed catalog for the shared builder.""" + temp_file = tempfile.NamedTemporaryFile( + mode="w", prefix="crossink-dictionary-fonts-", suffix=".yaml", delete=False + ) + try: + yaml.safe_dump(config, temp_file, sort_keys=False) + finally: + temp_file.close() + return Path(temp_file.name) + + +def package_family(output_dir: Path, family: dict) -> Path: + """Create ``.zip`` containing the matching ``/`` folder.""" + family_name = family["name"] + family_dir = output_dir / family_name + if not family_dir.is_dir(): + raise FileNotFoundError(f"Expected generated family directory: {family_dir}") + + expected_files = len(family.get("sizes", [])) + generated_files = list(family_dir.glob("*.cpfont")) + if len(generated_files) != expected_files: + raise RuntimeError( + f"{family_name}: expected {expected_files} .cpfont files, " + f"found {len(generated_files)}; rerun with --clean to remove stale output" + ) + + archive_base = output_dir / family_name + archive_path = Path( + shutil.make_archive( + str(archive_base), "zip", root_dir=str(output_dir), base_dir=family_name + ) + ) + return archive_path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Build dictionary SD-card fonts and package each family as a ZIP" + ) + parser.add_argument( + "--config", default=str(DEFAULT_CONFIG), help="Font catalog YAML (default: sd-fonts.yaml)" + ) + parser.add_argument( + "--output-dir", + default=str(DEFAULT_OUTPUT), + help="Output directory (default: sibling crossink-fonts/dictionary-fonts)", + ) + parser.add_argument("--only", help="Comma-separated family names to build") + parser.add_argument("--jobs", "-j", type=int, default=None, help="Maximum parallel families") + parser.add_argument("--timeout", type=int, default=600, help="Per-family timeout in seconds") + parser.add_argument("--clean", action="store_true", help="Clean the output directory before building") + parser.add_argument( + "--verbose", "-v", action="store_true", help="Stream child font-converter output" + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + config_path = Path(args.config).expanduser().resolve() + output_dir = Path(args.output_dir).expanduser().resolve() + + if not SHARED_BUILDER.is_file(): + print(f"ERROR: shared builder not found: {SHARED_BUILDER}", file=sys.stderr) + return 1 + if not FALLBACK_FONT.is_file(): + print(f"ERROR: Noto Sans fallback not found: {FALLBACK_FONT}", file=sys.stderr) + return 1 + if not config_path.is_file(): + print(f"ERROR: config not found: {config_path}", file=sys.stderr) + return 1 + + try: + transformed_config, families = load_dictionary_config(config_path) + except (OSError, ValueError, yaml.YAMLError) as error: + print(f"ERROR: unable to load dictionary config: {error}", file=sys.stderr) + return 1 + + if args.only: + requested = {name.strip() for name in args.only.split(",") if name.strip()} + families = [family for family in families if family.get("name") in requested] + if not families: + print(f"ERROR: no matching families for --only {args.only}", file=sys.stderr) + return 1 + transformed_config["families"] = families + + temp_config = write_temporary_config(transformed_config) + command = [ + sys.executable, + str(SHARED_BUILDER), + "--config", + str(temp_config), + "--output-dir", + str(output_dir), + "--timeout", + str(args.timeout), + "--no-package", + ] + if args.only: + command.extend(["--only", args.only]) + if args.jobs is not None: + command.extend(["--jobs", str(args.jobs)]) + if args.clean: + command.append("--clean") + if args.verbose: + command.append("--verbose") + + print(f"Dictionary intervals: {DICTIONARY_INTERVALS},builtin") + print(f"Noto Sans fallback: {FALLBACK_FONT}") + try: + result = subprocess.run(command, check=False) + finally: + temp_config.unlink(missing_ok=True) + + if result.returncode != 0: + return result.returncode + + print("\n=== Packaging dictionary font ZIPs ===\n") + packaged_archives = [] + try: + for family in families: + archive = package_family(output_dir, family) + packaged_archives.append(archive) + print(f" ZIP: {archive}") + except (FileNotFoundError, OSError, RuntimeError) as error: + print(f"ERROR: packaging failed: {error}", file=sys.stderr) + return 1 + + total_files = sum( + len(list((output_dir / family["name"]).glob("*.cpfont"))) for family in families + ) + print( + f"\nPackaged {total_files} .cpfont files and {len(packaged_archives)} ZIPs " + f"in {output_dir}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/lib/EpdFont/scripts/build-sd-fonts.py b/lib/EpdFont/scripts/build-sd-fonts.py index 512c5078ad..7e155170ef 100755 --- a/lib/EpdFont/scripts/build-sd-fonts.py +++ b/lib/EpdFont/scripts/build-sd-fonts.py @@ -6,9 +6,12 @@ generates the fonts.json manifest. Usage: - # Generate fonts (output in ./output/) + # Generate fonts + per-family ZIPs in ../crossink-fonts/cpfonts python3 build-sd-fonts.py + # Generate fonts without ZIP packaging + python3 build-sd-fonts.py --no-package + # Generate fonts + manifest python3 build-sd-fonts.py --manifest --base-url "http://localhost:8000/" @@ -40,11 +43,12 @@ import yaml -SCRIPT_DIR = Path(__file__).parent +SCRIPT_DIR = Path(__file__).resolve().parent FONTCONVERT = SCRIPT_DIR / "fontconvert_sdcard.py" EPDFONTS_DIR = SCRIPT_DIR.parent # lib/EpdFont +PROJECT_ROOT = SCRIPT_DIR.parents[2] DEFAULT_CONFIG = SCRIPT_DIR / "sd-fonts.yaml" -DEFAULT_OUTPUT = SCRIPT_DIR / "output" +DEFAULT_OUTPUT = PROJECT_ROOT.parent / "crossink-fonts" / "cpfonts" DOWNLOAD_DIR = SCRIPT_DIR / "downloaded_fonts" INSTANCE_DIR = SCRIPT_DIR / "instanced_fonts" DEFAULT_FALLBACK_FONT = EPDFONTS_DIR / "builtinFonts/source/NotoSans/NotoSans-Regular.ttf" @@ -52,6 +56,49 @@ # as the built-in reader fonts. Families can still add extra script presets. PATCHED_INTERVAL_PRESETS = ("builtin",) +# Keep the SD-card fallback stack aligned with convert-builtin-fonts.sh. Each +# tuple is an inclusive Unicode range assigned to its corresponding fallback +# face. Noto Sans remains the final unrestricted fallback for missing glyphs, +# including the IPA ranges requested by dictionary fonts. +COMMON_FALLBACK_RANGES = ( + (0x03BB, 0x03BB), + (0x0410, 0x0414), (0x0418, 0x0418), (0x041B, 0x041B), + (0x041D, 0x0423), (0x0425, 0x0425), (0x0427, 0x0427), + (0x042B, 0x042C), (0x042E, 0x0432), (0x0434, 0x0435), + (0x0437, 0x0437), (0x043A, 0x043A), (0x043D, 0x043E), + (0x0440, 0x0440), (0x0442, 0x0442), (0x0446, 0x0446), + (0x044C, 0x044C), (0x044E, 0x044E), (0x2113, 0x2113), +) +EMOJI_FALLBACK_RANGES = ( + (0x2669, 0x266F), + (0x1F600, 0x1F607), (0x1F609, 0x1F614), (0x1F618, 0x1F618), + (0x1F61A, 0x1F61A), (0x1F61C, 0x1F61D), (0x1F620, 0x1F622), + (0x1F624, 0x1F625), (0x1F629, 0x1F629), (0x1F62C, 0x1F62E), + (0x1F631, 0x1F635), (0x1F641, 0x1F642), (0x1F644, 0x1F644), + (0x1F44B, 0x1F44F), (0x2764, 0x2764), +) +SYMBOL_FALLBACK_RANGES = ((0x2669, 0x266F),) +PHM_FALLBACK_RANGES = ( + (0x4F1A, 0x4F1A), (0x53BB, 0x53BB), (0x5458, 0x5458), + (0x59DA, 0x59DA), (0x5B98, 0x5B98), (0x5BA4, 0x5BA4), + (0x5E26, 0x5E26), (0x6211, 0x6211), (0x62C9, 0x62C9), + (0x653E, 0x653E), (0x6746, 0x677F), (0x7532, 0x7532), + (0x7684, 0x7684), (0x8BAE, 0x8BAE), (0x8BF7, 0x8BF7), + (0x91CA, 0x91CA), +) +PATCHED_INTERVAL_RANGES = ( + *COMMON_FALLBACK_RANGES, + *EMOJI_FALLBACK_RANGES, + *SYMBOL_FALLBACK_RANGES, + *PHM_FALLBACK_RANGES, +) +STYLE_SUFFIXES = { + "regular": "Regular", + "bold": "Bold", + "italic": "Italic", + "bolditalic": "BoldItalic", +} + def is_url(value: str) -> bool: return value.startswith(("http://", "https://")) @@ -89,9 +136,65 @@ def patched_intervals(intervals: str) -> str: if preset not in normalized: patched.append(preset) + for start, end in PATCHED_INTERVAL_RANGES: + interval = f"(0x{start:04X}-0x{end:04X})" + if interval.lower() not in normalized: + patched.append(interval) + return ",".join(patched) +def builtin_fallback_specs(style_name: str, family_name: str) -> list[tuple[Path, tuple | None]]: + """Return the built-in fallback faces and their restricted ranges.""" + suffix = STYLE_SUFFIXES[style_name] + specs = [] + + # ChareInk is the common fallback used by the built-in reader families. + # Do not add it to ChareInk itself; its primary face is already that font. + if family_name.lower() != "chareink": + specs.append(( + EPDFONTS_DIR / f"builtinFonts/source/ChareInk7/ChareInk7-{suffix}.ttf", + COMMON_FALLBACK_RANGES, + )) + + # The built-in script uses the regular emoji/symbol face for every style. + specs.append(( + EPDFONTS_DIR / "builtinFonts/source/NotoEmoji/NotoEmoji-Regular.ttf", + EMOJI_FALLBACK_RANGES, + )) + specs.append(( + EPDFONTS_DIR / "builtinFonts/source/NotoSymbols/NotoSansSymbols-Regular.ttf", + SYMBOL_FALLBACK_RANGES, + )) + + # PHM CJK fallback is intentionally only part of regular reader fonts. + if style_name == "regular": + specs.append(( + EPDFONTS_DIR / "builtinFonts/source/NotoSansCJKsc/NotoSansCJKsc-Regular.otf", + PHM_FALLBACK_RANGES, + )) + + # Keep the existing SD-font behavior for any requested glyph not supplied + # by the built-in fallback stack, including dictionary IPA coverage. + specs.append((DEFAULT_FALLBACK_FONT, None)) + return specs + + +def encode_fallback_ranges(ranges: tuple | None) -> str: + """Encode fallback ranges for fontconvert_sdcard.py's CLI.""" + if not ranges: + return "" + return ";".join(f"0x{start:X}-0x{end:X}" for start, end in ranges) + + +def append_fallback_args(cmd: list[str], style_name: str, family_name: str) -> None: + """Append one ordered fallback stack for a style.""" + range_flag = f"--fallback-{style_name}-ranges" + font_flag = f"--fallback-{style_name}" + for fallback_path, ranges in builtin_fallback_specs(style_name, family_name): + cmd.extend([font_flag, str(fallback_path), range_flag, encode_fallback_ranges(ranges)]) + + _orig_getaddrinfo = socket.getaddrinfo @@ -257,14 +360,14 @@ def build_family( # Multi-style mode for style_name, font_path in resolved_styles.items(): cmd.extend([f"--{style_name}", str(font_path)]) - cmd.extend([f"--fallback-{style_name}", str(DEFAULT_FALLBACK_FONT)]) + append_fallback_args(cmd, style_name, name) else: # Single-style mode style_name = next(iter(resolved_styles)) font_path = resolved_styles[style_name] cmd.append(str(font_path)) cmd.extend(["--style", style_name]) - cmd.extend([f"--fallback-{style_name}", str(DEFAULT_FALLBACK_FONT)]) + append_fallback_args(cmd, style_name, name) cmd.extend(["--intervals", intervals]) cmd.extend(["--sizes", sizes]) @@ -274,6 +377,12 @@ def build_family( if family.get("force_autohint", False): cmd.append("--force-autohint") + # SD-card fonts are reader fonts, so they get the same darkened anti-alias + # thresholds as the built-in reader fonts in convert-builtin-fonts.sh + # (READING_FONT_RENDER_ARGS). Without this the two look noticeably + # different at the same size on the same panel. + cmd.append("--darken-aa") + # Run fontconvert_sdcard.py start = time.monotonic() try: @@ -325,6 +434,29 @@ def build_family( return name, False, str(e) +def package_family(output_base: Path, family: dict) -> Path: + """Create ``.zip`` containing the matching ``/`` folder.""" + family_name = family["name"] + family_dir = output_base / family_name + if not family_dir.is_dir(): + raise FileNotFoundError(f"Expected generated family directory: {family_dir}") + + expected_files = len(family.get("sizes", [])) + generated_files = list(family_dir.glob("*.cpfont")) + if len(generated_files) != expected_files: + raise RuntimeError( + f"{family_name}: expected {expected_files} .cpfont files, " + f"found {len(generated_files)}; rerun with --clean to remove stale output" + ) + + archive_base = output_base / family_name + return Path( + shutil.make_archive( + str(archive_base), "zip", root_dir=str(output_base), base_dir=family_name + ) + ) + + def generate_manifest( config_path: Path, output_base: Path, base_url: str, manifest_path: Path ): @@ -363,7 +495,9 @@ def main(): "--config", default=str(DEFAULT_CONFIG), help="Path to font families YAML config" ) parser.add_argument( - "--output-dir", default=str(DEFAULT_OUTPUT), help="Output directory for .cpfont files" + "--output-dir", + default=str(DEFAULT_OUTPUT), + help="Output directory for family folders and ZIPs (default: sibling crossink-fonts/cpfonts)", ) parser.add_argument("--only", help="Comma-separated family names to build (default: all)") parser.add_argument("--manifest", action="store_true", help="Also generate fonts.json manifest") @@ -384,6 +518,11 @@ def main(): "--timeout", type=int, default=600, help="Per-family timeout in seconds (default: 600)" ) + parser.add_argument( + "--no-package", + action="store_true", + help="Skip per-family ZIP packaging", + ) args = parser.parse_args() if args.manifest and not args.base_url: @@ -412,6 +551,19 @@ def main(): ) sys.exit(1) + fallback_paths = { + fallback_path + for family in families + for style_name in family.get("styles", {}) + for fallback_path, _ in builtin_fallback_specs(style_name, family["name"]) + } + missing_fallbacks = sorted(path for path in fallback_paths if not path.is_file()) + if missing_fallbacks: + print("ERROR: Missing built-in fallback fonts:", file=sys.stderr) + for fallback_path in missing_fallbacks: + print(f" {fallback_path}", file=sys.stderr) + sys.exit(1) + # Filter if --only specified if args.only: only_names = set(args.only.split(",")) @@ -482,6 +634,24 @@ def main(): if failed: print(f"\nFailed families: {', '.join(failed)}", file=sys.stderr) + # Package only a completely successful build. This keeps an old ZIP from + # looking current when one family failed partway through conversion. + if not failed and not args.no_package: + print("\n=== Packaging SD-card font ZIPs ===\n") + packaged_archives = [] + try: + for family in families: + archive = package_family(output_base, family) + packaged_archives.append(archive) + print(f" ZIP: {archive}") + except (FileNotFoundError, OSError, RuntimeError) as error: + print(f"ERROR: packaging failed: {error}", file=sys.stderr) + sys.exit(1) + print( + f"\nPackaged {total_files} .cpfont files and {len(packaged_archives)} ZIPs " + f"in {output_base}" + ) + # Manifest if args.manifest: manifest_path = Path(args.manifest_output) if args.manifest_output else output_base / "fonts.json" diff --git a/lib/EpdFont/scripts/convert-builtin-fonts.sh b/lib/EpdFont/scripts/convert-builtin-fonts.sh index 2eb75ef24e..dc89daa789 100755 --- a/lib/EpdFont/scripts/convert-builtin-fonts.sh +++ b/lib/EpdFont/scripts/convert-builtin-fonts.sh @@ -225,14 +225,43 @@ generate_reading_variant ../builtinFonts/noemoji no "no-emoji" UI_FONT_SIZES=(10 12) UI_FONT_STYLES=("Regular" "Bold") +# Arabic glyphs for UI text (menus, file browser titles). The built-in fonts +# must cover the *output* of MiniBidi's do_shape() — contextual presentation +# forms — not base letters, or shaped UI text silently drops glyphs. +# Curated for firmware-size budget: core Arabic (Presentation Forms-B, +# incl. the Lam-Alef ligature forms) plus the Farsi/Urdu extra letters' +# Presentation Forms-A blocks, the few characters shaping leaves at their +# base codepoint, Arabic punctuation, and both digit sets. No harakat and +# no Sindhi/Pashto/Kurdish forms — book text gets those from SD-card fonts. +ARABIC_INTERVALS=( + --additional-intervals 0x060C,0x060C # Arabic comma + --additional-intervals 0x061B,0x061B # Arabic semicolon + --additional-intervals 0x061F,0x061F # Arabic question mark + --additional-intervals 0x0621,0x0621 # hamza (non-joining, never shaped) + --additional-intervals 0x0640,0x0640 # tatweel + --additional-intervals 0x0660,0x0669 # Arabic-Indic digits + --additional-intervals 0x06BA,0x06BA # noon ghunna base (initial/medial keep base cp) + --additional-intervals 0x06D4,0x06D4 # Urdu full stop + --additional-intervals 0x06F0,0x06F9 # extended Arabic-Indic digits (Farsi/Urdu) + --additional-intervals 0xFB56,0xFB59 # peh (Farsi) + --additional-intervals 0xFB66,0xFB69 # tteh (Urdu) + --additional-intervals 0xFB7A,0xFB7D # tcheh (Farsi) + --additional-intervals 0xFB88,0xFB95 # ddal, jeh, rreh (Urdu), keheh, gaf (Farsi/Urdu) + --additional-intervals 0xFB9E,0xFB9F # noon ghunna isolated/final (Urdu) + --additional-intervals 0xFBA6,0xFBB1 # heh goal, heh doachashmee, yeh barree(+hamza) (Urdu) + --additional-intervals 0xFBFC,0xFBFF # farsi yeh (Farsi/Urdu) + --additional-intervals 0xFE80,0xFEFC # Presentation Forms-B: core Arabic + Lam-Alef +) + for size in ${UI_FONT_SIZES[@]}; do for style in ${UI_FONT_STYLES[@]}; do font_name="inter_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')" font_path="../builtinFonts/source/Inter/Inter-${style}.ttf" hebrew_path="../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-${style}.ttf" + arabic_path="../builtinFonts/source/NotoSansArabic/NotoSansArabic-${style}.ttf" output_path="../builtinFonts/${font_name}.h" - python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \ - --additional-intervals 0x05D0,0x05EA > $output_path + python fontconvert.py $font_name $size $font_path $hebrew_path $arabic_path \ + --additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > $output_path echo "Generated $output_path" done done @@ -242,7 +271,8 @@ done python fontconvert.py inter_8_regular 8 \ ../builtinFonts/source/Inter/Inter-Regular.ttf \ ../builtinFonts/source/IBMPlexSansHebrew/IBMPlexSansHebrew-Regular.ttf \ - --additional-intervals 0x05D0,0x05EA > ../builtinFonts/inter_8_regular.h + ../builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf \ + --additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > ../builtinFonts/inter_8_regular.h echo "" echo "Running compression verification..." diff --git a/lib/EpdFont/scripts/fontconvert_sdcard.py b/lib/EpdFont/scripts/fontconvert_sdcard.py index c7dd9a7127..3f654f081e 100755 --- a/lib/EpdFont/scripts/fontconvert_sdcard.py +++ b/lib/EpdFont/scripts/fontconvert_sdcard.py @@ -216,6 +216,51 @@ def fp4_from_design_units(du, scale): } +def extract_ligature_glyph_indices_fonttools(font_path): + """Map standard ligature codepoints without a cmap entry to their glyph IDs. + + Most fonts use unencoded glyphs such as ``f_f`` for standard ligatures. + The reader represents those substitutions with the Unicode compatibility + codepoints, so rasterize the GSUB target directly instead of falling back + to another font for a codepoint that the primary cmap does not contain. + """ + from fontTools.ttLib import TTFont + + font = TTFont(font_path) + cmap = font.getBestCmap() or {} + glyph_to_cp = {gname: cp for cp, gname in cmap.items()} + glyph_indices = {gname: index for index, gname in enumerate(font.getGlyphOrder())} + overrides = {} + + if 'GSUB' in font: + gsub = font['GSUB'].table + liga_lookup_indices = set() + if gsub.FeatureList: + for fr in gsub.FeatureList.FeatureRecord: + if fr.FeatureTag in ('liga', 'rlig'): + liga_lookup_indices.update(fr.Feature.LookupListIndex) + + for li in liga_lookup_indices: + lookup = gsub.LookupList.Lookup[li] + for st in lookup.SubTable: + actual = st.ExtSubTable if lookup.LookupType == 7 and hasattr(st, 'ExtSubTable') else st + if not hasattr(actual, 'ligatures'): + continue + for first_glyph, ligature_list in actual.ligatures.items(): + if first_glyph not in glyph_to_cp: + continue + for lig in ligature_list: + if any(component not in glyph_to_cp for component in lig.Component): + continue + seq = tuple([glyph_to_cp[first_glyph]] + [glyph_to_cp[component] for component in lig.Component]) + lig_cp = STANDARD_LIGATURE_MAP.get(seq) + if lig_cp is not None and lig_cp not in cmap: + overrides[lig_cp] = glyph_indices[lig.LigGlyph] + + font.close() + return overrides + + def _extract_pairpos_subtable(subtable, glyph_to_cp, raw_kern): """Extract kerning from a PairPos subtable (Format 1 or 2).""" if subtable.Format == 1: @@ -542,24 +587,64 @@ def extract_ligatures_fonttools(font_path, codepoints): return pairs +def code_point_in_intervals(code_point, intervals): + """Return whether a code point is allowed for a fallback face.""" + return any(start <= code_point <= end for start, end in intervals) + + +def parse_fallback_range_spec(spec): + """Parse ``0xSTART-0xEND;0xSTART-0xEND`` fallback ranges.""" + if not spec: + return None + + ranges = [] + for range_spec in spec.split(";"): + parsed = parse_hex_range(f"({range_spec})") + if parsed is None: + raise ValueError( + f"invalid fallback range '{range_spec}'; expected 0xSTART-0xEND" + ) + ranges.append(parsed) + return ranges + + def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=False, - fallback_fontfile=None): + fallback_fontfiles=None, fallback_include_intervals=None, + fallback_fontfile=None, darken_aa=False): """Rasterize all glyphs for one font style. Returns StyleRasterData.""" import freetype style_names = {0: "regular", 1: "bold", 2: "italic", 3: "bolditalic"} style_label = style_names.get(style_id, str(style_id)) + # Matches the built-in reader fonts in fontconvert.py so SD-card fonts land + # on the same 4-level shades for a given coverage value. + aa_thresholds = (3, 6, 10) if darken_aa else (4, 8, 12) + face = freetype.Face(fontfile) # Set font size at 150 DPI (matching fontconvert.py) BEFORE any glyph load. # load_glyph() with FT_LOAD_RENDER renders at the active size, so calling # it before set_char_size() would waste work at the default size and risk # Invalid_Size_Handle on some fonts. face.set_char_size(size << 6, size << 6, 150, 150) - fallback_face = None - if fallback_fontfile: - fallback_face = freetype.Face(fallback_fontfile) + ligature_glyph_indices = extract_ligature_glyph_indices_fonttools(fontfile) + # ``fallback_fontfile`` is retained as a compatibility alias for callers + # that used the original single-fallback API. New callers should pass an + # ordered list through ``fallback_fontfiles``. + if fallback_fontfiles is None and fallback_fontfile: + fallback_fontfiles = [fallback_fontfile] + elif isinstance(fallback_fontfiles, (str, os.PathLike)): + fallback_fontfiles = [fallback_fontfiles] + fallback_fontfiles = fallback_fontfiles or [] + fallback_include_intervals = fallback_include_intervals or [None] * len(fallback_fontfiles) + if len(fallback_include_intervals) != len(fallback_fontfiles): + raise ValueError("fallback font and fallback range counts must match") + + fallback_faces = [] + for fallback_path in fallback_fontfiles: + fallback_face = freetype.Face(fallback_path) fallback_face.set_char_size(size << 6, size << 6, 150, 150) + fallback_faces.append(fallback_face) load_flags = freetype.FT_LOAD_RENDER if force_autohint: @@ -567,10 +652,15 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F def load_glyph(code_point): glyph_index = face.get_char_index(code_point) + if glyph_index == 0: + glyph_index = ligature_glyph_indices.get(code_point, 0) if glyph_index > 0: face.load_glyph(glyph_index, load_flags) return face - if fallback_face: + for fallback_face, allowed_intervals in zip( + fallback_faces, fallback_include_intervals): + if allowed_intervals and not code_point_in_intervals(code_point, allowed_intervals): + continue fallback_glyph_index = fallback_face.get_char_index(code_point) if fallback_glyph_index > 0: fallback_face.load_glyph(fallback_glyph_index, load_flags) @@ -586,8 +676,14 @@ def load_glyph(code_point): for i_start, i_end in intervals: start = i_start for code_point in range(i_start, i_end + 1): - has_primary = face.get_char_index(code_point) != 0 - has_fallback = fallback_face and fallback_face.get_char_index(code_point) != 0 + has_primary = face.get_char_index(code_point) != 0 or code_point in ligature_glyph_indices + has_fallback = any( + (not allowed_intervals or code_point_in_intervals(code_point, allowed_intervals)) + and fallback_face.get_char_index(code_point) != 0 + for fallback_face, allowed_intervals in zip( + fallback_faces, fallback_include_intervals + ) + ) has_synthetic_blank = is_synthetic_blank_codepoint(code_point) if not has_primary and not has_fallback and not has_synthetic_blank: if start < code_point: @@ -649,7 +745,9 @@ def load_glyph(code_point): pixels4g.append(px) px = 0 - # Downsample to 2-bit bitmap + # Downsample to 2-bit bitmap. + # Default: 0-3 white, 4-7 light grey, 8-11 dark grey, 12-15 black. + # --darken-aa lowers the cutoffs to push edge pixels one shade darker. pixels2b = [] px = 0 pitch = (bitmap.width // 2) + (bitmap.width % 2) @@ -659,11 +757,11 @@ def load_glyph(code_point): bm = pixels4g[y * pitch + (x // 2)] bm = (bm >> ((x % 2) * 4)) & 0xF - if bm >= 12: + if bm >= aa_thresholds[2]: px += 3 - elif bm >= 8: + elif bm >= aa_thresholds[1]: px += 2 - elif bm >= 4: + elif bm >= aa_thresholds[0]: px += 1 if (y * bitmap.width + x) % 4 == 3: @@ -805,11 +903,13 @@ def style_sections_total_size(sections): # --- File writers --- def generate_cpfont_multistyle(style_fonts, size, intervals, output_path, - force_autohint=False, fallback_style_fonts=None): + force_autohint=False, fallback_style_fonts=None, + fallback_style_intervals=None, darken_aa=False): """Generate a multi-style v4 .cpfont file. style_fonts: dict of {style_id: fontfile_path} e.g. {0: "Regular.ttf", 2: "Italic.ttf"} - fallback_style_fonts: optional dict of {style_id: fallback_fontfile_path} + fallback_style_fonts: optional dict of {style_id: [fallback_fontfile_path]} + fallback_style_intervals: optional dict of {style_id: [allowed ranges]} """ MAGIC = b"CPFONT\x00\x00" HEADER_SIZE = 32 @@ -820,14 +920,18 @@ def generate_cpfont_multistyle(style_fonts, size, intervals, output_path, # Rasterize each style raster_data = {} # style_id -> StyleRasterData fallback_style_fonts = fallback_style_fonts or {} + fallback_style_intervals = fallback_style_intervals or {} for style_id in sorted(style_fonts.keys()): fontfile = style_fonts[style_id] - fallback_fontfile = fallback_style_fonts.get(style_id) + fallback_fontfiles = fallback_style_fonts.get(style_id, []) + fallback_include_intervals = fallback_style_intervals.get(style_id, []) print(f" Rasterizing style {style_id}...", file=sys.stderr) raster_data[style_id] = rasterize_font_style( fontfile, size, intervals, style_id=style_id, force_autohint=force_autohint, - fallback_fontfile=fallback_fontfile) + fallback_fontfiles=fallback_fontfiles, + fallback_include_intervals=fallback_include_intervals, + darken_aa=darken_aa) # Pack binary sections for each style packed_sections = {} # style_id -> tuple of section bytearrays @@ -922,6 +1026,8 @@ def main(): help="Font family name for output filenames (default: derived from font filename).") parser.add_argument("--force-autohint", dest="force_autohint", action="store_true", help="Force FreeType auto-hinter instead of native font hinting.") + parser.add_argument("--darken-aa", dest="darken_aa", action="store_true", + help="Use darker 2-bit anti-aliasing thresholds, matching the built-in reader fonts.") parser.add_argument("-o", "--output", dest="output", help="Output file path (for single-size mode).") parser.add_argument("--output-dir", dest="output_dir", @@ -938,14 +1044,22 @@ def main(): help="Font file for italic style.") parser.add_argument("--bolditalic", dest="font_bolditalic", help="Font file for bold-italic style.") - parser.add_argument("--fallback-regular", dest="fallback_regular", - help="Fallback font file for regular style.") - parser.add_argument("--fallback-bold", dest="fallback_bold", - help="Fallback font file for bold style.") - parser.add_argument("--fallback-italic", dest="fallback_italic", - help="Fallback font file for italic style.") - parser.add_argument("--fallback-bolditalic", dest="fallback_bolditalic", - help="Fallback font file for bold-italic style.") + parser.add_argument("--fallback-regular", dest="fallback_regular", action="append", + help="Fallback font file for regular style (repeat for a fallback stack).") + parser.add_argument("--fallback-bold", dest="fallback_bold", action="append", + help="Fallback font file for bold style (repeat for a fallback stack).") + parser.add_argument("--fallback-italic", dest="fallback_italic", action="append", + help="Fallback font file for italic style (repeat for a fallback stack).") + parser.add_argument("--fallback-bolditalic", dest="fallback_bolditalic", action="append", + help="Fallback font file for bold-italic style (repeat for a fallback stack).") + parser.add_argument("--fallback-regular-ranges", dest="fallback_regular_ranges", action="append", + help="Allowed ranges for each regular fallback, as 0xSTART-0xEND;... .") + parser.add_argument("--fallback-bold-ranges", dest="fallback_bold_ranges", action="append", + help="Allowed ranges for each bold fallback, as 0xSTART-0xEND;... .") + parser.add_argument("--fallback-italic-ranges", dest="fallback_italic_ranges", action="append", + help="Allowed ranges for each italic fallback, as 0xSTART-0xEND;... .") + parser.add_argument("--fallback-bolditalic-ranges", dest="fallback_bolditalic_ranges", action="append", + help="Allowed ranges for each bold-italic fallback, as 0xSTART-0xEND;... .") args = parser.parse_args() @@ -968,14 +1082,30 @@ def main(): style_fonts[3] = args.font_bolditalic fallback_style_fonts = {} - if args.fallback_regular: - fallback_style_fonts[0] = args.fallback_regular - if args.fallback_bold: - fallback_style_fonts[1] = args.fallback_bold - if args.fallback_italic: - fallback_style_fonts[2] = args.fallback_italic - if args.fallback_bolditalic: - fallback_style_fonts[3] = args.fallback_bolditalic + fallback_style_intervals = {} + fallback_args = ( + (0, args.fallback_regular, args.fallback_regular_ranges), + (1, args.fallback_bold, args.fallback_bold_ranges), + (2, args.fallback_italic, args.fallback_italic_ranges), + (3, args.fallback_bolditalic, args.fallback_bolditalic_ranges), + ) + for style_id, fallback_paths, fallback_ranges in fallback_args: + if fallback_paths: + fallback_style_fonts[style_id] = fallback_paths + if fallback_ranges: + if len(fallback_ranges) != len(fallback_paths): + parser.error( + f"fallback font/range count mismatch for style {style_id}: " + f"{len(fallback_paths)} fonts, {len(fallback_ranges)} range specs" + ) + try: + fallback_style_intervals[style_id] = [ + parse_fallback_range_spec(spec) for spec in fallback_ranges + ] + except ValueError as error: + parser.error(str(error)) + else: + fallback_style_intervals[style_id] = [None] * len(fallback_paths) is_multistyle = len(style_fonts) > 0 fontfile = args.fontfile @@ -1045,7 +1175,9 @@ def main(): total_size += generate_cpfont_multistyle( style_fonts, sz, intervals, output_path, force_autohint=args.force_autohint, - fallback_style_fonts=fallback_style_fonts) + fallback_style_fonts=fallback_style_fonts, + fallback_style_intervals=fallback_style_intervals, + darken_aa=args.darken_aa) print(f"\nTotal: {len(sizes)} files, {total_size / 1024 / 1024:.2f} MB", file=sys.stderr) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index eb520c6f1e..40af6b72b5 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -56,6 +56,29 @@ void buildXLocationsJsonFilter(JsonDocument& filter) { spineEntry["wordCount"] = true; spineEntry["characterStart"] = true; spineEntry["characterCount"] = true; + spineEntry["chapterGroup"] = true; + + JsonArray chapterGroups = root["chapterGroups"].to(); + JsonObject chapterGroup = chapterGroups.add(); + chapterGroup["index"] = true; + chapterGroup["firstSpineIndex"] = true; + chapterGroup["lastSpineIndex"] = true; + chapterGroup["startSpineIndex"] = true; + chapterGroup["endSpineIndex"] = true; + + JsonObject sourceMap = root["sourceSpineMap"].to(); + sourceMap["version"] = true; + sourceMap["spineCount"] = true; + JsonArray sourceSpine = sourceMap["spine"].to(); + JsonObject sourceEntry = sourceSpine.add(); + sourceEntry["index"] = true; + sourceEntry["sourceSpineIndex"] = true; + sourceEntry["containerDepth"] = true; + JsonArray ranges = sourceEntry["childRanges"].to(); + JsonObject range = ranges.add(); + range["name"] = true; + range["offset"] = true; + range["count"] = true; } float clampUnit(const float value) { @@ -286,7 +309,8 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const { return true; } -bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, const bool writeSpineEntries) { +bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, const bool writeSpineEntries, + const bool collectCssFiles) { std::string contentOpfFilePath; if (!findContentOpfFile(&contentOpfFilePath)) { LOG_ERR("EBP", "Could not find content.opf in zip"); @@ -295,8 +319,6 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, const contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1); - LOG_DBG("EBP", "Parsing content.opf: %s", contentOpfFilePath.c_str()); - size_t contentOpfSize; if (!getItemSize(contentOpfFilePath, &contentOpfSize)) { LOG_ERR("EBP", "Could not get size of content.opf"); @@ -304,14 +326,20 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, const } ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, - writeSpineEntries ? bookMetadataCache.get() : nullptr); + writeSpineEntries ? bookMetadataCache.get() : nullptr, collectCssFiles); if (!opfParser.setup()) { LOG_ERR("EBP", "Could not setup content.opf parser"); + if (opfParser.failedForLowMemory()) { + lastLoadFailure = OpenFailure::OutOfMemory; + } return false; } if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) { LOG_ERR("EBP", "Could not read content.opf"); + if (opfParser.failedForLowMemory()) { + lastLoadFailure = OpenFailure::OutOfMemory; + } return false; } @@ -349,11 +377,10 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, const tocNavItem = opfParser.tocNavPath; } - if (!opfParser.cssFiles.empty()) { - cssFiles = opfParser.cssFiles; + if (collectCssFiles && !opfParser.cssFiles.empty()) { + cssFiles = std::move(opfParser.cssFiles); } - LOG_DBG("EBP", "Successfully parsed content.opf"); return true; } @@ -364,20 +391,11 @@ bool Epub::parseTocNcxFile() const { return false; } - LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str()); - - const auto tmpNcxPath = getCachePath() + "/toc.ncx"; - FsFile tempNcxFile; - if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) { + size_t ncxSize; + if (!getItemSize(tocNcxItem, &ncxSize)) { + LOG_ERR("EBP", "Could not get size of toc ncx file"); return false; } - readItemContentsToStream(tocNcxItem, tempNcxFile, 1024); - // Explicitly close() file before reopening for reading - tempNcxFile.close(); - if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) { - return false; - } - const auto ncxSize = tempNcxFile.size(); TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get()); @@ -386,30 +404,13 @@ bool Epub::parseTocNcxFile() const { return false; } - const auto ncxBuffer = static_cast(malloc(1024)); - if (!ncxBuffer) { - LOG_ERR("EBP", "Could not allocate memory for toc ncx parser"); + // Stream the decompressed NCX straight into the parser instead of round-tripping + // through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete). + if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) { + LOG_ERR("EBP", "Could not read toc ncx file"); return false; } - while (tempNcxFile.available()) { - const auto readSize = tempNcxFile.read(ncxBuffer, 1024); - if (readSize == 0) break; - const auto processedSize = ncxParser.write(ncxBuffer, readSize); - - if (processedSize != readSize) { - LOG_ERR("EBP", "Could not process all toc ncx data"); - free(ncxBuffer); - return false; - } - } - - free(ncxBuffer); - // Explicitly close() file before calling Storage.remove() - tempNcxFile.close(); - Storage.remove(tmpNcxPath.c_str()); - - LOG_DBG("EBP", "Parsed TOC items"); return true; } @@ -420,20 +421,11 @@ bool Epub::parseTocNavFile() const { return false; } - LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str()); - - const auto tmpNavPath = getCachePath() + "/toc.nav"; - FsFile tempNavFile; - if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) { - return false; - } - readItemContentsToStream(tocNavItem, tempNavFile, 1024); - // Explicitly close() file before reopening for reading - tempNavFile.close(); - if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) { + size_t navSize; + if (!getItemSize(tocNavItem, &navSize)) { + LOG_ERR("EBP", "Could not get size of toc nav file"); return false; } - const auto navSize = tempNavFile.size(); // Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf // and the HTMLX nav file will have hrefs relative to itself @@ -445,29 +437,13 @@ bool Epub::parseTocNavFile() const { return false; } - const auto navBuffer = static_cast(malloc(1024)); - if (!navBuffer) { - LOG_ERR("EBP", "Could not allocate memory for toc nav parser"); + // Stream the decompressed nav document straight into the parser instead of round-tripping + // through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete). + if (!readItemContentsToStream(tocNavItem, navParser, 1024)) { + LOG_ERR("EBP", "Could not read toc nav file"); return false; } - while (tempNavFile.available()) { - const auto readSize = tempNavFile.read(navBuffer, 1024); - const auto processedSize = navParser.write(navBuffer, readSize); - - if (processedSize != readSize) { - LOG_ERR("EBP", "Could not process all toc nav data"); - free(navBuffer); - return false; - } - } - - free(navBuffer); - // Explicitly close() file before calling Storage.remove() - tempNavFile.close(); - Storage.remove(tmpNavPath.c_str()); - - LOG_DBG("EBP", "Parsed TOC nav items"); return true; } @@ -488,13 +464,19 @@ void Epub::discoverCssFilesFromZip() { return; } - LOG_DBG("EBP", "Discovered CSS file via ZIP enumeration: %.*s", (int)filePath.size(), filePath.data()); cssFiles.push_back(std::string{filePath}); })) { LOG_ERR("EBP", "Failed to enumerate ZIP file paths for CSS discovery"); } } +void Epub::releaseCssFileList() { + // Stylesheet paths are one-shot cache-build scratch. Swap with an empty + // vector so hundreds of generator-emitted paths do not remain resident + // while the chapter itself is laid out. + std::vector().swap(cssFiles); +} + Epub::CssParseStatus Epub::parseCssFiles(const bool forceRebuild) const { // Maximum CSS file size we'll attempt to parse (uncompressed) // Larger files risk memory exhaustion on ESP32 @@ -502,23 +484,23 @@ Epub::CssParseStatus Epub::parseCssFiles(const bool forceRebuild) const { // Minimum heap required before attempting CSS parsing constexpr size_t MIN_HEAP_FOR_CSS_PARSING = 64 * 1024; // 64KB - if (cssFiles.empty()) { - LOG_DBG("EBP", "No CSS files to parse, but CssParser created for inline styles"); - } - - LOG_DBG("EBP", "CSS files to parse: %zu", cssFiles.size()); - - // See if we have a usable cached version of the CSS rules. File existence alone is not enough: - // stale cache formats are removed by loadFromCache(), then rebuilt below. - if (cssParser->hasCache() && !forceRebuild) { - if (cssParser->loadFromCache()) { - const bool partialCache = cssParser->isCachePartial(); - LOG_DBG("EBP", "CSS cache valid, skipping parseCssFiles"); - cssParser->clear(); - return partialCache ? CssParseStatus::Partial : CssParseStatus::Complete; + // Cache validation is intentionally disk-only here. Hydrating the rules just + // to inspect the header duplicates the section builder's peak allocation. + if (!forceRebuild) { + switch (cssParser->inspectCache()) { + case CssParser::CacheStatus::Complete: + LOG_DBG("EBP", "CSS cache valid, skipping parseCssFiles"); + return CssParseStatus::Complete; + case CssParser::CacheStatus::Partial: + LOG_DBG("EBP", "Partial CSS cache valid, skipping parseCssFiles"); + return CssParseStatus::Partial; + case CssParser::CacheStatus::Invalid: + LOG_DBG("EBP", "CSS cache invalid, rebuilding CSS rules"); + cssParser->deleteCache(); + break; + case CssParser::CacheStatus::Missing: + break; } - LOG_DBG("EBP", "CSS cache invalid, rebuilding CSS rules"); - cssParser->clear(); } // No cache yet - parse CSS files. If memory runs out partway through, keep @@ -526,11 +508,55 @@ Epub::CssParseStatus Epub::parseCssFiles(const bool forceRebuild) const { // chapter layout can still use most of the book's stylesheet. bool parsedAllCss = true; size_t parsedCssFileCount = 0; + size_t skippedDuplicateCount = 0; size_t failedCssFileIndex = 0; std::string failedCssPath; + + // Some EPUB generators emit a byte-identical stylesheet for every chapter. + // Resolve all CSS identities with one ZIP directory scan, then retain only + // identities whose first stylesheet parsed successfully. These temporary + // arrays are fallible and live only for this one-time cache build. + std::unique_ptr cssTargets; + std::unique_ptr cssIdentities; + std::unique_ptr parsedIdentities; + size_t parsedIdentityCount = 0; + if (cssFiles.size() > 1 && cssFiles.size() <= UINT16_MAX) { + cssTargets = makeUniqueNoThrow(cssFiles.size()); + cssIdentities = makeUniqueNoThrow(cssFiles.size()); + parsedIdentities = makeUniqueNoThrow(cssFiles.size()); + if (!cssTargets || !cssIdentities || !parsedIdentities) { + LOG_ERR("EBP", "Could not allocate CSS duplicate metadata; parsing all stylesheets"); + cssTargets.reset(); + cssIdentities.reset(); + parsedIdentities.reset(); + } else { + for (size_t i = 0; i < cssFiles.size(); ++i) { + cssTargets[i] = {ZipFile::fnvHash64(cssFiles[i].data(), cssFiles[i].size()), + static_cast(cssFiles[i].size()), static_cast(i), cssFiles[i].c_str()}; + } + std::sort(cssTargets.get(), cssTargets.get() + cssFiles.size(), + [](const ZipFile::EntryTarget& a, const ZipFile::EntryTarget& b) { + return a.hash < b.hash || (a.hash == b.hash && a.len < b.len); + }); + ZipFile(filepath).fillEntryIdentities(cssTargets.get(), cssFiles.size(), cssIdentities.get(), cssFiles.size()); + } + } + for (size_t cssFileIndex = 0; cssFileIndex < cssFiles.size(); ++cssFileIndex) { const auto& cssPath = cssFiles[cssFileIndex]; - LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str()); + const auto* identity = cssIdentities ? &cssIdentities[cssFileIndex] : nullptr; + if (identity && identity->found) { + const auto duplicate = std::find_if(parsedIdentities.get(), parsedIdentities.get() + parsedIdentityCount, + [identity](const ZipFile::EntryIdentity& parsed) { + return parsed.crc32 == identity->crc32 && + parsed.compressedSize == identity->compressedSize && + parsed.uncompressedSize == identity->uncompressedSize; + }); + if (duplicate != parsedIdentities.get() + parsedIdentityCount) { + ++skippedDuplicateCount; + continue; + } + } // Check heap before parsing - CSS parsing allocates heavily const uint32_t freeHeap = ESP.getFreeHeap(); @@ -584,6 +610,9 @@ Epub::CssParseStatus Epub::parseCssFiles(const bool forceRebuild) const { parsedAllCss = false; } else { ++parsedCssFileCount; + if (identity && identity->found) { + parsedIdentities[parsedIdentityCount++] = *identity; + } } // Explicitly close() file before calling Storage.remove() tempCssFile.close(); @@ -614,22 +643,26 @@ Epub::CssParseStatus Epub::parseCssFiles(const bool forceRebuild) const { LOG_DBG("EBP", "Loaded %zu %s CSS style rules from %zu/%zu files", cssParser->ruleCount(), parsedAllCss ? "complete" : "partial", parsedCssFileCount, cssFiles.size()); + if (skippedDuplicateCount > 0) { + LOG_DBG("EBP", "Skipped %zu byte-identical CSS files", skippedDuplicateCount); + } cssParser->clear(); return parsedAllCss ? CssParseStatus::Complete : CssParseStatus::Partial; } // load in the meta data for the epub file -bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { - LOG_DBG("EBP", "Loading ePub: %s", filepath.c_str()); - +bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss, const XLocationLoadMode xLocationLoadMode) { + lastLoadFailure = OpenFailure::InvalidOrUnreadable; // Initialize spine/TOC cache bookMetadataCache = makeBookMetadataCacheNoThrow(cachePath); if (!bookMetadataCache) { + lastLoadFailure = OpenFailure::OutOfMemory; return false; } // Always create CssParser - needed for inline style parsing even without CSS files cssParser = makeCssParserNoThrow(cachePath); if (!cssParser) { + lastLoadFailure = OpenFailure::OutOfMemory; bookMetadataCache.reset(); return false; } @@ -637,30 +670,33 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { // Try to load existing cache first if (bookMetadataCache->load()) { if (!skipLoadingCss) { - // Rebuild CSS cache when missing or when cache version changed (loadFromCache removes stale file) + // Rebuild CSS cache when missing, invalid, or previously incomplete. bool rebuildCssCache = false; bool forceCssRebuild = false; bool retryingPartialCssCache = false; - if (!cssParser->hasCache()) { - LOG_DBG("EBP", "CSS rules cache missing, attempting to parse CSS files"); - rebuildCssCache = true; - } else if (!cssParser->loadFromCache()) { - LOG_DBG("EBP", "CSS rules cache stale, attempting to parse CSS files"); - cssParser->deleteCache(); - rebuildCssCache = true; - } else if (cssParser->isCachePartial()) { - LOG_DBG("EBP", "CSS rules cache is partial, attempting to rebuild complete CSS cache"); - cssParser->clear(); - rebuildCssCache = true; - forceCssRebuild = true; - retryingPartialCssCache = true; - } else { - cssParser->clear(); + switch (cssParser->inspectCache()) { + case CssParser::CacheStatus::Missing: + LOG_DBG("EBP", "CSS rules cache missing, attempting to parse CSS files"); + rebuildCssCache = true; + break; + case CssParser::CacheStatus::Invalid: + LOG_DBG("EBP", "CSS rules cache invalid, attempting to parse CSS files"); + cssParser->deleteCache(); + rebuildCssCache = true; + break; + case CssParser::CacheStatus::Partial: + LOG_DBG("EBP", "CSS rules cache is partial, attempting to rebuild complete CSS cache"); + rebuildCssCache = true; + forceCssRebuild = true; + retryingPartialCssCache = true; + break; + case CssParser::CacheStatus::Complete: + break; } if (rebuildCssCache) { BookMetadataCache::BookMetadata cachedMetadata = bookMetadataCache->coreMetadata; - if (!parseContentOpf(cachedMetadata, /*writeSpineEntries=*/false)) { + if (!parseContentOpf(cachedMetadata, /*writeSpineEntries=*/false, /*collectCssFiles=*/true)) { LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files"); // continue anyway - book will work without CSS and we'll still load any inline style CSS } else { @@ -668,8 +704,10 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { } bookMetadataCache.reset(); const CssParseStatus cssStatus = parseCssFiles(forceCssRebuild); + releaseCssFileList(); bookMetadataCache = makeBookMetadataCacheNoThrow(cachePath); if (!bookMetadataCache) { + lastLoadFailure = OpenFailure::OutOfMemory; return false; } if (!bookMetadataCache->load()) { @@ -687,8 +725,15 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { } } } - loadXLocations(); - LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str()); + // Release the resolved CSS rule map: it is only needed transiently while building + // section caches, and createSectionFile reloads it from cache on demand. Holding it + // resident pins tens of KB for the whole reading session (more on warm resume into + // an already-cached chapter, where createSectionFile never runs to clear it). + cssParser->clear(); + if (xLocationLoadMode == XLocationLoadMode::Immediate) { + loadXLocations(); + } + lastLoadFailure = OpenFailure::None; return true; } @@ -710,27 +755,29 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { } // OPF Pass - const uint32_t opfStart = millis(); BookMetadataCache::BookMetadata bookMetadata; if (!bookMetadataCache->beginContentOpfPass()) { LOG_ERR("EBP", "Could not begin writing content.opf pass"); return false; } - if (!parseContentOpf(bookMetadata)) { + if (!parseContentOpf(bookMetadata, /*writeSpineEntries=*/true, /*collectCssFiles=*/!skipLoadingCss)) { LOG_ERR("EBP", "Could not parse content.opf"); return false; } - discoverCssFilesFromZip(); + if (!skipLoadingCss) { + discoverCssFilesFromZip(); + } if (!bookMetadataCache->endContentOpfPass()) { LOG_ERR("EBP", "Could not end writing content.opf pass"); return false; } - LOG_DBG("EBP", "OPF pass completed in %lu ms", millis() - opfStart); // TOC Pass - try EPUB 3 nav first, fall back to NCX - const uint32_t tocStart = millis(); if (!bookMetadataCache->beginTocPass()) { LOG_ERR("EBP", "Could not begin writing toc pass"); + if (bookMetadataCache->failedForLowMemory()) { + lastLoadFailure = OpenFailure::OutOfMemory; + } return false; } @@ -738,13 +785,11 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { // Try EPUB 3 nav document first (preferred) if (!tocNavItem.empty()) { - LOG_DBG("EBP", "Attempting to parse EPUB 3 nav document"); tocParsed = parseTocNavFile(); } // Fall back to NCX if nav parsing failed or wasn't available if (!tocParsed && !tocNcxItem.empty()) { - LOG_DBG("EBP", "Falling back to NCX TOC"); tocParsed = parseTocNcxFile(); } @@ -757,7 +802,6 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { LOG_ERR("EBP", "Could not end writing toc pass"); return false; } - LOG_DBG("EBP", "TOC pass completed in %lu ms", millis() - tocStart); // Close the cache files if (!bookMetadataCache->endWrite()) { @@ -766,12 +810,13 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { } // Build final book.bin - const uint32_t buildStart = millis(); if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) { LOG_ERR("EBP", "Could not update mappings and sizes"); + if (bookMetadataCache->failedForLowMemory()) { + lastLoadFailure = OpenFailure::OutOfMemory; + } return false; } - LOG_DBG("EBP", "buildBookBin completed in %lu ms", millis() - buildStart); LOG_DBG("EBP", "Total indexing completed in %lu ms", millis() - indexingStart); if (!bookMetadataCache->cleanupTmpFiles()) { @@ -786,11 +831,13 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { } else { LOG_ERR("EBP", "CSS cache build failed; leaving any existing section caches in place"); } + releaseCssFileList(); } // Reload the cache from disk so it's in the correct state bookMetadataCache = makeBookMetadataCacheNoThrow(cachePath); if (!bookMetadataCache) { + lastLoadFailure = OpenFailure::OutOfMemory; return false; } if (!bookMetadataCache->load()) { @@ -798,14 +845,15 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { return false; } - loadXLocations(); - LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str()); + if (xLocationLoadMode == XLocationLoadMode::Immediate) { + loadXLocations(); + } + lastLoadFailure = OpenFailure::None; return true; } bool Epub::clearCache() const { if (!Storage.exists(cachePath.c_str())) { - LOG_DBG("EPB", "Cache does not exist, no action needed"); return true; } @@ -814,7 +862,6 @@ bool Epub::clearCache() const { return false; } - LOG_DBG("EPB", "Cache cleared successfully"); return true; } @@ -857,6 +904,10 @@ const std::string& Epub::getLanguage() const { return bookMetadataCache->coreMetadata.language; } +bool Epub::hasCoverImage() const { + return bookMetadataCache && bookMetadataCache->isLoaded() && !bookMetadataCache->coreMetadata.coverItemHref.empty(); +} + std::string Epub::getCoverBmpPath(bool cropped) const { const auto coverFileName = std::string("cover") + (cropped ? "_crop" : ""); return cachePath + "/" + coverFileName + ".bmp"; @@ -880,7 +931,6 @@ bool Epub::generateCoverBmp(bool cropped, const GfxRenderer* renderer, const int } if (FsHelpers::hasJpgExtension(coverImageHref)) { - LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit"); std::string coverJpgPath; if (!ensureCachedCoverImage(coverImageHref, coverJpgPath)) { return false; @@ -906,12 +956,10 @@ bool Epub::generateCoverBmp(bool cropped, const GfxRenderer* renderer, const int LOG_ERR("EBP", "Failed to generate BMP from cover image"); Storage.remove(getCoverBmpPath(cropped).c_str()); } - LOG_DBG("EBP", "Generated BMP from JPG cover image, success: %s", success ? "yes" : "no"); return success; } if (FsHelpers::hasPngExtension(coverImageHref)) { - LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit"); std::string coverPngPath; if (!ensureCachedCoverImage(coverImageHref, coverPngPath)) { return false; @@ -937,7 +985,6 @@ bool Epub::generateCoverBmp(bool cropped, const GfxRenderer* renderer, const int LOG_ERR("EBP", "Failed to generate BMP from PNG cover image"); Storage.remove(getCoverBmpPath(cropped).c_str()); } - LOG_DBG("EBP", "Generated BMP from PNG cover image, success: %s", success ? "yes" : "no"); return success; } @@ -1023,7 +1070,6 @@ bool Epub::ensureCachedCoverImage(const std::string& coverImageHref, std::string return false; } - LOG_DBG("EBP", "Cached cover image source: %s", outPath.c_str()); return true; } @@ -1050,7 +1096,6 @@ bool Epub::generateThumbBmpInternal(int width, int height, const bool adaptiveCo if (coverImageHref.empty()) { LOG_DBG("EBP", "No known cover image for thumbnail"); } else if (FsHelpers::hasJpgExtension(coverImageHref)) { - LOG_DBG("EBP", "Generating thumb BMP from JPG cover image"); std::string coverJpgPath; if (!ensureCachedCoverImage(coverImageHref, coverJpgPath)) { return false; @@ -1079,10 +1124,8 @@ bool Epub::generateThumbBmpInternal(int width, int height, const bool adaptiveCo LOG_ERR("EBP", "Failed to generate thumb BMP from JPG cover image"); Storage.remove(thumbPath.c_str()); } - LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no"); return success; } else if (FsHelpers::hasPngExtension(coverImageHref)) { - LOG_DBG("EBP", "Generating thumb BMP from PNG cover image"); std::string coverPngPath; if (!ensureCachedCoverImage(coverImageHref, coverPngPath)) { return false; @@ -1111,7 +1154,6 @@ bool Epub::generateThumbBmpInternal(int width, int height, const bool adaptiveCo LOG_ERR("EBP", "Failed to generate thumb BMP from PNG cover image"); Storage.remove(thumbPath.c_str()); } - LOG_DBG("EBP", "Generated thumb BMP from PNG cover image, success: %s", success ? "yes" : "no"); return success; } else { LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail"); @@ -1137,14 +1179,41 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size return content; } -bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const { +bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize, + const bool allowEarlyStop) const { if (itemHref.empty()) { LOG_DBG("EBP", "Failed to read item, empty href"); return false; } const std::string path = FsHelpers::normalisePath(itemHref); - return ZipFile(filepath).readFileToStream(path.c_str(), out, chunkSize); + return ZipFile(filepath).readFileToStream(path.c_str(), out, chunkSize, allowEarlyStop); +} + +bool Epub::extractItemToFile(const std::string& itemHref, const std::string& destPath) const { + FsFile out; + if (!Storage.openFileForWrite("EBP", destPath, out)) { + return false; + } + + const bool success = readItemContentsToStream(itemHref, out, 4096); + out.flush(); + out.close(); + if (!success) { + Storage.remove(destPath.c_str()); + } + return success; +} + +std::unique_ptr Epub::openItemContentsStream(const std::string& itemHref, + const size_t chunkSize) const { + if (itemHref.empty()) { + LOG_DBG("EBP", "Failed to open item stream, empty href"); + return nullptr; + } + + const std::string path = FsHelpers::normalisePath(itemHref); + return ZipFile(filepath).openFileStream(path.c_str(), chunkSize); } bool Epub::getItemSize(const std::string& itemHref, size_t* size) const { @@ -1153,7 +1222,16 @@ bool Epub::getItemSize(const std::string& itemHref, size_t* size) const { } bool Epub::loadXLocations() { - locationSpine.clear(); + locationSpine.reset(); + locationSpineCount = 0; + locationChapterGroups.reset(); + locationChapterGroupCount = 0; + sourceSpineMap.reset(); + sourceChildRanges.reset(); + sourceSpineMapCount = 0; + sourceChildRangeCount = 0; + sourceSpineCount = 0; + sourceSpineMapDeclared = false; totalLocations = 0; totalWords = 0; wordsPerReferencePage = 0; @@ -1210,13 +1288,19 @@ bool Epub::loadXLocations() { useCharacterReferencePages ? parsedCharactersPerReferencePage : parsedWordsPerReferencePage; const uint32_t parsedTotalReferencePages = doc["totalReferencePages"] | 0; JsonArrayConst spine = doc["spine"]; + JsonArrayConst chapterGroups = doc["chapterGroups"]; + JsonObjectConst parsedSourceMap = doc["sourceSpineMap"]; if (!isSupportedLocationsFormat(format) || version != 1 || parsedTotalLocations == 0 || spine.isNull()) { LOG_ERR("EBP", "Ignoring unsupported X locations manifest"); return false; } - locationSpine.assign(static_cast(spineCount), {}); + auto parsedSpine = makeUniqueNoThrow(static_cast(spineCount)); + if (!parsedSpine) { + LOG_ERR("EBP", "OOM: X location spine (%zu bytes)", static_cast(spineCount) * sizeof(LocationSpineEntry)); + return false; + } bool hasValidEntry = false; size_t ordinal = 0; for (JsonObjectConst spineItem : spine) { @@ -1232,6 +1316,10 @@ bool Epub::loadXLocations() { useCharacterReferencePages ? (spineItem["characterStart"] | 0) : (spineItem["wordStart"] | 0); const uint32_t wordCount = useCharacterReferencePages ? (spineItem["characterCount"] | 0) : (spineItem["wordCount"] | 0); + const int chapterGroup = spineItem["chapterGroup"] | -1; + const uint16_t parsedChapterGroup = + chapterGroup >= 0 && chapterGroup <= UINT16_MAX ? static_cast(chapterGroup) : UINT16_MAX; + parsedSpine[static_cast(index)].chapterGroup = parsedChapterGroup; if (startLocation == 0 && endLocation == 0) { continue; } @@ -1240,15 +1328,17 @@ bool Epub::loadXLocations() { continue; } - locationSpine[static_cast(index)] = {startLocation, endLocation, wordStart, wordCount}; + parsedSpine[static_cast(index)] = {startLocation, endLocation, wordStart, wordCount, parsedChapterGroup}; hasValidEntry = true; } if (!hasValidEntry) { - locationSpine.clear(); return false; } + locationSpine = std::move(parsedSpine); + locationSpineCount = static_cast(spineCount); + totalLocations = parsedTotalLocations; totalWords = parsedReferenceUnits; wordsPerReferencePage = @@ -1257,13 +1347,185 @@ bool Epub::loadXLocations() { if (totalReferencePages == 0 && totalWords > 0 && wordsPerReferencePage > 0) { totalReferencePages = (totalWords + wordsPerReferencePage - 1) / wordsPerReferencePage; } + + if (!chapterGroups.isNull() && chapterGroups.size() > 0 && chapterGroups.size() <= static_cast(spineCount)) { + const size_t groupCount = chapterGroups.size(); + auto parsedGroups = makeUniqueNoThrow(groupCount); + if (!parsedGroups) { + LOG_ERR("EBP", "OOM: chapter groups (%zu bytes)", groupCount * sizeof(LocationChapterGroupEntry)); + } else { + size_t ordinalGroup = 0; + for (JsonObjectConst group : chapterGroups) { + const int index = group["index"] | static_cast(ordinalGroup); + ordinalGroup++; + if (index < 0 || index >= static_cast(groupCount)) continue; + int firstSpineIndex = group["firstSpineIndex"] | -1; + int lastSpineIndex = group["lastSpineIndex"] | -1; + if (firstSpineIndex < 0) firstSpineIndex = group["startSpineIndex"] | -1; + if (lastSpineIndex < 0) lastSpineIndex = group["endSpineIndex"] | -1; + if (firstSpineIndex < 0 || lastSpineIndex < firstSpineIndex || lastSpineIndex >= spineCount) continue; + parsedGroups[static_cast(index)] = {static_cast(firstSpineIndex), + static_cast(lastSpineIndex), true}; + } + for (size_t i = 0; i < locationSpineCount; i++) { + auto& entry = locationSpine[i]; + if (entry.chapterGroup >= groupCount || !parsedGroups[entry.chapterGroup].valid) { + entry.chapterGroup = UINT16_MAX; + } + } + locationChapterGroups = std::move(parsedGroups); + locationChapterGroupCount = groupCount; + } + } + + if (!parsedSourceMap.isNull()) { + sourceSpineMapDeclared = true; + const int mapVersion = parsedSourceMap["version"] | 0; + const int parsedSourceSpineCount = parsedSourceMap["spineCount"] | 0; + JsonArrayConst mappedSpine = parsedSourceMap["spine"]; + bool mapValid = mapVersion == 1 && parsedSourceSpineCount > 0 && parsedSourceSpineCount <= UINT16_MAX && + !mappedSpine.isNull() && mappedSpine.size() == static_cast(spineCount); + size_t totalRanges = 0; + if (mapValid) { + for (JsonObjectConst mappedEntry : mappedSpine) { + JsonArrayConst childRanges = mappedEntry["childRanges"]; + if (!childRanges.isNull()) totalRanges += childRanges.size(); + if (totalRanges > static_cast(spineCount) * 16U || totalRanges > UINT16_MAX) { + mapValid = false; + break; + } + } + } + + auto parsedEntries = mapValid ? makeUniqueNoThrow(static_cast(spineCount)) : nullptr; + auto parsedRanges = mapValid && totalRanges > 0 ? makeUniqueNoThrow(totalRanges) : nullptr; + if (mapValid && (!parsedEntries || (totalRanges > 0 && !parsedRanges))) { + LOG_ERR("EBP", "OOM: source spine map (%zu bytes)", + static_cast(spineCount) * sizeof(SourceSpineMapEntry) + totalRanges * sizeof(SourceChildRange)); + mapValid = false; + } + + if (mapValid) { + size_t nextRange = 0; + auto seen = makeUniqueNoThrow(static_cast(spineCount)); + if (!seen) { + LOG_ERR("EBP", "OOM: source spine validation (%d bytes)", spineCount); + mapValid = false; + } else { + size_t ordinalEntry = 0; + for (JsonObjectConst mappedEntry : mappedSpine) { + const int index = mappedEntry["index"] | static_cast(ordinalEntry); + ordinalEntry++; + const int sourceIndex = mappedEntry["sourceSpineIndex"] | -1; + const int containerDepth = mappedEntry["containerDepth"] | 0; + JsonArrayConst childRanges = mappedEntry["childRanges"]; + const size_t rangeCount = childRanges.isNull() ? 0 : childRanges.size(); + if (index < 0 || index >= spineCount || seen[static_cast(index)] || sourceIndex < 0 || + sourceIndex >= parsedSourceSpineCount || containerDepth < 0 || containerDepth >= 16 || + rangeCount > UINT8_MAX) { + mapValid = false; + break; + } + seen[static_cast(index)] = true; + SourceSpineMapEntry& target = parsedEntries[static_cast(index)]; + target.sourceSpineIndex = static_cast(sourceIndex); + target.firstRange = static_cast(nextRange); + target.rangeCount = static_cast(rangeCount); + target.containerDepth = static_cast(containerDepth); + for (JsonObjectConst childRange : childRanges) { + const char* name = childRange["name"] | ""; + const int offset = childRange["offset"] | -1; + const int count = childRange["count"] | 0; + const size_t nameLen = std::strlen(name); + if (nameLen == 0 || nameLen >= sizeof(SourceChildRange::name) || offset < 0 || offset > UINT16_MAX || + count <= 0 || count > UINT16_MAX || static_cast(offset) + count > UINT16_MAX) { + mapValid = false; + break; + } + SourceChildRange& range = parsedRanges[nextRange++]; + std::memcpy(range.name, name, nameLen + 1); + range.offset = static_cast(offset); + range.count = static_cast(count); + } + if (!mapValid) break; + } + } + } + + if (mapValid) { + sourceSpineMap = std::move(parsedEntries); + sourceChildRanges = std::move(parsedRanges); + sourceSpineMapCount = static_cast(spineCount); + sourceChildRangeCount = totalRanges; + sourceSpineCount = static_cast(parsedSourceSpineCount); + } else { + LOG_ERR("EBP", "Ignoring malformed source spine map"); + } + } xLocationsLoaded = true; LOG_INF("EBP", "Loaded X locations: %lu locations, %lu reference pages across %zu spine items", static_cast(totalLocations), static_cast(totalReferencePages), - locationSpine.size()); + locationSpineCount); return true; } +bool Epub::resolveChapterGroupRange(const int currentSpineIndex, int& firstSpineIndex, int& lastSpineIndex) const { + firstSpineIndex = currentSpineIndex; + lastSpineIndex = currentSpineIndex; + if (currentSpineIndex < 0 || currentSpineIndex >= static_cast(locationSpineCount) || !locationChapterGroups) { + return false; + } + const uint16_t groupIndex = locationSpine[static_cast(currentSpineIndex)].chapterGroup; + if (groupIndex >= locationChapterGroupCount || !locationChapterGroups[groupIndex].valid) return false; + const auto& group = locationChapterGroups[groupIndex]; + if (currentSpineIndex < group.firstSpineIndex || currentSpineIndex > group.lastSpineIndex) return false; + firstSpineIndex = group.firstSpineIndex; + lastSpineIndex = group.lastSpineIndex; + return true; +} + +bool Epub::getSourceSpineMapEntry(const int currentSpineIndex, SourceSpineMapEntry& entry) const { + if (!sourceSpineMap || currentSpineIndex < 0 || currentSpineIndex >= static_cast(sourceSpineMapCount)) { + return false; + } + entry = sourceSpineMap[static_cast(currentSpineIndex)]; + return entry.sourceSpineIndex != UINT16_MAX; +} + +const Epub::SourceChildRange* Epub::getSourceChildRange(const SourceSpineMapEntry& entry, const size_t ordinal) const { + if (!sourceChildRanges || ordinal >= entry.rangeCount) return nullptr; + const size_t index = static_cast(entry.firstRange) + ordinal; + return index < sourceChildRangeCount ? &sourceChildRanges[index] : nullptr; +} + +bool Epub::findCurrentSpineForSource(const int sourceIndex, const uint8_t containerDepth, const char* childName, + const uint16_t sourceSiblingIndex, int& currentSpineIndex, + uint16_t& currentSiblingIndex) const { + if (!sourceSpineMap || sourceIndex < 0 || sourceIndex >= sourceSpineCount) return false; + for (size_t i = 0; i < sourceSpineMapCount; ++i) { + const SourceSpineMapEntry& entry = sourceSpineMap[i]; + if (entry.sourceSpineIndex != sourceIndex) continue; + if (!childName || entry.rangeCount == 0) { + currentSpineIndex = static_cast(i); + currentSiblingIndex = sourceSiblingIndex; + return true; + } + if (entry.containerDepth != containerDepth) continue; + for (size_t rangeIndex = 0; rangeIndex < entry.rangeCount; ++rangeIndex) { + const SourceChildRange* range = getSourceChildRange(entry, rangeIndex); + if (!range || strcasecmp(range->name, childName) != 0) continue; + const uint32_t first = static_cast(range->offset) + 1U; + const uint32_t last = static_cast(range->offset) + range->count; + if (sourceSiblingIndex >= first && sourceSiblingIndex <= last) { + currentSpineIndex = static_cast(i); + currentSiblingIndex = static_cast(sourceSiblingIndex - range->offset); + return true; + } + } + } + return false; +} + int Epub::getSpineItemsCount() const { if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { return 0; @@ -1351,10 +1613,6 @@ int Epub::getSpineIndexForTextReference() const { LOG_ERR("EBP", "getSpineIndexForTextReference called but cache not loaded"); return 0; } - LOG_DBG("EBP", "Core Metadata: cover(%d)=%s, textReference(%d)=%s", - bookMetadataCache->coreMetadata.coverItemHref.size(), bookMetadataCache->coreMetadata.coverItemHref.c_str(), - bookMetadataCache->coreMetadata.textReferenceHref.size(), - bookMetadataCache->coreMetadata.textReferenceHref.c_str()); if (bookMetadataCache->coreMetadata.textReferenceHref.empty()) { // there was no textReference in epub, so we return 0 (the first chapter) @@ -1364,8 +1622,6 @@ int Epub::getSpineIndexForTextReference() const { // loop through spine items to get the correct index matching the text href for (size_t i = 0; i < getSpineItemsCount(); i++) { if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) { - LOG_DBG("EBP", "Text reference %s found at index %d", bookMetadataCache->coreMetadata.textReferenceHref.c_str(), - i); return i; } } @@ -1389,7 +1645,7 @@ float Epub::calculateSizeProgress(const int currentSpineIndex, const float curre // Calculate progress in book (returns 0.0-1.0) float Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const { if (!xLocationsLoaded || totalLocations == 0 || currentSpineIndex < 0 || - currentSpineIndex >= static_cast(locationSpine.size())) { + currentSpineIndex >= static_cast(locationSpineCount)) { return calculateSizeProgress(currentSpineIndex, currentSpineRead); } @@ -1405,7 +1661,7 @@ float Epub::calculateProgress(const int currentSpineIndex, const float currentSp } bool Epub::resolveLocationPercentToSpineProgress(const int percent, int& spineIndex, float& spineProgress) const { - if (!xLocationsLoaded || totalLocations == 0 || locationSpine.empty()) { + if (!xLocationsLoaded || totalLocations == 0 || locationSpineCount == 0) { return false; } @@ -1417,7 +1673,7 @@ bool Epub::resolveLocationPercentToSpineProgress(const int percent, int& spineIn } if (clampedPercent >= 100) { - for (int i = static_cast(locationSpine.size()) - 1; i >= 0; i--) { + for (int i = static_cast(locationSpineCount) - 1; i >= 0; i--) { const LocationSpineEntry& entry = locationSpine[static_cast(i)]; if (entry.startLocation > 0 && entry.endLocation >= entry.startLocation) { spineIndex = i; @@ -1430,7 +1686,7 @@ bool Epub::resolveLocationPercentToSpineProgress(const int percent, int& spineIn const float targetCompletedLocations = static_cast(totalLocations) * static_cast(clampedPercent) / 100.0f; - for (size_t i = 0; i < locationSpine.size(); i++) { + for (size_t i = 0; i < locationSpineCount; i++) { const LocationSpineEntry& entry = locationSpine[i]; if (entry.startLocation == 0 || entry.endLocation < entry.startLocation) { continue; @@ -1456,7 +1712,7 @@ bool Epub::resolveReferencePage(const int currentSpineIndex, const float current currentPage = 0; pageCount = 0; if (!xLocationsLoaded || totalWords == 0 || wordsPerReferencePage == 0 || totalReferencePages == 0 || - currentSpineIndex < 0 || currentSpineIndex >= static_cast(locationSpine.size())) { + currentSpineIndex < 0 || currentSpineIndex >= static_cast(locationSpineCount)) { return false; } diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 76dea3662a..f3244a4bd3 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -12,9 +12,11 @@ #include "Epub/css/CssParser.h" class ZipFile; +class ZipFileStreamReader; class GfxRenderer; class Epub { + private: // the ncx file (EPUB 2) std::string tocNcxItem; // the nav file (EPUB 3) @@ -36,13 +38,50 @@ class Epub { uint32_t endLocation = 0; uint32_t wordStart = 0; uint32_t wordCount = 0; + uint16_t chapterGroup = UINT16_MAX; + }; + struct LocationChapterGroupEntry { + uint16_t firstSpineIndex = 0; + uint16_t lastSpineIndex = 0; + bool valid = false; + }; + + public: + enum class OpenFailure : uint8_t { + None, + OutOfMemory, + InvalidOrUnreadable, + }; + + struct SourceChildRange { + char name[12] = {}; + uint16_t offset = 0; + uint16_t count = 0; }; - std::vector locationSpine; + struct SourceSpineMapEntry { + uint16_t sourceSpineIndex = UINT16_MAX; + uint16_t firstRange = 0; + uint8_t rangeCount = 0; + uint8_t containerDepth = 0; + }; + + private: + std::unique_ptr locationSpine; + size_t locationSpineCount = 0; + std::unique_ptr locationChapterGroups; + size_t locationChapterGroupCount = 0; + std::unique_ptr sourceSpineMap; + std::unique_ptr sourceChildRanges; + size_t sourceSpineMapCount = 0; + size_t sourceChildRangeCount = 0; + uint16_t sourceSpineCount = 0; uint32_t totalLocations = 0; uint32_t totalWords = 0; uint32_t wordsPerReferencePage = 0; uint32_t totalReferencePages = 0; bool xLocationsLoaded = false; + OpenFailure lastLoadFailure = OpenFailure::None; + bool sourceSpineMapDeclared = false; enum class CssParseStatus : uint8_t { Failed, Partial, @@ -51,13 +90,20 @@ class Epub { void migrateLegacyCachePath(const std::string& cacheDir) const; bool findContentOpfFile(std::string* contentOpfFile) const; - bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, bool writeSpineEntries = true); + bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, bool writeSpineEntries = true, + bool collectCssFiles = true); bool parseTocNcxFile() const; bool parseTocNavFile() const; CssParseStatus parseCssFiles(bool forceRebuild = false) const; void discoverCssFilesFromZip(); + void releaseCssFileList(); public: + enum class XLocationLoadMode : uint8_t { + Immediate, + Skip, + }; + explicit Epub(std::string filepath, const std::string& cacheDir); ~Epub() = default; static std::string cachePathForFilePath(const std::string& filepath, const std::string& cacheDir); @@ -65,7 +111,12 @@ class Epub { // hit the fast path instead of rebuilding. Cheap: no parsing, just a stat. static bool hasCache(const std::string& filepath, const std::string& cacheDir); std::string& getBasePath() { return contentBasePath; } - bool load(bool buildIfMissing = true, bool skipLoadingCss = false); + bool load(bool buildIfMissing = true, bool skipLoadingCss = false, + XLocationLoadMode xLocationLoadMode = XLocationLoadMode::Immediate); + // Loads optional stable-page and source-spine metadata after a Skip-mode open. + // Failure leaves normal size-based progress available. + bool loadXLocations(); + OpenFailure getLastLoadFailure() const { return lastLoadFailure; } bool clearCache() const; void setupCacheDir() const; const std::string& getCachePath() const; @@ -73,6 +124,8 @@ class Epub { const std::string& getTitle() const; const std::string& getAuthor() const; const std::string& getLanguage() const; + // True when parsed EPUB metadata identifies a cover image. Requires load(). + bool hasCoverImage() const; std::string getCoverBmpPath(bool cropped = false) const; bool generateCoverBmp(bool cropped = false, const GfxRenderer* renderer = nullptr, int readerFontId = 0) const; std::string getThumbBmpPath() const; @@ -100,7 +153,10 @@ class Epub { int readerFontId = 0) 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; + bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize, + bool allowEarlyStop = false) const; + bool extractItemToFile(const std::string& itemHref, const std::string& destPath) const; + std::unique_ptr openItemContentsStream(const std::string& itemHref, size_t chunkSize) const; bool getItemSize(const std::string& itemHref, size_t* size) const; BookMetadataCache::SpineEntry getSpineItem(int spineIndex) const; BookMetadataCache::TocEntry getTocItem(int tocIndex) const; @@ -121,11 +177,19 @@ class Epub { bool resolveLocationPercentToSpineProgress(int percent, int& spineIndex, float& spineProgress) const; bool resolveReferencePage(int currentSpineIndex, float currentSpineRead, uint32_t& currentPage, uint32_t& pageCount) const; + bool resolveChapterGroupRange(int currentSpineIndex, int& firstSpineIndex, int& lastSpineIndex) const; + bool hasChapterGroups() const { return locationChapterGroupCount > 0; } + bool hasSourceSpineMap() const { return sourceSpineMapCount > 0; } + bool requiresSourceSpineMap() const { return (hasChapterGroups() || sourceSpineMapDeclared) && !hasSourceSpineMap(); } + bool getSourceSpineMapEntry(int currentSpineIndex, SourceSpineMapEntry& entry) const; + const SourceChildRange* getSourceChildRange(const SourceSpineMapEntry& entry, size_t ordinal) const; + bool findCurrentSpineForSource(int sourceIndex, uint8_t containerDepth, const char* childName, + uint16_t sourceSiblingIndex, int& currentSpineIndex, + uint16_t& currentSiblingIndex) const; CssParser* getCssParser() const { return cssParser.get(); } int resolveHrefToSpineIndex(const std::string& href) const; private: - bool loadXLocations(); std::string getCachedCoverImagePath(const std::string& coverImageHref) const; bool ensureCachedCoverImage(const std::string& coverImageHref, std::string& outPath) const; bool generateThumbBmpInternal(int width, int height, bool adaptiveContain, const GfxRenderer* renderer, diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index d7532ab9a7..7351bc5fde 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -1,5 +1,6 @@ #include "BookMetadataCache.h" +#include #include #include #include @@ -11,39 +12,91 @@ namespace { constexpr uint32_t BOOK_CACHE_MAGIC = 0x425843FF; // bytes: 0xFF, "CXB" -constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-composed +constexpr uint8_t BOOK_CACHE_VERSION = 9; // v9: NFC titles and updated guide start-reference handling constexpr char bookBinFile[] = "/book.bin"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; constexpr size_t METADATA_ARENA_SLAB_BYTES = 4096; +// Buffer size for the buildBookBin streams. 3 buffers x 4KB, transient (freed on +// return); 4KB = 8 SD sectors per transfer, enough to stop the sector-cache thrash. +constexpr size_t BUILD_IO_BUFFER_SIZE = 4096; + +// Entry (de)serializers, templated so they run over HalFile and the Buffered* +// wrappers alike (two instantiations each -- a few hundred bytes of flash, in +// exchange for the build path streaming at SD speed instead of per-pod). +template +uint32_t writeSpineEntryTo(F& file, const BookMetadataCache::SpineEntry& entry) { + const uint32_t pos = file.position(); + serialization::writeString(file, entry.href); + serialization::writePod(file, entry.cumulativeSize); + serialization::writePod(file, entry.tocIndex); + return pos; +} + +template +uint32_t writeTocEntryTo(F& file, const BookMetadataCache::TocEntry& entry) { + const uint32_t pos = file.position(); + serialization::writeString(file, entry.title); + serialization::writeString(file, entry.href); + serialization::writeString(file, entry.anchor); + serialization::writePod(file, entry.level); + serialization::writePod(file, entry.spineIndex); + return pos; +} + +template +BookMetadataCache::SpineEntry readSpineEntryFrom(F& file) { + BookMetadataCache::SpineEntry entry; + serialization::readString(file, entry.href); + serialization::readPod(file, entry.cumulativeSize); + serialization::readPod(file, entry.tocIndex); + return entry; +} + +template +BookMetadataCache::TocEntry readTocEntryFrom(F& file) { + BookMetadataCache::TocEntry entry; + serialization::readString(file, entry.title); + serialization::readString(file, entry.href); + serialization::readString(file, entry.anchor); + serialization::readPod(file, entry.level); + serialization::readPod(file, entry.spineIndex); + return entry; +} } // namespace /* ============= WRITING / BUILDING FUNCTIONS ================ */ bool BookMetadataCache::beginWrite() { + lowMemoryFailure = false; buildMode = true; spineCount = 0; tocCount = 0; - LOG_DBG("BMC", "Entering write mode"); return true; } bool BookMetadataCache::beginContentOpfPass() { - LOG_DBG("BMC", "Beginning content opf pass"); - // Open spine file for writing - return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile); + if (!Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile)) { + return false; + } + // Wrapper OOM is fine: createSpineEntry falls back to unbuffered writes. + passOut = makeUniqueNoThrow(spineFile, BUILD_IO_BUFFER_SIZE); + return true; } bool BookMetadataCache::endContentOpfPass() { + const bool flushed = !passOut || passOut->flush(); + passOut.reset(); // Explicit close() required: member variable persists beyond function scope spineFile.close(); - return true; + if (!flushed) { + LOG_ERR("BMC", "Failed writing spine tmp file"); + } + return flushed; } bool BookMetadataCache::beginTocPass() { - LOG_DBG("BMC", "Beginning toc pass"); - if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { return false; } @@ -58,6 +111,7 @@ bool BookMetadataCache::beginTocPass() { spineHrefIndexArena.release(); if (!spineHrefIndexArena.init(METADATA_ARENA_SLAB_BYTES) || !spineHrefIndex.resize(spineCount)) { LOG_ERR("BMC", "Failed to allocate spine href index arena for %u spine items", spineCount); + lowMemoryFailure = true; spineHrefIndex.resetStorage(); spineHrefIndexArena.release(); tocFile.close(); @@ -79,15 +133,21 @@ bool BookMetadataCache::beginTocPass() { }); spineFile.seek(0); useSpineHrefIndex = true; - LOG_DBG("BMC", "Using fast index for %d spine items", spineCount); } else { useSpineHrefIndex = false; } + // Wrapper OOM is fine: createTocEntry falls back to unbuffered writes. + passOut = makeUniqueNoThrow(tocFile, BUILD_IO_BUFFER_SIZE); return true; } bool BookMetadataCache::endTocPass() { + const bool flushed = !passOut || passOut->flush(); + passOut.reset(); + if (!flushed) { + LOG_ERR("BMC", "Failed writing toc tmp file"); + } // Explicit close() required: member variables persist beyond function scope tocFile.close(); spineFile.close(); @@ -96,7 +156,7 @@ bool BookMetadataCache::endTocPass() { spineHrefIndexArena.release(); useSpineHrefIndex = false; - return true; + return flushed; } bool BookMetadataCache::endWrite() { @@ -106,7 +166,6 @@ bool BookMetadataCache::endWrite() { } buildMode = false; - LOG_DBG("BMC", "Wrote %d spine, %d TOC entries", spineCount, tocCount); return true; } @@ -136,6 +195,13 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta constexpr uint32_t headerASize = sizeof(BOOK_CACHE_MAGIC) + sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount); + // Buffered streams for the whole build: every access below is sequential per + // file, but interleaved ACROSS files, which thrashes SdFat's single shared + // sector cache when unbuffered (one 512B SD transaction per 4-byte pod -- + // measured 31s for a 1,732-spine omnibus). Three 4KB buffers, freed on return. + serialization::BufferedFileWriter bookOut(bookFile, BUILD_IO_BUFFER_SIZE); + serialization::BufferedFileReader spineIn(spineFile, BUILD_IO_BUFFER_SIZE); + serialization::BufferedFileReader tocIn(tocFile, BUILD_IO_BUFFER_SIZE); const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() + metadata.coverItemHref.size() + metadata.textReferenceHref.size() + sizeof(uint32_t) * 5; @@ -143,32 +209,35 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta const uint32_t lutOffset = headerASize + metadataSize; // Header A - serialization::writePod(bookFile, BOOK_CACHE_MAGIC); - serialization::writePod(bookFile, BOOK_CACHE_VERSION); - serialization::writePod(bookFile, lutOffset); - serialization::writePod(bookFile, spineCount); - serialization::writePod(bookFile, tocCount); + serialization::writePod(bookOut, BOOK_CACHE_MAGIC); + serialization::writePod(bookOut, BOOK_CACHE_VERSION); + serialization::writePod(bookOut, lutOffset); + serialization::writePod(bookOut, spineCount); + serialization::writePod(bookOut, tocCount); // Metadata - serialization::writeString(bookFile, metadata.title); - serialization::writeString(bookFile, metadata.author); - serialization::writeString(bookFile, metadata.language); - serialization::writeString(bookFile, metadata.coverItemHref); - serialization::writeString(bookFile, metadata.textReferenceHref); + serialization::writeString(bookOut, metadata.title); + serialization::writeString(bookOut, metadata.author); + serialization::writeString(bookOut, metadata.language); + serialization::writeString(bookOut, metadata.coverItemHref); + serialization::writeString(bookOut, metadata.textReferenceHref); // Loop through spine entries, writing LUT positions - spineFile.seek(0); + spineIn.seek(0); for (int i = 0; i < spineCount; i++) { - uint32_t pos = spineFile.position(); - auto spineEntry = readSpineEntry(spineFile); - serialization::writePod(bookFile, pos + lutOffset + lutSize); + const uint32_t pos = spineIn.position(); + readSpineEntryFrom(spineIn); + serialization::writePod(bookOut, pos + lutOffset + lutSize); } + // Total size of the spine tmp file: entries land in book.bin after the toc LUT + // and the full spine block, so toc LUT positions are offset by it. + const auto spineBytes = static_cast(spineIn.position()); // Loop through toc entries, writing LUT positions - tocFile.seek(0); + tocIn.seek(0); for (int i = 0; i < tocCount; i++) { - uint32_t pos = tocFile.position(); - auto tocEntry = readTocEntry(tocFile); - serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast(spineFile.position())); + const uint32_t pos = tocIn.position(); + readTocEntryFrom(tocIn); + serialization::writePod(bookOut, pos + lutOffset + lutSize + spineBytes); } // LUTs complete @@ -178,6 +247,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta if (!metadataArena.init(METADATA_ARENA_SLAB_BYTES)) { LOG_ERR("BMC", "Failed to allocate metadata scratch arena (%u bytes)", static_cast(METADATA_ARENA_SLAB_BYTES)); + lowMemoryFailure = true; closeBuildFiles(); return false; } @@ -186,15 +256,16 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta ArenaVector spineToTocIndex(metadataArena); if (!spineToTocIndex.resize(spineCount)) { LOG_ERR("BMC", "Failed to allocate spine-to-TOC index for %u spine items", spineCount); + lowMemoryFailure = true; closeBuildFiles(); return false; } for (size_t i = 0; i < spineToTocIndex.size(); ++i) { spineToTocIndex[i] = -1; } - tocFile.seek(0); + tocIn.seek(0); for (int j = 0; j < tocCount; j++) { - auto tocEntry = readTocEntry(tocFile); + auto tocEntry = readTocEntryFrom(tocIn); if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) { if (spineToTocIndex[tocEntry.spineIndex] == -1) { spineToTocIndex[tocEntry.spineIndex] = static_cast(j); @@ -222,19 +293,18 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta bool useBatchSizes = false; if (spineCount >= LARGE_SPINE_THRESHOLD) { - LOG_DBG("BMC", "Using batch size lookup for %d spine items", spineCount); - ArenaVector targets(metadataArena); if (!targets.resize(spineCount) || !spineSizes.resize(spineCount)) { LOG_ERR("BMC", "Failed to allocate batch size lookup scratch for %u spine items", spineCount); + lowMemoryFailure = true; zip.close(); closeBuildFiles(); return false; } - spineFile.seek(0); + spineIn.seek(0); for (int i = 0; i < spineCount; i++) { - auto entry = readSpineEntry(spineFile); + auto entry = readSpineEntryFrom(spineIn); std::string path = FsHelpers::normalisePath(entry.href); ZipFile::SizeTarget t; @@ -249,17 +319,15 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta }); int matched = zip.fillUncompressedSizes(targets.data(), targets.size(), spineSizes.data(), spineSizes.size()); - LOG_DBG("BMC", "Batch lookup matched %d/%d spine items (arena=%u bytes)", matched, spineCount, - static_cast(metadataArena.used())); useBatchSizes = true; } uint32_t cumSize = 0; - spineFile.seek(0); + spineIn.seek(0); int lastSpineTocIndex = -1; for (int i = 0; i < spineCount; i++) { - auto spineEntry = readSpineEntry(spineFile); + auto spineEntry = readSpineEntryFrom(spineIn); spineEntry.tocIndex = spineToTocIndex[i]; @@ -300,22 +368,31 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta spineEntry.cumulativeSize = cumSize; // Write out spine data to book.bin - writeSpineEntry(bookFile, spineEntry); + writeSpineEntryTo(bookOut, spineEntry); } // Close opened zip file zip.close(); // Loop through toc entries from toc file writing to book.bin - tocFile.seek(0); + tocIn.seek(0); for (int i = 0; i < tocCount; i++) { - auto tocEntry = readTocEntry(tocFile); - writeTocEntry(bookFile, tocEntry); + auto tocEntry = readTocEntryFrom(tocIn); + writeTocEntryTo(bookOut, tocEntry); } + const bool written = bookOut.flush(); + // Explicit close() required: member variables persist beyond function scope closeBuildFiles(); - LOG_DBG("BMC", "Successfully built book.bin"); + if (!written) { + // A short write (card full/removed) would leave a truncated book.bin that + // still passes the version check on load; remove it so the next open rebuilds. + LOG_ERR("BMC", "Failed writing book.bin, removing truncated file"); + Storage.remove((cachePath + bookBinFile).c_str()); + return false; + } + return true; } @@ -332,21 +409,11 @@ bool BookMetadataCache::cleanupTmpFiles() const { } uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const { - const uint32_t pos = file.position(); - serialization::writeString(file, entry.href); - serialization::writePod(file, entry.cumulativeSize); - serialization::writePod(file, entry.tocIndex); - return pos; + return writeSpineEntryTo(file, entry); } uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const { - const uint32_t pos = file.position(); - serialization::writeString(file, entry.title); - serialization::writeString(file, entry.href); - serialization::writeString(file, entry.anchor); - serialization::writePod(file, entry.level); - serialization::writePod(file, entry.spineIndex); - return pos; + return writeTocEntryTo(file, entry); } // Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called @@ -358,7 +425,11 @@ void BookMetadataCache::createSpineEntry(const std::string& href) { } const SpineEntry entry(href, 0, -1); - writeSpineEntry(spineFile, entry); + if (passOut) { + writeSpineEntryTo(*passOut, entry); + } else { + writeSpineEntry(spineFile, entry); + } spineCount++; } @@ -406,7 +477,11 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri // Compose the title to NFC at index time so the cache stores precomposed glyphs; // device fonts have no combining-mark positioning, so NFD titles render broken. const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex); - writeTocEntry(tocFile, entry); + if (passOut) { + writeTocEntryTo(*passOut, entry); + } else { + writeTocEntry(tocFile, entry); + } tocCount++; } @@ -464,7 +539,6 @@ bool BookMetadataCache::load() { } loaded = true; - LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount); return true; } @@ -533,19 +607,7 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) { } BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const { - SpineEntry entry; - serialization::readString(file, entry.href); - serialization::readPod(file, entry.cumulativeSize); - serialization::readPod(file, entry.tocIndex); - return entry; + return readSpineEntryFrom(file); } -BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const { - TocEntry entry; - serialization::readString(file, entry.title); - serialization::readString(file, entry.href); - serialization::readString(file, entry.anchor); - serialization::readPod(file, entry.level); - serialization::readPod(file, entry.spineIndex); - return entry; -} +BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const { return readTocEntryFrom(file); } diff --git a/lib/Epub/Epub/BookMetadataCache.h b/lib/Epub/Epub/BookMetadataCache.h index e65c510e05..48a351f9f5 100644 --- a/lib/Epub/Epub/BookMetadataCache.h +++ b/lib/Epub/Epub/BookMetadataCache.h @@ -2,9 +2,11 @@ #include #include +#include #include #include +#include #include class BookMetadataCache { @@ -55,6 +57,11 @@ class BookMetadataCache { // Temp file handles during build HalFile spineFile; HalFile tocFile; + // Buffers the per-entry tmp-file writes during the OPF/TOC passes: those + // writes interleave with zip-inflate SD reads, and unbuffered they thrash + // SdFat's shared sector cache (one 512B transaction per 4-byte pod). One + // wrapper serves whichever pass is active (spine, then toc). + std::unique_ptr passOut; // Index for fast href→spineIndex lookup (used only for large EPUBs) struct SpineHrefIndexEntry { @@ -65,6 +72,7 @@ class BookMetadataCache { Arena spineHrefIndexArena; ArenaVector spineHrefIndex; bool useSpineHrefIndex = false; + bool lowMemoryFailure = false; static constexpr uint16_t LARGE_SPINE_THRESHOLD = 300; @@ -116,6 +124,7 @@ class BookMetadataCache { // Reading phase (read mode) bool load(); + bool failedForLowMemory() const { return lowMemoryFailure; } SpineEntry getSpineEntry(int index); size_t getSpineCumulativeSize(int index); TocEntry getTocEntry(int index); diff --git a/lib/Epub/Epub/FootnoteEntry.h b/lib/Epub/Epub/FootnoteEntry.h index 6034a6a5c3..65b2af8d54 100644 --- a/lib/Epub/Epub/FootnoteEntry.h +++ b/lib/Epub/Epub/FootnoteEntry.h @@ -1,13 +1,18 @@ #pragma once +#include #include #define FOOTNOTE_NUMBER_LEN 32 #define FOOTNOTE_HREF_LEN 96 +inline constexpr uint16_t EPUB_MAX_FOOTNOTES_PER_PAGE = 16; struct FootnoteEntry { char number[FOOTNOTE_NUMBER_LEN]; char href[FOOTNOTE_HREF_LEN]; + // Matches this destination to the laid-out words that display the link. + // Zero is reserved for older/non-interactive entries. + uint8_t linkId = 0; FootnoteEntry() { number[0] = '\0'; diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 365d4bda93..4e6f8ddf9a 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -15,7 +15,7 @@ static_assert(TableFragmentRow::MAX_SERIALIZED_CELLS == MAX_TABLE_CELLS_PER_ROW) static_assert(PageTableFragment::MAX_SERIALIZED_ROWS == MAX_TABLE_ROWS_PER_FRAGMENT); template -void renderFilteredPageElements(const std::vector>& elements, GfxRenderer& renderer, +void renderFilteredPageElements(const std::vector>& elements, GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, const bool foregroundBlack, Predicate&& predicate) { for (const auto& element : elements) { @@ -66,9 +66,14 @@ std::unique_ptr PageLine::deserialize(FsFile& file) { void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, const bool foregroundBlack) { - (void)foregroundBlack; - // Images don't use fontId or text rendering - imageBlock->render(renderer, xPos + xOffset, yPos + yOffset); + (void)fontId; + // Images don't use fontId for text rendering + imageBlock->render(renderer, xPos + xOffset, yPos + yOffset, foregroundBlack); +} + +void PageImage::renderPlaceholder(GfxRenderer& renderer, const int xOffset, const int yOffset, + const bool foregroundBlack) const { + imageBlock->renderPlaceholder(renderer, xPos + xOffset, yPos + yOffset, foregroundBlack); } bool PageImage::serialize(FsFile& file) { @@ -349,8 +354,8 @@ std::unique_ptr PageTableFragment::deserialize(FsFile& file) void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, const bool foregroundBlack) const { - renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset, foregroundBlack, - [](const PageElement&) { return true; }); + renderText(renderer, fontId, xOffset, yOffset, foregroundBlack); + renderImages(renderer, fontId, xOffset, yOffset, foregroundBlack); } void Page::renderText(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, @@ -359,11 +364,28 @@ void Page::renderText(GfxRenderer& renderer, const int fontId, const int xOffset [](const PageElement& element) { return element.getTag() != TAG_PageImage; }); } -void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { - renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset, true, +void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, + const bool foregroundBlack) const { + renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset, foregroundBlack, [](const PageElement& element) { return element.getTag() == TAG_PageImage; }); } +void Page::renderWithImagePlaceholders(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, + const bool foregroundBlack) const { + renderText(renderer, fontId, xOffset, yOffset, foregroundBlack); + for (const auto& element : elements) { + if (element->getTag() != TAG_PageImage) { + continue; + } + auto& pageImage = static_cast(*element); + if (pageImage.getImageBlock().needsDecode()) { + pageImage.renderPlaceholder(renderer, xOffset, yOffset, foregroundBlack); + } else { + pageImage.render(renderer, fontId, xOffset, yOffset, foregroundBlack); + } + } +} + bool Page::serialize(FsFile& file) const { const uint16_t count = elements.size(); if (elements.size() > MAX_PAGE_ELEMENTS) { @@ -396,7 +418,7 @@ bool Page::serialize(FsFile& file) const { for (uint16_t i = 0; i < fnCount; i++) { const auto& fn = footnotes[i]; if (file.write(fn.number, sizeof(fn.number)) != sizeof(fn.number) || - file.write(fn.href, sizeof(fn.href)) != sizeof(fn.href)) { + file.write(fn.href, sizeof(fn.href)) != sizeof(fn.href) || !serialization::tryWritePod(file, fn.linkId)) { LOG_ERR("PGE", "Failed to write footnote"); return false; } @@ -489,7 +511,8 @@ std::unique_ptr Page::deserialize(FsFile& file) { for (uint16_t i = 0; i < fnCount; i++) { auto& entry = page->footnotes[i]; if (file.read(entry.number, sizeof(entry.number)) != sizeof(entry.number) || - file.read(entry.href, sizeof(entry.href)) != sizeof(entry.href)) { + file.read(entry.href, sizeof(entry.href)) != sizeof(entry.href) || + !serialization::tryReadPod(file, entry.linkId)) { LOG_ERR("PGE", "Failed to read footnote %u", i); return nullptr; } diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index ee6b53a4b0..be5d4f38a0 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -46,12 +47,13 @@ class PageLine final : public PageElement { // New PageImage class class PageImage final : public PageElement { - std::shared_ptr imageBlock; + std::unique_ptr imageBlock; public: - PageImage(std::shared_ptr block, const int16_t xPos, const int16_t yPos) + PageImage(std::unique_ptr 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, bool foregroundBlack = true) override; + void renderPlaceholder(GfxRenderer& renderer, int xOffset, int yOffset, bool foregroundBlack) const; bool serialize(FsFile& file) override; PageElementTag getTag() const override { return TAG_PageImage; } static std::unique_ptr deserialize(FsFile& file); @@ -127,15 +129,21 @@ class Page { }; // the list of block index and line numbers on this page - std::vector> elements; + // Page elements have one owner: their page. Text blocks remain shared by + // PageLine entries when a laid-out block spans multiple lines or pages. + std::vector> elements; std::vector footnotes; std::vector publisherPageMarkers; - static constexpr uint16_t MAX_FOOTNOTES_PER_PAGE = 16; + static constexpr uint16_t MAX_FOOTNOTES_PER_PAGE = EPUB_MAX_FOOTNOTES_PER_PAGE; static constexpr uint8_t INITIAL_FOOTNOTE_RESERVE = 2; static constexpr uint8_t MAX_PUBLISHER_PAGE_MARKERS_PER_PAGE = 8; static constexpr uint8_t INITIAL_PUBLISHER_PAGE_MARKER_RESERVE = 2; - void addFootnote(const char* number, const char* href) { + void addFootnote(const char* number, const char* href, const uint8_t linkId = 0) { + if (linkId != 0 && std::any_of(footnotes.begin(), footnotes.end(), + [linkId](const FootnoteEntry& entry) { return entry.linkId == linkId; })) { + return; + } if (footnotes.size() >= MAX_FOOTNOTES_PER_PAGE) return; // Cap per-page footnotes if (footnotes.empty()) { footnotes.reserve(INITIAL_FOOTNOTE_RESERVE); @@ -145,6 +153,7 @@ class Page { entry.number[sizeof(entry.number) - 1] = '\0'; std::strncpy(entry.href, href, sizeof(entry.href) - 1); entry.href[sizeof(entry.href) - 1] = '\0'; + entry.linkId = linkId; footnotes.push_back(entry); } @@ -162,14 +171,23 @@ class Page { void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool foregroundBlack = true) const; void renderText(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool foregroundBlack = true) const; - void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; + void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool foregroundBlack = true) const; + void renderWithImagePlaceholders(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, + bool foregroundBlack = true) const; bool serialize(FsFile& file) const; static std::unique_ptr deserialize(FsFile& file); // Check if page contains any images (used to force full refresh) bool hasImages() const { return std::any_of(elements.begin(), elements.end(), - [](const std::shared_ptr& el) { return el->getTag() == TAG_PageImage; }); + [](const std::unique_ptr& el) { return el->getTag() == TAG_PageImage; }); + } + + bool hasImagesNeedingDecode() const { + return std::any_of(elements.begin(), elements.end(), [](const std::unique_ptr& element) { + return element->getTag() == TAG_PageImage && + static_cast(*element).getImageBlock().needsDecode(); + }); } // Get bounding box of all images on the page (union of image rects) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index 5dc77a6a92..9dd689e552 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -145,6 +145,17 @@ bool containsCjkBreakableCodepoint(const std::string& text) { return false; } +uint32_t countCodepoints(const std::string_view text) { + const auto* ptr = reinterpret_cast(text.data()); + const auto* const end = ptr + text.size(); + uint32_t count = 0; + while (ptr < end) { + utf8NextCodepoint(&ptr); + count++; + } + return count; +} + bool hasCjkBreakOpportunityBetween(const uint32_t leftCp, const uint32_t rightCp) { if (!utf8IsCjkBreakable(leftCp) && !utf8IsCjkBreakable(rightCp)) return false; if (isNoBreakAfterCjkPunctuation(leftCp) || isNoBreakBeforeCjkPunctuation(rightCp)) return false; @@ -308,8 +319,8 @@ BionicTokenMetadata computeBionicMetadata(const std::string_view segment, const return {baseStyle, static_cast(std::min(splitByteOffset, UINT8_MAX))}; } -int measureBionicSuffixX(const GfxRenderer& renderer, const int fontId, const std::string& word, - const EpdFontFamily::Style style, const uint8_t boundary) { +int measureBionicRunOffset(const GfxRenderer& renderer, const int fontId, const std::string& word, + const EpdFontFamily::Style style, const uint8_t boundary, const bool rtl) { if (boundary == 0 || boundary >= word.size()) { return 0; } @@ -320,10 +331,17 @@ int measureBionicSuffixX(const GfxRenderer& renderer, const int fontId, const st memcpy(prefixBuf, word.c_str(), prefixLen); prefixBuf[prefixLen] = '\0'; - const int prefixWidth = renderer.getTextAdvanceX(fontId, prefixBuf, boldStyle); - const int kern = renderer.getKerning(fontId, lastCodepointBeforeByteOffset(word, boundary), - firstCodepointAtByteOffset(word, boundary), boldStyle); - return prefixWidth + kern; + const uint32_t firstSuffixCp = firstCodepointAtByteOffset(word, boundary); + if (!rtl) { + // The suffix starts in a separate drawText() call. Keep the final bold + // glyph's advance and cross-run kerning in one fixed-point rounding step, + // matching drawText() and preventing one-pixel collisions at the split. + return renderer.getTextAdvanceX(fontId, prefixBuf, boldStyle, firstSuffixCp); + } + + const int suffixWidth = renderer.getTextAdvanceX(fontId, word.c_str() + boundary, style); + const int kern = renderer.getKerning(fontId, lastCodepointBeforeByteOffset(word, boundary), firstSuffixCp, boldStyle); + return suffixWidth + kern; } uint16_t measureTokenWidth(const GfxRenderer& renderer, const int fontId, const std::string& word, @@ -333,7 +351,7 @@ uint16_t measureTokenWidth(const GfxRenderer& renderer, const int fontId, const return measureWordWidth(renderer, fontId, word, style, appendHyphen); } - const int suffixX = measureBionicSuffixX(renderer, fontId, word, style, bionicBoundary); + const int suffixX = measureBionicRunOffset(renderer, fontId, word, style, bionicBoundary, false); const int suffixWidth = renderer.getTextAdvanceX(fontId, word.c_str() + bionicBoundary, style); return static_cast(std::max(0, suffixX + suffixWidth)); } @@ -349,11 +367,31 @@ size_t guideDotGapSlots(const std::string& rightWord) { return 1 + (isClosingPunctuationForJustify(firstCodepoint(rightWord)) ? 0 : 1); } +int wordSpacingExtraFromGap(const int gap, const uint8_t wordSpacing) { + if (gap <= 0) { + return 0; + } + // Add a fixed pixel amount per level instead of scaling from the font's + // natural space width, so narrow-space fonts still visibly widen. + constexpr int WORD_SPACING_LEVEL_PX = 10; + const int level = std::min(wordSpacing, 4); + return level * WORD_SPACING_LEVEL_PX; +} + +int guideDotWordSpacingExtra(const GfxRenderer& renderer, const int fontId, const std::string& leftWord, + const std::string& rightWord, const EpdFontFamily::Style leftStyle, + const uint8_t wordSpacing) { + const int naturalWordGap = + renderer.getSpaceAdvance(fontId, lastCodepoint(leftWord), firstCodepoint(rightWord), leftStyle); + return wordSpacingExtraFromGap(naturalWordGap, wordSpacing); +} + int naturalGapBeforeToken(const GfxRenderer& renderer, const int fontId, const std::string& leftWord, const std::string& rightWord, const EpdFontFamily::Style leftStyle, const bool continues, - const bool noSpaceBefore, const bool guideDotBefore) { + const bool noSpaceBefore, const bool guideDotBefore, const uint8_t wordSpacing) { if (guideDotBefore) { - return guideDotNaturalGap(renderer, fontId, leftWord, rightWord, leftStyle); + const int extraGap = guideDotWordSpacingExtra(renderer, fontId, leftWord, rightWord, leftStyle, wordSpacing); + return guideDotNaturalGap(renderer, fontId, leftWord, rightWord, leftStyle) + extraGap; } if (noSpaceBefore) { return 0; @@ -361,7 +399,9 @@ int naturalGapBeforeToken(const GfxRenderer& renderer, const int fontId, const s if (continues) { return renderer.getKerning(fontId, lastCodepoint(leftWord), firstCodepoint(rightWord), leftStyle); } - return renderer.getSpaceAdvance(fontId, lastCodepoint(leftWord), firstCodepoint(rightWord), leftStyle); + const int naturalGap = + renderer.getSpaceAdvance(fontId, lastCodepoint(leftWord), firstCodepoint(rightWord), leftStyle); + return naturalGap + wordSpacingExtraFromGap(naturalGap, wordSpacing); } size_t gapSlotsBeforeToken(const std::string& rightWord, const bool continues, const bool noSpaceBefore, @@ -413,33 +453,105 @@ bool isWordCharacter(uint32_t cp) { return true; } +bool isCjkIdeograph(const uint32_t cp) { + return (cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3400 && cp <= 0x4DBF) || (cp >= 0xF900 && cp <= 0xFAFF) || + (cp >= 0x20000 && cp <= 0x3FFFF); +} + } // namespace void ParsedText::reserveTokenCapacity(const size_t additionalTokens) { const size_t requiredSize = words.size() + additionalTokens; - if (words.capacity() >= requiredSize && wordStyles.capacity() >= requiredSize && - wordContinues.capacity() >= requiredSize && wordNoSpaceBefore.capacity() >= requiredSize && - wordBionicBoundary.capacity() >= requiredSize && wordGuideDotBefore.capacity() >= requiredSize && - wordBackgroundBlack.capacity() >= requiredSize) { + if (wordStyles.capacity() >= requiredSize && wordContinues.capacity() >= requiredSize && + wordNoSpaceBefore.capacity() >= requiredSize && wordBionicBoundary.capacity() >= requiredSize && + wordGuideDotBefore.capacity() >= requiredSize && wordBackgroundBlack.capacity() >= requiredSize && + wordVisibleOffsetDeltas.capacity() >= requiredSize) { return; } - size_t newCapacity = words.capacity() == 0 ? INITIAL_TOKEN_VECTOR_RESERVE : words.capacity() * 2; + size_t newCapacity = wordStyles.capacity() == 0 ? INITIAL_TOKEN_VECTOR_RESERVE : wordStyles.capacity() * 2; if (newCapacity < requiredSize) { newCapacity = requiredSize; } - words.reserve(newCapacity); + // words and rubyTexts are deques: their chunked growth avoids the large + // contiguous reallocations this method is intended to prevent. wordStyles.reserve(newCapacity); wordContinues.reserve(newCapacity); wordNoSpaceBefore.reserve(newCapacity); wordBionicBoundary.reserve(newCapacity); wordGuideDotBefore.reserve(newCapacity); wordBackgroundBlack.reserve(newCapacity); + wordVisibleOffsetDeltas.reserve(newCapacity); +} + +uint32_t ParsedText::visibleOffsetBaseAt(const size_t wordIndex) const { + uint32_t base = visibleOffsetBase; + for (const auto& rebase : visibleOffsetRebases) { + if (rebase.wordIndex > wordIndex) break; + base = rebase.base; + } + return base; +} + +uint32_t ParsedText::visibleOffsetAt(const size_t wordIndex) const { + if (wordIndex >= wordVisibleOffsetDeltas.size()) return 0; + return visibleOffsetBaseAt(wordIndex) + wordVisibleOffsetDeltas[wordIndex]; +} + +void ParsedText::pushVisibleOffset(const uint32_t offset) { + uint32_t base = visibleOffsetBase; + if (wordVisibleOffsetDeltas.empty()) { + visibleOffsetBase = offset; + base = offset; + } else if (!visibleOffsetRebases.empty()) { + base = visibleOffsetRebases.back().base; + } + if (offset < base || offset - base > std::numeric_limits::max()) { + visibleOffsetRebases.push_back({wordVisibleOffsetDeltas.size(), offset}); + base = offset; + } + wordVisibleOffsetDeltas.push_back(static_cast(offset - base)); +} + +void ParsedText::insertVisibleOffset(const size_t wordIndex, const uint32_t offset) { + const uint32_t base = wordIndex > 0 ? visibleOffsetBaseAt(wordIndex - 1) : visibleOffsetBase; + for (auto& rebase : visibleOffsetRebases) { + if (rebase.wordIndex >= wordIndex) rebase.wordIndex++; + } + uint32_t insertionBase = base; + if (offset < base || offset - base > std::numeric_limits::max()) { + const auto it = std::find_if(visibleOffsetRebases.begin(), visibleOffsetRebases.end(), + [wordIndex](const auto& rebase) { return rebase.wordIndex > wordIndex; }); + visibleOffsetRebases.insert(it, {wordIndex, offset}); + insertionBase = offset; + } + wordVisibleOffsetDeltas.insert(wordVisibleOffsetDeltas.begin() + wordIndex, + static_cast(offset - insertionBase)); +} + +void ParsedText::eraseVisibleOffsetPrefix(const size_t count) { + if (count >= wordVisibleOffsetDeltas.size()) { + wordVisibleOffsetDeltas.clear(); + visibleOffsetRebases.clear(); + visibleOffsetBase = 0; + return; + } + const uint32_t newBase = visibleOffsetBaseAt(count); + wordVisibleOffsetDeltas.erase(wordVisibleOffsetDeltas.begin(), wordVisibleOffsetDeltas.begin() + count); + size_t write = 0; + for (auto rebase : visibleOffsetRebases) { + if (rebase.wordIndex <= count) continue; + rebase.wordIndex -= count; + visibleOffsetRebases[write++] = rebase; + } + visibleOffsetRebases.resize(write); + visibleOffsetBase = newBase; } void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline, - const bool attachToPrevious, const bool backgroundBlack) { + const bool attachToPrevious, const bool backgroundBlack, const uint8_t linkId, + const uint32_t visibleTextOffset) { if (word.empty()) return; // The device fonts carry no combining-mark positioning, so EPUB text stored in NFD @@ -459,7 +571,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, bool guideDotBeforeNextToken = false; const auto pushToken = [&](std::string token, const bool continues, const bool noSpaceBefore, - const EpdFontFamily::Style tokenStyle, const uint8_t bionicBoundary) { + const EpdFontFamily::Style tokenStyle, const uint8_t bionicBoundary, + const uint32_t tokenVisibleOffset) { reserveTokenCapacity(1); words.push_back(std::move(token)); wordStyles.push_back(tokenStyle); @@ -468,11 +581,22 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, wordBionicBoundary.push_back(bionicBoundary); wordGuideDotBefore.push_back(guideDotBeforeNextToken); guideDotBeforeNextToken = false; - wordBackgroundBlack.push_back(backgroundBlack ? TextBlock::WORD_FLAG_BACKGROUND_BLACK : 0); + const uint8_t linkFlags = + static_cast((linkId << TextBlock::WORD_FLAG_LINK_ID_SHIFT) & TextBlock::WORD_FLAG_LINK_ID_MASK); + wordBackgroundBlack.push_back( + static_cast((backgroundBlack ? TextBlock::WORD_FLAG_BACKGROUND_BLACK : 0) | linkFlags)); + pushVisibleOffset(tokenVisibleOffset); + if (!rubyTexts.empty()) { + rubyTexts.push_back(""); + } }; bool effectiveAttachToPrevious = attachToPrevious; bool effectiveNoSpaceBefore = false; + // Only a glued token (attachToPrevious == true, i.e. no whitespace separated it from the + // previous one in the source) may be turned into a gap-less break opportunity. When real + // whitespace separated the two words, that space is content and must be rendered: Korean + // is a space-delimited script written in Hangul, which utf8IsCjkBreakable() covers. if (attachToPrevious && !words.empty() && hasCjkBreakOpportunityBetween(lastCodepoint(words.back()), firstCodepoint(word))) { effectiveAttachToPrevious = false; @@ -485,18 +609,24 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, } if (auto breakOffsets = cjkCharacterBreakByteOffsets(word); !breakOffsets.empty()) { + // CJK-heavy paragraphs can push hundreds of tiny tokens quickly when CSS toggles + // inline styles. Reserve once up front to avoid repeated vector growth reallocations. + reserveTokenCapacity(breakOffsets.size() + 1); bool firstToken = true; size_t tokenStart = 0; + uint32_t tokenVisibleOffset = visibleTextOffset; for (const size_t breakOffset : breakOffsets) { if (breakOffset <= tokenStart || breakOffset > word.size()) continue; - pushToken(word.substr(tokenStart, breakOffset - tokenStart), firstToken ? effectiveAttachToPrevious : false, - firstToken ? effectiveNoSpaceBefore : true, baseStyle, 0); + const std::string_view token(word.data() + tokenStart, breakOffset - tokenStart); + pushToken(std::string(token), firstToken ? effectiveAttachToPrevious : false, + firstToken ? effectiveNoSpaceBefore : true, baseStyle, 0, tokenVisibleOffset); + tokenVisibleOffset += countCodepoints(token); firstToken = false; tokenStart = breakOffset; } if (tokenStart < word.size()) { pushToken(word.substr(tokenStart), firstToken ? effectiveAttachToPrevious : false, - firstToken ? effectiveNoSpaceBefore : true, baseStyle, 0); + firstToken ? effectiveNoSpaceBefore : true, baseStyle, 0, tokenVisibleOffset); } if (wordStartsRtl) { hasRtlWord = true; @@ -505,7 +635,7 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, } if (containsCjkBreakableCodepoint(word)) { - pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, baseStyle, 0); + pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, baseStyle, 0, visibleTextOffset); if (wordStartsRtl) { hasRtlWord = true; } @@ -514,7 +644,7 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, // Already-bold text should stay fully bold; bionic splitting would make its suffix regular later. if (!this->bionicReadingEnabled || (baseStyle & EpdFontFamily::BOLD) != 0) { - pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, baseStyle, 0); + pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, baseStyle, 0, visibleTextOffset); if (wordStartsRtl) { hasRtlWord = true; } @@ -523,18 +653,25 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, // --- FOCUS READING LOGIC BELOW --- - // Pre-reserve capacity to prevent mid-word heap reallocations. + // Worst case: a segment boundary on each byte (highly punctuated UTF-8 text). reserveTokenCapacity(word.length()); // Lambda helper to process and push individual sub-segments of the string // Use std::string_view to avoid heap allocations when slicing auto processSegment = [&](std::string_view segment, bool isWord, bool attach, bool noSpaceBefore) { + const auto* begin = reinterpret_cast(word.data()); + const auto* segmentBegin = reinterpret_cast(segment.data()); + uint32_t segmentOffset = visibleTextOffset; + while (begin < segmentBegin) { + utf8NextCodepoint(&begin); + segmentOffset++; + } if (!isWord) { // Punctuation and Numbers stay regular - pushToken(std::string(segment), attach, noSpaceBefore, baseStyle, 0); + pushToken(std::string(segment), attach, noSpaceBefore, baseStyle, 0, segmentOffset); } else { const BionicTokenMetadata bionic = computeBionicMetadata(segment, baseStyle, bionicReadingEnabled); - pushToken(std::string(segment), attach, noSpaceBefore, bionic.style, bionic.boundary); + pushToken(std::string(segment), attach, noSpaceBefore, bionic.style, bionic.boundary, segmentOffset); } }; @@ -580,11 +717,43 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, } } +void ParsedText::setRubyForWordAt(size_t index, const std::string& ruby) { + if (index >= words.size()) return; + if (rubyTexts.size() <= index) { + rubyTexts.resize(words.size()); + } + rubyTexts[index] = ruby; +} + +void ParsedText::setRubyGroupAt(size_t startIndex, size_t count, const std::string& ruby) { + if (startIndex >= words.size()) return; + if (rubyTexts.size() <= startIndex) { + rubyTexts.resize(words.size()); + } + rubyTexts[startIndex] = ruby; + for (size_t i = 1; i < count; i++) { + size_t idx = startIndex + i; + if (idx >= words.size()) break; + if (rubyTexts.size() <= idx) { + rubyTexts.resize(words.size()); + } + rubyTexts[idx] = ""; + wordStyles[idx] = + static_cast(static_cast(wordStyles[idx]) | EpdFontFamily::RUBY_CONTINUE); + wordContinues[idx] = true; // Prevent page breaker from splitting the Group Ruby! + } +} + +void ParsedText::ensureRubyCapacity() { + // No-op: rubyTexts is a std::deque (chunked growth, no capacity to pre-reserve + // and no large contiguous reallocation to avoid). Kept for call-site stability. +} + int ParsedText::resolveFirstLineIndent(const bool isFirstLine, const GfxRenderer& renderer, const int fontId) const { const bool naturalAlign = blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::None || (blockStyle.isRtl ? blockStyle.alignment == CssTextAlign::Right : blockStyle.alignment == CssTextAlign::Left); - if (!isFirstLine || !naturalAlign) { + if (!isFirstLine || isContinuation_ || !naturalAlign) { return 0; } if (blockStyle.textIndentDefined) { @@ -601,7 +770,7 @@ int ParsedText::resolveFirstLineIndent(const bool isFirstLine, const GfxRenderer // Consumes data to minimize memory usage bool ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth, - const std::function)>& processLine, + const std::function, uint32_t)>& processLine, const bool includeLastLine) { if (words.empty()) { return true; @@ -644,6 +813,9 @@ bool ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo } if (styleMask == 0) styleMask = 0x01; // defensive: regular only renderer.ensureSdCardFontReady(fontId, words, hyphenationEnabled, styleMask); + if (!rubyTexts.empty()) { + renderer.ensureSdCardFontReady(fontId, rubyTexts, false, 0x01); + } if (guideReadingEnabled) { renderer.ensureSdCardFontReady(fontId, GUIDE_DOT_UTF8, 0x01); } @@ -693,6 +865,16 @@ bool ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo wordBionicBoundary.erase(wordBionicBoundary.begin(), wordBionicBoundary.begin() + consumed); wordGuideDotBefore.erase(wordGuideDotBefore.begin(), wordGuideDotBefore.begin() + consumed); wordBackgroundBlack.erase(wordBackgroundBlack.begin(), wordBackgroundBlack.begin() + consumed); + eraseVisibleOffsetPrefix(consumed); + if (!rubyTexts.empty()) { + const size_t rtConsumed = std::min(consumed, rubyTexts.size()); + rubyTexts.erase(rubyTexts.begin(), rubyTexts.begin() + rtConsumed); + } + } + if (lineCount > 0) { + // A partial flush leaves the remaining words in this same logical + // paragraph. Mark them so the next pass starts at the normal left edge. + isContinuation_ = !includeLastLine; } return true; } @@ -701,7 +883,52 @@ bool ParsedText::layoutAndExtractLinesPreservingSource( const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth, const std::function)>& processLine) const { ParsedText layoutProbe(*this); - return layoutProbe.layoutAndExtractLines(renderer, fontId, viewportWidth, processLine); + return layoutProbe.layoutAndExtractLines( + renderer, fontId, viewportWidth, + [&processLine](std::shared_ptr line, const uint32_t) { processLine(std::move(line)); }); +} + +int ParsedText::calculateRubyExtraStartOffset(const size_t wordIdx, const size_t maxWordIdx, + const GfxRenderer& renderer, const int fontId) const { + if (rubyTexts.empty() || wordIdx >= rubyTexts.size() || rubyTexts[wordIdx].empty() || + (wordStyles[wordIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) { + return 0; + } + + size_t groupWordCount = 1; + while (wordIdx + groupWordCount < maxWordIdx && + (wordStyles[wordIdx + groupWordCount] & EpdFontFamily::RUBY_CONTINUE) != 0) { + groupWordCount++; + } + + int groupActualWidth = 0; + for (size_t k = 0; k < groupWordCount; ++k) { + const size_t index = wordIdx + k; + groupActualWidth += measureTokenWidth(renderer, fontId, words[index], wordStyles[index], wordBionicBoundary[index]); + } + const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[wordIdx].c_str(), EpdFontFamily::SUP); + return rubyWidth > groupActualWidth ? (rubyWidth - groupActualWidth) / 2 : 0; +} + +int ParsedText::calculateRubyExtraEndOffset(const size_t lineStartIdx, const size_t lineBreakIdx, + const GfxRenderer& renderer, const int fontId) const { + if (rubyTexts.empty() || lineBreakIdx == 0 || lineStartIdx >= lineBreakIdx) return 0; + + size_t leaderIdx = lineBreakIdx - 1; + while (leaderIdx > lineStartIdx && (wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) { + leaderIdx--; + } + if (leaderIdx >= rubyTexts.size() || rubyTexts[leaderIdx].empty() || + (wordStyles[leaderIdx] & EpdFontFamily::RUBY_CONTINUE) != 0) { + return 0; + } + + int groupActualWidth = 0; + for (size_t index = leaderIdx; index < lineBreakIdx; ++index) { + groupActualWidth += measureTokenWidth(renderer, fontId, words[index], wordStyles[index], wordBionicBoundary[index]); + } + const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[leaderIdx].c_str(), EpdFontFamily::SUP); + return rubyWidth > groupActualWidth ? (rubyWidth - groupActualWidth) / 2 : 0; } bool ParsedText::calculateWordWidths(ArenaVector& wordWidths, const GfxRenderer& renderer, const int fontId) { @@ -715,6 +942,100 @@ bool ParsedText::calculateWordWidths(ArenaVector& wordWidths, const Gf } } + // Adjust widths for ruby groups to comply with JLReq standards + if (!rubyTexts.empty()) { + struct RubyGroupInfo { + size_t start; + size_t count; + int baseWidth; + int rubyWidth; + int leftOverlap; + int rightOverlap; + }; + + std::vector groups; + for (size_t i = 0; i < words.size(); ++i) { + if (i < rubyTexts.size() && !rubyTexts[i].empty() && (wordStyles[i] & EpdFontFamily::RUBY_CONTINUE) == 0) { + RubyGroupInfo g; + g.start = i; + g.baseWidth = wordWidths[i]; + g.count = 1; + while (i + g.count < words.size() && (wordStyles[i + g.count] & EpdFontFamily::RUBY_CONTINUE) != 0) { + g.baseWidth += wordWidths[i + g.count]; + g.count++; + } + g.rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[i].c_str(), EpdFontFamily::SUP); + g.leftOverlap = std::max(0, (g.rubyWidth - g.baseWidth) / 2); + g.rightOverlap = std::max(0, (g.rubyWidth - g.baseWidth) / 2); + groups.push_back(g); + i += g.count - 1; + } + } + + // Adjust widths based on adjacent characters and group-to-group spacing + for (size_t gIdx = 0; gIdx < groups.size(); ++gIdx) { + const auto& g = groups[gIdx]; + + // 1. Preceding character (left overhang) + if (g.start > 0) { + const uint32_t cpPrev = lastCodepoint(words[g.start - 1]); + if (isCjkIdeograph(cpPrev)) { + wordWidths[g.start - 1] += g.leftOverlap; + } else { + const int maxLeftOverhang = wordWidths[g.start - 1] / 2; + wordWidths[g.start - 1] += std::max(0, g.leftOverlap - maxLeftOverhang); + } + } + + // 2. Succeeding character (right overhang / group collision) + const size_t nextIdx = g.start + g.count; + if (nextIdx < words.size()) { + if (gIdx + 1 < groups.size() && groups[gIdx + 1].start == nextIdx) { + // Adjacent ruby groups: compute collision + const auto& nextG = groups[gIdx + 1]; + const int collision = g.rightOverlap + nextG.leftOverlap; + if (collision > 0) { + wordWidths[g.start + g.count - 1] += collision; + } + } else { + // Regular character following: check if it's Kanji + const uint32_t cpNext = firstCodepoint(words[nextIdx]); + if (isCjkIdeograph(cpNext)) { + wordWidths[g.start + g.count - 1] += g.rightOverlap; + } else { + const int maxRightOverhang = wordWidths[nextIdx] / 2; + wordWidths[g.start + g.count - 1] += std::max(0, g.rightOverlap - maxRightOverhang); + } + + // Check if there is another ruby group further ahead separated only by non-ideographs + if (gIdx + 1 < groups.size()) { + const auto& nextG = groups[gIdx + 1]; + bool onlyNonIdeographsInBetween = true; + int gapWidth = 0; + for (size_t k = nextIdx; k < nextG.start; ++k) { + const uint32_t cp = firstCodepoint(words[k]); + if (isCjkIdeograph(cp)) { + onlyNonIdeographsInBetween = false; + break; + } + gapWidth += wordWidths[k]; + } + if (onlyNonIdeographsInBetween) { + const int maxRightOverhang = wordWidths[g.start + g.count - 1] / 2; + const int maxLeftOverhang = wordWidths[nextG.start - 1] / 2; + const int allowedRight = std::min(g.rightOverlap, maxRightOverhang); + const int allowedLeft = std::min(nextG.leftOverlap, maxLeftOverhang); + const int touchOverlap = allowedRight + allowedLeft - gapWidth; + if (touchOverlap > 0) { + wordWidths[g.start + g.count - 1] += touchOverlap; + } + } + } + } + } + } + } + return true; } @@ -740,8 +1061,9 @@ bool ParsedText::calculateGapMetrics(ArenaVector& naturalGaps, ArenaVec const bool continues = wordContinues[i]; const bool noSpaceBefore = wordNoSpaceBefore[i]; const bool guideDotBefore = wordGuideDotBefore[i]; - naturalGaps[i] = static_cast(naturalGapBeforeToken( - renderer, fontId, words[i - 1], words[i], wordStyles[i - 1], continues, noSpaceBefore, guideDotBefore)); + naturalGaps[i] = + static_cast(naturalGapBeforeToken(renderer, fontId, words[i - 1], words[i], wordStyles[i - 1], + continues, noSpaceBefore, guideDotBefore, wordSpacing)); gapSlots[i] = static_cast( std::min(UINT8_MAX, gapSlotsBeforeToken(words[i], continues, noSpaceBefore, guideDotBefore))); } @@ -806,7 +1128,11 @@ bool ParsedText::computeLineBreaks(Arena& scratchArena, const GfxRenderer& rende if (j > static_cast(i)) { gap = naturalGaps[j]; } - currlen += wordWidths[j] + gap; + + const int extraStartOffset = + j == static_cast(i) ? calculateRubyExtraStartOffset(i, totalWordCount, renderer, fontId) : 0; + + currlen += wordWidths[j] + gap + (j == i ? extraStartOffset : 0); if (currlen > effectivePageWidth) { break; @@ -817,6 +1143,12 @@ bool ParsedText::computeLineBreaks(Arena& scratchArena, const GfxRenderer& rende continue; } + const int extraEndOffset = calculateRubyExtraEndOffset(i, j + 1, renderer, fontId); + + if (currlen + extraEndOffset > effectivePageWidth) { + continue; // Cannot split here as it would overflow the right margin + } + int cost; if (j == totalWordCount - 1) { cost = 0; // Last line @@ -832,7 +1164,8 @@ bool ParsedText::computeLineBreaks(Arena& scratchArena, const GfxRenderer& rende } } - if (cost < dp[i]) { + // Equal-cost alternatives should keep more CJK text on the current line. + if (cost <= dp[i]) { dp[i] = cost; ans[i] = j; // j is the index of the last word in this optimal line } @@ -897,7 +1230,7 @@ bool ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const if (!isFirstWord) { spacing = naturalGapBeforeToken(renderer, fontId, words[currentIndex - 1], words[currentIndex], wordStyles[currentIndex - 1], continuesVec[currentIndex], - noSpaceBeforeVec[currentIndex], wordGuideDotBefore[currentIndex]); + noSpaceBeforeVec[currentIndex], wordGuideDotBefore[currentIndex], wordSpacing); } const int candidateWidth = spacing + wordWidths[currentIndex]; @@ -1002,6 +1335,13 @@ 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); + uint32_t remainderOffset = visibleOffsetAt(wordIndex); + const auto* offsetPtr = reinterpret_cast(word.data()); + const auto* const splitPtr = offsetPtr + chosenOffset; + while (offsetPtr < splitPtr) { + utf8NextCodepoint(&offsetPtr); + remainderOffset++; + } words[wordIndex].resize(chosenOffset); if (chosenNeedsHyphen) { words[wordIndex].push_back('-'); @@ -1014,12 +1354,16 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl // Insert the remainder word (with matching style and continuation flag) directly after the prefix. const BionicTokenMetadata remainderBionic = computeBionicMetadata(remainder, style, bionicReadingEnabled); words.insert(words.begin() + wordIndex + 1, remainder); + insertVisibleOffset(wordIndex + 1, remainderOffset); wordStyles.insert(wordStyles.begin() + wordIndex + 1, remainderBionic.style); wordBackgroundBlack.insert(wordBackgroundBlack.begin() + wordIndex + 1, wordBackgroundBlack[wordIndex]); // The hyphen remainder starts fresh on the next line and does not inherit a virtual guide dot. wordBionicBoundary.insert(wordBionicBoundary.begin() + wordIndex + 1, remainderBionic.boundary); wordGuideDotBefore.insert(wordGuideDotBefore.begin() + wordIndex + 1, false); - wordBackgroundBlack[wordIndex + 1] &= TextBlock::WORD_FLAG_BACKGROUND_BLACK; + wordBackgroundBlack[wordIndex + 1] &= static_cast(~TextBlock::WORD_FLAG_INSERTED_HYPHEN); + if (wordIndex + 1 <= rubyTexts.size()) { + rubyTexts.insert(rubyTexts.begin() + wordIndex + 1, ""); + } // Continuation flag handling after splitting a word into prefix + remainder. // @@ -1043,6 +1387,9 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl // wordContinues[wordIndex] is intentionally left unchanged — the prefix keeps its original attachment. wordContinues.insert(wordContinues.begin() + wordIndex + 1, false); wordNoSpaceBefore.insert(wordNoSpaceBefore.begin() + wordIndex + 1, false); + if (!rubyTexts.empty()) { + rubyTexts.insert(rubyTexts.begin() + wordIndex + 1, ""); + } // Update cached widths to reflect the new prefix/remainder pairing. wordWidths[wordIndex] = static_cast(chosenWidth); @@ -1094,17 +1441,25 @@ bool ParsedText::splitPathologicalTokenAtIndex(const size_t wordIndex, const int } std::string remainder = word.substr(chosenOffset); + uint32_t remainderOffset = visibleOffsetAt(wordIndex); + const auto* offsetPtr = reinterpret_cast(word.data()); + const auto* const splitPtr = offsetPtr + chosenOffset; + while (offsetPtr < splitPtr) { + utf8NextCodepoint(&offsetPtr); + remainderOffset++; + } words[wordIndex].resize(chosenOffset); const BionicTokenMetadata prefixBionic = computeBionicMetadata(words[wordIndex], style, bionicReadingEnabled); wordStyles[wordIndex] = prefixBionic.style; wordBionicBoundary[wordIndex] = prefixBionic.boundary; const BionicTokenMetadata remainderBionic = computeBionicMetadata(remainder, style, bionicReadingEnabled); words.insert(words.begin() + wordIndex + 1, remainder); + insertVisibleOffset(wordIndex + 1, remainderOffset); wordStyles.insert(wordStyles.begin() + wordIndex + 1, remainderBionic.style); wordBackgroundBlack.insert(wordBackgroundBlack.begin() + wordIndex + 1, wordBackgroundBlack[wordIndex]); wordBionicBoundary.insert(wordBionicBoundary.begin() + wordIndex + 1, remainderBionic.boundary); wordGuideDotBefore.insert(wordGuideDotBefore.begin() + wordIndex + 1, false); - wordBackgroundBlack[wordIndex + 1] &= TextBlock::WORD_FLAG_BACKGROUND_BLACK; + wordBackgroundBlack[wordIndex + 1] &= static_cast(~TextBlock::WORD_FLAG_INSERTED_HYPHEN); wordContinues.insert(wordContinues.begin() + wordIndex + 1, false); wordNoSpaceBefore.insert(wordNoSpaceBefore.begin() + wordIndex + 1, false); @@ -1124,10 +1479,11 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const const ArenaVector& wordWidths, const std::vector& continuesVec, const std::vector& noSpaceBeforeVec, const ArenaVector& naturalGaps, const ArenaVector& gapSlots, const ArenaVector& lineBreakIndices, - const std::function)>& processLine, + const std::function, uint32_t)>& processLine, const GfxRenderer& renderer, const int fontId) { const size_t lineBreak = lineBreakIndices[breakIndex]; const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0; + const uint32_t lineVisibleOffset = visibleOffsetAt(lastBreakAt); size_t lineWordCount = lineBreak - lastBreakAt; const int firstLineIndent = resolveFirstLineIndent(breakIndex == 0, renderer, fontId); @@ -1144,6 +1500,14 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const lineBionicBoundary.clear(); lineGuideDotBefore.clear(); lineBackgroundBlack.clear(); + std::vector lineRubyTexts; + if (!rubyTexts.empty() && lastBreakAt < rubyTexts.size()) { + lineRubyTexts.resize(lineWordCount); + const size_t copyCount = std::min(lineBreak, rubyTexts.size()) - lastBreakAt; + std::copy(rubyTexts.begin() + lastBreakAt, rubyTexts.begin() + lastBreakAt + copyCount, lineRubyTexts.begin()); + } + const int extraStartOffset = calculateRubyExtraStartOffset(lastBreakAt, lineBreak, renderer, fontId); + const int extraEndOffset = calculateRubyExtraEndOffset(lastBreakAt, lineBreak, renderer, fontId); lineWords.reserve(lineWordCount); lineWordStyles.reserve(lineWordCount); lineWordWidths.reserve(lineWordCount); @@ -1187,8 +1551,8 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const ? CssTextAlign::Right : blockStyle.alignment; - // For justified text, compute per-gap extra to distribute remaining space evenly - const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps; + // Keep the visual overhang of edge ruby groups inside the page margins. + const int spareSpace = effectivePageWidth - extraStartOffset - extraEndOffset - lineWordWidthSum - totalNaturalGaps; const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine) ? computeJustifyExtra(spareSpace, actualGapCount) : 0; @@ -1207,6 +1571,8 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const int activeJustifyExtra = justifyExtra; if (willReorder) { + std::vector reorderedRubyTexts; + if (!lineRubyTexts.empty()) reorderedRubyTexts.reserve(visualOrderScratch.size()); reorderedWordsScratch.clear(); reorderedStylesScratch.clear(); reorderedWidthsScratch.clear(); @@ -1231,6 +1597,7 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const reorderedWidthsScratch.push_back(lineWordWidths[src]); reorderedBionicBoundaryScratch.push_back(lineBionicBoundary[src]); reorderedBackgroundBlackScratch.push_back(lineBackgroundBlack[src]); + if (!lineRubyTexts.empty()) reorderedRubyTexts.push_back(std::move(lineRubyTexts[src])); bool continues = false; bool guideDotBefore = false; @@ -1262,14 +1629,15 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const reorderedGapCount += gapSlotsBeforeToken(reorderedWordsScratch[wordIdx], reorderedContinuesScratch[wordIdx], reorderedNoSpaceBeforeScratch[wordIdx], reorderedGuideDotBeforeScratch[wordIdx]); - reorderedNaturalGaps += - naturalGapBeforeToken(renderer, fontId, reorderedWordsScratch[wordIdx - 1], reorderedWordsScratch[wordIdx], - reorderedStylesScratch[wordIdx - 1], reorderedContinuesScratch[wordIdx], - reorderedNoSpaceBeforeScratch[wordIdx], reorderedGuideDotBeforeScratch[wordIdx]); + reorderedNaturalGaps += naturalGapBeforeToken( + renderer, fontId, reorderedWordsScratch[wordIdx - 1], reorderedWordsScratch[wordIdx], + reorderedStylesScratch[wordIdx - 1], reorderedContinuesScratch[wordIdx], + reorderedNoSpaceBeforeScratch[wordIdx], reorderedGuideDotBeforeScratch[wordIdx], wordSpacing); } } - const int reorderedSpare = effectivePageWidth - reorderedWordWidthSum - reorderedNaturalGaps; + const int reorderedSpare = + effectivePageWidth - extraStartOffset - extraEndOffset - reorderedWordWidthSum - reorderedNaturalGaps; const int reorderedJustifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine) ? computeJustifyExtra(reorderedSpare, reorderedGapCount) : 0; @@ -1306,9 +1674,9 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const const bool nextContinues = reorderedContinuesScratch[wordIdx + 1]; const bool nextNoSpace = reorderedNoSpaceBeforeScratch[wordIdx + 1]; const bool nextGuideDot = reorderedGuideDotBeforeScratch[wordIdx + 1]; - int gap = - naturalGapBeforeToken(renderer, fontId, reorderedWordsScratch[wordIdx], reorderedWordsScratch[wordIdx + 1], - reorderedStylesScratch[wordIdx], nextContinues, nextNoSpace, nextGuideDot); + int gap = naturalGapBeforeToken(renderer, fontId, reorderedWordsScratch[wordIdx], + reorderedWordsScratch[wordIdx + 1], reorderedStylesScratch[wordIdx], + nextContinues, nextNoSpace, nextGuideDot, wordSpacing); gap += reorderedJustifyExtra * static_cast(gapSlotsBeforeToken(reorderedWordsScratch[wordIdx + 1], nextContinues, nextNoSpace, nextGuideDot)); xpos += gap; @@ -1321,6 +1689,7 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const lineBionicBoundary.swap(reorderedBionicBoundaryScratch); lineGuideDotBefore.swap(reorderedGuideDotBeforeScratch); lineBackgroundBlack.swap(reorderedBackgroundBlackScratch); + if (!lineRubyTexts.empty()) lineRubyTexts.swap(reorderedRubyTexts); lineWordCount = lineWords.size(); } else { // Standard LTR/RTL positioning loop when no visual reordering is needed @@ -1351,7 +1720,7 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const } } else { // LTR: position words from left to right - int xpos = firstLineIndent; + int xpos = firstLineIndent + extraStartOffset; if (effectiveAlignment == CssTextAlign::Right) { xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps; } else if (effectiveAlignment == CssTextAlign::Center) { @@ -1399,7 +1768,7 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const std::vector outXPos; std::vector outStyles; std::vector outBoundaries; - std::vector outSuffixX; + std::vector outRunOffsets; std::vector outGuideDotXOffset; std::vector outBackgroundBlack; outWords.reserve(lineWordCount); @@ -1407,7 +1776,7 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const outStyles.reserve(lineWordCount); if (lineHasBionicSplit) { outBoundaries.reserve(lineWordCount); - outSuffixX.reserve(lineWordCount); + outRunOffsets.reserve(lineWordCount); } if (lineHasGuideDot) { outGuideDotXOffset.reserve(lineWordCount); @@ -1418,24 +1787,32 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const for (size_t i = 0; i < lineWordCount; i++) { const uint8_t boundary = lineBionicBoundary[i]; - const uint16_t suffixX = - boundary > 0 ? static_cast(std::max( - 0, measureBionicSuffixX(renderer, fontId, lineWords[i], lineWordStyles[i], boundary))) - : 0; + const bool wordIsRtl = BidiUtils::detectParagraphLevel(lineWords[i].c_str(), blockStyle.isRtl ? 1 : 0) == + static_cast(BidiUtils::BidiBaseDir::RTL); + const uint16_t runOffset = + boundary > 0 + ? static_cast(std::max( + 0, measureBionicRunOffset(renderer, fontId, lineWords[i], lineWordStyles[i], boundary, wordIsRtl))) + : 0; outWords.push_back(std::move(lineWords[i])); outXPos.push_back(lineXPos[i]); outStyles.push_back(lineWordStyles[i]); if (lineHasBionicSplit) { outBoundaries.push_back(boundary); - outSuffixX.push_back(suffixX); + outRunOffsets.push_back(runOffset); } if (lineHasGuideDot) { outGuideDotXOffset.push_back(0); if (i > 0 && lineGuideDotBefore[i]) { + const int wordSpacingSecondHalf = + guideDotWordSpacingExtra(renderer, fontId, outWords[outWords.size() - 2], outWords.back(), + outStyles[outStyles.size() - 2], wordSpacing) / + 2; const int secondGap = guideDotSecondGap(renderer, fontId, outWords.back()) + - (isClosingPunctuationForJustify(firstCodepoint(outWords.back())) ? 0 : activeJustifyExtra); + (isClosingPunctuationForJustify(firstCodepoint(outWords.back())) ? 0 : activeJustifyExtra) + + wordSpacingSecondHalf; const int dotX = static_cast(lineXPos[i]) - secondGap - renderer.getTextAdvanceX(fontId, GUIDE_DOT_UTF8, EpdFontFamily::REGULAR); const int dotDelta = dotX - static_cast(outXPos[outXPos.size() - 2]); @@ -1447,12 +1824,13 @@ bool ParsedText::extractLine(Arena& scratchArena, const size_t breakIndex, const } } - auto block = std::make_shared(outWords, outXPos, outStyles, outBoundaries, outSuffixX, outGuideDotXOffset, - outBackgroundBlack, blockStyle); + auto block = + std::make_shared(outWords, outXPos, outStyles, outBoundaries, outRunOffsets, outGuideDotXOffset, + outBackgroundBlack, blockStyle, std::move(lineRubyTexts)); if (!block->valid()) { LOG_ERR("PTX", "Dropping line: TextBlock arena allocation failed"); return false; } - processLine(std::move(block)); + processLine(std::move(block), lineVisibleOffset); return true; } diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index 374510c6ae..e60fc75bdb 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -16,20 +17,43 @@ template class ArenaVector; class ParsedText { - std::vector words; + // words/rubyTexts are std::deque, not std::vector: a paragraph can hold thousands + // of tokens (CJK splits every character), and a vector grows by reallocating its + // whole element array into one contiguous block (32 B/std::string -> 64-128 KB at + // a few thousand tokens). On the ESP32-C3 that single large contiguous request + // fails under a fragmented, BLE-resident heap and the throwing operator new + // abort()s the firmware (fresh-open CJK crash). A deque grows in fixed ~512 B nodes + // (largest contiguous alloc stays ~2 KB regardless of token count), so it never + // triggers that. The per-token parallel arrays below stay vectors: 1 byte / 1 bit + // each, they never approach the contiguous-block ceiling. + std::deque words; std::vector wordStyles; std::vector wordContinues; // true = word attaches to previous (no space before it) std::vector wordNoSpaceBefore; // true = may break before token, but no synthetic space when joined std::vector wordBionicBoundary; // UTF-8 byte offset where the regular suffix starts; 0 = no split std::vector wordGuideDotBefore; // true = virtual guide dot belongs between previous token and this one std::vector wordBackgroundBlack; + // Layout-only text coordinates. The rendered page never retains these; use + // compact deltas while a paragraph is pending to protect C3 heap headroom. + struct VisibleOffsetRebase { + size_t wordIndex; + uint32_t base; + }; + std::vector wordVisibleOffsetDeltas; + uint32_t visibleOffsetBase = 0; + std::vector visibleOffsetRebases; + std::deque rubyTexts; bool extraParagraphSpacing; bool forceParagraphIndents; bool hyphenationEnabled; bool bionicReadingEnabled; bool guideReadingEnabled; + uint8_t wordSpacing; BlockStyle blockStyle; bool hasRtlWord; + // True after an intermediate flush leaves the rest of the same paragraph + // buffered. The next layout pass must not apply first-line paragraph rules. + bool isContinuation_ = false; std::vector reorderedWordsScratch; std::vector reorderedStylesScratch; std::vector reorderedWidthsScratch; @@ -61,35 +85,53 @@ class ParsedText { ArenaVector& wordWidths, bool allowFallbackBreaks); bool splitPathologicalTokenAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId, ArenaVector& wordWidths); + uint32_t visibleOffsetBaseAt(size_t wordIndex) const; + uint32_t visibleOffsetAt(size_t wordIndex) const; + void pushVisibleOffset(uint32_t offset); + void insertVisibleOffset(size_t wordIndex, uint32_t offset); + void eraseVisibleOffsetPrefix(size_t count); + int calculateRubyExtraStartOffset(size_t wordIdx, size_t maxWordIdx, const GfxRenderer& renderer, int fontId) const; + int calculateRubyExtraEndOffset(size_t lineStartIdx, size_t lineBreakIdx, const GfxRenderer& renderer, + int fontId) const; bool extractLine(Arena& scratchArena, size_t breakIndex, int pageWidth, const ArenaVector& wordWidths, const std::vector& continuesVec, const std::vector& noSpaceBeforeVec, const ArenaVector& naturalGaps, const ArenaVector& gapSlots, const ArenaVector& lineBreakIndices, - const std::function)>& processLine, const GfxRenderer& renderer, - int fontId); + const std::function, uint32_t)>& processLine, + const GfxRenderer& renderer, int fontId); bool calculateWordWidths(ArenaVector& wordWidths, const GfxRenderer& renderer, int fontId); public: explicit ParsedText(const bool extraParagraphSpacing, const bool forceParagraphIndents = false, const bool hyphenationEnabled = false, const bool bionicReadingEnabled = false, - const bool guideReadingEnabled = false, const BlockStyle& blockStyle = BlockStyle()) + const bool guideReadingEnabled = false, const uint8_t wordSpacing = 0, + const BlockStyle& blockStyle = BlockStyle()) : extraParagraphSpacing(extraParagraphSpacing), forceParagraphIndents(forceParagraphIndents), hyphenationEnabled(hyphenationEnabled), bionicReadingEnabled(bionicReadingEnabled), guideReadingEnabled(guideReadingEnabled), + wordSpacing(wordSpacing), blockStyle(blockStyle), hasRtlWord(false) {} ~ParsedText() = default; void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false, - bool backgroundBlack = false); + bool backgroundBlack = false, uint8_t linkId = 0, uint32_t visibleTextOffset = 0); + void setRubyForWordAt(size_t index, const std::string& ruby); + void setRubyGroupAt(size_t startIndex, size_t count, const std::string& ruby); + EpdFontFamily::Style getWordStyleAt(size_t index) const { + return index < wordStyles.size() ? wordStyles[index] : EpdFontFamily::REGULAR; + } + std::string getRubyTextAt(size_t index) const { return index < rubyTexts.size() ? rubyTexts[index] : std::string(); } + void ensureRubyCapacity(); void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; } BlockStyle& getBlockStyle() { return blockStyle; } size_t size() const { return words.size(); } bool isEmpty() const { return words.empty(); } + bool isContinuation() const { return isContinuation_; } bool layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth, - const std::function)>& processLine, + const std::function, uint32_t)>& processLine, bool includeLastLine = true); bool layoutAndExtractLinesPreservingSource(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth, const std::function)>& processLine) const; diff --git a/lib/Epub/Epub/ReaderRenderSpec.h b/lib/Epub/Epub/ReaderRenderSpec.h new file mode 100644 index 0000000000..51be9e2ce3 --- /dev/null +++ b/lib/Epub/Epub/ReaderRenderSpec.h @@ -0,0 +1,53 @@ +#pragma once +#include + +#include "EpubRenderMode.h" + +// The resolved text-rendering configuration a reader hands to the layout +// engine. Section-cache validation keys on every field: a section file built +// with a different spec is discarded and rebuilt. +// +// Build one via CrossPointSettings::readerRenderSpec(width, height), which +// fills every field: the settings-derived ones from the store, the viewport +// from the caller. Taking the viewport as arguments is what keeps a spec from +// existing in a half-filled state — the 0 defaults below are a last-resort +// backstop (a 0x0 viewport lays out nothing), not an invitation to omit it. +struct ReaderRenderSpec { + int fontId = 0; + float lineCompression = 1.0f; + bool extraParagraphSpacing = false; + bool forceParagraphIndents = false; + uint8_t paragraphAlignment = 0; + uint16_t viewportWidth = 0; + uint16_t viewportHeight = 0; + bool hyphenationEnabled = false; + bool embeddedStyle = true; + uint8_t imageRendering = 0; + bool bionicReadingEnabled = false; + bool guideReadingEnabled = false; + uint8_t wordSpacing = 0; + EpubRenderMode renderMode = EpubRenderMode::CrossInkDefault; +}; + +inline uint32_t readerRenderSpecSignature(const ReaderRenderSpec& spec) { + uint32_t signature = 2166136261U; + const auto mix = [&signature](const uint32_t value) { + signature ^= value; + signature *= 16777619U; + }; + mix(static_cast(spec.fontId)); + mix(static_cast(spec.lineCompression * 1000.0f)); + mix(spec.extraParagraphSpacing); + mix(spec.forceParagraphIndents); + mix(spec.paragraphAlignment); + mix(spec.viewportWidth); + mix(spec.viewportHeight); + mix(spec.hyphenationEnabled); + mix(spec.embeddedStyle); + mix(spec.imageRendering); + mix(spec.bionicReadingEnabled); + mix(spec.guideReadingEnabled); + mix(spec.wordSpacing); + mix(static_cast(spec.renderMode)); + return signature == 0 ? 1 : signature; +} diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 2c1cd83990..57590de017 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -3,11 +3,10 @@ #include #include #include -#include +#include #include #include #include -#include #include #include "Epub/css/CssParser.h" @@ -17,15 +16,19 @@ namespace { constexpr uint32_t SECTION_CACHE_MAGIC = 0x535843FF; // bytes: 0xFF, "CXS" -// v44: TextBlock word data is stored as one flat arena with optional bionic, -// guide-dot, and word-flag arrays. -constexpr uint8_t SECTION_FILE_VERSION = 44; +// v60: reserve page-edge space for ruby overhang and prefer longer equal-cost +// CJK lines, invalidating cached pagination from the prior layout contract. +constexpr uint8_t SECTION_FILE_VERSION = 60; +// Suspended incremental build: valid pages plus LUTs and a parse-watermark trailer. +// Change this with layout or payload changes so stale partial pages cannot resume +// under a different layout contract. +constexpr uint8_t SECTION_FILE_PARTIAL_VERSION = 0xF9; constexpr uint16_t INITIAL_SECTION_PAGE_LUT_ENTRIES = 1024; constexpr uint32_t HEADER_SIZE = sizeof(SECTION_CACHE_MAGIC) + sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(bool) + sizeof(uint8_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + sizeof(uint8_t) + sizeof(bool) + - sizeof(bool) + sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t) + - sizeof(uint32_t) + sizeof(uint32_t); + sizeof(bool) + sizeof(uint8_t) + sizeof(uint8_t) + sizeof(uint32_t) + + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t); constexpr size_t SECTION_HTML_STREAM_CHUNK_SIZE = 8192; constexpr size_t LOW_MEMORY_SECTION_HTML_STREAM_CHUNK_SIZE = 1024; @@ -33,6 +36,7 @@ struct PageLutEntry { uint32_t fileOffset; uint16_t paragraphIndex; uint16_t listItemIndex; + uint32_t visibleTextOffset; }; template @@ -56,18 +60,10 @@ bool ensurePageLutCapacity(std::unique_ptr& lut, uint16_t& lutCapacity, return true; } -ScratchWorkspace::Lease acquireSectionZipInflateScratch(GfxRenderer& renderer, const int fontId, const char* reason) { - if (ESP.getMaxAllocHeap() < InflateReader::STREAMING_DICT_SIZE && renderer.isSdCardFont(fontId)) { +void prepareSectionZipInflate(GfxRenderer& renderer, const int fontId) { + if (ESP.getMaxAllocHeap() < InflateStream::requiredStorageSize(true) && renderer.isSdCardFont(fontId)) { renderer.releaseSdCardFontForLowMemory(fontId); } - - auto scratch = ScratchWorkspace::acquire(InflateReader::STREAMING_DICT_SIZE, reason); - if (scratch || !renderer.isSdCardFont(fontId)) { - return scratch; - } - - renderer.releaseSdCardFontForLowMemory(fontId); - return ScratchWorkspace::acquire(InflateReader::STREAMING_DICT_SIZE, reason); } size_t sectionHtmlStreamChunkSize(const bool preview) { @@ -76,26 +72,79 @@ size_t sectionHtmlStreamChunkSize(const bool preview) { } const uint32_t maxAlloc = ESP.getMaxAllocHeap(); - const size_t largeStreamBudget = InflateReader::STREAMING_DICT_SIZE + (2U * SECTION_HTML_STREAM_CHUNK_SIZE); + const size_t largeStreamBudget = InflateStream::requiredStorageSize(true) + (2U * SECTION_HTML_STREAM_CHUNK_SIZE); if (maxAlloc < largeStreamBudget) { LOG_DBG("SCT", "Using low-memory HTML stream chunk (maxAlloc=%u)", maxAlloc); return LOW_MEMORY_SECTION_HTML_STREAM_CHUNK_SIZE; } return SECTION_HTML_STREAM_CHUNK_SIZE; } + +std::string sectionBackupPath(const std::string& filePath) { return filePath + ".bak"; } + +void recoverSectionCacheBackup(const std::string& filePath) { + const std::string backupPath = sectionBackupPath(filePath); + if (!Storage.exists(backupPath.c_str())) return; + + if (Storage.exists(filePath.c_str())) { + Storage.remove(backupPath.c_str()); + return; + } + + if (Storage.rename(backupPath.c_str(), filePath.c_str())) { + LOG_INF("SCT", "Recovered section cache backup: %s", filePath.c_str()); + } else { + LOG_ERR("SCT", "Failed to recover section cache backup: %s", filePath.c_str()); + } +} + +bool promoteSectionCache(const std::string& tmpPath, const std::string& filePath) { + recoverSectionCacheBackup(filePath); + if (!Storage.exists(filePath.c_str())) { + return Storage.rename(tmpPath.c_str(), filePath.c_str()); + } + + const std::string backupPath = sectionBackupPath(filePath); + if (!Storage.rename(filePath.c_str(), backupPath.c_str())) { + LOG_ERR("SCT", "Failed to preserve old section cache before replacement"); + return false; + } + if (Storage.rename(tmpPath.c_str(), filePath.c_str())) { + Storage.remove(backupPath.c_str()); + return true; + } + + LOG_ERR("SCT", "Failed to promote section cache; restoring previous cache"); + if (!Storage.rename(backupPath.c_str(), filePath.c_str())) { + LOG_ERR("SCT", "Failed to restore section cache backup"); + } + return false; +} } // namespace Section::Section(const std::shared_ptr& epub, const int spineIndex, GfxRenderer& renderer, const char* cacheSuffix) - : epub(epub), + : Section(*epub, spineIndex, renderer, cacheSuffix) { + epubOwner = epub; +} + +Section::Section(Epub& epub, const int spineIndex, GfxRenderer& renderer, const char* cacheSuffix) + : epub(&epub), spineIndex(spineIndex), renderer(renderer), - filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + (cacheSuffix ? cacheSuffix : "") + - ".bin") {} + filePath(epub.getCachePath() + "/sections/" + std::to_string(spineIndex) + (cacheSuffix ? cacheSuffix : "") + + ".bin") { + recoverSectionCacheBackup(filePath); +} + +// Suspend any in-progress build so every section.reset() / navigation / sleep path +// persists the pages already laid out as a partial .bin instead of discarding them +// (no-op once a build has completed or never started). +Section::~Section() { suspendBuild(); } uint32_t Section::onPageComplete(std::unique_ptr page) { - if (!file) { - LOG_ERR("SCT", "File not open for writing page %d", pageCount); + if (!ensureBuildFileOpen()) { + LOG_ERR("SCT", "File not open for writing page %d", builtPageCount_); return 0; } if (!page) { @@ -104,47 +153,90 @@ uint32_t Section::onPageComplete(std::unique_ptr page) { } const uint32_t position = file.position(); - const uint32_t serializeStart = millis(); if (!page->serialize(file)) { - LOG_ERR("SCT", "Failed to serialize page %d", pageCount); + LOG_ERR("SCT", "Failed to serialize page %d", builtPageCount_); return 0; } - LOG_DBG("SCT", "Page %d processed (pos=%lu, serialize=%lums, free=%u, maxAlloc=%u)", pageCount, - static_cast(position), static_cast(millis() - serializeStart), - ESP.getFreeHeap(), ESP.getMaxAllocHeap()); - pageCount++; + builtPageCount_++; + // pageCount is the pages available to read: a rebuild over a partial only raises it + // once it has laid out more pages than the partial already covers. + if (builtPageCount_ > pageCount) { + pageCount = builtPageCount_; + } return position; } -bool Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, - const bool forceParagraphIndents, const uint8_t paragraphAlignment, - const uint16_t viewportWidth, const uint16_t viewportHeight, - const bool hyphenationEnabled, const bool embeddedStyle, - const uint8_t imageRendering, const bool bionicReadingEnabled, - const bool guideReadingEnabled, const EpubRenderMode renderMode) { +bool Section::ensureBuildFileOpen() { + if (file) { + return true; + } + const std::string& tmpSectionPath = + build_ && !build_->tmpSectionPath.empty() ? build_->tmpSectionPath : activeBuildTmpSectionPath_; + if (tmpSectionPath.empty()) { + return false; + } + file = Storage.open(tmpSectionPath.c_str(), O_RDWR); + if (!file) { + LOG_ERR("SCT", "Failed to reopen section temp file"); + return false; + } + if (!file.seek(file.size())) { + LOG_ERR("SCT", "Failed to seek section temp file"); + file.close(); + return false; + } + return true; +} + +void Section::releaseBuildFile() { + if (!build_) { + return; + } + if (file) { + file.flush(); + if (!file.sync()) { + LOG_ERR("SCT", "Failed to sync incremental section temp file before release"); + } + if (!file.close()) { + LOG_ERR("SCT", "Failed to close incremental section temp file before progress save"); + } + } + if (build_->parser) { + build_->parser->releaseInputFile(); + } +} + +bool Section::writeSectionFileHeader(const ReaderRenderSpec& spec) { if (!file) { LOG_DBG("SCT", "File not open for writing header"); return false; } - static_assert(HEADER_SIZE == sizeof(SECTION_CACHE_MAGIC) + sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + - sizeof(lineCompression) + sizeof(extraParagraphSpacing) + - sizeof(forceParagraphIndents) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + - sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) + - sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(bionicReadingEnabled) + - sizeof(guideReadingEnabled) + sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t) + - sizeof(uint32_t) + sizeof(uint32_t), + static_assert(HEADER_SIZE == sizeof(SECTION_CACHE_MAGIC) + sizeof(SECTION_FILE_VERSION) + sizeof(spec.fontId) + + sizeof(spec.lineCompression) + sizeof(spec.extraParagraphSpacing) + + sizeof(spec.forceParagraphIndents) + sizeof(spec.paragraphAlignment) + + sizeof(spec.viewportWidth) + sizeof(spec.viewportHeight) + sizeof(pageCount) + + sizeof(spec.hyphenationEnabled) + sizeof(spec.embeddedStyle) + + sizeof(spec.imageRendering) + sizeof(spec.bionicReadingEnabled) + + sizeof(spec.guideReadingEnabled) + sizeof(uint8_t) + sizeof(uint32_t) + + sizeof(uint32_t) + sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t) + + sizeof(uint32_t), "Header size mismatch"); return serialization::tryWritePod(file, SECTION_CACHE_MAGIC) && - serialization::tryWritePod(file, SECTION_FILE_VERSION) && serialization::tryWritePod(file, fontId) && - serialization::tryWritePod(file, lineCompression) && serialization::tryWritePod(file, extraParagraphSpacing) && - serialization::tryWritePod(file, forceParagraphIndents) && - serialization::tryWritePod(file, paragraphAlignment) && serialization::tryWritePod(file, viewportWidth) && - serialization::tryWritePod(file, viewportHeight) && serialization::tryWritePod(file, hyphenationEnabled) && - serialization::tryWritePod(file, embeddedStyle) && serialization::tryWritePod(file, imageRendering) && - serialization::tryWritePod(file, bionicReadingEnabled) && - serialization::tryWritePod(file, guideReadingEnabled) && - serialization::tryWritePod(file, static_cast(renderMode)) && + serialization::tryWritePod(file, SECTION_FILE_VERSION) && serialization::tryWritePod(file, spec.fontId) && + serialization::tryWritePod(file, spec.lineCompression) && + serialization::tryWritePod(file, spec.extraParagraphSpacing) && + serialization::tryWritePod(file, spec.forceParagraphIndents) && + serialization::tryWritePod(file, spec.paragraphAlignment) && + serialization::tryWritePod(file, spec.viewportWidth) && + serialization::tryWritePod(file, spec.viewportHeight) && + serialization::tryWritePod(file, spec.hyphenationEnabled) && + serialization::tryWritePod(file, spec.embeddedStyle) && + serialization::tryWritePod(file, spec.imageRendering) && + serialization::tryWritePod(file, spec.bionicReadingEnabled) && + serialization::tryWritePod(file, spec.guideReadingEnabled) && + serialization::tryWritePod(file, spec.wordSpacing) && + serialization::tryWritePod(file, static_cast(spec.renderMode)) && serialization::tryWritePod(file, pageCount) && // Placeholder for page count (will be initially 0, patched later) serialization::tryWritePod(file, static_cast(0)) && // Placeholder for LUT offset (patched later) @@ -153,20 +245,17 @@ bool Section::writeSectionFileHeader(const int fontId, const float lineCompressi serialization::tryWritePod( file, static_cast(0)) && // Placeholder for paragraph LUT offset (patched later) - serialization::tryWritePod(file, static_cast(0)); // Placeholder for li LUT offset (patched later) + serialization::tryWritePod(file, static_cast(0)) && // li LUT offset + serialization::tryWritePod(file, static_cast(0)); // visible text LUT offset } -bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, - const bool forceParagraphIndents, const uint8_t paragraphAlignment, - const uint16_t viewportWidth, const uint16_t viewportHeight, - const bool hyphenationEnabled, const bool embeddedStyle, const uint8_t imageRendering, - const bool bionicReadingEnabled, const bool guideReadingEnabled, - const EpubRenderMode renderMode) { +bool Section::loadSectionFile(const ReaderRenderSpec& spec) { if (!Storage.openFileForRead("SCT", filePath, file)) { return false; } // Match parameters + bool filePartial = false; { uint32_t magic; if (!serialization::tryReadPod(file, magic)) { @@ -189,13 +278,14 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con clearCache(); return false; } - if (version != SECTION_FILE_VERSION) { + if (version != SECTION_FILE_VERSION && version != SECTION_FILE_PARTIAL_VERSION) { // Explicit close() required: member variable persists beyond function scope file.close(); LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version); clearCache(); return false; } + filePartial = (version == SECTION_FILE_PARTIAL_VERSION); int fileFontId; uint16_t fileViewportWidth, fileViewportHeight; @@ -208,6 +298,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con uint8_t fileImageRendering; bool fileBionicReadingEnabled; bool fileGuideReadingEnabled; + uint8_t fileWordSpacing; uint8_t fileRenderMode; if (!serialization::tryReadPod(file, fileFontId) || !serialization::tryReadPod(file, fileLineCompression) || !serialization::tryReadPod(file, fileExtraParagraphSpacing) || @@ -217,20 +308,22 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con !serialization::tryReadPod(file, fileHyphenationEnabled) || !serialization::tryReadPod(file, fileEmbeddedStyle) || !serialization::tryReadPod(file, fileImageRendering) || !serialization::tryReadPod(file, fileBionicReadingEnabled) || - !serialization::tryReadPod(file, fileGuideReadingEnabled) || !serialization::tryReadPod(file, fileRenderMode)) { + !serialization::tryReadPod(file, fileGuideReadingEnabled) || + !serialization::tryReadPod(file, fileWordSpacing) || !serialization::tryReadPod(file, fileRenderMode)) { file.close(); LOG_ERR("SCT", "Deserialization failed: truncated section header"); clearCache(); return false; } - if (fontId != fileFontId || lineCompression != fileLineCompression || - extraParagraphSpacing != fileExtraParagraphSpacing || forceParagraphIndents != fileForceParagraphIndents || - paragraphAlignment != fileParagraphAlignment || viewportWidth != fileViewportWidth || - viewportHeight != fileViewportHeight || hyphenationEnabled != fileHyphenationEnabled || - embeddedStyle != fileEmbeddedStyle || imageRendering != fileImageRendering || - bionicReadingEnabled != fileBionicReadingEnabled || guideReadingEnabled != fileGuideReadingEnabled || - static_cast(renderMode) != fileRenderMode) { + if (spec.fontId != fileFontId || spec.lineCompression != fileLineCompression || + spec.extraParagraphSpacing != fileExtraParagraphSpacing || + spec.forceParagraphIndents != fileForceParagraphIndents || spec.paragraphAlignment != fileParagraphAlignment || + spec.viewportWidth != fileViewportWidth || spec.viewportHeight != fileViewportHeight || + spec.hyphenationEnabled != fileHyphenationEnabled || spec.embeddedStyle != fileEmbeddedStyle || + spec.imageRendering != fileImageRendering || spec.bionicReadingEnabled != fileBionicReadingEnabled || + spec.guideReadingEnabled != fileGuideReadingEnabled || spec.wordSpacing != fileWordSpacing || + static_cast(spec.renderMode) != fileRenderMode) { file.close(); LOG_ERR("SCT", "Deserialization failed: Parameters do not match"); clearCache(); @@ -245,16 +338,60 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con return false; } + if (filePartial) { + // A partial's pageCount is the watermark of a suspended build. Read the watermark + // trailer (appended after the page-start visible-text LUT) so estimatedTotalPages can extrapolate. + uint32_t visibleTextLutOffset = 0; + if (!file.seek(HEADER_SIZE - sizeof(uint32_t)) || !serialization::tryReadPod(file, visibleTextLutOffset)) { + file.close(); + LOG_ERR("SCT", "Deserialization failed: missing partial trailer offset"); + clearCache(); + pageCount = 0; + return false; + } + const uint32_t trailerOffset = visibleTextLutOffset + static_cast(pageCount) * sizeof(uint32_t); + const bool trailerValid = + pageCount > 0 && visibleTextLutOffset >= HEADER_SIZE && trailerOffset + 2 * sizeof(uint32_t) <= file.size(); + if (!trailerValid) { + file.close(); + LOG_ERR("SCT", "Deserialization failed: malformed partial section"); + clearCache(); + pageCount = 0; + return false; + } + if (!file.seek(trailerOffset) || !serialization::tryReadPod(file, partialBytesConsumed_) || + !serialization::tryReadPod(file, partialTotalBytes_)) { + file.close(); + LOG_ERR("SCT", "Deserialization failed: truncated partial trailer"); + clearCache(); + pageCount = 0; + return false; + } + partial_ = true; + partialPageCount_ = pageCount; + } else { + partial_ = false; + partialPageCount_ = 0; + partialBytesConsumed_ = 0; + partialTotalBytes_ = 0; + } + // Explicit close() required: member variable persists beyond function scope file.close(); - LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount); return true; } // Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem) bool Section::clearCache() const { + const std::string tmpBin = binTmpPath(); + if (Storage.exists(tmpBin.c_str())) { + Storage.remove(tmpBin.c_str()); + } + const std::string backupPath = sectionBackupPath(filePath); + if (Storage.exists(backupPath.c_str())) { + Storage.remove(backupPath.c_str()); + } if (!Storage.exists(filePath.c_str())) { - LOG_DBG("SCT", "Cache does not exist, no action needed"); return true; } @@ -263,25 +400,48 @@ bool Section::clearCache() const { return false; } - LOG_DBG("SCT", "Cache cleared successfully"); return true; } -bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, - const bool forceParagraphIndents, const uint8_t paragraphAlignment, - const uint16_t viewportWidth, const uint16_t viewportHeight, - const bool hyphenationEnabled, const bool embeddedStyle, const uint8_t imageRendering, - const bool bionicReadingEnabled, const bool guideReadingEnabled, - const std::function& popupFn, bool* imagesWereSuppressed, - bool* layoutAbortedForLowMemory, const EpubRenderMode renderMode, +bool Section::createSectionFile(const ReaderRenderSpec& spec, const std::function& popupFn, + bool* imagesWereSuppressed, bool* layoutAbortedForLowMemory, const SectionBuildOptions buildOptions) { + const int fontId = spec.fontId; + const float lineCompression = spec.lineCompression; + const bool extraParagraphSpacing = spec.extraParagraphSpacing; + const bool forceParagraphIndents = spec.forceParagraphIndents; + const uint8_t paragraphAlignment = spec.paragraphAlignment; + const uint16_t viewportWidth = spec.viewportWidth; + const uint16_t viewportHeight = spec.viewportHeight; + const bool hyphenationEnabled = spec.hyphenationEnabled; + const bool embeddedStyle = spec.embeddedStyle; + const uint8_t imageRendering = spec.imageRendering; + const bool bionicReadingEnabled = spec.bionicReadingEnabled; + const bool guideReadingEnabled = spec.guideReadingEnabled; + const uint8_t wordSpacing = spec.wordSpacing; + const EpubRenderMode renderMode = spec.renderMode; const auto localPath = epub->getSpineItem(spineIndex).href; const auto htmlDir = epub->getCachePath() + "/html"; const auto htmlPath = htmlDir + "/" + std::to_string(spineIndex) + ".html"; const auto tmpHtmlPath = htmlDir + "/.tmp_" + std::to_string(spineIndex) + ".html"; const auto tmpSectionPath = filePath + ".tmp"; + activeBuildTmpSectionPath_ = tmpSectionPath; + struct ClearActiveBuildTmpPath { + std::string& path; + ~ClearActiveBuildTmpPath() { path.clear(); } + } clearActiveBuildTmpPath{activeBuildTmpSectionPath_}; pageCount = 0; if (layoutAbortedForLowMemory) *layoutAbortedForLowMemory = false; + if (buildOptions.cancellationObserved) *buildOptions.cancellationObserved = false; + const auto cancelBuild = [&buildOptions]() { + if (!buildOptions.isCancellationRequested()) { + return false; + } + if (buildOptions.cancellationObserved) { + *buildOptions.cancellationObserved = true; + } + return true; + }; const bool effectiveBionicReadingEnabled = bionicReadingEnabled; const bool effectiveGuideReadingEnabled = guideReadingEnabled; LOG_DBG("SCT", @@ -301,7 +461,8 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c // book (it lives in the per-book cache dir), not on render settings, so it survives the invalidation // that wipes the layout (.bin) caches when font/margin/orientation change -- rebuilds then skip zip // inflation entirely. It's promoted by an atomic rename as soon as the inflate succeeds (below), so - // future rebuilds can skip the multi-second inflate. If htmlPath exists it is known-complete. + // even a window-only giant spine -- whose .bin never finalizes -- still caches its HTML, letting a + // reopen skip the multi-second inflate. If htmlPath exists it is known-complete. const bool reusedHtml = Storage.exists(htmlPath.c_str()); bool htmlCached = reusedHtml; const auto cleanupTempHtml = [&]() { @@ -309,9 +470,11 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c Storage.remove(tmpHtmlPath.c_str()); } }; - if (reusedHtml) { - LOG_DBG("SCT", "Reusing cached HTML %s", htmlPath.c_str()); - } else { + if (cancelBuild()) { + LOG_DBG("SCT", "Section build cancelled before HTML inflate: spine=%d", spineIndex); + return false; + } + if (!reusedHtml) { Storage.mkdir(htmlDir.c_str()); // Retry logic for SD card timing issues @@ -333,10 +496,8 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c continue; } const size_t htmlStreamChunkSize = sectionHtmlStreamChunkSize(buildOptions.isPreview()); - { - auto zipInflateScratch = acquireSectionZipInflateScratch(renderer, fontId, "section one-shot HTML inflate"); - streamed = epub->readItemContentsToStream(localPath, tmpHtml, htmlStreamChunkSize); - } + prepareSectionZipInflate(renderer, fontId); + streamed = epub->readItemContentsToStream(localPath, tmpHtml, htmlStreamChunkSize); fileSize = tmpHtml.size(); // Explicitly close() file before calling Storage.remove() tmpHtml.close(); @@ -353,11 +514,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c return false; } - LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes, free=%u, maxAlloc=%u)", tmpHtmlPath.c_str(), fileSize, - ESP.getFreeHeap(), ESP.getMaxAllocHeap()); - - // Promote to the persistent HTML cache immediately -- the inflate is complete, and caching it - // lets future section rebuilds skip re-inflation. If the rename fails we just parse the temp. + // Promote to the persistent HTML cache immediately -- the inflate is complete and the bytes are + // valid regardless of whether the layout build finishes, so reopening (even a window-only spine + // that never finalizes its .bin) skips re-inflation. If the rename fails we just parse the temp. if (Storage.rename(tmpHtmlPath.c_str(), htmlPath.c_str())) { htmlCached = true; } else { @@ -366,6 +525,12 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } const std::string& parsePath = htmlCached ? htmlPath : tmpHtmlPath; + if (cancelBuild()) { + LOG_DBG("SCT", "Section build cancelled after HTML inflate: spine=%d", spineIndex); + cleanupTempHtml(); + return false; + } + if (Storage.exists(tmpSectionPath.c_str())) { Storage.remove(tmpSectionPath.c_str()); } @@ -374,9 +539,10 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c cleanupTempHtml(); return false; } - if (!writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, forceParagraphIndents, paragraphAlignment, - viewportWidth, viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, - effectiveBionicReadingEnabled, effectiveGuideReadingEnabled, renderMode)) { + ReaderRenderSpec effectiveSpec = spec; + effectiveSpec.bionicReadingEnabled = effectiveBionicReadingEnabled; + effectiveSpec.guideReadingEnabled = effectiveGuideReadingEnabled; + if (!writeSectionFileHeader(effectiveSpec)) { LOG_ERR("SCT", "Failed to write section header"); file.close(); Storage.remove(tmpSectionPath.c_str()); @@ -435,11 +601,12 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } ChapterHtmlSlimParser visitor( - epub, parsePath, renderer, fontId, lineCompression, extraParagraphSpacing, forceParagraphIndents, + *epub, parsePath, renderer, fontId, lineCompression, extraParagraphSpacing, forceParagraphIndents, paragraphAlignment, viewportWidth, viewportHeight, hyphenationEnabled, effectiveBionicReadingEnabled, - effectiveGuideReadingEnabled, + effectiveGuideReadingEnabled, wordSpacing, [this, &lut, &lutCapacity, &lutCount, &pageCompletionFailed, layoutAbortedForLowMemory]( - std::unique_ptr page, const uint16_t paragraphIndex, const uint16_t listItemIndex) { + std::unique_ptr page, const uint16_t paragraphIndex, const uint16_t listItemIndex, + const uint32_t visibleTextOffset) { if (pageCompletionFailed) { return; } @@ -459,13 +626,40 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c pageCompletionFailed = true; return; } - lut[lutCount++] = {fileOffset, paragraphIndex, listItemIndex}; + lut[lutCount++] = {fileOffset, paragraphIndex, listItemIndex, visibleTextOffset}; }, embeddedStyle, contentBase, imageBasePath, imageRendering, std::move(tocAnchors), popupFn, cssParser, renderMode, buildOptions.isPreview() ? std::string(buildOptions.previewAnchor) : std::string{}, buildOptions.previewMaxPages); Hyphenator::setPreferredLanguage(epub->getLanguage()); - LOG_DBG("SCT", "Parser start: spine=%d free=%u maxAlloc=%u", spineIndex, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); - const bool success = visitor.parseAndBuildPages(); + bool cancelled = false; + bool success = false; + if (cancelBuild()) { + cancelled = true; + } else { + success = visitor.beginParse(); + } + while (success && !cancelled) { + if (cancelBuild()) { + visitor.abortParse(); + cancelled = true; + break; + } + const auto status = visitor.parseStep(); + if (status == ChapterHtmlSlimParser::ParseStatus::Error) { + visitor.abortParse(); + success = false; + break; + } + if (status == ChapterHtmlSlimParser::ParseStatus::Done) { + if (cancelBuild()) { + visitor.abortParse(); + cancelled = true; + } else { + success = visitor.finishParse(); + } + break; + } + } LOG_DBG("SCT", "Parser done: spine=%d success=%u pages=%u free=%u maxAlloc=%u", spineIndex, success, pageCount, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); @@ -489,8 +683,12 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } } - if (!success || pageCompletionFailed) { - LOG_ERR("SCT", "Failed to parse XML and build pages"); + if (!success || pageCompletionFailed || cancelled) { + if (cancelled) { + LOG_DBG("SCT", "Section build cancelled during parse: spine=%d", spineIndex); + } else { + LOG_ERR("SCT", "Failed to parse XML and build pages"); + } // Explicitly close() file before calling Storage.remove() file.close(); Storage.remove(tmpSectionPath.c_str()); @@ -561,11 +759,21 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } } - // Patch header with final pageCount, lutOffset, anchorMapOffset, paragraphLutOffset, and liLutOffset. - if (!file.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(pageCount)) || + const uint32_t visibleTextLutOffset = static_cast(file.position()); + for (uint16_t i = 0; i < lutCount; i++) { + if (!serialization::tryWritePod(file, lut[i].visibleTextOffset)) { + file.close(); + Storage.remove(tmpSectionPath.c_str()); + return false; + } + } + + // Patch header with final pageCount and all cache lookup-table offsets. + if (!file.seek(HEADER_SIZE - sizeof(uint32_t) * 5 - sizeof(pageCount)) || !serialization::tryWritePod(file, pageCount) || !serialization::tryWritePod(file, lutOffset) || !serialization::tryWritePod(file, anchorMapOffset) || !serialization::tryWritePod(file, paragraphLutOffset) || - !serialization::tryWritePod(file, liLutFileOffset) || !file.sync()) { + !serialization::tryWritePod(file, liLutFileOffset) || !serialization::tryWritePod(file, visibleTextLutOffset) || + !file.sync()) { LOG_ERR("SCT", "Failed to finalize section cache"); file.close(); Storage.remove(tmpSectionPath.c_str()); @@ -576,10 +784,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } // Explicit close() required: member variable persists beyond function scope file.close(); - if (Storage.exists(filePath.c_str())) { - Storage.remove(filePath.c_str()); - } - if (!Storage.rename(tmpSectionPath.c_str(), filePath.c_str())) { + if (!promoteSectionCache(tmpSectionPath, filePath)) { LOG_ERR("SCT", "Failed to promote temp section cache into place"); Storage.remove(tmpSectionPath.c_str()); if (cssParser) { @@ -590,48 +795,605 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c if (cssParser) { cssParser->clear(); } - LOG_DBG("SCT", "Create section done: spine=%d pages=%u free=%u maxAlloc=%u", spineIndex, pageCount, ESP.getFreeHeap(), + return true; +} + +bool Section::startBuild(const ReaderRenderSpec& spec, const SectionBuildOptions buildOptions, + const std::function& popupFn) { + const int fontId = spec.fontId; + const float lineCompression = spec.lineCompression; + const bool extraParagraphSpacing = spec.extraParagraphSpacing; + const bool forceParagraphIndents = spec.forceParagraphIndents; + const uint8_t paragraphAlignment = spec.paragraphAlignment; + const uint16_t viewportWidth = spec.viewportWidth; + const uint16_t viewportHeight = spec.viewportHeight; + const bool hyphenationEnabled = spec.hyphenationEnabled; + const bool embeddedStyle = spec.embeddedStyle; + const uint8_t imageRendering = spec.imageRendering; + const bool bionicReadingEnabled = spec.bionicReadingEnabled; + const bool guideReadingEnabled = spec.guideReadingEnabled; + const uint8_t wordSpacing = spec.wordSpacing; + const EpubRenderMode renderMode = spec.renderMode; + if (build_) { + LOG_ERR("SCT", "startBuild called while a build is already active"); + return false; + } + + const auto localPath = epub->getSpineItem(spineIndex).href; + const auto htmlDir = epub->getCachePath() + "/html"; + const auto htmlPath = htmlDir + "/" + std::to_string(spineIndex) + ".html"; + const auto tmpHtmlPath = htmlDir + "/.tmp_" + std::to_string(spineIndex) + ".html"; + const auto tmpSectionPath = binTmpPath(); + builtPageCount_ = 0; + pageCount = partial_ ? partialPageCount_ : 0; + buildComplete_ = false; + lastImagesWereSuppressed_ = false; + lastLayoutAbortedForLowMemory_ = false; + + if (Storage.exists(tmpSectionPath.c_str())) { + Storage.remove(tmpSectionPath.c_str()); + } + + LOG_DBG("SCT", + "Start incremental section build: spine=%d mode=%u preview=%u viewport=%ux%u image=%u bionic=%u guide=%u " + "free=%u maxAlloc=%u", + spineIndex, static_cast(renderMode), buildOptions.isPreview() ? 1U : 0U, viewportWidth, + viewportHeight, imageRendering, bionicReadingEnabled, guideReadingEnabled, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + + { + const auto sectionsDir = epub->getCachePath() + "/sections"; + Storage.mkdir(sectionsDir.c_str()); + } + + const bool reusedHtml = Storage.exists(htmlPath.c_str()); + bool htmlCached = reusedHtml; + const auto cleanupTempHtml = [&]() { + if (!htmlCached && Storage.exists(tmpHtmlPath.c_str())) { + Storage.remove(tmpHtmlPath.c_str()); + } + }; + if (!reusedHtml) { + Storage.mkdir(htmlDir.c_str()); + + bool streamed = false; + uint32_t fileSize = 0; + for (int attempt = 0; attempt < 3 && !streamed; attempt++) { + if (attempt > 0) { + LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1); + delay(50); + } + if (Storage.exists(tmpHtmlPath.c_str())) { + Storage.remove(tmpHtmlPath.c_str()); + } + + HalFile tmpHtml; + if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) { + continue; + } + const size_t htmlStreamChunkSize = sectionHtmlStreamChunkSize(buildOptions.isPreview()); + prepareSectionZipInflate(renderer, fontId); + streamed = epub->readItemContentsToStream(localPath, tmpHtml, htmlStreamChunkSize); + fileSize = tmpHtml.size(); + tmpHtml.close(); + if (!streamed && Storage.exists(tmpHtmlPath.c_str())) { + Storage.remove(tmpHtmlPath.c_str()); + } + } + + if (!streamed) { + LOG_ERR("SCT", "Failed to stream item contents to temp file after retries"); + return false; + } + if (Storage.rename(tmpHtmlPath.c_str(), htmlPath.c_str())) { + htmlCached = true; + } else { + LOG_DBG("SCT", "Failed to promote HTML cache; parsing from temp"); + } + } + + if (!Storage.openFileForWrite("SCT", tmpSectionPath, file)) { + cleanupTempHtml(); + return false; + } + if (!writeSectionFileHeader(spec)) { + LOG_ERR("SCT", "Failed to write section header"); + file.close(); + Storage.remove(tmpSectionPath.c_str()); + cleanupTempHtml(); + return false; + } + + auto ctx = makeUniqueNoThrow(); + if (!ctx) { + LOG_ERR("SCT", "Failed to allocate section build context"); + lastLayoutAbortedForLowMemory_ = true; + file.close(); + Storage.remove(tmpSectionPath.c_str()); + cleanupTempHtml(); + return false; + } + ctx->lutCapacity = INITIAL_SECTION_PAGE_LUT_ENTRIES; + ctx->lut = makeUniqueNoThrow(ctx->lutCapacity); + if (!ctx->lut) { + LOG_ERR("SCT", "Failed to allocate incremental page LUT (%u bytes)", + static_cast(sizeof(Section::PageLutEntry) * ctx->lutCapacity)); + lastLayoutAbortedForLowMemory_ = true; + file.close(); + Storage.remove(tmpSectionPath.c_str()); + cleanupTempHtml(); + return false; + } + ctx->reusedHtml = htmlCached; + ctx->htmlPath = htmlPath; + ctx->tmpHtmlPath = tmpHtmlPath; + ctx->tmpSectionPath = tmpSectionPath; + ctx->parsePath = htmlCached ? htmlPath : tmpHtmlPath; + + const size_t lastSlash = localPath.find_last_of('/'); + ctx->contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : ""; + ctx->imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_"; + + if (embeddedStyle) { + ctx->cssParser = epub->getCssParser(); + if (ctx->cssParser) { + const auto cssHeapBefore = MemoryBudget::snapshot(); + const bool cssLoaded = ctx->cssParser->loadFromCache(); + const auto cssHeapAfter = MemoryBudget::snapshot(); + LOG_DBG("SCT", "CSS cache load: ok=%u partial=%u rules=%u free=%u->%u delta=%d maxAlloc=%u->%u delta=%d", + cssLoaded ? 1U : 0U, ctx->cssParser->isCachePartial() ? 1U : 0U, + static_cast(ctx->cssParser->ruleCount()), cssHeapBefore.freeHeap, cssHeapAfter.freeHeap, + static_cast(cssHeapAfter.freeHeap) - static_cast(cssHeapBefore.freeHeap), + cssHeapBefore.maxAllocHeap, cssHeapAfter.maxAllocHeap, + static_cast(cssHeapAfter.maxAllocHeap) - static_cast(cssHeapBefore.maxAllocHeap)); + if (!cssLoaded) { + LOG_ERR("SCT", "Failed to load CSS from cache"); + } + } + } + + std::vector tocAnchors; + const int startTocIndex = buildOptions.isPreview() ? -1 : epub->getTocIndexForSpineIndex(spineIndex); + if (startTocIndex >= 0) { + for (int i = startTocIndex; i < epub->getTocItemsCount(); i++) { + auto entry = epub->getTocItem(i); + if (entry.spineIndex != spineIndex) break; + if (!entry.anchor.empty()) { + tocAnchors.push_back(std::move(entry.anchor)); + } + } + } + + BuildContext* ctxPtr = ctx.get(); + ctx->parser = makeUniqueNoThrow( + *epub, ctxPtr->parsePath, renderer, fontId, lineCompression, extraParagraphSpacing, forceParagraphIndents, + paragraphAlignment, viewportWidth, viewportHeight, hyphenationEnabled, bionicReadingEnabled, guideReadingEnabled, + wordSpacing, + [this, ctxPtr](std::unique_ptr page, const uint16_t paragraphIndex, const uint16_t listItemIndex, + const uint32_t visibleTextOffset) { + if (ctxPtr->pageCompletionFailed) { + return; + } + if (ctxPtr->lutCount == UINT16_MAX || + !ensurePageLutCapacity(ctxPtr->lut, ctxPtr->lutCapacity, ctxPtr->lutCount)) { + LOG_ERR("SCT", "Failed to grow incremental section page LUT from %u entries", ctxPtr->lutCapacity); + ctxPtr->pageCompletionFailed = true; + lastLayoutAbortedForLowMemory_ = true; + return; + } + const uint32_t fileOffset = this->onPageComplete(std::move(page)); + if (fileOffset == 0) { + ctxPtr->pageCompletionFailed = true; + return; + } + ctxPtr->lut[ctxPtr->lutCount++] = {fileOffset, paragraphIndex, listItemIndex, visibleTextOffset}; + }, + embeddedStyle, ctxPtr->contentBase, ctxPtr->imageBasePath, imageRendering, std::move(tocAnchors), popupFn, + ctxPtr->cssParser, renderMode, buildOptions.isPreview() ? std::string(buildOptions.previewAnchor) : std::string{}, + buildOptions.previewMaxPages); + if (!ctx->parser) { + LOG_ERR("SCT", "Failed to allocate section parser"); + lastLayoutAbortedForLowMemory_ = true; + if (ctx->cssParser) ctx->cssParser->clear(); + file.close(); + Storage.remove(tmpSectionPath.c_str()); + cleanupTempHtml(); + return false; + } + + Hyphenator::setPreferredLanguage(epub->getLanguage()); + build_ = std::move(ctx); + if (!build_->parser->beginParse()) { + LOG_ERR("SCT", "Failed to begin incremental section parse"); + lastLayoutAbortedForLowMemory_ = build_->parser->wasLowMemoryAbortTriggered(); + if (lastLayoutAbortedForLowMemory_ && partial_) { + suspendBuild(); + } else { + abandonBuild(); + } + return false; + } + build_->totalBytes = build_->parser->parseTotalBytes(); return true; } +bool Section::buildSomeMore(const int maxPages) { + if (!build_ || !build_->parser) { + LOG_ERR("SCT", "buildSomeMore called with no active build"); + return false; + } + // Pace on pages laid out by THIS build, not pageCount: during a rebuild over a partial, + // pageCount stays pinned at the partial's watermark until the build passes it, which + // would otherwise turn one "small" chunk into a blocking rebuild of the whole watermark. + const int startCount = builtPageCount_; + for (;;) { + const auto status = build_->parser->parseStep(); + lastImagesWereSuppressed_ = lastImagesWereSuppressed_ || build_->parser->wasLowMemoryFallbackTriggered(); + lastLayoutAbortedForLowMemory_ = lastLayoutAbortedForLowMemory_ || build_->parser->wasLowMemoryAbortTriggered(); + if (build_->pageCompletionFailed || status == ChapterHtmlSlimParser::ParseStatus::Error) { + LOG_ERR("SCT", "Failed during incremental section build"); + // A low-memory replay over an existing partial may fail before rebuilding page 0. + // Suspend in that case too so suspendBuild() keeps the older readable partial. + if (lastLayoutAbortedForLowMemory_ && (builtPageCount_ > 0 || partial_)) { + suspendBuild(); + } else { + abandonBuild(); + } + return false; + } + if (status == ChapterHtmlSlimParser::ParseStatus::Done) { + return finalizeBuild(); + } + // ParseStatus::More: yield once we've laid out the requested number of pages. + if (maxPages > 0 && (builtPageCount_ - startCount) >= maxPages) { + build_->bytesConsumed = build_->parser->parseBytesConsumed(); + return true; + } + } +} + bool Section::hasHtmlCache() const { const std::string htmlPath = epub->getCachePath() + "/html/" + std::to_string(spineIndex) + ".html"; return Storage.exists(htmlPath.c_str()); } -std::unique_ptr Section::loadPageFromSectionFile() { +std::optional Section::findAnchorDuringBuild(const std::string& anchor) const { + if (!build_ || !build_->parser) return std::nullopt; + for (const auto& [key, page] : build_->parser->getAnchors()) { + if (key == anchor) return page; + } + return std::nullopt; +} + +std::optional Section::findAnchor(const std::string& anchor) const { + if (const auto page = findAnchorDuringBuild(anchor)) { + return page; + } + // Fall back to the on-disk anchor map: a finalized section, or a partial whose map + // covers everything up to its watermark (nullopt past it -- build further and retry). + return getPageForAnchor(anchor); +} + +uint16_t Section::estimatedTotalPages() const { + // Extrapolation from a suspended session's watermark trailer. A static snapshot, so no EMA + // damping is needed. Also the best guess while a rebuild is running but hasn't laid out + // enough pages yet to extrapolate from its own progress. + const auto partialEstimate = [this]() -> uint16_t { + if (!partial_ || partialBytesConsumed_ == 0 || partialTotalBytes_ <= partialBytesConsumed_) { + return pageCount; + } + const uint64_t est = static_cast(partialPageCount_) * partialTotalBytes_ / partialBytesConsumed_; + if (est <= pageCount) return pageCount; + return est > 60000 ? 60000 : static_cast(est); + }; + + if (!build_) { + return partial_ ? partialEstimate() : pageCount; // partial -> extrapolate, finalized -> exact + } + const uint32_t consumed = build_->bytesConsumed; + const uint32_t total = build_->totalBytes; + if (builtPageCount_ == 0 || consumed == 0 || total <= consumed) return partialEstimate(); + + // Raw extrapolation: scale the pages built so far by the fraction of HTML still unparsed. This + // re-derives from a growing, non-uniform sample, so it jitters up and down as the build crosses + // dense vs sparse regions of the chapter. + const uint64_t raw = static_cast(builtPageCount_) * total / consumed; + + // Damp that jitter with an exponential moving average. Step it once per build advance (keyed on + // bytesConsumed) rather than per status-bar redraw, so the smoothing rate doesn't depend on how + // often we repaint. As the build nears the end, consumed -> total and raw -> the built count, so + // the average settles onto the true count (and finalizeBuild then returns the exact pageCount). + constexpr float ALPHA = 0.25f; // weight of each new sample; lower = steadier but slower to settle + if (build_->smoothedEstimate <= 0) { + build_->smoothedEstimate = static_cast(raw); // seed on the first estimate + } else if (consumed != build_->smoothedAtConsumed) { + build_->smoothedEstimate += ALPHA * (static_cast(raw) - build_->smoothedEstimate); + } + build_->smoothedAtConsumed = consumed; + + const uint64_t est = static_cast(build_->smoothedEstimate + 0.5f); + if (est <= pageCount) return pageCount; // never fewer than the pages already available + return est > 60000 ? 60000 : static_cast(est); +} + +// Write the LUTs and anchor map into the open tmp .bin, patch the header with the built +// page count and table offsets, stamp `version` as the commit point, then swap the tmp +// file over filePath. For SECTION_FILE_PARTIAL_VERSION a watermark trailer +// (bytesConsumed, totalBytes) is appended after the li LUT so a later open can estimate +// the total page count. The parser must still be alive (anchors are read from it). +// On failure the tmp is removed and any pre-existing file at filePath is left intact. +bool Section::commitBuildFile(const uint8_t version, const uint32_t bytesConsumed, const uint32_t totalBytes) { + const bool asPartial = (version == SECTION_FILE_PARTIAL_VERSION); + + const auto failCommit = [this]() { + // Explicit close() required before remove (member variable, O_RDWR handle). + file.close(); + if (build_ && !build_->tmpSectionPath.empty()) { + Storage.remove(build_->tmpSectionPath.c_str()); + } + return false; + }; + + if (!ensureBuildFileOpen()) { + return failCommit(); + } + + const uint32_t lutOffset = file.position(); + for (uint16_t i = 0; i < build_->lutCount; i++) { + if (build_->lut[i].fileOffset == 0 || !serialization::tryWritePod(file, build_->lut[i].fileOffset)) { + LOG_ERR("SCT", "Failed to write LUT due to invalid page positions"); + return failCommit(); + } + } + + // Write anchor-to-page map for fragment navigation (e.g. footnote targets). For a + // partial, skip anchors that landed on the incomplete trailing page the suspend drops. + const uint32_t anchorMapOffset = file.position(); + const auto& anchors = build_->parser->getAnchors(); + uint16_t anchorCount = 0; + for (const auto& [anchor, page] : anchors) { + if (!asPartial || page < builtPageCount_) anchorCount++; + } + if (!serialization::tryWritePod(file, anchorCount)) { + return failCommit(); + } + for (const auto& [anchor, page] : anchors) { + if (asPartial && page >= builtPageCount_) continue; + if (!serialization::tryWriteString(file, anchor) || !serialization::tryWritePod(file, page)) { + return failCommit(); + } + } + + const uint32_t paragraphLutOffset = file.position(); + if (!serialization::tryWritePod(file, build_->lutCount)) { + return failCommit(); + } + for (uint16_t i = 0; i < build_->lutCount; i++) { + if (!serialization::tryWritePod(file, build_->lut[i].paragraphIndex)) { + return failCommit(); + } + } + + const uint32_t liLutFileOffset = static_cast(file.position()); + for (uint16_t i = 0; i < build_->lutCount; i++) { + if (!serialization::tryWritePod(file, build_->lut[i].listItemIndex)) { + return failCommit(); + } + } + + const uint32_t visibleTextLutOffset = static_cast(file.position()); + for (uint16_t i = 0; i < build_->lutCount; i++) { + if (!serialization::tryWritePod(file, build_->lut[i].visibleTextOffset)) { + return failCommit(); + } + } + + if (asPartial) { + // Watermark trailer follows the page-start visible-text LUT. + if (!serialization::tryWritePod(file, bytesConsumed) || !serialization::tryWritePod(file, totalBytes)) { + return failCommit(); + } + } + + // Patch header with the built page count and section offsets... + if (!file.seek(HEADER_SIZE - sizeof(uint32_t) * 5 - sizeof(builtPageCount_)) || + !serialization::tryWritePod(file, builtPageCount_) || !serialization::tryWritePod(file, lutOffset) || + !serialization::tryWritePod(file, anchorMapOffset) || !serialization::tryWritePod(file, paragraphLutOffset) || + !serialization::tryWritePod(file, liLutFileOffset) || !serialization::tryWritePod(file, visibleTextLutOffset) || + !file.seek(sizeof(SECTION_CACHE_MAGIC)) || !serialization::tryWritePod(file, version) || !file.sync()) { + LOG_ERR("SCT", "Failed to commit section cache"); + return failCommit(); + } + // Explicit close() required: member variable persists beyond function scope + file.close(); + + // Keep the readable cache as a backup until the completed replacement is in + // place. A reboot after the backup rename recovers it in the constructor. + if (!promoteSectionCache(build_->tmpSectionPath, filePath)) { + LOG_ERR("SCT", "Failed to move built section into place"); + Storage.remove(build_->tmpSectionPath.c_str()); + return false; + } + return true; +} + +bool Section::finalizeBuild() { + if (!build_ || !build_->parser) { + return false; + } + + const bool success = build_->parser->finishParse(); + lastImagesWereSuppressed_ = lastImagesWereSuppressed_ || build_->parser->wasLowMemoryFallbackTriggered(); + lastLayoutAbortedForLowMemory_ = lastLayoutAbortedForLowMemory_ || build_->parser->wasLowMemoryAbortTriggered(); + if (!success || build_->pageCompletionFailed) { + LOG_ERR("SCT", "Failed to finalize parser output"); + abandonBuild(); + return false; + } + + const bool committed = commitBuildFile(SECTION_FILE_VERSION, 0, 0); + if (build_->cssParser) build_->cssParser->clear(); + if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) { + Storage.remove(build_->tmpHtmlPath.c_str()); + } + build_.reset(); + if (!committed) { + // The previous cache was retained/restored by promoteSectionCache(). Keep + // its in-memory watermark too so this Section remains readable. + pageCount = partial_ ? partialPageCount_ : 0; + builtPageCount_ = 0; + return false; + } + buildComplete_ = true; + partial_ = false; + partialPageCount_ = 0; + pageCount = builtPageCount_; + return true; +} + +void Section::suspendBuild() { + if (!build_) return; + + // Only worth persisting if this build produced pages a pre-existing partial doesn't + // already cover; otherwise keep the older (bigger) partial and just drop the tmp. + const bool worthKeeping = builtPageCount_ > 0 && (!partial_ || builtPageCount_ > partialPageCount_); + + bool committed = false; + if (worthKeeping) { + // Capture the parse watermark and commit before tearing the parser down (the anchor + // map is read from it). The incomplete trailing page is intentionally not flushed: + // only fully laid-out pages are persisted, and the rebuild re-derives the rest. + const uint32_t consumed = static_cast(build_->parser->parseBytesConsumed()); + committed = commitBuildFile(SECTION_FILE_PARTIAL_VERSION, consumed, build_->totalBytes); + if (committed) { + partial_ = true; + partialPageCount_ = builtPageCount_; + partialBytesConsumed_ = consumed; + partialTotalBytes_ = build_->totalBytes; + LOG_INF("SCT", "Suspended build: %u pages persisted", builtPageCount_); + } + } + + if (build_->parser) build_->parser->abortParse(); + if (build_->cssParser) build_->cssParser->clear(); + if (!committed && file) { + // Explicit close() required before remove (member variable, O_RDWR handle). + file.close(); + } + if (!committed && Storage.exists(binTmpPath().c_str())) { + Storage.remove(binTmpPath().c_str()); + } + if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) { + Storage.remove(build_->tmpHtmlPath.c_str()); + } + build_.reset(); + buildComplete_ = false; + pageCount = partial_ ? partialPageCount_ : 0; + builtPageCount_ = 0; +} + +void Section::abandonBuild() { + if (!build_) return; + if (build_->parser) { + build_->parser->abortParse(); + } + if (build_->cssParser) { + build_->cssParser->clear(); + } + if (file) { + file.close(); + } + if (!build_->tmpSectionPath.empty() && Storage.exists(build_->tmpSectionPath.c_str())) { + Storage.remove(build_->tmpSectionPath.c_str()); + } + // A parse error would recur against the same HTML, so drop any partial too -- resuming + // from it would just re-enter the failing build every open. + if (Storage.exists(filePath.c_str())) { + Storage.remove(filePath.c_str()); + } + if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) { + Storage.remove(build_->tmpHtmlPath.c_str()); + } + build_.reset(); + buildComplete_ = false; + partial_ = false; + partialPageCount_ = 0; + pageCount = 0; + builtPageCount_ = 0; +} + +std::unique_ptr Section::loadPageDuringBuild(const int page) { + if (!build_ || page < 0 || page >= static_cast(build_->lutCount)) { + return nullptr; + } + const uint32_t pos = build_->lut[page].fileOffset; + if (pos == 0) { + return nullptr; + } + if (!file) { - if (!Storage.openFileForRead("SCT", filePath, file)) { + HalFile tmp; + if (!Storage.openFileForRead("SCT", build_->tmpSectionPath, tmp) || !tmp.seek(pos)) { return nullptr; } + return Page::deserialize(tmp); } - auto closeAndReturnNull = [this]() -> std::unique_ptr { - file.close(); + const uint32_t writePos = file.position(); + if (!file.seek(pos)) { return nullptr; - }; + } + auto pageData = Page::deserialize(file); + file.seek(writePos); + return pageData; +} - if (!file.seek(HEADER_SIZE - sizeof(uint32_t) * 4)) { - return closeAndReturnNull(); +// Read a page from the committed file at filePath (finalized section or partial from a +// previous session). Uses a local handle so it is safe while a build holds the member +// `file` open on the tmp .bin. +std::unique_ptr Section::loadPageAt(const int page) const { + HalFile f; + if (!Storage.openFileForRead("SCT", filePath, f)) { + return nullptr; + } + + if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 5)) { + return nullptr; } uint32_t lutOffset; - if (!serialization::tryReadPod(file, lutOffset) || !file.seek(lutOffset + sizeof(uint32_t) * currentPage)) { - return closeAndReturnNull(); + if (!serialization::tryReadPod(f, lutOffset) || !f.seek(lutOffset + sizeof(uint32_t) * page)) { + return nullptr; } uint32_t pagePos; - if (!serialization::tryReadPod(file, pagePos) || !file.seek(pagePos)) { - return closeAndReturnNull(); + if (!serialization::tryReadPod(f, pagePos) || !f.seek(pagePos)) { + return nullptr; } - auto page = Page::deserialize(file); - file.close(); - return page; + return Page::deserialize(f); + // No f.close() needed -- DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit +} + +std::unique_ptr Section::loadPage(const int page) { + if (page < 0) { + return nullptr; + } + if (build_ && page < static_cast(build_->lutCount)) { + return loadPageDuringBuild(page); + } + // Not (yet) in the active build: serve from the file on disk -- a finalized section, + // or a partial from a previous session whose pages the rebuild hasn't reached again. + const int onDisk = partial_ ? partialPageCount_ : (build_ ? 0 : pageCount); + if (page >= onDisk) { + return nullptr; + } + return loadPageAt(page); } +std::unique_ptr Section::loadPageFromSectionFile() { return loadPage(currentPage); } + std::string Section::getTextFromSectionFile() { std::string fullText; - auto p = loadPageFromSectionFile(); + auto p = loadPage(currentPage); if (p) { for (const auto& el : p->elements) { if (el->getTag() == TAG_PageLine) { @@ -660,6 +1422,9 @@ std::optional Section::getCachedPageCount() const { return std::nullopt; } + // Only a finalized section's count is the chapter total; a partial's count is just the + // suspended build's watermark, which would skew progress mapping. Callers fall back to + // their own estimates. uint32_t magic; if (!serialization::tryReadPod(f, magic) || magic != SECTION_CACHE_MAGIC) { return std::nullopt; @@ -672,7 +1437,7 @@ std::optional Section::getCachedPageCount() const { return std::nullopt; } - if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t))) { + if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 5 - sizeof(uint16_t))) { return std::nullopt; } uint16_t count; @@ -689,7 +1454,7 @@ std::optional Section::getPageForAnchor(const std::string& anchor) con } const uint32_t fileSize = f.size(); - if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 3)) { + if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 4)) { return std::nullopt; } uint32_t anchorMapOffset; @@ -722,13 +1487,17 @@ std::optional Section::getPageForAnchor(const std::string& anchor) con } std::optional Section::getPageForParagraphIndex(const uint16_t pIndex) const { + if (const auto page = findParagraphDuringBuild(pIndex)) { + return page; + } + FsFile f; if (!Storage.openFileForRead("SCT", filePath, f)) { return std::nullopt; } const uint32_t fileSize = f.size(); - if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 2)) { + if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 3)) { return std::nullopt; } uint32_t paragraphLutOffset; @@ -770,14 +1539,29 @@ std::optional Section::getPageForParagraphIndex(const uint16_t pIndex) return resultPage; } +std::optional Section::findParagraphDuringBuild(const uint16_t pIndex) const { + if (build_) { + for (uint16_t i = 0; i < build_->lutCount; i++) { + if (build_->lut[i].paragraphIndex >= pIndex) { + return i; + } + } + } + return std::nullopt; +} + std::optional Section::getParagraphIndexForPage(const uint16_t page) const { + if (build_ && page < build_->lutCount) { + return build_->lut[page].paragraphIndex; + } + FsFile f; if (!Storage.openFileForRead("SCT", filePath, f)) { return std::nullopt; } const uint32_t fileSize = f.size(); - if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 2)) { + if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 3)) { return std::nullopt; } uint32_t paragraphLutOffset; @@ -815,13 +1599,17 @@ std::optional Section::getParagraphIndexForPage(const uint16_t page) c } std::optional Section::getListItemIndexForPage(const uint16_t page) const { + if (build_ && page < build_->lutCount) { + return build_->lut[page].listItemIndex; + } + FsFile f; if (!Storage.openFileForRead("SCT", filePath, f)) { return std::nullopt; } const uint32_t fileSize = f.size(); - if (!f.seek(HEADER_SIZE - sizeof(uint32_t))) { + if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 2)) { return std::nullopt; } uint32_t liLutOffset; @@ -832,7 +1620,7 @@ std::optional Section::getListItemIndexForPage(const uint16_t page) co return std::nullopt; } - if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 2)) { + if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 3)) { return std::nullopt; } uint32_t paragraphLutOffset; @@ -876,7 +1664,7 @@ std::optional Section::getPageForListItemIndex(const uint16_t liIndex) } const uint32_t fileSize = f.size(); - if (!f.seek(HEADER_SIZE - sizeof(uint32_t))) { + if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 2)) { return std::nullopt; } uint32_t liLutOffset; @@ -888,7 +1676,7 @@ std::optional Section::getPageForListItemIndex(const uint16_t liIndex) } // The li LUT shares count with the paragraph LUT; read count from paragraphLutOffset - if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 2)) { + if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 3)) { return std::nullopt; } uint32_t paragraphLutOffset; @@ -932,3 +1720,77 @@ std::optional Section::getPageForListItemIndex(const uint16_t liIndex) return resultPage; } + +std::optional Section::getVisibleTextOffsetForPage(const uint16_t page) const { + if (build_ && page < build_->lutCount) { + return build_->lut[page].visibleTextOffset; + } + + FsFile f; + if (!Storage.openFileForRead("SCT", filePath, f)) return std::nullopt; + const uint32_t fileSize = f.size(); + if (!f.seek(HEADER_SIZE - sizeof(uint32_t) * 5 - sizeof(uint16_t))) return std::nullopt; + uint16_t count = 0; + if (!serialization::tryReadPod(f, count) || page >= count) return std::nullopt; + if (!f.seek(HEADER_SIZE - sizeof(uint32_t))) return std::nullopt; + uint32_t lutOffset = 0; + if (!serialization::tryReadPod(f, lutOffset) || lutOffset < HEADER_SIZE || + lutOffset + (static_cast(page) + 1U) * sizeof(uint32_t) > fileSize || + !f.seek(lutOffset + static_cast(page) * sizeof(uint32_t))) { + return std::nullopt; + } + uint32_t offset = 0; + return serialization::tryReadPod(f, offset) ? std::optional(offset) : std::nullopt; +} + +std::optional Section::getPageForVisibleTextOffset(const uint32_t offset, + const bool preferFirstAtOffset) const { + if (build_ && build_->lutCount > 0) { + uint16_t result = 0; + for (uint16_t i = 0; i < build_->lutCount; i++) { + const uint32_t start = build_->lut[i].visibleTextOffset; + if (start > offset) break; + result = i; + if (preferFirstAtOffset && start == offset) break; + } + const uint32_t last = build_->lut[build_->lutCount - 1].visibleTextOffset; + if (offset <= last) return result; + // An extension build starts from page zero while its previously committed + // partial remains readable. Its shorter live prefix must not hide a page + // that the committed cache already knows how to resolve. + } + + FsFile f; + if (!Storage.openFileForRead("SCT", filePath, f)) return std::nullopt; + const uint32_t fileSize = f.size(); + uint32_t magic = 0; + uint8_t version = 0; + if (!serialization::tryReadPod(f, magic) || magic != SECTION_CACHE_MAGIC || !serialization::tryReadPod(f, version) || + (version != SECTION_FILE_VERSION && version != SECTION_FILE_PARTIAL_VERSION) || + !f.seek(HEADER_SIZE - sizeof(uint32_t) * 5 - sizeof(uint16_t))) { + return std::nullopt; + } + uint16_t count = 0; + if (!serialization::tryReadPod(f, count) || count == 0 || !f.seek(HEADER_SIZE - sizeof(uint32_t))) { + return std::nullopt; + } + uint32_t lutOffset = 0; + if (!serialization::tryReadPod(f, lutOffset) || lutOffset < HEADER_SIZE || + lutOffset + static_cast(count) * sizeof(uint32_t) > fileSize) { + return std::nullopt; + } + uint16_t result = 0; + uint32_t last = 0; + for (uint16_t i = 0; i < count; i++) { + uint32_t start = 0; + if (!f.seek(lutOffset + static_cast(i) * sizeof(uint32_t)) || !serialization::tryReadPod(f, start)) { + return std::nullopt; + } + last = start; + if (start > offset) break; + result = i; + if (preferFirstAtOffset && start == offset) break; + } + if (version == SECTION_FILE_PARTIAL_VERSION && offset > last) return std::nullopt; + return result; +} diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 3fe91cf7b2..d0e4a784b2 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -6,28 +6,91 @@ #include "Epub.h" #include "EpubRenderMode.h" +#include "ReaderRenderSpec.h" class Page; class GfxRenderer; +class ChapterHtmlSlimParser; +class CssParser; struct SectionBuildOptions { const char* previewAnchor = nullptr; uint16_t previewMaxPages = 0; + // Full-section callers can stop between parser chunks without creating a + // readable partial cache. The callback must remain valid for the build. + bool (*shouldCancel)(void* context) = nullptr; + void* cancelContext = nullptr; + bool* cancellationObserved = nullptr; bool isPreview() const { return previewAnchor && previewAnchor[0] != '\0' && previewMaxPages > 0; } + bool isCancellationRequested() const { return shouldCancel && shouldCancel(cancelContext); } }; class Section { - std::shared_ptr epub; + std::shared_ptr epubOwner; + Epub* epub; const int spineIndex; GfxRenderer& renderer; std::string filePath; HalFile file; - bool writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, bool forceParagraphIndents, - uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, - bool hyphenationEnabled, bool embeddedStyle, uint8_t imageRendering, - bool bionicReadingEnabled, bool guideReadingEnabled, EpubRenderMode renderMode); + + struct PageLutEntry { + uint32_t fileOffset; + uint16_t paragraphIndex; + uint16_t listItemIndex; + uint32_t visibleTextOffset; + }; + + struct BuildContext { + std::unique_ptr parser; + std::unique_ptr lut; + uint16_t lutCapacity = 0; + uint16_t lutCount = 0; + std::string parsePath; + std::string contentBase; + std::string imageBasePath; + std::string htmlPath; + std::string tmpHtmlPath; + std::string tmpSectionPath; + bool reusedHtml = false; + bool pageCompletionFailed = false; + CssParser* cssParser = nullptr; + // HTML byte progress, for estimating the section's total page count while it's still building. + uint32_t bytesConsumed = 0; + uint32_t totalBytes = 0; + // Exponentially-smoothed page-count estimate (0 = not yet seeded) and the bytesConsumed at its + // last update. The raw byte-ratio estimate jitters as the build crosses dense/sparse regions; + // the EMA is stepped once per build advance (not per redraw) to damp that wobble. + float smoothedEstimate = 0; + uint32_t smoothedAtConsumed = 0; + }; + std::unique_ptr build_; + bool buildComplete_ = false; + bool lastImagesWereSuppressed_ = false; + bool lastLayoutAbortedForLowMemory_ = false; + // Pages laid out by the active build. Distinct from pageCount, which is the pages + // available to read and may include a loaded partial file's pages. + uint16_t builtPageCount_ = 0; + bool partial_ = false; + uint16_t partialPageCount_ = 0; + uint32_t partialBytesConsumed_ = 0; + uint32_t partialTotalBytes_ = 0; + std::string activeBuildTmpSectionPath_; + + bool writeSectionFileHeader(const ReaderRenderSpec& spec); uint32_t onPageComplete(std::unique_ptr page); + bool ensureBuildFileOpen(); + bool finalizeBuild(); + // Write the LUTs/anchor map (and, for a partial, the watermark trailer), patch the + // header, stamp the version byte, and swap the tmp .bin over filePath. + bool commitBuildFile(uint8_t version, uint32_t bytesConsumed, uint32_t totalBytes); + // Builds write here and are swapped over filePath only on commit, so a prior + // partial/finalized file stays readable while a rebuild is in progress. + std::string binTmpPath() const { return filePath + ".part"; } + std::unique_ptr loadPageAt(int page) const; + // Read a page already laid out by the in-progress build (page < build LUT size), from + // the partially-written tmp .bin without disturbing the build's write cursor. + std::unique_ptr loadPageDuringBuild(int page); public: uint16_t pageCount = 0; @@ -35,36 +98,70 @@ class Section { explicit Section(const std::shared_ptr& epub, int spineIndex, GfxRenderer& renderer, const char* cacheSuffix = ""); - ~Section() = default; - bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, bool forceParagraphIndents, - uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, - bool hyphenationEnabled, bool embeddedStyle, uint8_t imageRendering, bool bionicReadingEnabled, - bool guideReadingEnabled, EpubRenderMode renderMode); + explicit Section(Epub& epub, int spineIndex, GfxRenderer& renderer, const char* cacheSuffix = ""); + ~Section(); + bool loadSectionFile(const ReaderRenderSpec& spec); bool clearCache() const; - bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, bool forceParagraphIndents, - uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, - bool hyphenationEnabled, bool embeddedStyle, uint8_t imageRendering, bool bionicReadingEnabled, - bool guideReadingEnabled, const std::function& popupFn = nullptr, + bool createSectionFile(const ReaderRenderSpec& spec, const std::function& popupFn = nullptr, bool* imagesWereSuppressed = nullptr, bool* layoutAbortedForLowMemory = nullptr, - EpubRenderMode renderMode = EpubRenderMode::CrossInkDefault, SectionBuildOptions buildOptions = {}); + bool startBuild(const ReaderRenderSpec& spec, SectionBuildOptions buildOptions = {}, + const std::function& popupFn = nullptr); + bool buildSomeMore(int maxPages); + void releaseBuildFile(); + bool lastBuildImagesWereSuppressed() const { return lastImagesWereSuppressed_; } + bool lastBuildLayoutAbortedForLowMemory() const { return lastLayoutAbortedForLowMemory_; } + bool isBuilding() const { return static_cast(build_); } + bool isBuildComplete() const { return buildComplete_; } + bool activeBuildHasCaughtReadablePages() const { return !build_ || builtPageCount_ >= pageCount; } + // Best-known total page count: the exact pageCount once finalized, or a smoothed byte-based + // estimate (pages so far scaled by totalBytes/bytesConsumed, damped by an EMA) while a giant spine + // is still building, so "page X of Y" / progress don't read off the small build watermark. + uint16_t estimatedTotalPages() const; + void abandonBuild(); + // Persist an in-progress build as a partial section file (version sentinel + LUTs + + // watermark trailer) instead of discarding it, so the next open of this spine can show + // its pages instantly and only rebuild in the background. Called by the destructor, so + // any teardown path (exit, sleep, navigation) keeps the work already done. Keeps a + // pre-existing partial when it covers more pages than this build reached. + void suspendBuild(); + // True when a partial file was loaded: pageCount is a watermark, not the chapter total. + bool isPartial() const { return partial_; } + + // Unified page read: from the active build if it has reached the page, otherwise from + // the on-disk file (finalized section, or a partial the rebuild hasn't caught up to). + std::unique_ptr loadPage(int page); + std::unique_ptr loadPageFromSectionFile(); std::string getTextFromSectionFile(); + // Resolve an anchor from the in-progress build first, then the on-disk anchor map + // (covers finalized sections and partials from a previous session). + std::optional findAnchor(const std::string& anchor) const; + // True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a - // giant spine) zip inflation. + // giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild. bool hasHtmlCache() const; // Look up the page number for an anchor id from the section cache file. std::optional getPageForAnchor(const std::string& anchor) const; + // Look up an anchor among the pages built so far by the in-progress build, so an anchor jump + // (TOC / chapter select, usually the chapter top = page 0) can resolve without laying out the + // whole chapter. Returns nullopt if the anchor hasn't been reached yet (build more) or no build. + std::optional findAnchorDuringBuild(const std::string& anchor) const; + // Get the page count from the section cache file without fully loading it. std::optional getCachedPageCount() const; // Look up the page number for a synthetic paragraph index from XPath p[N]. + // Checks the active incremental build before falling back to the committed cache. std::optional getPageForParagraphIndex(uint16_t pIndex) const; + // Look up a synthetic paragraph among pages produced by the active build only. + std::optional findParagraphDuringBuild(uint16_t pIndex) const; + // Look up the page number for a running list-item index from the li LUT. std::optional getPageForListItemIndex(uint16_t liIndex) const; @@ -73,4 +170,9 @@ class Section { // Look up the running list-item index for the given rendered page. std::optional getListItemIndexForPage(uint16_t page) const; + + // Content coordinate recorded at the start of each rendered page. Available + // for finalized sections and the readable prefix of incremental sections. + std::optional getVisibleTextOffsetForPage(uint16_t page) const; + std::optional getPageForVisibleTextOffset(uint32_t offset, bool preferFirstAtOffset = false) const; }; diff --git a/lib/Epub/Epub/blocks/BlockStyle.h b/lib/Epub/Epub/blocks/BlockStyle.h index cd0d243275..ca9b0bfc49 100644 --- a/lib/Epub/Epub/blocks/BlockStyle.h +++ b/lib/Epub/Epub/blocks/BlockStyle.h @@ -53,6 +53,14 @@ struct BlockStyle { return result; } + // Return a copy with top margins/padding zeroed out. + [[nodiscard]] BlockStyle withoutTop() const { + BlockStyle result = *this; + result.marginTop = 0; + result.paddingTop = 0; + return result; + } + // Return a copy with bottom margins/padding collapsed (max) with the source's. // Uses CSS margin collapsing: adjacent parent-child margins resolve to the larger value. [[nodiscard]] BlockStyle addBottom(const BlockStyle& source) const { diff --git a/lib/Epub/Epub/blocks/ImageBlock.cpp b/lib/Epub/Epub/blocks/ImageBlock.cpp index 3b1f008a91..323029a02a 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.cpp +++ b/lib/Epub/Epub/blocks/ImageBlock.cpp @@ -5,6 +5,9 @@ #include #include +#include +#include + #include "Epub/converters/DirectPixelWriter.h" #include "Epub/converters/ImageDecoderFactory.h" @@ -13,8 +16,16 @@ // - 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) {} +ImageBlock::ImageBlock(std::string imagePath, std::string sourcePath, int16_t width, int16_t height) + : imagePath(std::move(imagePath)), sourcePath(std::move(sourcePath)), width(width), height(height) {} + +void* ImageBlock::extractContext = nullptr; +ImageBlock::ExtractFn ImageBlock::extractFn = nullptr; + +void ImageBlock::setExtractor(void* context, ExtractFn fn) { + extractContext = context; + extractFn = fn; +} bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); } @@ -58,6 +69,54 @@ void clampCachedRowsToLandscapeStrip(const GfxRenderer& renderer, const int imag if (rowEnd > stripRowEnd) rowEnd = stripRowEnd; } +bool readValidCacheHeader(FsFile& cacheFile, const int expectedWidth, const int expectedHeight, uint16_t& cachedWidth, + uint16_t& cachedHeight) { + if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) { + return false; + } + + const int widthDiff = abs(cachedWidth - expectedWidth); + const int heightDiff = abs(cachedHeight - expectedHeight); + if (widthDiff > 1 || heightDiff > 1) { + return false; + } + + const size_t bytesPerRow = (cachedWidth + 3) / 4; + const size_t expectedSize = 4 + bytesPerRow * cachedHeight; + return cacheFile.size() >= expectedSize; +} + +// Pages are deserialized afresh on each visit. Keep a bounded, allocation-free +// record so an image that failed renders its placeholder directly for the rest +// of the reader session instead of paying another placeholder refresh and +// decode. The reader clears this on entry so transient memory/storage failures +// are retried. +constexpr size_t MAX_SESSION_IMAGE_FAILURES = 16; +uint64_t failedImageHashes[MAX_SESSION_IMAGE_FAILURES]; +size_t failedImageCount = 0; + +uint64_t imagePathHash(const std::string& path) { + uint64_t hash = 14695981039346656037ull; + for (const char c : path) { + hash ^= static_cast(c); + hash *= 1099511628211ull; + } + return hash; +} + +bool imageFailedThisSession(const std::string& path) { + const uint64_t hash = imagePathHash(path); + for (size_t i = 0; i < failedImageCount; i++) { + if (failedImageHashes[i] == hash) return true; + } + return false; +} + +void rememberImageFailure(const std::string& path) { + if (failedImageCount == MAX_SESSION_IMAGE_FAILURES || imageFailedThisSession(path)) return; + failedImageHashes[failedImageCount++] = imagePathHash(path); +} + bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth, int expectedHeight) { FsFile cacheFile; @@ -66,18 +125,8 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, } uint16_t cachedWidth, cachedHeight; - if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) { - cacheFile.close(); - return false; - } - - // Verify dimensions are close (allow 1 pixel tolerance for rounding differences) - int widthDiff = abs(cachedWidth - expectedWidth); - int heightDiff = abs(cachedHeight - expectedHeight); - if (widthDiff > 1 || heightDiff > 1) { - LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth, - expectedHeight); - cacheFile.close(); + if (!readValidCacheHeader(cacheFile, expectedWidth, expectedHeight, cachedWidth, cachedHeight)) { + LOG_ERR("IMG", "Invalid image cache: %s", cachePath.c_str()); return false; } @@ -85,8 +134,6 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, expectedWidth = cachedWidth; expectedHeight = cachedHeight; - LOG_DBG("IMG", "Loading from cache: %s (%dx%d)", cachePath.c_str(), cachedWidth, cachedHeight); - const int screenWidth = renderer.getScreenWidth(); const int screenHeight = renderer.getScreenHeight(); int clipXStart = 0; @@ -183,13 +230,36 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, free(readBuffer); cacheFile.close(); - LOG_DBG("IMG", "Cache render complete"); return true; } } // namespace -void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { +bool ImageBlock::hasValidCache() const { + const auto cachePath = getCachePath(imagePath); + FsFile cacheFile; + if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) { + return false; + } + + uint16_t cachedWidth, cachedHeight; + const bool valid = readValidCacheHeader(cacheFile, width, height, cachedWidth, cachedHeight); + cacheFile.close(); + return valid; +} + +bool ImageBlock::needsDecode() const { return !imageFailedThisSession(imagePath) && !hasValidCache(); } + +void ImageBlock::clearSessionRenderFailures() { failedImageCount = 0; } + +void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y, const bool foregroundBlack) const { + renderer.fillRect(x, y, width, height, foregroundBlack); + if (width > 2 && height > 2) { + renderer.fillRect(x + 1, y + 1, width - 2, height - 2, !foregroundBlack); + } +} + +void ImageBlock::render(GfxRenderer& renderer, const int x, const int y, const bool foregroundBlack) { // The font-prewarm scan pass only accumulates glyphs; an image contributes // none, and its DirectPixelWriter output bypasses the renderer's scan-mode // suppression, so it would otherwise do a full (discarded) cache render every @@ -198,8 +268,6 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { FontCacheManager* fcm = renderer.getFontCacheManager(); if (fcm && fcm->isScanning()) return; - LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height); - const int screenWidth = renderer.getScreenWidth(); const int screenHeight = renderer.getScreenHeight(); @@ -227,17 +295,30 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { return; } + if (imageFailedThisSession(imagePath)) { + renderPlaceholder(renderer, x, y, foregroundBlack); + 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 } + if (!sourcePath.empty() && extractFn && !Storage.exists(imagePath.c_str())) { + if (!extractFn(extractContext, sourcePath.c_str(), imagePath.c_str())) { + LOG_ERR("IMG", "Lazy extraction failed: %s", sourcePath.c_str()); + } + } + // No cache - need to decode the image // Check if image file exists FsFile file; if (!Storage.openFileForRead("IMG", imagePath, file)) { LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str()); + rememberImageFailure(imagePath); + renderPlaceholder(renderer, x, y, foregroundBlack); return; } size_t fileSize = file.size(); @@ -245,11 +326,11 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { if (fileSize == 0) { LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str()); + rememberImageFailure(imagePath); + renderPlaceholder(renderer, x, y, foregroundBlack); return; } - LOG_DBG("IMG", "Decoding and caching: %s", imagePath.c_str()); - RenderConfig config; config.x = x; config.y = y; @@ -266,23 +347,23 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) { ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath); if (!decoder) { LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str()); + rememberImageFailure(imagePath); + renderPlaceholder(renderer, x, y, foregroundBlack); return; } - LOG_DBG("IMG", "Using %s decoder", decoder->getFormatName()); - bool success = decoder->decodeToFramebuffer(imagePath, renderer, config); if (!success) { LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str()); + rememberImageFailure(imagePath); + renderPlaceholder(renderer, x, y, foregroundBlack); return; } - - LOG_DBG("IMG", "Decode successful"); } bool ImageBlock::serialize(FsFile& file) { - return serialization::tryWriteString(file, imagePath) && serialization::tryWritePod(file, width) && - serialization::tryWritePod(file, height); + return serialization::tryWriteString(file, imagePath) && serialization::tryWriteString(file, sourcePath) && + serialization::tryWritePod(file, width) && serialization::tryWritePod(file, height); } std::unique_ptr ImageBlock::deserialize(FsFile& file) { @@ -291,13 +372,18 @@ std::unique_ptr ImageBlock::deserialize(FsFile& file) { LOG_ERR("IMG", "Deserialization failed: could not read image path"); return nullptr; } + std::string source; + if (!serialization::tryReadString(file, source)) { + LOG_ERR("IMG", "Deserialization failed: could not read image source path"); + return nullptr; + } int16_t w, h; if (!serialization::tryReadPod(file, w) || !serialization::tryReadPod(file, h)) { LOG_ERR("IMG", "Deserialization failed: truncated image metadata"); return nullptr; } - auto* imageBlock = new (std::nothrow) ImageBlock(path, w, h); + auto* imageBlock = new (std::nothrow) ImageBlock(std::move(path), std::move(source), w, h); if (!imageBlock) { LOG_ERR("IMG", "Deserialization failed: could not allocate ImageBlock"); return nullptr; diff --git a/lib/Epub/Epub/blocks/ImageBlock.h b/lib/Epub/Epub/blocks/ImageBlock.h index e6cccae782..19b3687921 100644 --- a/lib/Epub/Epub/blocks/ImageBlock.h +++ b/lib/Epub/Epub/blocks/ImageBlock.h @@ -8,7 +8,7 @@ class ImageBlock final : public Block { public: - ImageBlock(const std::string& imagePath, int16_t width, int16_t height); + ImageBlock(std::string imagePath, std::string sourcePath, int16_t width, int16_t height); ~ImageBlock() override = default; const std::string& getImagePath() const { return imagePath; } @@ -16,16 +16,29 @@ class ImageBlock final : public Block { int16_t getHeight() const { return height; } bool imageExists() const; + bool hasValidCache() const; + bool needsDecode() const; + void renderPlaceholder(GfxRenderer& renderer, int x, int y, bool foregroundBlack) const; + static void clearSessionRenderFailures(); + + // The section builder only reads image headers. The reader supplies this + // allocation-free callback to extract a full image on its first render. + using ExtractFn = bool (*)(void* context, const char* sourcePath, const char* destinationPath); + static void setExtractor(void* context, ExtractFn fn); BlockType getType() override { return IMAGE_BLOCK; } bool isEmpty() override { return false; } - void render(GfxRenderer& renderer, const int x, const int y); - bool serialize(HalFile& file); - static std::unique_ptr deserialize(HalFile& file); + void render(GfxRenderer& renderer, const int x, const int y, const bool foregroundBlack); + bool serialize(FsFile& file); + static std::unique_ptr deserialize(FsFile& file); private: std::string imagePath; + std::string sourcePath; int16_t width; int16_t height; + + static void* extractContext; + static ExtractFn extractFn; }; diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp index 634341e8c4..97265bde7d 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -77,7 +77,7 @@ void TextBlock::bindArenaPointers() { xposArr = reinterpret_cast(base + wc * 2); size_t off = wc * 4; if (bionicPresent) { - bionicSuffixXArr = reinterpret_cast(base + off); + bionicRunOffsetArr = reinterpret_cast(base + off); off += wc * 2; } if (guideDotsPresent) { @@ -99,22 +99,31 @@ void TextBlock::bindArenaPointers() { TextBlock::TextBlock(const std::vector& words, const std::vector& wordXpos, const std::vector& wordStyles, const std::vector& bionicBoundary, - const std::vector& bionicSuffixX, const std::vector& guideDotXOffset, - const std::vector& wordFlags, const BlockStyle& blockStyle) - : blockStyle(blockStyle) { + const std::vector& bionicRunOffset, const std::vector& guideDotXOffset, + const std::vector& wordFlags, const BlockStyle& blockStyle, + std::vector rubyTexts) + : blockStyle(blockStyle), rubyTexts(std::move(rubyTexts)) { + // A ruby-less line needs no per-word ruby vector. ParsedText passes one for + // every extracted line once a book contains any ruby, so free all-empty + // vectors before they stay resident with the page. + if (!hasRuby()) { + this->rubyTexts = std::vector{}; + } + const bool hasBionic = !bionicBoundary.empty(); const bool hasGuideDots = !guideDotXOffset.empty(); const bool hasWordFlags = !wordFlags.empty(); if (words.size() != wordXpos.size() || words.size() != wordStyles.size() || words.size() > MAX_WORDS_PER_TEXT_BLOCK || - (hasBionic && (words.size() != bionicBoundary.size() || words.size() != bionicSuffixX.size())) || - (!hasBionic && !bionicSuffixX.empty()) || (hasGuideDots && words.size() != guideDotXOffset.size()) || - (hasWordFlags && words.size() != wordFlags.size())) { + (hasBionic && (words.size() != bionicBoundary.size() || words.size() != bionicRunOffset.size())) || + (!hasBionic && !bionicRunOffset.empty()) || (hasGuideDots && words.size() != guideDotXOffset.size()) || + (hasWordFlags && words.size() != wordFlags.size()) || + (!this->rubyTexts.empty() && words.size() != this->rubyTexts.size())) { LOG_ERR("TXB", - "Construction failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u, " + "Construction failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, runOffset=%u, " "dotX=%u, flags=%u)", static_cast(words.size()), static_cast(wordXpos.size()), static_cast(wordStyles.size()), static_cast(bionicBoundary.size()), - static_cast(bionicSuffixX.size()), static_cast(guideDotXOffset.size()), + static_cast(bionicRunOffset.size()), static_cast(guideDotXOffset.size()), static_cast(wordFlags.size())); isValid = false; return; @@ -171,10 +180,10 @@ TextBlock::TextBlock(const std::vector& words, const std::vector(bionicSuffixXArr); + auto* runOffset = const_cast(bionicRunOffsetArr); auto* boundary = const_cast(bionicBoundaryArr); for (uint16_t i = 0; i < numWords; i++) { - suffixX[i] = bionicSuffixX[i]; + runOffset[i] = bionicRunOffset[i]; boundary[i] = bionicBoundary[i]; } } @@ -192,6 +201,13 @@ TextBlock::TextBlock(const std::vector& words, const std::vector({static_cast(boundary), static_cast(wordLen), sizeof(boldBuf) - 1}); memcpy(boldBuf, word, boldLen); boldBuf[boldLen] = '\0'; - renderer.drawText(fontId, wordX, wordY, boldBuf, foregroundBlack, boldStyle, baseDir); - renderer.drawText(fontId, wordX + bionicSuffixX(i), wordY, word + boldLen, foregroundBlack, currentStyle, - baseDir); + const int secondRunX = wordX + bionicRunOffset(i); + if (baseDir == BidiUtils::BidiBaseDir::RTL) { + renderer.drawText(fontId, wordX, wordY, word + boldLen, foregroundBlack, currentStyle, baseDir); + renderer.drawText(fontId, secondRunX, wordY, boldBuf, foregroundBlack, boldStyle, baseDir); + } else { + renderer.drawText(fontId, wordX, wordY, boldBuf, foregroundBlack, boldStyle, baseDir); + renderer.drawText(fontId, secondRunX, wordY, word + boldLen, foregroundBlack, currentStyle, baseDir); + } } else { renderer.drawText(fontId, wordX, wordY, word, foregroundBlack, currentStyle, baseDir); } + if (i < rubyTexts.size() && !rubyTexts[i].empty() && (currentStyle & EpdFontFamily::RUBY_CONTINUE) == 0) { + uint16_t groupWords = 1; + while (i + groupWords < numWords && (wordStyle(i + groupWords) & EpdFontFamily::RUBY_CONTINUE) != 0) { + ++groupWords; + } + int groupWidth = 0; + for (uint16_t j = 0; j < groupWords; ++j) { + groupWidth += renderer.getTextAdvanceX(fontId, wordText(i + j), wordStyle(i + j)); + } + const int rubyWidth = renderer.getTextAdvanceX(fontId, rubyTexts[i].c_str(), EpdFontFamily::SUP); + // ParsedText reserves any edge overhang in the line layout, so the ruby + // can remain centered over its base text without screen-edge clamping. + const int rubyX = wordX + (groupWidth - rubyWidth) / 2; + renderer.drawText(fontId, rubyX, wordY - ascender, rubyTexts[i].c_str(), foregroundBlack, EpdFontFamily::SUP, + baseDir); + } + const uint16_t dotOffset = guideDotXOffset(i); if (dotOffset > 0) { renderer.drawText(fontId, wordX + dotOffset, wordY, "\xc2\xb7", foregroundBlack, EpdFontFamily::REGULAR, baseDir); @@ -259,7 +297,19 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int underlineWidth = (underlineWidth + 1) / 2; } - renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, 3, foregroundBlack); + int underlineEndX = startX + underlineWidth; + if (i + 1 < numWords) { + const EpdFontFamily::Style nextStyle = wordStyle(i + 1); + const bool nextSharesBaseline = (nextStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) == + (currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)); + if ((nextStyle & EpdFontFamily::UNDERLINE) != 0 && nextSharesBaseline) { + const int nextStartX = wordXpos(i + 1) + x; + underlineEndX = std::max(underlineEndX, nextStartX); + startX = std::min(startX, nextStartX); + } + } + + renderer.drawLine(startX, underlineY, underlineEndX, underlineY, 3, foregroundBlack); } if ((currentStyle & EpdFontFamily::STRIKETHROUGH) != 0) { @@ -278,7 +328,19 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int strikeWidth = (strikeWidth + 1) / 2; } - renderer.drawLine(startX, strikeY, startX + strikeWidth, strikeY, 3, foregroundBlack); + int strikeEndX = startX + strikeWidth; + if (i + 1 < numWords) { + const EpdFontFamily::Style nextStyle = wordStyle(i + 1); + const bool nextSharesBaseline = (nextStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) == + (currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)); + if ((nextStyle & EpdFontFamily::STRIKETHROUGH) != 0 && nextSharesBaseline) { + const int nextStartX = wordXpos(i + 1) + x; + strikeEndX = std::max(strikeEndX, nextStartX); + startX = std::min(startX, nextStartX); + } + } + + renderer.drawLine(startX, strikeY, strikeEndX, strikeY, 3, foregroundBlack); } } } @@ -305,6 +367,16 @@ bool TextBlock::serialize(HalFile& file) const { } } + uint16_t rubyCount = 0; + for (uint16_t i = 0; i < numWords && i < rubyTexts.size(); ++i) { + if (!rubyTexts[i].empty()) ++rubyCount; + } + if (!serialization::tryWritePod(file, rubyCount)) return false; + for (uint16_t i = 0; i < numWords && i < rubyTexts.size(); ++i) { + if (rubyTexts[i].empty()) continue; + if (!serialization::tryWritePod(file, i) || !serialization::tryWriteString(file, rubyTexts[i])) return false; + } + return serialization::tryWritePod(file, blockStyle.alignment) && serialization::tryWritePod(file, blockStyle.textAlignDefined) && serialization::tryWritePod(file, blockStyle.marginTop) && @@ -392,6 +464,22 @@ std::unique_ptr TextBlock::deserialize(HalFile& file) { } } + uint16_t rubyCount = 0; + if (!serialization::tryReadPod(file, rubyCount) || rubyCount > wc) { + LOG_ERR("TXB", "Deserialization failed: invalid ruby count %u", rubyCount); + return nullptr; + } + if (rubyCount > 0) block->rubyTexts.resize(wc); + for (uint16_t i = 0; i < rubyCount; ++i) { + uint16_t wordIndex = 0; + std::string ruby; + if (!serialization::tryReadPod(file, wordIndex) || wordIndex >= wc || !serialization::tryReadString(file, ruby)) { + LOG_ERR("TXB", "Deserialization failed: invalid ruby annotation"); + return nullptr; + } + block->rubyTexts[wordIndex] = std::move(ruby); + } + BlockStyle& blockStyle = block->blockStyle; if (!serialization::tryReadPod(file, blockStyle.alignment) || !serialization::tryReadPod(file, blockStyle.textAlignDefined) || diff --git a/lib/Epub/Epub/blocks/TextBlock.h b/lib/Epub/Epub/blocks/TextBlock.h index 635e050b58..8987e7c28c 100644 --- a/lib/Epub/Epub/blocks/TextBlock.h +++ b/lib/Epub/Epub/blocks/TextBlock.h @@ -25,12 +25,15 @@ class TextBlock final : public Block { const uint16_t* textOffArr = nullptr; const int16_t* xposArr = nullptr; - const uint16_t* bionicSuffixXArr = nullptr; // null when !bionicPresent + // Distance from the word's left edge to the second visual run. The suffix is + // second for LTR words; the bold logical prefix is second for RTL words. + const uint16_t* bionicRunOffsetArr = nullptr; // null when !bionicPresent const uint16_t* guideDotXOffsetArr = nullptr; // null when !guideDotsPresent const uint8_t* stylesArr = nullptr; const uint8_t* bionicBoundaryArr = nullptr; // null when !bionicPresent const uint8_t* wordFlagsArr = nullptr; // null when !wordFlagsPresent const char* textArr = nullptr; + std::vector rubyTexts; TextBlock() = default; // deserialize() fills the fields directly. static size_t arenaSize(uint16_t wordCount, bool hasBionic, bool hasGuideDots, bool hasWordFlags, uint16_t textBytes); @@ -39,11 +42,14 @@ class TextBlock final : public Block { public: static constexpr uint8_t WORD_FLAG_BACKGROUND_BLACK = 0x01; static constexpr uint8_t WORD_FLAG_INSERTED_HYPHEN = 0x02; + static constexpr uint8_t WORD_FLAG_LINK_ID_SHIFT = 2; + static constexpr uint8_t WORD_FLAG_LINK_ID_MASK = 0xFC; explicit TextBlock(const std::vector& words, const std::vector& wordXpos, const std::vector& wordStyles, const std::vector& bionicBoundary, - const std::vector& bionicSuffixX, const std::vector& guideDotXOffset, - const std::vector& wordFlags, const BlockStyle& blockStyle = BlockStyle()); + const std::vector& bionicRunOffset, const std::vector& guideDotXOffset, + const std::vector& wordFlags, const BlockStyle& blockStyle = BlockStyle(), + std::vector rubyTexts = {}); ~TextBlock() override = default; TextBlock(const TextBlock&) = delete; TextBlock& operator=(const TextBlock&) = delete; @@ -61,10 +67,16 @@ class TextBlock final : public Block { int16_t wordXpos(const uint16_t i) const { return xposArr[i]; } EpdFontFamily::Style wordStyle(const uint16_t i) const { return static_cast(stylesArr[i]); } uint8_t bionicBoundary(const uint16_t i) const { return bionicPresent ? bionicBoundaryArr[i] : 0; } - uint16_t bionicSuffixX(const uint16_t i) const { return bionicPresent ? bionicSuffixXArr[i] : 0; } + uint16_t bionicRunOffset(const uint16_t i) const { return bionicPresent ? bionicRunOffsetArr[i] : 0; } uint16_t guideDotXOffset(const uint16_t i) const { return guideDotsPresent ? guideDotXOffsetArr[i] : 0; } uint8_t wordFlags(const uint16_t i) const { return wordFlagsPresent ? wordFlagsArr[i] : 0; } + uint8_t wordLinkId(const uint16_t i) const { + return static_cast((wordFlags(i) & WORD_FLAG_LINK_ID_MASK) >> WORD_FLAG_LINK_ID_SHIFT); + } bool wordEndsWithInsertedHyphen(const uint16_t i) const { return (wordFlags(i) & WORD_FLAG_INSERTED_HYPHEN) != 0; } + bool hasRuby() const; + int getRubyShift(int ascender) const { return hasRuby() ? (ascender / 2) : 0; } + const std::vector& getRubyTexts() const { return rubyTexts; } void render(const GfxRenderer& renderer, int fontId, int x, int y, bool foregroundBlack = true) const; BlockType getType() override { return TEXT_BLOCK; } diff --git a/lib/Epub/Epub/converters/ImageDimsProbe.cpp b/lib/Epub/Epub/converters/ImageDimsProbe.cpp new file mode 100644 index 0000000000..9f07894664 --- /dev/null +++ b/lib/Epub/Epub/converters/ImageDimsProbe.cpp @@ -0,0 +1,141 @@ +#include "ImageDimsProbe.h" + +#include + +namespace { +constexpr uint8_t PNG_SIGNATURE[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; + +bool isJpegStartOfFrame(const uint8_t marker) { + return marker >= 0xC0 && marker <= 0xCF && marker != 0xC4 && marker != 0xC8 && marker != 0xCC; +} +} // namespace + +bool ImageDimsProbe::feed(const uint8_t byte) { + switch (state) { + case State::Sniff: + if (byte == 0xFF) { + state = State::JpegSoi; + } else if (byte == PNG_SIGNATURE[0]) { + state = State::PngHeader; + } else { + state = State::Failed; + return false; + } + position = 1; + return true; + + case State::PngHeader: + // Bytes 1..7 are the signature, 12..15 are "IHDR", and 16..23 + // contain the big-endian width and height. + if (position < 8) { + if (byte != PNG_SIGNATURE[position]) { + state = State::Failed; + return false; + } + } else if (position >= 12 && position < 16) { + if (byte != "IHDR"[position - 12]) { + state = State::Failed; + return false; + } + } else if (position >= 16 && position < 20) { + width = (width << 8) | byte; + } else if (position >= 20 && position < 24) { + height = (height << 8) | byte; + if (position == 23) { + state = State::Done; + return false; + } + } + position++; + return true; + + case State::JpegSoi: + if (byte != 0xD8) { + state = State::Failed; + return false; + } + state = State::JpegFf; + return true; + + case State::JpegFf: + if (byte != 0xFF) { + state = State::Failed; + return false; + } + state = State::JpegMarker; + return true; + + case State::JpegMarker: + if (byte == 0xFF) return true; + if (byte == 0x01 || (byte >= 0xD0 && byte <= 0xD8)) { + state = State::JpegFf; + return true; + } + if (byte == 0xD9 || byte == 0xDA) { + state = State::Failed; + return false; + } + sofPending = isJpegStartOfFrame(byte); + state = State::JpegLenHi; + return true; + + case State::JpegLenHi: + segmentLength = static_cast(byte << 8); + state = State::JpegLenLo; + return true; + + case State::JpegLenLo: + segmentLength = static_cast(segmentLength | byte); + if (segmentLength < 2 || (sofPending && segmentLength < 7)) { + state = State::Failed; + return false; + } + if (sofPending) { + sofByteCount = 0; + state = State::JpegSof; + } else if (segmentLength == 2) { + state = State::JpegFf; + } else { + skipLeft = static_cast(segmentLength) - 2; + state = State::JpegSkip; + } + return true; + + case State::JpegSkip: + if (--skipLeft == 0) state = State::JpegFf; + return true; + + case State::JpegSof: + sofBytes[sofByteCount++] = byte; + if (sofByteCount == sizeof(sofBytes)) { + height = static_cast((sofBytes[1] << 8) | sofBytes[2]); + width = static_cast((sofBytes[3] << 8) | sofBytes[4]); + state = State::Done; + return false; + } + return true; + + case State::Done: + case State::Failed: + return false; + } + return false; +} + +size_t ImageDimsProbe::write(const uint8_t byte) { return feed(byte) ? 1 : 0; } + +size_t ImageDimsProbe::write(const uint8_t* data, const size_t len) { + for (size_t index = 0; index < len; index++) { + if (!feed(data[index])) return index; + } + return len; +} + +bool ImageDimsProbe::getDimensions(ImageDimensions& out) const { + if (state != State::Done || width == 0 || height == 0 || width > INT16_MAX || height > INT16_MAX) { + return false; + } + out.width = static_cast(width); + out.height = static_cast(height); + return true; +} diff --git a/lib/Epub/Epub/converters/ImageDimsProbe.h b/lib/Epub/Epub/converters/ImageDimsProbe.h new file mode 100644 index 0000000000..f1130cb1d5 --- /dev/null +++ b/lib/Epub/Epub/converters/ImageDimsProbe.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "ImageToFramebufferDecoder.h" + +// Streaming JPEG/PNG header parser. Feed bytes through Print; write() returns +// short once dimensions are known or the stream is unusable, allowing ZIP +// inflation to stop without extracting the complete image. +class ImageDimsProbe final : public Print { + public: + size_t write(uint8_t byte) override; + size_t write(const uint8_t* data, size_t len) override; + + bool getDimensions(ImageDimensions& out) const; + + private: + bool feed(uint8_t byte); + + enum class State : uint8_t { + Sniff, + PngHeader, + JpegSoi, + JpegFf, + JpegMarker, + JpegLenHi, + JpegLenLo, + JpegSkip, + JpegSof, + Done, + Failed, + }; + + State state = State::Sniff; + uint32_t position = 0; + uint32_t skipLeft = 0; + uint16_t segmentLength = 0; + bool sofPending = false; + uint8_t sofBytes[5] = {}; + uint8_t sofByteCount = 0; + uint32_t width = 0; + uint32_t height = 0; +}; diff --git a/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp b/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp index db655f1c20..ed6b215a09 100644 --- a/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp +++ b/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp @@ -376,7 +376,6 @@ bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePat out.width = jpeg->getWidth(); out.height = jpeg->getHeight(); - LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height); jpeg->close(); delete jpeg; @@ -385,8 +384,6 @@ bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePat bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) { - LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str()); - if (!MemoryBudget::hasHeapForImageDecoder("JPG", "JPEG", JPEG_DECODER_APPROX_SIZE)) { return false; } @@ -487,10 +484,6 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat ctx.invScaleFPX = (int32_t)((int64_t)ctx.scaledSrcWidth * FP_ONE / destWidth); ctx.invScaleFPY = (int32_t)((int64_t)ctx.scaledSrcHeight * FP_ONE / destHeight); - LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f, jpegScale 1/%d, fineScale %.2fx%.2f)%s", srcWidth, srcHeight, - destWidth, destHeight, targetScale, jpegScaleDenom, (float)destWidth / ctx.scaledSrcWidth, - (float)destHeight / ctx.scaledSrcHeight, isProgressive ? " [progressive]" : ""); - // Set pixel type to 8-bit grayscale (must be after open()) jpeg->setPixelType(EIGHT_BIT_GRAYSCALE); jpeg->setUserPointer(&ctx); @@ -507,9 +500,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat } } - unsigned long decodeStart = millis(); rc = jpeg->decode(0, 0, jpegScaleOption); - unsigned long decodeTime = millis() - decodeStart; if (rc != 1) { LOG_ERR("JPG", "Decode failed (rc=%d, lastError=%d)", rc, jpeg->getLastError()); @@ -521,7 +512,6 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat jpeg->close(); delete jpeg; - LOG_DBG("JPG", "JPEG decoding complete - render time: %lu ms", decodeTime); // Finalize the streamed cache file. Note: a flush failure mid-decode clears // ctx.caching (the partial file is dropped), so re-read the flag here. diff --git a/lib/Epub/Epub/converters/PixelCache.h b/lib/Epub/Epub/converters/PixelCache.h index 354840d5f1..e1311b2709 100644 --- a/lib/Epub/Epub/converters/PixelCache.h +++ b/lib/Epub/Epub/converters/PixelCache.h @@ -112,7 +112,6 @@ struct PixelCache { return false; } - LOG_DBG("IMG", "Cache stream started: %s (%dx%d, band %d rows)", cachePath.c_str(), w, h, bandRows); ok = true; return true; } @@ -157,8 +156,6 @@ struct PixelCache { } } file.close(); - LOG_DBG("IMG", "Cache written: %s (%dx%d, %d bytes)", cachePathStr.c_str(), width, height, - 4 + bytesPerRow * height); ok = false; // file handed off; nothing left to clean up return true; } diff --git a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp index 69ddeee0b6..c4f69be7ff 100644 --- a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp +++ b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp @@ -101,24 +101,58 @@ int bytesPerPixelFromType(int pixelType) { } } -int requiredPngInternalBufferBytes(int srcWidth, int pixelType) { +int packedRowBytes(int srcWidth, int bitsPerSample) { return (srcWidth * bitsPerSample + 7) / 8; } + +int requiredPngInternalBufferBytes(int srcWidth, int pixelType, int bitsPerSample) { // +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin. int pitch = srcWidth * bytesPerPixelFromType(pixelType); + if ((pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED) && bitsPerSample < 8) { + pitch = packedRowBytes(srcWidth, bitsPerSample); + } return ((pitch + 1) * 2) + 32; } +bool isSupportedBitDepth(int pixelType, int bitsPerSample) { + if (bitsPerSample == 8) return true; + if (bitsPerSample != 1 && bitsPerSample != 2 && bitsPerSample != 4) return false; + return pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED; +} + +uint8_t readPackedSample(const uint8_t* pixels, int x, int bitsPerSample) { + if (bitsPerSample == 8) return pixels[x]; + + const int bitOffset = x * bitsPerSample; + const int shift = 8 - bitsPerSample - (bitOffset & 7); + const uint8_t mask = (1U << bitsPerSample) - 1; + return (pixels[bitOffset >> 3] >> shift) & mask; +} + +uint8_t expandSampleToByte(uint8_t sample, int bitsPerSample) { + if (bitsPerSample == 8) return sample; + const uint8_t maxSample = (1U << bitsPerSample) - 1; + return static_cast((sample * 255U) / maxSample); +} + // Convert entire source line to grayscale with alpha blending to white background. +// Low-bit-depth grayscale/indexed scanlines are packed most-significant sample first. // For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards. // Processing the whole line at once improves cache locality and reduces per-pixel overhead. -void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) { +void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, int bitsPerSample, + uint8_t* palette, int hasAlpha) { switch (pixelType) { case PNG_PIXEL_GRAYSCALE: - memcpy(grayLine, pPixels, width); + if (bitsPerSample == 8) { + memcpy(grayLine, pPixels, width); + } else { + for (int x = 0; x < width; x++) { + grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample); + } + } break; case PNG_PIXEL_TRUECOLOR: for (int x = 0; x < width; x++) { - uint8_t* p = &pPixels[x * 3]; + const uint8_t* p = &pPixels[x * 3]; grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); } break; @@ -127,7 +161,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel if (palette) { if (hasAlpha) { for (int x = 0; x < width; x++) { - uint8_t idx = pPixels[x]; + uint8_t idx = readPackedSample(pPixels, x, bitsPerSample); uint8_t* p = &palette[idx * 3]; uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); uint8_t alpha = palette[768 + idx]; @@ -135,12 +169,15 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel } } else { for (int x = 0; x < width; x++) { - uint8_t* p = &palette[pPixels[x] * 3]; + uint8_t idx = readPackedSample(pPixels, x, bitsPerSample); + uint8_t* p = &palette[idx * 3]; grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); } } } else { - memcpy(grayLine, pPixels, width); + for (int x = 0; x < width; x++) { + grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample); + } } break; @@ -154,7 +191,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel case PNG_PIXEL_TRUECOLOR_ALPHA: for (int x = 0; x < width; x++) { - uint8_t* p = &pPixels[x * 4]; + const uint8_t* p = &pPixels[x * 4]; uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8); uint8_t alpha = p[3]; grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255); @@ -174,74 +211,83 @@ int pngDrawCallback(PNGDRAW* pDraw) { int srcY = pDraw->y; int srcWidth = ctx->srcWidth; - // Calculate destination Y with scaling - int dstY = (int)(srcY * ctx->scale); - - // Skip if we already rendered this destination row (multiple source rows map to same dest) - if (dstY == ctx->lastDstY) return 1; - ctx->lastDstY = dstY; - - // Check bounds - if (dstY >= ctx->dstHeight) return 1; + // Map source rows with the exact output-height ratio. During downscaling, + // multiple source rows can select the same output row; during upscaling, one + // source row must be repeated across every output row in its range. Emitting + // only the first row of an upscale leaves zero-filled (black) gaps in the + // streamed pixel cache. + int firstDstY = (srcY * ctx->dstHeight) / ctx->srcHeight; + int endDstY = firstDstY + 1; + if (ctx->dstHeight > ctx->srcHeight) { + endDstY = ((srcY + 1) * ctx->dstHeight) / ctx->srcHeight; + } - int outY = ctx->config->y + dstY; - if (outY >= ctx->screenHeight) return 1; + if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1; + if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1; + if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight; // Convert entire source line to grayscale (improves cache locality) - convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette, + convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->iBpp, pDraw->pPalette, pDraw->iHasAlpha); - // Render scaled row using Bresenham-style integer stepping (no floating-point division) + // Render scaled rows using Bresenham-style integer stepping (no floating-point division) int dstWidth = ctx->dstWidth; int outXBase = ctx->config->x; int screenWidth = ctx->screenWidth; bool useDithering = ctx->config->useDithering; - bool caching = ctx->caching; - // Pre-compute orientation and render-mode state once per row + // Pre-compute orientation and render-mode state once per callback. DirectPixelWriter pw; pw.init(*ctx->renderer); - pw.beginRow(outY); - - // The cache streams to disk one row at a time. Flushing rows below this one - // (PNGdec delivers scanlines top to bottom) repositions the single-row band. - // A flush failure stops caching for the rest of the decode so we never write - // past the band buffer; finalize() then drops the partial file. - DirectCacheWriter cw; - if (caching) { - if (!ctx->cache.advanceTo(dstY)) { - caching = false; - ctx->caching = false; - } else { - cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX); - cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart); + + for (int dstY = firstDstY; dstY < endDstY; dstY++) { + ctx->lastDstY = dstY; + int outY = ctx->config->y + dstY; + if (outY >= ctx->screenHeight) continue; + + pw.beginRow(outY); + + // The cache streams to disk one row at a time. Flushing rows below this one + // (PNGdec delivers scanlines top to bottom) repositions the single-row band. + // A flush failure stops caching for the rest of the decode so we never write + // past the band buffer; finalize() then drops the partial file. + bool caching = ctx->caching; + DirectCacheWriter cw; + if (caching) { + if (!ctx->cache.advanceTo(dstY)) { + caching = false; + ctx->caching = false; + } else { + cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX); + cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart); + } } - } - int srcX = 0; - int error = 0; + int srcX = 0; + int error = 0; - for (int dstX = 0; dstX < dstWidth; dstX++) { - int outX = outXBase + dstX; - if (outX >= 0 && outX < screenWidth) { - uint8_t gray = ctx->grayLineBuffer[srcX]; + for (int dstX = 0; dstX < dstWidth; dstX++) { + int outX = outXBase + dstX; + if (outX >= 0 && outX < screenWidth) { + uint8_t gray = ctx->grayLineBuffer[srcX]; - uint8_t ditheredGray; - if (useDithering) { - ditheredGray = applyBayerDither4Level(gray, outX, outY); - } else { - ditheredGray = gray / 85; - if (ditheredGray > 3) ditheredGray = 3; + uint8_t ditheredGray; + if (useDithering) { + ditheredGray = applyBayerDither4Level(gray, outX, outY); + } else { + ditheredGray = gray / 85; + if (ditheredGray > 3) ditheredGray = 3; + } + pw.writePixel(outX, ditheredGray); + if (caching) cw.writePixel(outX, ditheredGray); } - pw.writePixel(outX, ditheredGray); - if (caching) cw.writePixel(outX, ditheredGray); - } - // Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth - error += srcWidth; - while (error >= dstWidth) { - error -= dstWidth; - srcX++; + // Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth + error += srcWidth; + while (error >= dstWidth) { + error -= dstWidth; + srcX++; + } } } @@ -280,8 +326,6 @@ bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) { - LOG_DBG("PNG", "Decoding PNG: %s", imagePath.c_str()); - if (!MemoryBudget::hasHeapForImageDecoder("PNG", "PNG", PNG_DECODER_APPROX_SIZE)) { return false; } @@ -334,34 +378,50 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath } ctx.lastDstY = -1; // Reset row tracking - LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight, - ctx.scale, png->getBpp()); - const int pixelType = png->getPixelType(); - const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType); + const int bitsPerSample = png->getBpp(); + + const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType, bitsPerSample); if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) { - LOG_ERR("PNG", - "PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d", - requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS); + LOG_ERR( + "PNG", + "PNG row buffer too small: need %d bytes for width=%d type=%d bpp=%d, configured PNG_MAX_BUFFERED_PIXELS=%d", + requiredInternal, ctx.srcWidth, pixelType, bitsPerSample, PNG_MAX_BUFFERED_PIXELS); LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow"); png->close(); delete png; return false; } - if (png->getBpp() != 8) { - warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath); + if (!isSupportedBitDepth(pixelType, bitsPerSample)) { + warnUnsupportedFeature( + "bit depth (" + std::to_string(bitsPerSample) + "bpp) for pixel type " + std::to_string(pixelType), imagePath); + png->close(); + delete png; + return false; + } + + // The converter expands each source row to 8-bit grayscale before dithering, + // so this scratch buffer is sized by source pixels even when PNGdec reads a + // packed 1/2/4-bit row internally. + constexpr size_t MAX_GRAY_LINE_BUFFER_BYTES = PNG_MAX_BUFFERED_PIXELS / 2; + const size_t grayBufSize = static_cast(ctx.srcWidth); + if (grayBufSize > MAX_GRAY_LINE_BUFFER_BYTES) { + LOG_ERR("PNG", "Expanded gray row too wide: need %u bytes for width=%d, max=%u", static_cast(grayBufSize), + ctx.srcWidth, static_cast(MAX_GRAY_LINE_BUFFER_BYTES)); + png->close(); + delete png; + return false; } - // Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode - const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2; - ctx.grayLineBuffer = static_cast(malloc(grayBufSize)); - if (!ctx.grayLineBuffer) { + auto grayLineBuffer = makeUniqueNoThrow(grayBufSize); + if (!grayLineBuffer) { LOG_ERR("PNG", "Failed to allocate gray line buffer"); png->close(); delete png; return false; } + ctx.grayLineBuffer = grayLineBuffer.get(); // Stream the pixel cache to disk. PNGdec delivers source scanlines top to // bottom and we emit at most one (downscaled) output row per callback, so the @@ -377,11 +437,8 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath } } - unsigned long decodeStart = millis(); rc = png->decode(&ctx, 0); - unsigned long decodeTime = millis() - decodeStart; - free(ctx.grayLineBuffer); ctx.grayLineBuffer = nullptr; if (rc != PNG_SUCCESS) { @@ -394,7 +451,6 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath png->close(); delete png; - LOG_DBG("PNG", "PNG decoding complete - render time: %lu ms", decodeTime); // Finalize the streamed cache (caching may have been cleared on a flush error). if (ctx.caching) { diff --git a/lib/Epub/Epub/css/CssParser.cpp b/lib/Epub/Epub/css/CssParser.cpp index cefcae79c2..ce25c8614c 100644 --- a/lib/Epub/Epub/css/CssParser.cpp +++ b/lib/Epub/Epub/css/CssParser.cpp @@ -46,6 +46,12 @@ constexpr size_t MAX_RULES = 1500; // Maximum number of two-part descendant rules (ancestor subject) to store constexpr size_t MAX_DESCENDANT_RULES = CssParser::MAX_DESCENDANT_RULES; +// Growing the selector containers uses throwing STL allocators. With firmware +// exceptions disabled, allocation failure aborts instead of returning an +// error, so stop early and let the caller persist a usable partial CSS cache. +constexpr size_t MIN_FREE_HEAP_FOR_RULE_GROWTH = 64 * 1024; +constexpr size_t MIN_LARGEST_BLOCK_FOR_RULE_GROWTH = 8 * 1024; + // Minimum free heap required to apply CSS during rendering // If below this threshold, we skip CSS to avoid display artifacts. constexpr size_t MIN_FREE_HEAP_FOR_CSS = 48 * 1024; @@ -62,9 +68,9 @@ constexpr size_t CSS_RULE_ARENA_EXTRA_BYTES = 1024; constexpr size_t MAX_SELECTOR_LENGTH = 256; constexpr size_t CSS_LENGTH_FIELD_COUNT = 11; constexpr size_t CSS_LENGTH_BYTES = sizeof(float) + sizeof(uint8_t); -constexpr size_t CSS_FIXED_STYLE_BYTES = 4 * sizeof(uint8_t) + (CSS_LENGTH_FIELD_COUNT * CSS_LENGTH_BYTES) + +constexpr size_t CSS_FIXED_STYLE_BYTES = 5 * sizeof(uint8_t) + (CSS_LENGTH_FIELD_COUNT * CSS_LENGTH_BYTES) + 4 * sizeof(uint8_t) + 2 * sizeof(uint8_t) + sizeof(uint32_t); -static_assert(CSS_FIXED_STYLE_BYTES == 69, +static_assert(CSS_FIXED_STYLE_BYTES == 70, "CssStyle cache payload changed; update read/writeCssStylePayload and bump CSS_CACHE_VERSION"); // Check if character is CSS whitespace @@ -326,6 +332,20 @@ CssFontWeight CssParser::interpretFontWeight(std::string_view val) { return CssFontWeight::Normal; } +CssFontVariantCaps CssParser::interpretFontVariantCaps(std::string_view val) { + val = trimCssWhitespace(stripTrailingImportant(val)); + + CssFontVariantCaps result = CssFontVariantCaps::Normal; + forEachDelimitedToken(val, isCssWhitespace, [&](const std::string_view token) { + if (iequalsAscii(token, "small-caps")) { + result = CssFontVariantCaps::SmallCaps; + } else if (iequalsAscii(token, "normal")) { + result = CssFontVariantCaps::Normal; + } + }); + return result; +} + CssTextDecoration CssParser::interpretDecoration(std::string_view val) { // text-decoration can have multiple space-separated values. Compare whole tokens // so malformed values like "notunderline" do not accidentally enable a line. @@ -407,6 +427,9 @@ void CssParser::parseDeclarationIntoStyle(std::string_view decl, CssStyle& style } else if (iequalsAscii(name, "font-weight")) { style.fontWeight = interpretFontWeight(value); style.defined.fontWeight = 1; + } else if (iequalsAscii(name, "font-variant") || iequalsAscii(name, "font-variant-caps")) { + style.fontVariantCaps = interpretFontVariantCaps(value); + style.defined.fontVariantCaps = 1; } else if (iequalsAscii(name, "text-decoration") || iequalsAscii(name, "text-decoration-line")) { style.textDecoration = interpretDecoration(value); style.defined.textDecoration = 1; @@ -554,6 +577,11 @@ bool CssParser::selectorMatchesElement(std::string_view selector, std::string_vi // Rule processing bool CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) { + // Skip rules that don't define any supported properties to save RAM. + if (!style.defined.anySet()) { + return true; + } + // Check if we've reached the rule limit before processing if (rulesBySelector_.size() >= MAX_RULES) { LOG_ERR("CSS", "Reached max rules limit (%zu), treating CSS parse as incomplete", MAX_RULES); @@ -565,6 +593,17 @@ bool CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const // silently; the only heap allocation per kept selector is the std::string // map key, which is unavoidable since the map owns its keys. bool limitReached = false; + auto hasHeapForRuleGrowth = [&]() { + const size_t freeHeap = ESP.getFreeHeap(); + const size_t largestBlock = ESP.getMaxAllocHeap(); + if (freeHeap >= MIN_FREE_HEAP_FOR_RULE_GROWTH && largestBlock >= MIN_LARGEST_BLOCK_FOR_RULE_GROWTH) { + return true; + } + LOG_ERR("CSS", "Stopping CSS parse before rule allocation (free=%u maxAlloc=%u rules=%u)", + static_cast(freeHeap), static_cast(largestBlock), + static_cast(rulesBySelector_.size())); + return false; + }; forEachDelimitedToken( selectorGroup, [](char c) { return c == ','; }, [&](std::string_view sel) { @@ -606,6 +645,10 @@ bool CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const if (it != descendantRules_.end()) { it->style.applyOver(style); } else { + if (!hasHeapForRuleGrowth()) { + limitReached = true; + return; + } descendantRules_.push_back({std::string(parts[0]), std::string(parts[1]), style}); } return; @@ -624,6 +667,10 @@ bool CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const if (it != rulesBySelector_.end()) { it->second.applyOver(style); } else { + if (!hasHeapForRuleGrowth()) { + limitReached = true; + return; + } rulesBySelector_.emplace(std::string(sel), style); } }); @@ -778,8 +825,6 @@ bool CssParser::loadFromStream(FsFile& source) { return false; } - LOG_DBG("CSS", "Parsed %zu rules and %zu descendant rules from %zu bytes", rulesBySelector_.size(), - descendantRules_.size(), totalRead); return true; } @@ -887,11 +932,11 @@ bool CssParser::writeCssStylePayload(FsFile& file, const CssStyle& style) { if (!writeByte(static_cast(style.textAlign)) || !writeByte(static_cast(style.fontStyle)) || !writeByte(static_cast(style.fontWeight)) || !writeByte(static_cast(style.textDecoration)) || - !writeLength(style.textIndent) || !writeLength(style.marginTop) || !writeLength(style.marginBottom) || - !writeLength(style.marginLeft) || !writeLength(style.marginRight) || !writeLength(style.paddingTop) || - !writeLength(style.paddingBottom) || !writeLength(style.paddingLeft) || !writeLength(style.paddingRight) || - !writeLength(style.imageHeight) || !writeLength(style.imageWidth) || - !writeByte(static_cast(style.display)) || + !writeByte(static_cast(style.fontVariantCaps)) || !writeLength(style.textIndent) || + !writeLength(style.marginTop) || !writeLength(style.marginBottom) || !writeLength(style.marginLeft) || + !writeLength(style.marginRight) || !writeLength(style.paddingTop) || !writeLength(style.paddingBottom) || + !writeLength(style.paddingLeft) || !writeLength(style.paddingRight) || !writeLength(style.imageHeight) || + !writeLength(style.imageWidth) || !writeByte(static_cast(style.display)) || !writeByte(static_cast(style.backgroundBlack ? 1 : 0)) || !writeByte(static_cast(style.verticalAlign)) || !writeByte(static_cast(style.direction)) || !writeByte(static_cast(style.pageBreakBefore ? 1 : 0)) || @@ -921,6 +966,7 @@ bool CssParser::writeCssStylePayload(FsFile& file, const CssStyle& style) { if (style.defined.direction) definedBits |= 1 << 18; if (style.defined.pageBreakBefore) definedBits |= 1 << 20; if (style.defined.pageBreakAfter) definedBits |= 1 << 21; + if (style.defined.fontVariantCaps) definedBits |= 1 << 22; return writeBytes(&definedBits, sizeof(definedBits)); } @@ -942,6 +988,8 @@ bool CssParser::readCssStylePayload(FsFile& file, CssStyle& style) { style.fontWeight = static_cast(enumVal); if (file.read(&enumVal, 1) != 1) return false; style.textDecoration = static_cast(enumVal & CSS_TEXT_DECORATION_MASK); + if (file.read(&enumVal, 1) != 1) return false; + style.fontVariantCaps = static_cast(enumVal); if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) || !readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) || !readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight) || @@ -989,6 +1037,7 @@ bool CssParser::readCssStylePayload(FsFile& file, CssStyle& style) { style.defined.direction = (definedBits & 1 << 18) != 0; style.defined.pageBreakBefore = (definedBits & 1 << 20) != 0; style.defined.pageBreakAfter = (definedBits & 1 << 21) != 0; + style.defined.fontVariantCaps = (definedBits & 1 << 22) != 0; return true; } @@ -1058,6 +1107,69 @@ bool CssParser::lookupRule(std::string_view selector, CssStyle& outStyle) const bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); } +CssParser::CacheStatus CssParser::inspectCache() const { + if (cachePath.empty() || !hasCache()) { + return CacheStatus::Missing; + } + + FsFile file; + if (!Storage.openFileForRead("CSS", cachePath + rulesCache, file)) { + return CacheStatus::Invalid; + } + struct FileGuard { + FsFile& file; + ~FileGuard() { + if (file.isOpen()) file.close(); + } + } fileGuard{file}; + + const auto readExact = [&file](void* out, const size_t size) { return file.read(out, size) == size; }; + const auto skipBytes = [&file](const size_t size) { + if (static_cast(file.available()) < size) return false; + return file.seek(file.position() + size); + }; + + uint32_t magic = 0; + uint8_t version = 0; + uint8_t flags = 0; + uint16_t ruleCount = 0; + if (!readExact(&magic, sizeof(magic)) || magic != CSS_CACHE_MAGIC || !readExact(&version, sizeof(version)) || + version != CSS_CACHE_VERSION || !readExact(&flags, sizeof(flags)) || (flags & ~CSS_CACHE_FLAG_PARTIAL) != 0 || + !readExact(&ruleCount, sizeof(ruleCount)) || ruleCount > MAX_RULES) { + return CacheStatus::Invalid; + } + + if (!skipBytes(static_cast(ruleCount) * sizeof(SelectorEntry))) { + return CacheStatus::Invalid; + } + for (uint16_t i = 0; i < ruleCount; ++i) { + uint16_t selectorLen = 0; + if (!readExact(&selectorLen, sizeof(selectorLen)) || selectorLen == 0 || selectorLen > MAX_SELECTOR_LENGTH || + !skipBytes(static_cast(selectorLen) + CSS_FIXED_STYLE_BYTES)) { + return CacheStatus::Invalid; + } + } + + uint16_t descendantCount = 0; + if (!readExact(&descendantCount, sizeof(descendantCount)) || descendantCount > MAX_DESCENDANT_RULES) { + return CacheStatus::Invalid; + } + for (uint16_t i = 0; i < descendantCount; ++i) { + for (uint8_t selectorIndex = 0; selectorIndex < 2; ++selectorIndex) { + uint16_t selectorLen = 0; + if (!readExact(&selectorLen, sizeof(selectorLen)) || selectorLen == 0 || selectorLen > MAX_SELECTOR_LENGTH || + !skipBytes(selectorLen)) { + return CacheStatus::Invalid; + } + } + if (!skipBytes(CSS_FIXED_STYLE_BYTES)) { + return CacheStatus::Invalid; + } + } + + return (flags & CSS_CACHE_FLAG_PARTIAL) != 0 ? CacheStatus::Partial : CacheStatus::Complete; +} + void CssParser::deleteCache() const { if (hasCache()) Storage.remove((cachePath + rulesCache).c_str()); Storage.remove((cachePath + rulesCacheTmp).c_str()); @@ -1202,8 +1314,6 @@ bool CssParser::saveToCache(const bool complete) const { Storage.remove(backupPath.c_str()); } - LOG_DBG("CSS", "Saved %u rules + %u descendant rules to %s cache", ruleCount, descendantCount, - complete ? "complete" : "partial"); return true; } @@ -1275,6 +1385,9 @@ bool CssParser::loadFromCache() { return false; } + // Size the bucket array up front to avoid incremental rehashes while loading rules. + rulesBySelector_.reserve(ruleCount); + auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool { return static_cast(file.available()) >= neededBytes; }; @@ -1372,8 +1485,6 @@ bool CssParser::loadFromCache() { break; } } - LOG_DBG("CSS", "Hydrated %u CSS rules into arena (%u bytes)", static_cast(cachedRuleTableCount_), - static_cast(cachedRuleArena_.used())); } // Read descendant rules diff --git a/lib/Epub/Epub/css/CssParser.h b/lib/Epub/Epub/css/CssParser.h index 1da8347ec3..937252fcf6 100644 --- a/lib/Epub/Epub/css/CssParser.h +++ b/lib/Epub/Epub/css/CssParser.h @@ -47,9 +47,16 @@ struct CssAncestorEntry { class CssParser { public: + enum class CacheStatus : uint8_t { + Missing, + Invalid, + Complete, + Partial, + }; + // Bump when CSS cache format or rules change; section caches are invalidated when this changes static constexpr uint32_t CSS_CACHE_MAGIC = 0x435843FF; // bytes: 0xFF, "CXC" - static constexpr uint8_t CSS_CACHE_VERSION = 14; + static constexpr uint8_t CSS_CACHE_VERSION = 15; static constexpr size_t MAX_DESCENDANT_RULES = 100; static constexpr size_t CSS_INDEX_BYTES_PER_RULE = 8; @@ -125,6 +132,11 @@ class CssParser { */ bool hasCache() const; + /** + * Validate the cache structure without hydrating any rule containers. + */ + CacheStatus inspectCache() const; + /** * Delete CSS rules cache file exists */ @@ -225,6 +237,7 @@ class CssParser { static CssTextAlign interpretAlignment(std::string_view val); static CssFontStyle interpretFontStyle(std::string_view val); static CssFontWeight interpretFontWeight(std::string_view val); + static CssFontVariantCaps interpretFontVariantCaps(std::string_view val); static CssTextDecoration interpretDecoration(std::string_view val); static CssLength interpretLength(std::string_view val); /** Returns true only when a numeric length was parsed (e.g. 2em, 50%). False for auto/inherit/initial. */ diff --git a/lib/Epub/Epub/css/CssStyle.h b/lib/Epub/Epub/css/CssStyle.h index 81fda3eefe..889127fedc 100644 --- a/lib/Epub/Epub/css/CssStyle.h +++ b/lib/Epub/Epub/css/CssStyle.h @@ -52,6 +52,9 @@ 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 }; +// Font variant caps options matching the small subset CrossInk renders. +enum class CssFontVariantCaps : uint8_t { Normal = 0, SmallCaps = 1 }; + // Text decoration options. Values are bit flags so CSS can combine multiple line decorations. enum class CssTextDecoration : uint8_t { None = 0, Underline = 1, LineThrough = 2 }; @@ -95,6 +98,7 @@ struct CssPropertyFlags { uint32_t direction : 1; uint32_t pageBreakBefore : 1; uint32_t pageBreakAfter : 1; + uint32_t fontVariantCaps : 1; CssPropertyFlags() : textAlign(0), @@ -117,12 +121,14 @@ struct CssPropertyFlags { verticalAlign(0), direction(0), pageBreakBefore(0), - pageBreakAfter(0) {} + pageBreakAfter(0), + fontVariantCaps(0) {} [[nodiscard]] bool anySet() const { return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom || marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || imageHeight || - imageWidth || display || backgroundBlack || verticalAlign || direction || pageBreakBefore || pageBreakAfter; + imageWidth || display || backgroundBlack || verticalAlign || direction || pageBreakBefore || + pageBreakAfter || fontVariantCaps; } void clearAll() { @@ -130,7 +136,7 @@ struct CssPropertyFlags { marginTop = marginBottom = marginLeft = marginRight = 0; paddingTop = paddingBottom = paddingLeft = paddingRight = 0; imageHeight = imageWidth = display = backgroundBlack = verticalAlign = direction = 0; - pageBreakBefore = pageBreakAfter = 0; + pageBreakBefore = pageBreakAfter = fontVariantCaps = 0; } }; @@ -146,6 +152,7 @@ struct CssStyle { CssFontWeight fontWeight = CssFontWeight::Normal; CssTextDecoration textDecoration = CssTextDecoration::None; CssTextDirection direction = CssTextDirection::Ltr; + CssFontVariantCaps fontVariantCaps = CssFontVariantCaps::Normal; CssLength textIndent; // First-line indent (deferred resolution) CssLength marginTop; // Vertical spacing before block @@ -253,6 +260,10 @@ struct CssStyle { pageBreakAfter = base.pageBreakAfter; defined.pageBreakAfter = 1; } + if (base.hasFontVariantCaps()) { + fontVariantCaps = base.fontVariantCaps; + defined.fontVariantCaps = 1; + } } [[nodiscard]] bool hasTextAlign() const { return defined.textAlign; } @@ -276,6 +287,7 @@ struct CssStyle { [[nodiscard]] bool hasDirection() const { return defined.direction; } [[nodiscard]] bool hasPageBreakBefore() const { return defined.pageBreakBefore; } [[nodiscard]] bool hasPageBreakAfter() const { return defined.pageBreakAfter; } + [[nodiscard]] bool hasFontVariantCaps() const { return defined.fontVariantCaps; } void reset() { textAlign = CssTextAlign::Left; @@ -283,6 +295,7 @@ struct CssStyle { fontWeight = CssFontWeight::Normal; textDecoration = CssTextDecoration::None; direction = CssTextDirection::Ltr; + fontVariantCaps = CssFontVariantCaps::Normal; textIndent = CssLength{}; marginTop = marginBottom = marginLeft = marginRight = CssLength{}; paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{}; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 24e145ef67..435f9d0cdd 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -23,20 +23,18 @@ #include "Epub.h" #include "Epub/Page.h" #include "Epub/converters/ImageDecoderFactory.h" +#include "Epub/converters/ImageDimsProbe.h" #include "Epub/converters/ImageToFramebufferDecoder.h" #include "Epub/htmlEntities.h" +#include "PreviewBlockLocator.h" // Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB constexpr size_t PARSE_BUFFER_SIZE = 1024; // Initial slab for the parse arena. Covers both style stacks (~2 KB) with headroom for growth. constexpr size_t PARSE_ARENA_SLAB_SIZE = 4 * 1024; -constexpr size_t IMAGE_EXTRACT_CHUNK_SIZE = 1024; -constexpr uint32_t MIN_FREE_HEAP_FOR_TEXT_LAYOUT = 44 * 1024; -constexpr uint32_t MIN_MAX_ALLOC_FOR_TEXT_LAYOUT = 32 * 1024; -constexpr uint32_t MIN_FREE_HEAP_FOR_TABLE_BUFFERING = 64 * 1024; -constexpr uint32_t MIN_MAX_ALLOC_FOR_TABLE_BUFFERING = 40 * 1024; constexpr size_t DEFAULT_BUFFERED_WORDS_BEFORE_LAYOUT = 350; +constexpr size_t CSS_BUFFERED_WORDS_BEFORE_LAYOUT = 320; constexpr uint16_t DEFAULT_TEXT_RUN_BYTES_BEFORE_LAYOUT = 2048; constexpr size_t SINGLE_READING_AID_BUFFERED_WORDS_BEFORE_LAYOUT = 240; constexpr uint16_t SINGLE_READING_AID_TEXT_RUN_BYTES_BEFORE_LAYOUT = 1536; @@ -44,6 +42,15 @@ constexpr size_t COMBINED_READING_AID_BUFFERED_WORDS_BEFORE_LAYOUT = 175; constexpr uint16_t COMBINED_READING_AID_TEXT_RUN_BYTES_BEFORE_LAYOUT = 1024; constexpr size_t SECTION_ADVANCE_PREWARM_READ_BUFFER_SIZE = 512; constexpr uint32_t SECTION_ADVANCE_PREWARM_MAX_CODEPOINTS = 4096; +// The whole-section advance prewarm is a batch-I/O optimization, not a requirement: +// layout falls back to per-paragraph advance loads (ParsedText loads glyph metrics per +// block, masked to the styles that block uses). Running it costs a 16KB contiguous +// codepoint scratch array plus the resident per-style advance tables (~30KB total +// observed), on top of the 44KB layout floor. 80KB keeps the prewarm for ordinary +// chapter opens (observed surviving from 81KB free) and skips it for low-heap +// extension starts (observed aborting the build when run from 70KB free). +constexpr uint32_t MIN_FREE_HEAP_FOR_SECTION_PREWARM = 80 * 1024; +constexpr uint32_t MIN_MAX_ALLOC_FOR_SECTION_PREWARM = 24 * 1024; constexpr uint8_t INITIAL_PAGE_ELEMENT_RESERVE = 8; constexpr uint8_t INITIAL_TABLE_FRAGMENT_ROW_RESERVE = 8; constexpr uint32_t PAGE_ELEMENT_RESERVE_MIN_MAX_ALLOC = 1024; @@ -60,8 +67,8 @@ static constexpr const char* const BOLD_TAGS[] = {"b", "strong"}; static constexpr const char* const ITALIC_TAGS[] = {"i", "em"}; static constexpr const char* const UNDERLINE_TAGS[] = {"u", "ins"}; static constexpr const char* const STRIKETHROUGH_TAGS[] = {"s", "strike", "del"}; -static constexpr const char* const IMAGE_TAGS[] = {"img"}; -static constexpr const char* const SKIP_TAGS[] = {"head"}; +static constexpr const char* const IMAGE_TAGS[] = {"img", "image"}; +static constexpr const char* const SKIP_TAGS[] = {"head", "style", "script", "title", "rp", "rt"}; bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; } @@ -144,6 +151,34 @@ static bool tokenEqualsIgnoreCase(const char* value, const char* token, const si return true; } +std::string trimAndNormalize(const std::string& str) { + if (str.empty()) return ""; + size_t start = 0; + while (start < str.size() && isWhitespace(str[start])) { + start++; + } + if (start == str.size()) return ""; + size_t end = str.size() - 1; + while (end > start && isWhitespace(str[end])) { + end--; + } + std::string result; + result.reserve(end - start + 1); + bool inSpace = false; + for (size_t i = start; i <= end; i++) { + if (isWhitespace(str[i])) { + if (!inSpace) { + result.push_back(' '); + inSpace = true; + } + } else { + result.push_back(str[i]); + inSpace = false; + } + } + return result; +} + bool matches(const char* tag_name, const char* const* possible_tags, size_t count) { for (size_t i = 0; i < count; i++) { if (strcmp(tag_name, possible_tags[i]) == 0) { @@ -234,6 +269,13 @@ void ChapterHtmlSlimParser::applyDirectionToEntry(StyleStackEntry& entry, const } } +void ChapterHtmlSlimParser::applySmallCapsToEntry(StyleStackEntry& entry, const CssStyle& css) { + if (css.hasFontVariantCaps()) { + entry.hasSmallCaps = true; + entry.smallCaps = css.fontVariantCaps == CssFontVariantCaps::SmallCaps; + } +} + // Update effective bold/italic/underline based on block style and inline style stack void ChapterHtmlSlimParser::updateEffectiveInlineStyle() { // Start with block-level styles @@ -249,6 +291,8 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() { effectiveDirection = currentCssStyle.direction; effectiveSup = currentCssStyle.hasVerticalAlign() && currentCssStyle.verticalAlign == CssVerticalAlign::Super; effectiveSub = currentCssStyle.hasVerticalAlign() && currentCssStyle.verticalAlign == CssVerticalAlign::Sub; + effectiveSmallCaps = + currentCssStyle.hasFontVariantCaps() && currentCssStyle.fontVariantCaps == CssFontVariantCaps::SmallCaps; // Apply inline style stack in order for (size_t i = 0; i < inlineStyleCount_; ++i) { @@ -280,6 +324,9 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() { effectiveSub = entry.sub; if (entry.sub) effectiveSup = false; } + if (entry.hasSmallCaps) { + effectiveSmallCaps = entry.smallCaps; + } } if (currentTextBlock && currentTextBlock->isEmpty()) { @@ -300,7 +347,7 @@ bool ChapterHtmlSlimParser::shouldAbortForLowMemory(const char* stage) { } auto heap = MemoryBudget::snapshot(); - if (heap.freeHeap >= MIN_FREE_HEAP_FOR_TEXT_LAYOUT && heap.maxAllocHeap >= MIN_MAX_ALLOC_FOR_TEXT_LAYOUT) { + if (MemoryBudget::hasHeapForEpubTextLayoutStart(heap)) { return false; } @@ -311,7 +358,7 @@ bool ChapterHtmlSlimParser::shouldAbortForLowMemory(const char* stage) { LOG_DBG("EHP", "Released SD font caches before %s: free=%u->%u maxAlloc=%u->%u", stage, heap.freeHeap, afterRelease.freeHeap, heap.maxAllocHeap, afterRelease.maxAllocHeap); heap = afterRelease; - if (heap.freeHeap >= MIN_FREE_HEAP_FOR_TEXT_LAYOUT && heap.maxAllocHeap >= MIN_MAX_ALLOC_FOR_TEXT_LAYOUT) { + if (MemoryBudget::hasHeapForEpubTextLayoutStart(heap)) { return false; } } @@ -333,12 +380,14 @@ bool ChapterHtmlSlimParser::startNewPage(const char* reason) { } const auto heap = MemoryBudget::snapshot(); - if (heap.freeHeap >= MIN_FREE_HEAP_FOR_TEXT_LAYOUT && heap.maxAllocHeap >= PAGE_ELEMENT_RESERVE_MIN_MAX_ALLOC) { + if (heap.freeHeap >= MemoryBudget::EPUB_TEXT_LAYOUT_MIN_FREE && + heap.maxAllocHeap >= PAGE_ELEMENT_RESERVE_MIN_MAX_ALLOC) { currentPage->elements.reserve(INITIAL_PAGE_ELEMENT_RESERVE); } currentPageNextY = 0; currentPageParagraphIndex = 0; currentPageListItemIndex = 0; + currentPageVisibleOffsetSet = false; return true; } @@ -353,7 +402,13 @@ void ChapterHtmlSlimParser::markCurrentPageFromCurrentElement() { } void ChapterHtmlSlimParser::completeCurrentPage() { - completePageFn(std::move(currentPage), currentPageParagraphIndex, currentPageListItemIndex); + completePageFn(std::move(currentPage), currentPageParagraphIndex, currentPageListItemIndex, currentPageVisibleOffset); +} + +void ChapterHtmlSlimParser::setCurrentPageVisibleOffset(const uint32_t offset) { + if (currentPageVisibleOffsetSet) return; + currentPageVisibleOffset = completedPageCount == 0 ? 0 : offset; + currentPageVisibleOffsetSet = true; } void ChapterHtmlSlimParser::flushPendingAnchor() { @@ -381,7 +436,60 @@ void ChapterHtmlSlimParser::flushPendingAnchor() { pendingAnchorFromInlineA = false; } +// Resolves previewAnchor to the element ordinal the preview should start rendering at. Runs before +// the render pass so the preview can begin at the block enclosing the anchor instead of +// mid-paragraph. Leaves previewStartOrdinal at 0 on any failure, which falls back to matching the +// anchor id, i.e. the behaviour before block resolution existed. +void ChapterHtmlSlimParser::locatePreviewBlockStart() { + PreviewBlockLocator locator(previewAnchor.c_str(), isHeaderOrBlock); + if (!locator.ok()) { + LOG_ERR("EHP", "Couldn't create parser to locate preview block"); + return; + } + + // Heap rather than stack: PARSE_BUFFER_SIZE is a quarter of the parsing task's stack. The buffer + // is released before the render pass opens the file, so the two never overlap. + auto buf = makeUniqueNoThrow(PARSE_BUFFER_SIZE); + if (!buf) { + LOG_ERR("EHP", "Couldn't allocate buffer to locate preview block"); + return; + } + + FsFile file; + if (!Storage.openFileForRead("EHP", filepath, file)) { + return; + } + + while (!locator.done()) { + const size_t len = file.read(buf.get(), PARSE_BUFFER_SIZE); + const bool isFinal = file.available() == 0; + if (len == 0 && !isFinal) { + LOG_ERR("EHP", "File read error while locating preview block"); + break; + } + if (!locator.feed(buf.get(), static_cast(len), isFinal) || isFinal) { + break; + } + } + file.close(); + + previewStartOrdinal = locator.startOrdinal(); + if (previewStartOrdinal == 0) { + LOG_DBG("EHP", "No enclosing block located for preview anchor '%s'; starting at the anchor", previewAnchor.c_str()); + return; + } + LOG_DBG("EHP", "Preview anchor '%s' resolves to block at element #%u", previewAnchor.c_str(), previewStartOrdinal); +} + bool ChapterHtmlSlimParser::handlePreviewScanStart(const XML_Char** atts) { + previewElementOrdinal += 1; + if (previewStartOrdinal != 0 && previewElementOrdinal == previewStartOrdinal) { + startPreviewAtAnchor(); + return false; + } + + // Fallback for when no block was located, and a safety net in case the two passes disagree on + // element ordinals: starting at the anchor is the pre-existing behaviour. const char* idValue = getAttribute(atts, "id"); if (!idValue || strcmp(idValue, previewAnchor.c_str()) != 0) { depth += 1; @@ -409,7 +517,6 @@ void ChapterHtmlSlimParser::startPreviewAtAnchor() { : static_cast(paragraphAlignment); startNewTextBlock(paragraphAlignmentBlockStyle); } - LOG_DBG("EHP", "Started preview at anchor '%s'", previewAnchor.c_str()); } void ChapterHtmlSlimParser::stopPreviewIfPageLimitReached() { @@ -421,7 +528,6 @@ void ChapterHtmlSlimParser::stopPreviewIfPageLimitReached() { if (activeParser) { XML_StopParser(activeParser, XML_TRUE); } - LOG_DBG("EHP", "Stopping preview after %u pages for anchor '%s'", previewMaxPages, previewAnchor.c_str()); } void ChapterHtmlSlimParser::addPendingPublisherPageMarker(const char* label) { @@ -486,11 +592,15 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() { } else if (effectiveSub) { fontStyle = static_cast(fontStyle | EpdFontFamily::SUB); } + if (effectiveSmallCaps) { + fontStyle = static_cast(fontStyle | EpdFontFamily::SMALL_CAPS); + } // flush the buffer partWordBuffer[partWordBufferIndex] = '\0'; currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues, - honorsPublisherDecorations() && effectiveBackgroundBlack); + honorsPublisherDecorations() && effectiveBackgroundBlack, + insideFootnoteLink ? currentFootnote.linkId : 0, partWordVisibleOffset); currentTextRunBytes = static_cast( std::min(currentTextRunBytes + static_cast(partWordBufferIndex), UINT16_MAX)); partWordBufferIndex = 0; @@ -504,7 +614,7 @@ size_t ChapterHtmlSlimParser::bufferedWordsBeforeLayoutLimit() const { if (bionicReadingEnabled || guideReadingEnabled) { return SINGLE_READING_AID_BUFFERED_WORDS_BEFORE_LAYOUT; } - return DEFAULT_BUFFERED_WORDS_BEFORE_LAYOUT; + return embeddedStyle ? CSS_BUFFERED_WORDS_BEFORE_LAYOUT : DEFAULT_BUFFERED_WORDS_BEFORE_LAYOUT; } uint16_t ChapterHtmlSlimParser::textRunBytesBeforeLayoutLimit() const { @@ -530,16 +640,15 @@ void ChapterHtmlSlimParser::flushLongTextRunIfNeeded(const bool force) { return; } - LOG_DBG("EHP", "Text block flush: force=%u words=%u/%u bytes=%u/%u footnotes=%u/%u", force ? 1U : 0U, - static_cast(wordCount), static_cast(wordLimit), - static_cast(currentTextRunBytes), static_cast(byteLimit), - static_cast(pendingFootnotes.size()), static_cast(MAX_PENDING_FOOTNOTES_BEFORE_LAYOUT)); const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset(); const uint16_t effectiveWidth = (horizontalInset < viewportWidth) ? static_cast(viewportWidth - horizontalInset) : viewportWidth; if (!currentTextBlock->layoutAndExtractLines( renderer, fontId, effectiveWidth, - [this](const std::shared_ptr& textBlock) { addLineToPage(textBlock); }, false)) { + [this](const std::shared_ptr& textBlock, const uint32_t offset) { + addLineToPage(textBlock, offset); + }, + false)) { LOG_ERR("EHP", "Failed to lay out long text run"); lowMemoryAbort = true; return; @@ -586,8 +695,9 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) { // If the pending anchor is a TOC chapter boundary, force a page break after the previous // block is flushed so the chapter starts on a fresh page. flushPendingAnchor(); - currentTextBlock.reset(new (std::nothrow) ParsedText(extraParagraphSpacing, forceParagraphIndents, hyphenationEnabled, - bionicReadingEnabled, guideReadingEnabled, blockStyle)); + currentTextBlock.reset(new (std::nothrow) + ParsedText(extraParagraphSpacing, forceParagraphIndents, hyphenationEnabled, + bionicReadingEnabled, guideReadingEnabled, wordSpacing, blockStyle)); if (!currentTextBlock) { const auto heap = MemoryBudget::snapshot(); LOG_ERR("EHP", "Failed to create text block (%u free, %u max alloc)", heap.freeHeap, heap.maxAllocHeap); @@ -633,6 +743,7 @@ void ChapterHtmlSlimParser::finalizeCurrentTableCell() { BufferedTableCell cell; cell.isHeader = currentTableCellIsHeader; cell.colSpan = currentTableCellColSpan; + cell.visibleTextOffset = currentTableCellVisibleOffset; cell.text = std::move(currentTextBlock); cell.footnotes = std::move(pendingFootnotes); pendingFootnotes.clear(); @@ -649,8 +760,7 @@ void ChapterHtmlSlimParser::finalizeCurrentTableCell() { currentTableBuffer->totalCells++; currentTableBuffer->maxCols = std::max(currentTableBuffer->maxCols, row.effectiveColumnCount); - if (currentTableBuffer->totalCells > MAX_SIMPLE_TABLE_CELLS || - currentTableBuffer->maxCols > MAX_SIMPLE_TABLE_COLUMNS) { + if (currentTableBuffer->maxCols > MAX_SIMPLE_TABLE_COLUMNS) { currentTableBuffer->unsupported = true; } @@ -658,7 +768,6 @@ void ChapterHtmlSlimParser::finalizeCurrentTableCell() { currentTableCellColSpan = 1; wordsExtractedInBlock = 0; nextWordContinues = false; - fallbackCurrentTableBufferIfNeeded("cell complete"); } void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) { @@ -707,13 +816,14 @@ void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) { currentPageNextY += topSpacing; attachPendingPublisherPageMarkers(currentPageNextY); - auto pageRule = std::shared_ptr( - new (std::nothrow) PageHorizontalRule(width, ruleThickness, xPos, currentPageNextY)); + auto pageRule = makeUniqueNoThrow(width, ruleThickness, xPos, currentPageNextY); if (!pageRule) { LOG_ERR("EHP", "Failed to create PageHorizontalRule"); + lowMemoryAbort = true; return; } - currentPage->elements.push_back(pageRule); + currentPage->elements.push_back(std::move(pageRule)); + setCurrentPageVisibleOffset(visibleTextOffset); markCurrentPageFromCurrentElement(); currentPageNextY = static_cast(currentPageNextY + ruleThickness + bottomSpacing); headingOpenerActive = false; @@ -778,10 +888,204 @@ void ChapterHtmlSlimParser::emitBufferedTableAsParagraphs(BufferedTable& table) } } +bool ChapterHtmlSlimParser::flushStreamingTableFragment(BufferedTable& table) { + if (table.streamingFragmentRows.empty()) { + return true; + } + + if (!currentPage && !startNewPage("streaming table fragment")) { + return false; + } + + const int horizontalInset = table.blockStyle.totalHorizontalInset(); + const uint16_t tableWidth = + (horizontalInset < viewportWidth) ? static_cast(viewportWidth - horizontalInset) : viewportWidth; + const uint16_t lineHeight = renderer.getLineHeight(fontId) * lineCompression; + const uint16_t fragmentHeight = table.streamingFragmentHeight; + auto fragment = makeUniqueNoThrow( + tableWidth, table.streamingFragmentColumnCount, TABLE_CELL_PADDING, lineHeight, + std::move(table.streamingFragmentRows), table.blockStyle.leftInset(), currentPageNextY); + if (!fragment) { + LOG_ERR("EHP", "Failed to create streaming PageTableFragment"); + lowMemoryAbort = true; + return false; + } + + currentPage->elements.push_back(std::move(fragment)); + setCurrentPageVisibleOffset(table.streamingFragmentVisibleOffset); + markCurrentPageFromCurrentElement(); + for (const auto& footnote : table.streamingFragmentFootnotes) { + currentPage->addFootnote(footnote.number, footnote.href, footnote.linkId); + } + table.streamingFragmentFootnotes.clear(); + table.streamingFragmentRows.clear(); + table.streamingFragmentHeight = 1; + table.streamingFragmentColumnCount = 0; + currentPageNextY = static_cast(currentPageNextY + fragmentHeight); + return true; +} + +void ChapterHtmlSlimParser::emitStreamingTableRowsAsParagraphs(BufferedTable& table) { + for (auto& row : table.rows) { + for (auto& cell : row.cells) { + if (!cell.text) continue; + pendingFootnotes = std::move(cell.footnotes); + currentTextBlock = std::move(cell.text); + wordsExtractedInBlock = 0; + makePages(); + currentTextBlock.reset(); + pendingFootnotes.clear(); + if (lowMemoryAbort) return; + } + } + table.rows.clear(); + table.totalCells = 0; + table.maxCols = 0; +} + +void ChapterHtmlSlimParser::fallbackStreamingTableToParagraphs(const char* reason) { + if (!currentTableBuffer) return; + auto& table = *currentTableBuffer; + LOG_DBG("EHP", "Streaming table fallback: %s", reason); + if (!flushStreamingTableFragment(table)) return; + if (!table.streamingTopSpacingApplied) { + if (!currentPage && !startNewPage("streaming table paragraph fallback")) return; + currentPageNextY = + static_cast(currentPageNextY + table.blockStyle.marginTop + table.blockStyle.paddingTop); + table.streamingTopSpacingApplied = true; + } + table.streamingFlattened = true; + emitStreamingTableRowsAsParagraphs(table); +} + +bool ChapterHtmlSlimParser::streamCurrentTableRow() { + if (!currentTableBuffer || !currentTableBuffer->streaming || currentTableBuffer->rows.empty()) { + return true; + } + + auto& table = *currentTableBuffer; + if (table.streamingFlattened) { + emitStreamingTableRowsAsParagraphs(table); + return !lowMemoryAbort; + } + + auto& row = table.rows.back(); + if (table.unsupported || row.cells.empty()) { + fallbackStreamingTableToParagraphs(table.unsupported ? "unsupported structure" : "empty row"); + return !lowMemoryAbort; + } + + if (table.streamingColumnCount == 0) { + table.streamingColumnCount = static_cast(row.effectiveColumnCount); + } + const bool isFullWidthSingleCellRow = row.cells.size() == 1 && row.cells[0].colSpan == table.streamingColumnCount; + const bool rowHasMergedCells = + std::any_of(row.cells.begin(), row.cells.end(), [](const BufferedTableCell& cell) { return cell.colSpan != 1; }); + if (table.streamingColumnCount == 0 || table.streamingColumnCount > MAX_SIMPLE_TABLE_COLUMNS || + (row.effectiveColumnCount != table.streamingColumnCount && !isFullWidthSingleCellRow) || + (rowHasMergedCells && !isFullWidthSingleCellRow)) { + fallbackStreamingTableToParagraphs("inconsistent column structure"); + return !lowMemoryAbort; + } + + const uint8_t columnCount = isFullWidthSingleCellRow ? 1 : table.streamingColumnCount; + const int horizontalInset = table.blockStyle.totalHorizontalInset(); + const uint16_t tableWidth = + (horizontalInset < viewportWidth) ? static_cast(viewportWidth - horizontalInset) : viewportWidth; + const uint16_t baseColumnWidth = tableWidth / columnCount; + const uint16_t innerColumnWidth = + baseColumnWidth > TABLE_CELL_PADDING * 2 ? static_cast(baseColumnWidth - TABLE_CELL_PADDING * 2) : 0; + if (innerColumnWidth < 20) { + fallbackStreamingTableToParagraphs("column width too small"); + return !lowMemoryAbort; + } + + TableFragmentRow fragmentRow; + fragmentRow.cells.resize(columnCount); + fragmentRow.headerSeparator = row.hasHeaderCell && !row.hasDataCell; + const uint16_t lineHeight = renderer.getLineHeight(fontId) * lineCompression; + uint32_t rowHeight = static_cast(lineHeight) + TABLE_CELL_PADDING * 2; + std::vector rowFootnotes; + for (size_t cellIndex = 0; cellIndex < row.cells.size(); ++cellIndex) { + const auto& sourceCell = row.cells[cellIndex]; + auto& destCell = fragmentRow.cells[cellIndex]; + destCell.isHeader = sourceCell.isHeader; + if (sourceCell.text && + !sourceCell.text->layoutAndExtractLinesPreservingSource( + renderer, fontId, innerColumnWidth, + [&destCell](const std::shared_ptr& textBlock) { destCell.lines.push_back(textBlock); })) { + fallbackStreamingTableToParagraphs("cell layout failed"); + return !lowMemoryAbort; + } + for (const auto& [wordIndex, footnote] : sourceCell.footnotes) { + (void)wordIndex; + rowFootnotes.push_back(footnote); + } + if (destCell.lines.size() > TableFragmentCell::MAX_SERIALIZED_LINES) { + fallbackStreamingTableToParagraphs("cell has too many lines"); + return !lowMemoryAbort; + } + const uint32_t cellHeight = std::max(1, destCell.lines.size()) * lineHeight + TABLE_CELL_PADDING * 2; + if (cellHeight > viewportHeight) { + fallbackStreamingTableToParagraphs("row exceeds viewport"); + return !lowMemoryAbort; + } + rowHeight = std::max(rowHeight, cellHeight); + } + fragmentRow.height = static_cast(rowHeight); + + if (!currentPage && !startNewPage("streaming table row")) return false; + if (!table.streamingTopSpacingApplied) { + currentPageNextY = + static_cast(currentPageNextY + table.blockStyle.marginTop + table.blockStyle.paddingTop); + table.streamingTopSpacingApplied = true; + } + + const bool startsNewFragment = + !table.streamingFragmentRows.empty() && + (table.streamingFragmentColumnCount != columnCount || + table.streamingFragmentRows.size() >= PageTableFragment::MAX_SERIALIZED_ROWS || + currentPageNextY + table.streamingFragmentHeight + fragmentRow.height > viewportHeight); + if (startsNewFragment && !flushStreamingTableFragment(table)) return false; + + if (table.streamingFragmentRows.empty() && currentPageNextY + 1 + fragmentRow.height > viewportHeight && + !currentPage->elements.empty()) { + completeCurrentPage(); + completedPageCount++; + stopPreviewIfPageLimitReached(); + if (previewStopRequested) return true; + if (!startNewPage("streaming table page break")) return false; + } + + if (table.streamingFragmentRows.empty()) { + table.streamingFragmentColumnCount = columnCount; + table.streamingFragmentVisibleOffset = row.cells.front().visibleTextOffset; + } + table.streamingFragmentHeight = static_cast(table.streamingFragmentHeight + fragmentRow.height); + table.streamingFragmentRows.push_back(std::move(fragmentRow)); + table.streamingFragmentFootnotes.insert(table.streamingFragmentFootnotes.end(), rowFootnotes.begin(), + rowFootnotes.end()); + std::vector().swap(table.rows); + table.totalCells = 0; + table.maxCols = 0; + return true; +} + +void ChapterHtmlSlimParser::finishStreamingTable(BufferedTable& table) { + if (!table.streamingFlattened && !streamCurrentTableRow()) return; + if (!flushStreamingTableFragment(table)) return; + currentPageNextY = + static_cast(currentPageNextY + table.blockStyle.marginBottom + table.blockStyle.paddingBottom); + if (extraParagraphSpacing) { + currentPageNextY += renderer.getLineHeight(fontId) * lineCompression / 2; + } +} + void ChapterHtmlSlimParser::emitBufferedTableAsFragments(BufferedTable& table) { struct PreparedRow { TableFragmentRow fragmentRow; std::vector footnotes; + uint32_t visibleTextOffset = 0; }; struct PreparedSegment { @@ -824,6 +1128,7 @@ void ChapterHtmlSlimParser::emitBufferedTableAsFragments(BufferedTable& table) { } PreparedRow prepared; + prepared.visibleTextOffset = row.cells.empty() ? visibleTextOffset : row.cells.front().visibleTextOffset; prepared.fragmentRow.cells.resize(columnCount); prepared.fragmentRow.headerSeparator = row.hasHeaderCell && !row.hasDataCell; @@ -920,6 +1225,7 @@ void ChapterHtmlSlimParser::emitBufferedTableAsFragments(BufferedTable& table) { std::vector fragmentFootnotes; fragmentRows.reserve(std::min(segment.rows.size() - nextRowIndex, INITIAL_TABLE_FRAGMENT_ROW_RESERVE)); uint16_t fragmentHeight = 1; // Bottom border. + const uint32_t fragmentVisibleOffset = segment.rows[nextRowIndex].visibleTextOffset; while (nextRowIndex < segment.rows.size()) { const uint16_t nextHeight = @@ -955,12 +1261,19 @@ void ChapterHtmlSlimParser::emitBufferedTableAsFragments(BufferedTable& table) { nextRowIndex++; } - currentPage->elements.push_back( - std::make_shared(tableWidth, segment.columnCount, TABLE_CELL_PADDING, lineHeight, - std::move(fragmentRows), table.blockStyle.leftInset(), currentPageNextY)); + auto fragment = + makeUniqueNoThrow(tableWidth, segment.columnCount, TABLE_CELL_PADDING, lineHeight, + std::move(fragmentRows), table.blockStyle.leftInset(), currentPageNextY); + if (!fragment) { + LOG_ERR("EHP", "Failed to create PageTableFragment"); + lowMemoryAbort = true; + return; + } + currentPage->elements.push_back(std::move(fragment)); + setCurrentPageVisibleOffset(fragmentVisibleOffset); markCurrentPageFromCurrentElement(); for (const auto& footnote : fragmentFootnotes) { - currentPage->addFootnote(footnote.number, footnote.href); + currentPage->addFootnote(footnote.number, footnote.href, footnote.linkId); } currentPageNextY += fragmentHeight; @@ -1040,22 +1353,6 @@ void ChapterHtmlSlimParser::fallbackCurrentTableBufferToParagraphs(const char* r currentTableCellColSpan = activeTableCellColSpan; } -void ChapterHtmlSlimParser::fallbackCurrentTableBufferIfNeeded(const char* stage) { - if (!currentTableBuffer) { - return; - } - - if (currentTableBuffer->unsupported) { - fallbackCurrentTableBufferToParagraphs(stage); - return; - } - - const auto heap = MemoryBudget::snapshot(); - if (!MemoryBudget::hasHeap(heap, MIN_FREE_HEAP_FOR_TABLE_BUFFERING, MIN_MAX_ALLOC_FOR_TABLE_BUFFERING)) { - fallbackCurrentTableBufferToParagraphs(stage); - } -} - void ChapterHtmlSlimParser::flushMalformedPartialContent() { if (partWordBufferIndex > 0) { flushPartWordBuffer(); @@ -1114,12 +1411,18 @@ bool ChapterHtmlSlimParser::appendMalformedMarkupWarningPage() { auto block = std::make_shared(std::move(words), std::move(xPos), std::move(styles), std::vector{}, std::vector{}, std::vector{}, std::vector{}); - auto pageLine = std::make_shared( + auto pageLine = makeUniqueNoThrow( std::move(block), static_cast( std::max(0, (static_cast(viewportWidth) - renderer.getTextWidth(fontId, line.c_str(), style)) / 2)), static_cast(yPos)); + if (!pageLine) { + LOG_ERR("EHP", "Failed to create malformed-markup warning line"); + lowMemoryAbort = true; + return false; + } currentPage->elements.push_back(std::move(pageLine)); + setCurrentPageVisibleOffset(visibleTextOffset); return true; }; @@ -1203,6 +1506,19 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } self->pendingAnchorId = idValue; self->pendingAnchorFromInlineA = !isTocAnchor && strcmp(name, "a") == 0; + + // A chapter's TOC target is sometimes an empty inline tucked *inside* an + // already-open heading, ahead of the chapter-number/ornament/title runs + // (e.g.

    Chapter NTitle

    ). Resolving it lazily at the + // next block flush defers the chapter's page break until after the "Chapter N" run + // is already on the page: the break then lands mid-heading, orphaning the number and + // pushing the ornament + title onto the next page. While the heading opener is still + // active and has produced no content yet, resolve it now so any needed break lands at + // the heading's start and the whole opener stays together. + if (isTocAnchor && self->headingOpenerActive && self->partWordBufferIndex == 0 && self->currentTextBlock && + self->currentTextBlock->isEmpty()) { + self->flushPendingAnchor(); + } } } else if (strcmp(atts[i], "dir") == 0) { dirAttr = attrValue; @@ -1244,6 +1560,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* cssStyle.defined.direction = 1; } + // font-variant-caps is inherited in CSS; propagate the parent's small-caps + // state to this element's style unless it sets its own (including "normal", + // which explicitly opts back out). + if (!cssStyle.hasFontVariantCaps() && self->effectiveSmallCaps) { + cssStyle.fontVariantCaps = CssFontVariantCaps::SmallCaps; + cssStyle.defined.fontVariantCaps = 1; + } + const char* roleAttr = getAttribute(atts, "role"); const char* epubTypeAttr = getAttribute(atts, "epub:type"); const bool isPublisherPageBreak = @@ -1266,8 +1590,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* return; } - // Special handling for tables/cells: buffer simple tables for grid layout, with - // a clean flat-paragraph fallback for anything more complex. + // Special handling for tables/cells: stream simple table rows into page fragments, + // with a clean flat-paragraph fallback for anything more complex. if (self->flattensTables()) { if (strcmp(name, "table") == 0) { if (self->tableDepth > 0) { @@ -1324,7 +1648,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* LOG_ERR("EHP", "inline style stack overflow (table cell label)"); } self->updateEffectiveInlineStyle(); + self->syntheticCharacterData = true; self->characterData(userData, headerText, static_cast(strlen(headerText))); + self->syntheticCharacterData = false; if (self->partWordBufferIndex > 0) { self->flushPartWordBuffer(); } @@ -1349,7 +1675,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (self->tableDepth > 0) { if (self->currentTableBuffer) { self->currentTableBuffer->unsupported = true; - self->fallbackCurrentTableBufferIfNeeded("nested table"); } self->tableDepth += 1; return; @@ -1370,6 +1695,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* return; } self->currentTableBuffer->blockStyle = tableBlockStyle; + self->currentTableBuffer->streaming = true; + LOG_DBG("EHP", "Streaming table layout: row-by-row"); self->tableDepth += 1; self->tableRowIndex = 0; self->tableColIndex = 0; @@ -1407,7 +1734,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (endPtr == colspan || *endPtr != '\0' || parsedValue <= 0 || parsedValue > UINT8_MAX) { if (self->currentTableBuffer) { self->currentTableBuffer->unsupported = true; - self->fallbackCurrentTableBufferIfNeeded("invalid colspan"); } } else { parsedColSpan = static_cast(parsedValue); @@ -1415,34 +1741,41 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } if (self->currentTableBuffer && rowspan && strcmp(rowspan, "1") != 0) { self->currentTableBuffer->unsupported = true; - self->fallbackCurrentTableBufferIfNeeded("rowspan"); } self->currentTableCellColSpan = parsedColSpan; + self->currentTableCellVisibleOffset = self->visibleTextOffset; auto tableCellBlockStyle = BlockStyle(); tableCellBlockStyle.textAlignDefined = true; // Default table cells to left alignment so narrow columns don't inherit paragraph // justification or other reader-wide alignment settings that damage readability. tableCellBlockStyle.alignment = cssStyle.hasTextAlign() ? cssStyle.textAlign : CssTextAlign::Left; + // Cell paragraphs are transparent wrappers. Mark the indent explicitly so the + // generic three-space paragraph fallback does not indent every table cell. + tableCellBlockStyle.textIndentDefined = true; + tableCellBlockStyle.textIndent = 0; if (cssStyle.hasDirection()) { tableCellBlockStyle.isRtl = cssStyle.direction == CssTextDirection::Rtl; tableCellBlockStyle.directionDefined = true; } self->currentTableCellIsHeader = strcmp(name, "th") == 0; - if (self->currentTableCellIsHeader) { - StyleStackEntry headerStyle; - headerStyle.depth = self->depth; - headerStyle.hasBold = true; - headerStyle.bold = true; + { + StyleStackEntry cellStyle; + cellStyle.depth = self->depth; + if (self->currentTableCellIsHeader) { + cellStyle.hasBold = true; + cellStyle.bold = true; + } if (cssStyle.hasBackgroundBlack()) { - headerStyle.hasBackgroundBlack = true; - headerStyle.backgroundBlack = cssStyle.backgroundBlack; + cellStyle.hasBackgroundBlack = true; + cellStyle.backgroundBlack = cssStyle.backgroundBlack; } - ChapterHtmlSlimParser::applyDirectionToEntry(headerStyle, cssStyle); + ChapterHtmlSlimParser::applyDirectionToEntry(cellStyle, cssStyle); + ChapterHtmlSlimParser::applySmallCapsToEntry(cellStyle, cssStyle); if (self->inlineStyleCount_ < MAX_INLINE_STYLE_DEPTH) { - self->inlineStyleBuf_[self->inlineStyleCount_++] = headerStyle; + self->inlineStyleBuf_[self->inlineStyleCount_++] = cellStyle; } else { - LOG_ERR("EHP", "inline style stack overflow (table header)"); + LOG_ERR("EHP", "inline style stack overflow (table cell)"); } self->updateEffectiveInlineStyle(); } @@ -1471,7 +1804,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (self->tableDepth == 1 && matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS))) { if (self->currentTableBuffer) { self->currentTableBuffer->unsupported = true; - self->fallbackCurrentTableBufferIfNeeded("table image"); } const char* altAttr = getAttribute(atts, "alt"); if (altAttr && altAttr[0] != '\0') { @@ -1488,7 +1820,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (self->tableDepth == 1 && strcmp(name, "hr") == 0) { if (self->currentTableBuffer) { self->currentTableBuffer->unsupported = true; - self->fallbackCurrentTableBufferIfNeeded("table horizontal rule"); } self->pushCssAncestor(self->depth, name, classAttr); self->depth += 1; @@ -1503,6 +1834,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* for (int i = 0; atts[i]; i += 2) { if (strcmp(atts[i], "src") == 0) { src = atts[i + 1]; + } else if (src.empty() && (strcmp(atts[i], "href") == 0 || strcmp(atts[i], "xlink:href") == 0)) { + src = atts[i + 1]; } else if (strcmp(atts[i], "alt") == 0) { alt = atts[i + 1]; } else if (strncmp(atts[i], "data-AmznRemoved-M8", 19) == 0) { @@ -1516,6 +1849,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* return; } + const size_t fragmentPos = src.find('#'); + if (fragmentPos != std::string::npos) { + src.resize(fragmentPos); + } + // imageRendering: 0=display, 1=placeholder (alt text only), 2=suppress entirely if (self->imageRendering == 2) { self->skipCurrentElement(); @@ -1537,7 +1875,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } if (!src.empty() && self->imageRendering != 1) { - LOG_DBG("EHP", "Found image: src=%s", src.c_str()); const std::string resolvedPath = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(self->contentBase + src)); if (isSvgImagePath(resolvedPath)) { LOG_DBG("EHP", "Skipping unsupported SVG image: %s", resolvedPath.c_str()); @@ -1556,17 +1893,20 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } const auto heapBeforeImage = MemoryBudget::snapshot(); - LOG_DBG("EHP", "Heap before image extraction: free=%u maxAlloc=%u src=%s", heapBeforeImage.freeHeap, - heapBeforeImage.maxAllocHeap, src.c_str()); - if (!self->lowMemoryImageFallback && !MemoryBudget::hasHeapForEpubInlineImage("EHP", src.c_str())) { - self->lowMemoryImageFallback = true; - } if (self->lowMemoryImageFallback) { self->skipCurrentElement(); return; } else { if (ImageDecoderFactory::isFormatSupported(resolvedPath)) { + // Unsupported formats are skipped regardless of heap, so only + // formats we can render should trip the low-memory image fallback. + if (!MemoryBudget::hasHeapForEpubInlineImage("EHP", src.c_str())) { + self->lowMemoryImageFallback = true; + self->skipCurrentElement(); + return; + } + // Create a unique filename for the cached image std::string ext; size_t extPos = resolvedPath.rfind('.'); @@ -1575,204 +1915,217 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } std::string cachedImagePath = self->imageBasePath + std::to_string(self->imageCounter++) + ext; - // Extract image to cache file - FsFile cachedImageFile; - bool extractSuccess = false; - if (Storage.openFileForWrite("EHP", cachedImagePath, cachedImageFile)) { - extractSuccess = - self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, IMAGE_EXTRACT_CHUNK_SIZE); - cachedImageFile.flush(); - cachedImageFile.close(); - delay(50); // Give SD card time to sync - } - - if (extractSuccess) { - LOG_DBG("EHP", "Heap after image extraction: free=%u maxAlloc=%u path=%s", ESP.getFreeHeap(), - ESP.getMaxAllocHeap(), cachedImagePath.c_str()); - // Get image dimensions - ImageDimensions dims = {0, 0}; + // Read just enough compressed data to find dimensions. The full + // image remains inside the EPUB until its page is first rendered. + ImageDimensions dims = {0, 0}; + ImageDimsProbe headerProbe; + bool gotDimensions = + self->epub->readItemContentsToStream(resolvedPath, headerProbe, 1024, /*allowEarlyStop=*/true) && + headerProbe.getDimensions(dims); + std::string sourcePath; + if (gotDimensions) { + sourcePath = resolvedPath; + } else if (self->epub->extractItemToFile(resolvedPath, cachedImagePath)) { + // Unusual headers fall back to the existing full-file decoder. + // Retry only if needed to tolerate slow SD-card sync. ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath); - if (decoder && decoder->getDimensions(cachedImagePath, dims)) { - LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height); - - if (!MemoryBudget::hasHeapForEpubInlineImage("EHP", cachedImagePath.c_str())) { - self->lowMemoryImageFallback = true; - Storage.remove(cachedImagePath.c_str()); - self->skipCurrentElement(); - return; + for (int attempt = 0; attempt < 3 && !gotDimensions; attempt++) { + if (attempt > 0) { + delay(50); } + gotDimensions = decoder && decoder->getDimensions(cachedImagePath, dims); + } + } - int displayWidth = 0; - int displayHeight = 0; - const float emSize = static_cast(self->renderer.getFontAscenderSize(self->fontId)); - CssStyle imgStyle; - if (!self->isLightMode()) { - imgStyle = self->cssParser - ? (self->usesSimpleCssLookup() - ? self->cssParser->resolveStyle("img", classAttr) - : self->cssParser->resolveStyle("img", classAttr, self->ancestorStack_)) - : CssStyle{}; - // Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules - if (!styleAttr.empty()) { - imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr)); - } + if (gotDimensions) { + if (!MemoryBudget::hasHeapForEpubInlineImage("EHP", cachedImagePath.c_str())) { + self->lowMemoryImageFallback = true; + Storage.remove(cachedImagePath.c_str()); + self->skipCurrentElement(); + return; + } + + int displayWidth = 0; + int displayHeight = 0; + const float emSize = static_cast(self->renderer.getFontAscenderSize(self->fontId)); + CssStyle imgStyle; + if (!self->isLightMode()) { + imgStyle = self->cssParser + ? (self->usesSimpleCssLookup() + ? self->cssParser->resolveStyle("img", classAttr) + : self->cssParser->resolveStyle("img", classAttr, self->ancestorStack_)) + : CssStyle{}; + // Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules + if (!styleAttr.empty()) { + imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr)); } - const bool hasCssHeight = imgStyle.hasImageHeight(); - const bool hasCssWidth = imgStyle.hasImageWidth(); - int containerWidth = self->viewportWidth; - if (self->currentTextBlock) { - const int inset = self->currentTextBlock->getBlockStyle().totalHorizontalInset(); - if (inset > 0 && inset < self->viewportWidth) { - containerWidth = self->viewportWidth - inset; - } + } + const bool hasCssHeight = imgStyle.hasImageHeight(); + const bool hasCssWidth = imgStyle.hasImageWidth(); + int containerWidth = self->viewportWidth; + if (self->currentTextBlock) { + const int inset = self->currentTextBlock->getBlockStyle().totalHorizontalInset(); + if (inset > 0 && inset < self->viewportWidth) { + containerWidth = self->viewportWidth - inset; } + } - if (hasCssHeight && hasCssWidth && dims.width > 0 && dims.height > 0) { - // Both CSS height and width set: resolve both, then clamp to viewport preserving requested ratio - displayHeight = static_cast( - imgStyle.imageHeight.toPixels(emSize, static_cast(self->viewportHeight)) + 0.5f); - displayWidth = static_cast( - imgStyle.imageWidth.toPixels(emSize, static_cast(containerWidth)) + 0.5f); - if (displayHeight < 1) displayHeight = 1; + if (hasCssHeight && hasCssWidth && dims.width > 0 && dims.height > 0) { + // Both CSS height and width set: resolve both, then clamp to viewport preserving requested ratio + displayHeight = static_cast( + imgStyle.imageHeight.toPixels(emSize, static_cast(self->viewportHeight)) + 0.5f); + displayWidth = + static_cast(imgStyle.imageWidth.toPixels(emSize, static_cast(containerWidth)) + 0.5f); + if (displayHeight < 1) displayHeight = 1; + if (displayWidth < 1) displayWidth = 1; + if (displayWidth > containerWidth || displayHeight > self->viewportHeight) { + float scaleX = + (displayWidth > containerWidth) ? static_cast(containerWidth) / displayWidth : 1.0f; + float scaleY = (displayHeight > self->viewportHeight) + ? static_cast(self->viewportHeight) / displayHeight + : 1.0f; + float scale = (scaleX < scaleY) ? scaleX : scaleY; + displayWidth = static_cast(displayWidth * scale + 0.5f); + displayHeight = static_cast(displayHeight * scale + 0.5f); if (displayWidth < 1) displayWidth = 1; - if (displayWidth > containerWidth || displayHeight > self->viewportHeight) { - float scaleX = - (displayWidth > containerWidth) ? static_cast(containerWidth) / displayWidth : 1.0f; - float scaleY = (displayHeight > self->viewportHeight) - ? static_cast(self->viewportHeight) / displayHeight - : 1.0f; - float scale = (scaleX < scaleY) ? scaleX : scaleY; - displayWidth = static_cast(displayWidth * scale + 0.5f); - displayHeight = static_cast(displayHeight * scale + 0.5f); - if (displayWidth < 1) displayWidth = 1; - if (displayHeight < 1) displayHeight = 1; - } - LOG_DBG("EHP", "Display size from CSS height+width: %dx%d", displayWidth, displayHeight); - } else if (hasCssHeight && !hasCssWidth && dims.width > 0 && dims.height > 0) { - // Use CSS height (resolve % against viewport height) and derive width from aspect ratio - displayHeight = static_cast( - imgStyle.imageHeight.toPixels(emSize, static_cast(self->viewportHeight)) + 0.5f); if (displayHeight < 1) displayHeight = 1; + } + } else if (hasCssHeight && !hasCssWidth && dims.width > 0 && dims.height > 0) { + // Use CSS height (resolve % against viewport height) and derive width from aspect ratio + displayHeight = static_cast( + imgStyle.imageHeight.toPixels(emSize, static_cast(self->viewportHeight)) + 0.5f); + if (displayHeight < 1) displayHeight = 1; + displayWidth = + static_cast(displayHeight * (static_cast(dims.width) / dims.height) + 0.5f); + if (displayHeight > self->viewportHeight) { + displayHeight = self->viewportHeight; + // Rescale width to preserve aspect ratio when height is clamped displayWidth = static_cast(displayHeight * (static_cast(dims.width) / dims.height) + 0.5f); - if (displayHeight > self->viewportHeight) { - displayHeight = self->viewportHeight; - // Rescale width to preserve aspect ratio when height is clamped - displayWidth = - static_cast(displayHeight * (static_cast(dims.width) / dims.height) + 0.5f); - if (displayWidth < 1) displayWidth = 1; - } - if (displayWidth > containerWidth) { - displayWidth = containerWidth; - // Rescale height to preserve aspect ratio when width is clamped - displayHeight = - static_cast(displayWidth * (static_cast(dims.height) / dims.width) + 0.5f); - if (displayHeight < 1) displayHeight = 1; - } - if (displayWidth < 1) displayWidth = 1; - LOG_DBG("EHP", "Display size from CSS height: %dx%d", displayWidth, displayHeight); - } else if (hasCssWidth && !hasCssHeight && dims.width > 0 && dims.height > 0) { - // Use CSS width (resolve % against container width) and derive height from aspect ratio - displayWidth = static_cast( - imgStyle.imageWidth.toPixels(emSize, static_cast(containerWidth)) + 0.5f); - if (displayWidth > containerWidth) displayWidth = containerWidth; if (displayWidth < 1) displayWidth = 1; + } + if (displayWidth > containerWidth) { + displayWidth = containerWidth; + // Rescale height to preserve aspect ratio when width is clamped displayHeight = static_cast(displayWidth * (static_cast(dims.height) / dims.width) + 0.5f); - if (displayHeight > self->viewportHeight) { - displayHeight = self->viewportHeight; - // Rescale width to preserve aspect ratio when height is clamped - displayWidth = - static_cast(displayHeight * (static_cast(dims.width) / dims.height) + 0.5f); - if (displayWidth < 1) displayWidth = 1; - } if (displayHeight < 1) displayHeight = 1; - LOG_DBG("EHP", "Display size from CSS width: %dx%d", displayWidth, displayHeight); - } else { - // Scale to fit container while preserving aspect ratio - int maxWidth = containerWidth; - 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; - - displayWidth = (int)(dims.width * scale); - displayHeight = (int)(dims.height * scale); - LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale); - } - - // Flush any pending text block so it appears before the image - if (self->partWordBufferIndex > 0) { - self->flushPartWordBuffer(); } - if (self->currentTextBlock && !self->currentTextBlock->isEmpty()) { - const BlockStyle parentBlockStyle = self->currentTextBlock->getBlockStyle(); - self->startNewTextBlock(parentBlockStyle); + if (displayWidth < 1) displayWidth = 1; + } else if (hasCssWidth && !hasCssHeight && dims.width > 0 && dims.height > 0) { + // Use CSS width (resolve % against container width) and derive height from aspect ratio + displayWidth = + static_cast(imgStyle.imageWidth.toPixels(emSize, static_cast(containerWidth)) + 0.5f); + if (displayWidth > containerWidth) displayWidth = containerWidth; + if (displayWidth < 1) displayWidth = 1; + displayHeight = + static_cast(displayWidth * (static_cast(dims.height) / dims.width) + 0.5f); + if (displayHeight > self->viewportHeight) { + displayHeight = self->viewportHeight; + // Rescale width to preserve aspect ratio when height is clamped + displayWidth = + static_cast(displayHeight * (static_cast(dims.width) / dims.height) + 0.5f); + if (displayWidth < 1) displayWidth = 1; } + if (displayHeight < 1) displayHeight = 1; + } else { + // Scale to fit container while preserving aspect ratio + int maxWidth = containerWidth; + 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; + + displayWidth = (int)(dims.width * scale); + displayHeight = (int)(dims.height * scale); + } - int16_t imageMarginTop = 0; - int16_t imageMarginBottom = 0; - if (self->currentTextBlock && self->currentTextBlock->isEmpty()) { - const auto& bs = self->currentTextBlock->getBlockStyle(); - imageMarginTop = bs.topInset(); - if (self->blockStyleCount_ > 1) { - imageMarginBottom = self->blockStyleBuf_[self->blockStyleCount_ - 1].bottomInset(); - } - } + // Flush any pending text block so it appears before the image + if (self->partWordBufferIndex > 0) { + self->flushPartWordBuffer(); + } + bool openerNumberPrecededImage = false; + if (self->currentTextBlock && !self->currentTextBlock->isEmpty()) { + const BlockStyle parentBlockStyle = self->currentTextBlock->getBlockStyle(); + self->startNewTextBlock(parentBlockStyle); + // The block synthesized above copies the heading style. Inside a chapter opener that + // style still carries the heading's drop margin (the large gap that pushes "Chapter N" + // down the page), which was already spent positioning the number. Remember that so it + // is not re-applied a second time as the ornament's top gap. + openerNumberPrecededImage = self->headingOpenerActive; + } - // Create page for image - only break if image won't fit remaining space - if (!self->headingOpenerActive && self->currentPage && !self->currentPage->elements.empty() && - (self->currentPageNextY + imageMarginTop + displayHeight + imageMarginBottom > - self->viewportHeight)) { - self->completeCurrentPage(); - self->completedPageCount++; - self->stopPreviewIfPageLimitReached(); - if (self->previewStopRequested) { - return; - } - if (!self->startNewPage("image page break")) { - return; - } - } else if (!self->currentPage) { - if (!self->startNewPage("image page")) { - return; - } + int16_t imageMarginTop = 0; + int16_t imageMarginBottom = 0; + if (self->currentTextBlock && self->currentTextBlock->isEmpty()) { + const auto& bs = self->currentTextBlock->getBlockStyle(); + imageMarginTop = openerNumberPrecededImage ? 0 : bs.topInset(); + if (self->blockStyleCount_ > 1) { + imageMarginBottom = self->blockStyleBuf_[self->blockStyleCount_ - 1].bottomInset(); } + } - self->currentPageNextY += imageMarginTop; - self->attachPendingPublisherPageMarkers(self->currentPageNextY); - - // Create ImageBlock and add to page - auto imageBlock = std::shared_ptr( - new (std::nothrow) ImageBlock(cachedImagePath, displayWidth, displayHeight)); - if (!imageBlock) { - LOG_ERR("EHP", "Failed to create ImageBlock"); + // Keep a chapter number with its first ornament, but let later heading images continue on the + // next page instead of overflowing the viewport (including the reserved status-bar area). + const bool keepFirstOpenerImageWithHeading = + self->headingOpenerActive && (!self->currentPage || !self->currentPage->hasImages()); + if (!keepFirstOpenerImageWithHeading && self->currentPage && !self->currentPage->elements.empty() && + (self->currentPageNextY + imageMarginTop + displayHeight + imageMarginBottom > + self->viewportHeight)) { + self->completeCurrentPage(); + self->completedPageCount++; + self->stopPreviewIfPageLimitReached(); + if (self->previewStopRequested) { return; } - int xPos = (self->viewportWidth - displayWidth) / 2; - auto pageImage = std::shared_ptr(new (std::nothrow) - PageImage(imageBlock, xPos, self->currentPageNextY)); - if (!pageImage) { - LOG_ERR("EHP", "Failed to create PageImage"); + if (!self->startNewPage("image page break")) { return; } - self->currentPage->elements.push_back(pageImage); - self->markCurrentPageFromCurrentElement(); - self->currentPageNextY += displayHeight + imageMarginBottom; - - if (self->currentTextBlock && self->currentTextBlock->isEmpty()) { - self->currentTextBlock->setBlockStyle( - self->blockStyleBuf_[self->blockStyleCount_ - 1].withoutBottom()); + } else if (!self->currentPage) { + if (!self->startNewPage("image page")) { + return; } + } + + self->currentPageNextY += imageMarginTop; + self->attachPendingPublisherPageMarkers(self->currentPageNextY); - self->pushCssAncestor(self->depth, name, classAttr); - self->depth += 1; + // Create ImageBlock and add to page + auto imageBlock = makeUniqueNoThrow(std::move(cachedImagePath), std::move(sourcePath), + displayWidth, displayHeight); + if (!imageBlock) { + LOG_ERR("EHP", "Failed to create ImageBlock"); + self->lowMemoryAbort = true; return; - } else { - LOG_ERR("EHP", "Failed to get image dimensions"); - Storage.remove(cachedImagePath.c_str()); } + int xPos = (self->viewportWidth - displayWidth) / 2; + auto pageImage = makeUniqueNoThrow(std::move(imageBlock), xPos, self->currentPageNextY); + if (!pageImage) { + LOG_ERR("EHP", "Failed to create PageImage"); + self->lowMemoryAbort = true; + return; + } + self->currentPage->elements.push_back(std::move(pageImage)); + self->setCurrentPageVisibleOffset(self->visibleTextOffset); + self->markCurrentPageFromCurrentElement(); + self->currentPageNextY += displayHeight + imageMarginBottom; + + if (self->currentTextBlock && self->currentTextBlock->isEmpty()) { + BlockStyle afterImageStyle = self->blockStyleBuf_[self->blockStyleCount_ - 1].withoutBottom(); + if (self->headingOpenerActive) { + // The title run trailing the ornament copies the heading style too and would inherit + // the same drop margin, stacking a second heading-sized gap under the image. Replace + // it with a modest half-line gap so the title sits just below the ornament instead. + afterImageStyle.marginTop = static_cast(self->effectiveLineHeight() / 2); + afterImageStyle.paddingTop = 0; + } + self->currentTextBlock->setBlockStyle(afterImageStyle); + } + + self->pushCssAncestor(self->depth, name, classAttr); + self->depth += 1; + return; } else { Storage.remove(cachedImagePath.c_str()); const uint32_t postFailureFreeHeap = ESP.getFreeHeap(); @@ -1783,7 +2136,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* LOG_ERR("EHP", "Disabling remaining image extraction after failure (%u free, %u max alloc)", postFailureFreeHeap, postFailureMaxAllocHeap); } - LOG_ERR("EHP", "Failed to extract image"); + LOG_ERR("EHP", "Failed to get image dimensions"); } } // isFormatSupported } @@ -1810,6 +2163,32 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } } + // Ruby tag handling + if (strcmp(name, "ruby") == 0) { + // is an inline element: a base that follows text with no whitespace between them + // continues the same visual word, exactly like / handling in endElement(). + if (self->partWordBufferIndex > 0) { + self->flushPartWordBuffer(); + self->nextWordContinues = true; + } + self->inRuby = true; + self->rubyStartWordIndex = self->currentTextBlock ? static_cast(self->currentTextBlock->size()) : 0; + if (self->currentTextBlock) { + self->currentTextBlock->ensureRubyCapacity(); + } + self->rubyTextBuffer.clear(); + self->depth += 1; + return; + } + if (strcmp(name, "rt") == 0) { + if (self->partWordBufferIndex > 0) { + self->flushPartWordBuffer(); + } + self->collectingRubyText = true; + self->depth += 1; + return; + } + if (matches(name, SKIP_TAGS, std::size(SKIP_TAGS))) { // start skip self->skipCurrentElement(); @@ -1843,6 +2222,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* strncpy(self->currentFootnote.href, href, sizeof(self->currentFootnote.href) - 1); self->currentFootnote.href[sizeof(self->currentFootnote.href) - 1] = '\0'; self->currentFootnote.number[0] = '\0'; + self->currentFootnote.linkId = self->nextFootnoteLinkId; + self->nextFootnoteLinkId = static_cast((self->nextFootnoteLinkId % 63) + 1); self->currentFootnoteLinkTextLen = 0; // Apply underline style to visually indicate the link @@ -1856,6 +2237,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.backgroundBlack = cssStyle.backgroundBlack; } applyDirectionToEntry(entry, cssStyle); + applySmallCapsToEntry(entry, cssStyle); if (self->inlineStyleCount_ < MAX_INLINE_STYLE_DEPTH) { self->inlineStyleBuf_[self->inlineStyleCount_++] = entry; } else { @@ -1966,13 +2348,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* // flush word preceding
    to currentTextBlock before calling startNewTextBlock self->flushPartWordBuffer(); } - // Tag the new block so startNewTextBlock can inject a full line-height gap if - // the block remains empty (i.e.
    is a section separator between paragraphs). - // If the block gets text added before the next block opens it becomes non-empty, - // goes through makePages() normally, and the flag has no effect (inline
    case). - BlockStyle brStyle = self->blockStyleBuf_[self->blockStyleCount_ - 1].withoutBottom(); - if (self->currentTextBlock) { - brStyle = self->currentTextBlock->getBlockStyle(); + // A
    after text is a line break: start the next block without the + // container's vertical margins, which browsers do not re-apply at a + // line break. A
    that leaves its block empty (a consecutive or + // standalone
    ) retains those margins and becomes a scene break. + // Use the active stack style rather than the current block so styles + // from a closed element cannot leak into the next paragraph. + BlockStyle brStyle = self->blockStyleBuf_[self->blockStyleCount_ - 1]; + if (self->currentTextBlock && !self->currentTextBlock->isEmpty()) { + brStyle = brStyle.withoutTop().withoutBottom(); } brStyle.fromBrElement = true; self->startNewTextBlock(brStyle); @@ -2001,7 +2385,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (strcmp(name, "li") == 0) { self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR, false, false, - self->honorsPublisherDecorations() && self->effectiveBackgroundBlack); + self->honorsPublisherDecorations() && self->effectiveBackgroundBlack, 0, + self->visibleTextOffset); self->pendingListMarkerDepth = self->depth; } } @@ -2030,6 +2415,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.backgroundBlack = cssStyle.backgroundBlack; } applyDirectionToEntry(entry, cssStyle); + applySmallCapsToEntry(entry, cssStyle); if (self->inlineStyleCount_ < MAX_INLINE_STYLE_DEPTH) { self->inlineStyleBuf_[self->inlineStyleCount_++] = entry; } else { @@ -2061,6 +2447,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.backgroundBlack = cssStyle.backgroundBlack; } applyDirectionToEntry(entry, cssStyle); + applySmallCapsToEntry(entry, cssStyle); if (self->inlineStyleCount_ < MAX_INLINE_STYLE_DEPTH) { self->inlineStyleBuf_[self->inlineStyleCount_++] = entry; } else { @@ -2094,6 +2481,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.backgroundBlack = cssStyle.backgroundBlack; } applyDirectionToEntry(entry, cssStyle); + applySmallCapsToEntry(entry, cssStyle); if (self->inlineStyleCount_ < MAX_INLINE_STYLE_DEPTH) { self->inlineStyleBuf_[self->inlineStyleCount_++] = entry; } else { @@ -2127,6 +2515,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.backgroundBlack = cssStyle.backgroundBlack; } applyDirectionToEntry(entry, cssStyle); + applySmallCapsToEntry(entry, cssStyle); if (self->inlineStyleCount_ < MAX_INLINE_STYLE_DEPTH) { self->inlineStyleBuf_[self->inlineStyleCount_++] = entry; } else { @@ -2148,6 +2537,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.sub = true; } ChapterHtmlSlimParser::applyDirectionToEntry(entry, cssStyle); + ChapterHtmlSlimParser::applySmallCapsToEntry(entry, cssStyle); if (self->inlineStyleCount_ < MAX_INLINE_STYLE_DEPTH) { self->inlineStyleBuf_[self->inlineStyleCount_++] = entry; } else { @@ -2157,7 +2547,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } else if (strcmp(name, "span") == 0 || !isHeaderOrBlock(name)) { // Handle span and other inline elements for CSS styling if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration() || - cssStyle.hasBackgroundBlack() || cssStyle.hasVerticalAlign() || cssStyle.hasDirection()) { + cssStyle.hasBackgroundBlack() || cssStyle.hasVerticalAlign() || cssStyle.hasDirection() || + cssStyle.hasFontVariantCaps()) { // Flush buffer before style change so preceding text gets current style if (self->partWordBufferIndex > 0) { self->flushPartWordBuffer(); @@ -2184,6 +2575,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.backgroundBlack = cssStyle.backgroundBlack; } applyDirectionToEntry(entry, cssStyle); + applySmallCapsToEntry(entry, cssStyle); if (cssStyle.hasVerticalAlign()) { if (cssStyle.verticalAlign == CssVerticalAlign::Super) { entry.hasSup = true; @@ -2213,9 +2605,6 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char if (self->isScanningForPreviewAnchor() || self->previewStopRequested) { return; } - if (self->tableDepth == 1) { - self->fallbackCurrentTableBufferIfNeeded("low heap while buffering table"); - } if (self->shouldAbortForLowMemory("character data")) { return; } @@ -2230,6 +2619,26 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char return; } + // Keep the source coordinate independent of wrapping, fonts, and orientation. + // `head`/`rp` are skipped above; synthetic table labels and ruby annotations do + // not represent the document body position. + const uint32_t callbackVisibleOffset = self->visibleTextOffset; + const bool countVisibleOffsets = !self->syntheticCharacterData && !self->collectingRubyText; + if (countVisibleOffsets) { + const auto* ptr = reinterpret_cast(s); + const auto* const end = ptr + len; + while (ptr < end) { + utf8NextCodepoint(&ptr); + self->visibleTextOffset++; + } + } + + // Collect ruby text instead of normal word processing + if (self->collectingRubyText) { + self->rubyTextBuffer.append(s, len); + return; + } + // Collect footnote link display text (for the number label) // Skip whitespace and brackets to normalize noterefs like "[1]" → "1" if (self->insideFootnoteLink) { @@ -2258,7 +2667,9 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char self->currentFootnote.number[self->currentFootnoteLinkTextLen] = '\0'; } + uint32_t codepointOffset = callbackVisibleOffset; for (int i = 0; i < len; i++) { + const bool startsCodepoint = (static_cast(s[i]) & 0xC0) != 0x80; if (isWhitespace(s[i])) { // Currently looking at whitespace, if there's anything in the partWordBuffer, flush it if (self->partWordBufferIndex > 0) { @@ -2266,6 +2677,7 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char } // Whitespace is a real word boundary — reset continuation state self->nextWordContinues = false; + if (startsCodepoint && countVisibleOffsets) codepointOffset++; // Skip the whitespace char continue; } @@ -2296,12 +2708,14 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char self->partWordBuffer[0] = ' '; self->partWordBuffer[1] = '\0'; self->partWordBufferIndex = 1; + self->partWordVisibleOffset = codepointOffset; self->nextWordContinues = true; // Attach space to previous word (no break). self->flushPartWordBuffer(); self->nextWordContinues = true; // Next real word attaches to this space (no break). i++; // Skip the second byte (0xA0) + if (countVisibleOffsets) codepointOffset++; continue; } @@ -2315,12 +2729,14 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char self->partWordBuffer[0] = ' '; self->partWordBuffer[1] = '\0'; self->partWordBufferIndex = 1; + self->partWordVisibleOffset = codepointOffset; self->nextWordContinues = true; self->flushPartWordBuffer(); self->nextWordContinues = true; i += 2; // Skip the remaining two bytes (0x80 0xAF) + if (countVisibleOffsets) codepointOffset++; continue; } @@ -2349,22 +2765,36 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char if (safeLen < self->partWordBufferIndex && safeLen > 0) { // Incomplete UTF-8 sequence at the end — save it before flushing int overflow = self->partWordBufferIndex - safeLen; + uint32_t overflowOffset = self->partWordVisibleOffset; + const auto* codepoint = reinterpret_cast(self->partWordBuffer); + const auto* const prefixEnd = codepoint + safeLen; + while (codepoint < prefixEnd) { + utf8NextCodepoint(&codepoint); + overflowOffset++; + } char saved[4]; for (int j = 0; j < overflow; j++) { saved[j] = self->partWordBuffer[safeLen + j]; } self->partWordBufferIndex = safeLen; self->flushPartWordBuffer(); + self->nextWordContinues = true; for (int j = 0; j < overflow; j++) { self->partWordBuffer[j] = saved[j]; } self->partWordBufferIndex = overflow; + self->partWordVisibleOffset = overflowOffset; } else { self->flushPartWordBuffer(); + self->nextWordContinues = true; } } + if (self->partWordBufferIndex == 0) { + self->partWordVisibleOffset = codepointOffset; + } self->partWordBuffer[self->partWordBufferIndex++] = s[i]; + if (startsCodepoint && countVisibleOffsets) codepointOffset++; } // If a paragraph keeps growing, perform the layout and consume all but the last line. @@ -2422,6 +2852,50 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n return; } + // Ruby text: distributes ruby to base words,
    resets ruby state + if (strcmp(name, "rt") == 0) { + self->collectingRubyText = false; + if (self->inRuby && self->currentTextBlock) { + const int currentWordCount = static_cast(self->currentTextBlock->size()); + const int baseWordCount = currentWordCount - self->rubyStartWordIndex; + std::string cleanRuby = trimAndNormalize(self->rubyTextBuffer); + if (!cleanRuby.empty()) { + if (baseWordCount > 0) { + self->currentTextBlock->setRubyGroupAt(self->rubyStartWordIndex, baseWordCount, cleanRuby); + self->rubyStartWordIndex = currentWordCount; + } else if (self->rubyStartWordIndex > 0) { + int leaderIdx = self->rubyStartWordIndex - 1; + while (leaderIdx >= 0 && + (self->currentTextBlock->getWordStyleAt(leaderIdx) & EpdFontFamily::RUBY_CONTINUE) != 0) { + leaderIdx--; + } + if (leaderIdx >= 0) { + std::string prevRuby = self->currentTextBlock->getRubyTextAt(leaderIdx); + self->currentTextBlock->setRubyForWordAt(leaderIdx, prevRuby + cleanRuby); + } + } + } + } + self->rubyTextBuffer.clear(); + // Inline close: the next base (e.g. 字 in かん) joins the + // preceding one with no space. Whitespace in the source resets this in characterData(). + if (self->currentTextBlock && !self->currentTextBlock->isEmpty()) { + self->nextWordContinues = true; + } + self->depth -= 1; + return; + } + if (strcmp(name, "ruby") == 0 && self->inRuby) { + self->inRuby = false; + self->rubyStartWordIndex = -1; + self->rubyTextBuffer.clear(); + // Inline close: text following joins the annotated base with no space. + if (self->currentTextBlock && !self->currentTextBlock->isEmpty()) { + self->nextWordContinues = true; + } + self->depth -= 1; + return; + } // 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) @@ -2480,6 +2954,7 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n entry.number[sizeof(entry.number) - 1] = '\0'; strncpy(entry.href, self->currentFootnote.href, sizeof(entry.href) - 1); entry.href[sizeof(entry.href) - 1] = '\0'; + entry.linkId = self->currentFootnote.linkId; int wordIndex = self->wordsExtractedInBlock + (self->currentTextBlock ? static_cast(self->currentTextBlock->size()) : 0); self->pendingFootnotes.push_back({wordIndex, entry}); @@ -2505,11 +2980,19 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n } if (self->tableDepth == 1 && (strcmp(name, "tr") == 0)) { + if (self->currentTableBuffer && self->currentTableBuffer->streaming && !self->streamCurrentTableRow()) { + return; + } self->nextWordContinues = false; } if (self->tableDepth == 1 && strcmp(name, "table") == 0) { - self->emitCurrentTableBuffer(); + if (self->currentTableBuffer && self->currentTableBuffer->streaming) { + self->finishStreamingTable(*self->currentTableBuffer); + self->currentTableBuffer.reset(); + } else { + self->emitCurrentTableBuffer(); + } self->tableDepth -= 1; self->tableRowIndex = 0; self->tableColIndex = 0; @@ -2570,6 +3053,9 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n self->blockStyleCount_--; } } + if (self->headingDepth == self->depth) { + self->headingDepth = -1; + } } void ChapterHtmlSlimParser::prewarmSectionAdvanceTable(FsFile& file) const { @@ -2577,6 +3063,13 @@ void ChapterHtmlSlimParser::prewarmSectionAdvanceTable(FsFile& file) const { return; } + const auto heap = MemoryBudget::snapshot(); + if (!MemoryBudget::hasHeap(heap, MIN_FREE_HEAP_FOR_SECTION_PREWARM, MIN_MAX_ALLOC_FOR_SECTION_PREWARM)) { + LOG_DBG("EHP", "Skipping section advance prewarm: low heap (free=%u, maxAlloc=%u, need %u/%u)", heap.freeHeap, + heap.maxAllocHeap, MIN_FREE_HEAP_FOR_SECTION_PREWARM, MIN_MAX_ALLOC_FOR_SECTION_PREWARM); + return; + } + std::unique_ptr codepoints(new (std::nothrow) uint32_t[SECTION_ADVANCE_PREWARM_MAX_CODEPOINTS]); std::unique_ptr buffer(new (std::nothrow) uint8_t[SECTION_ADVANCE_PREWARM_READ_BUFFER_SIZE]); if (!codepoints || !buffer) { @@ -2668,8 +3161,40 @@ void ChapterHtmlSlimParser::prewarmSectionAdvanceTable(FsFile& file) const { ChapterHtmlSlimParser::~ChapterHtmlSlimParser() { abortParse(); } +bool ChapterHtmlSlimParser::ensureInputFileOpen() { + if (parseFile_) { + return true; + } + if (!Storage.openFileForRead("EHP", filepath, parseFile_)) { + return false; + } + if (parseFileOffset_ > 0 && !parseFile_.seek(parseFileOffset_)) { + LOG_ERR("EHP", "Failed to seek parser input to %u", static_cast(parseFileOffset_)); + parseFile_.close(); + return false; + } + if (parseFileSize_ == 0) { + parseFileSize_ = parseFile_.size(); + } + return true; +} + +void ChapterHtmlSlimParser::releaseInputFile() { + if (!parseFile_) { + return; + } + parseFileOffset_ = parseFile_.position(); + parseFile_.close(); +} + bool ChapterHtmlSlimParser::beginParse() { malformedMarkupTruncated = false; + parseFileOffset_ = 0; + parseFileSize_ = 0; + // Runs before the render pass opens the file, so only one reader is ever open at a time. + if (isPreviewBuild()) { + locatePreviewBlockStart(); + } // Initialize block style stack with a root entry representing "no ancestor block elements". // The user's paragraph alignment is set as the default so child elements without explicit // text-align inherit it correctly through getCombinedBlockStyle. @@ -2679,12 +3204,14 @@ bool ChapterHtmlSlimParser::beginParse() { : static_cast(this->paragraphAlignment); if (!parseArena_.init(PARSE_ARENA_SLAB_SIZE)) { LOG_ERR("EHP", "Failed to init parse arena"); + lowMemoryAbort = true; return false; } inlineStyleBuf_ = arenaNewArray(parseArena_, MAX_INLINE_STYLE_DEPTH); blockStyleBuf_ = arenaNewArray(parseArena_, MAX_BLOCK_STYLE_DEPTH); if (!inlineStyleBuf_ || !blockStyleBuf_) { LOG_ERR("EHP", "Parse arena OOM for style stacks"); + lowMemoryAbort = true; parseArena_.release(); return false; } @@ -2713,6 +3240,7 @@ bool ChapterHtmlSlimParser::beginParse() { activeParser = XML_ParserCreate(nullptr); if (!activeParser) { LOG_ERR("EHP", "Couldn't allocate memory for parser"); + lowMemoryAbort = true; parseArena_.release(); inlineStyleBuf_ = nullptr; blockStyleBuf_ = nullptr; @@ -2731,9 +3259,10 @@ bool ChapterHtmlSlimParser::beginParse() { blockStyleBuf_ = nullptr; return false; } + parseFileSize_ = parseFile_.size(); // Get file size to decide whether to show indexing popup. - if (popupFn && parseFile_.size() >= MIN_SIZE_FOR_POPUP) { + if (popupFn && parseFileSize_ >= MIN_SIZE_FOR_POPUP) { popupFn(); } @@ -2752,6 +3281,10 @@ ChapterHtmlSlimParser::ParseStatus ChapterHtmlSlimParser::parseStep() { LOG_ERR("EHP", "parseStep called without an active parser"); return ParseStatus::Error; } + if (!ensureInputFileOpen()) { + LOG_ERR("EHP", "Failed to reopen parser input"); + return ParseStatus::Error; + } void* const buf = XML_GetBuffer(activeParser, PARSE_BUFFER_SIZE); if (!buf) { @@ -2760,6 +3293,7 @@ ChapterHtmlSlimParser::ParseStatus ChapterHtmlSlimParser::parseStep() { } const size_t len = parseFile_.read(buf, PARSE_BUFFER_SIZE); + parseFileOffset_ = parseFile_.position(); if (len == 0 && parseFile_.available() > 0) { LOG_ERR("EHP", "File read error"); return ParseStatus::Error; @@ -2796,6 +3330,8 @@ void ChapterHtmlSlimParser::abortParse() { if (parseFile_) { parseFile_.close(); } + parseFileOffset_ = 0; + parseFileSize_ = 0; parseArena_.release(); inlineStyleBuf_ = nullptr; inlineStyleCount_ = 0; @@ -2811,6 +3347,8 @@ bool ChapterHtmlSlimParser::finishParse() { activeParser = nullptr; } parseFile_.close(); + parseFileOffset_ = 0; + parseFileSize_ = 0; if (malformedMarkupTruncated) { LOG_DBG("EHP", "Malformed markup encountered; finalizing partial chapter content"); @@ -2862,7 +3400,6 @@ bool ChapterHtmlSlimParser::finishParse() { return false; } - LOG_DBG("EHP", "Parse arena used %u bytes", (unsigned)parseArena_.used()); parseArena_.release(); inlineStyleBuf_ = nullptr; blockStyleBuf_ = nullptr; @@ -2887,12 +3424,12 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { return finishParse(); } -void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr line) { +void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr line, const uint32_t visibleOffset) { if (lowMemoryAbort) { return; } - const int lineHeight = effectiveLineHeight(); + const int lineHeight = effectiveLineHeight() + line->getRubyShift(renderer.getFontAscenderSize(fontId)); if (!currentPage) { if (!startNewPage("line layout")) { @@ -2912,11 +3449,25 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr line) { } } - // Track cumulative words to assign footnotes to the page containing their anchor + setCurrentPageVisibleOffset(visibleOffset); + + // Keep a link available on every page where its text is visible. Usually this + // adds one compact entry; a long wrapped link can span lines or pages. + for (uint16_t wordIndex = 0; wordIndex < line->wordCount(); ++wordIndex) { + const uint8_t linkId = line->wordLinkId(wordIndex); + if (linkId == 0) continue; + const auto entry = std::find_if(pendingFootnotes.begin(), pendingFootnotes.end(), + [linkId](const auto& pending) { return pending.second.linkId == linkId; }); + if (entry != pendingFootnotes.end()) { + currentPage->addFootnote(entry->second.number, entry->second.href, entry->second.linkId); + } + } + + // Track cumulative words to retire links after laying out their final word. wordsExtractedInBlock += line->wordCount(); auto footnoteIt = pendingFootnotes.begin(); while (footnoteIt != pendingFootnotes.end() && footnoteIt->first <= wordsExtractedInBlock) { - currentPage->addFootnote(footnoteIt->second.number, footnoteIt->second.href); + currentPage->addFootnote(footnoteIt->second.number, footnoteIt->second.href, footnoteIt->second.linkId); ++footnoteIt; } pendingFootnotes.erase(pendingFootnotes.begin(), footnoteIt); @@ -2924,7 +3475,13 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr line) { // Apply horizontal left inset (margin + padding) as x position offset const int16_t xOffset = line->getBlockStyle().leftInset(); - currentPage->elements.push_back(std::make_shared(line, xOffset, currentPageNextY)); + auto pageLine = makeUniqueNoThrow(line, xOffset, currentPageNextY); + if (!pageLine) { + LOG_ERR("EHP", "Failed to create PageLine"); + lowMemoryAbort = true; + return; + } + currentPage->elements.push_back(std::move(pageLine)); markCurrentPageFromCurrentTextBlock(); currentPageNextY += lineHeight; } @@ -2949,14 +3506,18 @@ void ChapterHtmlSlimParser::makePages() { } } - // Apply top spacing before the paragraph (stored in pixels) + // Apply top spacing before the paragraph (stored in pixels). An + // intermediate text-run flush has already emitted the first lines and + // consumed this spacing, so do not apply it again to the remainder. const BlockStyle& blockStyle = currentTextBlock->getBlockStyle(); const int lineHeight = effectiveLineHeight(); - if (blockStyle.marginTop > 0) { - currentPageNextY += blockStyle.marginTop; - } - if (blockStyle.paddingTop > 0) { - currentPageNextY += blockStyle.paddingTop; + if (!currentTextBlock->isContinuation()) { + if (blockStyle.marginTop > 0) { + currentPageNextY += blockStyle.marginTop; + } + if (blockStyle.paddingTop > 0) { + currentPageNextY += blockStyle.paddingTop; + } } // Calculate effective width accounting for horizontal margins/padding @@ -2965,8 +3526,9 @@ void ChapterHtmlSlimParser::makePages() { (horizontalInset < viewportWidth) ? static_cast(viewportWidth - horizontalInset) : viewportWidth; if (!currentTextBlock->layoutAndExtractLines( - renderer, fontId, effectiveWidth, - [this](const std::shared_ptr& textBlock) { addLineToPage(textBlock); })) { + renderer, fontId, effectiveWidth, [this](const std::shared_ptr& textBlock, const uint32_t offset) { + addLineToPage(textBlock, offset); + })) { LOG_ERR("EHP", "Failed to lay out text block"); lowMemoryAbort = true; return; @@ -2980,7 +3542,7 @@ void ChapterHtmlSlimParser::makePages() { // edge cases where a footnote's word index equals the exact block size. if (!pendingFootnotes.empty() && currentPage) { for (const auto& [idx, fn] : pendingFootnotes) { - currentPage->addFootnote(fn.number, fn.href); + currentPage->addFootnote(fn.number, fn.href, fn.linkId); } pendingFootnotes.clear(); } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 46ab9a1a43..d2bf6b46bc 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -13,30 +14,32 @@ #include "Epub/EpubRenderMode.h" #include "Epub/FootnoteEntry.h" +#include "Epub/Page.h" #include "Epub/ParsedText.h" #include "Epub/blocks/ImageBlock.h" #include "Epub/blocks/TextBlock.h" #include "Epub/css/CssParser.h" #include "Epub/css/CssStyle.h" -class Page; class GfxRenderer; class Epub; - #define MAX_WORD_SIZE 200 class ChapterHtmlSlimParser { + public: + enum class ParseStatus { More, Done, Error }; + + private: static constexpr uint8_t MAX_SIMPLE_TABLE_COLUMNS = 8; - static constexpr uint16_t MAX_SIMPLE_TABLE_CELLS = 64; static constexpr uint16_t MAX_SIMPLE_TABLE_CELL_WORDS = 160; static constexpr uint8_t TABLE_CELL_PADDING = 6; static constexpr size_t MAX_INLINE_STYLE_DEPTH = 64; static constexpr size_t MAX_BLOCK_STYLE_DEPTH = 16; - std::shared_ptr epub; + Epub* epub; const std::string& filepath; GfxRenderer& renderer; - std::function, uint16_t, uint16_t)> completePageFn; + std::function, uint16_t, uint16_t, uint32_t)> completePageFn; std::function popupFn; // Popup callback int depth = 0; int skipUntilDepth = INT_MAX; @@ -51,11 +54,20 @@ class ChapterHtmlSlimParser { // leave one char at end for null pointer char partWordBuffer[MAX_WORD_SIZE + 1] = {}; int partWordBufferIndex = 0; + uint32_t partWordVisibleOffset = 0; + uint32_t visibleTextOffset = 0; uint16_t currentTextRunBytes = 0; bool nextWordContinues = false; // true when next flushed word attaches to previous (inline element boundary) std::unique_ptr currentTextBlock = nullptr; + // Ruby text state + bool inRuby = false; + int rubyStartWordIndex = -1; + bool collectingRubyText = false; + std::string rubyTextBuffer; std::unique_ptr currentPage = nullptr; int16_t currentPageNextY = 0; + uint32_t currentPageVisibleOffset = 0; + bool currentPageVisibleOffsetSet = false; int fontId; float lineCompression; bool extraParagraphSpacing; @@ -66,6 +78,7 @@ class ChapterHtmlSlimParser { bool hyphenationEnabled; bool bionicReadingEnabled; bool guideReadingEnabled; + uint8_t wordSpacing; CssParser* cssParser; bool embeddedStyle; uint8_t imageRendering; @@ -80,11 +93,32 @@ class ChapterHtmlSlimParser { uint16_t previewMaxPages = 0; bool previewAnchorFound = false; bool previewStopRequested = false; + // Element ordinals (1-based, counting every startElement) used to start a footnote preview at the + // block enclosing the anchor rather than at the anchor itself. 0 means "no block located". + uint32_t previewStartOrdinal = 0; + uint32_t previewElementOrdinal = 0; bool malformedMarkupTruncated = false; + bool syntheticCharacterData = false; XML_Parser activeParser = nullptr; FsFile parseFile_; + size_t parseFileOffset_ = 0; + size_t parseFileSize_ = 0; uint32_t parseStartTime_ = 0; + struct PendingImageExtraction { + std::unique_ptr stream; + HalFile file; + std::string tag; + std::string classAttr; + std::string styleAttr; + std::string alt; + std::string cachedImagePath; + bool failed = false; + }; + std::unique_ptr pendingImageExtraction_; + + bool ensureInputFileOpen(); + // Style tracking (replaces depth-based approach) struct StyleStackEntry { int depth = 0; @@ -97,6 +131,7 @@ class ChapterHtmlSlimParser { CssTextDirection direction = CssTextDirection::Ltr; bool hasSup = false, sup = false; bool hasSub = false, sub = false; + bool hasSmallCaps = false, smallCaps = false; }; // Arena-backed style stacks. Initialized in parseAndBuildPages(); pointers are // null before and after each parse. StyleStackEntry and BlockStyle are trivially @@ -116,10 +151,12 @@ class ChapterHtmlSlimParser { CssTextDirection effectiveDirection = CssTextDirection::Ltr; bool effectiveSup = false; bool effectiveSub = false; + bool effectiveSmallCaps = false; struct BufferedTableCell { std::unique_ptr text; std::vector> footnotes; + uint32_t visibleTextOffset = 0; bool isHeader = false; uint8_t colSpan = 1; }; @@ -137,6 +174,17 @@ class ChapterHtmlSlimParser { uint16_t maxCols = 0; uint16_t totalCells = 0; bool unsupported = false; + // When the whole-table reservation is unavailable, retain only the current + // source row plus the render-ready rows that fit on the active page. + bool streaming = false; + bool streamingFlattened = false; + bool streamingTopSpacingApplied = false; + uint8_t streamingColumnCount = 0; + uint8_t streamingFragmentColumnCount = 0; + uint16_t streamingFragmentHeight = 1; + uint32_t streamingFragmentVisibleOffset = 0; + std::vector streamingFragmentRows; + std::vector streamingFragmentFootnotes; }; int tableDepth = 0; @@ -145,6 +193,7 @@ class ChapterHtmlSlimParser { int pendingListMarkerDepth = -1; bool currentTableCellIsHeader = false; uint8_t currentTableCellColSpan = 1; + uint32_t currentTableCellVisibleOffset = 0; std::unique_ptr currentTableBuffer = nullptr; std::vector ancestorStack_; @@ -165,6 +214,7 @@ class ChapterHtmlSlimParser { bool insideFootnoteLink = false; int footnoteLinkDepth = -1; FootnoteEntry currentFootnote = {}; + uint8_t nextFootnoteLinkId = 1; int currentFootnoteLinkTextLen = 0; std::vector> pendingFootnotes; // int wordsExtractedInBlock = 0; @@ -190,12 +240,14 @@ class ChapterHtmlSlimParser { uint16_t textRunBytesBeforeLayoutLimit() const; void markCurrentPageFromCurrentTextBlock(); void markCurrentPageFromCurrentElement(); + void setCurrentPageVisibleOffset(uint32_t offset); void completeCurrentPage(); void makePages(); int effectiveLineHeight() const; bool isPreviewBuild() const { return !previewAnchor.empty() && previewMaxPages > 0; } bool isScanningForPreviewAnchor() const { return isPreviewBuild() && !previewAnchorFound; } bool handlePreviewScanStart(const XML_Char** atts); + void locatePreviewBlockStart(); void startPreviewAtAnchor(); void stopPreviewIfPageLimitReached(); bool usesSimpleCssLookup() const { return renderMode != EpubRenderMode::CrossInkDefault; } @@ -204,16 +256,26 @@ class ChapterHtmlSlimParser { bool honorsPublisherDecorations() const { return renderMode != EpubRenderMode::Light; } void pushCssAncestor(int depth, const char* tag, std::string_view classAttr); static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css); + static void applySmallCapsToEntry(StyleStackEntry& entry, const CssStyle& css); void emitHorizontalRule(const BlockStyle& blockStyle); void finalizeCurrentTableCell(); void emitBufferedTableAsParagraphs(BufferedTable& table); void emitBufferedTableAsFragments(BufferedTable& table); + bool streamCurrentTableRow(); + bool flushStreamingTableFragment(BufferedTable& table); + void emitStreamingTableRowsAsParagraphs(BufferedTable& table); + void finishStreamingTable(BufferedTable& table); + void fallbackStreamingTableToParagraphs(const char* reason); void emitCurrentTableBuffer(); void fallbackCurrentTableBufferToParagraphs(const char* reason); - void fallbackCurrentTableBufferIfNeeded(const char* stage); void flushMalformedPartialContent(); bool appendMalformedMarkupWarningPage(); void prewarmSectionAdvanceTable(FsFile& file) const; + bool startImageExtraction(const char* tag, std::string_view classAttr, std::string_view styleAttr, + const std::string& alt, const std::string& resolvedPath); + ParseStatus pumpPendingImageExtraction(); + bool finishPendingImageExtraction(PendingImageExtraction& pending); + void fallbackPendingImage(PendingImageExtraction& pending); // XML callbacks static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts); static void XMLCALL characterData(void* userData, const XML_Char* s, int len); @@ -222,17 +284,18 @@ class ChapterHtmlSlimParser { public: explicit ChapterHtmlSlimParser( - std::shared_ptr epub, const std::string& filepath, GfxRenderer& renderer, const int fontId, - const float lineCompression, const bool extraParagraphSpacing, const bool forceParagraphIndents, - const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, - const bool hyphenationEnabled, const bool bionicReadingEnabled, const bool guideReadingEnabled, - const std::function, uint16_t, uint16_t)>& completePageFn, const bool embeddedStyle, - const std::string& contentBase, const std::string& imageBasePath, const uint8_t imageRendering = 0, - std::vector tocAnchors = {}, const std::function& popupFn = nullptr, - CssParser* cssParser = nullptr, const EpubRenderMode renderMode = EpubRenderMode::CrossInkDefault, - std::string previewAnchor = {}, const uint16_t previewMaxPages = 0) + Epub& epub, const std::string& filepath, GfxRenderer& renderer, const int fontId, const float lineCompression, + const bool extraParagraphSpacing, const bool forceParagraphIndents, const uint8_t paragraphAlignment, + const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled, + const bool bionicReadingEnabled, const bool guideReadingEnabled, const uint8_t wordSpacing, + const std::function, uint16_t, uint16_t, uint32_t)>& completePageFn, + const bool embeddedStyle, const std::string& contentBase, const std::string& imageBasePath, + const uint8_t imageRendering = 0, std::vector tocAnchors = {}, + const std::function& popupFn = nullptr, CssParser* cssParser = nullptr, + const EpubRenderMode renderMode = EpubRenderMode::CrossInkDefault, std::string previewAnchor = {}, + const uint16_t previewMaxPages = 0) - : epub(epub), + : epub(&epub), filepath(filepath), renderer(renderer), fontId(fontId), @@ -245,6 +308,7 @@ class ChapterHtmlSlimParser { hyphenationEnabled(hyphenationEnabled), bionicReadingEnabled(bionicReadingEnabled), guideReadingEnabled(guideReadingEnabled), + wordSpacing(wordSpacing > 4 ? 4 : wordSpacing), completePageFn(completePageFn), popupFn(popupFn), cssParser(cssParser), @@ -259,16 +323,20 @@ class ChapterHtmlSlimParser { ~ChapterHtmlSlimParser(); bool parseAndBuildPages(); - const std::vector>& getAnchors() const { return anchorData; } - bool wasLowMemoryFallbackTriggered() const { return lowMemoryImageFallback; } - bool wasLowMemoryAbortTriggered() const { return lowMemoryAbort; } - - private: - enum class ParseStatus { More, Done, Error }; bool beginParse(); ParseStatus parseStep(); bool finishParse(); // flush the trailing page and tear down; returns true void abortParse(); // tear down without flushing (error / abandon) + void releaseInputFile(); + + void addLineToPage(std::shared_ptr line, uint32_t visibleOffset); + const std::vector>& getAnchors() const { return anchorData; } + bool wasLowMemoryFallbackTriggered() const { return lowMemoryImageFallback; } + bool wasLowMemoryAbortTriggered() const { return lowMemoryAbort; } - void addLineToPage(std::shared_ptr line); + // Byte progress of the in-flight parse, used to estimate a still-building section's total page + // count (a giant single-spine book never fully lays out, so its real count is unknown). Valid + // between beginParse() and finishParse()/abortParse(). + size_t parseBytesConsumed() { return parseFile_ ? parseFile_.position() : parseFileOffset_; } + size_t parseTotalBytes() { return parseFileSize_; } }; diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 5c458947d8..c8e9b30063 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -37,12 +37,14 @@ bool ContentOpfParser::setup() { if (!itemIndexArena.init(ITEM_INDEX_ARENA_SLAB_BYTES)) { LOG_ERR("COF", "Failed to allocate manifest index arena (%u bytes)", static_cast(ITEM_INDEX_ARENA_SLAB_BYTES)); + lowMemoryFailure = true; return false; } parser = XML_ParserCreate(nullptr); if (!parser) { LOG_DBG("COF", "Couldn't allocate memory for parser"); + lowMemoryFailure = true; return false; } @@ -76,6 +78,7 @@ size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) { if (!buf) { LOG_ERR("COF", "Couldn't allocate memory for buffer"); + lowMemoryFailure = true; destroyXmlParser(parser); return 0; } @@ -147,6 +150,9 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error."); } + // Sort the compact item index so every idref lookup uses binary search. + // The temp file stores hash/length plus href, avoiding a second full copy + // of every manifest ID. std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) { return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen); }); @@ -158,7 +164,6 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) { self->state = IN_GUIDE; // TODO Remove print - LOG_DBG("COF", "Entering guide state."); if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error."); } @@ -210,6 +215,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (!self->itemIndex.push_back(entry)) { LOG_ERR("COF", "Manifest index arena OOM at %zu items", self->itemIndex.size()); self->parseFailed = true; + self->lowMemoryFailure = true; if (self->parser) { XML_StopParser(self->parser, XML_FALSE); } @@ -243,7 +249,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name } // Collect CSS files - if (mediaType == MEDIA_TYPE_CSS) { + if (self->collectCssFiles && mediaType == MEDIA_TYPE_CSS) { self->cssFiles.push_back(href); } @@ -252,7 +258,6 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name // Properties is space-separated, check if "nav" is present as a word if (properties == "nav" || properties.find("nav ") == 0 || properties.find(" nav") != std::string::npos) { self->tocNavPath = href; - LOG_DBG("COF", "Found EPUB 3 nav document: %s", href.c_str()); } } @@ -319,11 +324,14 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name } } if (!guideHref.empty()) { - if (type == "text" || (type == "start" && self->textReferenceHref.empty())) { + // EPUB 2 guides often mark every content file as "text", so that type + // does not identify a reliable first-reading location. Only use the + // explicit "start" semantic; otherwise the reader opens at spine index 0. + if (type == "start" && !self->hasExplicitStartReference) { LOG_DBG("COF", "Found %s reference in guide: %s", type.c_str(), guideHref.c_str()); self->textReferenceHref = guideHref; + self->hasExplicitStartReference = true; } else if ((type == "cover" || type == "cover-page") && self->guideCoverPageHref.empty()) { - LOG_DBG("COF", "Found cover reference in guide: %s", guideHref.c_str()); self->guideCoverPageHref = guideHref; } } diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.h b/lib/Epub/Epub/parsers/ContentOpfParser.h index 0fe6c1fbb0..cd77a3f6ba 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.h +++ b/lib/Epub/Epub/parsers/ContentOpfParser.h @@ -34,6 +34,9 @@ class ContentOpfParser final : public Print { std::string coverItemId; Arena itemIndexArena; bool parseFailed = false; + bool lowMemoryFailure = false; + bool hasExplicitStartReference = false; + bool collectCssFiles = true; // Index for compact idref->href lookup. The temp manifest rows store only // hash/length plus href, not a second full copy of every manifest ID. @@ -81,15 +84,17 @@ class ContentOpfParser final : public Print { std::vector cssFiles; // CSS stylesheet paths explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize, - BookMetadataCache* cache) + BookMetadataCache* cache, const bool collectCssFiles = true) : cachePath(cachePath), baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache), + collectCssFiles(collectCssFiles), itemIndex(itemIndexArena) {} ~ContentOpfParser() override; bool setup(); + bool failedForLowMemory() const { return lowMemoryFailure; } size_t write(uint8_t) override; size_t write(const uint8_t* buffer, size_t size) override; diff --git a/lib/Epub/Epub/parsers/PreviewBlockLocator.cpp b/lib/Epub/Epub/parsers/PreviewBlockLocator.cpp new file mode 100644 index 0000000000..cf2a2812c9 --- /dev/null +++ b/lib/Epub/Epub/parsers/PreviewBlockLocator.cpp @@ -0,0 +1,78 @@ +#include "PreviewBlockLocator.h" + +#include + +PreviewBlockLocator::PreviewBlockLocator(const char* anchorId, const IsBlockTagFn isBlockTag) + : anchorId_(anchorId), isBlockTag_(isBlockTag) { + if (!anchorId_ || anchorId_[0] == '\0' || !isBlockTag_) { + return; + } + + parser_ = XML_ParserCreate(nullptr); + if (!parser_) { + return; + } + XML_SetUserData(parser_, this); + XML_SetElementHandler(parser_, handleStartElement, handleEndElement); +} + +PreviewBlockLocator::~PreviewBlockLocator() { + if (parser_) { + XML_ParserFree(parser_); + } +} + +bool PreviewBlockLocator::feed(const char* data, const int len, const bool isFinal) { + if (!parser_ || done()) { + return true; + } + + const XML_Status status = XML_Parse(parser_, data, len, isFinal ? XML_TRUE : XML_FALSE); + if (status != XML_STATUS_OK) { + // XML_StopParser in the start handler reports XML_ERROR_ABORTED; that is our success path. + return XML_GetErrorCode(parser_) == XML_ERROR_ABORTED; + } + return true; +} + +void XMLCALL PreviewBlockLocator::handleStartElement(void* userData, const XML_Char* name, const XML_Char** atts) { + auto* self = static_cast(userData); + self->ordinal_ += 1; + + const char* id = nullptr; + for (int i = 0; atts && atts[i]; i += 2) { + if (strcmp(atts[i], "id") == 0) { + id = atts[i + 1]; + break; + } + } + + if (id && strcmp(id, self->anchorId_) == 0) { + // A block that carries the anchor itself is already the right starting point. An inline anchor + // resolves outwards to its innermost enclosing block so that block's leading text is kept. + self->startOrdinal_ = (self->isBlockTag_(name) || self->openBlockCount_ == 0) + ? self->ordinal_ + : self->openBlockOrdinals_[self->openBlockCount_ - 1]; + XML_StopParser(self->parser_, XML_FALSE); + return; + } + + if (self->isBlockTag_(name) && self->openBlockCount_ < MAX_BLOCK_NESTING) { + self->openBlockOrdinals_[self->openBlockCount_] = self->ordinal_; + self->openBlockDepths_[self->openBlockCount_] = self->depth_; + self->openBlockCount_ += 1; + } + self->depth_ += 1; +} + +void XMLCALL PreviewBlockLocator::handleEndElement(void* userData, const XML_Char* name) { + (void)name; + auto* self = static_cast(userData); + if (self->depth_ > 0) { + self->depth_ -= 1; + } + // The innermost open block closes when depth returns to the depth it was opened at. + if (self->openBlockCount_ > 0 && self->openBlockDepths_[self->openBlockCount_ - 1] == self->depth_) { + self->openBlockCount_ -= 1; + } +} diff --git a/lib/Epub/Epub/parsers/PreviewBlockLocator.h b/lib/Epub/Epub/parsers/PreviewBlockLocator.h new file mode 100644 index 0000000000..e2ffbb748e --- /dev/null +++ b/lib/Epub/Epub/parsers/PreviewBlockLocator.h @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include + +// Resolves a footnote-preview anchor id to the element the preview should start rendering at. +// +// Thee target of a cross-reference is sometimes put on an empty inline element in +// the middle of a note paragraph (`(p.
    81)`). Rendering from that +// element drops the paragraph's leading text, so the preview starts mid-sentence. This locator +// walks the note file and reports the innermost enclosing block-level element instead. +// +// Elements are identified by ordinal (1-based, counting every start tag) so the render pass can +// find the same element without re-reading attributes: it counts start tags the same way and +// begins rendering when the ordinals match. +// +// The caller streams the file in through feed(); file access is deliberately kept outside so this +// stays host-testable. +class PreviewBlockLocator { + public: + // Returns true for tags that open a block-level box. Injected so the locator does not need the + // renderer's tag tables, and so tests can supply their own. + using IsBlockTagFn = bool (*)(const char* tagName); + + // anchorId must outlive the locator. + PreviewBlockLocator(const char* anchorId, IsBlockTagFn isBlockTag); + ~PreviewBlockLocator(); + + PreviewBlockLocator(const PreviewBlockLocator&) = delete; + PreviewBlockLocator& operator=(const PreviewBlockLocator&) = delete; + + // False if the parser could not be created; feed() is a no-op in that case. + bool ok() const { return parser_ != nullptr; } + + // True once the anchor has been resolved. Stop feeding. + bool done() const { return startOrdinal_ != 0; } + + // Feeds one chunk. Returns false on XML error, which the caller should treat as "not located". + bool feed(const char* data, int len, bool isFinal); + + // Ordinal of the element to start rendering at, or 0 if the anchor was never found. + uint32_t startOrdinal() const { return startOrdinal_; } + + private: + // Block nesting deeper than this is not tracked; an anchor below it resolves to the innermost + // block that is still tracked, which still beats starting at the anchor itself. Mirrors the + // block-style stack depth in ChapterHtmlSlimParser. + static constexpr uint8_t MAX_BLOCK_NESTING = 16; + + static void XMLCALL handleStartElement(void* userData, const XML_Char* name, const XML_Char** atts); + static void XMLCALL handleEndElement(void* userData, const XML_Char* name); + + const char* anchorId_; + IsBlockTagFn isBlockTag_; + XML_Parser parser_ = nullptr; + int depth_ = 0; + uint32_t ordinal_ = 0; + uint32_t startOrdinal_ = 0; + + // Ordinal and depth of each currently open block-level element, innermost last. + uint32_t openBlockOrdinals_[MAX_BLOCK_NESTING] = {}; + int openBlockDepths_[MAX_BLOCK_NESTING] = {}; + uint8_t openBlockCount_ = 0; +}; diff --git a/lib/Epub/Epub/parsers/TocNavParser.cpp b/lib/Epub/Epub/parsers/TocNavParser.cpp index 325dd40561..9e5b2a613b 100644 --- a/lib/Epub/Epub/parsers/TocNavParser.cpp +++ b/lib/Epub/Epub/parsers/TocNavParser.cpp @@ -73,7 +73,6 @@ void XMLCALL TocNavParser::startElement(void* userData, const XML_Char* name, co for (int i = 0; atts[i]; i += 2) { if ((strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) && strcmp(atts[i + 1], "toc") == 0) { self->state = IN_NAV_TOC; - LOG_DBG("NAV", "Found nav toc element"); return; } } @@ -165,7 +164,6 @@ void XMLCALL TocNavParser::endElement(void* userData, const XML_Char* name) { if (strcmp(name, "nav") == 0 && self->state >= IN_NAV_TOC) { self->state = IN_BODY; - LOG_DBG("NAV", "Finished parsing nav toc"); return; } } diff --git a/lib/FileIndex/FileIndex.cpp b/lib/FileIndex/FileIndex.cpp index efab2e1154..7e39c42074 100644 --- a/lib/FileIndex/FileIndex.cpp +++ b/lib/FileIndex/FileIndex.cpp @@ -79,7 +79,6 @@ bool FileIndex::open(const char* dirPath, AcceptFn accept) { } if (loadExisting(dirPath, signature, dirs, files)) { - LOG_DBG("FIDX", "index valid for %s (%u dirs, %u files)", dirPath, hdr.dirCount, hdr.fileCount); return true; } diff --git a/lib/GfxRenderer/FontCacheManager.cpp b/lib/GfxRenderer/FontCacheManager.cpp index 08bd9c423c..dcd71a442b 100644 --- a/lib/GfxRenderer/FontCacheManager.cpp +++ b/lib/GfxRenderer/FontCacheManager.cpp @@ -19,11 +19,13 @@ void FontCacheManager::clearCache() { } } -bool FontCacheManager::prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask) { +bool FontCacheManager::prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask, + const PreparationPolicy policy) { // SD card font prewarm path: prewarm all requested styles in one call auto it = sdCardFonts_.find(fontId); if (it != sdCardFonts_.end()) { - int missed = it->second->prewarm(utf8Text, styleMask); + int missed = + it->second->prewarm(utf8Text, styleMask, /*metadataOnly=*/false, policy != PreparationPolicy::DictionaryLean); if (missed > 0) { LOG_DBG("FCM", "prewarmCache(SD): %d glyph(s) not found (styleMask=0x%02X)", missed, styleMask); } @@ -64,7 +66,13 @@ void FontCacheManager::resetStats() { bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; } void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) { - scanText_ += text; + if ((style & EpdFontFamily::SMALL_CAPS) != 0) { + for (const char* p = text; *p != '\0'; ++p) { + scanText_.push_back((*p >= 'a' && *p <= 'z') ? static_cast(*p - ('a' - 'A')) : *p); + } + } else { + scanText_ += text; + } if (scanFontId_ < 0) scanFontId_ = fontId; const uint8_t baseStyle = static_cast(style) & 0x03; const unsigned char* p = reinterpret_cast(text); @@ -78,7 +86,8 @@ void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::S // --- PrewarmScope implementation --- -FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manager_(&manager) { +FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager, const PreparationPolicy policy) + : manager_(&manager), policy_(policy) { manager_->scanMode_ = ScanMode::Scanning; manager_->clearCache(); manager_->resetStats(); @@ -99,7 +108,7 @@ bool FontCacheManager::PrewarmScope::endScanAndPrewarm() { } if (styleMask == 0) styleMask = 1; // default to regular - const bool ok = manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_.c_str(), styleMask); + const bool ok = manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_.c_str(), styleMask, policy_); // Keep the grown capacity around so the next page can reuse it without // another allocate-grow-shrink cycle. @@ -115,8 +124,10 @@ FontCacheManager::PrewarmScope::~PrewarmScope() { } FontCacheManager::PrewarmScope::PrewarmScope(PrewarmScope&& other) noexcept - : manager_(other.manager_), active_(other.active_) { + : manager_(other.manager_), policy_(other.policy_), active_(other.active_) { other.active_ = false; } -FontCacheManager::PrewarmScope FontCacheManager::createPrewarmScope() { return PrewarmScope(*this); } +FontCacheManager::PrewarmScope FontCacheManager::createPrewarmScope(const PreparationPolicy policy) { + return PrewarmScope(*this, policy); +} diff --git a/lib/GfxRenderer/FontCacheManager.h b/lib/GfxRenderer/FontCacheManager.h index e75ae6ec1e..3a6cbefd78 100644 --- a/lib/GfxRenderer/FontCacheManager.h +++ b/lib/GfxRenderer/FontCacheManager.h @@ -11,12 +11,15 @@ class SdCardFont; class FontCacheManager { public: + enum class PreparationPolicy : uint8_t { Normal, DictionaryLean }; + FontCacheManager(const std::map& fontMap, const std::map& sdCardFonts); void setFontDecompressor(FontDecompressor* d); void clearCache(); - bool prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F); + bool prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F, + PreparationPolicy policy = PreparationPolicy::Normal); void logStats(const char* label = "render"); void resetStats(); @@ -30,7 +33,7 @@ class FontCacheManager { // RAII scope for two-pass prewarm pattern class PrewarmScope { public: - explicit PrewarmScope(FontCacheManager& manager); + explicit PrewarmScope(FontCacheManager& manager, PreparationPolicy policy); ~PrewarmScope(); bool endScanAndPrewarm(); PrewarmScope(PrewarmScope&& other) noexcept; @@ -40,9 +43,10 @@ class FontCacheManager { private: FontCacheManager* manager_; + PreparationPolicy policy_; bool active_ = true; }; - PrewarmScope createPrewarmScope(); + PrewarmScope createPrewarmScope(PreparationPolicy policy = PreparationPolicy::Normal); private: const std::map& fontMap_; diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 0419f27a01..d010cd64d8 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -1,6 +1,7 @@ #include "GfxRenderer.h" #include +#include #include #include #include @@ -35,6 +36,71 @@ int32_t resolveSdCardAdvanceFP(const SdCardFont& sdFont, const EpdFontFamily& fo const EpdGlyph* glyph = font.getGlyph(cp, style); return glyph ? glyph->advanceX : 0; } + +int32_t halfAdvanceFP(const int32_t advanceFP) { return (advanceFP + 1) / 2; } + +int32_t smallCapsAdvanceFP(const int32_t advanceFP) { + return advanceFP >= 0 ? (advanceFP * 3 + 2) / 4 : (advanceFP * 3 - 2) / 4; +} + +int scaleSmallCapsMetric(const int value) { return value * 3 / 4; } + +int smallCapsScaledExtent(const int value) { return (value * 3 + 3) / 4; } + +int scaled75SourceEnd(const int dst, const int srcLimit) { + const int srcStart = dst * 4 / 3; + return std::min(srcLimit, std::max(srcStart + 1, ((dst + 1) * 4 + 2) / 3)); +} + +void draw2BitFontPixel(const GfxRenderer& renderer, const GfxRenderer::RenderMode renderMode, const int x, const int y, + const uint8_t raw, const bool pixelState) { + const uint8_t bmpVal = 3 - raw; + if (renderMode == GfxRenderer::BW && bmpVal < 3) { + renderer.drawPixel(x, y, pixelState); + } else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { + renderer.drawPixel(x, y, false); + } else if (renderMode == GfxRenderer::GRAYSCALE_LSB && bmpVal == 1) { + renderer.drawPixel(x, y, false); + } +} + +const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir); + +// Appends the shaped visual form of every RTL token in `text` to `shapedOut`. +// getTextAdvanceX() measures the bidi-reordered, Arabic-shaped codepoint stream, +// so the SD advance table must be warmed with the presentation forms as well as +// the logical codepoints — otherwise every RTL word measurement misses the fast +// path and falls through to onGlyphMiss(), which opens the .cpfont and reads +// glyph metadata + bitmap into the 8-slot overflow ring, once per glyph. +// Tokens without RTL lead bytes (0xD6-0xDB) are skipped with a byte scan, so +// pure-LTR text pays almost nothing. +template +void forEachShapedRtlToken(const char* text, std::string& tokenScratch, std::string& visualScratch, Sink&& sink) { + const auto isBreak = [](const char c) { return c == ' ' || c == '\n' || c == '\r' || c == '\t'; }; + const char* p = text; + while (*p) { + while (*p && isBreak(*p)) ++p; + const char* start = p; + bool hasRtlBytes = false; + while (*p && !isBreak(*p)) { + const auto b = static_cast(*p); + hasRtlBytes = hasRtlBytes || (b >= 0xD6 && b <= 0xDB); + ++p; + } + if (!hasRtlBytes) continue; + tokenScratch.assign(start, p - start); + if (BidiUtils::applyBidiVisual(tokenScratch.c_str(), visualScratch, + static_cast(BidiUtils::BidiBaseDir::AUTO))) { + sink(visualScratch.c_str()); + } + } +} + +void appendShapedRtlTokens(const char* text, std::string& shapedOut) { + std::string token; + std::string visual; + forEachShapedRtlToken(text, token, visual, [&shapedOut](const char* shaped) { shapedOut += shaped; }); +} } // namespace const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const { @@ -90,21 +156,28 @@ GfxRenderer::BitmapScratchLock::~BitmapScratchLock() { void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const { auto it = sdCardFonts_.find(fontId); if (it != sdCardFonts_.end()) { - int missed = it->second->buildAdvanceTable(utf8Text, styleMask); + std::string shaped; + appendShapedRtlTokens(utf8Text, shaped); + int missed = it->second->buildAdvanceTable(utf8Text, styleMask, shaped.empty() ? nullptr : shaped.c_str()); if (missed > 0) { LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed); } } } -void GfxRenderer::ensureSdCardFontReady(int fontId, const std::vector& words, bool includeHyphen, +void GfxRenderer::ensureSdCardFontReady(int fontId, const std::deque& words, bool includeHyphen, uint8_t styleMask) const { auto it = sdCardFonts_.find(fontId); if (it != sdCardFonts_.end()) { // Augment the persistent advance-only table for layout measurement. // The table survives across paragraphs/sections (capped per font), so // repeated indexing of the same SD font amortizes glyph-metric SD reads. - int missed = it->second->buildAdvanceTable(words, includeHyphen, styleMask); + std::string shaped; + for (const auto& w : words) { + appendShapedRtlTokens(w.c_str(), shaped); + } + int missed = + it->second->buildAdvanceTable(words, includeHyphen, styleMask, shaped.empty() ? nullptr : shaped.c_str()); if (missed > 0) { LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed); } @@ -123,6 +196,27 @@ void GfxRenderer::ensureSdCardFontReady(int fontId, const uint32_t* codepoints, } } +bool GfxRenderer::collectSdCardFontShapedRtlCodepoints(const char* utf8Text, uint32_t* codepoints, uint16_t& cpCount, + const uint16_t capacity, std::string& tokenScratch, + std::string& visualScratch) const { + if (!utf8Text || !codepoints || cpCount >= capacity) return cpCount >= capacity; + + bool truncated = false; + forEachShapedRtlToken(utf8Text, tokenScratch, visualScratch, [&](const char* shaped) { + const auto* p = reinterpret_cast(shaped); + uint32_t cp = 0; + while ((cp = utf8NextCodepoint(&p))) { + if (std::find(codepoints, codepoints + cpCount, cp) != codepoints + cpCount) continue; + if (cpCount >= capacity) { + truncated = true; + return; + } + codepoints[cpCount++] = cp; + } + }); + return truncated; +} + bool GfxRenderer::releaseSdCardFontForLowMemory(int fontId, const bool preserveAdvanceTable) const { auto it = sdCardFonts_.find(fontId); if (it == sdCardFonts_.end()) return false; @@ -189,6 +283,47 @@ bool GfxRenderer::ensureBitmapScratchBuffers(const size_t outputRowSize, const s return true; } +void GfxRenderer::releaseFrameBufferForBuild() { + // Lend the framebuffer's bytes IN PLACE: the allocation is never freed, so + // it cannot move and repeated loans cannot fragment the heap (the previous + // free+realloc model measurably decayed the max contiguous block over a + // session). The bytes are deposited in the build-scratch registry so + // memory-hungry build phases (e.g. InflateStream's tinfl state + window) + // can claim them instead of allocating. + uint32_t size = 0; + uint8_t* scratch = display.lendFrameBufferStorage(&size); + frameBuffer = nullptr; + if (scratch) { + buildscratch::lend(scratch, size); + } +} + +bool GfxRenderer::restoreFrameBufferAfterBuild() { + buildscratch::reclaim(); + display.returnFrameBufferStorage(); // cannot fail: the allocation was never freed + frameBuffer = display.getFrameBuffer(); + return frameBuffer != nullptr; +} + +GfxRenderer::FrameBufferLoan::FrameBufferLoan(GfxRenderer& renderer) : renderer_(renderer) { + // Nesting guard: if the framebuffer is already lent out (an outer loan), + // stay inert so this end() cannot return storage the outer loan still owns. + if (!renderer_.hasFrameBuffer()) return; + renderer_.releaseFrameBufferForBuild(); + active_ = true; +} + +void GfxRenderer::FrameBufferLoan::end() { + if (!active_) return; + active_ = false; + if (!renderer_.restoreFrameBufferAfterBuild()) { + // Only reachable if the framebuffer never existed, which begin() already + // asserts against; kept as a backstop since running blind helps nobody. + LOG_ERR("GFX", "Framebuffer restore failed - restarting"); + ESP.restart(); + } +} + bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); } void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { @@ -198,6 +333,36 @@ void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { } } +int GfxRenderer::resolveTextFontId(const int fontId, const char* text, const EpdFontFamily::Style style) const { + if (fallbackFontMap_.empty() || text == nullptr || *text == '\0') { + return fontId; + } + const auto fbIt = fallbackFontMap_.find(fontId); + if (fbIt == fallbackFontMap_.end()) { + return fontId; // no fallback registered for this font + } + const int fallbackFontId = fbIt->second; + const auto fontIt = fontMap.find(fontId); + const auto fallbackIt = fontMap.find(fallbackFontId); + if (fontIt == fontMap.end() || fallbackIt == fontMap.end()) { + return fontId; // unknown primary or fallback not loaded — let the caller handle it + } + const EpdFontFamily& primary = fontIt->second; + const EpdFontFamily& fallback = fallbackIt->second; + const char* cursor = text; + uint32_t cp; + while ((cp = utf8NextCodepoint(reinterpret_cast(&cursor)))) { + // Only redirect for CJK the primary font cannot draw but the fallback can. + // Latin/symbol strings the built-in UI fonts already cover are left + // untouched, and a partial-coverage fallback (e.g. kana-only) is not worth + // dragging the whole string into for glyphs it would also miss. + if (utf8IsCjkCodepoint(cp) && !primary.hasCodepoint(cp, style) && fallback.hasCodepoint(cp, style)) { + return fallbackFontId; + } + } + return fontId; +} + // Translate logical (x,y) coordinates to physical panel coordinates based on current orientation // This should always be inlined for better performance static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX, @@ -232,6 +397,48 @@ static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, } } +struct AlignedMemRect { + uint16_t x = 0; + uint16_t y = 0; + uint16_t w = 0; + uint16_t h = 0; + bool valid = false; +}; + +static AlignedMemRect screenRectToAlignedMemRect(const GfxRenderer::Orientation orientation, const int sx, const int sy, + const int sw, const int sh, const uint16_t panelWidth, + const uint16_t panelHeight) { + AlignedMemRect out; + if (sw <= 0 || sh <= 0) return out; + + int x0, y0, x1, y1; + rotateCoordinates(orientation, sx, sy, &x0, &y0, panelWidth, panelHeight); + rotateCoordinates(orientation, sx + sw - 1, sy + sh - 1, &x1, &y1, panelWidth, panelHeight); + + const int memXLo = std::min(x0, x1); + const int memYLo = std::min(y0, y1); + const int memXHi = std::max(x0, x1) + 1; + const int memYHi = std::max(y0, y1) + 1; + + int alignedXLo = memXLo & ~0x7; + int alignedXHi = (memXHi + 7) & ~0x7; + if (alignedXLo < 0) alignedXLo = 0; + if (alignedXHi > panelWidth) alignedXHi = panelWidth; + + int clampedYLo = memYLo; + int clampedYHi = memYHi; + if (clampedYLo < 0) clampedYLo = 0; + if (clampedYHi > panelHeight) clampedYHi = panelHeight; + + if (alignedXHi <= alignedXLo || clampedYHi <= clampedYLo) return out; + out.x = static_cast(alignedXLo); + out.y = static_cast(clampedYLo); + out.w = static_cast(alignedXHi - alignedXLo); + out.h = static_cast(clampedYHi - clampedYLo); + out.valid = true; + return out; +} + enum class TextRotation { None, Rotated90CW }; struct SyntheticSolidGlyphMetrics { @@ -455,7 +662,7 @@ static void drawSyntheticGreekGlyphRotated90CW(const GfxRenderer& renderer, cons // Shared glyph rendering logic for normal and rotated text. // Coordinate mapping and cursor advance direction are selected at compile time via the template parameter. -// Render a glyph at 50% scale. Used for SUP/SUB style bits. +// Render a glyph at 50% scale. Used for SUP/SUB. // // Each destination pixel represents a 2x2 source block. Drawing when that block // contains ink preserves thin strokes that nearest-neighbor sampling can skip. @@ -529,6 +736,69 @@ static void renderCharScaled(const GfxRenderer& renderer, GfxRenderer::RenderMod } } +static void renderCharSmallCaps(const GfxRenderer& renderer, GfxRenderer::RenderMode renderMode, + const EpdFontFamily& fontFamily, const uint32_t cp, int cursorX, int cursorY, + const bool pixelState, const EpdFontFamily::Style style) { + const EpdGlyph* glyph = fontFamily.getGlyph(cp, style); + if (!glyph) return; + + const EpdFontData* fontData = fontFamily.getData(style); + const uint8_t* bitmap = renderer.getGlyphBitmap(fontData, glyph); + if (!bitmap) return; + + const int srcW = glyph->width; + const int srcH = glyph->height; + const int dstW = smallCapsScaledExtent(srcW); + const int dstH = smallCapsScaledExtent(srcH); + const int baseX = cursorX + scaleSmallCapsMetric(glyph->left); + const int baseY = cursorY - scaleSmallCapsMetric(glyph->top); + + if (fontData->is2Bit) { + for (int dstY = 0; dstY < dstH; dstY++) { + const int srcY = dstY * 4 / 3; + const int srcYEnd = scaled75SourceEnd(dstY, srcH); + for (int dstX = 0; dstX < dstW; dstX++) { + const int srcX = dstX * 4 / 3; + const int srcXEnd = scaled75SourceEnd(dstX, srcW); + uint8_t maxRaw = 0; + for (int sampleY = srcY; sampleY < srcYEnd; sampleY++) { + for (int sampleX = srcX; sampleX < srcXEnd; sampleX++) { + const int pos = sampleY * srcW + sampleX; + const uint8_t byte = bitmap[pos >> 2]; + const uint8_t raw = (byte >> ((3 - (pos & 3)) * 2)) & 0x3; + if (raw > maxRaw) maxRaw = raw; + } + } + draw2BitFontPixel(renderer, renderMode, baseX + dstX, baseY + dstY, maxRaw, pixelState); + } + } + } else { + for (int dstY = 0; dstY < dstH; dstY++) { + const int srcY = dstY * 4 / 3; + const int srcYEnd = scaled75SourceEnd(dstY, srcH); + for (int dstX = 0; dstX < dstW; dstX++) { + const int srcX = dstX * 4 / 3; + const int srcXEnd = scaled75SourceEnd(dstX, srcW); + bool hasInk = false; + for (int sampleY = srcY; sampleY < srcYEnd && !hasInk; sampleY++) { + for (int sampleX = srcX; sampleX < srcXEnd; sampleX++) { + const int pos = sampleY * srcW + sampleX; + const uint8_t byte = bitmap[pos >> 3]; + const uint8_t bit = 7 - (pos & 7); + if ((byte >> bit) & 1) { + hasInk = true; + break; + } + } + } + if (hasInk) { + renderer.drawPixel(baseX + dstX, baseY + dstY, pixelState); + } + } + } + } +} + template static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode renderMode, const EpdFontFamily& fontFamily, const uint32_t cp, int cursorX, int cursorY, @@ -641,6 +911,29 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode // IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and // efficient as possible. +bool GfxRenderer::isPixelBlack(const int x, const int y) const { + int phyX = 0; + int phyY = 0; + rotateCoordinates(orientation, x, y, &phyX, &phyY, panelWidth, panelHeight); + if (phyX < 0 || phyX >= panelWidth || phyY < 0 || phyY >= panelHeight) { + return false; + } + + const uint8_t* target = frameBuffer; + uint32_t rowY = static_cast(phyY); + if (_stripActive) { + if (phyY < _stripY0 || phyY >= _stripY0 + _stripRows) { + return false; + } + target = _stripBuf; + rowY = static_cast(phyY - _stripY0); + } + + const uint32_t byteIndex = rowY * panelWidthBytes + (phyX / 8); + const uint8_t bitPosition = 7 - (phyX % 8); + return (target[byteIndex] & (1 << bitPosition)) == 0; +} + void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { int phyX = 0; int phyY = 0; @@ -648,9 +941,10 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { // Note: this call should be inlined for better performance rotateCoordinates(orientation, x, y, &phyX, &phyY, panelWidth, panelHeight); - // Bounds checking against runtime panel dimensions + // Text glyphs and shapes may cross a clipping edge. This is a per-pixel hot + // path, so logging here can turn one off-screen glyph into thousands of slow + // serial writes and starve input/power handling for minutes. if (phyX < 0 || phyX >= panelWidth || phyY < 0 || phyY >= panelHeight) { - LOG_ERR("GFX", "!! Outside range (%d, %d) -> (%d, %d)", x, y, phyX, phyY); return; } @@ -678,6 +972,44 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { } namespace { + +// Recognizes lowercase codepoints eligible for small-caps scaling: ASCII plus the +// accented Latin (Latin-1 Supplement, Latin Extended-A), Greek, and Cyrillic +// letters actually shipped in this repo's font builds (see EpdFont/scripts/ +// fontconvert.py and fontconvert_sdcard.py's latin-ext/greek/cyrillic intervals). +// Ranges without a clean, single-codepoint uppercase pair (e.g. ß, digraphs) are +// left unscaled rather than guessed at. +bool isSmallCapsLowercase(const EpdFontFamily::Style style, const uint32_t cp) { + if ((style & EpdFontFamily::SMALL_CAPS) == 0) return false; + if (cp >= 'a' && cp <= 'z') return true; + // Latin-1 Supplement: à-ö, ø-þ (÷ at 0x00F7 is not a letter), plus ÿ (paired below). + if ((cp >= 0x00E0 && cp <= 0x00F6) || (cp >= 0x00F8 && cp <= 0x00FE) || cp == 0x00FF) return true; + // Latin Extended-A: alternating lower/upper pairs, plus the ź/ż/ž tail whose + // parity flips after the standalone Ÿ at 0x0178. + if (cp >= 0x0101 && cp <= 0x0177 && (cp & 1) != 0) return true; + if (cp == 0x017A || cp == 0x017C || cp == 0x017E) return true; + // Greek: α-ω, including final sigma ς (mapped to Σ, not the unassigned 0x03A2). + if (cp >= 0x03B1 && cp <= 0x03C9) return true; + // Cyrillic: а-я, the ѐ-џ supplement row (Ukrainian є/і/ї among them), and ґ. + if ((cp >= 0x0430 && cp <= 0x044F) || (cp >= 0x0450 && cp <= 0x045F) || cp == 0x0491) return true; + return false; +} + +uint32_t smallCapsUppercaseCodepoint(const uint32_t cp) { + if (cp >= 'a' && cp <= 'z') return cp - ('a' - 'A'); + if (cp == 0x00FF) return 0x0178; // ÿ -> Ÿ (not contiguous with à-þ) + if (cp >= 0x00E0 && cp <= 0x00FE) return cp - 0x20; // à-þ + if (cp >= 0x0101 && cp <= 0x0177) return cp - 1; // Latin Extended-A pairs + if (cp == 0x017A) return 0x0179; // ź -> Ź + if (cp == 0x017C) return 0x017B; // ż -> Ż + if (cp == 0x017E) return 0x017D; // ž -> Ž + if (cp == 0x03C2) return 0x03A3; // ς -> Σ (final sigma) + if (cp >= 0x03B1 && cp <= 0x03C9) return cp - 0x20; // Greek base letters + if (cp == 0x0491) return 0x0490; // ґ -> Ґ + if (cp >= 0x0450 && cp <= 0x045F) return cp - 0x50; // Cyrillic supplement row + return cp - 0x20; // Cyrillic main block (а-я) +} + const char* resolveVisualText(const char* text, std::string& visualBuffer, const BidiUtils::BidiBaseDir baseDir) { if (!text || *text == '\0') return text; @@ -705,17 +1037,22 @@ int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontF return 0; } + const int resolvedFontId = resolveTextFontId(fontId, text, style); + std::string visualBuffer; const char* textCursor = resolveVisualText(text, visualBuffer, baseDir); + if ((style & EpdFontFamily::SMALL_CAPS) != 0) { + return getTextAdvanceX(resolvedFontId, textCursor, style); + } // SD-card fonts can measure from their persistent advance table during layout. - auto sdIt = sdCardFonts_.find(fontId); + auto sdIt = sdCardFonts_.find(resolvedFontId); if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) { int32_t widthFP = 0; const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style); - const auto fontIt = fontMap.find(fontId); + const auto fontIt = fontMap.find(resolvedFontId); if (fontIt == fontMap.end()) { - LOG_ERR("GFX", "Font %d not found", fontId); + LOG_ERR("GFX", "Font %d not found", resolvedFontId); return 0; } const auto& font = fontIt->second; @@ -725,9 +1062,9 @@ int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontF return fp4::toPixel(widthFP); } - const auto fontIt = fontMap.find(fontId); + const auto fontIt = fontMap.find(resolvedFontId); if (fontIt == fontMap.end()) { - LOG_ERR("GFX", "Font %d not found", fontId); + LOG_ERR("GFX", "Font %d not found", resolvedFontId); return 0; } @@ -744,47 +1081,61 @@ void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* te void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black, const EpdFontFamily::Style style, const BidiUtils::BidiBaseDir baseDir) const { - const int yPos = y + getFontAscenderSize(fontId); + // cannot draw a NULL / empty string + if (text == nullptr || *text == '\0') { + return; + } + + const int resolvedFontId = resolveTextFontId(fontId, text, style); + const int yPos = y + getFontAscenderSize(resolvedFontId); int lastBaseX = x; int lastBaseLeft = 0; int lastBaseWidth = 0; int lastBaseTop = 0; int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap - // cannot draw a NULL / empty string - if (text == nullptr || *text == '\0') { - return; - } - if (fontCacheManager_ && fontCacheManager_->isScanning()) { - fontCacheManager_->recordText(text, fontId, style); + // Prewarm the exact codepoints the real pass will draw. Arabic shaping can + // replace logical characters with presentation forms; recording the input + // text here left those visual glyphs cold and produced replacement diamonds + // when the SD font's on-demand path was under memory pressure. + std::string visualBuffer; + fontCacheManager_->recordText(resolveVisualText(text, visualBuffer, baseDir), resolvedFontId, style); return; } std::string visualBuffer; const char* textCursor = resolveVisualText(text, visualBuffer, baseDir); - const auto fontIt = fontMap.find(fontId); + const auto fontIt = fontMap.find(resolvedFontId); if (fontIt == fontMap.end()) { - LOG_ERR("GFX", "Font %d not found", fontId); + LOG_ERR("GFX", "Font %d not found", resolvedFontId); return; } const auto& font = fontIt->second; uint32_t cp; uint32_t prevCp = 0; + bool prevScaledSmallCap = false; while ((cp = utf8NextCodepoint(reinterpret_cast(&textCursor)))) { - if (utf8IsCombiningMark(cp)) { + if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) { const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); if (!combiningGlyph) continue; - const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop); - const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left, - combiningGlyph->width); + const auto anchor = combiningMark::anchorFor(cp); + const int raiseBy = + combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop); + const int combiningX = combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth, + combiningGlyph->left, combiningGlyph->width); renderCharImpl(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style); continue; } - cp = font.applyLigatures(cp, textCursor, style); + const bool scaledSmallCap = isSmallCapsLowercase(style, cp); + if (scaledSmallCap) { + cp = smallCapsUppercaseCodepoint(cp); + } else { + cp = font.applyLigatures(cp, textCursor, style); + } cp = font.getFallbackCodepoint(cp, style); const bool hasRealGlyph = font.findGlyphData(cp, style).glyph != nullptr; @@ -792,8 +1143,11 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha // identical character pairs always produce the same pixel step regardless of // where they fall on the line. if (prevCp != 0) { - const auto kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern - lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel + int32_t kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern + if (prevScaledSmallCap || scaledSmallCap) { + kernFP = smallCapsAdvanceFP(kernFP); + } + lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel } if (!hasRealGlyph && syntheticGlyph::isSpaceFallback(cp)) { @@ -802,6 +1156,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha lastBaseWidth = 0; lastBaseTop = 0; prevCp = 0; + prevScaledSmallCap = false; continue; } @@ -813,6 +1168,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha lastBaseTop = metrics.top; prevAdvanceFP = metrics.advanceX; prevCp = cp; + prevScaledSmallCap = false; continue; } @@ -824,6 +1180,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha lastBaseTop = metrics.top; prevAdvanceFP = metrics.advanceX; prevCp = cp; + prevScaledSmallCap = false; continue; } @@ -835,6 +1192,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha lastBaseTop = metrics.top; prevAdvanceFP = metrics.advanceX; prevCp = cp; + prevScaledSmallCap = false; continue; } @@ -848,6 +1206,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha lastBaseWidth = 0; lastBaseTop = 0; prevCp = 0; + prevScaledSmallCap = false; continue; } @@ -858,18 +1217,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha const bool isSupSub = (style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0; if (isSupSub) { - // Halve the advance so the cursor advances by the same amount the scaled glyph - // actually occupies, keeping spacing correct without needing a separate smaller font. - prevAdvanceFP = (prevAdvanceFP + 1) / 2; + prevAdvanceFP = halfAdvanceFP(prevAdvanceFP); + } else if (scaledSmallCap) { + prevAdvanceFP = smallCapsAdvanceFP(prevAdvanceFP); } if (isSupSub) { // yPos already carries the vertical offset applied by TextBlock::render(). renderCharScaled(*this, renderMode, font, cp, lastBaseX, yPos, black, style); + } else if (scaledSmallCap) { + renderCharSmallCaps(*this, renderMode, font, cp, lastBaseX, yPos, black, style); } else { renderCharImpl(*this, renderMode, font, cp, lastBaseX, yPos, black, style); } prevCp = cp; + prevScaledSmallCap = scaledSmallCap; } } @@ -935,11 +1297,14 @@ void GfxRenderer::drawRect(const int x, const int y, const int width, const int // Border is inside the rectangle void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const int lineWidth, const bool state) const { + // Keep the border inside [x, x+width) like the thin overload: the previous + // right/bottom edges at x+width / y+height sat one pixel outside the rect, + // so stroked boxes looked shifted against fills computed from the rect. for (int i = 0; i < lineWidth; i++) { - drawLine(x + i, y + i, x + width - i, y + i, state); - drawLine(x + width - i, y + i, x + width - i, y + height - i, state); - drawLine(x + width - i, y + height - i, x + i, y + height - i, state); - drawLine(x + i, y + height - i, x + i, y + i, state); + drawLine(x + i, y + i, x + width - 1 - i, y + i, state); + drawLine(x + width - 1 - i, y + i, x + width - 1 - i, y + height - 1 - i, state); + drawLine(x + width - 1 - i, y + height - 1 - i, x + i, y + height - 1 - i, state); + drawLine(x + i, y + height - 1 - i, x + i, y + i, state); } } @@ -1606,8 +1971,6 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con bool isScaled = false; int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f); int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f); - LOG_DBG("GFX", "Cropping %dx%d by %dx%d pix, is %s", bitmap.getWidth(), bitmap.getHeight(), cropPixX, cropPixY, - bitmap.isTopDown() ? "top-down" : "bottom-up"); const float croppedWidth = (1.0f - cropX) * static_cast(bitmap.getWidth()); const float croppedHeight = (1.0f - cropY) * static_cast(bitmap.getHeight()); @@ -1629,7 +1992,6 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con scale = fitScale; isScaled = true; } - LOG_DBG("GFX", "Scaling by %f - %s", scale, isScaled ? "scaled" : "not scaled"); BitmapScratchLock scratchLock(*this); if (!scratchLock.isLocked()) return; @@ -1877,11 +2239,7 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi free(nodeX); } -// For performance measurement (using static to allow "const" methods) -static unsigned long start_ms = 0; - void GfxRenderer::clearScreen(const uint8_t color) const { - start_ms = millis(); if (_stripActive) { // Clear only the active band's scratch, not the shared framebuffer. memset(_stripBuf, color, static_cast(panelWidthBytes) * _stripRows); @@ -1929,12 +2287,103 @@ void GfxRenderer::invertScreen() const { } } +void GfxRenderer::invertRect(const int x, const int y, const int width, const int height) const { + if (frameBuffer == nullptr || width <= 0 || height <= 0) return; + + const int lx0 = std::max(0, x); + const int ly0 = std::max(0, y); + const int lx1 = std::min(getScreenWidth(), x + width); + const int ly1 = std::min(getScreenHeight(), y + height); + if (lx0 >= lx1 || ly0 >= ly1) return; + + int paX, paY, pbX, pbY; + rotateCoordinates(orientation, lx0, ly0, &paX, &paY, panelWidth, panelHeight); + rotateCoordinates(orientation, lx1 - 1, ly1 - 1, &pbX, &pbY, panelWidth, panelHeight); + + const int phyX0 = std::min(paX, pbX); + const int phyX1 = std::max(paX, pbX); + int phyY0 = std::min(paY, pbY); + int phyY1 = std::max(paY, pbY); + + uint8_t* target = getWriteTarget(); + const int originY = getWriteOriginY(); + phyY0 = std::max(phyY0, originY); + phyY1 = std::min(phyY1, originY + getWriteRows() - 1); + if (phyY0 > phyY1) return; + + const int byteStart = phyX0 >> 3; + const int byteEnd = phyX1 >> 3; + const uint8_t headMask = static_cast(0xFFu >> (phyX0 & 7)); + const uint8_t tailMask = static_cast(0xFFu << (7 - (phyX1 & 7))); + + for (int py = phyY0; py <= phyY1; ++py) { + uint8_t* row = target + static_cast(py - originY) * panelWidthBytes; + if (byteStart == byteEnd) { + row[byteStart] ^= headMask & tailMask; + continue; + } + + row[byteStart] ^= headMask; + for (int byte = byteStart + 1; byte < byteEnd; ++byte) { + row[byte] ^= 0xFFu; + } + row[byteEnd] ^= tailMask; + } +} + void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode, const bool turnOffScreen) const { - auto elapsed = millis() - start_ms; - LOG_DBG("GFX", "Time = %lu ms from clearScreen to displayBuffer", elapsed); display.displayBuffer(refreshMode, fadingFix || turnOffScreen); } +size_t GfxRenderer::readFramebufferRegion(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t* dst, + size_t dstCapacity) const { + if (frameBuffer == nullptr || dst == nullptr || w == 0 || h == 0) return 0; + + const AlignedMemRect mem = screenRectToAlignedMemRect(orientation, x, y, w, h, panelWidth, panelHeight); + if (!mem.valid) return 0; + + const size_t rowBytes = mem.w / 8; + const size_t needed = rowBytes * mem.h; + if (needed > dstCapacity) return 0; + + for (uint16_t row = 0; row < mem.h; row++) { + const uint8_t* srcRow = frameBuffer + (static_cast(mem.y + row) * panelWidthBytes) + (mem.x / 8); + uint8_t* dstRow = dst + (static_cast(row) * rowBytes); + memcpy(dstRow, srcRow, rowBytes); + } + return needed; +} + +void GfxRenderer::writeFramebufferRegion(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t* src) { + if (frameBuffer == nullptr || src == nullptr || w == 0 || h == 0) return; + + const AlignedMemRect mem = screenRectToAlignedMemRect(orientation, x, y, w, h, panelWidth, panelHeight); + if (!mem.valid) return; + + const size_t rowBytes = mem.w / 8; + for (uint16_t row = 0; row < mem.h; row++) { + const uint8_t* srcRow = src + (static_cast(row) * rowBytes); + uint8_t* dstRow = frameBuffer + (static_cast(mem.y + row) * panelWidthBytes) + (mem.x / 8); + memcpy(dstRow, srcRow, rowBytes); + } +} + +void GfxRenderer::displayBufferAsync(const HalDisplay::RefreshMode refreshMode) const { + // The async path has no turn-off-screen hook, which the sunlight fading fix + // relies on; keep those users on the blocking path. + if (fadingFix) { + display.displayBuffer(refreshMode, fadingFix); + return; + } + display.displayBufferAsync(refreshMode); +} + +void GfxRenderer::waitRefreshComplete() const { display.waitRefreshComplete(); } + +bool GfxRenderer::supportsAsyncRefresh() const { return !fadingFix && display.supportsAsyncRefresh(); } + +bool GfxRenderer::supportsAsyncGrayscaleBase() const { return !fadingFix && display.supportsAsyncGrayscaleBase(); } + std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, const EpdFontFamily::Style style) const { if (!text || maxWidth <= 0) return ""; @@ -2048,6 +2497,35 @@ int GfxRenderer::getScreenHeight() const { return panelWidth; } +void GfxRenderer::tapToLogical(float nx, float ny, int& outX, int& outY) const { + int phyX = static_cast(nx * panelWidth); + int phyY = static_cast(ny * panelHeight); + if (phyX < 0) phyX = 0; + if (phyX > panelWidth - 1) phyX = panelWidth - 1; + if (phyY < 0) phyY = 0; + if (phyY > panelHeight - 1) phyY = panelHeight - 1; + + switch (orientation) { + case Portrait: + outX = panelHeight - 1 - phyY; + outY = phyX; + break; + case PortraitInverted: + outX = phyY; + outY = panelWidth - 1 - phyX; + break; + case LandscapeClockwise: + outX = panelWidth - 1 - phyX; + outY = panelHeight - 1 - phyY; + break; + case LandscapeCounterClockwise: + default: + outX = phyX; + outY = phyY; + break; + } +} + // Translate a logical rect through rotateCoordinates and take the bounding // box of its four corners on the physical panel. Output coords are inclusive // and clamped. Returns false if the rect ends up fully off-panel. @@ -2188,31 +2666,72 @@ int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint3 return fp4::toPixel(kernFP); // snap 4.4 fixed-point to nearest pixel } -int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const { +int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, const EpdFontFamily::Style style, + const uint32_t followingCp) const { + // Match the font drawText would use for CJK-bearing strings (see resolveTextFontId). + const int resolvedFontId = resolveTextFontId(fontId, text, style); + // Measure the exact codepoint stream drawText renders: bidi-reordered and + // Arabic-shaped (contextual presentation forms, Lam-Alef collapse). + // Measuring the raw logical text counts the Alef a ligature absorbs and + // uses base-letter advances instead of presentation-form advances, so RTL + // lines come out wider than they draw — uneven word gaps and a ragged + // right margin. + std::string visual; + text = resolveVisualText(text, visual, BidiUtils::BidiBaseDir::AUTO); + // Advance table fast-path for SD card fonts during layout. // No kerning/ligature lookup — consistent with previous metadataOnly behavior // where kern/lig data was not loaded. - auto sdIt = sdCardFonts_.find(fontId); + auto sdIt = sdCardFonts_.find(resolvedFontId); if (sdIt != sdCardFonts_.end() && sdIt->second->hasAdvanceTable()) { int32_t widthFP = 0; const bool isSupSub = (style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0; const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style); - const auto fontIt = fontMap.find(fontId); + const auto fontIt = fontMap.find(resolvedFontId); if (fontIt == fontMap.end()) { - LOG_ERR("GFX", "Font %d not found", fontId); + LOG_ERR("GFX", "Font %d not found", resolvedFontId); return 0; } const auto& font = fontIt->second; + uint32_t lastCp = 0; + bool lastScaledSmallCap = false; while (uint32_t cp = utf8NextCodepoint(reinterpret_cast(&text))) { + if (BidiUtils::isTransparentMark(cp)) { + continue; + } + const bool scaledSmallCap = isSmallCapsLowercase(style, cp); + if (scaledSmallCap) { + cp = smallCapsUppercaseCodepoint(cp); + } const int32_t advFP = resolveSdCardAdvanceFP(*sdIt->second, font, cp, style, styleIdx); - widthFP += isSupSub ? (advFP + 1) / 2 : advFP; + if (isSupSub) { + widthFP += halfAdvanceFP(advFP); + } else if (scaledSmallCap) { + widthFP += smallCapsAdvanceFP(advFP); + } else { + widthFP += advFP; + } + lastCp = cp; + lastScaledSmallCap = scaledSmallCap; + } + if (followingCp != 0 && lastCp != 0) { + uint32_t adjustedFollowingCp = followingCp; + const bool followingScaledSmallCap = isSmallCapsLowercase(style, adjustedFollowingCp); + if (followingScaledSmallCap) { + adjustedFollowingCp = smallCapsUppercaseCodepoint(adjustedFollowingCp); + } + int32_t kernFP = font.getKerning(lastCp, adjustedFollowingCp, style); + if (lastScaledSmallCap || followingScaledSmallCap) { + kernFP = smallCapsAdvanceFP(kernFP); + } + widthFP += kernFP; } return fp4::toPixel(widthFP); } - const auto fontIt = fontMap.find(fontId); + const auto fontIt = fontMap.find(resolvedFontId); if (fontIt == fontMap.end()) { - LOG_ERR("GFX", "Font %d not found", fontId); + LOG_ERR("GFX", "Font %d not found", resolvedFontId); return 0; } @@ -2220,54 +2739,88 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami uint32_t prevCp = 0; int widthPx = 0; int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap + bool prevScaledSmallCap = false; const auto& font = fontIt->second; while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { + // RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width. + if (BidiUtils::isTransparentMark(cp)) { + continue; + } if (utf8IsCombiningMark(cp)) { continue; } - cp = font.applyLigatures(cp, text, style); + const bool scaledSmallCap = isSmallCapsLowercase(style, cp); + if (scaledSmallCap) { + cp = smallCapsUppercaseCodepoint(cp); + } else { + cp = font.applyLigatures(cp, text, style); + } cp = font.getFallbackCodepoint(cp, style); const bool hasRealGlyph = font.findGlyphData(cp, style).glyph != nullptr; // Differential rounding: snap (previous advance + current kern) together, // matching drawText so measurement and rendering agree exactly. if (prevCp != 0) { - const auto kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern - widthPx += fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel + int32_t kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern + if (prevScaledSmallCap || scaledSmallCap) { + kernFP = smallCapsAdvanceFP(kernFP); + } + widthPx += fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel } if (!hasRealGlyph && syntheticGlyph::isSpaceFallback(cp)) { prevAdvanceFP = 0; prevCp = 0; + prevScaledSmallCap = false; continue; } if (!hasRealGlyph && syntheticGlyph::isSolid(cp)) { prevAdvanceFP = getSyntheticSolidGlyphMetrics(font, style, cp).advanceX; prevCp = cp; + prevScaledSmallCap = false; continue; } if (!hasRealGlyph && syntheticGlyph::isGreekFallback(cp)) { prevAdvanceFP = getSyntheticGreekGlyphMetrics(font, style, cp).advanceX; prevCp = cp; + prevScaledSmallCap = false; continue; } if (!hasRealGlyph && syntheticGlyph::isReplacementFallback(cp)) { prevAdvanceFP = getSyntheticReplacementGlyphMetrics(font, style).advanceX; prevCp = cp; + prevScaledSmallCap = false; continue; } const EpdGlyph* glyph = font.getGlyph(cp, style); prevAdvanceFP = glyph ? glyph->advanceX : 0; if ((style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) { - prevAdvanceFP = (prevAdvanceFP + 1) / 2; + prevAdvanceFP = halfAdvanceFP(prevAdvanceFP); + } else if (scaledSmallCap) { + prevAdvanceFP = smallCapsAdvanceFP(prevAdvanceFP); } prevCp = cp; + prevScaledSmallCap = scaledSmallCap; + } + if (followingCp != 0 && prevCp != 0) { + uint32_t adjustedFollowingCp = followingCp; + const bool followingScaledSmallCap = isSmallCapsLowercase(style, adjustedFollowingCp); + if (followingScaledSmallCap) { + adjustedFollowingCp = smallCapsUppercaseCodepoint(adjustedFollowingCp); + } + adjustedFollowingCp = font.getFallbackCodepoint(adjustedFollowingCp, style); + int32_t kernFP = font.getKerning(prevCp, adjustedFollowingCp, style); // 4.4 fixed-point kern + if (prevScaledSmallCap || followingScaledSmallCap) { + kernFP = smallCapsAdvanceFP(kernFP); + } + widthPx += fp4::toPixel(prevAdvanceFP + kernFP); + } else { + widthPx += fp4::toPixel(prevAdvanceFP); // final glyph's advance } - widthPx += fp4::toPixel(prevAdvanceFP); // final glyph's advance return widthPx; } @@ -2307,9 +2860,11 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y return; } - const auto fontIt = fontMap.find(fontId); + // Route CJK-bearing strings to the fallback font (see resolveTextFontId). + const int resolvedFontId = resolveTextFontId(fontId, text, style); + const auto fontIt = fontMap.find(resolvedFontId); if (fontIt == fontMap.end()) { - LOG_ERR("GFX", "Font %d not found", fontId); + LOG_ERR("GFX", "Font %d not found", resolvedFontId); return; } @@ -2324,12 +2879,21 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y uint32_t cp; uint32_t prevCp = 0; while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { - if (utf8IsCombiningMark(cp)) { + // RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark + // path: zero-advance overlays on the preceding base glyph (applyBidiVisual + // emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive + // niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other + // marks stay centered, raised above the base or (kasra) at their + // font-native position. Fonts without their glyphs — the built-ins — miss + // the getGlyph lookup and skip them, as before. + if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) { const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); if (!combiningGlyph) continue; - const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop); + const auto anchor = combiningMark::anchorFor(cp); + const int raiseBy = + combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop); const int combiningX = x - raiseBy; - const int combiningY = combiningMark::centerOverRotated90CW(lastBaseY, lastBaseLeft, lastBaseWidth, + const int combiningY = combiningMark::anchorOverRotated90CW(anchor, lastBaseY, lastBaseLeft, lastBaseWidth, combiningGlyph->left, combiningGlyph->width); renderCharImpl(*this, renderMode, font, cp, combiningX, combiningY, black, style); continue; diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 45b9e4104d..d469370f0f 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -18,6 +18,7 @@ class SdCardFont; #include #include +#include #include #include #include @@ -94,6 +95,19 @@ class GfxRenderer { // as before, concentrated in a single pointer instead of four fields. mutable FontCacheManager* fontCacheManager_ = nullptr; + // CJK UI font fallback map: primary (built-in, Latin-only) UI font id -> a + // size-matched SD-card font id that carries CJK glyphs. When a string drawn + // or measured with a mapped primary font contains a CJK codepoint the primary + // cannot render, the whole string is routed to the mapped fallback so it + // appears at the same point size as the surrounding UI text. Populated by the + // app-level SD font setup when an SD family is loaded. See resolveTextFontId(). + std::map fallbackFontMap_; + + // If `text` contains a CJK codepoint that `fontId` cannot render and `fontId` + // has a registered fallback, returns the fallback id; otherwise returns + // fontId unchanged. The whole string is routed as a unit so each draw/measure + // call stays single-font (consistent bit depth, metrics, wrapping). + int resolveTextFontId(int fontId, const char* text, EpdFontFamily::Style style) const; void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState, EpdFontFamily::Style style) const; void freeBwBufferChunks(); @@ -153,14 +167,24 @@ class GfxRenderer { void clearSdCardFonts() { sdCardFonts_.clear(); } const std::map& getSdCardFonts() const { return sdCardFonts_; } bool isSdCardFont(int fontId) const { return sdCardFonts_.count(fontId) > 0; } + // Register/clear size-matched CJK UI fallbacks (see fallbackFontMap_). + // setFallbackFont maps a primary UI font id to an SD font id of the same size. + void setFallbackFont(int primaryFontId, int fallbackFontId) { fallbackFontMap_[primaryFontId] = fallbackFontId; } + void clearFallbackFonts() { fallbackFontMap_.clear(); } // Ensure SD card font glyph data is loaded for the given text. Called from layout code // (which holds a const GfxRenderer&) before measuring word widths. Safe to call on non-SD fonts (no-op). // styleMask: bitmask of styles to prepare (bit 0=regular, 1=bold, 2=italic, 3=bold-italic). void ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F) const; - void ensureSdCardFontReady(int fontId, const std::vector& words, bool includeHyphen, + void ensureSdCardFontReady(int fontId, const std::deque& words, bool includeHyphen, uint8_t styleMask = 0x0F) const; void ensureSdCardFontReady(int fontId, const uint32_t* codepoints, uint32_t cpCount, bool includeSpace, bool includeHyphen, uint8_t styleMask = 0x0F) const; + // Adds shaped RTL presentation forms to one bounded style bucket. The caller + // owns the reusable scratch strings so clipping can batch pages without a + // temporary heap allocation for every word. + bool collectSdCardFontShapedRtlCodepoints(const char* utf8Text, uint32_t* codepoints, uint16_t& cpCount, + uint16_t capacity, std::string& tokenScratch, + std::string& visualScratch) const; bool releaseSdCardFontForLowMemory(int fontId, bool preserveAdvanceTable = false) const; // Orientation control (affects logical width/height and coordinate transforms) @@ -173,10 +197,24 @@ class GfxRenderer { // Screen ops int getScreenWidth() const; int getScreenHeight() const; + void tapToLogical(float nx, float ny, int& outX, int& outY) const; void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH, bool turnOffScreen = false) const; + // Non-blocking refresh: starts the waveform and returns so CPU work (e.g. + // grayscale strip rendering) can overlap the panel's refresh time. The + // framebuffer must stay untouched until waitRefreshComplete(). Falls back to + // a blocking refresh when fadingFix is enabled or the panel lacks deferral + // support. See HalDisplay::displayBufferAsync for the baseline contract. + void displayBufferAsync(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const; + void waitRefreshComplete() const; + // True when displayBufferAsync() genuinely overlaps: panel defers and + // fadingFix isn't forcing the blocking path. Callers can skip overlap + // scaffolding (e.g. whole-plane grayscale buffers) when false. + bool supportsAsyncRefresh() const; + bool supportsAsyncGrayscaleBase() const; // EXPERIMENTAL: Windowed update - display only a rectangular region // void displayWindow(int x, int y, int width, int height) const; void invertScreen() const; + void invertRect(int x, int y, int width, int height) const; void clearScreen(uint8_t color = 0xFF) const; void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const; @@ -189,6 +227,7 @@ class GfxRenderer { int getWriteRows() const { return _stripActive ? _stripRows : panelHeight; } // Drawing + bool isPixelBlack(int x, int y) const; void drawPixel(int x, int y, bool state = true) const; void drawLine(int x1, int y1, int x2, int y2, bool state = true) const; void drawLine(int x1, int y1, int x2, int y2, int lineWidth, bool state) const; @@ -217,6 +256,11 @@ class GfxRenderer { // bounding box of width `w` and height `max(hL, hR)` whose top-left is (x, y). void drawPerspectiveBitmap(const Bitmap& bitmap, int x, int y, int w, int hL, int hR) const; void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const; + // Snapshot / restore a screen-coordinate framebuffer region. The renderer + // byte-aligns the panel-memory rectangle internally, so callers must pass the + // same rectangle to restore the saved pixels. + size_t readFramebufferRegion(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t* dst, size_t dstCapacity) const; + void writeFramebufferRegion(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t* src); // Text int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR, @@ -234,7 +278,10 @@ class GfxRenderer { int getSpaceAdvance(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const; /// Returns the kerning adjustment between two adjacent codepoints. int getKerning(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const; - int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const; + /// Returns the rendered advance of \p text. When \p followingCp is supplied, + /// includes its kerning with the final glyph in the same fixed-point rounding + /// step that drawText() uses, without drawing or consuming that codepoint. + int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style, uint32_t followingCp = 0) const; int getFontAscenderSize(int fontId) const; int getLineHeight(int fontId) const; std::string truncatedText(int fontId, const char* text, int maxWidth, @@ -276,6 +323,34 @@ class GfxRenderer { // Font helpers const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const; + // Lend the 48 KB framebuffer's bytes to a memory-hungry phase (chapter + // builds) WITHOUT freeing the allocation, so it never moves and repeated + // loans cannot fragment the heap. Between release and restore NOTHING may + // draw or display — the panel keeps showing its last refreshed image. The + // lent bytes are published via buildscratch::claim() for consumers like + // InflateStream. restore returns the buffer white, so the caller must + // redraw the full screen; it cannot fail (no allocation involved). + void releaseFrameBufferForBuild(); + bool restoreFrameBufferAfterBuild(); + bool hasFrameBuffer() const { return frameBuffer != nullptr; } + + // RAII form of the loan above, for blocking build regions with early-return + // error paths: restores on scope exit (or explicitly via end()). Display the + // popup/screen the panel should hold BEFORE constructing one. Constructing + // while the framebuffer is already lent yields an inert loan (nesting-safe). + class FrameBufferLoan { + public: + explicit FrameBufferLoan(GfxRenderer& renderer); + ~FrameBufferLoan() { end(); } + void end(); + FrameBufferLoan(const FrameBufferLoan&) = delete; + FrameBufferLoan& operator=(const FrameBufferLoan&) = delete; + + private: + GfxRenderer& renderer_; + bool active_ = false; + }; + // Low level functions uint8_t* getFrameBuffer() const; size_t getBufferSize() const; diff --git a/lib/HalClockSim/include/HalClock.h b/lib/HalClockSim/include/HalClock.h index 5f4cad1a7f..eeec14d512 100644 --- a/lib/HalClockSim/include/HalClock.h +++ b/lib/HalClockSim/include/HalClock.h @@ -3,16 +3,32 @@ #include #include +#define CROSSPOINT_SIMULATOR_HAS_DATE_SEPARATOR 1 + class HalClock; extern HalClock halClock; class HalClock { public: + enum DateFormat : uint8_t { + MONTH_DAY_YEAR_LONG = 0, + DAY_MONTH_YEAR_LONG = 1, + MONTH_DAY_YEAR_NUMERIC = 2, + DAY_MONTH_YEAR_NUMERIC = 3, + YEAR_MONTH_DAY_NUMERIC = 4, + MONTH_DAY_NUMERIC = 5, + DAY_MONTH_NUMERIC = 6, + MONTH_DAY_LONG = 7, + DAY_MONTH_LONG = 8, + DATE_FORMAT_COUNT + }; + void begin() {} bool isAvailable() const { return false; } bool getTime(uint8_t& hour, uint8_t& minute) const; bool getDateTime(uint16_t& year, uint8_t& month, uint8_t& day, uint8_t& hour, uint8_t& minute) const; bool formatTime(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased = 48, bool use12Hour = false) const; - bool formatDate(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased = 48) const; + bool formatDate(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased = 48, + DateFormat dateFormat = MONTH_DAY_YEAR_LONG, char numericSeparator = '/') const; bool syncFromNTP() { return false; } }; diff --git a/lib/HalClockSim/library.json b/lib/HalClockSim/library.json index 513d913db2..01087c6fd5 100644 --- a/lib/HalClockSim/library.json +++ b/lib/HalClockSim/library.json @@ -1,7 +1,7 @@ { "name": "HalClockSim", "version": "1.0.0", - "description": "Native simulator stub for the X3 RTC clock HAL", + "description": "Native simulator stub for the RTC clock HAL", "frameworks": "*", "platforms": "native", "build": { diff --git a/lib/HalClockSim/src/HalClock.cpp b/lib/HalClockSim/src/HalClock.cpp index 58860914d2..4b1c6a698a 100644 --- a/lib/HalClockSim/src/HalClock.cpp +++ b/lib/HalClockSim/src/HalClock.cpp @@ -25,9 +25,12 @@ bool HalClock::formatTime(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHou return false; } -bool HalClock::formatDate(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased) const { +bool HalClock::formatDate(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased, const DateFormat dateFormat, + const char numericSeparator) const { (void)buf; (void)bufSize; (void)utcOffsetQuarterHoursBiased; + (void)dateFormat; + (void)numericSeparator; return false; } diff --git a/lib/I18n/translations/arabic.yaml b/lib/I18n/translations/arabic.yaml new file mode 100644 index 0000000000..b262b5a810 --- /dev/null +++ b/lib/I18n/translations/arabic.yaml @@ -0,0 +1,669 @@ +_language_name: "العربية" +_language_code: "AR" +_order: "28" + +STR_CROSSINK: "CrossInk" +STR_BOOTING: "جاري بدء التشغيل" +STR_SLEEPING: "وضع السكون" +STR_ENTERING_SLEEP: "جاري الدخول في وضع السكون" +STR_BROWSE_FILES: "تصفح الملفات" +STR_BROWSE: "تصفح" +STR_FILE_TRANSFER: "نقل الملفات" +STR_SETTINGS_TITLE: "الإعدادات" +STR_SETTINGS_SHORT: "إعدادات" +STR_READ: "قراءة" +STR_MENU: "القائمة" +STR_CONTINUE_READING: "متابعة القراءة" +STR_NO_OPEN_BOOK: "لا يوجد كتاب مفتوح" +STR_START_READING: "ابدأ القراءة أدناه" +STR_NO_FILES_FOUND: "لم يتم العثور على ملفات" +STR_SELECT_CHAPTER: "اختر الفصل" +STR_NO_CHAPTERS: "لا توجد فصول" +STR_END_OF_BOOK: "نهاية الكتاب" +STR_EMPTY_CHAPTER: "فصل فارغ" +STR_INDEXING: "جاري الفهرسة" +STR_INDEX_FAILED: "فشلت الفهرسة - كتاب غير صالح" +STR_MEMORY_ERROR: "خطأ في الذاكرة" +STR_PAGE_LOAD_ERROR: "خطأ في تحميل الصفحة" +STR_EMPTY_FILE: "ملف فارغ" +STR_OUT_OF_BOUNDS: "خارج النطاق" +STR_LOADING: "جاري التحميل..." +STR_LOADING_POPUP: "جاري التحميل" +STR_WIFI_NETWORKS: "شبكات Wi-Fi" +STR_NO_NETWORKS: "لم يتم العثور على شبكات" +STR_NETWORKS_FOUND: "تم العثور على %zu شبكة" +STR_SCANNING: "جاري البحث..." +STR_FINDING_SAVED_WIFI: "جاري البحث عن شبكة Wi-Fi محفوظة..." +STR_CONNECTING: "جاري الاتصال..." +STR_CONNECTING_SAVED_WIFI: "جاري الاتصال بشبكة Wi-Fi محفوظة..." +STR_SHOW_NETWORKS: "عرض" +STR_CONNECTED: "تم الاتصال!" +STR_CONNECTION_FAILED: "فشل الاتصال" +STR_FORGET_NETWORK: "نسيان هذه الشبكة؟" +STR_SAVE_PASSWORD: "حفظ كلمة المرور للمرة القادمة؟" +STR_PRESS_OK_SCAN: "اضغط موافق لإعادة البحث" +STR_JOIN_NETWORK: "الانضمام إلى شبكة" +STR_CREATE_HOTSPOT: "إنشاء نقطة اتصال" +STR_NEARBY_DEVICE: "جهاز قريب" +STR_NEARBY_STATS_SYNC: "مزامنة الإحصائيات" +STR_RECEIVE_NEARBY_BOOK: "استلام ملف" +STR_RECEIVE_NEARBY_BOOK_DESC: "استلام ملف من جهاز قراءة آخر" +STR_NEARBY_POSITION_SYNC: "مزامنة الموقع القريب" +STR_JOIN_DESC: "الاتصال بشبكة Wi-Fi موجودة" +STR_HOTSPOT_DESC: "إنشاء شبكة Wi-Fi يمكن للآخرين الانضمام إليها" +STR_NEARBY_STATS_SYNC_DESC: "مزامنة إحصائيات القراءة بين أجهزة القراءة" +STR_NEARBY_STATS_VERSION_MISMATCH: "عدم تطابق الإصدار. لم تتم مزامنة الإحصائيات" +STR_NEARBY_POSITION_VERSION_MISMATCH: "عدم تطابق الإصدار. لم تتم مزامنة الموضع" +STR_NEARBY_POSITION_BOOK_MISMATCH: "كتاب مختلف. لم تتم مزامنة الموضع" +STR_STARTING_HOTSPOT: "جاري تشغيل نقطة الاتصال..." +STR_HOTSPOT_MODE: "وضع نقطة الاتصال" +STR_CONNECT_WIFI_HINT: "قم بتوصيل جهازك بشبكة Wi-Fi هذه" +STR_OPEN_URL_HINT: "افتح هذا العنوان في المتصفح" +STR_OR_HTTP_PREFIX: "أو http://" +STR_SCAN_QR_HINT: "أو امسح رمز QR بهاتفك:" +STR_CALIBRE_WIRELESS: "اتصال Calibre اللاسلكي" +STR_CALIBRE_WEB_URL: "عنوان URL لـ OPDS" +STR_NETWORK_LEGEND: "* = مشفرة | + = محفوظة" +STR_MAC_ADDRESS: "عنوان MAC:" +STR_CHECKING_WIFI: "جاري فحص Wi-Fi..." +STR_ENTER_WIFI_PASSWORD: "أدخل كلمة مرور الشبكة" +STR_ADD_HIDDEN_NETWORK: "إضافة شبكة مخفية..." +STR_ENTER_WIFI_SSID: "أدخل اسم الشبكة (SSID)" +STR_TO_PREFIX: "إلى " +STR_CALIBRE_RECEIVING: "جاري الاستلام: " +STR_CALIBRE_RECEIVED: "تم الاستلام: " +STR_CALIBRE_INSTRUCTION_1: "1) قم بتثبيت إضافة CrossInk Reader" +STR_CALIBRE_INSTRUCTION_2: "2) اتصل بنفس شبكة Wi-Fi" +STR_CALIBRE_INSTRUCTION_3: "3) في Calibre: اختر \"إرسال إلى الجهاز\"" +STR_CALIBRE_INSTRUCTION_4: "اترك هذه الشاشة مفتوحة أثناء الإرسال" +STR_CAT_DISPLAY: "الشاشة" +STR_CAT_READER: "القراءة" +STR_CAT_CONTROLS: "الأزرار" +STR_CAT_SYSTEM: "النظام" +STR_SLEEP_SCREEN: "شاشة السكون" +STR_SLEEP_SCREEN_WALLPAPER: "ورق جدران" +STR_QUICK_RESUME_TIMEOUT: "استئناف سريع بعد المهلة" +STR_AFTER_TIMEOUT: "بعد المهلة" +STR_SLEEP_COVER_MODE: "عرض الغلاف في وضع السكون" +STR_SLEEP_COVER_MODE_SHORT: "وضع الغطاء" +STR_HIDE_BATTERY: "إخفاء نسبة البطارية" +STR_DISPLAY_THEME_HOME: "الموضوع والصفحة الرئيسية" +STR_DISPLAY_REFRESH: "التحديث" +STR_DISPLAY_SLEEP_SCREEN: "شاشة النوم" +STR_DISPLAY_STATUS: "الحالة" +STR_EXTRA_SPACING: "تباعد إضافي بين الفقرات" +STR_FORCE_PARAGRAPH_INDENTS: "فرض المسافات البادئة للفقرة" +STR_TEXT_AA: "تنعيم حواف النص" +STR_READER_DARK_MODE: "الوضع المظلم" +STR_IMAGES: "الصور" +STR_IMAGES_DISPLAY: "عرض" +STR_IMAGES_PLACEHOLDER: "عنصر نائب" +STR_IMAGES_SUPPRESS: "إخفاء" +STR_EOB_HOME: "الرئيسية" +STR_EOB_CONTINUE_WITH: "المتابعة إلى" +STR_SHORT_PWR_BTN: "ضغطة قصيرة على زر التشغيل" +STR_ORIENTATION: "اتجاه القراءة" +STR_SIDE_BTN_LAYOUT: "تخطيط الأزرار الجانبية (أثناء القراءة)" +STR_ORIENTATION_AWARE: "علم التوجه" +STR_NAV_BUTTONS: "أزرار التنقل" +STR_ALL_BUTTONS: "جميع الأزرار" +STR_LONG_PRESS_SKIP: "الضغط لفترة طويلة على تخطي الفصل" +STR_SIDE_BTN_LONG_PRESS: "إجراء الضغط لفترة طويلة" +STR_CHAPTER_SKIP_OPT: "تخطي الفصل" +STR_CHANGE_FONT_SIZE: "تغيير حجم الخط" +STR_LONG_PRESS_BEHAVIOR: "سلوك الضغطة الطويلة" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "تدوير الأزرار الأمامية مع الشاشة" +STR_LONG_PRESS_BEHAVIOR_OFF: "إيقاف" +STR_LONG_PRESS_BEHAVIOR_SKIP: "تخطي الفصل" +STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "تغيير الاتجاه" +STR_FONT_FAMILY: "خط القراءة" +STR_DICTIONARY_FONT: "خط القاموس" +STR_USE_READER_FONT: "استخدم خط القارئ" +STR_DICTIONARY_FONT_SIZE: "حجم خط القاموس" +STR_USE_READER_FONT_SIZE: "استخدم حجم القارئ" +STR_FONT_SIZE: "حجم الخط" +STR_SD_FONT_SIZE_RANGE: "تحميل نطاق حجم الخط" +STR_READER_FONT_OPTIONS: "خيارات الخط" +STR_READER_PAGE_LAYOUT: "تخطيط الصفحة" +STR_READER_BOOK_STYLING: "تصميم الكتاب" +STR_READER_READING_AIDS: "مساعدات القراءة" +STR_READER_UI: "واجهة مستخدم القارئ" +STR_EPUB_RENDER_MODE: "وضع عرض EPUB" +STR_RENDER_MODE_CROSSINK_DEFAULT: "CrossInk الافتراضي" +STR_RENDER_MODE_BALANCED: "متوازن" +STR_RENDER_MODE_LIGHT: "خفيف" +STR_BALANCED_MODE: "الوضع المتوازن" +STR_LIGHT_MODE: "الوضع الخفيف" +STR_SAFE_MODE: "الوضع الآمن" +STR_LINE_SPACING: "تباعد الأسطر" +STR_WORD_SPACING: "تباعد الكلمات" +STR_LEVEL_1: "1" +STR_LEVEL_2: "2" +STR_LEVEL_3: "3" +STR_LEVEL_4: "4" +STR_SCREEN_MARGIN: "هوامش شاشة القراءة" +STR_PUBLISHER_PAGE_NUMBERS: "أرقام صفحات الناشر" +STR_PARA_ALIGNMENT: "محاذاة الفقرات" +STR_LONG_PRESS_MENU: "قائمة الضغطة الطويلة" +STR_FONT_PREVIEW_TEXT: "نص حكيم له سر قاطع وذو شأن عظيم مكتوب على ثوب أخضر ومغلف بجلد أزرق" +STR_HYPHENATION: "تقسيم الكلمات" +STR_TIME_TO_SLEEP: "مهلة الدخول في السكون" +STR_SHOW_HIDDEN_FILES: "عرض الملفات المخفية" +STR_HIDE_FILE_EXTENSION: "إخفاء امتداد الملف" +STR_FILE_BROWSER_DISPLAY: "عرض متصفح الملفات" +STR_FILE_BROWSER_DISPLAY_1_LINE: "1 خط" +STR_FILE_BROWSER_DISPLAY_2_LINES: "2 خطوط" +STR_REMOVE_READ_FROM_RECENTS: "إزالة الكتب المقروءة من قائمة الكتب الأخيرة" +STR_MOVE_FINISHED_TO_READ: "نقل الكتب المنتهية إلى مجلد المقروءة" +STR_AUTO_BACKUP_STATS: "النسخ الاحتياطية التلقائية" +STR_BACKUP_NOW: "النسخ الاحتياطي الآن" +STR_BACKUP_STATS_CONFIRM: "هل تريد حفظ نسخة في /.crossink-stats-backup/؟" +STR_BACKUP_STATS_DONE: "تم حفظ النسخة الاحتياطية" +STR_BACKUP_STATS_FAILED: "فشل النسخ الاحتياطي" +STR_SYSTEM_DEVICE: "جهاز" +STR_DEVICE_NAME: "اسم الجهاز" +STR_SYSTEM_FILES_CACHE: "الملفات وذاكرة التخزين المؤقت" +STR_ALL_TIME_STATS: "إحصائيات في كل الأوقات" +STR_IDLE_TIME_THRESHOLD: "عتبة وقت الخمول" +STR_SECONDS_VALUE_FORMAT: "%u ثانية" +STR_IDLE_TIME_THRESHOLD_STEP_HINT: "يسار/يمين: 10 ثوانٍ لأعلى/أسفل: 1 م" +STR_RESET_ALL_TIME_STATS: "إعادة تعيين الإحصائيات في كل الأوقات" +STR_RESET_ALL_TIME_STATS_CONFIRM: "هل ترغب في مسح الإحصائيات الدائمة لهذا القارئ؟ يتم الاحتفاظ بالنسخ الاحتياطية." +STR_SYSTEM_NETWORK_SYNC: "الشبكة والمزامنة" +STR_SYSTEM_UPDATES: "التحديثات" +STR_REFRESH_FREQ: "معدل تحديث الشاشة" +STR_KOREADER_SYNC: "مزامنة KOReader" +STR_CHECK_UPDATES: "التحقق من التحديثات" +STR_LANGUAGE: "اللغة" +STR_CLEAR_READING_CACHE: "مسح ذاكرة القراءة المؤقتة" +STR_USERNAME: "اسم المستخدم" +STR_PASSWORD: "كلمة المرور" +STR_SYNC_SERVER_URL: "عنوان خادم المزامنة" +STR_DOCUMENT_MATCHING: "مطابقة المستندات" +STR_SEND_METADATA: "إرسال البيانات الوصفية للمستند" +STR_AUTHENTICATE: "تسجيل الدخول" +STR_KOREADER_USERNAME: "اسم مستخدم KOReader" +STR_KOREADER_PASSWORD: "كلمة مرور KOReader" +STR_FILENAME: "اسم الملف" +STR_AUTHOR_TITLE: "المؤلف - العنوان" +STR_TITLE_AUTHOR: "العنوان - المؤلف" +STR_BINARY: "ثنائي" +STR_SET_CREDENTIALS_FIRST: "أدخل بيانات الاعتماد أولا" +STR_WIFI_CONN_FAILED: "فشل الاتصال بالشبكة" +STR_AUTHENTICATING: "جاري التحقق..." +STR_AUTH_SUCCESS: "تم تسجيل الدخول بنجاح!" +STR_KOREADER_AUTH: "تسجيل الدخول إلى KOReader" +STR_SYNC_READY: "مزامنة KOReader جاهزة للاستخدام" +STR_AUTH_FAILED: "فشل تسجيل الدخول" +STR_DONE: "تم" +STR_CLEAR_CACHE_WARNING_1: "سيؤدي هذا إلى مسح جميع بيانات الكتب المخزنة." +STR_CLEAR_CACHE_WARNING_2: "سيتم فقدان كل تقدم القراءة!" +STR_CLEAR_CACHE_WARNING_3: "ستحتاج الكتب إلى إعادة فهرسة" +STR_CLEAR_CACHE_WARNING_4: "عند فتحها مرة أخرى." +STR_CLEARING_CACHE: "جاري مسح الذاكرة المؤقتة..." +STR_CACHE_CLEARED: "تم مسح الذاكرة المؤقتة" +STR_ITEMS_REMOVED: "عناصر تمت إزالتها" +STR_FAILED_LOWER: "فشل" +STR_CLEAR_CACHE_FAILED: "فشل مسح الذاكرة المؤقتة" +STR_CHECK_SERIAL_OUTPUT: "راجع مخرجات المنفذ التسلسلي للتفاصيل" +STR_DARK: "داكن" +STR_LIGHT: "فاتح" +STR_CUSTOM: "مخصص" +STR_COVER: "الغلاف" +STR_NONE_OPT: "بدون" +STR_FIT: "ملاءمة" +STR_CROP: "قص" +STR_NEVER: "أبدا" +STR_IN_READER: "أثناء القراءة" +STR_ALWAYS: "دائما" +STR_IGNORE: "تجاهل" +STR_SLEEP: "سكون" +STR_PAGE_TURN: "تقليب الصفحة" +STR_FORCE_REFRESH: "تحديث الشاشة" +STR_CHANGE_FONT: "تغيير الخط" +STR_PORTRAIT: "عمودي" +STR_LANDSCAPE_CW: "أفقي (يمين)" +STR_INVERTED: "عكس الألوان" +STR_ORIENTATION_INVERTED: "عمودي 180°" +STR_LANDSCAPE_CCW: "أفقي (يسار)" +STR_PREV_NEXT: "السابق/التالي" +STR_NEXT_PREV: "التالي/السابق" +STR_DISABLED: "معطل" +STR_NEXT_NEXT: "التالي/التالي" +STR_LEXEND_DECA: "Lexend Deca" +STR_BITTER: "Bitter" +STR_CHAREINK: "ChareInk" +STR_ATKINSON_HL: "Atkinson Hyperlegible" +STR_COMPACT: "مدمج" +STR_TINY: "صغير الحجم" +STR_TEENSY: "ضئيل جداً" +STR_ITTY_BITTY: "إيتي بيتي" +STR_SMALL: "صغير" +STR_MEDIUM: "متوسط" +STR_LARGE: "كبير" +STR_X_LARGE: "كبير جدا" +STR_HUGE: "ضخم" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" +STR_FONT_RANGE_ALL: "8-20pt" +STR_TIGHT: "ضيق" +STR_NORMAL: "عادي" +STR_WIDE: "واسع" +STR_JUSTIFY: "ضبط الطرفين" +STR_ALIGN_LEFT: "يسار" +STR_CENTER: "وسط" +STR_ALIGN_RIGHT: "يمين" +STR_PAGES_1: "صفحة واحدة" +STR_PAGES_5: "5 صفحات" +STR_PAGES_10: "10 صفحات" +STR_PAGES_15: "15 صفحة" +STR_PAGES_30: "30 صفحة" +STR_UPDATE: "تحديث" +STR_CHECKING_UPDATE: "جاري التحقق من التحديثات..." +STR_NEW_UPDATE: "يتوفر تحديث جديد!" +STR_CURRENT_VERSION: "الإصدار الحالي: " +STR_NEW_VERSION: "الإصدار الجديد: " +STR_UPDATING: "جاري التحديث..." +STR_NO_UPDATE: "لا يتوفر تحديث" +STR_UPDATE_FAILED: "فشل التحديث" +STR_UPDATE_HASH_MISMATCH: "ملف التحديث غير متطابق" +STR_UPDATE_COMPLETE: "اكتمل التحديث" +STR_POWER_ON_HINT: "اضغط مطولا على زر التشغيل لتشغيل الجهاز من جديد" +STR_RESTARTING_HINT: "جاري إعادة التشغيل... إذا لم يعمل الجهاز، اضغط مطولا على زر التشغيل لعدة ثوان." +STR_NO_ENTRIES: "لم يتم العثور على عناصر" +STR_DOWNLOADING: "جاري التنزيل..." +STR_DOWNLOAD_FAILED: "فشل التنزيل" +STR_ERROR_MSG: "خطأ:" +STR_UNNAMED: "بدون اسم" +STR_NO_SERVER_URL: "لم يتم تحديد عنوان الخادم" +STR_FETCH_FEED_FAILED: "فشل جلب القائمة" +STR_PARSE_FEED_FAILED: "فشل تحليل القائمة" +STR_OPDS_FEED_BUFFER_MEMORY_ERROR: "تعذر تخصيص الذاكرة للمخزن المؤقت" +STR_EPUB_CHAPTER_INCOMPLETE_TITLE: "الفصل غير مكتمل" +STR_EPUB_CHAPTER_INCOMPLETE_BODY: "يحتوي هذا الفصل على علامة EPUB مشوهة. توقف CrossInk عند الجزء غير القابل للقراءة، لذلك قد يكون بعض المحتوى مفقودًا." +STR_NEXT_PAGE: "الصفحة التالية" +STR_PREV_PAGE: "الصفحة السابقة" +STR_NETWORK_PREFIX: "الشبكة: " +STR_IP_ADDRESS_PREFIX: "عنوان IP: " +STR_ERROR_GENERAL_FAILURE: "خطأ: فشل عام" +STR_ERROR_NETWORK_NOT_FOUND: "خطأ: الشبكة غير موجودة" +STR_ERROR_CONNECTION_TIMEOUT: "خطأ: انتهت مهلة الاتصال" +STR_SD_CARD: "بطاقة SD" +STR_TOGGLE_HIDDEN_FILES_HINT: "اضغط لفترة طويلة على مفتاح HOME لتبديل الملفات المخفية" +STR_BACK: "رجوع »" +STR_EXIT: "خروج »" +STR_HOME: "الرئيسية »" +STR_SELECT: "اختيار" +STR_SELECTED: "محدد" +STR_TOGGLE: "تبديل" +STR_CONFIRM: "تأكيد" +STR_CANCEL: "إلغاء" +STR_CONNECT: "اتصال" +STR_OPEN: "فتح" +STR_DOWNLOAD: "تنزيل" +STR_RETRY: "إعادة المحاولة" +STR_YES: "نعم" +STR_NO: "لا" +STR_SHOW: "عرض" +STR_HIDE: "إخفاء" +STR_STATE_ON: "تشغيل" +STR_STATE_OFF: "إيقاف" +STR_NOT_SET: "غير محدد" +STR_DIR_LEFT: "يسار" +STR_DIR_RIGHT: "يمين" +STR_DIR_UP: "أعلى" +STR_DIR_DOWN: "أسفل" +STR_OK_BUTTON: "موافق" +STR_SLEEP_COVER_FILTER: "مرشح غلاف شاشة السكون" +STR_SLEEP_COVER_FILTER_SHORT: "مرشح الغلاف" +STR_FILTER_CONTRAST: "التباين" +STR_CUSTOMISE_STATUS_BAR: "تخصيص شريط الحالة" +STR_CHAPTER_PAGE_COUNT: "عدد صفحات الفصل" +STR_STABLE_PAGE_NUMBERS: "أرقام الصفحات الثابتة" +STR_BOOK_PROGRESS_PERCENTAGE: "نسبة التقدم في الكتاب" +STR_PROGRESS_BAR: "شريط التقدم" +STR_PROGRESS_BAR_THICKNESS: "سمك شريط التقدم" +STR_PROGRESS_BAR_THIN: "رفيع" +STR_PROGRESS_BAR_MEDIUM: "متوسط" +STR_PROGRESS_BAR_THICK: "سميك" +STR_BOOK: "الكتاب" +STR_CHAPTER: "الفصل" +STR_EXAMPLE_CHAPTER: "الفصل 21" +STR_EXAMPLE_BOOK: "عنوان الكتاب" +STR_PREVIEW: "معاينة" +STR_TITLE: "العنوان" +STR_TIME_LEFT: "الوقت المتبقي" +STR_TIME_LEFT_CALCULATING: "جارٍ الحساب..." +STR_BATTERY: "البطارية" +STR_XTC_STATUS_BAR: "شريط حالة XTC" +STR_BOTTOM: "أسفل" +STR_TOP: "أعلى" +STR_CLOCK: "الساعة" +STR_HIDE_CLOCK: "إخفاء الساعة" +STR_CLOCK_UTC_OFFSET: "فرق التوقيت عن UTC" +STR_CLOCK_FORMAT: "تنسيق الساعة" +STR_CLOCK_FORMAT_24H: "24 ساعة" +STR_CLOCK_FORMAT_12H: "12 ساعة" +STR_CURRENT_TIME: "الوقت الحالي:" +STR_NEXT_FIELD: "التالي" +STR_CLOCK_SYNC: "مزامنة الساعة" +STR_CLOCK_SYNC_NOW: "مزامنة الساعة الآن" +STR_CLOCK_SYNCING: "جاري المزامنة عبر NTP..." +STR_CLOCK_SYNC_OK: "تمت مزامنة الساعة" +STR_CLOCK_SYNC_FAIL: "فشلت المزامنة" +STR_CLOCK_SYNC_NO_WIFI: "لا يوجد اتصال Wi-Fi" +STR_CLOCK_SYNC_NO_WIFI_HINT: "اتصل بشبكة Wi-Fi أولا، ثم حاول مرة أخرى." +STR_CLOCK_SYNCED: "تمت مزامنة الساعة" +STR_UI_THEME: "مظهر الواجهة" +STR_THEME_CLASSIC: "كلاسيكي" +STR_THEME_LYRA: "Lyra" +STR_THEME_ROUNDEDRAFF: "RoundedRaff" +STR_THEME_LYRA_EXTENDED: "Lyra موسع" +STR_THEME_LYRA_CAROUSEL: "ليرا كاروسيل" +STR_THEME_MINIMAL: "الحد الأدنى" +STR_THEME_DASHBOARD: "لوحة القيادة" +STR_THEME_MINIMAL_STATS: "إحصائيات الحد الأدنى" +STR_QUICK_RESUME: "استئناف سريع" +STR_RECENT_BOOKS_VIEW: "عرض الكتب الأخيرة" +STR_LIST_VIEW: "قائمة" +STR_GRID_VIEW: "شبكة" +STR_SUNLIGHT_FADING_FIX: "إصلاح بهتان الشاشة في الشمس" +STR_REMAP_FRONT_BUTTONS: "تغيير وظائف الأزرار الأمامية" +STR_REMAP_FRONT_BUTTONS_READER: "أزرار إعادة الخريطة (القارئ)" +STR_OPDS_BROWSER: "متصفح OPDS" +STR_SEARCH: "بحث" +STR_COVER_CUSTOM: "الغلاف + مخصص" +STR_PAGE_OVERLAY: "تراكب الصفحة" +STR_RECENTS: "الأخيرة" +STR_MENU_RECENT_BOOKS: "الكتب الأخيرة" +STR_REMOVE_FROM_RECENTS_ACTION: "إزالة من الكتب الأخيرة" +STR_REMOVE_FROM_RECENTS: "إزالة من الكتب الأخيرة؟" +STR_NO_RECENT_BOOKS: "لا توجد كتب أخيرة" +STR_CALIBRE_DESC: "استخدم النقل اللاسلكي من Calibre" +STR_FORGET_AND_REMOVE: "نسيان الشبكة وحذف كلمة المرور المحفوظة؟" +STR_FORGET_BUTTON: "نسيان" +STR_CALIBRE_STARTING: "جاري تشغيل Calibre..." +STR_CALIBRE_SETUP: "الإعداد" +STR_CALIBRE_STATUS: "الحالة" +STR_CLEAR_BUTTON: "مسح" +STR_DEFAULT_VALUE: "افتراضي" +STR_REMAP_PROMPT: "اضغط زرا أماميا لكل وظيفة" +STR_UNASSIGNED: "غير معين" +STR_ALREADY_ASSIGNED: "معين مسبقا" +STR_REMAP_RESET_HINT: "الزر الجانبي العلوي: استعادة التخطيط الافتراضي" +STR_REMAP_CANCEL_HINT: "الزر الجانبي السفلي: إلغاء التغيير" +STR_HW_BACK_LABEL: "رجوع (الزر 1)" +STR_HW_CONFIRM_LABEL: "تأكيد (الزر 2)" +STR_HW_LEFT_LABEL: "يسار (الزر 3)" +STR_HW_RIGHT_LABEL: "يمين (الزر 4)" +STR_GO_TO_PERCENT: "الانتقال إلى نسبة %" +STR_GO_HOME_BUTTON: "العودة إلى الرئيسية" +STR_SYNC_PROGRESS: "مزامنة التقدم" +STR_DELETE_CACHE: "حذف ذاكرة الكتاب المؤقتة" +STR_SAVE_CLIPPING: "إنشاء لقطة" +STR_VIEW_CLIPPINGS: "عرض القصاصات" +STR_CLIPPINGS: "قصاصات" +STR_BOOKMARKS_AND_CLIPPINGS: "الإشارات المرجعية والقصاصات" +STR_NO_CLIPPINGS: "لا توجد قصاصات بعد" +STR_DELETE_CLIPPINGS: "قصاصات واضحة" +STR_CLIPPING_SAVED: "تم حفظ القصاصة" +STR_CLIPPING_FAILED: "تعذر حفظ القصاصة" +STR_CLIPPING_LIMIT_REACHED: "تم الوصول إلى حد القطع" +STR_BOOK_CACHE_DELETED: "تم حذف ذاكرة التخزين المؤقت للكتاب" +STR_DELETE_BOOK_STATS: "حذف إحصائيات قراءة الكتاب" +STR_BOOK_STATS_DELETED: "تم حذف إحصائيات القراءة" +STR_RESET_BOOK_READER_SETTINGS: "إعادة ضبط إعدادات قارئ الكتب" +STR_BOOK_READER_SETTINGS_RESET: "إعادة ضبط إعدادات قارئ الكتب" +STR_RESET_READING_PACE: "إعادة ضبط سرعة القراءة" +STR_READING_PACE_RESET: "إعادة ضبط سرعة القراءة" +STR_DELETE: "حذف" +STR_PIN_AS_FAVORITE: "تثبيت كمفضل" +STR_UNPIN_AS_FAVORITE: "قم بإلغاء التثبيت كمفضل" +STR_PIN_PNG_WARNING: "إن تثبيت ملف PNG متوافق فقط مع وضع تراكب الصفحة" +STR_SET_AS_SLEEP_FOLDER: "تعيين كمجلد للنوم" +STR_USE_DEFAULT_SLEEP_FOLDERS: "استخدم مجلدات النوم الافتراضية" +STR_DISPLAY_QR: "عرض الصفحة كرمز QR" +STR_CHAPTER_PREFIX: "الفصل: " +STR_PAGES_SEPARATOR: " صفحات | " +STR_BOOK_PREFIX: "الكتاب: " +STR_CALIBRE_URL_HINT: "في Calibre، أضف /opds إلى العنوان" +STR_SYNCING_TIME: "جاري مزامنة الوقت..." +STR_CALC_HASH: "جاري حساب بصمة المستند..." +STR_HASH_FAILED: "فشل حساب بصمة المستند" +STR_FETCH_PROGRESS: "جاري جلب التقدم من الخادم..." +STR_UPLOAD_PROGRESS: "جاري رفع التقدم..." +STR_NO_CREDENTIALS_MSG: "لم يتم إعداد بيانات الاعتماد" +STR_KOREADER_SETUP_HINT: "قم بإعداد حساب KOReader في الإعدادات" +STR_PROGRESS_FOUND: "تم العثور على تقدم سابق!" +STR_REMOTE_LABEL: "الخادم:" +STR_LOCAL_LABEL: "الجهاز:" +STR_PAGE_OVERALL_FORMAT: "صفحة %d، %.2f%% إجمالا" +STR_PAGE_TOTAL_OVERALL_FORMAT: "صفحة %d من %d، %.2f%% إجمالا" +STR_DEVICE_FROM_FORMAT: " من: %s" +STR_APPLY_REMOTE: "استخدام تقدم الخادم" +STR_UPLOAD_LOCAL: "رفع التقدم المحلي" +STR_NO_REMOTE_MSG: "لا يوجد تقدم على الخادم" +STR_UPLOAD_PROMPT: "رفع الموضع الحالي؟" +STR_UPLOAD_SUCCESS: "تم رفع التقدم!" +STR_SYNC_FAILED_MSG: "فشلت المزامنة" +STR_KOREADER_SYNC_AUTH_REJECTED: "تم رفض اسم المستخدم أو كلمة المرور بواسطة خادم المزامنة" +STR_KOREADER_SYNC_NETWORK_ERROR: "تعذر الوصول إلى خادم المزامنة. تحقق من WiFi وعنوان URL للخادم." +STR_KOREADER_SYNC_NETWORK_REFUSED: "رفض خادم المزامنة الاتصال. تحقق من عنوان URL للخادم وأن الخادم متصل بالإنترنت." +STR_KOREADER_SYNC_NETWORK_TIMEOUT: "انتهت مهلة خادم المزامنة. تحقق من قوة WiFi وحاول مرة أخرى." +STR_KOREADER_SYNC_NETWORK_TLS: "تعذر إجراء اتصال آمن. تحقق من عنوان URL للخادم وإعداد HTTPS." +STR_KOREADER_SYNC_HTTP_404: "لم يتم العثور على نقطة نهاية المزامنة. تأكد من أن عنوان URL للخادم يشير إلى خادم مزامنة KOReader الخاص بك." +STR_KOREADER_SYNC_HTTP_STATUS_FORMAT: "قام خادم المزامنة بإرجاع HTTP %d. حاول مرة أخرى لاحقًا." +STR_KOREADER_SYNC_SERVER_ERROR: "قام خادم المزامنة بإرجاع استجابة غير متوقعة. حاول مرة أخرى لاحقًا." +STR_KOREADER_SYNC_BAD_RESPONSE: "رد خادم المزامنة ببيانات غير صالحة. تحقق من عنوان URL للخادم أو إعداد الوكيل." +STR_KOREADER_SYNC_LOW_MEMORY: "لا توجد ذاكرة خالية كافية لإنهاء المزامنة. يرجى إعادة المحاولة." +STR_UNKNOWN_ERROR: "خطأ غير معروف" +STR_SAVE_PROGRESS_FAILED: "تعذر حفظ التقدم" +STR_SECTION_PREFIX: "القسم " +STR_UPLOAD: "رفع" +STR_BOOK_S_STYLE: "تنسيق الكتاب الأصلي" +STR_EMBEDDED_STYLE: "التنسيق المضمن" +STR_BIONIC_READING: "القراءة الكترونية" +STR_GUIDE_READING: "النقاط الإرشادية" +STR_OPDS_SERVER_URL: "عنوان خادم OPDS" +STR_PWR_BTN_FOOTNOTE_BACK: "عودة سريعة من الحواشي" +STR_SET_SLEEP_COVER: "تعيين الغلاف" +STR_FOOTNOTES: "الحواشي" +STR_NO_FOOTNOTES: "لا توجد حواش في هذه الصفحة" +STR_LINK: "[رابط]" +STR_SCREENSHOT_BUTTON: "التقاط لقطة شاشة" +STR_AUTO_TURN_ENABLED: "التقليب التلقائي مفعل: " +STR_AUTO_TURN_INTERVAL_SECONDS: "الفاصل الزمني لتدوير الصفحة تلقائيًا (بالثواني)" +STR_AUTO_TURN_STEP_HINT: "يسار/يمين: 1 ثانية لأعلى/لأسفل: 5 ثوانٍ" +STR_SLEEP_TIMER_VALUE_FORMAT: "%u دقيقة" +STR_READING_STATS: "إحصائيات القراءة" +STR_EDIT: "يحرر" +STR_MORE: "أكثر" +STR_TRACK_READING_STATS: "تتبع إحصائيات القراءة" +STR_MARK_FINISHED: "وضع علامة \"منتهي\"." +STR_MARK_UNFINISHED: "وضع علامة \"غير مكتمل\"." +STR_MARK_FINISHED_PROMPT_TITLE: "وضع علامة \"منتهي\"؟" +STR_MARK_FINISHED_PROMPT_BODY: "لقد وصلت إلى 99% of هذا الكتاب" +STR_READER_OPTIONS: "خيارات الكتاب" +STR_BOOKMARKS: "الإشارات المرجعية" +STR_ADD_BOOKMARK: "إضافة إشارة مرجعية" +STR_REMOVE_BOOKMARK: "إزالة الإشارة المرجعية" +STR_VIEW_BOOKMARKS: "عرض الإشارات المرجعية" +STR_DELETE_BOOKMARKS: "مسح قائمة الإشارات المرجعية" +STR_BOOKMARK_ADDED: "تمت إضافة الإشارة المرجعية." +STR_BOOKMARK_REMOVED: "تمت إزالة الإشارة المرجعية." +STR_BOOKMARK_LIMIT_REACHED: "تم الوصول إلى حد الإشارة المرجعية" +STR_NO_BOOKMARKS: "لا توجد إشارات مرجعية حتى الآن" +STR_DELETE_BOOKMARK: "يمسح" +STR_BOOKMARK_PAGE_FORMAT: "الصفحة %d" +STR_UNKNOWN_CHAPTER: "الفصل غير معروف" +STR_STATS_SESSIONS: "الجلسات:" +STR_STATS_TOTAL_TIME: "الوقت الإجمالي:" +STR_STATS_AVG_SESSION: "متوسط حصة:" +STR_STATS_LESS_THAN_MIN: "< 1 دقيقة" +STR_STATS_THIS_BOOK: "هذا الكتاب" +STR_STATS_AVERAGES: "المتوسطات" +STR_STATS_PAGES_PER_MIN: "الصفحات/الدقيقة" +STR_STATS_COMPLETED_LBL: "كتب مقروءة" +STR_STATS_SESSIONS_LBL: "الجلسات" +STR_STATS_TIME_LBL: "وقت القراءة" +STR_STATS_PAGES_LBL: "تحولت الصفحات" +STR_STATS_PROGRESS_LBL: "تقدم" +STR_STATS_AVG_SESSION_LBL: "متوسط ​​الجلسة" +STR_STATS_DAILY_AVG_LBL: "المتوسط ​​اليومي" +STR_STATS_READING_STREAK_LBL: "خط القراءة" +STR_STATS_LONGEST_STREAK_LBL: "أطول خط" +STR_STATS_ALL_TIME: "جميع الكتب" +STR_STATS_TOTAL_READING_TIME_LBL: "إجمالي وقت القراءة" +STR_STATS_THIS_DEVICE: "جميع الكتب (هذا الجهاز)" +STR_STATS_ALL_DEVICES: "جميع الكتب (جميع الأجهزة)" +STR_STATS_THIS_DEVICE_SCREEN: "إحصائيات القراءة - هذا الجهاز" +STR_STATS_ALL_DEVICES_SCREEN: "إحصائيات القراءة - جميع الأجهزة المتزامنة" +STR_STATS_TIME_OF_DAY: "الوقت من اليوم" +STR_STATS_DAY_OF_WEEK: "يوم الأسبوع" +STR_STATS_MORNING: "صباح" +STR_STATS_AFTERNOON: "بعد الظهر" +STR_STATS_EVENING: "مساء" +STR_STATS_NIGHT: "ليلة" +STR_STATS_MON: "الاثنين" +STR_STATS_TUE: "الثلاثاء" +STR_STATS_WED: "تزوج" +STR_STATS_THU: "الخميس" +STR_STATS_FRI: "الجمعة" +STR_STATS_SAT: "قعد" +STR_STATS_SUN: "شمس" +STR_STATS_START_DATE: "تاريخ البدء" +STR_STATS_FINISHED_DATE: "تاريخ الانتهاء" +STR_STATS_EST_FINISH_DATE: "EST. تاريخ الانتهاء" +STR_STATS_STARTED: "بدأت" +STR_STATS_DAY: "يوم" +STR_STATS_DAYS: "أيام" +STR_STATS_DAY_STREAK_FORMAT: "%u خط اليوم" +STR_STATS_NO_STREAK: "لا يوجد خط حتى الآن" +STR_STATS_NEW_READER: "قارئ جديد" +STR_STATS_MORNING_READER: "قارئ الصباح" +STR_STATS_AFTERNOON_READER: "القارئ بعد الظهر" +STR_STATS_EVENING_READER: "قارئ المساء" +STR_STATS_NIGHT_READER: "القارئ الليلي" +STR_SLEEP_NEVER: "أبدا" +STR_STEP_HINT_FRONT: "الأزرار الأمامية:" +STR_STEP_HINT_SIDE: "الأزرار الجانبية:" +STR_ADD_SERVER: "إضافة خادم" +STR_SERVER_NAME: "اسم الخادم" +STR_NO_SERVERS: "لم يتم إعداد خوادم OPDS" +STR_DELETE_SERVER: "حذف الخادم" +STR_DELETE_CONFIRM: "هل تريد حذف هذا الخادم؟" +STR_OPDS_SERVERS: "خوادم OPDS" +STR_MOVE_TO_READ_FAILED_TITLE: "لا يمكن نقل الكتاب" +STR_MOVE_TO_READ_FAILED_BODY: "فشل في نقل \"%s\" إلى مجلد / القراءة." +STR_DOWNLOAD_FONTS: "تحميل الخطوط" +STR_FONT_DOWNLOAD: "تحميل الخط" +STR_MANAGE_FONTS: "إدارة الخطوط" +STR_FONT_BROWSER: "متصفح الخطوط" +STR_LOADING_FONT_LIST: "جاري تحميل قائمة الخطوط..." +STR_NO_FONTS_AVAILABLE: "لا تتوفر خطوط" +STR_FONT_INSTALLED: "تم تثبيت الخط!" +STR_FONT_INSTALL_FAILED: "فشل تثبيت الخط" +STR_FONT_DOWNLOAD_INTERRUPTED: "تمت مقاطعة الشبكة أثناء التنزيل" +STR_FONT_DOWNLOAD_RETRY_HINT: "ستؤدي إعادة المحاولة إلى مواصلة التقدم المحفوظ." +STR_FONT_DOWNLOAD_CHECKSUM_HINT: "ستؤدي إعادة المحاولة إلى تنزيل هذا الملف مرة أخرى." +STR_INSTALLED: "مثبت" +STR_CONFIRM_DOWNLOAD_PROMPT: "تحميل؟" +STR_SD_CARD_FULL: "مساحة بطاقة SD غير كافية" +STR_FILES_LABEL: "الملفات:" +STR_SIZE_LABEL: "مقاس:" +STR_REDOWNLOAD: "إعادة التحميل" +STR_DOWNLOAD_ALL: "تنزيل الكل" +STR_UPDATE_ALL: "تحديث الكل" +STR_ALL_FONTS_INSTALLED: "تم تثبيت كافة الخطوط!" +STR_UPDATE_AVAILABLE: "تحديث" +STR_CRASH_TITLE: "تعطل النظام" +STR_CRASH_DESCRIPTION: "تم حفظ تقرير مفصل في الملف crash_report.txt. يرجى إرفاق هذا الملف عند الإبلاغ عن المشكلة." +STR_CRASH_REASON: "سبب التعطل:" +STR_CRASH_NO_REASON: "(لم يتم تسجيل سبب)" +STR_TILT_PAGE_TURN: "تقليب الصفحة بالإمالة" +STR_TILT_PAGE_TURN_DIRECTION: "اتجاه الميل" +STR_TILT_DIRECTION_LEFT_RIGHT: "اليسار واليمين" +STR_TILT_DIRECTION_LEFT_RIGHT_INVERTED: "اليمين واليسار" +STR_TILT_DIRECTION_FORWARD_BACK: "إلى الأمام إلى الخلف" +STR_TILT_DIRECTION_FORWARD_BACK_INVERTED: "العودة إلى الأمام" +STR_KB_HINT_MOVE_CURSOR: "اضغط زر اليمين أو اليسار لتحريك المؤشر" +STR_KB_HINT_RETURN_CURSOR: "اضغط زر اليسار للعودة إلى موضع المؤشر" +STR_KB_HINT_HIDE_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [***] لإخفاء كلمة المرور" +STR_KB_HINT_SHOW_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [abc] لإظهار كلمة المرور" +STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "اضغط [***] لإخفاء كلمة المرور" +STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "اضغط [abc] لإظهار كلمة المرور" +STR_KB_HINT_EDIT_ENTRY: "اضغط مطولا على زر الأعلى للتعديل" +STR_KB_TIPS: "نصائح:" +STR_KB_HINT_RETURN_KEYBOARD: "اضغط زر الأسفل للعودة إلى لوحة المفاتيح" +STR_KB_HINT_EXIT_URL_MODE: "اضغط ABC للخروج من وضع URL" +STR_KB_HINT_CLEAR_TEXT: "اضغط مطولا على DEL لمسح كل النص" +STR_KB_HINT_SECONDARY_CHAR: "اضغط مطولا على زر الاختيار للحرف الثانوي" +STR_KB_HINT_UPPER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الكبير أو الثانوي" +STR_KB_HINT_LOWER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الصغير أو الثانوي" +STR_KB_HINT_URL_SNIPPETS: "اضغط URL للاختصارات" +STR_CONTROLS_GENERAL: "عام" +STR_CONTROLS_IN_READER: "الضوابط داخل القارئ" +STR_POWER_BUTTON: "زر الطاقة" +STR_FRONT_BUTTONS: "أزرار أمامية" +STR_SIDE_BUTTONS: "أزرار جانبية" +STR_OTHER: "آخر" +STR_LONG_PRESS_ACTION: "إجراء الضغط لفترة طويلة" +STR_LONG_PRESS_MENU_ACTION: "الضغط لفترة طويلة على إجراء القائمة" +STR_LONG_PRESS_BACK_ACTION: "الضغط لفترة طويلة على الإجراء الخلفي" +STR_TOGGLE_GUIDE_DOTS: "النقاط الإرشادية" +STR_TOGGLE_BIONIC_READING: "القراءة الكترونية" +STR_TOGGLE_BOOKMARK: "إضافة/إزالة إشارة مرجعية" +STR_CYCLE_PAGE_TURN: "تشغيل الصفحة تلقائيًا" +STR_MARKED_FINISHED: "تم وضع علامة \"منتهي\"." +STR_MARKED_UNFINISHED: "تم وضع علامة \"غير مكتمل\"." +STR_TILT_TO_TURN_OFF: "الميل إلى الدوران: إيقاف" +STR_TILT_TO_TURN_ON: "الميل إلى الدوران: تشغيل" +STR_LOW_MEMORY_IMAGES_TITLE: "تم تحميل الفصل بدون بعض أو كل الصور" +STR_LOW_MEMORY_IMAGES_BODY: "كانت الذاكرة المتوفرة منخفضة جدًا بحيث لا يمكن استخراج كل صورة لهذا الفصل." +STR_EPUB_LAYOUT_MEMORY_TITLE: "الفصل يحتاج إلى الكثير من الذاكرة" +STR_EPUB_LAYOUT_MEMORY_BODY: "يحتاج هذا الفصل إلى ذاكرة أكبر لوضعها مما يمكن لهذا الجهاز توفيره بأمان." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_BOTH: "حاول مرة أخرى مع إيقاف تشغيل القراءة الإلكترونية وتوجيه النقاط." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_BIONIC: "حاول مرة أخرى مع إيقاف تشغيل Bionic Reading." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_GUIDE: "حاول مرة أخرى مع إيقاف تشغيل \"النقاط الإرشادية\"." +STR_EPUB_FALLBACK_FONT_TITLE: "الفصل باستخدام الخط المدمج" +STR_EPUB_FALLBACK_FONT_BODY: "كانت الذاكرة المتوفرة منخفضة جدًا بحيث لا يمكن عرض هذا الفصل بخط بطاقة SD، لذلك تم تحميله بالخط المدمج." +STR_SD_FIRMWARE_UPDATE: "تحديث البرنامج الثابت من بطاقة SD" +STR_SELECT_FIRMWARE_FILE: "اختر ملف البرنامج الثابت (.bin)" +STR_NO_BIN_FILES: "لم يتم العثور على ملفات .bin" +STR_VALIDATING_FIRMWARE: "جاري التحقق من البرنامج الثابت..." +STR_INVALID_FIRMWARE: "ملف برنامج ثابت غير صالح" +STR_FIRMWARE_TOO_LARGE: "البرنامج الثابت أكبر من القسم المخصص له" +STR_FIRMWARE_TOO_SMALL: "ملف البرنامج الثابت صغير جدا" +STR_FIRMWARE_UPDATE_PROMPT: "تحديث البرنامج الثابت؟" +STR_FIRMWARE_FILE_OPEN_FAILED: "تعذر فتح الملف" +STR_FIRMWARE_WRITE_FAILED: "فشلت كتابة البرنامج الثابت" +STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "لا تطفئ الجهاز!" +STR_RECOVERY_MODE: "وضع الاستعادة" +STR_RECOVERY_MODE_HINT: "ضع الملف firmware.bin في المجلد الرئيسي لبطاقة SD ثم اختره" +STR_NEARBY_STATS_READY: "على استعداد لمزامنة كلا القراء" +STR_NEARBY_STATS_SYNC_BUTTON: "مزامنة" +STR_NEARBY_STATS_READY_HINT: "اضغط على Sync على قارئ واحد فقط" +STR_NEARBY_STATS_SCANNING: "العثور على القارئ لتبادل الإحصائيات" +STR_NEARBY_STATS_SYNCING: "إرسال واستقبال الإحصائيات" +STR_NEARBY_STATS_SYNCED: "تمت مزامنة كلا القراء" +STR_NEARBY_STATS_SIMULATOR_UNAVAILABLE: "مزامنة الإحصائيات القريبة غير متوفرة في جهاز المحاكاة" +STR_NEARBY_POSITION_READY: "على استعداد لمشاركة هذا الموقف" +STR_NEARBY_POSITION_SHARE_BUTTON: "يشارك" +STR_NEARBY_POSITION_READY_HINT: "اضغط على مشاركة على X3/X4 في الصفحة الصحيحة" +STR_NEARBY_POSITION_SCANNING: "العثور على القارئ لتبادل الموقف" +STR_NEARBY_POSITION_SHARING: "مشاركة هذا الموقف" +STR_NEARBY_POSITION_WAITING: "في انتظار الموقف المشترك" +STR_NEARBY_POSITION_APPLYING: "موقف الإرسال" +STR_NEARBY_POSITION_SHARED: "الموقف مشترك" +STR_NEARBY_POSITION_SYNCED: "تمت مزامنة موضع الكتاب" +STR_NEARBY_POSITION_FOUND: "تم العثور على موقع قريب!" +STR_NEARBY_POSITION_START_FAILED: "تعذر بدء مزامنة الموقع القريب" +STR_NEARBY_POSITION_DEVICE_ID_FAILED: "لا يمكن قراءة معرف الجهاز" +STR_NEARBY_POSITION_QUEUE_OVERFLOW: "تجاوزت قائمة انتظار المزامنة" +STR_NEARBY_POSITION_TIMEOUT: "انتهت مهلة مزامنة الموضع" +STR_NEARBY_POSITION_NO_READER: "لم يتم العثور على قارئ قريب" +STR_NEARBY_LABEL: "قريب:" +STR_APPLY_NEARBY_POSITION: "تطبيق موقف قريب" +STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "مزامنة الموقع القريب غير متوفرة في جهاز المحاكاة" +STR_NEARBY_TRANSFER_VALIDATING: "جارٍ التحقق..." diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index 5c2d4caa42..f41bce1cff 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Захаваць пароль?" STR_PRESS_OK_SCAN: "Націсніце OK для паўторнага пошуку" STR_JOIN_NETWORK: "Падключыцца да сеткі" STR_CREATE_HOTSPOT: "Стварыць кропку доступу" -STR_NEARBY_STATS_SYNC: "Сінхранізацыя статыстыкі побач" -STR_NEARBY_STATS_SYNC_DESC: "Сінхранізаваць статыстыку чытання CrossInk з X3/X4 побач" +STR_NEARBY_DEVICE: "Прылада побач" +STR_NEARBY_STATS_SYNC: "Сінхранізаваць статыстыку" +STR_RECEIVE_NEARBY_BOOK: "Атрымаць файл" +STR_RECEIVE_NEARBY_BOOK_DESC: "Атрымаць файл з іншай чыталкі" +STR_NEARBY_STATS_SYNC_DESC: "Сінхранізаваць статыстыку чытання паміж чыталкамі" STR_JOIN_DESC: "Падключэнне да існуючай сеткі Wi-Fi" STR_HOTSPOT_DESC: "Стварыць сетку Wi-Fi для падключэння іншых" STR_STARTING_HOTSPOT: "Запуск кропкі доступу..." @@ -85,12 +88,19 @@ STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі" STR_ORIENTATION_AWARE: "З улікам арыентацыі" STR_NAV_BUTTONS: "Кнопкі навігацыі" STR_ALL_BUTTONS: "Усе кнопкі" +STR_TOUCH_READER_CONTROLS: "Сэнсарнае кіраванне чытаннем" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Арыентаваць пярэднія кнопкі" STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела" STR_LONG_PRESS_BEHAVIOR: "Паводзіны доўгага націскання" STR_FONT_PREVIEW_TEXT: "У Іўі худы жвавы чорт у зялёнай камізэльцы пабег пад'есці фаршу з юшкай" STR_FONT_FAMILY: "Шрыфт чытання" +STR_DICTIONARY_FONT: "Шрыфт слоўніка" +STR_USE_READER_FONT: "Выкарыстоўваць шрыфт чыталкі" +STR_DICTIONARY_FONT_SIZE: "Памер шрыфту слоўніка" +STR_USE_READER_FONT_SIZE: "Выкарыстоўваць памер шрыфту чыталкі" STR_FONT_SIZE: "Памер шрыфту інтэрфейсу" STR_LINE_SPACING: "Міжрадковы інтэрвал" +STR_WORD_SPACING: "Інтэрвал паміж словамі" STR_SCREEN_MARGIN: "Палі экрана" STR_PUBLISHER_PAGE_NUMBERS: "Нумары старонак выдаўца" STR_PARA_ALIGNMENT: "Выраўноўванне абзаца" @@ -105,6 +115,7 @@ STR_USERNAME: "Імя карыстальніка" STR_PASSWORD: "Пароль" STR_SYNC_SERVER_URL: "URL сервера сінхранізацыі" STR_DOCUMENT_MATCHING: "Супастаўленне дакументаў" +STR_SEND_METADATA: "Адпраўляць метаданыя дакумента" STR_AUTHENTICATE: "Аўтарызацыя" STR_KOREADER_USERNAME: "Імя карыстальніка KOReader" STR_KOREADER_PASSWORD: "Пароль KOReader" @@ -393,11 +404,10 @@ STR_CLOCK_SYNC_NOW: "Сінхранізаваць дату/час" STR_CLOCK_UTC_OFFSET: "Зрух UTC гадзінніка" STR_DISABLED: "Адключана" STR_DISPLAY_SLEEP_SCREEN: "Экран сну" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_REFRESH: "Абнавіць экран" STR_FRONT_BUTTONS: "Пярэднія кнопкі" STR_HUGE: "Вялізны" @@ -466,3 +476,6 @@ STR_NEARBY_POSITION_NO_READER: "Чыталка побач не знойдзен STR_NEARBY_LABEL: "Побач:" STR_APPLY_NEARBY_POSITION: "Ужыць пазіцыю побач" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Сінхранізацыя пазіцыі побач недаступная ў сімулятары" +STR_ADD_HIDDEN_NETWORK: "Дадаць схаваную сетку..." +STR_ENTER_WIFI_SSID: "Увядзіце назву сеткі (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Праверка..." diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index 984ae68371..437ea385a8 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Voleu desar la contrasenya per a la propera vegada?" STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar" STR_JOIN_NETWORK: "Uneix-te a una xarxa" STR_CREATE_HOTSPOT: "Crea un punt d'accés" -STR_NEARBY_STATS_SYNC: "Sincronització propera" -STR_NEARBY_STATS_SYNC_DESC: "Sincronitza estadístiques de lectura de CrossInk amb un X3/X4 proper" +STR_NEARBY_DEVICE: "Dispositiu proper" +STR_NEARBY_STATS_SYNC: "Sincronitza estadístiques" +STR_RECEIVE_NEARBY_BOOK: "Rep un fitxer" +STR_RECEIVE_NEARBY_BOOK_DESC: "Rep un fitxer d'un altre lector" +STR_NEARBY_STATS_SYNC_DESC: "Sincronitza les estadístiques de lectura entre lectors" STR_JOIN_DESC: "Connecta't a una xarxa WiFi existent" STR_HOTSPOT_DESC: "Crea una xarxa WiFi per unir-s'hi" STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..." @@ -91,12 +94,19 @@ STR_ORIENTATION_AWARE: "Segons orientació" STR_NAV_BUTTONS: "Botons de navegació" STR_ALL_BUTTONS: "Tots els botons" STR_LONG_PRESS_BEHAVIOR: "Comportament de pulsació llarga" +STR_TOUCH_READER_CONTROLS: "Controls tàctils del lector" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals" STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat" STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació" STR_FONT_FAMILY: "Tipus de lletra" +STR_DICTIONARY_FONT: "Tipus de lletra del diccionari" +STR_USE_READER_FONT: "Utilitza la lletra del lector" +STR_DICTIONARY_FONT_SIZE: "Mida de la lletra del diccionari" +STR_USE_READER_FONT_SIZE: "Utilitza la mida del lector" STR_FONT_SIZE: "Mida de la lletra (UI)" STR_LINE_SPACING: "Interlineat del lector" +STR_WORD_SPACING: "Espaiat entre paraules" STR_SCREEN_MARGIN: "Marge de pantalla del lector" STR_PUBLISHER_PAGE_NUMBERS: "Números de pàgina de l'editorial" STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector" @@ -129,6 +139,7 @@ STR_USERNAME: "Nom d'usuari" STR_PASSWORD: "Contrasenya" STR_SYNC_SERVER_URL: "URL del servidor de sincronització" STR_DOCUMENT_MATCHING: "Coincidència de documents" +STR_SEND_METADATA: "Envia metadades del document" STR_AUTHENTICATE: "Autentica" STR_KOREADER_USERNAME: "Nom d'usuari del KOReader" STR_KOREADER_PASSWORD: "Contrasenya del KOReader" @@ -459,11 +470,10 @@ STR_CHANGE_FONT_SIZE: "Canvia la mida de la lletra" STR_CHAPTER_SKIP_OPT: "Saltar capítol" STR_CHAREINK: "ChareInk" STR_DISPLAY_SLEEP_SCREEN: "Pantalla de repòs" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "molt petit" -STR_FONT_RANGE_TINY: "petit" -STR_FONT_RANGE_XLARGE: "molt gran" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Força el sagnat dels paràgrafs" STR_FRONT_BUTTONS: "Botons frontals" STR_GUIDE_READING: "Punts de guia" @@ -527,3 +537,6 @@ STR_NEARBY_POSITION_NO_READER: "No s'ha trobat cap lector proper" STR_NEARBY_LABEL: "Proper:" STR_APPLY_NEARBY_POSITION: "Aplica la posició propera" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "La sincronització de posició propera no està disponible al simulador" +STR_ADD_HIDDEN_NETWORK: "Afegeix una xarxa oculta..." +STR_ENTER_WIFI_SSID: "Introdueix el nom de la xarxa (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "S'està validant..." diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index 26b2b66895..8a00e61ae3 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -21,6 +21,7 @@ STR_SELECT_CHAPTER: "Vybrat kapitolu" STR_NO_CHAPTERS: "Žádné kapitoly" STR_END_OF_BOOK: "Konec knihy" STR_EMPTY_CHAPTER: "Prázdná kapitola" +STR_INDEX_FAILED: "Indexování selhalo - neplatná kniha" STR_INDEXING: "Indexování" STR_MEMORY_ERROR: "Chyba paměti" STR_PAGE_LOAD_ERROR: "Chyba načítání stránky" @@ -32,7 +33,10 @@ STR_WIFI_NETWORKS: "WiFi sítě" STR_NO_NETWORKS: "Žádné sítě nenalezeny" STR_NETWORKS_FOUND: "Nalezeno %zu sítí" STR_SCANNING: "Skenování..." +STR_FINDING_SAVED_WIFI: "Hledání uložené Wi-Fi..." STR_CONNECTING: "Připojování..." +STR_CONNECTING_SAVED_WIFI: "Připojování k uložené Wi-Fi..." +STR_SHOW_NETWORKS: "Zobrazit" STR_CONNECTED: "Připojeno!" STR_CONNECTION_FAILED: "Připojení se nezdařilo" STR_FORGET_NETWORK: "Zapomenout síť?" @@ -40,8 +44,11 @@ STR_SAVE_PASSWORD: "Uložit heslo pro příště?" STR_PRESS_OK_SCAN: "Stiskněte OK pro přeskenování" STR_JOIN_NETWORK: "Připojit se k síti" STR_CREATE_HOTSPOT: "Vytvořit hotspot" -STR_NEARBY_STATS_SYNC: "Blízká synchronizace statistik" -STR_NEARBY_STATS_SYNC_DESC: "Synchronizovat statistiky čtení CrossInk s blízkým X3/X4" +STR_NEARBY_DEVICE: "Blízké zařízení" +STR_NEARBY_STATS_SYNC: "Synchronizovat statistiky" +STR_RECEIVE_NEARBY_BOOK: "Přijmout soubor" +STR_RECEIVE_NEARBY_BOOK_DESC: "Přijmout soubor z jiné čtečky" +STR_NEARBY_STATS_SYNC_DESC: "Synchronizovat statistiky čtení mezi čtečkami" STR_JOIN_DESC: "Připojit se k existující síti WiFi" STR_HOTSPOT_DESC: "Vytvořit síť WiFi, ke které se mohou připojit ostatní" STR_STARTING_HOTSPOT: "Spouštění hotspotu..." @@ -56,19 +63,19 @@ STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo" STR_MAC_ADDRESS: "MAC adresa:" STR_CHECKING_WIFI: "Kontrola WiFi..." STR_ENTER_WIFI_PASSWORD: "Zadejte heslo WiFi" -STR_TO_PREFIX: "pro" -STR_CALIBRE_RECEIVING: "Příjem:" -STR_CALIBRE_RECEIVED: "Přijato:" +STR_TO_PREFIX: "k " +STR_CALIBRE_RECEIVING: "Příjem: " +STR_CALIBRE_RECEIVED: "Přijato: " STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader" STR_CALIBRE_INSTRUCTION_2: "2) Buďte ve stejné síti WiFi" STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odeslat do zařízení“" -STR_CALIBRE_INSTRUCTION_4: "„Při odesílání ponechat tuto obrazovku otevřenou“" +STR_CALIBRE_INSTRUCTION_4: "Při odesílání nechte tuto obrazovku otevřenou" STR_CAT_DISPLAY: "Displej" STR_CAT_READER: "Čtečka" STR_CAT_CONTROLS: "Ovládání" STR_CAT_SYSTEM: "Systém" STR_SLEEP_SCREEN: "Obrazovka spánku" -STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu" +STR_SLEEP_COVER_MODE: "Režim obálky obrazovky spánku" STR_HIDE_BATTERY: "Skrýt baterii %" STR_EXTRA_SPACING: "Extra mezery mezi odstavci" STR_TEXT_AA: "Vyhlazování textu" @@ -88,13 +95,24 @@ STR_NAV_BUTTONS: "Navigační tlačítka" STR_ALL_BUTTONS: "Všechna tlačítka" STR_LONG_PRESS_SKIP: "Dlouhé stisknutí pro přeskočení kapitoly" STR_LONG_PRESS_BEHAVIOR: "Chování při dlouhém stisku" +STR_TOUCH_READER_CONTROLS: "Dotykové ovládání čtečky" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientovat přední tlačítka" STR_LONG_PRESS_BEHAVIOR_OFF: "VYP" STR_LONG_PRESS_BEHAVIOR_SKIP: "Přeskočení kapitoly" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Změna orientace" STR_FONT_PREVIEW_TEXT: "Příliš žluťoučký kůň úpěl ďábelské ódy" STR_FONT_FAMILY: "Rodina písem čtečky" +STR_DICTIONARY_FONT: "Písmo slovníku" +STR_USE_READER_FONT: "Použít písmo čtečky" +STR_DICTIONARY_FONT_SIZE: "Velikost písma slovníku" +STR_USE_READER_FONT_SIZE: "Použít velikost písma čtečky" STR_FONT_SIZE: "Velikost písma rozhraní" STR_LINE_SPACING: "Řádkování čtečky" +STR_WORD_SPACING: "Mezery mezi slovy" +STR_LEVEL_1: "1" +STR_LEVEL_2: "2" +STR_LEVEL_3: "3" +STR_LEVEL_4: "4" STR_SCREEN_MARGIN: "Okraj obrazovky čtečky" STR_PUBLISHER_PAGE_NUMBERS: "Čísla stran vydavatele" STR_PARA_ALIGNMENT: "Zarovnání odstavců čtečky" @@ -114,7 +132,7 @@ STR_IDLE_TIME_THRESHOLD_STEP_HINT: "Vlevo/Vpravo: 10s Nahoru/Dolů: 1m" STR_RESET_ALL_TIME_STATS: "Resetovat statistiky za celou dobu" STR_RESET_ALL_TIME_STATS_CONFIRM: "Vymazat statistiky za celou dobu na této čtečce? Zálohy zůstanou zachovány." STR_REFRESH_FREQ: "Frekvence obnovení" -STR_KOREADER_SYNC: "KOReaderu Sync" +STR_KOREADER_SYNC: "Synchronizace KOReader" STR_CHECK_UPDATES: "Zkontrolovat aktualizace" STR_LANGUAGE: "Jazyk" STR_CLEAR_READING_CACHE: "Vymazat mezipaměť čtení" @@ -122,7 +140,11 @@ STR_USERNAME: "Uživatelské jméno" STR_PASSWORD: "Heslo" STR_SYNC_SERVER_URL: "URL synch. serveru" STR_DOCUMENT_MATCHING: "Párování dokumentů" -STR_AUTHENTICATE: "Ověření" +STR_SEND_METADATA: "Odesílat metadata dokumentu" +STR_SYNC_BEHAVIOR: "Chování synchronizace" +STR_ASK_EVERY_TIME: "Vždy se zeptat" +STR_SMART_SYNC: "Chytrá synchronizace" +STR_AUTHENTICATE: "Ověřit" STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu" STR_KOREADER_PASSWORD: "Heslo KOReaderu" STR_FILENAME: "Název souboru" @@ -143,12 +165,12 @@ STR_CLEAR_CACHE_WARNING_3: "Knihy bude nutné znovu indexovat" STR_CLEAR_CACHE_WARNING_4: "při opětovném otevření." STR_CLEARING_CACHE: "Mazání mezipaměti..." STR_CACHE_CLEARED: "Mezipaměť vymazána" -STR_ITEMS_REMOVED: "položky odstraněny" +STR_ITEMS_REMOVED: "položek odstraněno" STR_FAILED_LOWER: "selhalo" STR_CLEAR_CACHE_FAILED: "Vymazání mezipaměti se nezdařilo" STR_CHECK_SERIAL_OUTPUT: "Podrobnosti naleznete v sériovém výstupu" -STR_DARK: "Tmavý" -STR_LIGHT: "Světlý" +STR_DARK: "Tmavá" +STR_LIGHT: "Světlá" STR_CUSTOM: "Vlastní" STR_COVER: "Obálka" STR_NONE_OPT: "Žádný" @@ -168,12 +190,10 @@ STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček" STR_PREV_NEXT: "Předchozí/Další" STR_NEXT_PREV: "Další/Předchozí" STR_NEXT_NEXT: "Další/Další" -STR_NOTO_SERIF: "Noto Serif" -STR_NOTO_SANS: "Noto Sans" STR_SMALL: "Malý" STR_MEDIUM: "Střední" STR_LARGE: "Velký" -STR_X_LARGE: "Obří" +STR_X_LARGE: "Extra velký" STR_TIGHT: "Těsný" STR_NORMAL: "Normální" STR_WIDE: "Široký" @@ -189,9 +209,9 @@ STR_PAGES_30: "30 stránek" STR_UPDATE: "Aktualizace" STR_CHECKING_UPDATE: "Kontrola aktualizací…" STR_NEW_UPDATE: "Nová aktualizace k dispozici!" -STR_CURRENT_VERSION: "Aktuální verze:" -STR_NEW_VERSION: "Nová verze:" -STR_UPDATING: "Aktualizace..." +STR_CURRENT_VERSION: "Aktuální verze: " +STR_NEW_VERSION: "Nová verze: " +STR_UPDATING: "Aktualizuje se..." STR_NO_UPDATE: "Žádná aktualizace k dispozici" STR_UPDATE_FAILED: "Aktualizace selhala" STR_UPDATE_COMPLETE: "Aktualizace dokončena" @@ -204,8 +224,8 @@ STR_UNNAMED: "Nepojmenované" STR_NO_SERVER_URL: "Není nakonfigurována adresa URL serveru" STR_FETCH_FEED_FAILED: "Načtení kanálu se nezdařilo" STR_PARSE_FEED_FAILED: "Analyzování kanálu se nezdařilo" -STR_NETWORK_PREFIX: "Síť:" -STR_IP_ADDRESS_PREFIX: "IP adresa:" +STR_NETWORK_PREFIX: "Síť: " +STR_IP_ADDRESS_PREFIX: "IP adresa: " STR_ERROR_GENERAL_FAILURE: "Chyba: Obecná chyba" STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Síť nenalezena" STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit připojení" @@ -234,8 +254,8 @@ STR_DIR_DOWN: "Dolů" STR_OK_BUTTON: "OK" STR_SLEEP_COVER_FILTER: "Filtr obrazovky spánku" STR_FILTER_CONTRAST: "Kontrast" -STR_UI_THEME: "Šablona rozhraní" -STR_THEME_CLASSIC: "Klasická" +STR_UI_THEME: "Motiv rozhraní" +STR_THEME_CLASSIC: "Klasický" STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci" @@ -256,7 +276,7 @@ STR_NO_RECENT_BOOKS: "Žádné nedávné knihy" STR_CALIBRE_DESC: "Používat přenosy bezdrátových zařízení Calibre" STR_FORGET_AND_REMOVE: "Zapomenout síť a odstranit uložené heslo?" STR_FORGET_BUTTON: "Zapomenout" -STR_CALIBRE_STARTING: "Spuštění Calibre..." +STR_CALIBRE_STARTING: "Spouštění Calibre..." STR_CALIBRE_SETUP: "Nastavení" STR_CALIBRE_STATUS: "Stav" STR_CLEAR_BUTTON: "Vymazat" @@ -272,24 +292,24 @@ STR_HW_LEFT_LABEL: "Vlevo (3. tlačítko)" STR_HW_RIGHT_LABEL: "Vpravo (4. tlačítko)" STR_GO_TO_PERCENT: "Přejít na %" STR_GO_HOME_BUTTON: "Přejít Domů" -STR_SYNC_PROGRESS: "Průběh synchronizace" +STR_SYNC_PROGRESS: "Synchronizovat průběh" STR_DELETE_CACHE: "Smazat mezipaměť knihy" STR_BOOK_CACHE_DELETED: "Mezipaměť knihy smazána" STR_DELETE_BOOK_STATS: "Smazat statistiky čtení knihy" -STR_RESET_BOOK_READER_SETTINGS: "Reset Book Reader Settings" -STR_BOOK_READER_SETTINGS_RESET: "Book Reader Settings Reset" +STR_RESET_BOOK_READER_SETTINGS: "Resetovat nastavení čtečky pro knihu" +STR_BOOK_READER_SETTINGS_RESET: "Nastavení čtečky pro knihu resetováno" STR_RESET_READING_PACE: "Resetovat tempo čtení" STR_READING_PACE_RESET: "Tempo čtení resetováno" STR_DELETE: "Smazat" -STR_CHAPTER_PREFIX: "Kapitola:" -STR_PAGES_SEPARATOR: "stránek |" -STR_BOOK_PREFIX: "Kniha:" +STR_CHAPTER_PREFIX: "Kapitola: " +STR_PAGES_SEPARATOR: " stránek | " +STR_BOOK_PREFIX: "Kniha: " STR_CALIBRE_URL_HINT: "Pro Calibre přidejte /opds do URL adresy" -STR_SYNCING_TIME: "Čas synchronizace..." +STR_SYNCING_TIME: "Synchronizace času..." STR_CALC_HASH: "Výpočet hashe dokumentu..." STR_HASH_FAILED: "Nepodařilo se vypočítat hash dokumentu" STR_FETCH_PROGRESS: "Načítání vzdáleného průběhu..." -STR_UPLOAD_PROGRESS: "Průběh nahrávání..." +STR_UPLOAD_PROGRESS: "Nahrávání průběhu..." STR_NO_CREDENTIALS_MSG: "Přihlašovací údaje nejsou nakonfigurovány" STR_KOREADER_SETUP_HINT: "Nastavit účet KOReader v Nastavení" STR_PROGRESS_FOUND: "Nalezen průběh!" @@ -298,14 +318,15 @@ STR_LOCAL_LABEL: "Lokální:" STR_PAGE_OVERALL_FORMAT: "Stránka %d, celkově %.2f%%" STR_PAGE_TOTAL_OVERALL_FORMAT: "Stránka %d/%d, celkově %.2f%%" STR_DEVICE_FROM_FORMAT: " Od: %s" -STR_APPLY_REMOTE: "Použít vzdálený postup" -STR_UPLOAD_LOCAL: "Nahrát lokální postup" -STR_NO_REMOTE_MSG: "Nenalezen žádný vzdálený postup" +STR_APPLY_REMOTE: "Použít vzdálený průběh" +STR_UPLOAD_LOCAL: "Nahrát lokální průběh" +STR_NO_REMOTE_MSG: "Nenalezen žádný vzdálený průběh" STR_UPLOAD_PROMPT: "Nahrát aktuální pozici?" -STR_UPLOAD_SUCCESS: "Postup nahrán!" +STR_UPLOAD_SUCCESS: "Průběh nahrán!" +STR_ALREADY_SYNCED: "Již synchronizováno" STR_SYNC_FAILED_MSG: "Synchronizace se nezdařila" -STR_SAVE_PROGRESS_FAILED: "Nepodařilo se uložit pokrok" -STR_SECTION_PREFIX: "Sekce" +STR_SAVE_PROGRESS_FAILED: "Nepodařilo se uložit průběh" +STR_SECTION_PREFIX: "Sekce " STR_UPLOAD: "Nahrát" STR_BOOK_S_STYLE: "Styl knihy" STR_EMBEDDED_STYLE: "Vložený styl" @@ -374,11 +395,10 @@ STR_CLOCK_UTC_OFFSET: "Posun hodin UTC" STR_CUSTOMISE_STATUS_BAR: "Přizpůsobit stavový řádek" STR_DISABLED: "Zakázáno" STR_DISPLAY_SLEEP_SCREEN: "Obrazovka spánku" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_REFRESH: "Obnovit obrazovku" STR_FRONT_BUTTONS: "Přední tlačítka" STR_HIDE: "Skrýt" @@ -391,7 +411,7 @@ STR_ITTY_BITTY: "Itty Bitty" STR_LEXEND_DECA: "Lexend Deca" STR_LONG_PRESS_ACTION: "Akce dlouhého stisku" STR_MANAGE_FONTS: "Správa písem" -STR_MARK_FINISHED: "Označit jako dokončené" +STR_MARK_FINISHED: "Označit jako dočtené" STR_POWER_BUTTON: "Tlačítko napájení" STR_PROGRESS_BAR: "Ukazatel průběhu" STR_PROGRESS_BAR_MEDIUM: "Střední" @@ -409,7 +429,7 @@ STR_RENDER_MODE_BALANCED: "Vyvážený" STR_RENDER_MODE_LIGHT: "Lehký" STR_BALANCED_MODE: "Vyvážený režim" STR_LIGHT_MODE: "Lehký režim" -STR_SAFE_MODE: "Safe Mode" +STR_SAFE_MODE: "Nouzový režim" STR_SD_FIRMWARE_UPDATE: "Aktualizace firmwaru SD karty" STR_SD_FONT_SIZE_RANGE: "Stáhnout rozsah velikostí písma" STR_SHOW_HIDDEN_FILES: "Zobrazit skryté soubory" @@ -431,7 +451,7 @@ STR_THEME_LYRA_CAROUSEL: "Lyra Carousel" STR_THEME_MINIMAL: "Minimal" STR_TINY: "Maličký" STR_TITLE: "Název" -STR_TOP: "Nahoru" +STR_TOP: "Nahoře" STR_XTC_STATUS_BAR: "Stavový řádek XTC" STR_NEARBY_STATS_VERSION_MISMATCH: "Neshoda verzí. Statistiky nebyly synchronizovány" @@ -459,3 +479,199 @@ STR_NEARBY_POSITION_NO_READER: "Nenalezena žádná blízká čtečka" STR_NEARBY_LABEL: "Blízko:" STR_APPLY_NEARBY_POSITION: "Použít blízkou pozici" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Blízká synchronizace pozice není v simulátoru dostupná" +STR_ADD_HIDDEN_NETWORK: "Přidat skrytou síť..." +STR_ENTER_WIFI_SSID: "Zadejte název sítě (SSID)" +STR_DISPLAY_THEME_HOME: "Motiv a domovská obrazovka" +STR_DISPLAY_REFRESH: "Obnovování" +STR_DISPLAY_STATUS: "Stav" +STR_EOB_HOME: "Domů" +STR_EOB_CONTINUE_WITH: "Pokračovat s" +STR_LONG_PRESS_MENU: "Dlouhý stisk Menu" +STR_SYSTEM_NETWORK_SYNC: "Síť a synchronizace" +STR_SYSTEM_UPDATES: "Aktualizace" +STR_ATKINSON_HL: "Atkinson Hyperlegible" +STR_COMPACT: "Kompaktní" +STR_UPDATE_HASH_MISMATCH: "Soubor aktualizace neodpovídá" +STR_RESTARTING_HINT: "Restartování... Pokud se zařízení nerestartuje, podržte tlačítko napájení několik sekund." +STR_OPDS_FEED_BUFFER_MEMORY_ERROR: "Nepodařilo se alokovat paměť pro buffer" +STR_EPUB_CHAPTER_INCOMPLETE_TITLE: "Neúplná kapitola" +STR_EPUB_CHAPTER_INCOMPLETE_BODY: "Tato kapitola obsahuje poškozené značky EPUB. CrossInk se zastavil na nečitelné části, část obsahu proto může chybět." +STR_NEXT_PAGE: "Další stránka »" +STR_PREV_PAGE: "« Předchozí stránka" +STR_SELECTED: "Vybráno" +STR_SHOW: "Zobrazit" +STR_STABLE_PAGE_NUMBERS: "Stabilní čísla stránek" +STR_EXAMPLE_CHAPTER: "Kapitola 21" +STR_EXAMPLE_BOOK: "Název knihy" +STR_PREVIEW: "Náhled" +STR_HIDE_CLOCK: "Skrýt hodiny" +STR_CURRENT_TIME: "Aktuální čas:" +STR_NEXT_FIELD: "Další" +STR_CLOCK_SYNC: "Synchronizace hodin" +STR_CLOCK_SYNCING: "Synchronizace z NTP..." +STR_CLOCK_SYNC_OK: "Hodiny synchronizovány" +STR_CLOCK_SYNC_FAIL: "Synchronizace selhala" +STR_CLOCK_SYNC_NO_WIFI: "WiFi není připojena" +STR_CLOCK_SYNC_NO_WIFI_HINT: "Nejprve se připojte k WiFi a zkuste to znovu." +STR_THEME_DASHBOARD: "Dashboard" +STR_THEME_MINIMAL_STATS: "Minimal Stats" +STR_SEARCH: "Hledat" +STR_RECENTS: "Nedávné" +STR_SAVE_CLIPPING: "Vytvořit výstřižek" +STR_VIEW_CLIPPINGS: "Zobrazit výstřižky" +STR_CLIPPINGS: "Výstřižky" +STR_BOOKMARKS_AND_CLIPPINGS: "Záložky a výstřižky" +STR_NO_CLIPPINGS: "Zatím žádné výstřižky" +STR_DELETE_CLIPPINGS: "Vymazat výstřižky" +STR_CLIPPING_SAVED: "Výstřižek uložen" +STR_CLIPPING_FAILED: "Výstřižek se nepodařilo uložit" +STR_CLIPPING_LIMIT_REACHED: "Byl dosažen limit výstřižků" +STR_BOOK_STATS_DELETED: "Statistiky čtení smazány" +STR_PIN_AS_FAVORITE: "Připnout jako oblíbené" +STR_UNPIN_AS_FAVORITE: "Odepnout z oblíbených" +STR_PIN_PNG_WARNING: "Připnutí PNG je kompatibilní pouze s režimem překrytí stránky" +STR_SET_AS_SLEEP_FOLDER: "Nastavit jako složku spánku" +STR_USE_DEFAULT_SLEEP_FOLDERS: "Použít výchozí složky spánku" +STR_DISPLAY_QR: "Zobrazit stránku jako QR" +STR_KOREADER_SYNC_AUTH_REJECTED: "Synchronizační server odmítl uživatelské jméno nebo heslo" +STR_KOREADER_SYNC_NETWORK_ERROR: "Synchronizační server je nedostupný. Zkontrolujte WiFi a URL serveru." +STR_KOREADER_SYNC_NETWORK_REFUSED: "Synchronizační server odmítl připojení. Zkontrolujte URL serveru a zda server běží." +STR_KOREADER_SYNC_NETWORK_TIMEOUT: "Vypršel časový limit serveru. Zkontrolujte sílu WiFi signálu a zkuste to znovu." +STR_KOREADER_SYNC_NETWORK_TLS: "Nepodařilo se navázat zabezpečené připojení. Zkontrolujte URL serveru a nastavení HTTPS." +STR_KOREADER_SYNC_HTTP_404: "Synchronizační endpoint nenalezen. Zkontrolujte, že URL míří na váš KOReader sync server." +STR_KOREADER_SYNC_HTTP_STATUS_FORMAT: "Synchronizační server vrátil HTTP %d. Zkuste to později." +STR_KOREADER_SYNC_SERVER_ERROR: "Synchronizační server vrátil neočekávanou odpověď. Zkuste to později." +STR_KOREADER_SYNC_BAD_RESPONSE: "Synchronizační server odpověděl neplatnými daty. Zkontrolujte URL serveru nebo nastavení proxy." +STR_KOREADER_SYNC_LOW_MEMORY: "Nedostatek volné paměti pro dokončení synchronizace. Zkuste to znovu." +STR_UNKNOWN_ERROR: "Neznámá chyba" +STR_SET_SLEEP_COVER: "Nastavit obálku" +STR_FOOTNOTES: "Poznámky pod čarou" +STR_NO_FOOTNOTES: "Na této stránce nejsou žádné poznámky" +STR_LINK: "[odkaz]" +STR_AUTO_TURN_ENABLED: "Automatické otáčení stránek zapnuto: " +STR_EDIT: "Upravit" +STR_MORE: "Více" +STR_TRACK_READING_STATS: "Sledovat statistiky čtení" +STR_MARK_UNFINISHED: "Označit jako nedočtené" +STR_MARK_FINISHED_PROMPT_TITLE: "Označit jako dočtené?" +STR_MARK_FINISHED_PROMPT_BODY: "Dosáhli jste 99 % této knihy" +STR_ADD_BOOKMARK: "Přidat záložku" +STR_REMOVE_BOOKMARK: "Odebrat záložku" +STR_VIEW_BOOKMARKS: "Zobrazit záložky" +STR_BOOKMARK_ADDED: "Záložka přidána" +STR_BOOKMARK_REMOVED: "Záložka odebrána" +STR_NO_BOOKMARKS: "Zatím žádné záložky" +STR_DELETE_BOOKMARK: "Smazat" +STR_BOOKMARK_PAGE_FORMAT: "Stránka %d" +STR_UNKNOWN_CHAPTER: "Neznámá kapitola" +STR_STATS_SESSIONS: "Sezení: " +STR_STATS_TOTAL_TIME: "Celkový čas: " +STR_STATS_AVG_SESSION: "Prům. sezení: " +STR_STATS_LESS_THAN_MIN: "< 1 min" +STR_STATS_THIS_BOOK: "Tato kniha" +STR_STATS_AVERAGES: "Průměry" +STR_STATS_PAGES_PER_MIN: "Stránek/min" +STR_STATS_COMPLETED_LBL: "Přečtené knihy" +STR_STATS_SESSIONS_LBL: "Sezení" +STR_STATS_TIME_LBL: "Čas čtení" +STR_STATS_PAGES_LBL: "Otočené stránky" +STR_STATS_PROGRESS_LBL: "Průběh" +STR_STATS_AVG_SESSION_LBL: "Prům. sezení" +STR_STATS_DAILY_AVG_LBL: "Denní průměr" +STR_STATS_READING_STREAK_LBL: "Série čtení" +STR_STATS_LONGEST_STREAK_LBL: "Nejdelší série" +STR_STATS_ALL_TIME: "Všechny knihy" +STR_STATS_THIS_DEVICE: "Všechny knihy (toto zařízení)" +STR_STATS_ALL_DEVICES: "Všechny knihy (všechna zařízení)" +STR_STATS_THIS_DEVICE_SCREEN: "Statistiky čtení - toto zařízení" +STR_STATS_ALL_DEVICES_SCREEN: "Statistiky čtení - všechna synchronizovaná zařízení" +STR_STATS_TIME_OF_DAY: "Denní doba" +STR_STATS_DAY_OF_WEEK: "Den v týdnu" +STR_STATS_MORNING: "Ráno" +STR_STATS_AFTERNOON: "Odpoledne" +STR_STATS_EVENING: "Večer" +STR_STATS_NIGHT: "Noc" +STR_STATS_MON: "Po" +STR_STATS_TUE: "Út" +STR_STATS_WED: "St" +STR_STATS_THU: "Čt" +STR_STATS_FRI: "Pá" +STR_STATS_SAT: "So" +STR_STATS_SUN: "Ne" +STR_STATS_START_DATE: "Datum zahájení" +STR_STATS_FINISHED_DATE: "Datum dočtení" +STR_STATS_EST_FINISH_DATE: "Odhad dočtení" +STR_STATS_STARTED: "Zahájeno" +STR_STATS_DAY: "den" +STR_STATS_DAYS: "dní" +STR_ADD_SERVER: "Přidat server" +STR_SERVER_NAME: "Název serveru" +STR_NO_SERVERS: "Žádné servery OPDS nejsou nakonfigurovány" +STR_DELETE_SERVER: "Smazat server" +STR_DELETE_CONFIRM: "Smazat tento server?" +STR_OPDS_DOWNLOAD_FOLDER: "Složka pro stahování" +STR_OPDS_SD_ROOT: "Kořen SD karty" +STR_MOVE_TO_READ_FAILED_TITLE: "Knihu se nepodařilo přesunout" +STR_MOVE_TO_READ_FAILED_BODY: "Nepodařilo se přesunout \"%s\" do složky /Read." +STR_DOWNLOAD_FONTS: "Stáhnout písma" +STR_FONT_DOWNLOAD: "Stažení písem" +STR_FONT_BROWSER: "Prohlížeč písem" +STR_LOADING_FONT_LIST: "Načítání seznamu písem..." +STR_NO_FONTS_AVAILABLE: "Žádná písma k dispozici" +STR_FONT_INSTALLED: "Písmo nainstalováno!" +STR_FONT_INSTALL_FAILED: "Instalace písma selhala" +STR_FONT_DOWNLOAD_INTERRUPTED: "Síť byla během stahování přerušena" +STR_FONT_DOWNLOAD_RETRY_HINT: "Opakování naváže na uložený průběh." +STR_FONT_DOWNLOAD_CHECKSUM_HINT: "Opakování stáhne tento soubor znovu." +STR_INSTALLED: "Nainstalováno" +STR_CONFIRM_DOWNLOAD_PROMPT: "Stáhnout?" +STR_SD_CARD_FULL: "Nedostatek místa na SD kartě" +STR_FILES_LABEL: "Soubory: " +STR_SIZE_LABEL: "Velikost: " +STR_REDOWNLOAD: "Stáhnout znovu" +STR_DOWNLOAD_ALL: "Stáhnout vše" +STR_UPDATE_ALL: "Aktualizovat vše" +STR_ALL_FONTS_INSTALLED: "Všechna písma nainstalována!" +STR_UPDATE_AVAILABLE: "Aktualizovat" +STR_CRASH_TITLE: "Pád systému" +STR_CRASH_DESCRIPTION: "Podrobná zpráva byla uložena do crash_report.txt. Přiložte prosím tento soubor k hlášení chyby." +STR_CRASH_REASON: "Důvod pádu:" +STR_CRASH_NO_REASON: "(Důvod nebyl zaznamenán)" +STR_KB_HINT_MOVE_CURSOR: "Stiskem VLEVO/VPRAVO posunete kurzor" +STR_KB_HINT_RETURN_CURSOR: "Stiskem VLEVO se vrátíte na pozici kurzoru" +STR_KB_HINT_HIDE_PASSWORD: "Podržte VPRAVO a stiskněte [***] pro skrytí hesla" +STR_KB_HINT_SHOW_PASSWORD: "Podržte VPRAVO a stiskněte [abc] pro zobrazení hesla" +STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Stiskem [***] skryjete heslo" +STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Stiskem [abc] zobrazíte heslo" +STR_KB_HINT_EDIT_ENTRY: "Podržením NAHORU upravíte položku" +STR_KB_TIPS: "Tipy:" +STR_KB_HINT_RETURN_KEYBOARD: "Stiskem DOLŮ se vrátíte na klávesnici" +STR_KB_HINT_EXIT_URL_MODE: "Stiskem ABC opustíte režim URL" +STR_KB_HINT_CLEAR_TEXT: "Podržením DEL smažete celý text" +STR_KB_HINT_SECONDARY_CHAR: "Podržte VYBRAT pro sekundární znak" +STR_KB_HINT_UPPER_SECONDARY: "Podržte VYBRAT pro VELKÁ písmena nebo sekundární znak" +STR_KB_HINT_LOWER_SECONDARY: "Podržte VYBRAT pro malá písmena nebo sekundární znak" +STR_KB_HINT_URL_SNIPPETS: "Stiskem URL vložíte úryvky" +STR_OTHER: "Ostatní" +STR_TILT_TO_TURN_OFF: "Otáčení nakloněním: VYP" +STR_TILT_TO_TURN_ON: "Otáčení nakloněním: ZAP" +STR_EPUB_LAYOUT_MEMORY_TITLE: "Kapitola vyžaduje příliš mnoho paměti" +STR_EPUB_LAYOUT_MEMORY_BODY: "Sazba této kapitoly vyžaduje více paměti, než může toto zařízení bezpečně uvolnit." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_BOTH: "Zkuste to znovu s vypnutým soustředěným čtením a vodicími tečkami." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_BIONIC: "Zkuste to znovu s vypnutým soustředěným čtením." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_GUIDE: "Zkuste to znovu s vypnutými vodicími tečkami." +STR_EPUB_FALLBACK_FONT_TITLE: "Kapitola používá vestavěné písmo" +STR_EPUB_FALLBACK_FONT_BODY: "Dostupné paměti bylo příliš málo pro sazbu kapitoly písmem z SD karty, proto bylo použito vestavěné písmo." +STR_SELECT_FIRMWARE_FILE: "Vyberte soubor firmwaru (.bin)" +STR_NO_BIN_FILES: "Nenalezeny žádné soubory .bin" +STR_VALIDATING_FIRMWARE: "Ověřování firmwaru..." +STR_INVALID_FIRMWARE: "Neplatný soubor firmwaru" +STR_FIRMWARE_TOO_LARGE: "Firmware je příliš velký pro oddíl" +STR_FIRMWARE_TOO_SMALL: "Soubor firmwaru je příliš malý" +STR_FIRMWARE_UPDATE_PROMPT: "Aktualizovat firmware?" +STR_FIRMWARE_FILE_OPEN_FAILED: "Soubor nelze otevřít" +STR_FIRMWARE_WRITE_FAILED: "Zápis firmwaru selhal" +STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínejte zařízení!" +STR_RECOVERY_MODE: "Režim obnovení" +STR_RECOVERY_MODE_HINT: "Umístěte firmware.bin do kořene SD karty a vyberte jej" +STR_NEARBY_TRANSFER_VALIDATING: "Ověřování..." diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index b3bdbce802..ff0d452941 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Gem adgangskode til næste gang?" STR_PRESS_OK_SCAN: "Tryk OK for at scanne igen" STR_JOIN_NETWORK: "Tilslut netværk" STR_CREATE_HOTSPOT: "Opret Hotspot" -STR_NEARBY_STATS_SYNC: "Nærliggende statistik-sync" -STR_NEARBY_STATS_SYNC_DESC: "Synkroniser CrossInk-læsestatistik med en nærliggende X3/X4" +STR_NEARBY_DEVICE: "Enhed i nærheden" +STR_NEARBY_STATS_SYNC: "Synkroniser statistik" +STR_RECEIVE_NEARBY_BOOK: "Modtag fil" +STR_RECEIVE_NEARBY_BOOK_DESC: "Modtag en fil fra en anden læser" +STR_NEARBY_STATS_SYNC_DESC: "Synkroniser læsestatistik mellem læsere" STR_JOIN_DESC: "Opret forbindelse til et eksisterende WiFi-netværk" STR_HOTSPOT_DESC: "Opret et WiFi-netværk andre kan tilslutte sig" STR_STARTING_HOTSPOT: "Starter Hotspot..." @@ -92,13 +95,20 @@ STR_NAV_BUTTONS: "Navigationsknapper" STR_ALL_BUTTONS: "Alle knapper" STR_LONG_PRESS_SKIP: "Langt tryk for at springe kapitel over" STR_LONG_PRESS_BEHAVIOR: "Adfærd ved langt tryk" +STR_TOUCH_READER_CONTROLS: "Touchkontroller i læser" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientér forreste knapper" STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado" STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambio de orientación" STR_FONT_PREVIEW_TEXT: "Høj bly gom vandt fræk sexquiz på wc" STR_FONT_FAMILY: "Læser skrifttype" +STR_DICTIONARY_FONT: "Ordbogsskrifttype" +STR_USE_READER_FONT: "Brug læserens skrifttype" +STR_DICTIONARY_FONT_SIZE: "Ordbogsskriftstørrelse" +STR_USE_READER_FONT_SIZE: "Brug læserens skriftstørrelse" STR_FONT_SIZE: "Læser skriftstørrelse" STR_LINE_SPACING: "Linjeafstand" +STR_WORD_SPACING: "Ordafstand" STR_SCREEN_MARGIN: "Skærmmargen" STR_PUBLISHER_PAGE_NUMBERS: "Forlagets sidetal" STR_PARA_ALIGNMENT: "Afsnitsjustering" @@ -131,6 +141,7 @@ STR_USERNAME: "Brugernavn" STR_PASSWORD: "Adgangskode" STR_SYNC_SERVER_URL: "Synkroniseringsserver-URL" STR_DOCUMENT_MATCHING: "Dokumentsammenkobling" +STR_SEND_METADATA: "Send dokumentmetadata" STR_AUTHENTICATE: "Godkend" STR_KOREADER_USERNAME: "KOReader brugernavn" STR_KOREADER_PASSWORD: "KOReader adgangskode" @@ -391,11 +402,10 @@ STR_CLOCK_SYNC_NOW: "Synkroniser dato/tid nu" STR_CLOCK_UTC_OFFSET: "Ur UTC offset" STR_DISABLED: "Deaktiveret" STR_DISPLAY_SLEEP_SCREEN: "Søvnskærm" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Gennemtvinge afsnitsindrykninger" STR_FORCE_REFRESH: "Opdater skærmen" STR_FRONT_BUTTONS: "Forreste knapper" @@ -466,3 +476,6 @@ STR_NEARBY_POSITION_NO_READER: "Ingen nærliggende læser fundet" STR_NEARBY_LABEL: "Nærliggende:" STR_APPLY_NEARBY_POSITION: "Anvend nærliggende position" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Nærliggende positions-sync er ikke tilgængelig i simulatoren" +STR_ADD_HIDDEN_NETWORK: "Tilføj skjult netværk..." +STR_ENTER_WIFI_SSID: "Indtast netværksnavn (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Validerer..." diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index a2938b2512..c250e65bc7 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Wachtwoord opslaan voor volgende keer?" STR_PRESS_OK_SCAN: "Druk op OK om opnieuw te scannen" STR_JOIN_NETWORK: "Verbind met netwerk" STR_CREATE_HOTSPOT: "Hotspot maken" -STR_NEARBY_STATS_SYNC: "Dichtbij stats syncen" -STR_NEARBY_STATS_SYNC_DESC: "Synchroniseer CrossInk-leesstatistieken met een X3/X4 dichtbij" +STR_NEARBY_DEVICE: "Apparaat in de buurt" +STR_NEARBY_STATS_SYNC: "Statistieken synchroniseren" +STR_RECEIVE_NEARBY_BOOK: "Bestand ontvangen" +STR_RECEIVE_NEARBY_BOOK_DESC: "Ontvang een bestand van een andere e-reader" +STR_NEARBY_STATS_SYNC_DESC: "Synchroniseer leesstatistieken tussen e-readers" STR_JOIN_DESC: "Verbind met een bestaand wifi-netwerk" STR_HOTSPOT_DESC: "Maak een wifi-netwerk waar anderen mee kunnen verbinden" STR_STARTING_HOTSPOT: "Hotspot starten..." @@ -91,14 +94,20 @@ STR_ORIENTATION_AWARE: "Oriëntatiebewust" STR_NAV_BUTTONS: "Navigatieknoppen" STR_ALL_BUTTONS: "Alle knoppen" STR_LONG_PRESS_BEHAVIOR: "Gedrag bij lang indrukken" +STR_TOUCH_READER_CONTROLS: "Aanraakbediening lezer" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Richt voorste knoppen" STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change" STR_FONT_PREVIEW_TEXT: "Pa's wijze lynx bezag vroom het fikse aquaduct" STR_FONT_FAMILY: "Lettertype lezer" +STR_DICTIONARY_FONT: "Woordenboeklettertype" +STR_USE_READER_FONT: "Lettertype van lezer gebruiken" +STR_DICTIONARY_FONT_SIZE: "Woordenboeklettergrootte" +STR_USE_READER_FONT_SIZE: "Lettergrootte van lezer gebruiken" STR_FONT_SIZE: "Lettergrootte lezer" STR_LINE_SPACING: "Regelafstand lezer" +STR_WORD_SPACING: "Woordafstand" STR_SCREEN_MARGIN: "Schermmarge lezer" STR_PUBLISHER_PAGE_NUMBERS: "Paginanummers uitgever" STR_PARA_ALIGNMENT: "Uitlijning alinea lezer" @@ -131,6 +140,7 @@ STR_USERNAME: "Gebruikersnaam" STR_PASSWORD: "Wachtwoord" STR_SYNC_SERVER_URL: "Sync-server URL" STR_DOCUMENT_MATCHING: "Documentkoppeling" +STR_SEND_METADATA: "Documentmetadata versturen" STR_AUTHENTICATE: "Authenticatie" STR_KOREADER_USERNAME: "KOReader gebruikersnaam" STR_KOREADER_PASSWORD: "KOReader wachtwoord" @@ -391,11 +401,10 @@ STR_CLOCK_SYNC_NOW: "Synchroniseer datum/tijd nu" STR_CLOCK_UTC_OFFSET: "Klok UTC-offset" STR_DISABLED: "Uitgeschakeld" STR_DISPLAY_SLEEP_SCREEN: "Slaapscherm" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Forceer alinea-inspringingen" STR_FORCE_REFRESH: "Scherm vernieuwen" STR_FRONT_BUTTONS: "Knopen aan de voorkant" @@ -466,3 +475,6 @@ STR_NEARBY_POSITION_NO_READER: "Geen reader dichtbij gevonden" STR_NEARBY_LABEL: "Dichtbij:" STR_APPLY_NEARBY_POSITION: "Dichtbij positie toepassen" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Dichtbij positie syncen is niet beschikbaar in simulator" +STR_ADD_HIDDEN_NETWORK: "Verborgen netwerk toevoegen..." +STR_ENTER_WIFI_SSID: "Voer netwerknaam in (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Valideren..." diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 471ff690e1..1b5c1eb4f3 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -22,6 +22,9 @@ STR_NO_CHAPTERS: "No chapters" STR_END_OF_BOOK: "End of book" STR_EMPTY_CHAPTER: "Empty chapter" STR_INDEXING: "Indexing" +STR_INDEX_FAILED: "Failed to index - invalid book" +STR_EPUB_OPEN_FAILED_BODY: "CrossInk could not read this EPUB. The file may be invalid or damaged." +STR_EPUB_OPEN_MEMORY_BODY: "CrossInk could not open this book because the device ran out of memory." STR_MEMORY_ERROR: "Memory error" STR_PAGE_LOAD_ERROR: "Page load error" STR_EMPTY_FILE: "Empty file" @@ -32,7 +35,10 @@ STR_WIFI_NETWORKS: "WiFi Networks" STR_NO_NETWORKS: "No networks found" STR_NETWORKS_FOUND: "%zu networks found" STR_SCANNING: "Scanning..." +STR_FINDING_SAVED_WIFI: "Finding saved Wi-Fi..." STR_CONNECTING: "Connecting..." +STR_CONNECTING_SAVED_WIFI: "Connecting to saved Wi-Fi..." +STR_SHOW_NETWORKS: "Show" STR_CONNECTED: "Connected!" STR_CONNECTION_FAILED: "Connection Failed" STR_FORGET_NETWORK: "Forget Network?" @@ -40,11 +46,47 @@ STR_SAVE_PASSWORD: "Save password for next time?" STR_PRESS_OK_SCAN: "Press OK to scan again" STR_JOIN_NETWORK: "Join a Network" STR_CREATE_HOTSPOT: "Create Hotspot" -STR_NEARBY_STATS_SYNC: "Nearby Stats Sync" +STR_NEARBY_DEVICE: "Nearby Device" +STR_NEARBY_STATS_SYNC: "Sync Stats" STR_NEARBY_POSITION_SYNC: "Nearby Position Sync" +STR_NEARBY_BOOK_TRANSFER: "Nearby File Transfer" +STR_SEND_NEARBY_BOOK: "Send to Nearby Device" +STR_RECEIVE_NEARBY_BOOK: "Receive File" +STR_RECEIVE_NEARBY_BOOK_DESC: "Receive a file from another reader" +STR_NEARBY_RECEIVE_FOLDER: "Nearby receive folder" +STR_SELECT_RECEIVE_FOLDER: "Select receive folder" +STR_START_RECEIVING: "Start receiving" +STR_CHANGE_FOLDER: "Change folder" +STR_ACCEPT: "Accept" +STR_OK: "OK" +STR_REPLACE: "Replace" +STR_KEEP_BOTH: "Keep both" +STR_NEARBY_TRANSFER_LISTENING: "Waiting for a nearby sender" +STR_NEARBY_TRANSFER_DISCOVERING: "Looking for receiving devices" +STR_NEARBY_TRANSFER_WAITING_APPROVAL: "Waiting for receiver approval" +STR_NEARBY_TRANSFER_INCOMING: "Incoming file" +STR_NEARBY_TRANSFER_SIZE: "%lu bytes" +STR_NEARBY_TRANSFER_VALIDATING: "Validating..." +STR_NEARBY_TRANSFER_SENDING: "Sending file" +STR_NEARBY_TRANSFER_RECEIVING: "Receiving file" +STR_NEARBY_TRANSFER_SENT: "File sent" +STR_NEARBY_TRANSFER_RECEIVED: "File received" +STR_NEARBY_TRANSFER_FAILED: "Transfer failed" +STR_NEARBY_TRANSFER_UNSUPPORTED: "Unsupported file type" +STR_NEARBY_TRANSFER_SOURCE_FAILED: "Could not read the source file" +STR_NEARBY_TRANSFER_RADIO_FAILED: "Could not start nearby transfer" +STR_NEARBY_TRANSFER_REJECTED: "The receiver declined the transfer" +STR_NEARBY_TRANSFER_VERIFY_FAILED: "The received file did not pass verification" +STR_NEARBY_TRANSFER_CANCELLED: "Transfer cancelled" +STR_NEARBY_TRANSFER_WRITE_FAILED: "Could not write the received file" +STR_NEARBY_TRANSFER_NO_SPACE: "Not enough space on the SD card" +STR_NEARBY_TRANSFER_NAME_FAILED: "Could not choose an available filename" +STR_NEARBY_TRANSFER_REPLACE_FAILED: "Could not safely replace the existing file" +STR_NEARBY_TRANSFER_TIMEOUT: "The other device stopped responding" +STR_NEARBY_TRANSFER_PROGRESS_SAVE_FAILED: "Could not save reading progress" STR_JOIN_DESC: "Connect to an existing WiFi network" STR_HOTSPOT_DESC: "Create a WiFi network others can join" -STR_NEARBY_STATS_SYNC_DESC: "Sync CrossInk reading stats with nearby X3/X4" +STR_NEARBY_STATS_SYNC_DESC: "Sync reading stats between readers" STR_NEARBY_STATS_VERSION_MISMATCH: "Version mismatch. Stats not synced" STR_NEARBY_POSITION_VERSION_MISMATCH: "Version mismatch. Position not synced" STR_NEARBY_POSITION_BOOK_MISMATCH: "Different book. Position not synced" @@ -60,6 +102,8 @@ STR_NETWORK_LEGEND: "* = Encrypted | + = Saved" STR_MAC_ADDRESS: "MAC address:" STR_CHECKING_WIFI: "Checking WiFi..." STR_ENTER_WIFI_PASSWORD: "Enter WiFi Password" +STR_ADD_HIDDEN_NETWORK: "Add hidden network..." +STR_ENTER_WIFI_SSID: "Enter network name (SSID)" STR_TO_PREFIX: "to " STR_CALIBRE_RECEIVING: "Receiving: " STR_CALIBRE_RECEIVED: "Received: " @@ -90,6 +134,10 @@ STR_IMAGES: "Images" STR_IMAGES_DISPLAY: "Display" STR_IMAGES_PLACEHOLDER: "Placeholder" STR_IMAGES_SUPPRESS: "Suppress" +STR_TOUCH_READER_CONTROLS: "Touch Reader Controls" +STR_DISABLE_TOUCHSCREEN: "Disable Touchscreen" +STR_ON: "ON" +STR_OFF: "OFF" STR_EOB_HOME: "Home" STR_EOB_CONTINUE_WITH: "Continue with" STR_SHORT_PWR_BTN: "Short-press Action" @@ -108,6 +156,11 @@ STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter Skip" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation Change" STR_FONT_FAMILY: "Font Family" +STR_DICTIONARY_FONT: "Dictionary Font" +STR_USE_READER_FONT: "Use Reader Font" +STR_DICTIONARY_FONT_SIZE: "Dictionary Font Size" +STR_USE_READER_FONT_SIZE: "Use Reader Size" +STR_UNAVAILABLE: "unavailable" STR_FONT_SIZE: "Font Size" STR_SD_FONT_SIZE_RANGE: "Download Font Size Range" STR_READER_FONT_OPTIONS: "Font Options" @@ -119,16 +172,46 @@ STR_EPUB_RENDER_MODE: "EPUB Render Mode" STR_RENDER_MODE_CROSSINK_DEFAULT: "CrossInk Default" STR_RENDER_MODE_BALANCED: "Balanced" STR_RENDER_MODE_LIGHT: "Light" +STR_INDEXING_METHOD: "Indexing Method" +STR_INDEXING_INCREMENTAL: "Incremental" +STR_INDEXING_FULL_SECTION: "Full Section" STR_BALANCED_MODE: "Balanced Mode" STR_LIGHT_MODE: "Light Mode" STR_SAFE_MODE: "Safe Mode" STR_LINE_SPACING: "Line Spacing" +STR_WORD_SPACING: "Word Spacing" +STR_LEVEL_1: "1" +STR_LEVEL_2: "2" +STR_LEVEL_3: "3" +STR_LEVEL_4: "4" STR_SCREEN_MARGIN: "Screen Margin" STR_PUBLISHER_PAGE_NUMBERS: "Publisher Page Numbers" STR_PARA_ALIGNMENT: "Paragraph Alignment" STR_LONG_PRESS_MENU: "Long-press Menu" STR_FONT_PREVIEW_TEXT: "The quick brown fox jumps over the lazy dog" STR_HYPHENATION: "Hyphenation" +STR_DICTIONARY: "Dictionary" +STR_BOOK_DICTIONARY: "Book Dictionary" +STR_DICT_NONE: "None" +STR_DICT_USE_GLOBAL: "Use Global" +STR_DICT_NONE_FOUND: "No dictionaries found" +STR_DICT_INFO: "Dictionary Info" +STR_DICT_NO_METADATA: "No metadata available" +STR_DICT_VIEW_RAW: "View Raw" +STR_DICT_NO_DICT_SET: "No dictionary set" +STR_DICT_NO_WORD: "No word" +STR_DICT_LOOKING_UP: "Looking up..." +STR_DICT_NOT_FOUND: "Not found" +STR_DICT_READ_FAILED: "Unable to read definition" +STR_DICT_SEARCH_ALT_FORMS: "Search alternate forms?" +STR_DICT_DID_YOU_MEAN: "Did you mean?" +STR_LOOKUP: "Look Up Word" +STR_LOOKUP_SHORT: "Look Up" +STR_DICT_SWITCH: "Switch" +STR_SWITCH_DICTIONARY: "Switch Dictionary" +STR_LOOKUP_HISTORY: "Lookup History" +STR_LOOKUP_HISTORY_EMPTY: "No words looked up yet" +STR_LOOKUP_HISTORY_DELETE_HINT: "Hold to delete" STR_TIME_TO_SLEEP: "Time to Sleep" STR_SHOW_HIDDEN_FILES: "Show Hidden Files" STR_HIDE_FILE_EXTENSION: "Hide File Extension" @@ -162,7 +245,16 @@ STR_USERNAME: "Username" STR_PASSWORD: "Password" STR_SYNC_SERVER_URL: "Sync Server URL" STR_DOCUMENT_MATCHING: "Document Matching" +STR_SEND_METADATA: "Send Document Metadata" +STR_SYNC_BEHAVIOR: "Sync Behavior" +STR_ASK_EVERY_TIME: "Ask every time" +STR_SMART_SYNC: "Smart sync" STR_AUTHENTICATE: "Authenticate" +STR_SIGN_UP: "Sign Up" +STR_CREATING_ACCOUNT: "Creating account..." +STR_ACCOUNT_CREATED: "Account created" +STR_SIGNUP_FAILED: "Sign up failed" +STR_USERNAME_TAKEN: "Username is already registered" STR_KOREADER_USERNAME: "KOReader Username" STR_KOREADER_PASSWORD: "KOReader Password" STR_FILENAME: "Filename" @@ -224,11 +316,10 @@ STR_MEDIUM: "Medium" STR_LARGE: "Large" STR_X_LARGE: "Extra Large" STR_HUGE: "Huge" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_ALL: "all" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" +STR_FONT_RANGE_ALL: "8-20pt" STR_TIGHT: "Tight" STR_NORMAL: "Normal" STR_WIDE: "Wide" @@ -285,22 +376,30 @@ STR_CONNECT: "Connect" STR_OPEN: "Open" STR_DOWNLOAD: "Download" STR_RETRY: "Retry" +STR_TAP_TO_RETRY: "Tap to retry" STR_YES: "Yes" STR_NO: "No" STR_SHOW: "Show" STR_HIDE: "Hide" STR_STATE_ON: "ON" STR_STATE_OFF: "OFF" +STR_FRONTLIGHT: "Frontlight" +STR_BRIGHTNESS: "Brightness" +STR_WARMTH: "Warmth" +STR_RESTORE_LIGHT_ON_WAKE: "Restore Light on Wake" STR_NOT_SET: "Not Set" STR_DIR_LEFT: "Left" STR_DIR_RIGHT: "Right" STR_DIR_UP: "Up" STR_DIR_DOWN: "Down" STR_OK_BUTTON: "OK" +STR_KEY_SHIFT: "Shift" +STR_KEY_MODE_SYMBOLS: "?123" +STR_KEY_MODE_ABC: "abc" STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter" STR_SLEEP_COVER_FILTER_SHORT: "Cover Filter" STR_FILTER_CONTRAST: "Contrast" -STR_CUSTOMISE_STATUS_BAR: "Customise Status Bar" +STR_CUSTOMISE_STATUS_BAR: "Customize Status Bar" STR_CHAPTER_PAGE_COUNT: "Chapter Page Count" STR_STABLE_PAGE_NUMBERS: "Stable Page Numbers" STR_BOOK_PROGRESS_PERCENTAGE: "Book Progress Percentage" @@ -316,6 +415,7 @@ STR_EXAMPLE_BOOK: "Book Title" STR_PREVIEW: "Preview" STR_TITLE: "Title" STR_TIME_LEFT: "Time Left" +STR_TIME_LEFT_SHORT: "Time Left" STR_TIME_LEFT_CALCULATING: "Calculating..." STR_BATTERY: "Battery" STR_XTC_STATUS_BAR: "XTC Status Bar" @@ -327,6 +427,20 @@ STR_CLOCK_UTC_OFFSET: "Clock UTC Offset" STR_CLOCK_FORMAT: "Clock Format" STR_CLOCK_FORMAT_24H: "24-hour" STR_CLOCK_FORMAT_12H: "12-hour" +STR_DATE_FORMAT: "Date Format" +STR_DATE_FORMAT_MONTH_DAY_YEAR_LONG: "Dec 31, 2026" +STR_DATE_FORMAT_DAY_MONTH_YEAR_LONG: "31 Dec 2026" +STR_DATE_FORMAT_MONTH_DAY_YEAR_NUMERIC: "12/31/2026" +STR_DATE_FORMAT_DAY_MONTH_YEAR_NUMERIC: "31/12/2026" +STR_DATE_FORMAT_YEAR_MONTH_DAY_NUMERIC: "2026/12/31" +STR_DATE_FORMAT_MONTH_DAY_NUMERIC: "12/31" +STR_DATE_FORMAT_DAY_MONTH_NUMERIC: "31/12" +STR_DATE_FORMAT_MONTH_DAY_LONG: "December 31" +STR_DATE_FORMAT_DAY_MONTH_LONG: "31 December" +STR_DATE_SEPARATOR: "Date Separator" +STR_DATE_SEPARATOR_PERIOD: "Period (.)" +STR_DATE_SEPARATOR_HYPHEN: "Hyphen (-)" +STR_DATE_SEPARATOR_SLASH: "Slash (/)" STR_CURRENT_TIME: "Current time:" STR_NEXT_FIELD: "Next" STR_CLOCK_SYNC: "Sync Clock" @@ -338,6 +452,7 @@ STR_CLOCK_SYNC_NO_WIFI: "WiFi not connected" STR_CLOCK_SYNC_NO_WIFI_HINT: "Connect to WiFi first, then try again." STR_CLOCK_SYNCED: "Clock Synced" STR_UI_THEME: "UI Theme" +STR_UI_SCALE: "UI Scale" STR_THEME_CLASSIC: "Classic" STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" @@ -428,7 +543,9 @@ STR_UPLOAD_LOCAL: "Upload local progress" STR_NO_REMOTE_MSG: "No remote progress found" STR_UPLOAD_PROMPT: "Upload current position?" STR_UPLOAD_SUCCESS: "Progress uploaded!" +STR_ALREADY_SYNCED: "Already synced" STR_SYNC_FAILED_MSG: "Sync failed" +STR_SYNC_REOPTIMIZE_REQUIRED: "Re-optimize this EPUB before filename sync" STR_KOREADER_SYNC_AUTH_REJECTED: "Username or password was rejected by the sync server" STR_KOREADER_SYNC_NETWORK_ERROR: "Couldn't reach the sync server. Check WiFi and the server URL." STR_KOREADER_SYNC_NETWORK_REFUSED: "Sync server refused the connection. Check the server URL and that the server is online." @@ -477,6 +594,7 @@ STR_BOOKMARK_REMOVED: "Bookmark removed" STR_BOOKMARK_LIMIT_REACHED: "Bookmark limit reached" STR_NO_BOOKMARKS: "No bookmarks yet" STR_DELETE_BOOKMARK: "Delete" +STR_CONFIRM_DELETE_BOOKMARK: "Delete this bookmark?" STR_BOOKMARK_PAGE_FORMAT: "Page %d" STR_UNKNOWN_CHAPTER: "Unknown chapter" STR_STATS_SESSIONS: "Sessions: " @@ -497,6 +615,7 @@ STR_STATS_READING_STREAK_LBL: "Reading Streak" STR_STATS_LONGEST_STREAK_LBL: "Longest Streak" STR_STATS_ALL_TIME: "All Books" STR_STATS_TOTAL_READING_TIME_LBL: "Total Reading Time" +STR_STATS_TOTAL_READING_TIME_LBL_SHORT: "Reading Time" STR_STATS_THIS_DEVICE: "All Books (This Device)" STR_STATS_ALL_DEVICES: "All Books (All Devices)" STR_STATS_THIS_DEVICE_SCREEN: "Reading Stats - This Device" @@ -536,6 +655,8 @@ STR_NO_SERVERS: "No OPDS servers configured" STR_DELETE_SERVER: "Delete Server" STR_DELETE_CONFIRM: "Delete this server?" STR_OPDS_SERVERS: "OPDS Servers" +STR_OPDS_DOWNLOAD_FOLDER: "Download Folder" +STR_OPDS_SD_ROOT: "SD Root" STR_MOVE_TO_READ_FAILED_TITLE: "Could not move book" STR_MOVE_TO_READ_FAILED_BODY: "Failed to move \"%s\" to /Read folder." STR_DOWNLOAD_FONTS: "Download Fonts" @@ -617,6 +738,7 @@ STR_VALIDATING_FIRMWARE: "Validating firmware..." STR_INVALID_FIRMWARE: "Invalid firmware file" STR_FIRMWARE_TOO_LARGE: "Firmware too large for partition" STR_FIRMWARE_TOO_SMALL: "Firmware file is too small" +STR_FIRMWARE_WRONG_DEVICE: "Firmware is for a different device" STR_FIRMWARE_UPDATE_PROMPT: "Update firmware?" STR_FIRMWARE_FILE_OPEN_FAILED: "Cannot open file" STR_FIRMWARE_WRITE_FAILED: "Firmware write failed" diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index 278d0b6d1b..d9086dbdb7 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Tallenna salasana seuraavaa kertaa varten?" STR_PRESS_OK_SCAN: "Paina OK etsiäksesi uudelleen" STR_JOIN_NETWORK: "Liity verkkoon" STR_CREATE_HOTSPOT: "Luo yhteyspiste" -STR_NEARBY_STATS_SYNC: "Lähisynkronointi" -STR_NEARBY_STATS_SYNC_DESC: "Synkronoi CrossInk-lukutilastot lähellä olevan X3/X4:n kanssa" +STR_NEARBY_DEVICE: "Lähellä oleva laite" +STR_NEARBY_STATS_SYNC: "Synkronoi tilastot" +STR_RECEIVE_NEARBY_BOOK: "Vastaanota tiedosto" +STR_RECEIVE_NEARBY_BOOK_DESC: "Vastaanota tiedosto toiselta lukulaitteelta" +STR_NEARBY_STATS_SYNC_DESC: "Synkronoi lukutilastot lukulaitteiden välillä" STR_JOIN_DESC: "Yhdistä olemassa olevaan WiFi-verkkoon" STR_HOTSPOT_DESC: "Luo WiFi-verkko, johon muut voivat liittyä" STR_STARTING_HOTSPOT: "Käynnistetään yhteyspiste..." @@ -88,14 +91,20 @@ STR_NAV_BUTTONS: "Navigointipainikkeet" STR_ALL_BUTTONS: "Kaikki painikkeet" STR_LONG_PRESS_SKIP: "Pitkän painalluksen lukuhyppy" STR_LONG_PRESS_BEHAVIOR: "Pitkän painalluksen toiminta" +STR_TOUCH_READER_CONTROLS: "Lukijan kosketusohjaus" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Suuntaa etupainikkeet" STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change" STR_FONT_PREVIEW_TEXT: "Törkylempijävongahdus" STR_FONT_FAMILY: "Lukijan fonttiperhe" +STR_DICTIONARY_FONT: "Sanakirjan fontti" +STR_USE_READER_FONT: "Käytä lukijan fonttia" +STR_DICTIONARY_FONT_SIZE: "Sanakirjan fonttikoko" +STR_USE_READER_FONT_SIZE: "Käytä lukijan fonttikokoa" STR_FONT_SIZE: "Käyttöliittymän fonttikoko" STR_LINE_SPACING: "Lukijan riviväli" +STR_WORD_SPACING: "Sanaväli" STR_SCREEN_MARGIN: "Lukijan näyttömarginaali" STR_PUBLISHER_PAGE_NUMBERS: "Julkaisijan sivunumerot" STR_PARA_ALIGNMENT: "Lukijan kappaletasaus" @@ -123,6 +132,7 @@ STR_USERNAME: "Käyttäjänimi" STR_PASSWORD: "Salasana" STR_SYNC_SERVER_URL: "Synkronointipalvelimen osoite" STR_DOCUMENT_MATCHING: "Dokumenttien tunnistus" +STR_SEND_METADATA: "Lähetä asiakirjan metatiedot" STR_AUTHENTICATE: "Tunnistaudu" STR_KOREADER_USERNAME: "KOReader-käyttäjänimi" STR_KOREADER_PASSWORD: "KOReader-salasana" @@ -364,11 +374,10 @@ STR_CLOCK_UTC_OFFSET: "Kellon UTC Offset" STR_CUSTOMISE_STATUS_BAR: "Mukauta tilapalkkia" STR_DISABLED: "Ei käytössä" STR_DISPLAY_SLEEP_SCREEN: "Lepotilanäyttö" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Pakota kappaleen sisennykset" STR_FORCE_REFRESH: "Päivitä näyttö" STR_FRONT_BUTTONS: "Edessä painikkeet" @@ -455,3 +464,6 @@ STR_NEARBY_POSITION_NO_READER: "Lähellä olevaa lukijaa ei löytynyt" STR_NEARBY_LABEL: "Lähellä:" STR_APPLY_NEARBY_POSITION: "Käytä lähisijaintia" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Lähisijainnin synkronointi ei ole käytettävissä simulaattorissa" +STR_ADD_HIDDEN_NETWORK: "Lisää piilotettu verkko..." +STR_ENTER_WIFI_SSID: "Anna verkon nimi (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Tarkistetaan..." diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 53c5ca93ca..a0ac195a1e 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Enregistrer le mot de passe ?" STR_PRESS_OK_SCAN: "Appuyez sur OK pour scanner" STR_JOIN_NETWORK: "Rejoindre un réseau" STR_CREATE_HOTSPOT: "Créer un point d’accès" -STR_NEARBY_STATS_SYNC: "Synchro stats proche" -STR_NEARBY_STATS_SYNC_DESC: "Synchroniser les stats de lecture CrossInk avec un X3/X4 proche" +STR_NEARBY_DEVICE: "Appareil à proximité" +STR_NEARBY_STATS_SYNC: "Synchroniser les stats" +STR_RECEIVE_NEARBY_BOOK: "Recevoir un fichier" +STR_RECEIVE_NEARBY_BOOK_DESC: "Recevoir un fichier d’une autre liseuse" +STR_NEARBY_STATS_SYNC_DESC: "Synchroniser les statistiques de lecture entre liseuses" STR_JOIN_DESC: "Se connecter à un WiFi existant" STR_HOTSPOT_DESC: "Créer un WiFi pour d'autres appareils" STR_STARTING_HOTSPOT: "Création du point d’accès…" @@ -92,13 +95,20 @@ STR_NAV_BUTTONS: "Boutons navigation" STR_ALL_BUTTONS: "Tous les boutons" STR_LONG_PRESS_SKIP: "Appui long : saut de chapitre" STR_LONG_PRESS_BEHAVIOR: "Comportement appui long" +STR_TOUCH_READER_CONTROLS: "Commandes tactiles lecteur" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienter boutons avant" STR_LONG_PRESS_BEHAVIOR_OFF: "Désactivé" STR_LONG_PRESS_BEHAVIOR_SKIP: "Saut de chapitre" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Changement d'orientation" STR_FONT_PREVIEW_TEXT: "Portez ce vieux whisky au juge blond qui fume" STR_FONT_FAMILY: "Police de caractères du lecteur" +STR_DICTIONARY_FONT: "Police du dictionnaire" +STR_USE_READER_FONT: "Utiliser la police du lecteur" +STR_DICTIONARY_FONT_SIZE: "Taille de police du dictionnaire" +STR_USE_READER_FONT_SIZE: "Utiliser la taille du lecteur" STR_FONT_SIZE: "Taille texte interface" STR_LINE_SPACING: "Interligne" +STR_WORD_SPACING: "Espacement des mots" STR_SCREEN_MARGIN: "Marges du lecteur" STR_PUBLISHER_PAGE_NUMBERS: "Numéros de page de l'éditeur" STR_PARA_ALIGNMENT: "Alignement du texte" @@ -131,6 +141,7 @@ STR_USERNAME: "Nom d’utilisateur" STR_PASSWORD: "Mot de passe" STR_SYNC_SERVER_URL: "URL du serveur" STR_DOCUMENT_MATCHING: "Correspondance" +STR_SEND_METADATA: "Envoyer métadonnées" STR_AUTHENTICATE: "Connexion" STR_KOREADER_USERNAME: "Utilisateur" STR_KOREADER_PASSWORD: "Mot de passe" @@ -392,11 +403,10 @@ STR_CLOCK_SYNC_NOW: "Synchroniser date/heure maintenant" STR_CLOCK_UTC_OFFSET: "Décalage UTC de l'horloge" STR_DISABLED: "Désactivé" STR_DISPLAY_SLEEP_SCREEN: "Écran de veille" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Forcer le retrait des paragraphes" STR_FORCE_REFRESH: "Actualiser l'écran" STR_FRONT_BUTTONS: "Boutons avant" @@ -466,3 +476,6 @@ STR_NEARBY_POSITION_NO_READER: "Aucune liseuse proche trouvée" STR_NEARBY_LABEL: "Proche :" STR_APPLY_NEARBY_POSITION: "Appliquer la position proche" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "La synchro position proche n’est pas disponible dans le simulateur" +STR_ADD_HIDDEN_NETWORK: "Ajouter un réseau masqué..." +STR_ENTER_WIFI_SSID: "Saisir le nom du réseau (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Validation..." diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 7ebd30bffa..d13c7824e1 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -40,10 +40,13 @@ STR_SAVE_PASSWORD: "Passwort speichern?" STR_PRESS_OK_SCAN: "OK für erneute Suche" STR_JOIN_NETWORK: "Netzwerk beitreten" STR_CREATE_HOTSPOT: "Hotspot erstellen" -STR_NEARBY_STATS_SYNC: "Statistik-Nahsync" +STR_NEARBY_DEVICE: "Gerät in der Nähe" +STR_NEARBY_STATS_SYNC: "Statistiken synchronisieren" +STR_RECEIVE_NEARBY_BOOK: "Datei empfangen" +STR_RECEIVE_NEARBY_BOOK_DESC: "Eine Datei von einem anderen Reader empfangen" STR_JOIN_DESC: "Mit einem bestehenden WLAN-Netzwerk verbinden" STR_HOTSPOT_DESC: "WLAN-Hotspot für andere erstellen" -STR_NEARBY_STATS_SYNC_DESC: "CrossInk-Lesestatistik mit einem nahen X3/X4 synchronisieren" +STR_NEARBY_STATS_SYNC_DESC: "Lesestatistiken zwischen Readern synchronisieren" STR_NEARBY_STATS_VERSION_MISMATCH: "Versionsfehler. Statistik nicht synchronisiert" STR_STARTING_HOTSPOT: "Hotspot wird erstellt …" STR_HOTSPOT_MODE: "Hotspot-Modus" @@ -97,12 +100,18 @@ STR_LONG_PRESS_SKIP: "Langes Drücken überspringt Kap." STR_SIDE_BTN_LONG_PRESS: "Aktion bei langem Druck" STR_CHAPTER_SKIP_OPT: "Kapitel überspringen" STR_CHANGE_FONT_SIZE: "Schriftgröße ändern" +STR_TOUCH_READER_CONTROLS: "Touch-Steuerung beim Lesen" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten" STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck" STR_LONG_PRESS_BEHAVIOR_OFF: "AUS" STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapitel überspringen" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern" STR_FONT_PREVIEW_TEXT: "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich" STR_FONT_FAMILY: "Lese-Schriftfamilie" +STR_DICTIONARY_FONT: "Wörterbuchschriftart" +STR_USE_READER_FONT: "Schriftart des Lesers verwenden" +STR_DICTIONARY_FONT_SIZE: "Wörterbuchschriftgröße" +STR_USE_READER_FONT_SIZE: "Schriftgröße des Lesers verwenden" STR_FONT_SIZE: "Schriftgröße" STR_SD_FONT_SIZE_RANGE: "Schriftgrößenbereich herunterladen" STR_READER_FONT_OPTIONS: "Schriftartoptionen" @@ -118,6 +127,7 @@ STR_BALANCED_MODE: "Ausgewogener Modus" STR_LIGHT_MODE: "Leichter Modus" STR_SAFE_MODE: "Safe Mode" STR_LINE_SPACING: "Lese-Zeilenabstand" +STR_WORD_SPACING: "Wortabstand" STR_SCREEN_MARGIN: "Lese-Seitenränder" STR_PUBLISHER_PAGE_NUMBERS: "Seitenzahlen des Verlags" STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung" @@ -156,6 +166,7 @@ STR_USERNAME: "Benutzername" STR_PASSWORD: "Passwort" STR_SYNC_SERVER_URL: "Sync-Server-URL" STR_DOCUMENT_MATCHING: "Dateizuordnung" +STR_SEND_METADATA: "Metadaten senden" STR_AUTHENTICATE: "Authentifizieren" STR_KOREADER_USERNAME: "KOReader-Benutzername" STR_KOREADER_PASSWORD: "KOReader-Passwort" @@ -218,11 +229,10 @@ STR_MEDIUM: "Mittel" STR_LARGE: "Groß" STR_X_LARGE: "Extragroß" STR_HUGE: "Riesig" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_ALL: "all" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" +STR_FONT_RANGE_ALL: "8-20pt" STR_TIGHT: "Eng" STR_NORMAL: "Normal" STR_WIDE: "Breit" @@ -624,3 +634,6 @@ STR_NEARBY_POSITION_NO_READER: "Kein naher Reader gefunden" STR_NEARBY_LABEL: "In der Nähe:" STR_APPLY_NEARBY_POSITION: "Nahe Position anwenden" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Positions-Nahsync ist im Simulator nicht verfügbar" +STR_ADD_HIDDEN_NETWORK: "Verstecktes Netzwerk hinzufügen..." +STR_ENTER_WIFI_SSID: "Netzwerknamen eingeben (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Wird validiert..." diff --git a/lib/I18n/translations/hebrew.yaml b/lib/I18n/translations/hebrew.yaml index cc953828b4..55ef390fd8 100644 --- a/lib/I18n/translations/hebrew.yaml +++ b/lib/I18n/translations/hebrew.yaml @@ -2,7 +2,7 @@ _language_name: "עברית" _language_code: "HE" _order: "23" -STR_CROSSPOINT: "CrossPoint" +STR_CROSSINK: "CrossInk" STR_BOOTING: "מפעיל..." STR_SLEEPING: "מצב שינה" STR_ENTERING_SLEEP: "עובר למצב שינה" @@ -40,6 +40,11 @@ STR_SAVE_PASSWORD: "לשמור סיסמה לפעם הבאה?" STR_PRESS_OK_SCAN: "לחץ על אישור לסריקה מחדש" STR_JOIN_NETWORK: "הצטרף לרשת" STR_CREATE_HOTSPOT: "צור נקודה חמה" +STR_NEARBY_DEVICE: "מכשיר קרוב" +STR_NEARBY_STATS_SYNC: "סנכרון סטטיסטיקות" +STR_RECEIVE_NEARBY_BOOK: "קבלת קובץ" +STR_RECEIVE_NEARBY_BOOK_DESC: "קבלת קובץ מקורא אחר" +STR_NEARBY_STATS_SYNC_DESC: "סנכרון סטטיסטיקות קריאה בין קוראים" STR_JOIN_DESC: "התחבר לרשת אלחוטית קיימת" STR_HOTSPOT_DESC: "צור רשת אלחוטית שאחרים יוכלו להתחבר אליה" STR_STARTING_HOTSPOT: "מפעיל נקודה חמה..." @@ -85,6 +90,7 @@ STR_IMAGES_SUPPRESS: "הסתר תמונות" STR_SHORT_PWR_BTN: "לחיצה קצרה על כפתור ההפעלה" STR_ORIENTATION: "כיוון קריאה (מסך)" STR_SIDE_BTN_LAYOUT: "פריסת כפתורי צד (בקריאה)" +STR_TOUCH_READER_CONTROLS: "שליטה במגע (קורא)" STR_LONG_PRESS_BEHAVIOR: "פעולת לחיצה ארוכה" STR_LONG_PRESS_BEHAVIOR_OFF: "כבוי" STR_LONG_PRESS_BEHAVIOR_SKIP: "דלג פרק" @@ -92,8 +98,13 @@ STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "שנה כיוון מסך" STR_LONG_PRESS_BACK_ACTION: "פעולת חזרה בלחיצה ארוכה" STR_FONT_PREVIEW_TEXT: "דג סקרן שט בים מאוכזב ולפתע מצא חברה" STR_FONT_FAMILY: "גופן הקריאה" +STR_DICTIONARY_FONT: "גופן מילון" +STR_USE_READER_FONT: "השתמש בגופן הקורא" +STR_DICTIONARY_FONT_SIZE: "גודל גופן המילון" +STR_USE_READER_FONT_SIZE: "השתמש בגודל הקורא" STR_FONT_SIZE: "גודל גופן" STR_LINE_SPACING: "מרווח בין שורות" +STR_WORD_SPACING: "מרווח בין מילים" STR_SCREEN_MARGIN: "שולי המסך בקריאה" STR_PARA_ALIGNMENT: "יישור פסקאות" STR_HYPHENATION: "מיקוף" @@ -119,6 +130,7 @@ STR_USERNAME: "שם משתמש" STR_PASSWORD: "סיסמה" STR_SYNC_SERVER_URL: "כתובת שרת סנכרון" STR_DOCUMENT_MATCHING: "התאמת מסמכים" +STR_SEND_METADATA: "שלח מטא-נתוני מסמך" STR_AUTHENTICATE: "התחבר" STR_KOREADER_USERNAME: "שם משתמש ב-KOReader" STR_KOREADER_PASSWORD: "סיסמת KOReader" @@ -457,3 +469,6 @@ STR_NEARBY_POSITION_NO_READER: "לא נמצא קורא קרוב" STR_NEARBY_LABEL: "קרוב:" STR_APPLY_NEARBY_POSITION: "החל מיקום קרוב" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "סנכרון מיקום קרוב אינו זמין בסימולטור" +STR_ADD_HIDDEN_NETWORK: "הוסף רשת מוסתרת..." +STR_ENTER_WIFI_SSID: "הזן שם רשת (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "מתבצע אימות..." diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index 49a6323503..5e9979a407 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Jelszó mentése legközelebb?" STR_PRESS_OK_SCAN: "Nyomj OK-t az újraellenőrzéshez" STR_JOIN_NETWORK: "Csatlakozás hálózathoz" STR_CREATE_HOTSPOT: "Hotspot létrehozása" -STR_NEARBY_STATS_SYNC: "Közeli statisztika szinkron" -STR_NEARBY_STATS_SYNC_DESC: "CrossInk olvasási statisztikák szinkronizálása egy közeli X3/X4-gyel" +STR_NEARBY_DEVICE: "Közeli eszköz" +STR_NEARBY_STATS_SYNC: "Statisztikák szinkronizálása" +STR_RECEIVE_NEARBY_BOOK: "Fájl fogadása" +STR_RECEIVE_NEARBY_BOOK_DESC: "Fájl fogadása egy másik olvasóról" +STR_NEARBY_STATS_SYNC_DESC: "Olvasási statisztikák szinkronizálása olvasók között" STR_JOIN_DESC: "Csatlakozás meglévő WiFi hálózathoz" STR_HOTSPOT_DESC: "WiFi hálózat létrehozása másoknak" STR_STARTING_HOTSPOT: "Hotspot indítása..." @@ -56,6 +59,8 @@ STR_NETWORK_LEGEND: "* = Titkosított | + = Mentett" STR_MAC_ADDRESS: "MAC-cím:" STR_CHECKING_WIFI: "WiFi ellenőrzése..." STR_ENTER_WIFI_PASSWORD: "Add meg a WiFi jelszót" +STR_ADD_HIDDEN_NETWORK: "Rejtett hálózat hozzáadása..." +STR_ENTER_WIFI_SSID: "Add meg a hálózat nevét (SSID)" STR_TO_PREFIX: "- " STR_CALIBRE_RECEIVING: "Fogadás: " STR_CALIBRE_RECEIVED: "Fogadva: " @@ -90,12 +95,19 @@ STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)" STR_ORIENTATION_AWARE: "Tájolásfüggő" STR_NAV_BUTTONS: "Navigációs gombok" STR_ALL_BUTTONS: "Minden gomb" +STR_TOUCH_READER_CONTROLS: "Érintős olvasóvezérlés" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Elülső gombok tájolása" STR_LONG_PRESS_SKIP: "Hosszú nyomás - fejezet ugrás" STR_LONG_PRESS_BEHAVIOR: "Hosszú nyomás viselkedése" STR_FONT_PREVIEW_TEXT: "Egy hűtlen vejét fülöncsípő, dühös mexikói úr Wesselényinél mázol Quitóban" STR_FONT_FAMILY: "Olvasó betűkészlet" +STR_DICTIONARY_FONT: "Szótár betűtípusa" +STR_USE_READER_FONT: "Olvasó betűtípusának használata" +STR_DICTIONARY_FONT_SIZE: "Szótár betűmérete" +STR_USE_READER_FONT_SIZE: "Olvasó betűméretének használata" STR_FONT_SIZE: "Olvasó betűméret" STR_LINE_SPACING: "Olvasó sorköz" +STR_WORD_SPACING: "Szóköz" STR_SCREEN_MARGIN: "Olvasó margó" STR_PUBLISHER_PAGE_NUMBERS: "Kiadói oldalszámok" STR_PARA_ALIGNMENT: "Olvasó szövegigazítás" @@ -128,6 +140,7 @@ STR_USERNAME: "Felhasználónév" STR_PASSWORD: "Jelszó" STR_SYNC_SERVER_URL: "Szinkronizálás szerver URL" STR_DOCUMENT_MATCHING: "Dokumentum egyeztetés" +STR_SEND_METADATA: "Dokumentum metaadatok küldése" STR_AUTHENTICATE: "Hitelesítés" STR_KOREADER_USERNAME: "KOReader felhasználónév" STR_KOREADER_PASSWORD: "KOReader jelszó" @@ -388,11 +401,10 @@ STR_CLOCK_SYNC_NOW: "Dátum/idő szinkronizálása most" STR_CLOCK_UTC_OFFSET: "Óra UTC eltolás" STR_DISABLED: "Letiltva" STR_DISPLAY_SLEEP_SCREEN: "Alvó képernyő" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Bekezdés behúzásának kényszerítése" STR_FORCE_REFRESH: "Képernyő frissítése" STR_FRONT_BUTTONS: "Elülső gombok" @@ -466,3 +478,4 @@ STR_NEARBY_POSITION_NO_READER: "Nem található közeli olvasó" STR_NEARBY_LABEL: "Közeli:" STR_APPLY_NEARBY_POSITION: "Közeli pozíció alkalmazása" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "A közeli pozíció szinkron nem érhető el a szimulátorban" +STR_NEARBY_TRANSFER_VALIDATING: "Ellenőrzés..." diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 9f7753cf55..cf1c2d0da0 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Salvare la password?" STR_PRESS_OK_SCAN: "Premere OK per ripetere scansione" STR_JOIN_NETWORK: "Connettiti a una rete" STR_CREATE_HOTSPOT: "Crea Hotspot" -STR_NEARBY_STATS_SYNC: "Sync statistiche vicino" -STR_NEARBY_STATS_SYNC_DESC: "Sincronizza le statistiche di lettura CrossInk con un X3/X4 vicino" +STR_NEARBY_DEVICE: "Dispositivo vicino" +STR_NEARBY_STATS_SYNC: "Sincronizza statistiche" +STR_RECEIVE_NEARBY_BOOK: "Ricevi file" +STR_RECEIVE_NEARBY_BOOK_DESC: "Ricevi un file da un altro lettore" +STR_NEARBY_STATS_SYNC_DESC: "Sincronizza le statistiche di lettura tra lettori" STR_JOIN_DESC: "Connessione ad una rete WiFi" STR_HOTSPOT_DESC: "Creazione di una rete WiFi" STR_STARTING_HOTSPOT: "Avvio Hotspot..." @@ -93,13 +96,19 @@ STR_ALL_BUTTONS: "Tutti i pulsanti" STR_LONG_PRESS_SKIP: "Pressione lunga: salta capitolo" STR_LONG_PRESS_BEHAVIOR: "Comportamento pressione lunga" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienta pulsanti frontali" +STR_TOUCH_READER_CONTROLS: "Controlli touch lettore" STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_SKIP: "Salta capitolo" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientamento" STR_FONT_PREVIEW_TEXT: "Pranzo d'acqua fa volti sghembi" STR_FONT_FAMILY: "Font lettore" +STR_DICTIONARY_FONT: "Carattere del dizionario" +STR_USE_READER_FONT: "Usa il carattere del lettore" +STR_DICTIONARY_FONT_SIZE: "Dimensione carattere del dizionario" +STR_USE_READER_FONT_SIZE: "Usa dimensione carattere del lettore" STR_FONT_SIZE: "Dimensione font" STR_LINE_SPACING: "Interlinea lettore" +STR_WORD_SPACING: "Spaziatura parole" STR_SCREEN_MARGIN: "Margine schermo lettore" STR_PUBLISHER_PAGE_NUMBERS: "Numeri di pagina dell'editore" STR_PARA_ALIGNMENT: "Allineamento paragrafo" @@ -132,6 +141,7 @@ STR_USERNAME: "Nome utente" STR_PASSWORD: "Password" STR_SYNC_SERVER_URL: "URL server di sincronizzazione" STR_DOCUMENT_MATCHING: "Corrispondenza documenti" +STR_SEND_METADATA: "Invia metadati documento" STR_AUTHENTICATE: "Autentica" STR_KOREADER_USERNAME: "Nome utente KOReader" STR_KOREADER_PASSWORD: "Password KOReader" @@ -455,11 +465,10 @@ STR_CLOCK_SYNCED: "Orologio sincronizzato" STR_CLOCK_SYNC_NOW: "Sincronizza data/ora adesso" STR_CLOCK_UTC_OFFSET: "Differenza UTC dell'orologio" STR_DISPLAY_SLEEP_SCREEN: "Schermata del sonno" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Forza i rientri dei paragrafi" STR_FRONT_BUTTONS: "Pulsanti frontali" STR_GUIDE_READING: "Punti guida" @@ -535,3 +544,9 @@ STR_NEARBY_POSITION_NO_READER: "Nessun lettore vicino trovato" STR_NEARBY_LABEL: "Vicino:" STR_APPLY_NEARBY_POSITION: "Applica posizione vicina" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "La sync posizione vicino non è disponibile nel simulatore" +STR_EOB_CONTINUE_WITH: "Continua con" +STR_EOB_HOME: "Home" +STR_INDEX_FAILED: "Indicizzazione fallita - libro non valido" +STR_ADD_HIDDEN_NETWORK: "Aggiungi rete nascosta..." +STR_ENTER_WIFI_SSID: "Inserisci il nome della rete (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Convalida..." diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index 3a227a9247..8cfa9ea972 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -39,8 +39,11 @@ STR_SAVE_PASSWORD: "Келесі жолға құпия сөзді сақтау STR_PRESS_OK_SCAN: "Қайта іздеу үшін OK басыңыз" STR_JOIN_NETWORK: "Желіге қосылу" STR_CREATE_HOTSPOT: "Хотспот жасау" -STR_NEARBY_STATS_SYNC: "Жақын статистиканы синхрондау" -STR_NEARBY_STATS_SYNC_DESC: "CrossInk оқу статистикасын жақындағы X3/X4 құрылғысымен синхрондау" +STR_NEARBY_DEVICE: "Жақын құрылғы" +STR_NEARBY_STATS_SYNC: "Статистиканы синхрондау" +STR_RECEIVE_NEARBY_BOOK: "Файлды қабылдау" +STR_RECEIVE_NEARBY_BOOK_DESC: "Басқа оқу құрылғысынан файл қабылдау" +STR_NEARBY_STATS_SYNC_DESC: "Оқу статистикасын құрылғылар арасында синхрондау" STR_JOIN_DESC: "Бар WiFi желісіне қосылу" STR_HOTSPOT_DESC: "Басқалар қоса алатын WiFi желісін жасау" STR_STARTING_HOTSPOT: "Хотспот іске қосылуда..." @@ -85,12 +88,19 @@ STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқ STR_ORIENTATION_AWARE: "Бағдарға қарай" STR_NAV_BUTTONS: "Навигация түймелері" STR_ALL_BUTTONS: "Барлық түймелер" +STR_TOUCH_READER_CONTROLS: "Оқырманның сенсорлық басқаруы" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Алдыңғы түймелерді бағдарлау" STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу" STR_LONG_PRESS_BEHAVIOR: "Ұзақ басу әрекеті" STR_FONT_PREVIEW_TEXT: "Канагаттандырылмагандыктарыныздан" STR_FONT_FAMILY: "Оқырман қаріп тобы" +STR_DICTIONARY_FONT: "Сөздік қарпі" +STR_USE_READER_FONT: "Оқырман қарпін пайдалану" +STR_DICTIONARY_FONT_SIZE: "Сөздік қаріп өлшемі" +STR_USE_READER_FONT_SIZE: "Оқырман қаріп өлшемін пайдалану" STR_FONT_SIZE: "Интерфейс қаріп өлшемі" STR_LINE_SPACING: "Оқырман жол аралығы" +STR_WORD_SPACING: "Сөздер аралығы" STR_SCREEN_MARGIN: "Оқырман экран жиегі" STR_PUBLISHER_PAGE_NUMBERS: "Баспагер бет нөмірлері" STR_PARA_ALIGNMENT: "Оқырман абзац туралануы" @@ -118,6 +128,7 @@ STR_USERNAME: "Пайдаланушы аты" STR_PASSWORD: "Құпия сөз" STR_SYNC_SERVER_URL: "Синхрондау сервері URL" STR_DOCUMENT_MATCHING: "Құжат сәйкестендіру" +STR_SEND_METADATA: "Құжат метадеректерін жіберу" STR_AUTHENTICATE: "Аутентификация" STR_KOREADER_USERNAME: "KOReader пайдаланушы аты" STR_KOREADER_PASSWORD: "KOReader құпия сөзі" @@ -383,11 +394,10 @@ STR_CLOCK_SYNC_NOW: "Күн/уақытты қазір синхрондау" STR_CLOCK_UTC_OFFSET: "Сағаттың UTC ығысуы" STR_DISABLED: "Өшірілген" STR_DISPLAY_SLEEP_SCREEN: "Ұйқы экраны" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Абзац шегіністерін мәжбүрлеу" STR_FORCE_REFRESH: "Экранды жаңарту" STR_FRONT_BUTTONS: "Алдыңғы түймелер" @@ -466,3 +476,6 @@ STR_NEARBY_POSITION_NO_READER: "Жақын оқу құрылғысы табыл STR_NEARBY_LABEL: "Жақын:" STR_APPLY_NEARBY_POSITION: "Жақын позицияны қолдану" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Жақын позиция синхрондауы симуляторда қолжетімді емес" +STR_ADD_HIDDEN_NETWORK: "Жасырын желіні қосу..." +STR_ENTER_WIFI_SSID: "Желі атауын енгізіңіз (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Тексерілуде..." diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index 6e5ed639bf..1c73c9f05d 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Išsaugoti slaptažodį?" STR_PRESS_OK_SCAN: "Ieškoti iš naujo (OK)" STR_JOIN_NETWORK: "Prisijungti" STR_CREATE_HOTSPOT: "Sukurti prieigą" -STR_NEARBY_STATS_SYNC: "Netoliese esančių statistikų sinchronizavimas" -STR_NEARBY_STATS_SYNC_DESC: "Sinchronizuoti CrossInk skaitymo statistiką su netoliese esančiu X3/X4" +STR_NEARBY_DEVICE: "Netoliese esantis įrenginys" +STR_NEARBY_STATS_SYNC: "Sinchronizuoti statistiką" +STR_RECEIVE_NEARBY_BOOK: "Gauti failą" +STR_RECEIVE_NEARBY_BOOK_DESC: "Gauti failą iš kito skaitytuvo" +STR_NEARBY_STATS_SYNC_DESC: "Sinchronizuoti skaitymo statistiką tarp skaitytuvų" STR_JOIN_DESC: "Jungtis prie esamo WiFi" STR_HOTSPOT_DESC: "Sukurti WiFi kitiems" STR_STARTING_HOTSPOT: "Kuriamas ryšys..." @@ -90,12 +93,19 @@ STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai" STR_ORIENTATION_AWARE: "Pagal orientaciją" STR_NAV_BUTTONS: "Navigacijos mygtukai" STR_ALL_BUTTONS: "Visi mygtukai" +STR_TOUCH_READER_CONTROLS: "Lietimo valdikliai skaityklėje" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuoti priekinius mygtukus" STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)" STR_LONG_PRESS_BEHAVIOR: "Ilgo paspaudimo veikimas" STR_FONT_PREVIEW_TEXT: "Įlinkdama fechtuotojo špaga sublykčiojusi pragręžė apvalų arbūzą" STR_FONT_FAMILY: "Šriftas" +STR_DICTIONARY_FONT: "Žodyno šriftas" +STR_USE_READER_FONT: "Naudoti skaityklės šriftą" +STR_DICTIONARY_FONT_SIZE: "Žodyno šrifto dydis" +STR_USE_READER_FONT_SIZE: "Naudoti skaityklės šrifto dydį" STR_FONT_SIZE: "Šrifto dydis" STR_LINE_SPACING: "Tarpai tarp eilučių" +STR_WORD_SPACING: "Tarpai tarp žodžių" STR_SCREEN_MARGIN: "Ekrano paraštės" STR_PUBLISHER_PAGE_NUMBERS: "Leidėjo puslapių numeriai" STR_PARA_ALIGNMENT: "Lygiavimas" @@ -128,6 +138,7 @@ STR_USERNAME: "Vartotojas" STR_PASSWORD: "Slaptažodis" STR_SYNC_SERVER_URL: "Serverio URL" STR_DOCUMENT_MATCHING: "Atpažinimas" +STR_SEND_METADATA: "Siųsti dokumento metaduomenis" STR_AUTHENTICATE: "Prisijungti" STR_KOREADER_USERNAME: "KOReader vartotojas" STR_KOREADER_PASSWORD: "KOReader slaptažodis" @@ -388,11 +399,10 @@ STR_CLOCK_SYNC_NOW: "Sinchronizuoti datą/laiką dabar" STR_CLOCK_UTC_OFFSET: "Laikrodžio UTC poslinkis" STR_DISABLED: "Išjungta" STR_DISPLAY_SLEEP_SCREEN: "Miego ekranas" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Priversti pastraipos įtraukas" STR_FORCE_REFRESH: "Atnaujinti ekraną" STR_FRONT_BUTTONS: "Priekiniai mygtukai" @@ -466,3 +476,6 @@ STR_NEARBY_POSITION_NO_READER: "Netoliese nerasta skaitytuvo" STR_NEARBY_LABEL: "Netoliese:" STR_APPLY_NEARBY_POSITION: "Taikyti netoliese esančią poziciją" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Netoliese esančios pozicijos sinchronizavimas simuliatoriuje nepasiekiamas" +STR_ADD_HIDDEN_NETWORK: "Pridėti paslėptą tinklą..." +STR_ENTER_WIFI_SSID: "Įveskite tinklo pavadinimą (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Tikrinama..." diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index 749d2198ff..2ae9216f63 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Zapisać hasło na przyszłość?" STR_PRESS_OK_SCAN: "Naciśnij OK aby skanować ponownie" STR_JOIN_NETWORK: "Dołącz do sieci" STR_CREATE_HOTSPOT: "Stwórz Hotspot" -STR_NEARBY_STATS_SYNC: "Synchronizacja statystyk w pobliżu" -STR_NEARBY_STATS_SYNC_DESC: "Synchronizuj statystyki czytania CrossInk z pobliskim X3/X4" +STR_NEARBY_DEVICE: "Urządzenie w pobliżu" +STR_NEARBY_STATS_SYNC: "Synchronizuj statystyki" +STR_RECEIVE_NEARBY_BOOK: "Odbierz plik" +STR_RECEIVE_NEARBY_BOOK_DESC: "Odbierz plik z innego czytnika" +STR_NEARBY_STATS_SYNC_DESC: "Synchronizuj statystyki czytania między czytnikami" STR_JOIN_DESC: "Podłącz do istniejącej sieci WiFi" STR_HOTSPOT_DESC: "Stwórz sieć WiFi do której podłączyć mogą się inni" STR_STARTING_HOTSPOT: "Startowanie Hotspot'a..." @@ -92,13 +95,20 @@ STR_NAV_BUTTONS: "Przyciski nawigacji" STR_ALL_BUTTONS: "Wszystkie przyciski" STR_LONG_PRESS_SKIP: "Przytrzymaj, aby przeskoczyć rozdział" STR_LONG_PRESS_BEHAVIOR: "Zachowanie przy przytrzymaniu" +STR_TOUCH_READER_CONTROLS: "Sterowanie dotykowe czytnika" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuj przednie przyciski" STR_LONG_PRESS_BEHAVIOR_OFF: "Wył." STR_LONG_PRESS_BEHAVIOR_SKIP: "Przeskocz rozdział" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientacja ekranu" STR_FONT_PREVIEW_TEXT: "Pchnąć w tę łódź jeża lub ośm skrzyń fig" STR_FONT_FAMILY: "Czcionka" +STR_DICTIONARY_FONT: "Czcionka słownika" +STR_USE_READER_FONT: "Użyj czcionki czytnika" +STR_DICTIONARY_FONT_SIZE: "Rozmiar czcionki słownika" +STR_USE_READER_FONT_SIZE: "Użyj rozmiaru czcionki czytnika" STR_FONT_SIZE: "Rozmiar czcionki" STR_LINE_SPACING: "Odstępy między wierszami" +STR_WORD_SPACING: "Odstępy między słowami" STR_SCREEN_MARGIN: "Marginesy ekranu" STR_PUBLISHER_PAGE_NUMBERS: "Numery stron wydawcy" STR_PARA_ALIGNMENT: "Wyjustowanie paragrafów" @@ -131,6 +141,7 @@ STR_USERNAME: "Użytkownik" STR_PASSWORD: "Hasło" STR_SYNC_SERVER_URL: "Serwer URL synchronizacji" STR_DOCUMENT_MATCHING: "Dopasowanie dokumentów" +STR_SEND_METADATA: "Wyślij metadane dokumentu" STR_AUTHENTICATE: "Uwierzytelnianie" STR_KOREADER_USERNAME: "Użytkownik KOReader" STR_KOREADER_PASSWORD: "Hasło KOReader" @@ -414,11 +425,10 @@ STR_CLOCK_SYNC_NOW: "Zsynchronizuj datę/czas teraz" STR_CLOCK_UTC_OFFSET: "Przesunięcie zegara UTC" STR_DISABLED: "Niepełnosprawny" STR_DISPLAY_SLEEP_SCREEN: "Ekran uśpienia" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_REFRESH: "Odśwież ekran" STR_FRONT_BUTTONS: "Przyciski z przodu" STR_HUGE: "Ogromny" @@ -487,3 +497,6 @@ STR_NEARBY_POSITION_NO_READER: "Nie znaleziono czytnika w pobliżu" STR_NEARBY_LABEL: "W pobliżu:" STR_APPLY_NEARBY_POSITION: "Zastosuj pozycję w pobliżu" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Synchronizacja pozycji w pobliżu nie jest dostępna w symulatorze" +STR_ADD_HIDDEN_NETWORK: "Dodaj ukrytą sieć..." +STR_ENTER_WIFI_SSID: "Wprowadź nazwę sieci (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Sprawdzanie..." diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese-BR.yaml similarity index 53% rename from lib/I18n/translations/portuguese.yaml rename to lib/I18n/translations/portuguese-BR.yaml index 095b1e024a..8f6fc81012 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese-BR.yaml @@ -1,14 +1,13 @@ _language_name: "Português (Brasil)" _language_code: "PT" _order: "5" - STR_CROSSINK: "CrossInk" STR_BOOTING: "INICIANDO" STR_SLEEPING: "EM REPOUSO" STR_ENTERING_SLEEP: "Entrando em repouso" -STR_BROWSE_FILES: "Arquivos" -STR_BROWSE: "Arquivos" -STR_FILE_TRANSFER: "Transferência" +STR_BROWSE_FILES: "Explorar arquivos" +STR_BROWSE: "Explorar" +STR_FILE_TRANSFER: "Transferência de arquivos" STR_SETTINGS_TITLE: "Configurações" STR_SETTINGS_SHORT: "Config." STR_READ: "Ler" @@ -23,31 +22,38 @@ STR_END_OF_BOOK: "Fim do livro" STR_EMPTY_CHAPTER: "Capítulo vazio" STR_INDEXING: "Indexando" STR_MEMORY_ERROR: "Erro de memória" -STR_PAGE_LOAD_ERROR: "Erro página" +STR_PAGE_LOAD_ERROR: "Erro ao carregar página" STR_EMPTY_FILE: "Arquivo vazio" STR_OUT_OF_BOUNDS: "Fora dos limites" STR_LOADING: "Carregando..." STR_LOADING_POPUP: "Carregando" STR_WIFI_NETWORKS: "Redes Wi‑Fi" -STR_NO_NETWORKS: "Sem redes" +STR_NO_NETWORKS: "Nenhuma rede encontrada" STR_NETWORKS_FOUND: "%zu redes encontradas" STR_SCANNING: "Procurando..." STR_CONNECTING: "Conectando..." STR_CONNECTED: "Conectado!" STR_CONNECTION_FAILED: "Falha na conexão" STR_FORGET_NETWORK: "Esquecer rede?" -STR_SAVE_PASSWORD: "Salvar senha a próxima vez?" -STR_PRESS_OK_SCAN: "Pressione OK procurar novamente" +STR_SAVE_PASSWORD: "Salvar senha para a próxima vez?" +STR_PRESS_OK_SCAN: "Pressione OK para procurar novamente" STR_JOIN_NETWORK: "Entrar em uma rede" STR_CREATE_HOTSPOT: "Criar hotspot" -STR_NEARBY_STATS_SYNC: "Sincronização próxima de estatísticas" -STR_NEARBY_STATS_SYNC_DESC: "Sincronizar estatísticas de leitura do CrossInk com um X3/X4 próximo" +STR_NEARBY_DEVICE: "Dispositivo próximo" +STR_NEARBY_STATS_SYNC: "Sincronizar estatísticas" +STR_RECEIVE_NEARBY_BOOK: "Receber arquivo" +STR_RECEIVE_NEARBY_BOOK_DESC: "Receber um arquivo de outro leitor" +STR_NEARBY_POSITION_SYNC: "Sincronização próxima de posição" STR_JOIN_DESC: "Conecte-se a uma rede Wi‑Fi existente" -STR_HOTSPOT_DESC: "Crie uma rede Wi‑Fi outras pessoas entrarem" +STR_HOTSPOT_DESC: "Crie uma rede Wi-Fi para outros dispositivos entrarem" +STR_NEARBY_STATS_SYNC_DESC: "Sincronizar estatísticas de leitura entre leitores" +STR_NEARBY_STATS_VERSION_MISMATCH: "Versões incompatíveis. Estatísticas não sincronizadas" +STR_NEARBY_POSITION_VERSION_MISMATCH: "Versões incompatíveis. Posição não sincronizada" +STR_NEARBY_POSITION_BOOK_MISMATCH: "Livro diferente. Posição não sincronizada" STR_STARTING_HOTSPOT: "Iniciando hotspot..." STR_HOTSPOT_MODE: "Modo hotspot" STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi‑Fi" -STR_OPEN_URL_HINT: "Abra este URL seu navegador" +STR_OPEN_URL_HINT: "Abra este URL no seu navegador" STR_OR_HTTP_PREFIX: "ou http://" STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:" STR_CALIBRE_WIRELESS: "Calibre sem fio" @@ -56,49 +62,86 @@ STR_NETWORK_LEGEND: "* = Criptografada | + = Salva" STR_MAC_ADDRESS: "Endereço MAC:" STR_CHECKING_WIFI: "Verificando Wi‑Fi..." STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi‑Fi" -STR_TO_PREFIX: "para" -STR_CALIBRE_RECEIVING: "Recebendo:" -STR_CALIBRE_RECEIVED: "Recebido:" +STR_TO_PREFIX: "para " +STR_CALIBRE_RECEIVING: "Recebendo: " +STR_CALIBRE_RECEIVED: "Recebido: " STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader" -STR_CALIBRE_INSTRUCTION_2: "2) Esteja mesma rede Wi‑Fi" -STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar o dispositivo\"" -STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\"" +STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi" +STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar para o dispositivo\"" +STR_CALIBRE_INSTRUCTION_4: "Mantenha esta tela aberta durante o envio" STR_CAT_DISPLAY: "Tela" STR_CAT_READER: "Leitor" STR_CAT_CONTROLS: "Controles" STR_CAT_SYSTEM: "Sistema" STR_SLEEP_SCREEN: "Tela de repouso" -STR_SLEEP_COVER_MODE: "Modo capa tela repouso" +STR_SLEEP_SCREEN_WALLPAPER: "Papel de parede" +STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite" +STR_AFTER_TIMEOUT: "Após tempo limite" +STR_SLEEP_COVER_MODE: "Modo da capa da tela de repouso" +STR_SLEEP_COVER_MODE_SHORT: "Modo de capa" STR_HIDE_BATTERY: "Ocultar % da bateria" -STR_EXTRA_SPACING: "Espaço de parágrafos extra" +STR_DISPLAY_THEME_HOME: "Tema e início" +STR_DISPLAY_REFRESH: "Atualização" +STR_DISPLAY_SLEEP_SCREEN: "Tela de suspensão" +STR_DISPLAY_STATUS: "Status" +STR_EXTRA_SPACING: "Espaçamento extra entre parágrafos" +STR_FORCE_PARAGRAPH_INDENTS: "Forçar recuos de parágrafo" STR_TEXT_AA: "Suavização de texto" STR_READER_DARK_MODE: "Modo escuro" -STR_STATS_DAY_STREAK_FORMAT: "sequência de %u dias" -STR_STATS_NO_STREAK: "Ainda sem sequência" -STR_STATS_NEW_READER: "Novo leitor" -STR_STATS_MORNING_READER: "Leitor matinal" -STR_STATS_AFTERNOON_READER: "Leitor da tarde" -STR_STATS_EVENING_READER: "Leitor da noite" -STR_STATS_NIGHT_READER: "Leitor noturno" -STR_SHORT_PWR_BTN: "Clique curto botão ligar" -STR_ORIENTATION: "Orientação de leitura" -STR_SIDE_BTN_LAYOUT: "Disposição botões laterais" +STR_IMAGES: "Imagens" +STR_IMAGES_DISPLAY: "Exibição" +STR_IMAGES_PLACEHOLDER: "Espaço reservado" +STR_IMAGES_SUPPRESS: "Suprimir" +STR_EOB_HOME: "Início" +STR_EOB_CONTINUE_WITH: "Continuar com" +STR_SHORT_PWR_BTN: "Ação do toque curto" +STR_ORIENTATION: "Orientação" +STR_SIDE_BTN_LAYOUT: "Layout" STR_ORIENTATION_AWARE: "Sensível à orientação" STR_NAV_BUTTONS: "Botões de navegação" STR_ALL_BUTTONS: "Todos os botões" +STR_LONG_PRESS_SKIP: "Pular capítulo com toque longo" +STR_SIDE_BTN_LONG_PRESS: "Ação de toque longo" +STR_CHAPTER_SKIP_OPT: "Pular capítulo" +STR_CHANGE_FONT_SIZE: "Alterar tamanho da fonte" STR_LONG_PRESS_BEHAVIOR: "Comportamento de pressão longa" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais" STR_LONG_PRESS_BEHAVIOR_OFF: "Desligado" -STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo" +STR_LONG_PRESS_BEHAVIOR_SKIP: "Pular capítulo" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação" -STR_FONT_PREVIEW_TEXT: "Vejo galã sexy pôr quinze kiwis à força em baú achatado" -STR_FONT_FAMILY: "Fonte do leitor" -STR_FONT_SIZE: "Tam. fonte UI" +STR_FONT_FAMILY: "Família da fonte" +STR_DICTIONARY_FONT: "Fonte do dicionário" +STR_USE_READER_FONT: "Usar fonte do leitor" +STR_DICTIONARY_FONT_SIZE: "Tamanho da fonte do dicionário" +STR_USE_READER_FONT_SIZE: "Usar tamanho da fonte do leitor" +STR_FONT_SIZE: "Tamanho da fonte" +STR_SD_FONT_SIZE_RANGE: "Baixar faixa de tamanho de fonte" +STR_READER_FONT_OPTIONS: "Opções de fonte" +STR_READER_PAGE_LAYOUT: "Layout da página" +STR_READER_BOOK_STYLING: "Estilo de livro" +STR_READER_READING_AIDS: "Auxiliares de leitura" +STR_READER_UI: "Interface do leitor" +STR_EPUB_RENDER_MODE: "Modo de renderização EPUB" +STR_RENDER_MODE_CROSSINK_DEFAULT: "Padrão CrossInk" +STR_RENDER_MODE_BALANCED: "Equilibrado" +STR_RENDER_MODE_LIGHT: "Leve" +STR_BALANCED_MODE: "Modo equilibrado" +STR_LIGHT_MODE: "Modo leve" +STR_SAFE_MODE: "Modo seguro" STR_LINE_SPACING: "Espaçamento entre linhas" -STR_SCREEN_MARGIN: "Margens da tela" +STR_WORD_SPACING: "Espaçamento entre palavras" +STR_SCREEN_MARGIN: "Margem da tela" STR_PUBLISHER_PAGE_NUMBERS: "Números de página da editora" -STR_PARA_ALIGNMENT: "Alinhamento parágrafo" +STR_PARA_ALIGNMENT: "Alinhamento do parágrafo" +STR_LONG_PRESS_MENU: "Menu de toque longo" +STR_FONT_PREVIEW_TEXT: "Vejo galã sexy pôr quinze kiwis à força em baú achatado" STR_HYPHENATION: "Hifenização" STR_TIME_TO_SLEEP: "Tempo para repousar" +STR_SHOW_HIDDEN_FILES: "Mostrar arquivos ocultos" +STR_HIDE_FILE_EXTENSION: "Ocultar extensão" +STR_FILE_BROWSER_DISPLAY: "Exibição do navegador" +STR_FILE_BROWSER_DISPLAY_1_LINE: "1 linha" +STR_FILE_BROWSER_DISPLAY_2_LINES: "2 linhas" STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes" STR_MOVE_FINISHED_TO_READ: "Mover livros lidos para a pasta Read" STR_AUTO_BACKUP_STATS: "Backups automáticos" @@ -106,21 +149,27 @@ STR_BACKUP_NOW: "Fazer backup agora" STR_BACKUP_STATS_CONFIRM: "Salvar uma cópia em /.crossink-stats-backup/?" STR_BACKUP_STATS_DONE: "Backup salvo" STR_BACKUP_STATS_FAILED: "Falha no backup" +STR_SYSTEM_DEVICE: "Dispositivo" +STR_DEVICE_NAME: "Nome do dispositivo" +STR_SYSTEM_FILES_CACHE: "Arquivos e cache" STR_ALL_TIME_STATS: "Estatísticas de todo o período" STR_IDLE_TIME_THRESHOLD: "Limite de tempo ocioso" STR_SECONDS_VALUE_FORMAT: "%u s" STR_IDLE_TIME_THRESHOLD_STEP_HINT: "Esq./Dir.: 10s Cima/Baixo: 1m" STR_RESET_ALL_TIME_STATS: "Redefinir estatísticas de todo o período" STR_RESET_ALL_TIME_STATS_CONFIRM: "Limpar as estatísticas de todo o período deste leitor? Os backups são mantidos." -STR_REFRESH_FREQ: "Frequência atualização" +STR_SYSTEM_NETWORK_SYNC: "Rede e sincronização" +STR_SYSTEM_UPDATES: "Atualizações" +STR_REFRESH_FREQ: "Frequência de atualização" STR_KOREADER_SYNC: "Sincronização KOReader" STR_CHECK_UPDATES: "Verificar atualizações" STR_LANGUAGE: "Idioma" STR_CLEAR_READING_CACHE: "Limpar cache de leitura" STR_USERNAME: "Nome de usuário" STR_PASSWORD: "Senha" -STR_SYNC_SERVER_URL: "URL servidor sincronização" -STR_DOCUMENT_MATCHING: "Documento correspondente" +STR_SYNC_SERVER_URL: "URL do servidor de sincronização" +STR_DOCUMENT_MATCHING: "Correspondência de documento" +STR_SEND_METADATA: "Enviar metadados do doc." STR_AUTHENTICATE: "Autenticar" STR_KOREADER_USERNAME: "Usuário do KOReader" STR_KOREADER_PASSWORD: "Senha do KOReader" @@ -133,10 +182,10 @@ STR_WIFI_CONN_FAILED: "Falha na conexão Wi‑Fi" STR_AUTHENTICATING: "Autenticando..." STR_AUTH_SUCCESS: "Autenticado com sucesso!" STR_KOREADER_AUTH: "Autenticação KOReader" -STR_SYNC_READY: "A sincronização KOReader está pronta uso" +STR_SYNC_READY: "A sincronização KOReader está pronta para uso" STR_AUTH_FAILED: "Falha na autenticação" STR_DONE: "Feito" -STR_CLEAR_CACHE_WARNING_1: "Isso vai limpar todos os dados livros em cache." +STR_CLEAR_CACHE_WARNING_1: "Isso limpará todos os dados de livros em cache." STR_CLEAR_CACHE_WARNING_2: "Todo o progresso de leitura será perdido!" STR_CLEAR_CACHE_WARNING_3: "Os livros precisarão ser reindexados" STR_CLEAR_CACHE_WARNING_4: "quando forem abertos novamente." @@ -145,7 +194,7 @@ STR_CACHE_CLEARED: "Cache limpo" STR_ITEMS_REMOVED: "itens removidos" STR_FAILED_LOWER: "falhou" STR_CLEAR_CACHE_FAILED: "Falha ao limpar o cache" -STR_CHECK_SERIAL_OUTPUT: "Ver saída serial" +STR_CHECK_SERIAL_OUTPUT: "Verifique a saída serial para detalhes" STR_DARK: "Escuro" STR_LIGHT: "Claro" STR_CUSTOM: "Personalizado" @@ -159,20 +208,34 @@ STR_ALWAYS: "Sempre" STR_IGNORE: "Ignorar" STR_SLEEP: "Repouso" STR_PAGE_TURN: "Virar página" +STR_FORCE_REFRESH: "Atualizar tela" +STR_CHANGE_FONT: "Alterar fonte" STR_PORTRAIT: "Retrato" -STR_LANDSCAPE_CW: "Paisagem H" +STR_LANDSCAPE_CW: "Paisagem horário" STR_INVERTED: "Invertido" STR_ORIENTATION_INVERTED: "Retrato 180°" -STR_LANDSCAPE_CCW: "Paisagem AH" +STR_LANDSCAPE_CCW: "Paisagem anti-horário" STR_PREV_NEXT: "Ant/Próx" STR_NEXT_PREV: "Próx/Ant" +STR_DISABLED: "Desativado" STR_NEXT_NEXT: "Próx/Próx" -STR_NOTO_SERIF: "Noto Serif" -STR_NOTO_SANS: "Noto Sans" +STR_LEXEND_DECA: "Lexend Deca" +STR_BITTER: "Bitter" +STR_CHAREINK: "ChareInk" +STR_ATKINSON_HL: "Atkinson Hyperlegible" +STR_COMPACT: "Compacto" +STR_TINY: "Minúsculo" +STR_TEENSY: "Teensy" +STR_ITTY_BITTY: "Itty Bitty" STR_SMALL: "Pequeno" STR_MEDIUM: "Médio" STR_LARGE: "Grande" STR_X_LARGE: "Extra grande" +STR_HUGE: "Enorme" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" +STR_FONT_RANGE_ALL: "8-20pt" STR_TIGHT: "Apertado" STR_NORMAL: "Normal" STR_WIDE: "Largo" @@ -188,32 +251,40 @@ STR_PAGES_30: "30 páginas" STR_UPDATE: "Atualizar" STR_CHECKING_UPDATE: "Verificando atualização..." STR_NEW_UPDATE: "Nova atualização disponível!" -STR_CURRENT_VERSION: "Versão atual:" -STR_NEW_VERSION: "Nova versão:" +STR_CURRENT_VERSION: "Versão atual: " +STR_NEW_VERSION: "Nova versão: " STR_UPDATING: "Atualizando..." STR_NO_UPDATE: "Nenhuma atualização disponível" STR_UPDATE_FAILED: "Falha na atualização" +STR_UPDATE_HASH_MISMATCH: "Arquivo de atualização não corresponde" STR_UPDATE_COMPLETE: "Atualização concluída" STR_POWER_ON_HINT: "Pressione e segure o botão energia ligar novamente" -STR_NO_ENTRIES: "Nenhum entries encontrado" +STR_RESTARTING_HINT: "Reiniciando... Se o dispositivo não reiniciar, segure o botão liga/desliga por alguns segundos." +STR_NO_ENTRIES: "Nenhuma entrada encontrada" STR_DOWNLOADING: "Baixando..." STR_DOWNLOAD_FAILED: "Falha no download" STR_ERROR_MSG: "Erro:" STR_UNNAMED: "Sem nome" -STR_NO_SERVER_URL: "Nenhum URL servidor configurado" +STR_NO_SERVER_URL: "Nenhum URL de servidor configurado" STR_FETCH_FEED_FAILED: "Falha ao buscar o feed" STR_PARSE_FEED_FAILED: "Falha ao interpretar o feed" -STR_NETWORK_PREFIX: "Rede:" -STR_IP_ADDRESS_PREFIX: "Endereço IP:" +STR_OPDS_FEED_BUFFER_MEMORY_ERROR: "Não foi possível alocar memória para o buffer" +STR_EPUB_CHAPTER_INCOMPLETE_TITLE: "Capítulo incompleto" +STR_EPUB_CHAPTER_INCOMPLETE_BODY: "Este capítulo contém marcação EPUB malformada. O CrossInk parou na parte ilegível, então parte do conteúdo pode estar ausente." +STR_NEXT_PAGE: "Próxima página »" +STR_PREV_PAGE: "« Página anterior" +STR_NETWORK_PREFIX: "Rede: " +STR_IP_ADDRESS_PREFIX: "Endereço IP: " STR_ERROR_GENERAL_FAILURE: "Erro: falha geral" STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada" -STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite conexão" +STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite da conexão" STR_SD_CARD: "Cartão SD" STR_TOGGLE_HIDDEN_FILES_HINT: "Mantenha Início pressionado para alternar arquivos ocultos" STR_BACK: "« Voltar" STR_EXIT: "« Sair" STR_HOME: "« Início" STR_SELECT: "Escolher" +STR_SELECTED: "Selecionado" STR_TOGGLE: "Alternar" STR_CONFIRM: "Confirmar" STR_CANCEL: "Cancelar" @@ -223,6 +294,8 @@ STR_DOWNLOAD: "Baixar" STR_RETRY: "Tentar novamente" STR_YES: "Sim" STR_NO: "Não" +STR_SHOW: "Mostrar" +STR_HIDE: "Ocultar" STR_STATE_ON: "LIG." STR_STATE_OFF: "DESL." STR_NOT_SET: "Não definido" @@ -232,22 +305,66 @@ STR_DIR_UP: "Cima" STR_DIR_DOWN: "Baixo" STR_OK_BUTTON: "OK" STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso" +STR_SLEEP_COVER_FILTER_SHORT: "Filtro da capa" STR_FILTER_CONTRAST: "Contraste" +STR_CUSTOMISE_STATUS_BAR: "Personalizar barra de status" +STR_CHAPTER_PAGE_COUNT: "Contagem de páginas do capítulo" +STR_STABLE_PAGE_NUMBERS: "Números de página estáveis" +STR_BOOK_PROGRESS_PERCENTAGE: "Porcentagem de progresso do livro" +STR_PROGRESS_BAR: "Barra de progresso" +STR_PROGRESS_BAR_THICKNESS: "Espessura da barra de progresso" +STR_PROGRESS_BAR_THIN: "Fino" +STR_PROGRESS_BAR_MEDIUM: "Médio" +STR_PROGRESS_BAR_THICK: "Grosso" +STR_BOOK: "Livro" +STR_CHAPTER: "Capítulo" +STR_EXAMPLE_CHAPTER: "Capítulo 21" +STR_EXAMPLE_BOOK: "Título do livro" +STR_PREVIEW: "Prévia" +STR_TITLE: "Título" +STR_TIME_LEFT: "Tempo restante" +STR_TIME_LEFT_CALCULATING: "Calculando..." +STR_BATTERY: "Bateria" +STR_XTC_STATUS_BAR: "Barra de status XTC" +STR_BOTTOM: "Inferior" +STR_TOP: "Superior" +STR_CLOCK: "Mostrar relógio" +STR_HIDE_CLOCK: "Ocultar relógio" +STR_CLOCK_UTC_OFFSET: "Deslocamento UTC do relógio" +STR_CLOCK_FORMAT: "Formato do relógio" +STR_CLOCK_FORMAT_24H: "24 horas" +STR_CLOCK_FORMAT_12H: "12 horas" +STR_CURRENT_TIME: "Hora atual:" +STR_NEXT_FIELD: "Próximo" +STR_CLOCK_SYNC: "Sincronizar relógio" +STR_CLOCK_SYNC_NOW: "Sincronizar data/hora" +STR_CLOCK_SYNCING: "Sincronizando via NTP..." +STR_CLOCK_SYNC_OK: "Relógio sincronizado" +STR_CLOCK_SYNC_FAIL: "Falha na sincronização" +STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi não conectado" +STR_CLOCK_SYNC_NO_WIFI_HINT: "Conecte ao Wi-Fi primeiro e tente novamente." +STR_CLOCK_SYNCED: "Relógio sincronizado" STR_UI_THEME: "Tema da interface" STR_THEME_CLASSIC: "Clássico" +STR_THEME_LYRA: "Lyra" +STR_THEME_ROUNDEDRAFF: "RoundedRaff" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" +STR_THEME_LYRA_CAROUSEL: "Lyra Carousel" +STR_THEME_MINIMAL: "Minimal" +STR_THEME_DASHBOARD: "Painel" +STR_THEME_MINIMAL_STATS: "Minimal Stats" +STR_QUICK_RESUME: "Retomada rápida" STR_RECENT_BOOKS_VIEW: "Vista de livros recentes" STR_LIST_VIEW: "Lista" STR_GRID_VIEW: "Grade" -STR_THEME_ROUNDEDRAFF: "RoundedRaff" -STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol" -STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite" -STR_AFTER_TIMEOUT: "Após tempo limite" STR_REMAP_FRONT_BUTTONS: "Remapear botões" +STR_REMAP_FRONT_BUTTONS_READER: "Remapear botões (leitor)" STR_OPDS_BROWSER: "Navegador OPDS" +STR_SEARCH: "Pesquisar" STR_COVER_CUSTOM: "Capa + personalizado" STR_PAGE_OVERLAY: "Sobreposição da página" -STR_QUICK_RESUME: "Retomada rápida" +STR_RECENTS: "Recentes" STR_MENU_RECENT_BOOKS: "Livros recentes" STR_REMOVE_FROM_RECENTS_ACTION: "Remover dos Livros recentes" STR_REMOVE_FROM_RECENTS: "Remover dos Livros recentes?" @@ -260,11 +377,11 @@ STR_CALIBRE_SETUP: "Configuração" STR_CALIBRE_STATUS: "Status" STR_CLEAR_BUTTON: "Limpar" STR_DEFAULT_VALUE: "Padrão" -STR_REMAP_PROMPT: "Pressione um botão frontal cada função" +STR_REMAP_PROMPT: "Pressione um botão frontal para cada função" STR_UNASSIGNED: "Não atribuído" STR_ALREADY_ASSIGNED: "Já atribuído" -STR_REMAP_RESET_HINT: "Botão lateral cima: redefinir o disposição padrão" -STR_REMAP_CANCEL_HINT: "Botão lateral baixo: cancelar remapeamento" +STR_REMAP_RESET_HINT: "Botão lateral Cima: redefinir para o layout padrão" +STR_REMAP_CANCEL_HINT: "Botão lateral Baixo: cancelar remapeamento" STR_HW_BACK_LABEL: "Voltar (1º botão)" STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)" STR_HW_LEFT_LABEL: "Esquerda (3º botão)" @@ -273,20 +390,36 @@ STR_GO_TO_PERCENT: "Ir para %" STR_GO_HOME_BUTTON: "Ir para o início" STR_SYNC_PROGRESS: "Sincronizar progresso" STR_DELETE_CACHE: "Excluir cache do livro" +STR_SAVE_CLIPPING: "Criar recorte" +STR_VIEW_CLIPPINGS: "Ver recortes" +STR_CLIPPINGS: "Recortes" +STR_BOOKMARKS_AND_CLIPPINGS: "Marcadores e recortes" +STR_NO_CLIPPINGS: "Ainda sem recortes" +STR_DELETE_CLIPPINGS: "Limpar recortes" +STR_CLIPPING_SAVED: "Recorte salvo" +STR_CLIPPING_FAILED: "Não foi possível salvar o recorte" +STR_CLIPPING_LIMIT_REACHED: "Limite de recortes atingido" STR_BOOK_CACHE_DELETED: "Cache do livro excluído" STR_DELETE_BOOK_STATS: "Excluir estatísticas de leitura do livro" -STR_RESET_BOOK_READER_SETTINGS: "Reset Book Reader Settings" -STR_BOOK_READER_SETTINGS_RESET: "Book Reader Settings Reset" +STR_BOOK_STATS_DELETED: "Estatísticas de leitura excluídas" +STR_RESET_BOOK_READER_SETTINGS: "Redefinir configurações de leitura do livro" +STR_BOOK_READER_SETTINGS_RESET: "Configurações de leitura do livro redefinidas" STR_RESET_READING_PACE: "Redefinir ritmo de leitura" STR_READING_PACE_RESET: "Ritmo de leitura redefinido" STR_DELETE: "Excluir" -STR_CHAPTER_PREFIX: "Capítulo:" -STR_PAGES_SEPARATOR: "páginas |" -STR_BOOK_PREFIX: "Livro:" +STR_PIN_AS_FAVORITE: "Fixar como favorito" +STR_UNPIN_AS_FAVORITE: "Desafixar dos favoritos" +STR_PIN_PNG_WARNING: "Fixar um PNG só é compatível com o modo Sobreposição da página" +STR_SET_AS_SLEEP_FOLDER: "Definir como pasta de repouso" +STR_USE_DEFAULT_SLEEP_FOLDERS: "Usar pastas de repouso padrão" +STR_DISPLAY_QR: "Mostrar página como QR" +STR_CHAPTER_PREFIX: "Capítulo: " +STR_PAGES_SEPARATOR: " páginas | " +STR_BOOK_PREFIX: "Livro: " STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL" STR_SYNCING_TIME: "Sincronizando horário..." -STR_CALC_HASH: "Calculando hash documento..." -STR_HASH_FAILED: "Falha ao calcular o hash documento" +STR_CALC_HASH: "Calculando hash do documento..." +STR_HASH_FAILED: "Falha ao calcular o hash do documento" STR_FETCH_PROGRESS: "Buscando progresso remoto..." STR_UPLOAD_PROGRESS: "Enviando progresso..." STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada" @@ -296,44 +429,207 @@ STR_REMOTE_LABEL: "Remoto:" STR_LOCAL_LABEL: "Local:" STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% total" STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d/%d, %.2f%% total" -STR_DEVICE_FROM_FORMAT: "De: %s" +STR_DEVICE_FROM_FORMAT: " De: %s" STR_APPLY_REMOTE: "Aplicar progresso remoto" STR_UPLOAD_LOCAL: "Enviar progresso local" STR_NO_REMOTE_MSG: "Nenhum progresso remoto encontrado" STR_UPLOAD_PROMPT: "Enviar posição atual?" STR_UPLOAD_SUCCESS: "Progresso enviado!" STR_SYNC_FAILED_MSG: "Falha na sincronização" +STR_KOREADER_SYNC_AUTH_REJECTED: "Nome de usuário ou senha rejeitados pelo servidor de sincronização" +STR_KOREADER_SYNC_NETWORK_ERROR: "Não foi possível acessar o servidor de sincronização. Verifique o Wi-Fi e o URL do servidor." +STR_KOREADER_SYNC_NETWORK_REFUSED: "O servidor de sincronização recusou a conexão. Verifique o URL e se o servidor está online." +STR_KOREADER_SYNC_NETWORK_TIMEOUT: "Tempo limite do servidor de sincronização. Verifique o sinal do Wi-Fi e tente novamente." +STR_KOREADER_SYNC_NETWORK_TLS: "Não foi possível criar uma conexão segura. Verifique o URL do servidor e a configuração HTTPS." +STR_KOREADER_SYNC_HTTP_404: "Endpoint de sincronização não encontrado. Verifique se o URL aponta para seu servidor KOReader sync." +STR_KOREADER_SYNC_HTTP_STATUS_FORMAT: "O servidor de sincronização retornou HTTP %d. Tente novamente mais tarde." +STR_KOREADER_SYNC_SERVER_ERROR: "O servidor de sincronização retornou uma resposta inesperada. Tente novamente mais tarde." +STR_KOREADER_SYNC_BAD_RESPONSE: "O servidor de sincronização respondeu com dados inválidos. Verifique o URL do servidor ou o proxy." +STR_KOREADER_SYNC_LOW_MEMORY: "Memória livre insuficiente para concluir a sincronização. Tente novamente." +STR_UNKNOWN_ERROR: "Erro desconhecido" STR_SAVE_PROGRESS_FAILED: "Não foi possível salvar o progresso" -STR_SECTION_PREFIX: "Seção" +STR_SECTION_PREFIX: "Seção " STR_UPLOAD: "Enviar" STR_BOOK_S_STYLE: "Estilo do livro" STR_EMBEDDED_STYLE: "Estilo embutido" STR_BIONIC_READING: "Leitura focada" +STR_GUIDE_READING: "Pontos guia" STR_OPDS_SERVER_URL: "URL do servidor OPDS" +STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido das notas" +STR_SET_SLEEP_COVER: "Definir capa" +STR_FOOTNOTES: "Notas de rodapé" +STR_NO_FOOTNOTES: "Sem notas de rodapé nesta página" +STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Capturar tela" -STR_REMAP_FRONT_BUTTONS_READER: "Remapear botões (leitor)" +STR_AUTO_TURN_ENABLED: "Virada automática de página ativada: " STR_AUTO_TURN_INTERVAL_SECONDS: "Intervalo da virada automática de página (segundos)" STR_AUTO_TURN_STEP_HINT: "Esq/Dir: 1 s Cima/Baixo: 5 s" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_READING_STATS: "Estatísticas de leitura" +STR_EDIT: "Editar" +STR_MORE: "Mais" +STR_TRACK_READING_STATS: "Registrar estatísticas de leitura" +STR_MARK_FINISHED: "Marcar como concluído" +STR_MARK_UNFINISHED: "Marcar como não concluído" +STR_MARK_FINISHED_PROMPT_TITLE: "Marcar como concluído?" +STR_MARK_FINISHED_PROMPT_BODY: "Você chegou a 99% deste livro" STR_READER_OPTIONS: "Opções do livro" STR_BOOKMARKS: "Marcadores" -STR_OPDS_SERVERS: "Servidores OPDS" -STR_LONG_PRESS_MENU_ACTION: "Ação do menu por pressão longa" -STR_LONG_PRESS_BACK_ACTION: "Ação Voltar por pressão longa" -STR_CYCLE_PAGE_TURN: "Virada automática" -STR_LOW_MEMORY_IMAGES_TITLE: "Capítulo carregado sem algumas ou todas as imagens" -STR_LOW_MEMORY_IMAGES_BODY: "A memória disponível era insuficiente para extrair todas as imagens deste capítulo." +STR_ADD_BOOKMARK: "Adicionar marcador" +STR_REMOVE_BOOKMARK: "Remover marcador" +STR_VIEW_BOOKMARKS: "Ver marcadores" +STR_DELETE_BOOKMARKS: "Limpar lista de marcadores" +STR_BOOKMARK_ADDED: "Marcador adicionado" +STR_BOOKMARK_REMOVED: "Marcador removido" +STR_BOOKMARK_LIMIT_REACHED: "Limite de marcadores atingido" +STR_NO_BOOKMARKS: "Ainda sem marcadores" +STR_DELETE_BOOKMARK: "Excluir" +STR_BOOKMARK_PAGE_FORMAT: "Página %d" +STR_UNKNOWN_CHAPTER: "Capítulo desconhecido" +STR_STATS_SESSIONS: "Sessões: " +STR_STATS_TOTAL_TIME: "Tempo total: " +STR_STATS_AVG_SESSION: "Sessão média: " +STR_STATS_LESS_THAN_MIN: "< 1 min" +STR_STATS_THIS_BOOK: "Este livro" +STR_STATS_AVERAGES: "Médias" +STR_STATS_PAGES_PER_MIN: "Páginas/min" +STR_STATS_COMPLETED_LBL: "Livros lidos" +STR_STATS_SESSIONS_LBL: "Sessões" +STR_STATS_TIME_LBL: "Tempo de leitura" +STR_STATS_PAGES_LBL: "Páginas viradas" +STR_STATS_PROGRESS_LBL: "Progresso" +STR_STATS_AVG_SESSION_LBL: "Sessão média" +STR_STATS_DAILY_AVG_LBL: "Média diária" +STR_STATS_READING_STREAK_LBL: "Sequência de leitura" +STR_STATS_LONGEST_STREAK_LBL: "Maior sequência" +STR_STATS_ALL_TIME: "Todos os livros" +STR_STATS_TOTAL_READING_TIME_LBL: "Tempo total de leitura" +STR_STATS_THIS_DEVICE: "Todos os livros (este dispositivo)" +STR_STATS_ALL_DEVICES: "Todos os livros (todos os dispositivos)" +STR_STATS_THIS_DEVICE_SCREEN: "Estatísticas de leitura - este dispositivo" +STR_STATS_ALL_DEVICES_SCREEN: "Estatísticas de leitura - todos os dispositivos sincronizados" +STR_STATS_TIME_OF_DAY: "Hora do dia" +STR_STATS_DAY_OF_WEEK: "Dia da semana" +STR_STATS_MORNING: "Manhã" +STR_STATS_AFTERNOON: "Tarde" +STR_STATS_EVENING: "Noite" +STR_STATS_NIGHT: "Madrugada" +STR_STATS_MON: "Seg" +STR_STATS_TUE: "Ter" +STR_STATS_WED: "Qua" +STR_STATS_THU: "Qui" +STR_STATS_FRI: "Sex" +STR_STATS_SAT: "Sáb" +STR_STATS_SUN: "Dom" +STR_STATS_START_DATE: "Data de início" +STR_STATS_FINISHED_DATE: "Data de conclusão" +STR_STATS_EST_FINISH_DATE: "Data estimada de conclusão" +STR_STATS_STARTED: "Iniciado" +STR_STATS_DAY: "dia" +STR_STATS_DAYS: "dias" +STR_STATS_DAY_STREAK_FORMAT: "sequência de %u dias" +STR_STATS_NO_STREAK: "Ainda sem sequência" +STR_STATS_NEW_READER: "Novo leitor" +STR_STATS_MORNING_READER: "Leitor matinal" +STR_STATS_AFTERNOON_READER: "Leitor da tarde" +STR_STATS_EVENING_READER: "Leitor da noite" +STR_STATS_NIGHT_READER: "Leitor noturno" STR_SLEEP_NEVER: "Nunca" STR_STEP_HINT_FRONT: "Botões frontais:" STR_STEP_HINT_SIDE: "Botões laterais:" +STR_ADD_SERVER: "Adicionar servidor" +STR_SERVER_NAME: "Nome do servidor" +STR_NO_SERVERS: "Nenhum servidor OPDS configurado" +STR_DELETE_SERVER: "Excluir servidor" +STR_DELETE_CONFIRM: "Excluir este servidor?" +STR_OPDS_SERVERS: "Servidores OPDS" +STR_MOVE_TO_READ_FAILED_TITLE: "Não foi possível mover o livro" +STR_MOVE_TO_READ_FAILED_BODY: "Falha ao mover \"%s\" para a pasta /Read." +STR_DOWNLOAD_FONTS: "Baixar fontes" +STR_FONT_DOWNLOAD: "Download de fonte" +STR_MANAGE_FONTS: "Gerenciar fontes" +STR_FONT_BROWSER: "Navegador de fontes" +STR_LOADING_FONT_LIST: "Carregando lista de fontes..." +STR_NO_FONTS_AVAILABLE: "Nenhuma fonte disponível" +STR_FONT_INSTALLED: "Fonte instalada!" +STR_FONT_INSTALL_FAILED: "Falha ao instalar fonte" +STR_FONT_DOWNLOAD_INTERRUPTED: "Rede interrompida durante o download" +STR_FONT_DOWNLOAD_RETRY_HINT: "A nova tentativa continuará do progresso salvo." +STR_FONT_DOWNLOAD_CHECKSUM_HINT: "A nova tentativa baixará este arquivo novamente." +STR_INSTALLED: "Instalado" +STR_CONFIRM_DOWNLOAD_PROMPT: "Baixar?" +STR_SD_CARD_FULL: "Espaço insuficiente no cartão SD" +STR_FILES_LABEL: "Arquivos: " +STR_SIZE_LABEL: "Tamanho: " +STR_REDOWNLOAD: "Baixar novamente" +STR_DOWNLOAD_ALL: "Baixar todos" +STR_UPDATE_ALL: "Atualizar todos" +STR_ALL_FONTS_INSTALLED: "Todas as fontes instaladas!" +STR_UPDATE_AVAILABLE: "Atualização" +STR_CRASH_TITLE: "Falha do sistema" +STR_CRASH_DESCRIPTION: "Um relatório detalhado foi salvo em crash_report.txt. Inclua esse arquivo no relatório de bug." +STR_CRASH_REASON: "Motivo da falha:" +STR_CRASH_NO_REASON: "(Nenhum motivo foi registrado)" STR_TILT_PAGE_TURN: "Virar página por inclinação" STR_TILT_PAGE_TURN_DIRECTION: "Direção da inclinação" STR_TILT_DIRECTION_LEFT_RIGHT: "Esquerda-direita" STR_TILT_DIRECTION_LEFT_RIGHT_INVERTED: "Direita-esquerda" STR_TILT_DIRECTION_FORWARD_BACK: "Frente-trás" STR_TILT_DIRECTION_FORWARD_BACK_INVERTED: "Trás-frente" -STR_BOOKMARK_LIMIT_REACHED: "Limite de marcadores atingido" +STR_KB_HINT_MOVE_CURSOR: "Pressione ESQUERDA ou DIREITA para mover o cursor" +STR_KB_HINT_RETURN_CURSOR: "Pressione ESQUERDA para voltar à posição do cursor" +STR_KB_HINT_HIDE_PASSWORD: "Segure DIREITA e pressione [***] para ocultar a senha" +STR_KB_HINT_SHOW_PASSWORD: "Segure DIREITA e pressione [abc] para mostrar a senha" +STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Pressione [***] para ocultar a senha" +STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Pressione [abc] para mostrar a senha" +STR_KB_HINT_EDIT_ENTRY: "Segure CIMA para editar entrada" +STR_KB_TIPS: "Dicas:" +STR_KB_HINT_RETURN_KEYBOARD: "Pressione BAIXO para voltar ao teclado" +STR_KB_HINT_EXIT_URL_MODE: "Pressione ABC para sair do modo URL" +STR_KB_HINT_CLEAR_TEXT: "Segure DEL para limpar todo o texto" +STR_KB_HINT_SECONDARY_CHAR: "Segure SELECT para caractere secundário" +STR_KB_HINT_UPPER_SECONDARY: "Segure SELECT para MAIÚSCULAS ou caractere secundário" +STR_KB_HINT_LOWER_SECONDARY: "Segure SELECT para minúsculas ou caractere secundário" +STR_KB_HINT_URL_SNIPPETS: "Pressione URL para trechos" +STR_CONTROLS_GENERAL: "Geral" +STR_CONTROLS_IN_READER: "Controles no leitor" +STR_POWER_BUTTON: "Botão liga/desliga" +STR_FRONT_BUTTONS: "Botões frontais" +STR_SIDE_BUTTONS: "Botões laterais" +STR_OTHER: "Outros" +STR_LONG_PRESS_ACTION: "Ação de toque longo" +STR_LONG_PRESS_MENU_ACTION: "Ação do menu por pressão longa" +STR_LONG_PRESS_BACK_ACTION: "Ação Voltar por pressão longa" +STR_TOGGLE_GUIDE_DOTS: "Pontos guia" +STR_TOGGLE_BIONIC_READING: "Leitura Biônica" +STR_TOGGLE_BOOKMARK: "Alternar marcador" +STR_CYCLE_PAGE_TURN: "Virada automática" +STR_MARKED_FINISHED: "Marcado como concluído" +STR_MARKED_UNFINISHED: "Marcado como não concluído" +STR_TILT_TO_TURN_OFF: "Virada por inclinação: DESL." +STR_TILT_TO_TURN_ON: "Virada por inclinação: LIG." +STR_LOW_MEMORY_IMAGES_TITLE: "Capítulo carregado sem algumas ou todas as imagens" +STR_LOW_MEMORY_IMAGES_BODY: "A memória disponível era insuficiente para extrair todas as imagens deste capítulo." +STR_EPUB_LAYOUT_MEMORY_TITLE: "Capítulo exige memória demais" +STR_EPUB_LAYOUT_MEMORY_BODY: "Este capítulo precisa de mais memória para diagramação do que o dispositivo pode reservar com segurança." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_BOTH: "Tente novamente com a Leitura Biônica e os Pontos guia desativados." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_BIONIC: "Tente novamente com a Leitura Biônica desativada." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_GUIDE: "Tente novamente com os Pontos guia desativados." +STR_EPUB_FALLBACK_FONT_TITLE: "Capítulo usando fonte integrada" +STR_EPUB_FALLBACK_FONT_BODY: "A memória disponível era insuficiente para diagramar este capítulo com a fonte do cartão SD, então ele foi carregado com a fonte integrada." +STR_SD_FIRMWARE_UPDATE: "Atualização de firmware do cartão SD" +STR_SELECT_FIRMWARE_FILE: "Selecionar arquivo de firmware (.bin)" +STR_NO_BIN_FILES: "Nenhum arquivo .bin encontrado" +STR_VALIDATING_FIRMWARE: "Validando firmware..." +STR_INVALID_FIRMWARE: "Arquivo de firmware inválido" +STR_FIRMWARE_TOO_LARGE: "Firmware grande demais para a partição" +STR_FIRMWARE_TOO_SMALL: "Arquivo de firmware pequeno demais" +STR_FIRMWARE_UPDATE_PROMPT: "Atualizar firmware?" +STR_FIRMWARE_FILE_OPEN_FAILED: "Não foi possível abrir o arquivo" +STR_FIRMWARE_WRITE_FAILED: "Falha ao gravar firmware" +STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Não desligue!" +STR_RECOVERY_MODE: "Modo de recuperação" +STR_RECOVERY_MODE_HINT: "Coloque firmware.bin na raiz do cartão SD e selecione-o" STR_NEARBY_STATS_READY: "Pronto para sincronizar os dois leitores" STR_NEARBY_STATS_SYNC_BUTTON: "Sync" STR_NEARBY_STATS_READY_HINT: "Pressione Sync em apenas um leitor" @@ -341,108 +637,13 @@ STR_NEARBY_STATS_SCANNING: "Procurando leitor para trocar estatísticas" STR_NEARBY_STATS_SYNCING: "Enviando e recebendo estatísticas" STR_NEARBY_STATS_SYNCED: "Os dois leitores foram sincronizados" STR_NEARBY_STATS_SIMULATOR_UNAVAILABLE: "A sincronização próxima de estatísticas não está disponível no simulador" -STR_TIME_LEFT: "Tempo restante" -STR_TIME_LEFT_CALCULATING: "Calculando..." -STR_BATTERY: "Bateria" -STR_BITTER: "Bitter" -STR_BOOK: "Livro" -STR_BOOK_PROGRESS_PERCENTAGE: "Porcentagem de progresso do livro" -STR_BOTTOM: "Inferior" -STR_CHANGE_FONT: "Alterar fonte" -STR_CHANGE_FONT_SIZE: "Alterar tamanho da fonte" -STR_CHAPTER: "Capítulo" -STR_CHAPTER_PAGE_COUNT: "Contagem de páginas do capítulo" -STR_CHAPTER_SKIP_OPT: "Pular capítulo" -STR_CHAREINK: "ChareInk" -STR_CLOCK: "Mostrar relógio" -STR_CLOCK_FORMAT: "Formato do relógio" -STR_CLOCK_FORMAT_12H: "12 horas" -STR_CLOCK_FORMAT_24H: "24 horas" -STR_CLOCK_SYNCED: "Relógio sincronizado" -STR_CLOCK_SYNC_NOW: "Sincronize data/hora agora" -STR_CLOCK_UTC_OFFSET: "Deslocamento UTC do relógio" -STR_CUSTOMISE_STATUS_BAR: "Personalizar barra de status" -STR_DISABLED: "Desativado" -STR_DISPLAY_SLEEP_SCREEN: "Tela de suspensão" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" -STR_FORCE_PARAGRAPH_INDENTS: "Forçar recuos de parágrafo" -STR_FORCE_REFRESH: "Atualizar tela" -STR_FRONT_BUTTONS: "Botões frontais" -STR_GUIDE_READING: "Pontos guia" -STR_HIDE: "Esconder" -STR_HUGE: "Enorme" -STR_IMAGES: "Imagens" -STR_IMAGES_DISPLAY: "Exibição" -STR_IMAGES_PLACEHOLDER: "Espaço reservado" -STR_IMAGES_SUPPRESS: "Suprimir" -STR_ITTY_BITTY: "Itty Bitty" -STR_LEXEND_DECA: "Lexend Deca" -STR_LONG_PRESS_ACTION: "Ação de toque longo" -STR_MANAGE_FONTS: "Gerenciar fontes" -STR_MARK_FINISHED: "Marcar como concluído" -STR_POWER_BUTTON: "Botão liga/desliga" -STR_PROGRESS_BAR: "Barra de progresso" -STR_PROGRESS_BAR_MEDIUM: "Médio" -STR_PROGRESS_BAR_THICK: "Grosso" -STR_PROGRESS_BAR_THICKNESS: "Espessura da barra de progresso" -STR_PROGRESS_BAR_THIN: "Fino" -STR_READER_BOOK_STYLING: "Estilo de livro" -STR_READER_FONT_OPTIONS: "Opções de fonte" -STR_READER_PAGE_LAYOUT: "Layout da página" -STR_READER_READING_AIDS: "Auxiliares de leitura" -STR_READER_UI: "Interface do leitor" -STR_EPUB_RENDER_MODE: "Modo de renderização EPUB" -STR_RENDER_MODE_CROSSINK_DEFAULT: "Padrão CrossInk" -STR_RENDER_MODE_BALANCED: "Equilibrado" -STR_RENDER_MODE_LIGHT: "Leve" -STR_BALANCED_MODE: "Modo equilibrado" -STR_LIGHT_MODE: "Modo leve" -STR_SAFE_MODE: "Safe Mode" -STR_SD_FIRMWARE_UPDATE: "Atualização de firmware do cartão SD" -STR_SD_FONT_SIZE_RANGE: "Baixar faixa de tamanho de fonte" -STR_SHOW_HIDDEN_FILES: "Mostrar arquivos ocultos" -STR_HIDE_FILE_EXTENSION: "Ocultar extensão" -STR_FILE_BROWSER_DISPLAY: "Exibição do navegador" -STR_FILE_BROWSER_DISPLAY_1_LINE: "1 linha" -STR_FILE_BROWSER_DISPLAY_2_LINES: "2 linhas" -STR_SIDE_BTN_LONG_PRESS: "Ação de toque longo" -STR_SIDE_BUTTONS: "Botões laterais" -STR_SLEEP_COVER_FILTER_SHORT: "Filtro da capa" -STR_SLEEP_COVER_MODE_SHORT: "Modo de capa" -STR_SLEEP_SCREEN_WALLPAPER: "Papel de parede" -STR_SYSTEM_DEVICE: "Dispositivo" -STR_DEVICE_NAME: "Nome do dispositivo" -STR_SYSTEM_FILES_CACHE: "Arquivos e cache" -STR_TEENSY: "Teensy" -STR_THEME_LYRA: "Lyra" -STR_THEME_LYRA_CAROUSEL: "Lyra Carousel" -STR_THEME_MINIMAL: "Minimal" -STR_TINY: "Minúsculo" -STR_TITLE: "Título" -STR_TOGGLE_BIONIC_READING: "Leitura Biônica" -STR_TOGGLE_BOOKMARK: "Alternar marcador" -STR_TOGGLE_GUIDE_DOTS: "Pontos guia" -STR_TOP: "Superior" -STR_XTC_STATUS_BAR: "Barra de status XTC" - -STR_NEARBY_STATS_VERSION_MISMATCH: "Versões incompatíveis. Estatísticas não sincronizadas" -STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido das notas" -STR_DELETE_BOOKMARKS: "Limpar lista de marcadores" -STR_STATS_TOTAL_READING_TIME_LBL: "Tempo total de leitura" -STR_NEARBY_POSITION_SYNC: "Sincronização próxima de posição" -STR_NEARBY_POSITION_VERSION_MISMATCH: "Versões incompatíveis. Posição não sincronizada" -STR_NEARBY_POSITION_BOOK_MISMATCH: "Livro diferente. Posição não sincronizada" STR_NEARBY_POSITION_READY: "Pronto para compartilhar esta posição" STR_NEARBY_POSITION_SHARE_BUTTON: "Compartilhar" STR_NEARBY_POSITION_READY_HINT: "Pressione Compartilhar no X3/X4 na página correta" STR_NEARBY_POSITION_SCANNING: "Procurando leitor para trocar posição" STR_NEARBY_POSITION_SHARING: "Compartilhando esta posição" STR_NEARBY_POSITION_WAITING: "Aguardando posição compartilhada" -STR_NEARBY_POSITION_APPLYING: "Enviando posição" +STR_NEARBY_POSITION_APPLYING: "Aplicando posição" STR_NEARBY_POSITION_SHARED: "Posição compartilhada" STR_NEARBY_POSITION_SYNCED: "Posição do livro sincronizada" STR_NEARBY_POSITION_FOUND: "Posição próxima encontrada!" @@ -454,3 +655,6 @@ STR_NEARBY_POSITION_NO_READER: "Nenhum leitor próximo encontrado" STR_NEARBY_LABEL: "Próximo:" STR_APPLY_NEARBY_POSITION: "Aplicar posição próxima" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "A sincronização próxima de posição não está disponível no simulador" +STR_ADD_HIDDEN_NETWORK: "Adicionar rede oculta..." +STR_ENTER_WIFI_SSID: "Insira o nome da rede (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Validando..." diff --git a/lib/I18n/translations/portuguese-PT.yaml b/lib/I18n/translations/portuguese-PT.yaml new file mode 100644 index 0000000000..4f80261062 --- /dev/null +++ b/lib/I18n/translations/portuguese-PT.yaml @@ -0,0 +1,760 @@ +_language_name: "Português (Portugal)" +_language_code: "PT2" +_order: "26" +STR_CROSSINK: "CrossInk" +STR_BOOTING: "A INICIAR" +STR_SLEEPING: "EM REPOUSO" +STR_ENTERING_SLEEP: "A entrar em repouso" +STR_BROWSE_FILES: "Explorar ficheiros" +STR_BROWSE: "Explorar" +STR_FILE_TRANSFER: "Transferência de ficheiros" +STR_SETTINGS_TITLE: "Configurações" +STR_SETTINGS_SHORT: "Config." +STR_READ: "Ler" +STR_MENU: "Menu" +STR_CONTINUE_READING: "Continuar a ler" +STR_NO_OPEN_BOOK: "Nenhum livro aberto" +STR_START_READING: "Comece a ler abaixo" +STR_NO_FILES_FOUND: "Nenhum ficheiro encontrado" +STR_SELECT_CHAPTER: "Escolher capítulo" +STR_NO_CHAPTERS: "Sem capítulos" +STR_END_OF_BOOK: "Fim do livro" +STR_EMPTY_CHAPTER: "Capítulo vazio" +STR_INDEXING: "A indexar" +STR_INDEX_FAILED: "Falha ao indexar - livro inválido" +STR_MEMORY_ERROR: "Erro de memória" +STR_PAGE_LOAD_ERROR: "Erro a carregar a página" +STR_EMPTY_FILE: "Ficheiro vazio" +STR_OUT_OF_BOUNDS: "Fora dos limites" +STR_LOADING: "A carregar..." +STR_LOADING_POPUP: "A carregar" +STR_WIFI_NETWORKS: "Redes Wi-Fi" +STR_NO_NETWORKS: "Nenhuma rede encontrada" +STR_NETWORKS_FOUND: "%zu redes encontradas" +STR_SCANNING: "A procurar..." +STR_FINDING_SAVED_WIFI: "A procurar Wi-Fi guardado..." +STR_CONNECTING: "A ligar..." +STR_CONNECTING_SAVED_WIFI: "A ligar ao Wi-Fi guardado..." +STR_SHOW_NETWORKS: "Mostrar" +STR_CONNECTED: "Ligado!" +STR_CONNECTION_FAILED: "Falha na ligação" +STR_FORGET_NETWORK: "Esquecer rede?" +STR_SAVE_PASSWORD: "Guardar palavra-passe para a próxima vez?" +STR_PRESS_OK_SCAN: "Prima OK para procurar novamente" +STR_JOIN_NETWORK: "Aceder a uma rede" +STR_CREATE_HOTSPOT: "Criar ponto de acesso" +STR_NEARBY_DEVICE: "Dispositivo próximo" +STR_NEARBY_STATS_SYNC: "Sincronizar estatísticas" +STR_NEARBY_POSITION_SYNC: "Sincronização próxima de posição" +STR_NEARBY_BOOK_TRANSFER: "Transferência de ficheiros próxima" +STR_SEND_NEARBY_BOOK: "Enviar para dispositivo próximo" +STR_RECEIVE_NEARBY_BOOK: "Receber ficheiro" +STR_RECEIVE_NEARBY_BOOK_DESC: "Receber um ficheiro de outro leitor" +STR_NEARBY_RECEIVE_FOLDER: "Pasta de receção próxima" +STR_SELECT_RECEIVE_FOLDER: "Selecionar pasta de receção" +STR_START_RECEIVING: "Começar a receber" +STR_CHANGE_FOLDER: "Alterar pasta" +STR_ACCEPT: "Aceitar" +STR_OK: "OK" +STR_REPLACE: "Substituir" +STR_KEEP_BOTH: "Manter ambos" +STR_NEARBY_TRANSFER_LISTENING: "À espera de um remetente próximo" +STR_NEARBY_TRANSFER_DISCOVERING: "À procura de dispositivos recetores" +STR_NEARBY_TRANSFER_WAITING_APPROVAL: "À espera de aprovação do recetor" +STR_NEARBY_TRANSFER_INCOMING: "Ficheiro a receber" +STR_NEARBY_TRANSFER_SIZE: "%lu bytes" +STR_NEARBY_TRANSFER_VALIDATING: "A validar..." +STR_NEARBY_TRANSFER_SENDING: "A enviar ficheiro" +STR_NEARBY_TRANSFER_RECEIVING: "A receber ficheiro" +STR_NEARBY_TRANSFER_SENT: "Ficheiro enviado" +STR_NEARBY_TRANSFER_RECEIVED: "Ficheiro recebido" +STR_NEARBY_TRANSFER_FAILED: "Falha na transferência" +STR_NEARBY_TRANSFER_UNSUPPORTED: "Tipo de ficheiro não suportado" +STR_NEARBY_TRANSFER_SOURCE_FAILED: "Não foi possível ler o ficheiro de origem" +STR_NEARBY_TRANSFER_RADIO_FAILED: "Não foi possível iniciar a transferência próxima" +STR_NEARBY_TRANSFER_REJECTED: "O recetor recusou a transferência" +STR_NEARBY_TRANSFER_VERIFY_FAILED: "O ficheiro recebido não passou na verificação" +STR_NEARBY_TRANSFER_CANCELLED: "Transferência cancelada" +STR_NEARBY_TRANSFER_WRITE_FAILED: "Não foi possível escrever o ficheiro recebido" +STR_NEARBY_TRANSFER_NO_SPACE: "Espaço insuficiente no cartão SD" +STR_NEARBY_TRANSFER_NAME_FAILED: "Não foi possível escolher um nome de ficheiro disponível" +STR_NEARBY_TRANSFER_REPLACE_FAILED: "Não foi possível substituir o ficheiro existente com segurança" +STR_NEARBY_TRANSFER_TIMEOUT: "O outro dispositivo parou de responder" +STR_NEARBY_TRANSFER_PROGRESS_SAVE_FAILED: "Não foi possível guardar o progresso de leitura" +STR_JOIN_DESC: "Ligue-se a uma rede Wi-Fi existente" +STR_HOTSPOT_DESC: "Crie uma rede Wi-Fi para outros dispositivos entrarem" +STR_NEARBY_STATS_SYNC_DESC: "Sincronizar estatísticas de leitura entre leitores" +STR_NEARBY_STATS_VERSION_MISMATCH: "Versões incompatíveis. Estatísticas não sincronizadas" +STR_NEARBY_POSITION_VERSION_MISMATCH: "Versões incompatíveis. Posição não sincronizada" +STR_NEARBY_POSITION_BOOK_MISMATCH: "Livro diferente. Posição não sincronizada" +STR_STARTING_HOTSPOT: "A iniciar ponto de acesso..." +STR_HOTSPOT_MODE: "Modo ponto de acesso" +STR_CONNECT_WIFI_HINT: "Ligue o seu dispositivo a esta rede Wi-Fi" +STR_OPEN_URL_HINT: "Abra este URL no seu navegador" +STR_OR_HTTP_PREFIX: "ou http://" +STR_SCAN_QR_HINT: "ou leia o código QR com o seu telemóvel:" +STR_CALIBRE_WIRELESS: "Calibre sem fios" +STR_CALIBRE_WEB_URL: "URL OPDS" +STR_NETWORK_LEGEND: "* = Encriptada | + = Guardada" +STR_MAC_ADDRESS: "Endereço MAC:" +STR_CHECKING_WIFI: "A verificar Wi-Fi..." +STR_ENTER_WIFI_PASSWORD: "Introduza a palavra-passe do Wi-Fi" +STR_ADD_HIDDEN_NETWORK: "Adicionar rede oculta..." +STR_ENTER_WIFI_SSID: "Insira o nome da rede (SSID)" +STR_TO_PREFIX: "para " +STR_CALIBRE_RECEIVING: "A receber: " +STR_CALIBRE_RECEIVED: "Recebido: " +STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader" +STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi" +STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar para o dispositivo\"" +STR_CALIBRE_INSTRUCTION_4: "Mantenha este ecrã aberto durante o envio" +STR_CAT_DISPLAY: "Ecrã" +STR_CAT_READER: "Leitor" +STR_CAT_CONTROLS: "Controlos" +STR_CAT_SYSTEM: "Sistema" +STR_SLEEP_SCREEN: "Ecrã de repouso" +STR_SLEEP_SCREEN_WALLPAPER: "Papel de parede" +STR_QUICK_RESUME_TIMEOUT: "Reinício rápido após tempo limite" +STR_AFTER_TIMEOUT: "Após tempo limite" +STR_SLEEP_COVER_MODE: "Modo da capa do ecrã de repouso" +STR_SLEEP_COVER_MODE_SHORT: "Modo da capa" +STR_HIDE_BATTERY: "Ocultar % da bateria" +STR_DISPLAY_THEME_HOME: "Tema e início" +STR_DISPLAY_REFRESH: "Atualização" +STR_DISPLAY_SLEEP_SCREEN: "Ecrã de repouso" +STR_DISPLAY_STATUS: "Estado" +STR_EXTRA_SPACING: "Espaçamento extra entre parágrafos" +STR_FORCE_PARAGRAPH_INDENTS: "Forçar recuos de parágrafo" +STR_TEXT_AA: "Suavização de texto" +STR_READER_DARK_MODE: "Modo escuro" +STR_IMAGES: "Imagens" +STR_IMAGES_DISPLAY: "Exibição" +STR_IMAGES_PLACEHOLDER: "Espaço reservado" +STR_IMAGES_SUPPRESS: "Suprimir" +STR_TOUCH_READER_CONTROLS: "Controlos táteis do leitor" +STR_DISABLE_TOUCHSCREEN: "Desativar ecrã tátil" +STR_ON: "LIG." +STR_OFF: "DESL." +STR_EOB_HOME: "Início" +STR_EOB_CONTINUE_WITH: "Continuar com" +STR_SHORT_PWR_BTN: "Ação de toque breve" +STR_ORIENTATION: "Orientação" +STR_SIDE_BTN_LAYOUT: "Esquema" +STR_ORIENTATION_AWARE: "Sensível à orientação" +STR_NAV_BUTTONS: "Botões de navegação" +STR_ALL_BUTTONS: "Todos os botões" +STR_LONG_PRESS_SKIP: "Saltar capítulo com toque longo" +STR_SIDE_BTN_LONG_PRESS: "Ação de toque longo" +STR_CHAPTER_SKIP_OPT: "Saltar capítulo" +STR_CHANGE_FONT_SIZE: "Alterar tamanho de letra" +STR_LONG_PRESS_BEHAVIOR: "Comportamento de toque longo" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais" +STR_LONG_PRESS_BEHAVIOR_OFF: "Desligado" +STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo" +STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação" +STR_FONT_FAMILY: "Família do tipo de letra" +STR_DICTIONARY_FONT: "Tipo de letra do dicionário" +STR_USE_READER_FONT: "Usar tipo de letra do leitor" +STR_DICTIONARY_FONT_SIZE: "Tamanho do tipo de letra do dicionário" +STR_USE_READER_FONT_SIZE: "Usar tamanho do tipo de letra do leitor" +STR_FONT_SIZE: "Tamanho do tipo de letra" +STR_SD_FONT_SIZE_RANGE: "Descarregar intervalo de tamanho do tipo de letra" +STR_READER_FONT_OPTIONS: "Opções de tipo de letra" +STR_READER_PAGE_LAYOUT: "Layout da página" +STR_READER_BOOK_STYLING: "Estilo de livro" +STR_READER_READING_AIDS: "Auxiliares de leitura" +STR_READER_UI: "Interface do leitor" +STR_EPUB_RENDER_MODE: "Modo de renderização EPUB" +STR_RENDER_MODE_CROSSINK_DEFAULT: "Padrão CrossInk" +STR_RENDER_MODE_BALANCED: "Equilibrado" +STR_RENDER_MODE_LIGHT: "Leve" +STR_INDEXING_METHOD: "Método de indexação" +STR_INDEXING_INCREMENTAL: "Incremental" +STR_INDEXING_FULL_SECTION: "Secção completa" +STR_BALANCED_MODE: "Modo equilibrado" +STR_LIGHT_MODE: "Modo leve" +STR_SAFE_MODE: "Modo seguro" +STR_LINE_SPACING: "Espaçamento entre linhas" +STR_WORD_SPACING: "Espaçamento entre palavras" +STR_LEVEL_1: "1" +STR_LEVEL_2: "2" +STR_LEVEL_3: "3" +STR_LEVEL_4: "4" +STR_SCREEN_MARGIN: "Margem do ecrã" +STR_PUBLISHER_PAGE_NUMBERS: "Números de página da editora" +STR_PARA_ALIGNMENT: "Alinhamento do parágrafo" +STR_LONG_PRESS_MENU: "Menu de toque longo" +STR_FONT_PREVIEW_TEXT: "Zé, o fidalgo de Alfama, pôs kiwi, whisky e xadrez no fado, que belo pão com chouriço!" +STR_HYPHENATION: "Hifenização" +STR_DICTIONARY: "Dicionário" +STR_BOOK_DICTIONARY: "Dicionário do livro" +STR_DICT_NONE: "Nenhum" +STR_DICT_USE_GLOBAL: "Usar global" +STR_DICT_NONE_FOUND: "Nenhum dicionário encontrado" +STR_DICT_INFO: "Informações do dicionário" +STR_DICT_NO_METADATA: "Sem metadados disponíveis" +STR_DICT_VIEW_RAW: "Ver conteúdo bruto" +STR_DICT_NO_DICT_SET: "Nenhum dicionário definido" +STR_DICT_NO_WORD: "Sem palavra" +STR_DICT_LOOKING_UP: "A procurar..." +STR_DICT_NOT_FOUND: "Não encontrado" +STR_DICT_SEARCH_ALT_FORMS: "Pesquisar formas alternativas?" +STR_DICT_DID_YOU_MEAN: "Quis dizer?" +STR_LOOKUP: "Procurar palavra" +STR_LOOKUP_SHORT: "Procurar" +STR_DICT_SWITCH: "Mudar" +STR_SWITCH_DICTIONARY: "Mudar dicionário" +STR_LOOKUP_HISTORY: "Histórico de pesquisas" +STR_LOOKUP_HISTORY_EMPTY: "Ainda nenhuma palavra pesquisada" +STR_LOOKUP_HISTORY_DELETE_HINT: "Manter premido para eliminar" +STR_TIME_TO_SLEEP: "Tempo até adormecer" +STR_SHOW_HIDDEN_FILES: "Mostrar ficheiros ocultos" +STR_HIDE_FILE_EXTENSION: "Ocultar extensão de ficheiro" +STR_FILE_BROWSER_DISPLAY: "Exibição do gestor de ficheiros" +STR_FILE_BROWSER_DISPLAY_1_LINE: "1 linha" +STR_FILE_BROWSER_DISPLAY_2_LINES: "2 linhas" +STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes" +STR_MOVE_FINISHED_TO_READ: "Mover livros lidos para a pasta Read" +STR_AUTO_BACKUP_STATS: "Backups automáticos" +STR_BACKUP_NOW: "Fazer backup agora" +STR_BACKUP_STATS_CONFIRM: "Guardar uma cópia em /.crossink-stats-backup/?" +STR_BACKUP_STATS_DONE: "Backup guardado" +STR_BACKUP_STATS_FAILED: "Falha no backup" +STR_SYSTEM_DEVICE: "Dispositivo" +STR_DEVICE_NAME: "Nome do dispositivo" +STR_SYSTEM_FILES_CACHE: "Ficheiros e cache" +STR_ALL_TIME_STATS: "Estatísticas de todo o tempo" +STR_IDLE_TIME_THRESHOLD: "Limite de tempo de inatividade" +STR_SECONDS_VALUE_FORMAT: "%u s" +STR_IDLE_TIME_THRESHOLD_STEP_HINT: "Esq./Dir.: 10s Cima/Baixo: 1m" +STR_RESET_ALL_TIME_STATS: "Repor estatísticas de todo o tempo" +STR_RESET_ALL_TIME_STATS_CONFIRM: "Limpar as estatísticas de todo o tempo deste leitor? Os backups são mantidos." +STR_SYSTEM_NETWORK_SYNC: "Rede e sincronização" +STR_SYSTEM_UPDATES: "Atualizações" +STR_REFRESH_FREQ: "Frequência de atualização" +STR_KOREADER_SYNC: "Sincronização KOReader" +STR_CHECK_UPDATES: "Verificar atualizações" +STR_LANGUAGE: "Idioma" +STR_CLEAR_READING_CACHE: "Limpar cache de leitura" +STR_USERNAME: "Nome de utilizador" +STR_PASSWORD: "Palavra-passe" +STR_SYNC_SERVER_URL: "URL do servidor de sincronização" +STR_DOCUMENT_MATCHING: "Correspondência de documento" +STR_SEND_METADATA: "Enviar metadados do documento" +STR_SYNC_BEHAVIOR: "Comportamento de sincronização" +STR_ASK_EVERY_TIME: "Perguntar sempre" +STR_SMART_SYNC: "Sincronização inteligente" +STR_AUTHENTICATE: "Autenticar" +STR_SIGN_UP: "Registar" +STR_CREATING_ACCOUNT: "A criar conta..." +STR_ACCOUNT_CREATED: "Conta criada" +STR_SIGNUP_FAILED: "Falha no registo" +STR_USERNAME_TAKEN: "O nome de utilizador já está registado" +STR_KOREADER_USERNAME: "Utilizador do KOReader" +STR_KOREADER_PASSWORD: "Palavra-passe do KOReader" +STR_FILENAME: "Nome do ficheiro" +STR_AUTHOR_TITLE: "Autor - Título" +STR_TITLE_AUTHOR: "Título - Autor" +STR_BINARY: "Binário" +STR_SET_CREDENTIALS_FIRST: "Defina as credenciais primeiro" +STR_WIFI_CONN_FAILED: "Falha na ligação Wi-Fi" +STR_AUTHENTICATING: "A autenticar..." +STR_AUTH_SUCCESS: "Autenticado com sucesso!" +STR_KOREADER_AUTH: "Autenticação KOReader" +STR_SYNC_READY: "A sincronização KOReader está pronta a usar" +STR_AUTH_FAILED: "Falha na autenticação" +STR_DONE: "Feito" +STR_CLEAR_CACHE_WARNING_1: "Isto irá limpar todos os dados de livros em cache." +STR_CLEAR_CACHE_WARNING_2: "Todo o progresso de leitura será perdido!" +STR_CLEAR_CACHE_WARNING_3: "Os livros terão de ser reindexados" +STR_CLEAR_CACHE_WARNING_4: "quando forem abertos novamente." +STR_CLEARING_CACHE: "A limpar a cache..." +STR_CACHE_CLEARED: "Cache limpa" +STR_ITEMS_REMOVED: "itens removidos" +STR_FAILED_LOWER: "falhou" +STR_CLEAR_CACHE_FAILED: "Falha ao limpar a cache" +STR_CHECK_SERIAL_OUTPUT: "Verifique a saída de série para obter detalhes" +STR_DARK: "Escuro" +STR_LIGHT: "Claro" +STR_CUSTOM: "Personalizado" +STR_COVER: "Capa" +STR_NONE_OPT: "Nenhum" +STR_FIT: "Ajustar" +STR_CROP: "Recortar" +STR_NEVER: "Nunca" +STR_IN_READER: "No leitor" +STR_ALWAYS: "Sempre" +STR_IGNORE: "Ignorar" +STR_SLEEP: "Repouso" +STR_PAGE_TURN: "Virar página" +STR_FORCE_REFRESH: "Atualizar o ecrã" +STR_CHANGE_FONT: "Alterar o tipo de letra" +STR_PORTRAIT: "Retrato" +STR_LANDSCAPE_CW: "Paisagem (horário)" +STR_INVERTED: "Invertido" +STR_ORIENTATION_INVERTED: "Retrato 180°" +STR_LANDSCAPE_CCW: "Paisagem (anti-horário)" +STR_PREV_NEXT: "Ant/Próx" +STR_NEXT_PREV: "Próx/Ant" +STR_DISABLED: "Desativado" +STR_NEXT_NEXT: "Próx/Próx" +STR_LEXEND_DECA: "Lexend Deca" +STR_BITTER: "Bitter" +STR_CHAREINK: "ChareInk" +STR_ATKINSON_HL: "Atkinson Hyperlegible" +STR_COMPACT: "Compacto" +STR_TINY: "Minúsculo" +STR_TEENSY: "Teensy" +STR_ITTY_BITTY: "Itty Bitty" +STR_SMALL: "Pequeno" +STR_MEDIUM: "Médio" +STR_LARGE: "Grande" +STR_X_LARGE: "Extra grande" +STR_HUGE: "Enorme" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" +STR_FONT_RANGE_ALL: "8-20pt" +STR_TIGHT: "Apertado" +STR_NORMAL: "Normal" +STR_WIDE: "Largo" +STR_JUSTIFY: "Justificar" +STR_ALIGN_LEFT: "Esquerda" +STR_CENTER: "Centrar" +STR_ALIGN_RIGHT: "Direita" +STR_PAGES_1: "1 página" +STR_PAGES_5: "5 páginas" +STR_PAGES_10: "10 páginas" +STR_PAGES_15: "15 páginas" +STR_PAGES_30: "30 páginas" +STR_UPDATE: "Atualizar" +STR_CHECKING_UPDATE: "A verificar atualização..." +STR_NEW_UPDATE: "Nova atualização disponível!" +STR_CURRENT_VERSION: "Versão atual: " +STR_NEW_VERSION: "Nova versão: " +STR_UPDATING: "A atualizar..." +STR_NO_UPDATE: "Nenhuma atualização disponível" +STR_UPDATE_FAILED: "Falha na atualização" +STR_UPDATE_HASH_MISMATCH: "O ficheiro de atualização não corresponde" +STR_UPDATE_COMPLETE: "Atualização concluída" +STR_POWER_ON_HINT: "Pressione e mantenha premido o botão de energia para ligar novamente" +STR_RESTARTING_HINT: "A reiniciar... Se o dispositivo não reiniciar, mantenha premido o botão de energia durante alguns segundos." +STR_NO_ENTRIES: "Nenhuma entrada encontrada" +STR_DOWNLOADING: "A descarregar..." +STR_DOWNLOAD_FAILED: "Falha na descarga" +STR_ERROR_MSG: "Erro:" +STR_UNNAMED: "Sem nome" +STR_NO_SERVER_URL: "Nenhum URL de servidor configurado" +STR_FETCH_FEED_FAILED: "Falha ao obter o feed" +STR_PARSE_FEED_FAILED: "Falha ao analisar o feed" +STR_OPDS_FEED_BUFFER_MEMORY_ERROR: "Não foi possível alocar memória para o buffer" +STR_EPUB_CHAPTER_INCOMPLETE_TITLE: "Capítulo incompleto" +STR_EPUB_CHAPTER_INCOMPLETE_BODY: "Este capítulo contém marcação EPUB mal formatada. O CrossInk parou na parte ilegível, pelo que parte do conteúdo poderá estar em falta." +STR_NEXT_PAGE: "Próxima página »" +STR_PREV_PAGE: "« Página anterior" +STR_NETWORK_PREFIX: "Rede: " +STR_IP_ADDRESS_PREFIX: "Endereço IP: " +STR_ERROR_GENERAL_FAILURE: "Erro: falha geral" +STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada" +STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite da ligação" +STR_SD_CARD: "Cartão SD" +STR_TOGGLE_HIDDEN_FILES_HINT: "Mantenha o botão HOME premido para alternar os ficheiros ocultos" +STR_BACK: "« Voltar" +STR_EXIT: "« Sair" +STR_HOME: "« Início" +STR_SELECT: "Selecionar" +STR_SELECTED: "Selecionado" +STR_TOGGLE: "Alternar" +STR_CONFIRM: "Confirmar" +STR_CANCEL: "Cancelar" +STR_CONNECT: "Ligar" +STR_OPEN: "Abrir" +STR_DOWNLOAD: "Descarregar" +STR_RETRY: "Tentar novamente" +STR_TAP_TO_RETRY: "Toque para tentar novamente" +STR_YES: "Sim" +STR_NO: "Não" +STR_SHOW: "Mostrar" +STR_HIDE: "Ocultar" +STR_STATE_ON: "LIG." +STR_STATE_OFF: "DESL." +STR_NOT_SET: "Não definido" +STR_DIR_LEFT: "Esquerda" +STR_DIR_RIGHT: "Direita" +STR_DIR_UP: "Cima" +STR_DIR_DOWN: "Baixo" +STR_OK_BUTTON: "OK" +STR_KEY_SHIFT: "Shift" +STR_KEY_MODE_SYMBOLS: "?123" +STR_KEY_MODE_ABC: "abc" +STR_SLEEP_COVER_FILTER: "Filtro da capa do ecrã de repouso" +STR_SLEEP_COVER_FILTER_SHORT: "Filtro da capa" +STR_FILTER_CONTRAST: "Contraste" +STR_CUSTOMISE_STATUS_BAR: "Personalizar barra de estado" +STR_CHAPTER_PAGE_COUNT: "Contagem de páginas do capítulo" +STR_STABLE_PAGE_NUMBERS: "Números de página estáveis" +STR_BOOK_PROGRESS_PERCENTAGE: "Percentagem de progresso do livro" +STR_PROGRESS_BAR: "Barra de progresso" +STR_PROGRESS_BAR_THICKNESS: "Espessura da barra de progresso" +STR_PROGRESS_BAR_THIN: "Fino" +STR_PROGRESS_BAR_MEDIUM: "Médio" +STR_PROGRESS_BAR_THICK: "Grosso" +STR_BOOK: "Livro" +STR_CHAPTER: "Capítulo" +STR_EXAMPLE_CHAPTER: "Capítulo 21" +STR_EXAMPLE_BOOK: "Título do livro" +STR_PREVIEW: "Pré-visualização" +STR_TITLE: "Título" +STR_TIME_LEFT: "Tempo restante" +STR_TIME_LEFT_CALCULATING: "A calcular..." +STR_BATTERY: "Bateria" +STR_XTC_STATUS_BAR: "Barra de estado XTC" +STR_BOTTOM: "Inferior" +STR_TOP: "Superior" +STR_CLOCK: "Mostrar relógio" +STR_HIDE_CLOCK: "Ocultar relógio" +STR_CLOCK_UTC_OFFSET: "Desvio UTC do relógio" +STR_CLOCK_FORMAT: "Formato do relógio" +STR_CLOCK_FORMAT_24H: "24 horas" +STR_CLOCK_FORMAT_12H: "12 horas" +STR_DATE_FORMAT: "Formato de data" +STR_DATE_FORMAT_MONTH_DAY_YEAR_LONG: "31 de dez. de 2026" +STR_DATE_FORMAT_DAY_MONTH_YEAR_LONG: "31 de dez. de 2026" +STR_DATE_FORMAT_MONTH_DAY_YEAR_NUMERIC: "31/12/2026" +STR_DATE_FORMAT_DAY_MONTH_YEAR_NUMERIC: "31/12/2026" +STR_DATE_FORMAT_YEAR_MONTH_DAY_NUMERIC: "2026/12/31" +STR_DATE_FORMAT_MONTH_DAY_NUMERIC: "31/12" +STR_DATE_FORMAT_DAY_MONTH_NUMERIC: "31/12" +STR_DATE_FORMAT_MONTH_DAY_LONG: "31 de dezembro" +STR_DATE_FORMAT_DAY_MONTH_LONG: "31 de dezembro" +STR_DATE_SEPARATOR: "Separador de data" +STR_DATE_SEPARATOR_PERIOD: "Ponto (.)" +STR_DATE_SEPARATOR_HYPHEN: "Hífen (-)" +STR_DATE_SEPARATOR_SLASH: "Barra (/)" +STR_CURRENT_TIME: "Hora atual:" +STR_NEXT_FIELD: "Seguinte" +STR_CLOCK_SYNC: "Sincronizar relógio" +STR_CLOCK_SYNC_NOW: "Sincronizar data/hora" +STR_CLOCK_SYNCING: "A sincronizar a partir do NTP..." +STR_CLOCK_SYNC_OK: "Relógio sincronizado" +STR_CLOCK_SYNC_FAIL: "Falha na sincronização" +STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi não ligado" +STR_CLOCK_SYNC_NO_WIFI_HINT: "Ligue primeiro ao Wi-Fi e tente novamente." +STR_CLOCK_SYNCED: "Relógio sincronizado" +STR_UI_THEME: "Tema da interface" +STR_UI_SCALE: "Escala da interface" +STR_THEME_CLASSIC: "Clássico" +STR_THEME_LYRA: "Lyra" +STR_THEME_ROUNDEDRAFF: "RoundedRaff" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" +STR_THEME_LYRA_CAROUSEL: "Lyra Carousel" +STR_THEME_MINIMAL: "Minimal" +STR_THEME_DASHBOARD: "Painel" +STR_THEME_MINIMAL_STATS: "Minimal Stats" +STR_QUICK_RESUME: "Retoma rápida" +STR_RECENT_BOOKS_VIEW: "Vista de livros recentes" +STR_LIST_VIEW: "Lista" +STR_GRID_VIEW: "Grelha" +STR_SUNLIGHT_FADING_FIX: "Correção de desvanecimento à luz solar" +STR_REMAP_FRONT_BUTTONS: "Remapear botões" +STR_REMAP_FRONT_BUTTONS_READER: "Remapear botões (leitor)" +STR_OPDS_BROWSER: "Navegador OPDS" +STR_SEARCH: "Pesquisar" +STR_COVER_CUSTOM: "Capa + personalizado" +STR_PAGE_OVERLAY: "Sobreposição da página" +STR_RECENTS: "Recentes" +STR_MENU_RECENT_BOOKS: "Livros recentes" +STR_REMOVE_FROM_RECENTS_ACTION: "Remover dos livros recentes" +STR_REMOVE_FROM_RECENTS: "Remover dos livros recentes?" +STR_NO_RECENT_BOOKS: "Sem livros recentes" +STR_CALIBRE_DESC: "Utilizar transferências de dispositivos sem fios do Calibre" +STR_FORGET_AND_REMOVE: "Esquecer a rede e remover a palavra-passe guardada?" +STR_FORGET_BUTTON: "Esquecer" +STR_CALIBRE_STARTING: "A iniciar o Calibre..." +STR_CALIBRE_SETUP: "Configuração" +STR_CALIBRE_STATUS: "Estado" +STR_CLEAR_BUTTON: "Limpar" +STR_DEFAULT_VALUE: "Predefinição" +STR_REMAP_PROMPT: "Pressione um botão frontal para cada função" +STR_UNASSIGNED: "Não atribuído" +STR_ALREADY_ASSIGNED: "Já atribuído" +STR_REMAP_RESET_HINT: "Botão lateral Cima: Repor o esquema predefinido" +STR_REMAP_CANCEL_HINT: "Botão lateral Baixo: Cancelar o remapeamento" +STR_HW_BACK_LABEL: "Voltar (1.º botão)" +STR_HW_CONFIRM_LABEL: "Confirmar (2.º botão)" +STR_HW_LEFT_LABEL: "Esquerda (3.º botão)" +STR_HW_RIGHT_LABEL: "Direita (4.º botão)" +STR_GO_TO_PERCENT: "Ir para %" +STR_GO_HOME_BUTTON: "Ir para o Início" +STR_SYNC_PROGRESS: "Sincronizar progresso" +STR_DELETE_CACHE: "Eliminar cache de livros" +STR_SAVE_CLIPPING: "Criar excerto" +STR_VIEW_CLIPPINGS: "Ver excertos" +STR_CLIPPINGS: "Excertos" +STR_BOOKMARKS_AND_CLIPPINGS: "Marcadores e excertos" +STR_NO_CLIPPINGS: "Ainda sem excertos" +STR_DELETE_CLIPPINGS: "Limpar excertos" +STR_CLIPPING_SAVED: "Excerto guardado" +STR_CLIPPING_FAILED: "Não foi possível guardar o excerto" +STR_CLIPPING_LIMIT_REACHED: "Limite de excertos atingido" +STR_BOOK_CACHE_DELETED: "Cache do livro eliminada" +STR_DELETE_BOOK_STATS: "Eliminar estatísticas de leitura do livro" +STR_BOOK_STATS_DELETED: "Estatísticas de leitura eliminadas" +STR_RESET_BOOK_READER_SETTINGS: "Repor configurações de leitura do livro" +STR_BOOK_READER_SETTINGS_RESET: "Configurações de leitura do livro repostas" +STR_RESET_READING_PACE: "Repor ritmo de leitura" +STR_READING_PACE_RESET: "Ritmo de leitura reposto" +STR_DELETE: "Eliminar" +STR_PIN_AS_FAVORITE: "Fixar como favorito" +STR_UNPIN_AS_FAVORITE: "Desafixar dos favoritos" +STR_PIN_PNG_WARNING: "Fixar um PNG só é compatível com o modo de sobreposição da página" +STR_SET_AS_SLEEP_FOLDER: "Definir como pasta de repouso" +STR_USE_DEFAULT_SLEEP_FOLDERS: "Utilizar pastas de repouso predefinidas" +STR_DISPLAY_QR: "Mostrar página como QR" +STR_CHAPTER_PREFIX: "Capítulo: " +STR_PAGES_SEPARATOR: " páginas | " +STR_BOOK_PREFIX: "Livro: " +STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL" +STR_SYNCING_TIME: "A sincronizar o horário..." +STR_CALC_HASH: "A calcular o hash do documento..." +STR_HASH_FAILED: "Falha ao calcular o hash do documento" +STR_FETCH_PROGRESS: "A obter progresso remoto..." +STR_UPLOAD_PROGRESS: "A enviar progresso..." +STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada" +STR_KOREADER_SETUP_HINT: "Configure a conta do KOReader nas Definições" +STR_PROGRESS_FOUND: "Progresso encontrado!" +STR_REMOTE_LABEL: "Remoto:" +STR_LOCAL_LABEL: "Local:" +STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% no total" +STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d/%d, %.2f%% no total" +STR_DEVICE_FROM_FORMAT: " De: %s" +STR_APPLY_REMOTE: "Aplicar progresso remoto" +STR_UPLOAD_LOCAL: "Enviar progresso local" +STR_NO_REMOTE_MSG: "Nenhum progresso remoto encontrado" +STR_UPLOAD_PROMPT: "Enviar a posição atual?" +STR_UPLOAD_SUCCESS: "Progresso enviado!" +STR_ALREADY_SYNCED: "Já sincronizado" +STR_SYNC_FAILED_MSG: "Falha na sincronização" +STR_SYNC_REOPTIMIZE_REQUIRED: "Reotimize este EPUB antes da sincronização por nome de ficheiro" +STR_KOREADER_SYNC_AUTH_REJECTED: "Nome de utilizador ou palavra-passe rejeitados pelo servidor de sincronização" +STR_KOREADER_SYNC_NETWORK_ERROR: "Não foi possível aceder ao servidor de sincronização. Verifique o Wi-Fi e o URL do servidor." +STR_KOREADER_SYNC_NETWORK_REFUSED: "O servidor de sincronização recusou a ligação. Verifique o URL do servidor e se o mesmo está online." +STR_KOREADER_SYNC_NETWORK_TIMEOUT: "O servidor de sincronização esgotou o tempo limite. Verifique a intensidade do sinal Wi-Fi e tente novamente." +STR_KOREADER_SYNC_NETWORK_TLS: "Não foi possível estabelecer uma ligação segura. Verifique o URL do servidor e a configuração HTTPS." +STR_KOREADER_SYNC_HTTP_404: "Ponto de extremidade de sincronização não encontrado. Verifique se o URL do servidor aponta para o seu servidor de sincronização KOReader." +STR_KOREADER_SYNC_HTTP_STATUS_FORMAT: "O servidor de sincronização devolveu o estado HTTP %d. Tente novamente mais tarde." +STR_KOREADER_SYNC_SERVER_ERROR: "O servidor de sincronização devolveu uma resposta inesperada. Tente novamente mais tarde." +STR_KOREADER_SYNC_BAD_RESPONSE: "O servidor de sincronização respondeu com dados inválidos. Verifique o URL do servidor ou a configuração do proxy." +STR_KOREADER_SYNC_LOW_MEMORY: "Memória livre insuficiente para concluir a sincronização. Tente novamente." +STR_UNKNOWN_ERROR: "Erro desconhecido" +STR_SAVE_PROGRESS_FAILED: "Não foi possível guardar o progresso" +STR_SECTION_PREFIX: "Secção " +STR_UPLOAD: "Enviar" +STR_BOOK_S_STYLE: "Estilo do livro" +STR_EMBEDDED_STYLE: "Estilo incorporado" +STR_BIONIC_READING: "Leitura Biónica" +STR_GUIDE_READING: "Pontos Guia" +STR_OPDS_SERVER_URL: "URL do servidor OPDS" +STR_PWR_BTN_FOOTNOTE_BACK: "Regresso rápido a partir de notas de rodapé" +STR_SET_SLEEP_COVER: "Definir capa" +STR_FOOTNOTES: "Notas de rodapé" +STR_NO_FOOTNOTES: "Sem notas de rodapé nesta página" +STR_LINK: "[link]" +STR_SCREENSHOT_BUTTON: "Capturar ecrã" +STR_AUTO_TURN_ENABLED: "Viragem automática de páginas ativada: " +STR_AUTO_TURN_INTERVAL_SECONDS: "Intervalo de viragem automática de páginas (segundos)" +STR_AUTO_TURN_STEP_HINT: "Esq./Dir.: 1s Cima/Baixo: 5s" +STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" +STR_READING_STATS: "Estatísticas de leitura" +STR_EDIT: "Editar" +STR_MORE: "Mais" +STR_TRACK_READING_STATS: "Registar estatísticas de leitura" +STR_MARK_FINISHED: "Marcar como concluído" +STR_MARK_UNFINISHED: "Marcar como não concluído" +STR_MARK_FINISHED_PROMPT_TITLE: "Marcar como concluído?" +STR_MARK_FINISHED_PROMPT_BODY: "Já leu 99% deste livro" +STR_READER_OPTIONS: "Opções do livro" +STR_BOOKMARKS: "Marcadores" +STR_ADD_BOOKMARK: "Adicionar marcador" +STR_REMOVE_BOOKMARK: "Remover marcador" +STR_VIEW_BOOKMARKS: "Ver marcadores" +STR_DELETE_BOOKMARKS: "Limpar lista de marcadores" +STR_BOOKMARK_ADDED: "Marcador adicionado" +STR_BOOKMARK_REMOVED: "Marcador removido" +STR_BOOKMARK_LIMIT_REACHED: "Limite de marcadores atingido" +STR_NO_BOOKMARKS: "Ainda não existem marcadores" +STR_DELETE_BOOKMARK: "Eliminar" +STR_CONFIRM_DELETE_BOOKMARK: "Eliminar este marcador?" +STR_BOOKMARK_PAGE_FORMAT: "Página %d" +STR_UNKNOWN_CHAPTER: "Capítulo desconhecido" +STR_STATS_SESSIONS: "Sessões: " +STR_STATS_TOTAL_TIME: "Tempo total: " +STR_STATS_AVG_SESSION: "Sessão média: " +STR_STATS_LESS_THAN_MIN: "< 1 min" +STR_STATS_THIS_BOOK: "Este livro" +STR_STATS_AVERAGES: "Médias" +STR_STATS_PAGES_PER_MIN: "Páginas/min" +STR_STATS_COMPLETED_LBL: "Livros lidos" +STR_STATS_SESSIONS_LBL: "Sessões" +STR_STATS_TIME_LBL: "Tempo de leitura" +STR_STATS_PAGES_LBL: "Páginas viradas" +STR_STATS_PROGRESS_LBL: "Progresso" +STR_STATS_AVG_SESSION_LBL: "Sessão média" +STR_STATS_DAILY_AVG_LBL: "Média diária" +STR_STATS_READING_STREAK_LBL: "Sequência de leitura" +STR_STATS_LONGEST_STREAK_LBL: "Maior sequência" +STR_STATS_ALL_TIME: "Todos os livros" +STR_STATS_TOTAL_READING_TIME_LBL: "Tempo total de leitura" +STR_STATS_THIS_DEVICE: "Todos os livros (este dispositivo)" +STR_STATS_ALL_DEVICES: "Todos os livros (todos os dispositivos)" +STR_STATS_THIS_DEVICE_SCREEN: "Estatísticas de leitura - este dispositivo" +STR_STATS_ALL_DEVICES_SCREEN: "Estatísticas de leitura - todos os dispositivos sincronizados" +STR_STATS_TIME_OF_DAY: "Hora do dia" +STR_STATS_DAY_OF_WEEK: "Dia da semana" +STR_STATS_MORNING: "Manhã" +STR_STATS_AFTERNOON: "Período da tarde" +STR_STATS_EVENING: "Final da tarde" +STR_STATS_NIGHT: "Noite" +STR_STATS_MON: "Seg" +STR_STATS_TUE: "Ter" +STR_STATS_WED: "Qua" +STR_STATS_THU: "Qui" +STR_STATS_FRI: "Sex" +STR_STATS_SAT: "Sáb" +STR_STATS_SUN: "Dom" +STR_STATS_START_DATE: "Data de início" +STR_STATS_FINISHED_DATE: "Data de conclusão" +STR_STATS_EST_FINISH_DATE: "Data estimada de conclusão" +STR_STATS_STARTED: "Iniciado" +STR_STATS_DAY: "dia" +STR_STATS_DAYS: "dias" +STR_STATS_DAY_STREAK_FORMAT: "%u dias seguidos" +STR_STATS_NO_STREAK: "Ainda sem sequência" +STR_STATS_NEW_READER: "Novo leitor" +STR_STATS_MORNING_READER: "Leitor matinal" +STR_STATS_AFTERNOON_READER: "Leitor da tarde" +STR_STATS_EVENING_READER: "Leitor do fim da tarde" +STR_STATS_NIGHT_READER: "Leitor noturno" +STR_SLEEP_NEVER: "Nunca" +STR_STEP_HINT_FRONT: "Botões frontais:" +STR_STEP_HINT_SIDE: "Botões laterais:" +STR_ADD_SERVER: "Adicionar servidor" +STR_SERVER_NAME: "Nome do servidor" +STR_NO_SERVERS: "Nenhum servidor OPDS configurado" +STR_DELETE_SERVER: "Eliminar servidor" +STR_DELETE_CONFIRM: "Eliminar este servidor?" +STR_OPDS_SERVERS: "Servidores OPDS" +STR_OPDS_DOWNLOAD_FOLDER: "Pasta de descargas" +STR_OPDS_SD_ROOT: "Raiz do cartão SD" +STR_MOVE_TO_READ_FAILED_TITLE: "Não foi possível mover o livro" +STR_MOVE_TO_READ_FAILED_BODY: "Falha ao mover \"%s\" para a pasta /Read." +STR_DOWNLOAD_FONTS: "Descarregar tipos de letra" +STR_FONT_DOWNLOAD: "Transferência de tipos de letra" +STR_MANAGE_FONTS: "Gerir tipos de letra" +STR_FONT_BROWSER: "Navegador de tipos de letra" +STR_LOADING_FONT_LIST: "A carregar lista de tipos de letra..." +STR_NO_FONTS_AVAILABLE: "Nenhum tipo de letra disponível" +STR_FONT_INSTALLED: "Tipo de letra instalado!" +STR_FONT_INSTALL_FAILED: "Falha na instalação do tipo de letra" +STR_FONT_DOWNLOAD_INTERRUPTED: "Ligação de rede interrompida durante a transferência" +STR_FONT_DOWNLOAD_RETRY_HINT: "Uma nova tentativa continuará a partir do progresso guardado." +STR_FONT_DOWNLOAD_CHECKSUM_HINT: "Uma nova tentativa voltará a transferir este ficheiro." +STR_INSTALLED: "Instalado" +STR_CONFIRM_DOWNLOAD_PROMPT: "Descarregar?" +STR_SD_CARD_FULL: "Espaço insuficiente no cartão SD" +STR_FILES_LABEL: "Ficheiros: " +STR_SIZE_LABEL: "Tamanho: " +STR_REDOWNLOAD: "Descarregar novamente" +STR_DOWNLOAD_ALL: "Descarregar todos" +STR_UPDATE_ALL: "Atualizar todos" +STR_ALL_FONTS_INSTALLED: "Todos os tipos de letra instalados!" +STR_UPDATE_AVAILABLE: "Atualizar" +STR_CRASH_TITLE: "Falha do sistema" +STR_CRASH_DESCRIPTION: "Foi guardado um relatório detalhado em crash_report.txt. Por favor, inclua este ficheiro no seu relatório de erros." +STR_CRASH_REASON: "Motivo da falha:" +STR_CRASH_NO_REASON: "(Não foi registado nenhum motivo)" +STR_TILT_PAGE_TURN: "Viragem de página por inclinação" +STR_TILT_PAGE_TURN_DIRECTION: "Direção da inclinação" +STR_TILT_DIRECTION_LEFT_RIGHT: "Esquerda-direita" +STR_TILT_DIRECTION_LEFT_RIGHT_INVERTED: "Direita-esquerda" +STR_TILT_DIRECTION_FORWARD_BACK: "Avançar-recuar" +STR_TILT_DIRECTION_FORWARD_BACK_INVERTED: "Recuar-avançar" +STR_KB_HINT_MOVE_CURSOR: "Pressione ESQUERDA ou DIREITA para mover o cursor" +STR_KB_HINT_RETURN_CURSOR: "Pressione ESQUERDA para voltar à posição do cursor" +STR_KB_HINT_HIDE_PASSWORD: "Mantenha premida a DIREITA e depois prima [***] para ocultar a palavra-passe" +STR_KB_HINT_SHOW_PASSWORD: "Mantenha premida a DIREITA e depois prima [abc] para mostrar a palavra-passe" +STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prima [***] para ocultar a palavra-passe" +STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prima [abc] para mostrar a palavra-passe" +STR_KB_HINT_EDIT_ENTRY: "Mantenha CIMA premido para editar a entrada" +STR_KB_TIPS: "Dicas:" +STR_KB_HINT_RETURN_KEYBOARD: "Prima BAIXO para voltar ao teclado" +STR_KB_HINT_EXIT_URL_MODE: "Prima ABC para sair do modo de URL" +STR_KB_HINT_CLEAR_TEXT: "Mantenha DEL premido para limpar todo o texto" +STR_KB_HINT_SECONDARY_CHAR: "Mantenha SELECT premido para inserir o caráter secundário" +STR_KB_HINT_UPPER_SECONDARY: "Mantenha SELECT premido para inserir LETRAS MAIÚSCULAS ou o caráter secundário" +STR_KB_HINT_LOWER_SECONDARY: "Mantenha SELECT premido para inserir letras minúsculas ou o caráter secundário" +STR_KB_HINT_URL_SNIPPETS: "Prima URL para aceder a trechos" +STR_CONTROLS_GENERAL: "Geral" +STR_CONTROLS_IN_READER: "Controlos no leitor" +STR_POWER_BUTTON: "Botão de energia" +STR_FRONT_BUTTONS: "Botões frontais" +STR_SIDE_BUTTONS: "Botões laterais" +STR_OTHER: "Outros" +STR_LONG_PRESS_ACTION: "Ação de pressão longa" +STR_LONG_PRESS_MENU_ACTION: "Ação de pressão longa no menu" +STR_LONG_PRESS_BACK_ACTION: "Ação de pressão longa no botão Voltar" +STR_TOGGLE_GUIDE_DOTS: "Pontos guia" +STR_TOGGLE_BIONIC_READING: "Leitura biónica" +STR_TOGGLE_BOOKMARK: "Alternar marcador" +STR_CYCLE_PAGE_TURN: "Viragem automática de página" +STR_MARKED_FINISHED: "Marcado como concluído" +STR_MARKED_UNFINISHED: "Marcado como não concluído" +STR_TILT_TO_TURN_OFF: "Virar por inclinação: DESL." +STR_TILT_TO_TURN_ON: "Virar por inclinação: LIG." +STR_LOW_MEMORY_IMAGES_TITLE: "Capítulo carregado sem algumas ou todas as imagens" +STR_LOW_MEMORY_IMAGES_BODY: "A memória disponível era demasiado baixa para extrair todas as imagens deste capítulo." +STR_EPUB_LAYOUT_MEMORY_TITLE: "Capítulo necessita de demasiada memória" +STR_EPUB_LAYOUT_MEMORY_BODY: "Este capítulo necessita de mais memória para ser paginado do que a que este dispositivo pode disponibilizar em segurança." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_BOTH: "Tente novamente com a leitura biónica e os pontos guia desativados." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_BIONIC: "Tente novamente com a leitura biónica desativada." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_GUIDE: "Tente novamente com os pontos guia desativados." +STR_EPUB_FALLBACK_FONT_TITLE: "Capítulo a utilizar o tipo de letra integrado" +STR_EPUB_FALLBACK_FONT_BODY: "A memória disponível era insuficiente para paginar este capítulo com o tipo de letra do cartão SD, pelo que o capítulo foi carregado com o tipo de letra integrado." +STR_SD_FIRMWARE_UPDATE: "Atualização do firmware através do cartão SD" +STR_SELECT_FIRMWARE_FILE: "Selecionar ficheiro de firmware (.bin)" +STR_NO_BIN_FILES: "Nenhum ficheiro .bin encontrado" +STR_VALIDATING_FIRMWARE: "A validar o firmware..." +STR_INVALID_FIRMWARE: "Ficheiro de firmware inválido" +STR_FIRMWARE_TOO_LARGE: "Firmware demasiado grande para a partição" +STR_FIRMWARE_TOO_SMALL: "O ficheiro de firmware é demasiado pequeno" +STR_FIRMWARE_UPDATE_PROMPT: "Atualizar o firmware?" +STR_FIRMWARE_FILE_OPEN_FAILED: "Não é possível abrir o ficheiro" +STR_FIRMWARE_WRITE_FAILED: "Falha na escrita do firmware" +STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Não desligue o dispositivo!" +STR_RECOVERY_MODE: "Modo de recuperação" +STR_RECOVERY_MODE_HINT: "Coloque o ficheiro firmware.bin na raiz do cartão SD e selecione-o." +STR_NEARBY_STATS_READY: "Pronto para sincronizar os dois leitores" +STR_NEARBY_STATS_SYNC_BUTTON: "Sincronizar" +STR_NEARBY_STATS_READY_HINT: "Prima Sincronizar em apenas um leitor" +STR_NEARBY_STATS_SCANNING: "A procurar um leitor para trocar estatísticas" +STR_NEARBY_STATS_SYNCING: "A enviar e a receber estatísticas" +STR_NEARBY_STATS_SYNCED: "Ambos os leitores sincronizados" +STR_NEARBY_STATS_SIMULATOR_UNAVAILABLE: "A sincronização de estatísticas entre dispositivos próximos não está disponível no simulador" +STR_NEARBY_POSITION_READY: "Pronto para partilhar esta posição" +STR_NEARBY_POSITION_SHARE_BUTTON: "Partilhar" +STR_NEARBY_POSITION_READY_HINT: "Prima Partilhar no X3/X4 na página correta" +STR_NEARBY_POSITION_SCANNING: "A procurar um leitor para trocar a posição" +STR_NEARBY_POSITION_SHARING: "A partilhar esta posição" +STR_NEARBY_POSITION_WAITING: "À espera da posição partilhada" +STR_NEARBY_POSITION_APPLYING: "A aplicar a posição" +STR_NEARBY_POSITION_SHARED: "Posição partilhada" +STR_NEARBY_POSITION_SYNCED: "Posição do livro sincronizada" +STR_NEARBY_POSITION_FOUND: "Posição de um dispositivo próximo encontrada!" +STR_NEARBY_POSITION_START_FAILED: "Não foi possível iniciar a sincronização da posição entre dispositivos próximos" +STR_NEARBY_POSITION_DEVICE_ID_FAILED: "Não foi possível ler o ID do dispositivo" +STR_NEARBY_POSITION_QUEUE_OVERFLOW: "Limite da fila de sincronização excedido" +STR_NEARBY_POSITION_TIMEOUT: "Tempo limite esgotado na sincronização da posição" +STR_NEARBY_POSITION_NO_READER: "Nenhum leitor próximo encontrado" +STR_NEARBY_LABEL: "Próximo:" +STR_APPLY_NEARBY_POSITION: "Aplicar posição do dispositivo próximo" +STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "A sincronização da posição entre dispositivos próximos não está disponível no simulador" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 068977e354..56dcedd382 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Salvaţi parola?" STR_PRESS_OK_SCAN: "Apăsaţi OK pentru a scana din nou" STR_JOIN_NETWORK: "Conectaţi-vă la o reţea" STR_CREATE_HOTSPOT: "Creaţi un hotspot" -STR_NEARBY_STATS_SYNC: "Sincronizare statistici apropiate" -STR_NEARBY_STATS_SYNC_DESC: "Sincronizează statisticile de citire CrossInk cu un X3/X4 apropiat" +STR_NEARBY_DEVICE: "Dispozitiv din apropiere" +STR_NEARBY_STATS_SYNC: "Sincronizare statistici" +STR_RECEIVE_NEARBY_BOOK: "Primește fișier" +STR_RECEIVE_NEARBY_BOOK_DESC: "Primește un fișier de la alt cititor" +STR_NEARBY_STATS_SYNC_DESC: "Sincronizează statisticile de lectură între cititoare" STR_JOIN_DESC: "Conectaţi-vă la o reţea WiFi existentă" STR_HOTSPOT_DESC: "Creaţi un hotspot WiFi" STR_STARTING_HOTSPOT: "Hotspot porneşte..." @@ -91,13 +94,20 @@ STR_ORIENTATION_AWARE: "În funcție de orientare" STR_NAV_BUTTONS: "Butoane navigare" STR_ALL_BUTTONS: "Toate butoanele" STR_LONG_PRESS_BEHAVIOR: "Comportament apăsare lungă" +STR_TOUCH_READER_CONTROLS: "Comenzi tactile cititor" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientare butoane frontale" STR_LONG_PRESS_BEHAVIOR_OFF: "Dezactivat" STR_LONG_PRESS_BEHAVIOR_SKIP: "Sărire capitol" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Schimbă orientarea" STR_FONT_PREVIEW_TEXT: "Încă vând gem, whisky bej și tequila roz, preț fix" STR_FONT_FAMILY: "Familie font lectură" +STR_DICTIONARY_FONT: "Font dicționar" +STR_USE_READER_FONT: "Folosește fontul cititorului" +STR_DICTIONARY_FONT_SIZE: "Dimensiune font dicționar" +STR_USE_READER_FONT_SIZE: "Folosește dimensiunea fontului cititorului" STR_FONT_SIZE: "Dimensiune font" STR_LINE_SPACING: "Spaţiere între rânduri" +STR_WORD_SPACING: "Spațiere între cuvinte" STR_SCREEN_MARGIN: "Margine ecran lectură" STR_PUBLISHER_PAGE_NUMBERS: "Numere de pagină ale editorului" STR_PARA_ALIGNMENT: "Aliniere paragrafe reader" @@ -130,6 +140,7 @@ STR_USERNAME: "Utilizator" STR_PASSWORD: "Parolă" STR_SYNC_SERVER_URL: "URL server sincronizare" STR_DOCUMENT_MATCHING: "Corespondenţă document" +STR_SEND_METADATA: "Trimite metadate document" STR_AUTHENTICATE: "Autentificare" STR_KOREADER_USERNAME: "Nume utilizator KOReader" STR_KOREADER_PASSWORD: "Parolă KOReader" @@ -390,11 +401,10 @@ STR_CLOCK_SYNC_NOW: "Sincronizați data/ora acum" STR_CLOCK_UTC_OFFSET: "Ceas UTC Offset" STR_DISABLED: "Dezactivat" STR_DISPLAY_SLEEP_SCREEN: "Ecran de somn" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Forțați liniuțele de paragraf" STR_FORCE_REFRESH: "Actualizează ecranul" STR_FRONT_BUTTONS: "Butoane frontale" @@ -465,3 +475,6 @@ STR_NEARBY_POSITION_NO_READER: "Nu s-a găsit niciun cititor apropiat" STR_NEARBY_LABEL: "Apropiat:" STR_APPLY_NEARBY_POSITION: "Aplică poziția apropiată" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Sincronizarea poziției apropiate nu este disponibilă în simulator" +STR_ADD_HIDDEN_NETWORK: "Adaugă rețea ascunsă..." +STR_ENTER_WIFI_SSID: "Introduceți numele rețelei (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Se validează..." diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 2175999bef..c2ac631213 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Сохранить пароль?" STR_PRESS_OK_SCAN: "Нажмите OK для повторного поиска" STR_JOIN_NETWORK: "Подключиться к сети" STR_CREATE_HOTSPOT: "Создать точку доступа" -STR_NEARBY_STATS_SYNC: "Синхронизация статистики рядом" -STR_NEARBY_STATS_SYNC_DESC: "Синхронизировать статистику чтения CrossInk с ближайшим X3/X4" +STR_NEARBY_DEVICE: "Устройство поблизости" +STR_NEARBY_STATS_SYNC: "Синхронизировать статистику" +STR_RECEIVE_NEARBY_BOOK: "Получить файл" +STR_RECEIVE_NEARBY_BOOK_DESC: "Получить файл с другого ридера" +STR_NEARBY_STATS_SYNC_DESC: "Синхронизировать статистику чтения между ридерами" STR_JOIN_DESC: "Подключение к существующей сети Wi-Fi" STR_HOTSPOT_DESC: "Создать сеть Wi-Fi для подключения других" STR_STARTING_HOTSPOT: "Запуск точки доступа..." @@ -92,13 +95,20 @@ STR_NAV_BUTTONS: "Кнопки навигации" STR_ALL_BUTTONS: "Все кнопки" STR_LONG_PRESS_SKIP: "Долгое нажатие — пропуск главы" STR_LONG_PRESS_BEHAVIOR: "Поведение долгого нажатия" +STR_TOUCH_READER_CONTROLS: "Сенсорное управление чтением" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ориентировать передние кнопки" STR_LONG_PRESS_BEHAVIOR_OFF: "Ничего" STR_LONG_PRESS_BEHAVIOR_SKIP: "Пропуск главы" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Изменить ориентацию" STR_FONT_PREVIEW_TEXT: "Съешь ещё этих мягких французских булок, да выпей же чаю" STR_FONT_FAMILY: "Шрифт чтения" +STR_DICTIONARY_FONT: "Шрифт словаря" +STR_USE_READER_FONT: "Использовать шрифт читалки" +STR_DICTIONARY_FONT_SIZE: "Размер шрифта словаря" +STR_USE_READER_FONT_SIZE: "Использовать размер шрифта читалки" STR_FONT_SIZE: "Размер шрифта интерфейса" STR_LINE_SPACING: "Межстрочный интервал" +STR_WORD_SPACING: "Интервал между словами" STR_SCREEN_MARGIN: "Поля экрана" STR_PUBLISHER_PAGE_NUMBERS: "Номера страниц издателя" STR_PARA_ALIGNMENT: "Выравнивание абзаца" @@ -131,6 +141,7 @@ STR_USERNAME: "Имя пользователя" STR_PASSWORD: "Пароль" STR_SYNC_SERVER_URL: "URL сервера синхронизации" STR_DOCUMENT_MATCHING: "Сопоставление документов" +STR_SEND_METADATA: "Отправлять метаданные" STR_AUTHENTICATE: "Авторизация" STR_KOREADER_USERNAME: "Имя пользователя KOReader" STR_KOREADER_PASSWORD: "Пароль KOReader" @@ -269,6 +280,7 @@ STR_EXAMPLE_BOOK: "Название книги" STR_PREVIEW: "Предпросмотр" STR_TITLE: "Заглавие" STR_TIME_LEFT: "Осталось времени" +STR_TIME_LEFT_SHORT: "Ост. времени" STR_TIME_LEFT_CALCULATING: "Расчёт..." STR_BATTERY: "Батарея" STR_XTC_STATUS_BAR: "Строка состояния XTC" @@ -497,6 +509,20 @@ STR_TOGGLE_GUIDE_DOTS: "Направляющие точки" STR_NEARBY_STATS_VERSION_MISMATCH: "Несовпадение версий. Статистика не синхронизирована" STR_STATS_TOTAL_READING_TIME_LBL: "Общее время чтения" +STR_STATS_TOTAL_READING_TIME_LBL_SHORT: "Время чтения" +STR_STATS_PAGES_PER_MIN: "Стр./мин." +STR_STATS_COMPLETED_LBL: "Прочитано" +STR_STATS_SESSIONS_LBL: "Сеансы" +STR_STATS_TIME_LBL: "Время чтения" +STR_STATS_PAGES_LBL: "Страниц" +STR_STATS_PROGRESS_LBL: "Прогресс" +STR_STATS_AVG_SESSION_LBL: "Ср. сеанс" +STR_STATS_DAILY_AVG_LBL: "Ср. в день" +STR_STATS_FINISHED_DATE: "Дата окончания" +STR_STATS_EST_FINISH_DATE: "План. дата оконч." +STR_STATS_STARTED: "Начато" +STR_STATS_DAY: "день" +STR_STATS_DAYS: "дн." STR_NEARBY_POSITION_SYNC: "Синхронизация позиции рядом" STR_NEARBY_POSITION_VERSION_MISMATCH: "Несовпадение версий. Позиция не синхронизирована" STR_NEARBY_POSITION_BOOK_MISMATCH: "Другая книга. Позиция не синхронизирована" @@ -518,3 +544,6 @@ STR_NEARBY_POSITION_NO_READER: "Читалка рядом не найдена" STR_NEARBY_LABEL: "Рядом:" STR_APPLY_NEARBY_POSITION: "Применить позицию рядом" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Синхронизация позиции рядом недоступна в симуляторе" +STR_ADD_HIDDEN_NETWORK: "Добавить скрытую сеть..." +STR_ENTER_WIFI_SSID: "Введите имя сети (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Проверка..." diff --git a/lib/I18n/translations/slovak.yaml b/lib/I18n/translations/slovak.yaml index 9b1990236a..937d2bed2d 100644 --- a/lib/I18n/translations/slovak.yaml +++ b/lib/I18n/translations/slovak.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Uložiť heslo na nabudúce?" STR_PRESS_OK_SCAN: "Stlačte OK pre opätovné skenovanie" STR_JOIN_NETWORK: "Pripojiť sa k sieti" STR_CREATE_HOTSPOT: "Vytvoriť hotspot" -STR_NEARBY_STATS_SYNC: "Synchronizácia štatistík v okolí" -STR_NEARBY_STATS_SYNC_DESC: "Synchronizovať štatistiky čítania CrossInk s blízkym X3/X4" +STR_NEARBY_DEVICE: "Blízke zariadenie" +STR_NEARBY_STATS_SYNC: "Synchronizovať štatistiky" +STR_RECEIVE_NEARBY_BOOK: "Prijať súbor" +STR_RECEIVE_NEARBY_BOOK_DESC: "Prijať súbor z inej čítačky" +STR_NEARBY_STATS_SYNC_DESC: "Synchronizovať štatistiky čítania medzi čítačkami" STR_JOIN_DESC: "Pripojiť sa k existujúcej Wi-Fi sieti" STR_HOTSPOT_DESC: "Vytvoriť Wi-Fi sieť, ku ktorej sa môžu pripojiť ostatní" STR_STARTING_HOTSPOT: "Spúšťanie hotspotu..." @@ -97,8 +100,13 @@ STR_SIDE_BTN_LONG_PRESS: "Akcia pri dlhom stlačení" STR_CHAPTER_SKIP_OPT: "Preskočenie kapitoly" STR_CHANGE_FONT_SIZE: "Zmeniť veľkosť písma" STR_FONT_FAMILY: "Rodina písiem čítačky" +STR_DICTIONARY_FONT: "Písmo slovníka" +STR_USE_READER_FONT: "Použiť písmo čítačky" +STR_DICTIONARY_FONT_SIZE: "Veľkosť písma slovníka" +STR_USE_READER_FONT_SIZE: "Použiť veľkosť písma čítačky" STR_FONT_SIZE: "Veľkosť písma rozhrania" STR_LINE_SPACING: "Riadkovanie čítačky" +STR_WORD_SPACING: "Medzery medzi slovami" STR_SCREEN_MARGIN: "Okraj obrazovky čítačky" STR_PUBLISHER_PAGE_NUMBERS: "Čísla strán vydavateľa" STR_PARA_ALIGNMENT: "Zarovnanie odsekov čítačky" @@ -130,6 +138,7 @@ STR_USERNAME: "Používateľské meno" STR_PASSWORD: "Heslo" STR_SYNC_SERVER_URL: "URL synchronizačného servera" STR_DOCUMENT_MATCHING: "Párovanie dokumentov" +STR_SEND_METADATA: "Odosielať metadáta dokumentu" STR_AUTHENTICATE: "Overiť" STR_KOREADER_USERNAME: "Používateľské meno KOReader" STR_KOREADER_PASSWORD: "Heslo KOReader" @@ -173,6 +182,7 @@ STR_CHANGE_FONT: "Zmeniť písmo" STR_PORTRAIT: "Na výšku" STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek" STR_INVERTED: "Obrátený" +STR_TOUCH_READER_CONTROLS: "Dotykové ovládanie čítačky" STR_ORIENTATION_INVERTED: "Na výšku 180°" STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek" STR_PREV_NEXT: "Predchádzajúca/Nasledujúca" @@ -456,11 +466,10 @@ STR_CLOCK_SYNC_NOW: "Synchronizovať teraz" STR_CLOCK_UTC_OFFSET: "UTC posun hodín" STR_DISABLED: "Vypnuté" STR_DISPLAY_SLEEP_SCREEN: "Obrazovka spánku" -STR_FONT_RANGE_ALL: "všetko" -STR_FONT_RANGE_NO_EMOJI: "bez emoji" -STR_FONT_RANGE_TEENSY: "maličké" -STR_FONT_RANGE_TINY: "drobné" -STR_FONT_RANGE_XLARGE: "veľké" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_HUGE: "Obrovský" STR_ITTY_BITTY: "Úplne maličké" STR_LONG_PRESS_BEHAVIOR_OFF: "VYP" @@ -498,7 +507,6 @@ STR_TOP: "Hore" STR_XTC_STATUS_BAR: "Stavový panel XTC" STR_NEARBY_STATS_VERSION_MISMATCH: "Nesúlad verzií. Štatistiky sa nesynchronizovali" STR_PWR_BTN_FOOTNOTE_BACK: "Rýchly návrat z poznámok pod čiarou" -STR_CROSSPOINT: "CrossPoint" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Prispôsobiť predné tlačidlá orientácii" STR_FONT_PREVIEW_TEXT: "Vypätá dcéra grófa Maxwella s IQ nižším ako kôň núti čeľaď hrýzť hŕbu jabĺk" STR_NOTO_SERIF: "Noto Serif" @@ -565,3 +573,6 @@ STR_NEARBY_POSITION_NO_READER: "Nenašla sa žiadna čítačka v okolí" STR_NEARBY_LABEL: "V okolí:" STR_APPLY_NEARBY_POSITION: "Použiť pozíciu v okolí" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Synchronizácia pozície v okolí nie je v simulátore dostupná" +STR_ADD_HIDDEN_NETWORK: "Pridať skrytú sieť..." +STR_ENTER_WIFI_SSID: "Zadajte názov siete (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Overuje sa..." diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index f6bcafa3e7..4c3e9a7ce9 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Shranim geslo za naslednjič?" STR_PRESS_OK_SCAN: "Pritisni OK za ponovno iskanje" STR_JOIN_NETWORK: "Poveži se v omrežje" STR_CREATE_HOTSPOT: "Ustvari dostopno točko" -STR_NEARBY_STATS_SYNC: "Sinhronizacija statistik v bližini" -STR_NEARBY_STATS_SYNC_DESC: "Sinhroniziraj statistiko branja CrossInk z bližnjim X3/X4" +STR_NEARBY_DEVICE: "Naprava v bližini" +STR_NEARBY_STATS_SYNC: "Sinhroniziraj statistiko" +STR_RECEIVE_NEARBY_BOOK: "Prejmi datoteko" +STR_RECEIVE_NEARBY_BOOK_DESC: "Prejmi datoteko iz drugega bralnika" +STR_NEARBY_STATS_SYNC_DESC: "Sinhroniziraj statistiko branja med bralniki" STR_JOIN_DESC: "Poveži se v obstoječe WiFi omrežje" STR_HOTSPOT_DESC: "Ustvari WiFi omrežje, v katerega se lahko povežejo drugi" STR_STARTING_HOTSPOT: "Zaganjanje dostopne točke..." @@ -90,12 +93,19 @@ STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov" STR_ORIENTATION_AWARE: "Glede na usmerjenost" STR_NAV_BUTTONS: "Navigacijski gumbi" STR_ALL_BUTTONS: "Vsi gumbi" +STR_TOUCH_READER_CONTROLS: "Upravljanje bralnika na dotik" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Usmeri sprednje gumbe" STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja" STR_LONG_PRESS_BEHAVIOR: "Vedenje dolgega pritiska" STR_FONT_PREVIEW_TEXT: "V kožuščku hudobnega fanta stopiclja mizar" STR_FONT_FAMILY: "Pisava bralnika" +STR_DICTIONARY_FONT: "Pisava slovarja" +STR_USE_READER_FONT: "Uporabi pisavo bralnika" +STR_DICTIONARY_FONT_SIZE: "Velikost pisave slovarja" +STR_USE_READER_FONT_SIZE: "Uporabi velikost pisave bralnika" STR_FONT_SIZE: "Velikost pisave" STR_LINE_SPACING: "Razmik med vrsticami" +STR_WORD_SPACING: "Razmik med besedami" STR_SCREEN_MARGIN: "Robovi zaslona" STR_PUBLISHER_PAGE_NUMBERS: "Založnikove številke strani" STR_PARA_ALIGNMENT: "Poravnava odstavkov" @@ -128,6 +138,7 @@ STR_USERNAME: "Uporabniško ime" STR_PASSWORD: "Geslo" STR_SYNC_SERVER_URL: "URL strežnika za sinhronizacijo" STR_DOCUMENT_MATCHING: "Ujemanje dokumentov" +STR_SEND_METADATA: "Pošlji metapodatke dokumenta" STR_AUTHENTICATE: "Avtentikacija" STR_KOREADER_USERNAME: "KOReader uporabnik" STR_KOREADER_PASSWORD: "KOReader geslo" @@ -388,11 +399,10 @@ STR_CLOCK_SYNC_NOW: "Sinhroniziraj datum/čas zdaj" STR_CLOCK_UTC_OFFSET: "Odmik ure UTC" STR_DISABLED: "Onemogočen" STR_DISPLAY_SLEEP_SCREEN: "Zaslon za spanje" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Vsili zamike odstavkov" STR_FORCE_REFRESH: "Osveži zaslon" STR_FRONT_BUTTONS: "Sprednji gumbi" @@ -466,3 +476,6 @@ STR_NEARBY_POSITION_NO_READER: "V bližini ni najdenega bralnika" STR_NEARBY_LABEL: "V bližini:" STR_APPLY_NEARBY_POSITION: "Uporabi položaj v bližini" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Sinhronizacija položaja v bližini ni na voljo v simulatorju" +STR_ADD_HIDDEN_NETWORK: "Dodaj skrito omrežje..." +STR_ENTER_WIFI_SSID: "Vnesite ime omrežja (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Preverjanje..." diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 1bfe14d779..d06dbcae8f 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -40,10 +40,17 @@ STR_SAVE_PASSWORD: "¿Guardar contraseña?" STR_PRESS_OK_SCAN: "Pulse OK para buscar de nuevo" STR_JOIN_NETWORK: "Unirse a una red" STR_CREATE_HOTSPOT: "Crear punto de acceso" -STR_NEARBY_STATS_SYNC: "Sincronización cercana de estadísticas" -STR_NEARBY_STATS_SYNC_DESC: "Sincronizar estadísticas de lectura de CrossInk con un X3/X4 cercano" +STR_NEARBY_DEVICE: "Dispositivo cercano" +STR_NEARBY_STATS_SYNC: "Sincronizar estadísticas" +STR_RECEIVE_NEARBY_BOOK: "Recibir archivo" +STR_RECEIVE_NEARBY_BOOK_DESC: "Recibir un archivo de otro lector" +STR_NEARBY_POSITION_SYNC: "Sincronización cercana de posición" STR_JOIN_DESC: "Conectarse a una red WiFi existente" STR_HOTSPOT_DESC: "Crear una red WiFi para que otros se conecten" +STR_NEARBY_STATS_SYNC_DESC: "Sincronizar estadísticas de lectura entre lectores" +STR_NEARBY_STATS_VERSION_MISMATCH: "Las versiones no coinciden. Estadísticas no sincronizadas" +STR_NEARBY_POSITION_VERSION_MISMATCH: "Las versiones no coinciden. Posición no sincronizada" +STR_NEARBY_POSITION_BOOK_MISMATCH: "Libro diferente. Posición no sincronizada" STR_STARTING_HOTSPOT: "Iniciando punto de acceso..." STR_HOTSPOT_MODE: "Modo punto de acceso" STR_CONNECT_WIFI_HINT: "Conecte su dispositivo a esta red WiFi" @@ -51,6 +58,7 @@ STR_OPEN_URL_HINT: "Abra esta dirección en su navegador" STR_OR_HTTP_PREFIX: "o http://" STR_SCAN_QR_HINT: "o escanee el código QR con su móvil:" STR_CALIBRE_WIRELESS: "Calibre inalámbrico" +STR_CALIBRE_WEB_URL: "URL OPDS" STR_NETWORK_LEGEND: "* (Con contraseña) | + (Guardada)" STR_MAC_ADDRESS: "Dirección MAC:" STR_CHECKING_WIFI: "Comprobando WiFi..." @@ -67,39 +75,68 @@ STR_CAT_READER: "Lector" STR_CAT_CONTROLS: "Controles" STR_CAT_SYSTEM: "Sistema" STR_SLEEP_SCREEN: "Fondo de pantalla de suspensión" +STR_SLEEP_SCREEN_WALLPAPER: "Fondo de pantalla" +STR_QUICK_RESUME_TIMEOUT: "Reanudación rápida tras tiempo de espera" +STR_AFTER_TIMEOUT: "Transcurrido el tiempo" STR_SLEEP_COVER_MODE: "Modo de pantalla de suspensión" +STR_SLEEP_COVER_MODE_SHORT: "Modo de portada" STR_HIDE_BATTERY: "Ocultar % de batería" +STR_DISPLAY_THEME_HOME: "Tema e inicio" +STR_DISPLAY_REFRESH: "Actualización" +STR_DISPLAY_SLEEP_SCREEN: "Pantalla de suspensión" +STR_DISPLAY_STATUS: "Estado" STR_EXTRA_SPACING: "Espaciado entre párrafos" +STR_FORCE_PARAGRAPH_INDENTS: "Forzar sangrías de párrafo" STR_TEXT_AA: "Suavizado de texto" STR_READER_DARK_MODE: "Modo oscuro" -STR_STATS_DAY_STREAK_FORMAT: "racha de %u días" -STR_STATS_NO_STREAK: "Aún sin racha" -STR_STATS_NEW_READER: "Nuevo lector" -STR_STATS_MORNING_READER: "Lector matutino" -STR_STATS_AFTERNOON_READER: "Lector de tarde" -STR_STATS_EVENING_READER: "Lector nocturno" -STR_STATS_NIGHT_READER: "Lector de madrugada" STR_IMAGES: "Imágenes" STR_IMAGES_DISPLAY: "Mostrar" STR_IMAGES_PLACEHOLDER: "Reemplazar" STR_IMAGES_SUPPRESS: "Ocultar" +STR_EOB_HOME: "Inicio" +STR_EOB_CONTINUE_WITH: "Continuar con" STR_SHORT_PWR_BTN: "Toque corto del encendido" STR_ORIENTATION: "Orientación" STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)" STR_ORIENTATION_AWARE: "Según orientación" STR_NAV_BUTTONS: "Botones navegación" STR_ALL_BUTTONS: "Todos los botones" +STR_LONG_PRESS_SKIP: "Saltar capítulo con pulsación larga" +STR_SIDE_BTN_LONG_PRESS: "Acción de pulsación larga" +STR_CHAPTER_SKIP_OPT: "Saltar capítulo" +STR_CHANGE_FONT_SIZE: "Cambiar tamaño de fuente" STR_LONG_PRESS_BEHAVIOR: "Comportamiento pulsación larga" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botones frontales" STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado" +STR_TOUCH_READER_CONTROLS: "Controles táctiles del lector" STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambiar orient." -STR_FONT_PREVIEW_TEXT: "Benjamín pidió una bebida de kiwi y fresa. Noé, sin vergüenza, la más exquisita champaña del menú" STR_FONT_FAMILY: "Tipografía" +STR_DICTIONARY_FONT: "Fuente del diccionario" +STR_USE_READER_FONT: "Usar fuente del lector" +STR_DICTIONARY_FONT_SIZE: "Tamaño de fuente del diccionario" +STR_USE_READER_FONT_SIZE: "Usar tamaño del lector" STR_FONT_SIZE: "Tamaño" +STR_SD_FONT_SIZE_RANGE: "Descargar rango de tamaño de fuente" +STR_READER_FONT_OPTIONS: "Opciones de fuente" +STR_READER_PAGE_LAYOUT: "Diseño de página" +STR_READER_BOOK_STYLING: "Estilo de libro" +STR_READER_READING_AIDS: "Ayudas para la lectura" +STR_READER_UI: "Interfaz del lector" +STR_EPUB_RENDER_MODE: "Modo de renderizado de EPUB" +STR_RENDER_MODE_CROSSINK_DEFAULT: "Predeterminado de CrossInk" +STR_RENDER_MODE_BALANCED: "Equilibrado" +STR_RENDER_MODE_LIGHT: "Ligero" +STR_BALANCED_MODE: "Modo equilibrado" +STR_LIGHT_MODE: "Modo ligero" +STR_SAFE_MODE: "Modo seguro" STR_LINE_SPACING: "Interlineado" +STR_WORD_SPACING: "Espaciado entre palabras" STR_SCREEN_MARGIN: "Margen de lectura" STR_PUBLISHER_PAGE_NUMBERS: "Números de página de la editorial" STR_PARA_ALIGNMENT: "Ajuste de párrafo" +STR_LONG_PRESS_MENU: "Menú de pulsación larga" +STR_FONT_PREVIEW_TEXT: "Benjamín pidió una bebida de kiwi y fresa. Noé, sin vergüenza, la más exquisita champaña del menú" STR_HYPHENATION: "División de palabras" STR_TIME_TO_SLEEP: "Auto suspensión" STR_SHOW_HIDDEN_FILES: "Mostrar archivos ocultos" @@ -114,12 +151,17 @@ STR_BACKUP_NOW: "Crear copia ahora" STR_BACKUP_STATS_CONFIRM: "¿Guardar una copia en /.crossink-stats-backup/?" STR_BACKUP_STATS_DONE: "Copia de seguridad guardada" STR_BACKUP_STATS_FAILED: "Error al crear la copia de seguridad" +STR_SYSTEM_DEVICE: "Dispositivo" +STR_DEVICE_NAME: "Nombre del dispositivo" +STR_SYSTEM_FILES_CACHE: "Archivos y caché" STR_ALL_TIME_STATS: "Estadísticas totales" STR_IDLE_TIME_THRESHOLD: "Umbral de inactividad" STR_SECONDS_VALUE_FORMAT: "%u s" STR_IDLE_TIME_THRESHOLD_STEP_HINT: "Izq./Der.: 10s Arriba/Abajo: 1m" STR_RESET_ALL_TIME_STATS: "Restablecer estadísticas totales" STR_RESET_ALL_TIME_STATS_CONFIRM: "¿Borrar las estadísticas totales de este lector? Las copias de seguridad se conservan." +STR_SYSTEM_NETWORK_SYNC: "Red y sincronización" +STR_SYSTEM_UPDATES: "Actualizaciones" STR_REFRESH_FREQ: "Frecuencia de refresco" STR_KOREADER_SYNC: "Sincronización de KOReader" STR_CHECK_UPDATES: "Buscar actualización" @@ -129,6 +171,7 @@ STR_USERNAME: "Usuario" STR_PASSWORD: "Contraseña" STR_SYNC_SERVER_URL: "URL del servidor de sinc." STR_DOCUMENT_MATCHING: "Coincidencia de doc." +STR_SEND_METADATA: "Enviar metadatos doc." STR_AUTHENTICATE: "Autenticar" STR_KOREADER_USERNAME: "Usuario de KOReader" STR_KOREADER_PASSWORD: "Contraseña de KOReader" @@ -168,6 +211,7 @@ STR_IGNORE: "Ignorar" STR_SLEEP: "Suspender" STR_PAGE_TURN: "Pasar página" STR_FORCE_REFRESH: "Refrescar pant." +STR_CHANGE_FONT: "Cambiar fuente" STR_PORTRAIT: "Vertical" STR_LANDSCAPE_CW: "Horizontal (horario)" STR_INVERTED: "Invertido" @@ -175,14 +219,25 @@ STR_ORIENTATION_INVERTED: "Al revés" STR_LANDSCAPE_CCW: "Horizontal (antihorario)" STR_PREV_NEXT: "Ant./Sig." STR_NEXT_PREV: "Sig./Ant." -STR_NEXT_NEXT: "Sig./Sig." STR_DISABLED: "Desactivados" -STR_NOTO_SERIF: "Noto Serif" -STR_NOTO_SANS: "Noto Sans" +STR_NEXT_NEXT: "Sig./Sig." +STR_LEXEND_DECA: "Lexend Deca" +STR_BITTER: "Bitter" +STR_CHAREINK: "ChareInk" +STR_ATKINSON_HL: "Atkinson Hyperlegible" +STR_COMPACT: "Compacto" +STR_TINY: "Diminuto" +STR_TEENSY: "Teensy" +STR_ITTY_BITTY: "Itty Bitty" STR_SMALL: "Pequeño" STR_MEDIUM: "Mediano" STR_LARGE: "Grande" STR_X_LARGE: "Extra grande" +STR_HUGE: "Enorme" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" +STR_FONT_RANGE_ALL: "8-20pt" STR_TIGHT: "Estrecho" STR_NORMAL: "Normal" STR_WIDE: "Amplio" @@ -198,11 +253,12 @@ STR_PAGES_30: "30 pág." STR_UPDATE: "Actualizar" STR_CHECKING_UPDATE: "Buscando actualización..." STR_NEW_UPDATE: "¡Nueva actualización disponible!" -STR_CURRENT_VERSION: "Versión actual:" -STR_NEW_VERSION: "Nueva versión:" +STR_CURRENT_VERSION: "Versión actual: " +STR_NEW_VERSION: "Nueva versión: " STR_UPDATING: "Actualizando..." STR_NO_UPDATE: "No hay actualizaciones disponibles" STR_UPDATE_FAILED: "Fallo de actualización" +STR_UPDATE_HASH_MISMATCH: "El archivo de actualización no coincide" STR_UPDATE_COMPLETE: "Actualización completada" STR_POWER_ON_HINT: "Reinicie manteniendo pulsado el encendido" STR_RESTARTING_HINT: "Reiniciando... Si no se reinicia, mantenga pulsado el encendido unos segundos." @@ -211,11 +267,12 @@ STR_DOWNLOADING: "Descargando..." STR_DOWNLOAD_FAILED: "Fallo de descarga" STR_ERROR_MSG: "Error:" STR_UNNAMED: "Sin nombre" -STR_HOLD_CONFIRM_TO_DELETE: "Mantenga pulsado Confirmar para borrar" -STR_BOOKMARK_INSTRUCTIONS: "Mantenga pulsado Confirmar en el lector para crear un marcador." STR_NO_SERVER_URL: "No se configuró URL de servidor" STR_FETCH_FEED_FAILED: "Fallo al obtener el feed" STR_PARSE_FEED_FAILED: "Fallo al procesar el feed" +STR_OPDS_FEED_BUFFER_MEMORY_ERROR: "No se pudo asignar memoria para el búfer" +STR_EPUB_CHAPTER_INCOMPLETE_TITLE: "Capítulo incompleto" +STR_EPUB_CHAPTER_INCOMPLETE_BODY: "Este capítulo contiene marcado EPUB incorrecto. CrossInk se detuvo en la parte no legible, por lo que puede faltar contenido." STR_NEXT_PAGE: "Pág. siguiente »" STR_PREV_PAGE: "« Pág. anterior" STR_NETWORK_PREFIX: "Red: " @@ -250,9 +307,11 @@ STR_DIR_UP: "Subir" STR_DIR_DOWN: "Bajar" STR_OK_BUTTON: "OK" STR_SLEEP_COVER_FILTER: "Filtro de pantalla de suspensión" +STR_SLEEP_COVER_FILTER_SHORT: "Filtro de portada" STR_FILTER_CONTRAST: "Contraste" STR_CUSTOMISE_STATUS_BAR: "Personalizar barra de estado" STR_CHAPTER_PAGE_COUNT: "Contador de pág. por cap." +STR_STABLE_PAGE_NUMBERS: "Números de página estables" STR_BOOK_PROGRESS_PERCENTAGE: "Porcentaje progreso libro" STR_PROGRESS_BAR: "Barra de progreso" STR_PROGRESS_BAR_THICKNESS: "Grosor de barra de progreso" @@ -272,6 +331,7 @@ STR_XTC_STATUS_BAR: "Barra de estado de XTC" STR_BOTTOM: "Abajo" STR_TOP: "Arriba" STR_CLOCK: "Mostrar reloj" +STR_HIDE_CLOCK: "Ocultar reloj" STR_CLOCK_UTC_OFFSET: "Desfase UTC del reloj" STR_CLOCK_FORMAT: "Formato del reloj" STR_CLOCK_FORMAT_24H: "24 horas" @@ -288,24 +348,27 @@ STR_CLOCK_SYNC_NO_WIFI_HINT: "Conéctese primero al WiFi y vuelva a intentarlo." STR_CLOCK_SYNCED: "Reloj sincronizado" STR_UI_THEME: "Interfaz" STR_THEME_CLASSIC: "Clásico" -STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_THEME_LYRA: "Lyra" STR_THEME_ROUNDEDRAFF: "RoundedRaff" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" +STR_THEME_LYRA_CAROUSEL: "Lyra Carousel" +STR_THEME_MINIMAL: "Minimal" +STR_THEME_DASHBOARD: "Dashboard" +STR_THEME_MINIMAL_STATS: "Minimal Stats" +STR_QUICK_RESUME: "Reanudación rápida" +STR_RECENT_BOOKS_VIEW: "Vista de libros recientes" +STR_LIST_VIEW: "Lista" +STR_GRID_VIEW: "Cuadrícula" STR_SUNLIGHT_FADING_FIX: "Corrección de desvanecimiento" -STR_QUICK_RESUME_TIMEOUT: "Reanudación rápida tras tiempo de espera" STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones" -STR_BOOKMARK_ADDED: "Marcador añadido." -STR_DELETE_BOOKMARKS: "Borrar lista de marcadores" +STR_REMAP_FRONT_BUTTONS_READER: "Reconfigurar botones (lector)" STR_OPDS_BROWSER: "Navegador OPDS" STR_SEARCH: "Buscar" STR_COVER_CUSTOM: "Portada + Pers." STR_PAGE_OVERLAY: "Superposición de página" -STR_QUICK_RESUME: "Reanudación rápida" +STR_RECENTS: "Recientes" STR_MENU_RECENT_BOOKS: "Libros recientes" STR_REMOVE_FROM_RECENTS_ACTION: "Eliminar de Libros recientes" -STR_RECENT_BOOKS_VIEW: "Vista de libros recientes" -STR_LIST_VIEW: "Lista" -STR_GRID_VIEW: "Cuadrícula" STR_REMOVE_FROM_RECENTS: "¿Eliminar de Libros recientes?" STR_NO_RECENT_BOOKS: "No hay libros recientes" STR_CALIBRE_DESC: "Transferir contenido a este dispositivo" @@ -329,14 +392,28 @@ STR_GO_TO_PERCENT: "Ir a %" STR_GO_HOME_BUTTON: "Volver al menú Inicio" STR_SYNC_PROGRESS: "Sincronizar progreso de lectura" STR_DELETE_CACHE: "Borrar caché del libro" +STR_SAVE_CLIPPING: "Crear recorte" +STR_VIEW_CLIPPINGS: "Ver recortes" +STR_CLIPPINGS: "Recortes" +STR_BOOKMARKS_AND_CLIPPINGS: "Marcadores y recortes" +STR_NO_CLIPPINGS: "Aún no hay recortes" +STR_DELETE_CLIPPINGS: "Borrar recortes" +STR_CLIPPING_SAVED: "Recorte guardado" +STR_CLIPPING_FAILED: "No se pudo guardar el recorte" +STR_CLIPPING_LIMIT_REACHED: "Límite de recortes alcanzado" STR_BOOK_CACHE_DELETED: "Caché del libro borrado" STR_DELETE_BOOK_STATS: "Borrar estadísticas de lectura del libro" +STR_BOOK_STATS_DELETED: "Estadísticas de lectura borradas" STR_RESET_BOOK_READER_SETTINGS: "Reset Book Reader Settings" STR_BOOK_READER_SETTINGS_RESET: "Book Reader Settings Reset" STR_RESET_READING_PACE: "Restablecer ritmo de lectura" STR_READING_PACE_RESET: "Ritmo de lectura restablecido" STR_DELETE: "Borrar" -STR_CONFIRM_DELETE_BOOKMARK: "¿Borrar este marcador?" +STR_PIN_AS_FAVORITE: "Fijar como favorito" +STR_UNPIN_AS_FAVORITE: "Quitar de favoritos" +STR_PIN_PNG_WARNING: "Fijar un PNG solo es compatible con el modo Superposición de página" +STR_SET_AS_SLEEP_FOLDER: "Establecer como carpeta de suspensión" +STR_USE_DEFAULT_SLEEP_FOLDERS: "Usar carpetas de suspensión predet." STR_DISPLAY_QR: "Mostrar página como QR" STR_CHAPTER_PREFIX: "Cap.: " STR_PAGES_SEPARATOR: " pág. | " @@ -361,50 +438,135 @@ STR_NO_REMOTE_MSG: "No se encontró progreso remoto" STR_UPLOAD_PROMPT: "¿Subir posición actual?" STR_UPLOAD_SUCCESS: "¡Progreso subido!" STR_SYNC_FAILED_MSG: "Fallo de sincronización" +STR_KOREADER_SYNC_AUTH_REJECTED: "El servidor de sincronización rechazó el usuario o la contraseña" +STR_KOREADER_SYNC_NETWORK_ERROR: "No se pudo contactar con el servidor de sincronización. Verifique el WiFi y la URL." +STR_KOREADER_SYNC_NETWORK_REFUSED: "El servidor de sincronización rechazó la conexión. Verifique la URL y que el servidor esté en línea." +STR_KOREADER_SYNC_NETWORK_TIMEOUT: "El servidor de sincronización agotó el tiempo de espera. Verifique la señal WiFi e intente de nuevo." +STR_KOREADER_SYNC_NETWORK_TLS: "No se pudo establecer una conexión segura. Verifique la URL y la configuración HTTPS." +STR_KOREADER_SYNC_HTTP_404: "No se encontró el endpoint de sincronización. Verifique que la URL apunte a su servidor KOReader." +STR_KOREADER_SYNC_HTTP_STATUS_FORMAT: "El servidor de sincronización devolvió HTTP %d. Intente más tarde." +STR_KOREADER_SYNC_SERVER_ERROR: "El servidor de sincronización devolvió una respuesta inesperada. Intente más tarde." +STR_KOREADER_SYNC_BAD_RESPONSE: "El servidor de sincronización respondió con datos no válidos. Verifique la URL o la configuración del proxy." +STR_KOREADER_SYNC_LOW_MEMORY: "No hay suficiente memoria libre para completar la sincronización. Intente de nuevo." +STR_UNKNOWN_ERROR: "Error desconocido" STR_SAVE_PROGRESS_FAILED: "No se pudo guardar el progreso" -STR_SECTION_PREFIX: "Secc.:" +STR_SECTION_PREFIX: "Secc.: " STR_UPLOAD: "Subir" STR_BOOK_S_STYLE: "Estilo del libro" STR_EMBEDDED_STYLE: "Estilo integrado" STR_BIONIC_READING: "Lectura enfocada" +STR_GUIDE_READING: "Puntos guía" STR_OPDS_SERVER_URL: "URL del servidor OPDS" +STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido desde notas" STR_SET_SLEEP_COVER: "Pant. sus." STR_FOOTNOTES: "Pie de página" STR_NO_FOOTNOTES: "No hay notas al pie de esta página" STR_LINK: "[enlace]" +STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla" +STR_AUTO_TURN_ENABLED: "Avance activado: " +STR_AUTO_TURN_INTERVAL_SECONDS: "Intervalo de avance auto. (segundos)" +STR_AUTO_TURN_STEP_HINT: "Izq./Dcha.: 1 s | Subir/Bajar: 5 s" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" +STR_READING_STATS: "Estadísticas de lectura" +STR_EDIT: "Editar" +STR_MORE: "Más" +STR_TRACK_READING_STATS: "Registrar estadísticas de lectura" +STR_MARK_FINISHED: "Marcar como terminado" +STR_MARK_UNFINISHED: "Marcar como no terminado" +STR_MARK_FINISHED_PROMPT_TITLE: "¿Marcar como terminado?" +STR_MARK_FINISHED_PROMPT_BODY: "Ha alcanzado el 99% de este libro" +STR_READER_OPTIONS: "Opciones del libro" +STR_BOOKMARKS: "Marcadores" +STR_ADD_BOOKMARK: "Añadir marcador" +STR_REMOVE_BOOKMARK: "Eliminar marcador" +STR_VIEW_BOOKMARKS: "Ver marcadores" +STR_DELETE_BOOKMARKS: "Borrar lista de marcadores" +STR_BOOKMARK_ADDED: "Marcador añadido." +STR_BOOKMARK_REMOVED: "Marcador eliminado" +STR_BOOKMARK_LIMIT_REACHED: "Se alcanzó el límite de marcadores" +STR_NO_BOOKMARKS: "Aún no hay marcadores" +STR_DELETE_BOOKMARK: "Borrar" +STR_BOOKMARK_PAGE_FORMAT: "Página %d" +STR_UNKNOWN_CHAPTER: "Capítulo desconocido" +STR_STATS_SESSIONS: "Sesiones: " +STR_STATS_TOTAL_TIME: "Tiempo total: " +STR_STATS_AVG_SESSION: "Media por sesión: " +STR_STATS_LESS_THAN_MIN: "< 1 min" +STR_STATS_THIS_BOOK: "Este libro" +STR_STATS_AVERAGES: "Medias" +STR_STATS_PAGES_PER_MIN: "Pág./min" +STR_STATS_COMPLETED_LBL: "Libros leídos" +STR_STATS_SESSIONS_LBL: "Sesiones" +STR_STATS_TIME_LBL: "Tiempo de lectura" +STR_STATS_PAGES_LBL: "Páginas pasadas" +STR_STATS_PROGRESS_LBL: "Progreso" +STR_STATS_AVG_SESSION_LBL: "Media por sesión" +STR_STATS_DAILY_AVG_LBL: "Media diaria" +STR_STATS_READING_STREAK_LBL: "Racha de lectura" +STR_STATS_LONGEST_STREAK_LBL: "Racha más larga" +STR_STATS_ALL_TIME: "Todos los libros" +STR_STATS_TOTAL_READING_TIME_LBL: "Tiempo total de lectura" +STR_STATS_THIS_DEVICE: "Todos los libros (este dispositivo)" +STR_STATS_ALL_DEVICES: "Todos los libros (todos los dispositivos)" +STR_STATS_THIS_DEVICE_SCREEN: "Estadísticas de lectura - Este dispositivo" +STR_STATS_ALL_DEVICES_SCREEN: "Estadísticas de lectura - Todos los dispositivos sincronizados" +STR_STATS_TIME_OF_DAY: "Momento del día" +STR_STATS_DAY_OF_WEEK: "Día de la semana" +STR_STATS_MORNING: "Mañana" +STR_STATS_AFTERNOON: "Tarde" +STR_STATS_EVENING: "Noche" +STR_STATS_NIGHT: "Madrugada" +STR_STATS_MON: "Lun" +STR_STATS_TUE: "Mar" +STR_STATS_WED: "Mié" +STR_STATS_THU: "Jue" +STR_STATS_FRI: "Vie" +STR_STATS_SAT: "Sáb" +STR_STATS_SUN: "Dom" +STR_STATS_START_DATE: "Fecha de inicio" +STR_STATS_FINISHED_DATE: "Fecha de fin" +STR_STATS_EST_FINISH_DATE: "Fecha de fin estimada" +STR_STATS_STARTED: "Comenzado" +STR_STATS_DAY: "día" +STR_STATS_DAYS: "días" +STR_STATS_DAY_STREAK_FORMAT: "racha de %u días" +STR_STATS_NO_STREAK: "Aún sin racha" +STR_STATS_NEW_READER: "Nuevo lector" +STR_STATS_MORNING_READER: "Lector matutino" +STR_STATS_AFTERNOON_READER: "Lector de tarde" +STR_STATS_EVENING_READER: "Lector nocturno" +STR_STATS_NIGHT_READER: "Lector de madrugada" STR_SLEEP_NEVER: "Nunca" STR_STEP_HINT_FRONT: "Botones frontales:" STR_STEP_HINT_SIDE: "Botones laterales:" -STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla" STR_ADD_SERVER: "Agregar servidor" STR_SERVER_NAME: "Nombre de servidor" STR_NO_SERVERS: "No se configuraron servidores OPDS" STR_DELETE_SERVER: "Borrar servidor" +STR_DELETE_CONFIRM: "¿Borrar este servidor?" STR_OPDS_SERVERS: "Servidores OPDS" -STR_AUTO_TURN_ENABLED: "Avance activado: " -STR_AUTO_TURN_INTERVAL_SECONDS: "Intervalo de avance auto. (segundos)" -STR_AUTO_TURN_STEP_HINT: "Izq./Dcha.: 1 s | Subir/Bajar: 5 s" -STR_REMAP_FRONT_BUTTONS_READER: "Reconfigurar botones (lector)" -STR_READING_STATS: "Estadísticas de lectura" -STR_READER_OPTIONS: "Opciones del libro" -STR_BOOKMARKS: "Marcadores" -STR_LONG_PRESS_MENU_ACTION: "Acción del menú en pulsación larga" -STR_LONG_PRESS_BACK_ACTION: "Acción Atrás en pulsación larga" -STR_CYCLE_PAGE_TURN: "Avance auto." -STR_LOW_MEMORY_IMAGES_TITLE: "Capítulo cargado sin algunas o todas las imágenes" -STR_LOW_MEMORY_IMAGES_BODY: "La memoria disponible era demasiado baja para extraer todas las imágenes de este capítulo." -STR_BOOKMARK_LIMIT_REACHED: "Se alcanzó el límite de marcadores" -STR_AUTO_TURN_PAGES_PER_MIN: "Avance auto. (pág./min)" +STR_MOVE_TO_READ_FAILED_TITLE: "No se pudo mover el libro" +STR_MOVE_TO_READ_FAILED_BODY: "No se pudo mover \"%s\" a la carpeta /Read." +STR_DOWNLOAD_FONTS: "Descargar tipografías" +STR_FONT_DOWNLOAD: "Descarga de tipografías" STR_MANAGE_FONTS: "Gestionar tipografías" STR_FONT_BROWSER: "Navegador de tipografías" STR_LOADING_FONT_LIST: "Cargando lista de tipografías..." STR_NO_FONTS_AVAILABLE: "No hay tipografías disponibles" STR_FONT_INSTALLED: "¡Se instaló la tipografía!" STR_FONT_INSTALL_FAILED: "Falló la instalación de tipografía" +STR_FONT_DOWNLOAD_INTERRUPTED: "Red interrumpida durante la descarga" +STR_FONT_DOWNLOAD_RETRY_HINT: "Al reintentar se continuará con el progreso guardado." +STR_FONT_DOWNLOAD_CHECKSUM_HINT: "Al reintentar se descargará el archivo de nuevo." STR_INSTALLED: "Instalada" +STR_CONFIRM_DOWNLOAD_PROMPT: "¿Descargar?" +STR_SD_CARD_FULL: "Espacio insuficiente en la tarjeta SD" +STR_FILES_LABEL: "Archivos: " +STR_SIZE_LABEL: "Tamaño: " +STR_REDOWNLOAD: "Volver a descargar" STR_DOWNLOAD_ALL: "Descargar todas" STR_UPDATE_ALL: "Actualizar todas" +STR_ALL_FONTS_INSTALLED: "¡Todas las tipografías instaladas!" STR_UPDATE_AVAILABLE: "Actualizar" STR_CRASH_TITLE: "Fallo del sistema" STR_CRASH_DESCRIPTION: "Se guardó un informe detallado en crash_report.txt. Inclúyalo al avisar sobre el error." @@ -423,7 +585,7 @@ STR_KB_HINT_SHOW_PASSWORD: "Pulse DCHA., luego [abc] para mostrar la contraseña STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Pulse [***] para ocultar la contraseña" STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Pulse [abc] para mostrar la contraseña" STR_KB_HINT_EDIT_ENTRY: "Mantenga SUBIR para editar la entrada" -STR_KB_TIPS: "Consejos: " +STR_KB_TIPS: "Consejos:" STR_KB_HINT_RETURN_KEYBOARD: "Pulse BAJAR para volver al teclado" STR_KB_HINT_EXIT_URL_MODE: "Pulse ABC para salir del modo URL" STR_KB_HINT_CLEAR_TEXT: "Mantenga RETROCESO para borrar todo" @@ -431,6 +593,32 @@ STR_KB_HINT_SECONDARY_CHAR: "Mantenga SELECC. para otros caracteres" STR_KB_HINT_UPPER_SECONDARY: "Mantenga SELECC. para MAYÚS./otros caracteres" STR_KB_HINT_LOWER_SECONDARY: "Mantenga SELECC. para minús./otros caracteres" STR_KB_HINT_URL_SNIPPETS: "Pulse URL para ingresar fragmentos" +STR_CONTROLS_GENERAL: "General" +STR_CONTROLS_IN_READER: "Controles en el lector" +STR_POWER_BUTTON: "Botón de encendido" +STR_FRONT_BUTTONS: "Botones frontales" +STR_SIDE_BUTTONS: "Botones laterales" +STR_OTHER: "Otros" +STR_LONG_PRESS_ACTION: "Acción de pulsación larga" +STR_LONG_PRESS_MENU_ACTION: "Acción del menú en pulsación larga" +STR_LONG_PRESS_BACK_ACTION: "Acción Atrás en pulsación larga" +STR_TOGGLE_GUIDE_DOTS: "Puntos guía" +STR_TOGGLE_BIONIC_READING: "Lectura biónica" +STR_TOGGLE_BOOKMARK: "Alternar marcador" +STR_CYCLE_PAGE_TURN: "Avance auto." +STR_MARKED_FINISHED: "Marcado como terminado" +STR_MARKED_UNFINISHED: "Marcado como no terminado" +STR_TILT_TO_TURN_OFF: "Inclinación para pasar página: DESACTIVADA" +STR_TILT_TO_TURN_ON: "Inclinación para pasar página: ACTIVADA" +STR_LOW_MEMORY_IMAGES_TITLE: "Capítulo cargado sin algunas o todas las imágenes" +STR_LOW_MEMORY_IMAGES_BODY: "La memoria disponible era demasiado baja para extraer todas las imágenes de este capítulo." +STR_EPUB_LAYOUT_MEMORY_TITLE: "El capítulo necesita demasiada memoria" +STR_EPUB_LAYOUT_MEMORY_BODY: "Este capítulo necesita más memoria de la que este dispositivo puede proporcionar de forma segura." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_BOTH: "Intente de nuevo con Lectura enfocada y Puntos guía desactivados." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_BIONIC: "Intente de nuevo con Lectura enfocada desactivada." +STR_EPUB_LAYOUT_MEMORY_READING_AIDS_HINT_GUIDE: "Intente de nuevo con Puntos guía desactivados." +STR_EPUB_FALLBACK_FONT_TITLE: "El capítulo usa la tipografía integrada" +STR_EPUB_FALLBACK_FONT_BODY: "La memoria disponible era demasiado baja para procesar este capítulo con la tipografía de la tarjeta SD, así que se cargó con la tipografía integrada." STR_SD_FIRMWARE_UPDATE: "Actualizar firmware por tarjeta SD" STR_SELECT_FIRMWARE_FILE: "Elija el archivo de firmware (.bin)" STR_NO_BIN_FILES: "No se encontraron archivos .bin" @@ -451,60 +639,6 @@ STR_NEARBY_STATS_SCANNING: "Buscando lector para intercambiar estadísticas" STR_NEARBY_STATS_SYNCING: "Enviando y recibiendo estadísticas" STR_NEARBY_STATS_SYNCED: "Ambos lectores sincronizados" STR_NEARBY_STATS_SIMULATOR_UNAVAILABLE: "La sincronización cercana de estadísticas no está disponible en el simulador" -STR_BITTER: "Bitter" -STR_CHANGE_FONT: "Cambiar fuente" -STR_CHANGE_FONT_SIZE: "Cambiar tamaño de fuente" -STR_CHAPTER_SKIP_OPT: "Saltar capítulo" -STR_CHAREINK: "ChareInk" -STR_DISPLAY_SLEEP_SCREEN: "Pantalla de suspensión" -STR_FONT_RANGE_ALL: "todas" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" -STR_FORCE_PARAGRAPH_INDENTS: "Forzar sangrías de párrafo" -STR_FRONT_BUTTONS: "Botones frontales" -STR_GUIDE_READING: "Puntos guía" -STR_HUGE: "Enorme" -STR_ITTY_BITTY: "Itty Bitty" -STR_LEXEND_DECA: "Lexend Deca" -STR_LONG_PRESS_ACTION: "Acción de pulsación larga" -STR_MARK_FINISHED: "Marcar como terminado" -STR_POWER_BUTTON: "Botón de encendido" -STR_READER_BOOK_STYLING: "Estilo de libro" -STR_READER_FONT_OPTIONS: "Opciones de fuente" -STR_READER_PAGE_LAYOUT: "Diseño de página" -STR_READER_READING_AIDS: "Ayudas para la lectura" -STR_READER_UI: "Interfaz de usuario del lector" -STR_EPUB_RENDER_MODE: "Modo de renderizado EPUB" -STR_RENDER_MODE_CROSSINK_DEFAULT: "Predeterminado de CrossInk" -STR_RENDER_MODE_BALANCED: "Equilibrado" -STR_RENDER_MODE_LIGHT: "Ligero" -STR_BALANCED_MODE: "Modo equilibrado" -STR_LIGHT_MODE: "Modo ligero" -STR_SAFE_MODE: "Safe Mode" -STR_SD_FONT_SIZE_RANGE: "Descargar rango de tamaño de fuente" -STR_SIDE_BTN_LONG_PRESS: "Acción de pulsación larga" -STR_SIDE_BUTTONS: "Botones laterales" -STR_SLEEP_COVER_FILTER_SHORT: "Filtro de cubierta" -STR_SLEEP_COVER_MODE_SHORT: "Modo de portada" -STR_SLEEP_SCREEN_WALLPAPER: "Fondo de pantalla" -STR_SYSTEM_DEVICE: "Dispositivo" -STR_DEVICE_NAME: "Nombre del dispositivo" -STR_SYSTEM_FILES_CACHE: "Archivos y caché" -STR_TEENSY: "Teensy" -STR_THEME_LYRA_CAROUSEL: "Lyra Carousel" -STR_THEME_MINIMAL: "Minimal" -STR_TINY: "diminuto" -STR_TOGGLE_BIONIC_READING: "Lectura biónica" -STR_TOGGLE_BOOKMARK: "Alternar marcador" -STR_TOGGLE_GUIDE_DOTS: "Puntos guía" -STR_NEARBY_STATS_VERSION_MISMATCH: "Las versiones no coinciden. Estadísticas no sincronizadas" -STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido de notas" -STR_STATS_TOTAL_READING_TIME_LBL: "Tiempo total de lectura" -STR_NEARBY_POSITION_SYNC: "Sincronización cercana de posición" -STR_NEARBY_POSITION_VERSION_MISMATCH: "Las versiones no coinciden. Posición no sincronizada" -STR_NEARBY_POSITION_BOOK_MISMATCH: "Libro diferente. Posición no sincronizada" STR_NEARBY_POSITION_READY: "Listo para compartir esta posición" STR_NEARBY_POSITION_SHARE_BUTTON: "Compartir" STR_NEARBY_POSITION_READY_HINT: "Pulse Compartir en el X3/X4 que está en la página correcta" @@ -523,3 +657,6 @@ STR_NEARBY_POSITION_NO_READER: "No se encontró ningún lector cercano" STR_NEARBY_LABEL: "Cercano:" STR_APPLY_NEARBY_POSITION: "Aplicar posición cercana" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "La sincronización cercana de posición no está disponible en el simulador" +STR_ADD_HIDDEN_NETWORK: "Añadir red oculta..." +STR_ENTER_WIFI_SSID: "Introduce el nombre de la red (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Validando..." diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index d22b444350..428d1779b2 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Spara lösenord till nästa gång?" STR_PRESS_OK_SCAN: "Tryck OK för att skanna igen" STR_JOIN_NETWORK: "Anslut till ett nätverk" STR_CREATE_HOTSPOT: "Skapa surfzon" -STR_NEARBY_STATS_SYNC: "Närliggande statistik-sync" -STR_NEARBY_STATS_SYNC_DESC: "Synka CrossInk-lässtatistik med en närliggande X3/X4" +STR_NEARBY_DEVICE: "Enhet i närheten" +STR_NEARBY_STATS_SYNC: "Synka statistik" +STR_RECEIVE_NEARBY_BOOK: "Ta emot fil" +STR_RECEIVE_NEARBY_BOOK_DESC: "Ta emot en fil från en annan läsare" +STR_NEARBY_STATS_SYNC_DESC: "Synka lässtatistik mellan läsare" STR_JOIN_DESC: "Anslut till ett befintligt trådlöst nätverk" STR_HOTSPOT_DESC: "Skapa ett trådlöst nätverk andra kan ansluta till" STR_STARTING_HOTSPOT: "Startar surfzon…" @@ -92,13 +95,20 @@ STR_NAV_BUTTONS: "Navigeringsknappar" STR_ALL_BUTTONS: "Alla knappar" STR_LONG_PRESS_SKIP: "Långtryck: Hoppa över kapitel" STR_LONG_PRESS_BEHAVIOR: "Beteende vid långtryck" +STR_TOUCH_READER_CONTROLS: "Pekkontroller i läsaren" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Rikta främre knappar" STR_LONG_PRESS_BEHAVIOR_OFF: "AV" STR_LONG_PRESS_BEHAVIOR_SKIP: "Hoppa över kapitel" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ändra orientering" STR_FONT_PREVIEW_TEXT: "Flygande bäckasiner söka hwila på mjuka tuvor" STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj" +STR_DICTIONARY_FONT: "Ordbokstypsnitt" +STR_USE_READER_FONT: "Använd läsarens typsnitt" +STR_DICTIONARY_FONT_SIZE: "Ordbokstypsnittsstorlek" +STR_USE_READER_FONT_SIZE: "Använd läsarens teckenstorlek" STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek" STR_LINE_SPACING: "Eboksläsarens linjemellanrum" +STR_WORD_SPACING: "Ordavstånd" STR_SCREEN_MARGIN: "Eboksläsarens skärmmarginal" STR_PUBLISHER_PAGE_NUMBERS: "Förlagets sidnummer" STR_PARA_ALIGNMENT: "Eboksläsarens paragrafjustering" @@ -131,6 +141,7 @@ STR_USERNAME: "Användarnamn" STR_PASSWORD: "Lösenord" STR_SYNC_SERVER_URL: "Synkronisera serveradress" STR_DOCUMENT_MATCHING: "Dokumentmatchning" +STR_SEND_METADATA: "Skicka dokumentmetadata" STR_AUTHENTICATE: "Autentisera " STR_KOREADER_USERNAME: "KOReader användarnamn" STR_KOREADER_PASSWORD: "KOReader lösenord" @@ -454,11 +465,10 @@ STR_CHANGE_FONT_SIZE: "Ändra teckenstorlek" STR_CHAPTER_SKIP_OPT: "Kapitel Hoppa över" STR_CHAREINK: "ChareInk" STR_DISPLAY_SLEEP_SCREEN: "Sömnskärm" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Tvinga styckeindrag" STR_FRONT_BUTTONS: "Främre knappar" STR_GUIDE_READING: "Guide prickar" @@ -523,3 +533,6 @@ STR_NEARBY_POSITION_NO_READER: "Ingen närliggande läsare hittades" STR_NEARBY_LABEL: "Närliggande:" STR_APPLY_NEARBY_POSITION: "Använd närliggande position" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Närliggande positions-sync är inte tillgänglig i simulatorn" +STR_ADD_HIDDEN_NETWORK: "Lägg till dolt nätverk..." +STR_ENTER_WIFI_SSID: "Ange nätverksnamn (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Validerar..." diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index 449a2c079a..d31b6e394c 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -39,8 +39,11 @@ STR_SAVE_PASSWORD: "Şifre kaydedilsin mi?" STR_PRESS_OK_SCAN: "Tekrar taramak için OK'e basın" STR_JOIN_NETWORK: "Bir Ağa Katıl" STR_CREATE_HOTSPOT: "Erişim Noktası Oluştur" -STR_NEARBY_STATS_SYNC: "Yakındaki istatistik senkronu" -STR_NEARBY_STATS_SYNC_DESC: "CrossInk okuma istatistiklerini yakındaki bir X3/X4 ile senkronize et" +STR_NEARBY_DEVICE: "Yakındaki cihaz" +STR_NEARBY_STATS_SYNC: "İstatistikleri senkronize et" +STR_RECEIVE_NEARBY_BOOK: "Dosya al" +STR_RECEIVE_NEARBY_BOOK_DESC: "Başka bir okuyucudan dosya al" +STR_NEARBY_STATS_SYNC_DESC: "Okuma istatistiklerini okuyucular arasında senkronize et" STR_JOIN_DESC: "Mevcut bir WiFi ağına bağlan" STR_HOTSPOT_DESC: "Başkalarının katılabileceği ağ oluştur" STR_STARTING_HOTSPOT: "Erişim Noktası Başlatılıyor..." @@ -86,14 +89,20 @@ STR_ORIENTATION_AWARE: "Yöne duyarlı" STR_NAV_BUTTONS: "Gezinme tuşları" STR_ALL_BUTTONS: "Tüm tuşlar" STR_LONG_PRESS_BEHAVIOR: "Uzun basış davranışı" +STR_TOUCH_READER_CONTROLS: "Dokunmatik okuyucu kontrolleri" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir" STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change" STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi" STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi" +STR_DICTIONARY_FONT: "Sözlük Yazı Tipi" +STR_USE_READER_FONT: "Okuyucu Yazı Tipini Kullan" +STR_DICTIONARY_FONT_SIZE: "Sözlük Yazı Tipi Boyutu" +STR_USE_READER_FONT_SIZE: "Okuyucu Yazı Tipi Boyutunu Kullan" STR_FONT_SIZE: "Arayüz Yazı Boyutu" STR_LINE_SPACING: "Okuyucu Satır Aralığı" +STR_WORD_SPACING: "Kelime Aralığı" STR_SCREEN_MARGIN: "Okuyucu Ekran Kenar Boşluğu" STR_PUBLISHER_PAGE_NUMBERS: "Yayıncı sayfa numaraları" STR_PARA_ALIGNMENT: "Okuyucu Paragraf Hizalaması" @@ -126,6 +135,7 @@ STR_USERNAME: "Kullanıcı Adı" STR_PASSWORD: "Şifre" STR_SYNC_SERVER_URL: "Senkronizasyon Sunucu Adresi" STR_DOCUMENT_MATCHING: "Belge Eşleştirme" +STR_SEND_METADATA: "Belge üst verisi gönder" STR_AUTHENTICATE: "Kimlik Doğrula" STR_KOREADER_USERNAME: "KOReader Kullanıcı Adı" STR_KOREADER_PASSWORD: "KOReader Şifresi" @@ -391,11 +401,10 @@ STR_CLOCK_SYNC_NOW: "Tarih/Saati Şimdi Senkronize Et" STR_CLOCK_UTC_OFFSET: "Saat UTC Farkı" STR_DISABLED: "Devre dışı" STR_DISPLAY_SLEEP_SCREEN: "Uyku Ekranı" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Paragraf Girintilerini Zorla" STR_FORCE_REFRESH: "Ekranı Yenile" STR_FRONT_BUTTONS: "Ön Düğmeler" @@ -466,3 +475,6 @@ STR_NEARBY_POSITION_NO_READER: "Yakında okuyucu bulunamadı" STR_NEARBY_LABEL: "Yakında:" STR_APPLY_NEARBY_POSITION: "Yakındaki konumu uygula" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Yakındaki konum senkronu simülatörde kullanılamaz" +STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..." +STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Doğrulanıyor..." diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index 64a1129d37..d0a5a80e23 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Зберегти пароль на наступний раз STR_PRESS_OK_SCAN: "Натисніть OK для повторного сканування" STR_JOIN_NETWORK: "Приєднатися до мережі" STR_CREATE_HOTSPOT: "Створити точку доступу" -STR_NEARBY_STATS_SYNC: "Синхронізація статистики поруч" -STR_NEARBY_STATS_SYNC_DESC: "Синхронізувати статистику читання CrossInk з найближчим X3/X4" +STR_NEARBY_DEVICE: "Пристрій поруч" +STR_NEARBY_STATS_SYNC: "Синхронізувати статистику" +STR_RECEIVE_NEARBY_BOOK: "Отримати файл" +STR_RECEIVE_NEARBY_BOOK_DESC: "Отримати файл з іншого рідера" +STR_NEARBY_STATS_SYNC_DESC: "Синхронізувати статистику читання між рідерами" STR_JOIN_DESC: "Підключитися до існуючої мережі WiFi" STR_HOTSPOT_DESC: "Дозволити іншим підключення по Wi-Fi" STR_STARTING_HOTSPOT: "Запуск точки доступу..." @@ -91,13 +94,20 @@ STR_ORIENTATION_AWARE: "З урахуванням орієнтації" STR_NAV_BUTTONS: "Кнопки навігації" STR_ALL_BUTTONS: "Усі кнопки" STR_LONG_PRESS_BEHAVIOR: "Поведінка довгого натискання" +STR_TOUCH_READER_CONTROLS: "Сенсорне керування читанням" +STR_FRONT_BTN_FOLLOW_ORIENTATION: "Орієнтувати передні кнопки" STR_LONG_PRESS_BEHAVIOR_OFF: "Немає" STR_LONG_PRESS_BEHAVIOR_SKIP: "Наступ. розділ (утримув.)" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Зміна орієнтації екрану" STR_FONT_PREVIEW_TEXT: "Єхидна, ґава, їжак ще й шиплячі плазуни бігцем форсують Янцзи" STR_FONT_FAMILY: "Шрифт" +STR_DICTIONARY_FONT: "Шрифт словника" +STR_USE_READER_FONT: "Використовувати шрифт читача" +STR_DICTIONARY_FONT_SIZE: "Розмір шрифту словника" +STR_USE_READER_FONT_SIZE: "Використовувати розмір шрифту читача" STR_FONT_SIZE: "Розмір шрифту" STR_LINE_SPACING: "Міжрядковий інтервал" +STR_WORD_SPACING: "Інтервал між словами" STR_SCREEN_MARGIN: "Відступи від країв" STR_PUBLISHER_PAGE_NUMBERS: "Номери сторінок видавця" STR_PARA_ALIGNMENT: "Вирівнювання тексту" @@ -130,6 +140,7 @@ STR_USERNAME: "Ім'я користувача" STR_PASSWORD: "Пароль" STR_SYNC_SERVER_URL: "URL для синхронізації" STR_DOCUMENT_MATCHING: "Порівняння документів" +STR_SEND_METADATA: "Надсилати метадані" STR_AUTHENTICATE: "Автентифікувати" STR_KOREADER_USERNAME: "Ім'я користувача KOReader" STR_KOREADER_PASSWORD: "Пароль KOReader" @@ -453,11 +464,10 @@ STR_CLOCK_SYNC_NOW: "Синхронізувати дату/час зараз" STR_CLOCK_UTC_OFFSET: "Час UTC Offset" STR_DISABLED: "Вимкнено" STR_DISPLAY_SLEEP_SCREEN: "Екран сну" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Примусово використовувати абзацні відступи" STR_FRONT_BUTTONS: "Передні кнопки" STR_GUIDE_READING: "Керівні точки" @@ -521,3 +531,6 @@ STR_NEARBY_POSITION_NO_READER: "Читалку поруч не знайдено" STR_NEARBY_LABEL: "Поруч:" STR_APPLY_NEARBY_POSITION: "Застосувати позицію поруч" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Синхронізація позиції поруч недоступна в симуляторі" +STR_ADD_HIDDEN_NETWORK: "Додати приховану мережу..." +STR_ENTER_WIFI_SSID: "Введіть назву мережі (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Перевірка..." diff --git a/lib/I18n/translations/valencian.yaml b/lib/I18n/translations/valencian.yaml index 10d7f67921..9f528bb1a8 100644 --- a/lib/I18n/translations/valencian.yaml +++ b/lib/I18n/translations/valencian.yaml @@ -2,7 +2,7 @@ _language_name: "Valencià" _language_code: "CAV" _order: "22" -STR_CROSSPOINT: "CrossPoint" +STR_CROSSINK: "CrossInk" STR_BOOTING: "ARRENCANT" STR_SLEEPING: "ENTRANT EN REPÒS" STR_ENTERING_SLEEP: "Entrant en repòs" @@ -40,6 +40,11 @@ STR_SAVE_PASSWORD: "Voleu guardar la contrasenya per a la pròxima vegada?" STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar" STR_JOIN_NETWORK: "Uneix-te a una xarxa" STR_CREATE_HOTSPOT: "Crea un punt d'accés" +STR_NEARBY_DEVICE: "Dispositiu pròxim" +STR_NEARBY_STATS_SYNC: "Sincronitza estadístiques" +STR_RECEIVE_NEARBY_BOOK: "Rep un fitxer" +STR_RECEIVE_NEARBY_BOOK_DESC: "Rep un fitxer d'un altre lector" +STR_NEARBY_STATS_SYNC_DESC: "Sincronitza les estadístiques de lectura entre lectors" STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent" STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi" STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..." @@ -85,6 +90,7 @@ STR_IMAGES_SUPPRESS: "Eliminar" STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada" STR_ORIENTATION: "Orientació de lectura" STR_SIDE_BTN_LAYOUT: "Disposició botons laterals" +STR_TOUCH_READER_CONTROLS: "Controls tàctils del lector" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals" STR_LONG_PRESS_BEHAVIOR: "Comportament de pulsació llarga" STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat" @@ -92,8 +98,13 @@ STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació" STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!" STR_FONT_FAMILY: "Família de fonts" +STR_DICTIONARY_FONT: "Tipus de lletra del diccionari" +STR_USE_READER_FONT: "Utilitza la lletra del lector" +STR_DICTIONARY_FONT_SIZE: "Grandària de la lletra del diccionari" +STR_USE_READER_FONT_SIZE: "Utilitza la grandària del lector" STR_FONT_SIZE: "Grandària de la lletra (UI)" STR_LINE_SPACING: "Interlineat del lector" +STR_WORD_SPACING: "Espaiat entre paraules" STR_SCREEN_MARGIN: "Marge de pantalla del lector" STR_PUBLISHER_PAGE_NUMBERS: "Números de pàgina de l'editorial" STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector" @@ -130,6 +141,7 @@ STR_USERNAME: "Nom d'usuari" STR_PASSWORD: "Contrasenya" STR_SYNC_SERVER_URL: "URL del servidor de sincronització" STR_DOCUMENT_MATCHING: "Coincidència de documents" +STR_SEND_METADATA: "Envia metadades del document" STR_AUTHENTICATE: "Autentica" STR_KOREADER_USERNAME: "Nom d'usuari del KOReader" STR_KOREADER_PASSWORD: "Contrasenya del KOReader" @@ -430,11 +442,10 @@ STR_CHAPTER_SKIP_OPT: "Saltar capítol" STR_CHAREINK: "ChareInk" STR_CYCLE_PAGE_TURN: "Canvi automàtic de pàgina" STR_DISPLAY_SLEEP_SCREEN: "Pantalla de repòs" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "molt menut" -STR_FONT_RANGE_TINY: "menut" -STR_FONT_RANGE_XLARGE: "molt gran" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_FORCE_PARAGRAPH_INDENTS: "Força el sagnat dels paràgrafs" STR_FRONT_BUTTONS: "Botons frontals" STR_GRID_VIEW: "Quadrícula" @@ -507,3 +518,6 @@ STR_NEARBY_POSITION_NO_READER: "No s'ha trobat cap lector proper" STR_NEARBY_LABEL: "Proper:" STR_APPLY_NEARBY_POSITION: "Aplica la posició propera" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "La sincronització de posició propera no està disponible en el simulador" +STR_ADD_HIDDEN_NETWORK: "Afig una xarxa oculta..." +STR_ENTER_WIFI_SSID: "Introduïx el nom de la xarxa (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "S'està validant..." diff --git a/lib/I18n/translations/vietnamese.yaml b/lib/I18n/translations/vietnamese.yaml index 3950ea3bc5..927b897bd1 100644 --- a/lib/I18n/translations/vietnamese.yaml +++ b/lib/I18n/translations/vietnamese.yaml @@ -40,8 +40,11 @@ STR_SAVE_PASSWORD: "Lưu mật khẩu cho lần sau?" STR_PRESS_OK_SCAN: "Nhấn OK để quét lại" STR_JOIN_NETWORK: "Tham gia mạng" STR_CREATE_HOTSPOT: "Tạo điểm phát sóng" -STR_NEARBY_STATS_SYNC: "Đồng bộ thống kê gần đây" -STR_NEARBY_STATS_SYNC_DESC: "Đồng bộ thống kê đọc CrossInk với X3/X4 ở gần" +STR_NEARBY_DEVICE: "Thiết bị ở gần" +STR_NEARBY_STATS_SYNC: "Đồng bộ thống kê" +STR_RECEIVE_NEARBY_BOOK: "Nhận tệp" +STR_RECEIVE_NEARBY_BOOK_DESC: "Nhận tệp từ một máy đọc khác" +STR_NEARBY_STATS_SYNC_DESC: "Đồng bộ thống kê đọc giữa các máy đọc" STR_JOIN_DESC: "Kết nối với mạng WiFi hiện có" STR_HOTSPOT_DESC: "Tạo một mạng WiFi để thiết bị khác kết nối" STR_STARTING_HOTSPOT: "Đang khởi động điểm phát sóng..." @@ -96,14 +99,20 @@ STR_LONG_PRESS_BEHAVIOR: "Hành vi nhấn giữ" STR_SIDE_BTN_LONG_PRESS: "Hành động nhấn giữ" STR_CHAPTER_SKIP_OPT: "Bỏ qua chương" STR_CHANGE_FONT_SIZE: "Đổi cỡ chữ" +STR_TOUCH_READER_CONTROLS: "Điều khiển đọc bằng cảm ứng" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Xoay nút trước theo hướng" STR_LONG_PRESS_BEHAVIOR_OFF: "TẮT" STR_LONG_PRESS_BEHAVIOR_SKIP: "Nhảy chương" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Đổi hướng" STR_FONT_PREVIEW_TEXT: "Trường quê em do bố của em xây kĩ nên sạch và đẹp lắm" STR_FONT_FAMILY: "Phông chữ trình đọc" +STR_DICTIONARY_FONT: "Phông chữ từ điển" +STR_USE_READER_FONT: "Dùng phông chữ trình đọc" +STR_DICTIONARY_FONT_SIZE: "Cỡ chữ từ điển" +STR_USE_READER_FONT_SIZE: "Dùng cỡ chữ trình đọc" STR_FONT_SIZE: "Cỡ chữ trình đọc" STR_LINE_SPACING: "Giãn dòng" +STR_WORD_SPACING: "Giãn cách từ" STR_SCREEN_MARGIN: "Lề màn hình trình đọc" STR_PUBLISHER_PAGE_NUMBERS: "Số trang của nhà xuất bản" STR_PARA_ALIGNMENT: "Căn chỉnh đoạn văn" @@ -135,6 +144,7 @@ STR_USERNAME: "Tên người dùng" STR_PASSWORD: "Mật khẩu" STR_SYNC_SERVER_URL: "URL máy chủ đồng bộ" STR_DOCUMENT_MATCHING: "Khớp tài liệu" +STR_SEND_METADATA: "Gửi siêu dữ liệu tài liệu" STR_AUTHENTICATE: "Xác thực" STR_KOREADER_USERNAME: "Tên người dùng KOReader" STR_KOREADER_PASSWORD: "Mật khẩu KOReader" @@ -464,11 +474,10 @@ STR_CLOCK_SYNC_NOW: "Đồng bộ ngày/giờ ngay bây giờ" STR_CLOCK_UTC_OFFSET: "Độ lệch UTC của đồng hồ" STR_DISABLED: "Đã tắt" STR_DISPLAY_SLEEP_SCREEN: "Màn hình ngủ" -STR_FONT_RANGE_ALL: "all" -STR_FONT_RANGE_NO_EMOJI: "no_emoji" -STR_FONT_RANGE_TEENSY: "teensy" -STR_FONT_RANGE_TINY: "tiny" -STR_FONT_RANGE_XLARGE: "xlarge" +STR_FONT_RANGE_ALL: "8-20pt" +STR_FONT_RANGE_TEENSY: "8-12pt" +STR_FONT_RANGE_TINY: "10-16pt" +STR_FONT_RANGE_XLARGE: "14-20pt" STR_HUGE: "Rất lớn" STR_ITTY_BITTY: "Itty Bitty" STR_MANAGE_FONTS: "Quản lý phông chữ" @@ -524,3 +533,6 @@ STR_NEARBY_POSITION_NO_READER: "Không tìm thấy máy đọc gần đây" STR_NEARBY_LABEL: "Gần đây:" STR_APPLY_NEARBY_POSITION: "Áp dụng vị trí gần đây" STR_NEARBY_POSITION_SIMULATOR_UNAVAILABLE: "Đồng bộ vị trí gần đây không khả dụng trong trình mô phỏng" +STR_ADD_HIDDEN_NETWORK: "Thêm mạng ẩn..." +STR_ENTER_WIFI_SSID: "Nhập tên mạng (SSID)" +STR_NEARBY_TRANSFER_VALIDATING: "Đang xác thực..." diff --git a/lib/InflateReader/InflateReader.cpp b/lib/InflateReader/InflateReader.cpp index ba98dda887..35f64eb976 100644 --- a/lib/InflateReader/InflateReader.cpp +++ b/lib/InflateReader/InflateReader.cpp @@ -1,52 +1,9 @@ #include "InflateReader.h" #include -#include - -namespace { -constexpr size_t INFLATE_DICT_SIZE = InflateReader::STREAMING_DICT_SIZE; -} - -// Guarantee the cast pattern in the header comment is valid. -static_assert(std::is_standard_layout::value, - "InflateReader must be standard-layout for the uzlib callback cast to work"); - -InflateReader::~InflateReader() { deinit(); } - -bool InflateReader::init(const bool streaming) { - deinit(); // free any previously allocated ring buffer and reset state - - if (streaming) { - ringBuffer = static_cast(malloc(INFLATE_DICT_SIZE)); - if (!ringBuffer) return false; - ownsRingBuffer = true; - memset(ringBuffer, 0, INFLATE_DICT_SIZE); - } - - uzlib_uncompress_init(&decomp, ringBuffer, ringBuffer ? INFLATE_DICT_SIZE : 0); - return true; -} - -bool InflateReader::initWithExternalDictionary(uint8_t* dictionary, const size_t dictionarySize) { - deinit(); - if (!dictionary || dictionarySize < INFLATE_DICT_SIZE) return false; - - ringBuffer = dictionary; - ownsRingBuffer = false; - memset(ringBuffer, 0, INFLATE_DICT_SIZE); - uzlib_uncompress_init(&decomp, ringBuffer, INFLATE_DICT_SIZE); - return true; -} - -void InflateReader::deinit() { - if (ringBuffer) { - if (ownsRingBuffer) { - free(ringBuffer); - } - ringBuffer = nullptr; - } - ownsRingBuffer = false; +void InflateReader::init() { memset(&decomp, 0, sizeof(decomp)); + uzlib_uncompress_init(&decomp, nullptr, 0); } void InflateReader::setSource(const uint8_t* src, size_t len) { @@ -54,19 +11,8 @@ void InflateReader::setSource(const uint8_t* src, size_t len) { decomp.source_limit = src + len; } -void InflateReader::setReadCallback(int (*cb)(struct uzlib_uncomp*)) { decomp.source_read_cb = cb; } - -void InflateReader::skipZlibHeader() { - uzlib_get_byte(&decomp); - uzlib_get_byte(&decomp); -} - bool InflateReader::read(uint8_t* dest, size_t len) { - if (!ringBuffer) { - // One-shot mode: back-references use absolute offset from dest_start. - // Valid only when read() is called once with the full output buffer. - decomp.dest_start = dest; - } + decomp.dest_start = dest; decomp.dest = dest; decomp.dest_limit = dest + len; @@ -74,20 +20,3 @@ bool InflateReader::read(uint8_t* dest, size_t len) { if (res < 0) return false; return decomp.dest == decomp.dest_limit; } - -InflateStatus InflateReader::readAtMost(uint8_t* dest, size_t maxLen, size_t* produced) { - if (!ringBuffer) { - // One-shot mode: back-references use absolute offset from dest_start. - // Valid only when readAtMost() is called once with the full output buffer. - decomp.dest_start = dest; - } - decomp.dest = dest; - decomp.dest_limit = dest + maxLen; - - const int res = uzlib_uncompress(&decomp); - *produced = static_cast(decomp.dest - dest); - - if (res == TINF_DONE) return InflateStatus::Done; - if (res < 0) return InflateStatus::Error; - return InflateStatus::Ok; -} diff --git a/lib/InflateReader/InflateReader.h b/lib/InflateReader/InflateReader.h index 8502c77d80..3062c1984d 100644 --- a/lib/InflateReader/InflateReader.h +++ b/lib/InflateReader/InflateReader.h @@ -5,90 +5,30 @@ #include #include -// Return value for readAtMost(). -enum class InflateStatus { - Ok, // Output buffer full; more compressed data remains. - Done, // Stream ended cleanly (TINF_DONE). produced may be < maxLen. - Error, // Decompression failed. -}; - -// Streaming deflate decompressor wrapping uzlib. -// -// Two modes: -// init(false) — one-shot: input is a contiguous buffer, call read() once. -// init(true) — streaming: allocates a 32KB ring buffer for back-references -// across multiple read() / readAtMost() calls. -// initWithExternalDictionary(...) — streaming with caller-owned scratch. -// -// Streaming callback pattern: -// The uzlib read callback receives a `struct uzlib_uncomp*` with no separate -// context pointer. To attach context, make InflateReader the *first member* of -// your context struct, then cast inside the callback: -// -// struct MyCtx { -// InflateReader reader; // must be first -// HalFile* file; -// // ... -// }; -// static int myCb(struct uzlib_uncomp* u) { -// MyCtx* ctx = reinterpret_cast(u); // valid: reader.decomp is at offset 0 -// // ... fill u->source / u->source_limit, return first byte -// } -// MyCtx ctx; -// ctx.reader.init(true); -// ctx.reader.setReadCallback(myCb); +// Small one-shot deflate decompressor wrapping uzlib. // +// Retained only for FontDecompressor's tiny flash-resident group +// decompressions, where uzlib's ~1KB state beats tinfl's ~11KB on the +// OOM-sensitive render path. All throughput paths (zip entries, PNG IDAT) use +// InflateStream (lib/miniz), which decodes several times faster. class InflateReader { public: - static constexpr size_t STREAMING_DICT_SIZE = 32768; - InflateReader() = default; - ~InflateReader(); - InflateReader(const InflateReader&) = delete; InflateReader& operator=(const InflateReader&) = delete; - // Initialise decompressor. streaming=true allocates a 32KB ring buffer needed - // when read() or readAtMost() will be called multiple times. - // Returns false only in streaming mode if the ring buffer allocation fails. - bool init(bool streaming = false); - - // Initialise streaming mode with caller-owned scratch. The caller must keep - // the dictionary alive until this reader is deinitialized or destroyed. - bool initWithExternalDictionary(uint8_t* dictionary, size_t dictionarySize); - - // Release the ring buffer and reset internal state. - void deinit(); + // Initialize one-shot mode. The destination buffer holds the entire output, + // so back-references resolve inside it without a separate 32KB dictionary. + void init(); // Set the entire compressed input as a contiguous memory buffer. - // Used in one-shot mode; not needed when a read callback is set. + // Used before the single read() call. void setSource(const uint8_t* src, size_t len); - // Set a uzlib-compatible read callback for streaming input. - // See class-level comment for the expected callback/context struct pattern. - void setReadCallback(int (*cb)(uzlib_uncomp*)); - - // Consume the 2-byte zlib header (CMF + FLG) from the input stream. - // Call this once before the first read() when input is zlib-wrapped (e.g. PNG IDAT). - void skipZlibHeader(); - // Decompress exactly len bytes into dest. // Returns false if the stream ends before producing len bytes, or on error. bool read(uint8_t* dest, size_t len); - // Decompress up to maxLen bytes into dest. - // Sets *produced to the number of bytes written. - // Returns Done when the stream ends cleanly, Ok when there is more to read, - // and Error on failure. - InflateStatus readAtMost(uint8_t* dest, size_t maxLen, size_t* produced); - - // Returns a pointer to the underlying TINF_DATA. - // Useful for advanced streaming setups where the callback needs access to the - // uzlib struct directly (e.g. updating source/source_limit). - uzlib_uncomp* raw() { return &decomp; } - private: uzlib_uncomp decomp = {}; - uint8_t* ringBuffer = nullptr; - bool ownsRingBuffer = false; }; diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index a4b1422d89..7c8686102e 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -595,8 +595,6 @@ static bool isProgressiveJpeg(FsFile& file) { // Internal implementation with configurable target size and bit depth bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight, bool oneBit, bool crop, bool adaptiveContain) { - LOG_DBG("JPG", "Converting JPEG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight); - if (ESP.getFreeHeap() < MIN_FREE_HEAP) { LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", ESP.getFreeHeap(), MIN_FREE_HEAP); return false; @@ -625,8 +623,6 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm const int srcWidth = jpeg->getWidth(); const int srcHeight = jpeg->getHeight(); - LOG_DBG("JPG", "JPEG dimensions: %dx%d%s", srcWidth, srcHeight, progressive ? " (progressive)" : ""); - constexpr int MAX_IMAGE_WIDTH = 2048; constexpr int MAX_IMAGE_HEIGHT = 3072; @@ -653,9 +649,6 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm const bool smoothUpscale = progressive && needsScaling && geometry.scaleX_fp <= FP_ONE && geometry.scaleY_fp <= FP_ONE && (geometry.scaleX_fp < FP_ONE || geometry.scaleY_fp < FP_ONE); - LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d, mode=%s, offset %u,%u)", effectiveSrcW, effectiveSrcH, outWidth, - outHeight, targetWidth, targetHeight, cropOutput ? "cover" : "contain", geometry.srcXOffset_fp >> 16, - geometry.srcYOffset_fp >> 16); // Write BMP header with output dimensions int bytesPerRow; @@ -787,7 +780,6 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm return false; } - LOG_DBG("JPG", "Successfully converted JPEG to BMP"); return true; } diff --git a/lib/KOReaderSync/ChapterXPathResolver.cpp b/lib/KOReaderSync/ChapterXPathResolver.cpp index a099e38b5d..c3ae1e024d 100644 --- a/lib/KOReaderSync/ChapterXPathResolver.cpp +++ b/lib/KOReaderSync/ChapterXPathResolver.cpp @@ -77,6 +77,10 @@ size_t countUtf8Codepoints(const XML_Char* data, const int len) { return count; } +bool isNonVisibleTextTag(const std::string& name) { + return name == "head" || name == "style" || name == "script" || name == "title" || name == "rp" || name == "rt"; +} + class ParagraphTextCounter final : public Print { public: ParagraphTextCounter() { @@ -155,9 +159,7 @@ class ParagraphTextCounter final : public Print { return; } - if (name == "p") { - paragraphDepth++; - } + if (isNonVisibleTextTag(name)) nonVisibleDepth++; depth++; } @@ -174,13 +176,11 @@ class ParagraphTextCounter final : public Print { return; } - if (name == "p" && paragraphDepth > 0) { - paragraphDepth--; - } + if (isNonVisibleTextTag(name) && nonVisibleDepth > 0) nonVisibleDepth--; } void onCharacterData(const XML_Char* data, const int len) { - if (!insideBody || paragraphDepth <= 0 || len <= 0) { + if (!insideBody || nonVisibleDepth > 0 || len <= 0) { return; } @@ -194,7 +194,7 @@ class ParagraphTextCounter final : public Print { bool stopped = false; int depth = 0; int bodyDepth = -1; - int paragraphDepth = 0; + int nonVisibleDepth = 0; size_t visibleChars = 0; }; @@ -422,6 +422,7 @@ class XPathProgressResolver final : public Print { if (name == "li") { liDepth++; } + if (isNonVisibleTextTag(name)) nonVisibleDepth++; depth++; } @@ -448,6 +449,7 @@ class XPathProgressResolver final : public Print { if (name == "li" && liDepth > 0) { liDepth--; } + if (isNonVisibleTextTag(name) && nonVisibleDepth > 0) nonVisibleDepth--; if (!textNodeIndexStack.empty()) { textNodeIndexStack.pop_back(); @@ -464,7 +466,7 @@ class XPathProgressResolver final : public Print { } void onCharacterData(const XML_Char* data, const int len) { - if (!insideBody || (paragraphDepth <= 0 && liDepth <= 0) || len <= 0 || stopped) { + if (!insideBody || nonVisibleDepth > 0 || len <= 0 || stopped) { return; } @@ -508,6 +510,7 @@ class XPathProgressResolver final : public Print { int bodyDepth = -1; int paragraphDepth = 0; int liDepth = 0; + int nonVisibleDepth = 0; size_t visibleChars = 0; size_t textNodeStartChars = 0; std::vector textNodeIndexStack; @@ -605,3 +608,33 @@ std::string ChapterXPathResolver::findXPathForProgress(const std::shared_ptr& epub, const int spineIndex, + const uint32_t visibleTextOffset) { + if (!epub || spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) { + return ""; + } + + const auto href = epub->getSpineItem(spineIndex).href; + if (href.empty()) { + return ""; + } + + // XPath text offsets are one-based, while the cache records the first + // visible codepoint on a page with a zero-based offset. + const size_t targetVisibleChar = static_cast(visibleTextOffset) + 1; + XPathProgressResolver resolver(targetVisibleChar); + if (!resolver.ok()) { + return ""; + } + resolver.spineIndex = spineIndex; + if (!epub->readItemContentsToStream(href, resolver, 1024) || !resolver.finish()) { + return ""; + } + if (!resolver.hasMatch()) { + LOG_DBG("KOX", "Could not resolve visible offset %lu in spine %d", static_cast(visibleTextOffset), + spineIndex); + return ""; + } + return resolver.getXPath(); +} diff --git a/lib/KOReaderSync/ChapterXPathResolver.h b/lib/KOReaderSync/ChapterXPathResolver.h index a06cb709ff..6f18700cee 100644 --- a/lib/KOReaderSync/ChapterXPathResolver.h +++ b/lib/KOReaderSync/ChapterXPathResolver.h @@ -34,4 +34,9 @@ class ChapterXPathResolver { * An empty string means parsing failed or the location could not be resolved. */ static std::string findXPathForProgress(const std::shared_ptr& epub, int spineIndex, float intraSpineProgress); + + // Resolve a zero-based visible Unicode codepoint offset. This is the stable + // page position stored in CrossInk's section cache, independent of layout. + static std::string findXPathForVisibleTextOffset(const std::shared_ptr& epub, int spineIndex, + uint32_t visibleTextOffset); }; diff --git a/lib/KOReaderSync/KOReaderCredentialStore.cpp b/lib/KOReaderSync/KOReaderCredentialStore.cpp index 32bc237fc7..f696783432 100644 --- a/lib/KOReaderSync/KOReaderCredentialStore.cpp +++ b/lib/KOReaderSync/KOReaderCredentialStore.cpp @@ -5,15 +5,27 @@ #include namespace { -// Default sync server URL -constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443"; +// Default sync server URL. crosspoint-sync speaks the full KOSync protocol, so +// pointing at any other kosync server (e.g. https://sync.koreader.rocks:443) +// still works via the custom server URL setting. +constexpr char DEFAULT_SERVER_URL[] = "https://sync.crosspointreader.com"; + +// Default before config version 2. Configs saved without a version stamp and an +// empty serverUrl were implicitly syncing here — they get pinned on upgrade. +constexpr char LEGACY_DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443"; + +// Bumped when a change to defaults would alter behavior for existing configs. +constexpr uint8_t CONFIG_VERSION = 2; } // namespace void KOReaderCredentialStore::toJson(JsonDocument& doc) const { + doc["cfgVersion"] = CONFIG_VERSION; doc["username"] = getUsername(); doc["password_obf"] = obfuscation::obfuscateToBase64(getPassword()); doc["serverUrl"] = getServerUrl(); doc["matchMethod"] = static_cast(getMatchMethod()); + doc["sendMetadata"] = getSendMetadata(); + doc["syncBehavior"] = static_cast(getSyncBehavior()); } bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) { @@ -36,6 +48,19 @@ bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) { setCredentials(user, pass); setServerUrl(doc["serverUrl"] | ""); + // The default server changed in config v2 (sync.koreader.rocks -> crosspoint-sync). + // A pre-v2 config with credentials and no explicit URL was actively syncing + // against the old default — pin that URL so the upgrade doesn't switch servers + // out from under the user. Fresh setups get the new default. + const uint8_t cfgVersion = doc["cfgVersion"] | (uint8_t)1; + if (cfgVersion < CONFIG_VERSION) { + if (getServerUrl().empty() && hasCredentials()) { + LOG_DBG("KRS", "Pre-v2 config used the old default server; pinning %s", LEGACY_DEFAULT_SERVER_URL); + setServerUrl(LEGACY_DEFAULT_SERVER_URL); + } + needsResave = true; // stamp cfgVersion so this migration runs once + } + uint8_t method = doc["matchMethod"] | (uint8_t)0; if (method <= static_cast(DocumentMatchMethod::BINARY)) { setMatchMethod(static_cast(method)); @@ -43,10 +68,23 @@ bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) { LOG_DBG("KRS", "Invalid matchMethod %u in JSON, resetting to FILENAME", method); setMatchMethod(DocumentMatchMethod::FILENAME); } + setSendMetadata(doc["sendMetadata"] | false); + + const JsonVariantConst behaviorValue = doc["syncBehavior"]; + const bool missingBehavior = behaviorValue.isNull(); + uint8_t behavior = behaviorValue | static_cast(KOReaderSyncBehavior::ASK_EVERY_TIME); + if (behavior <= static_cast(KOReaderSyncBehavior::SMART)) { + setSyncBehavior(static_cast(behavior)); + needsResave = needsResave || missingBehavior; + } else { + LOG_DBG("KRS", "Invalid syncBehavior %u in JSON, resetting to ASK_EVERY_TIME", behavior); + setSyncBehavior(KOReaderSyncBehavior::ASK_EVERY_TIME); + needsResave = true; + } if (needsResave) { - LOG_DBG("KRS", "Resaved KOReader credentials to update format"); - saveToFile(); + LOG_DBG("KRS", "Resaving KOReader credentials to update format"); + requestResave(); } return true; @@ -55,7 +93,6 @@ bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) { void KOReaderCredentialStore::setCredentials(const std::string& user, const std::string& pass) { username = user; password = pass; - LOG_DBG("KRS", "Set credentials for user: %s", user.c_str()); } std::string KOReaderCredentialStore::getMd5Password() const { @@ -78,13 +115,9 @@ void KOReaderCredentialStore::clearCredentials() { username.clear(); password.clear(); saveToFile(); - LOG_DBG("KRS", "Cleared KOReader credentials"); } -void KOReaderCredentialStore::setServerUrl(const std::string& url) { - serverUrl = url; - LOG_DBG("KRS", "Set server URL: %s", url.empty() ? "(default)" : url.c_str()); -} +void KOReaderCredentialStore::setServerUrl(const std::string& url) { serverUrl = url; } std::string KOReaderCredentialStore::getBaseUrl() const { std::string url; @@ -105,7 +138,15 @@ std::string KOReaderCredentialStore::getBaseUrl() const { return url; } -void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) { - matchMethod = method; - LOG_DBG("KRS", "Set match method: %s", method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary"); +bool KOReaderCredentialStore::usesCrossPointSyncServer() const { return getBaseUrl() == DEFAULT_SERVER_URL; } + +void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) { matchMethod = method; } + +void KOReaderCredentialStore::setSendMetadata(bool enabled) { sendMetadata = enabled; } + +void KOReaderCredentialStore::setSyncBehavior(KOReaderSyncBehavior behavior) { + if (static_cast(behavior) > static_cast(KOReaderSyncBehavior::SMART)) { + behavior = KOReaderSyncBehavior::ASK_EVERY_TIME; + } + syncBehavior = behavior; } diff --git a/lib/KOReaderSync/KOReaderCredentialStore.h b/lib/KOReaderSync/KOReaderCredentialStore.h index 1ac437a1c2..747da2d892 100644 --- a/lib/KOReaderSync/KOReaderCredentialStore.h +++ b/lib/KOReaderSync/KOReaderCredentialStore.h @@ -11,6 +11,12 @@ enum class DocumentMatchMethod : uint8_t { BINARY = 1, // Match by partial MD5 of file content (more accurate, but files must be identical) }; +// How manual "Sync Progress" resolves differences after fetching remote progress. +enum class KOReaderSyncBehavior : uint8_t { + ASK_EVERY_TIME = 0, // Preserve legacy behavior: always show Apply/Upload choices. + SMART = 1, // Auto-resolve simple cases using furthest progress. +}; + /** * Singleton class for storing KOReader sync credentials on the SD card. * Passwords are XOR-obfuscated with the device's unique hardware MAC address @@ -24,6 +30,8 @@ class KOReaderCredentialStore : public PersistableStore std::string password; std::string serverUrl; // Custom sync server URL (empty = default) DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility + bool sendMetadata = false; // Send document metadata with progress sync + KOReaderSyncBehavior syncBehavior = KOReaderSyncBehavior::SMART; // Private constructor for singleton KOReaderCredentialStore() = default; @@ -57,9 +65,20 @@ class KOReaderCredentialStore : public PersistableStore // Get base URL for API calls (with http:// normalization if no protocol, falls back to default) std::string getBaseUrl() const; + // Whether API calls target the CrossPoint sync server that supports protocol extensions. + bool usesCrossPointSyncServer() const; + // Document matching method void setMatchMethod(DocumentMatchMethod method); DocumentMatchMethod getMatchMethod() const { return matchMethod; } + + // Send metadata setting + void setSendMetadata(bool enabled); + bool getSendMetadata() const { return sendMetadata; } + + // Sync behavior + void setSyncBehavior(KOReaderSyncBehavior behavior); + KOReaderSyncBehavior getSyncBehavior() const { return syncBehavior; } }; // Helper macro to access credential store diff --git a/lib/KOReaderSync/KOReaderDocumentId.cpp b/lib/KOReaderSync/KOReaderDocumentId.cpp index 39fe2b109e..24fc245500 100644 --- a/lib/KOReaderSync/KOReaderDocumentId.cpp +++ b/lib/KOReaderSync/KOReaderDocumentId.cpp @@ -27,7 +27,6 @@ std::string KOReaderDocumentId::calculateFromFilename(const std::string& filePat md5.calculate(); std::string result = md5.toString().c_str(); - LOG_DBG("KODoc", "Filename hash: %s (from '%s')", result.c_str(), filename.c_str()); return result; } @@ -49,7 +48,6 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) { } const size_t fileSize = file.fileSize(); - LOG_DBG("KODoc", "Calculating hash for file: %s (size: %zu)", filePath.c_str(), fileSize); // Initialize MD5 builder MD5Builder md5; @@ -88,7 +86,5 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) { md5.calculate(); std::string result = md5.toString().c_str(); - LOG_DBG("KODoc", "Hash calculated: %s (from %zu bytes)", result.c_str(), totalBytesRead); - return result; } diff --git a/lib/KOReaderSync/KOReaderSyncClient.cpp b/lib/KOReaderSync/KOReaderSyncClient.cpp index 1b09a4c1d1..d1af6b50d1 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.cpp +++ b/lib/KOReaderSync/KOReaderSyncClient.cpp @@ -4,25 +4,33 @@ #ifdef SIMULATOR #include #endif -#include #include #include #ifdef SIMULATOR +#include #include #include #else -#include -#include -#include +#include +#include #endif #include #include #include #include +#include #include "KOReaderCredentialStore.h" +#ifndef SIMULATOR +// wolfSSL is built with DEBUG_WOLFSSL, whose Arduino backend expects the app to +// provide this print hook (the stock definition lives in , which no +// translation unit here includes). Route it through the firmware logger, same +// as upstream CrossPoint's HttpDownloader.cpp. +extern "C" void wolfSSL_Arduino_Serial_Print(const char* const msg) { LOG_DBG("WOLFSSL", "%s", msg); } +#endif + int KOReaderSyncClient::lastHttpCode = 0; int KOReaderSyncClient::lastTransportError = 0; @@ -49,21 +57,9 @@ std::string networkErrorMessage() { return tr(STR_KOREADER_SYNC_NETWORK_ERROR); } #else - switch (KOReaderSyncClient::lastTransportError) { - case ESP_ERR_HTTP_CONNECT: - case ESP_ERR_HTTP_CONNECTING: - case ESP_ERR_HTTP_CONNECTION_CLOSED: - return tr(STR_KOREADER_SYNC_NETWORK_REFUSED); - case ESP_ERR_HTTP_FETCH_HEADER: - case ESP_ERR_HTTP_EAGAIN: - case ESP_ERR_HTTP_READ_TIMEOUT: - case ESP_ERR_HTTP_INCOMPLETE_DATA: - return tr(STR_KOREADER_SYNC_NETWORK_TIMEOUT); - case ESP_ERR_HTTP_INVALID_TRANSPORT: - return tr(STR_KOREADER_SYNC_NETWORK_TLS); - default: - return tr(STR_KOREADER_SYNC_NETWORK_ERROR); - } + // SecureHttpClient reports transport failures as a bare -1 (no granular + // error codes), so there is nothing finer to translate on device. + return tr(STR_KOREADER_SYNC_NETWORK_ERROR); #endif } @@ -118,12 +114,13 @@ KOReaderSyncClient::Error validateAuthResponse(const char* body) { return KOReaderSyncClient::OK; } -// Cloudflare tunnels send a 3-cert Google Trust Services chain. During the TLS handshake -// mbedTLS makes many small allocations that collectively consume ~48KB of heap. With only -// ~50KB free after WiFi connects, the session drove min-free-ever down to 2600 bytes before -// failing with MBEDTLS_ERR_X509_ALLOC_FAILED (-0x2880). Check total free heap (not max -// contiguous block) because the failure mode is aggregate exhaustion, not one large alloc. -constexpr uint32_t MIN_HEAP_FOR_TLS = 55000; +// KOSync's TLS-1.3 servers can't be reached through the precompiled system +// mbedTLS (TLS 1.3 is stubbed out), so requests run over wolfSSL via +// SecureHttpClient. The handshake still needs working heap; gate on it. wolfSSL's +// footprint is smaller than mbedTLS's old ~48KB peak, but keep conservative +// floors for total free heap and the largest contiguous block. +constexpr uint32_t MIN_FREE_HEAP_FOR_TLS = 35000; +constexpr uint32_t MIN_MAX_ALLOC_HEAP_FOR_TLS = 20000; #ifdef SIMULATOR void addAuthHeaders(HTTPClient& http) { @@ -135,82 +132,29 @@ void addAuthHeaders(HTTPClient& http) { bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; } #else -// Small TLS buffers to fit in ESP32-C3's limited heap (~46KB free after WiFi). -// KOSync payloads are tiny JSON (<1KB), so 2KB buffers are sufficient. -// Default 16KB buffers cause OOM during TLS handshake. -constexpr int HTTP_BUF_SIZE = 2048; - -void logHeapStats(const char* phase, const char* url = nullptr) { - LOG_DBG("KOSync", "%s%s%s heap: free=%u min=%u max_alloc=%u", phase, url ? " " : "", url ? url : "", - (unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMinFreeHeap(), (unsigned)ESP.getMaxAllocHeap()); +// Apply the shared KOSync auth headers after begin(). x-auth-* is the native +// KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility. +void applyAuthHeaders(freeink::SecureHttpClient& http) { + http.addHeader("Accept", "application/vnd.koreader.v1+json"); + http.addHeader("x-auth-user", KOREADER_STORE.getUsername()); + http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password()); + const std::string credentials = KOREADER_STORE.getUsername() + ":" + KOREADER_STORE.getPassword(); + const String encoded = base64::encode(credentials.c_str()); + http.addHeader("Authorization", std::string("Basic ") + encoded.c_str()); } +#endif -// Response buffer for reading HTTP body -struct ResponseBuffer { - char* data = nullptr; - int len = 0; - int capacity = 0; - - ~ResponseBuffer() { free(data); } - - bool ensure(int size) { - if (size <= capacity) return true; - char* newData = (char*)realloc(data, size); - if (!newData) return false; - data = newData; - capacity = size; +// True when free heap is too low to risk a TLS handshake. +bool insufficientHeap() { + const uint32_t freeHeap = ESP.getFreeHeap(); + const uint32_t maxAllocHeap = ESP.getMaxAllocHeap(); + if (freeHeap < MIN_FREE_HEAP_FOR_TLS || maxAllocHeap < MIN_MAX_ALLOC_HEAP_FOR_TLS) { + LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u), %u max alloc (need %u)", freeHeap, + MIN_FREE_HEAP_FOR_TLS, maxAllocHeap, MIN_MAX_ALLOC_HEAP_FOR_TLS); return true; } -}; - -// HTTP event handler to collect response body -esp_err_t httpEventHandler(esp_http_client_event_t* evt) { - auto* buf = static_cast(evt->user_data); - if (evt->event_id == HTTP_EVENT_ON_DATA && buf) { - if (buf->ensure(buf->len + evt->data_len + 1)) { - memcpy(buf->data + buf->len, evt->data, evt->data_len); - buf->len += evt->data_len; - buf->data[buf->len] = '\0'; - } else { - LOG_ERR("KOSync", "Response buffer allocation failed (%d bytes)", evt->data_len); - } - } - return ESP_OK; -} - -// Create configured esp_http_client with small TLS buffers -esp_http_client_handle_t createClient(const char* url, ResponseBuffer* buf, - esp_http_client_method_t method = HTTP_METHOD_GET) { - esp_http_client_config_t config = {}; - config.url = url; - config.event_handler = httpEventHandler; - config.user_data = buf; - config.method = method; - config.timeout_ms = 15000; - config.buffer_size = HTTP_BUF_SIZE; - config.buffer_size_tx = HTTP_BUF_SIZE; - config.crt_bundle_attach = esp_crt_bundle_attach; - - // HTTP Basic Auth for Calibre-Web-Automated compatibility - config.username = KOREADER_STORE.getUsername().c_str(); - config.password = KOREADER_STORE.getPassword().c_str(); - config.auth_type = HTTP_AUTH_TYPE_BASIC; - - esp_http_client_handle_t client = esp_http_client_init(&config); - if (!client) return nullptr; - - // KOSync auth headers - if (esp_http_client_set_header(client, "Accept", "application/vnd.koreader.v1+json") != ESP_OK || - esp_http_client_set_header(client, "x-auth-user", KOREADER_STORE.getUsername().c_str()) != ESP_OK || - esp_http_client_set_header(client, "x-auth-key", KOREADER_STORE.getMd5Password().c_str()) != ESP_OK) { - LOG_ERR("KOSync", "Failed to set auth headers"); - esp_http_client_cleanup(client); - return nullptr; - } - - return client; + return false; } -#endif } // namespace KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { @@ -221,13 +165,9 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { return NO_CREDENTIALS; } - std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth"; - const uint32_t freeHeap = ESP.getFreeHeap(); - LOG_DBG("KOSync", "Authenticating: %s (heap: %u)", url.c_str(), (unsigned)freeHeap); - if (freeHeap < MIN_HEAP_FOR_TLS) { - LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS); - return LOW_MEMORY; - } + const std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth"; + LOG_DBG("KOSync", "Authenticating: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); + if (insufficientHeap()) return LOW_MEMORY; #ifdef SIMULATOR HTTPClient http; @@ -261,31 +201,71 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { if (httpCode < 0) return NETWORK_ERROR; return SERVER_ERROR; #else - ResponseBuffer buf; - logHeapStats("Before auth client", url.c_str()); - esp_http_client_handle_t client = createClient(url.c_str(), &buf); - if (!client) { - lastTransportError = ESP_ERR_NO_MEM; + freeink::SecureHttpClient http; + http.setInsecure(); + if (!http.begin(url)) { + LOG_ERR("KOSync", "Bad URL: %s", url.c_str()); return NETWORK_ERROR; } - - logHeapStats("Before auth perform"); - esp_err_t err = esp_http_client_perform(client); - const int httpCode = esp_http_client_get_status_code(client); + applyAuthHeaders(http); + const int httpCode = http.GET(); lastHttpCode = httpCode; - lastTransportError = static_cast(err); - logHeapStats("After auth perform"); - esp_http_client_cleanup(client); + lastTransportError = (httpCode < 0) ? httpCode : 0; - LOG_DBG("KOSync", "Auth response: %d (err: %d)", httpCode, err); + LOG_DBG("KOSync", "Auth response: %d", httpCode); - if (err != ESP_OK) return NETWORK_ERROR; - if (httpCode == 200) return validateAuthResponse(buf.data); + if (httpCode <= 0) { + http.end(); + return NETWORK_ERROR; + } + if (httpCode == 200) { + const Error result = validateAuthResponse(http.getString().c_str()); + http.end(); + return result; + } + http.end(); if (httpCode == 401) return AUTH_FAILED; return SERVER_ERROR; #endif } +KOReaderSyncClient::Error KOReaderSyncClient::createUser() { + lastHttpCode = 0; + if (!KOREADER_STORE.hasCredentials()) { + LOG_DBG("KOSync", "No credentials configured"); + return NO_CREDENTIALS; + } + + const std::string url = KOREADER_STORE.getBaseUrl() + "/users/create"; + LOG_DBG("KOSync", "Creating account: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); + if (insufficientHeap()) return LOW_MEMORY; + + JsonDocument doc; + doc["username"] = KOREADER_STORE.getUsername(); + doc["password"] = KOREADER_STORE.getMd5Password(); + std::string body; + serializeJson(doc, body); + + freeink::SecureHttpClient http; + http.setInsecure(); + if (!http.begin(url)) { + LOG_ERR("KOSync", "Bad URL: %s", url.c_str()); + return NETWORK_ERROR; + } + http.addHeader("Accept", "application/vnd.koreader.v1+json"); + http.addHeader("Content-Type", "application/json"); + const int httpCode = http.sendRequest("POST", body); + http.end(); + lastHttpCode = httpCode; + + LOG_DBG("KOSync", "Create user response: %d", httpCode); + + if (httpCode <= 0) return NETWORK_ERROR; + if (httpCode == 200 || httpCode == 201) return OK; + if (httpCode == 402) return USER_EXISTS; + return SERVER_ERROR; +} + KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash, KOReaderProgress& outProgress) { lastHttpCode = 0; @@ -295,13 +275,9 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc return NO_CREDENTIALS; } - std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash; - const uint32_t freeHeap = ESP.getFreeHeap(); - LOG_DBG("KOSync", "Getting progress: %s (heap: %u)", url.c_str(), (unsigned)freeHeap); - if (freeHeap < MIN_HEAP_FOR_TLS) { - LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS); - return LOW_MEMORY; - } + const std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash; + LOG_DBG("KOSync", "Getting progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); + if (insufficientHeap()) return LOW_MEMORY; #ifdef SIMULATOR HTTPClient http; @@ -352,32 +328,32 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc if (httpCode < 0) return NETWORK_ERROR; return SERVER_ERROR; #else - ResponseBuffer buf; - logHeapStats("Before get client", url.c_str()); - esp_http_client_handle_t client = createClient(url.c_str(), &buf); - if (!client) { - lastTransportError = ESP_ERR_NO_MEM; + freeink::SecureHttpClient http; + http.setInsecure(); + if (!http.begin(url)) { + LOG_ERR("KOSync", "Bad URL: %s", url.c_str()); return NETWORK_ERROR; } - - logHeapStats("Before get perform"); - esp_err_t err = esp_http_client_perform(client); - const int httpCode = esp_http_client_get_status_code(client); + applyAuthHeaders(http); + const int httpCode = http.GET(); lastHttpCode = httpCode; - lastTransportError = static_cast(err); - logHeapStats("After get perform"); - esp_http_client_cleanup(client); + lastTransportError = (httpCode < 0) ? httpCode : 0; - LOG_DBG("KOSync", "Get progress response: %d (err: %d)", httpCode, err); + LOG_DBG("KOSync", "Get progress response: %d", httpCode); - if (err != ESP_OK) return NETWORK_ERROR; + if (httpCode <= 0) { + http.end(); + return NETWORK_ERROR; + } - if (httpCode == 200 && buf.data) { + if (httpCode == 200) { + const std::string& body = http.getString(); JsonDocument doc; - const DeserializationError error = deserializeJson(doc, buf.data); + const DeserializationError error = deserializeJson(doc, body); if (error) { - logJsonParseFailure("Get progress", error, buf.data); + logJsonParseFailure("Get progress", error, body.c_str()); + http.end(); return JSON_ERROR; } @@ -388,10 +364,30 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc outProgress.deviceId = doc["device_id"].as(); outProgress.timestamp = doc["timestamp"].as(); + outProgress.position.reset(); + if (KOREADER_STORE.usesCrossPointSyncServer()) { + const JsonObjectConst pos = doc["position"].as(); + if (!pos.isNull()) { + KOReaderRichPosition rich; + rich.pctQ = pos["pctQ"].as(); + rich.spineIndex = pos["spine"].as(); + rich.pageNumber = pos["page"].as(); + const uint16_t pages = pos["pages"].as(); + rich.totalPages = pages > 0 ? pages : 1; + const uint16_t para = pos["para"].as(); + if (para > 0) rich.paragraphIndex = para; + rich.xpath = pos["xpath"].as() ? pos["xpath"].as() : ""; + LOG_DBG("KOSync", "Got rich position: spine=%u page=%u/%u para=%u", rich.spineIndex, rich.pageNumber, + rich.totalPages, para); + outProgress.position = std::move(rich); + } + } + LOG_DBG("KOSync", "Got progress: %.2f%% at %s", outProgress.percentage * 100, outProgress.progress.c_str()); return OK; } + http.end(); if (httpCode == 401) return AUTH_FAILED; if (httpCode == 404) return NOT_FOUND; return SERVER_ERROR; @@ -406,27 +402,39 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr return NO_CREDENTIALS; } - std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress"; - const uint32_t freeHeap = ESP.getFreeHeap(); - LOG_DBG("KOSync", "Updating progress: %s (heap: %u)", url.c_str(), (unsigned)freeHeap); - if (freeHeap < MIN_HEAP_FOR_TLS) { - LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS); - return LOW_MEMORY; - } + const std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress"; + LOG_DBG("KOSync", "Updating progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); + if (insufficientHeap()) return LOW_MEMORY; // Build JSON body JsonDocument doc; doc["document"] = progress.document; + if (progress.metadata.has_value()) { + auto meta = doc["metadata"].to(); + meta["filename"] = progress.metadata->filename; + meta["title"] = progress.metadata->title; + meta["authors"] = progress.metadata->authors; + } doc["progress"] = progress.progress; doc["percentage"] = progress.percentage; doc["device"] = progress.device; doc["device_id"] = DEVICE_ID; + if (progress.position.has_value() && KOREADER_STORE.usesCrossPointSyncServer()) { + // CrossPoint-specific extension: do not send it to third-party KOSync servers. + const auto& p = *progress.position; + auto pos = doc["position"].to(); + pos["pctQ"] = p.pctQ; + pos["spine"] = p.spineIndex; + pos["page"] = p.pageNumber; + pos["pages"] = p.totalPages; + if (p.paragraphIndex.has_value()) pos["para"] = *p.paragraphIndex; + // Server rejects the whole position object if xpath exceeds 120 bytes. + if (!p.xpath.empty() && p.xpath.size() <= 120) pos["xpath"] = p.xpath; + } std::string body; serializeJson(doc, body); - LOG_DBG("KOSync", "Request body: %s", body.c_str()); - #ifdef SIMULATOR HTTPClient http; std::unique_ptr secureClient; @@ -454,34 +462,22 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr if (httpCode < 0) return NETWORK_ERROR; return SERVER_ERROR; #else - ResponseBuffer buf; - logHeapStats("Before put client", url.c_str()); - esp_http_client_handle_t client = createClient(url.c_str(), &buf, HTTP_METHOD_PUT); - if (!client) { - lastTransportError = ESP_ERR_NO_MEM; - return NETWORK_ERROR; - } - - if (esp_http_client_set_header(client, "Content-Type", "application/json") != ESP_OK || - esp_http_client_set_post_field(client, body.c_str(), body.length()) != ESP_OK) { - LOG_ERR("KOSync", "Failed to set request body"); - lastTransportError = ESP_ERR_INVALID_STATE; - esp_http_client_cleanup(client); + freeink::SecureHttpClient http; + http.setInsecure(); + if (!http.begin(url)) { + LOG_ERR("KOSync", "Bad URL: %s", url.c_str()); return NETWORK_ERROR; } - - LOG_DBG("KOSync", "PUT body bytes=%u", static_cast(body.length())); - logHeapStats("Before put perform"); - esp_err_t err = esp_http_client_perform(client); - const int httpCode = esp_http_client_get_status_code(client); + applyAuthHeaders(http); + http.addHeader("Content-Type", "application/json"); + const int httpCode = http.sendRequest("PUT", body); + http.end(); lastHttpCode = httpCode; - lastTransportError = static_cast(err); - logHeapStats("After put perform"); - esp_http_client_cleanup(client); + lastTransportError = (httpCode < 0) ? httpCode : 0; - LOG_DBG("KOSync", "Update progress response: %d (err: %d)", httpCode, err); + LOG_DBG("KOSync", "Update progress response: %d", httpCode); - if (err != ESP_OK) return NETWORK_ERROR; + if (httpCode <= 0) return NETWORK_ERROR; if (httpCode == 200 || httpCode == 202) return OK; if (httpCode == 401) return AUTH_FAILED; return SERVER_ERROR; diff --git a/lib/KOReaderSync/KOReaderSyncClient.h b/lib/KOReaderSync/KOReaderSyncClient.h index e4ddc80085..9cc542618e 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.h +++ b/lib/KOReaderSync/KOReaderSyncClient.h @@ -1,16 +1,46 @@ #pragma once +#include +#include #include +/** + * Optional document metadata sent alongside progress sync requests. + * Mirrors the metadata object added in KOReader PR #15306. + * The official sync server ignores this field; custom servers may use it. + */ +struct KOReaderMetadata { + std::string filename; // e.g. "my_book.epub" + std::string title; // Document title from EPUB metadata + std::string authors; // Author(s) from EPUB metadata +}; + +/** + * Rich CrossPoint position sent alongside progress uploads. Maps 1:1 onto the + * crosspoint-sync extended `position` object (see crosspoint-sync docs/API.md). + * It is only transmitted to sync.crosspointreader.com, where it makes + * CrossPoint<->CrossPoint sync lossless instead of xpath-approximated. + */ +struct KOReaderRichPosition { + uint32_t pctQ = 0; // Percentage quantized 0..1,000,000 (authoritative) + uint16_t spineIndex = 0; // Spine (chapter) index + uint16_t pageNumber = 0; // Page within spine (layout-dependent hint) + uint16_t totalPages = 1; // Spine page count (layout-dependent hint) + std::optional paragraphIndex; // Synthetic 1-based paragraph index + std::string xpath; // KOReader-style xpath (server cap: 120 bytes) +}; + /** * Progress data from KOReader sync server. */ struct KOReaderProgress { - std::string document; // Document hash - std::string progress; // XPath-like progress string - float percentage; // Progress percentage (0.0 to 1.0) - std::string device; // Device name - std::string deviceId; // Device ID - int64_t timestamp; // Unix timestamp of last update + std::string document; // Document hash + std::string progress; // XPath-like progress string + float percentage; // Progress percentage (0.0 to 1.0) + std::string device; // Device name + std::string deviceId; // Device ID + int64_t timestamp; // Unix timestamp of last update + std::optional metadata; // Optional document metadata + std::optional position; // Optional rich position (crosspoint-sync servers only) }; /** @@ -38,7 +68,8 @@ class KOReaderSyncClient { JSON_ERROR, NOT_FOUND, INVALID_AUTH_RESPONSE, - LOW_MEMORY + LOW_MEMORY, + USER_EXISTS }; /** @@ -47,6 +78,14 @@ class KOReaderSyncClient { */ static Error authenticate(); + /** + * Register a new account on the sync server using the stored credentials + * (POST /users/create with the MD5 auth key — the server never sees the + * plain password). + * @return OK on success, USER_EXISTS if the username is taken + */ + static Error createUser(); + /** * Get reading progress for a document. * @param documentHash The document hash (from KOReaderDocumentId) diff --git a/lib/KOReaderSync/ProgressMapper.cpp b/lib/KOReaderSync/ProgressMapper.cpp index 42155a227e..3cfee45393 100644 --- a/lib/KOReaderSync/ProgressMapper.cpp +++ b/lib/KOReaderSync/ProgressMapper.cpp @@ -7,6 +7,7 @@ #include #include "ChapterXPathResolver.h" +#include "Epub/Section.h" #include "Epub/htmlEntities.h" #include "Utf8.h" @@ -141,10 +142,17 @@ int parseXPathSteps(const std::string& xpath, XPathStep steps[MAX_XPATH_DEPTH]) size_t stepsEnd = xpath.rfind("/text()"); if (stepsEnd == std::string::npos) { - stepsEnd = xpath.rfind('.'); - if (stepsEnd == std::string::npos || stepsEnd <= pos || stepsEnd + 1 >= xpath.size()) return 0; - for (size_t i = stepsEnd + 1; i < xpath.size(); i++) { - if (xpath[i] < '0' || xpath[i] > '9') return 0; + const size_t dot = xpath.rfind('.'); + if (dot != std::string::npos && dot > pos) { + if (dot + 1 >= xpath.size()) return 0; + for (size_t i = dot + 1; i < xpath.size(); i++) { + if (xpath[i] < '0' || xpath[i] > '9') return 0; + } + stepsEnd = dot; + } else { + // KOReader also accepts a structural XPath ending at the element itself. + // ChapterXPathResolver emits this form for a zero character offset. + stepsEnd = xpath.size(); } } if (stepsEnd <= pos) return 0; @@ -181,6 +189,92 @@ int parseXPathSteps(const std::string& xpath, XPathStep steps[MAX_XPATH_DEPTH]) return count; } +bool rewriteDocFragment(std::string& xpath, const int spineIndex) { + static constexpr char kPrefix[] = "/body/DocFragment["; + const size_t start = xpath.find(kPrefix); + if (start == std::string::npos) return false; + const size_t numberStart = start + strlen(kPrefix); + const size_t numberEnd = xpath.find(']', numberStart); + if (numberEnd == std::string::npos || spineIndex < 0) return false; + xpath.replace(numberStart, numberEnd - numberStart, std::to_string(spineIndex + 1)); + return true; +} + +bool rewriteXPathStepIndex(std::string& xpath, const int ordinal, const int siblingIndex) { + if (ordinal < 0 || siblingIndex <= 0) return false; + static constexpr char kBody[] = "]/body/"; + const size_t body = xpath.find(kBody); + if (body == std::string::npos) return false; + size_t segmentStart = body + strlen(kBody); + for (int current = 0; current <= ordinal; ++current) { + size_t segmentEnd = xpath.find('/', segmentStart); + if (segmentEnd == std::string::npos) segmentEnd = xpath.size(); + const size_t dot = xpath.find('.', segmentStart); + if (dot != std::string::npos && dot < segmentEnd) segmentEnd = dot; + if (segmentEnd <= segmentStart) return false; + if (current == ordinal) { + const size_t bracket = xpath.find('[', segmentStart); + if (bracket != std::string::npos && bracket < segmentEnd) { + const size_t close = xpath.find(']', bracket + 1); + if (close == std::string::npos || close > segmentEnd) return false; + xpath.replace(bracket + 1, close - bracket - 1, std::to_string(siblingIndex)); + } else { + xpath.insert(segmentEnd, "[" + std::to_string(siblingIndex) + "]"); + } + return true; + } + if (segmentEnd >= xpath.size() || xpath[segmentEnd] != '/') return false; + segmentStart = segmentEnd + 1; + } + return false; +} + +bool mapCurrentXPathToSource(const std::shared_ptr& epub, const int currentSpineIndex, std::string& xpath) { + if (!epub->hasSourceSpineMap()) return !epub->requiresSourceSpineMap(); + Epub::SourceSpineMapEntry entry; + if (!epub->getSourceSpineMapEntry(currentSpineIndex, entry) || !rewriteDocFragment(xpath, entry.sourceSpineIndex)) { + return false; + } + if (entry.rangeCount == 0) return true; + + XPathStep steps[MAX_XPATH_DEPTH]; + const int stepCount = parseXPathSteps(xpath, steps); + if (entry.containerDepth >= stepCount) return false; + XPathStep& child = steps[entry.containerDepth]; + for (size_t i = 0; i < entry.rangeCount; ++i) { + const Epub::SourceChildRange* range = epub->getSourceChildRange(entry, i); + if (!range || strcasecmp(range->name, child.tag) != 0 || child.siblingIndex <= 0 || + child.siblingIndex > range->count) { + continue; + } + return rewriteXPathStepIndex(xpath, entry.containerDepth, child.siblingIndex + range->offset); + } + return false; +} + +bool mapSourceXPathToCurrent(const std::shared_ptr& epub, std::string& xpath, int& currentSpineIndex) { + if (!epub->hasSourceSpineMap()) return !epub->requiresSourceSpineMap(); + const int sourceSpineIndex = parseIndex(xpath, "/body/DocFragment[") - 1; + if (sourceSpineIndex < 0) return false; + + XPathStep steps[MAX_XPATH_DEPTH]; + const int stepCount = parseXPathSteps(xpath, steps); + uint16_t currentSiblingIndex = 0; + bool mapped = false; + int matchedDepth = -1; + for (int depth = 0; depth < stepCount && !mapped; ++depth) { + mapped = epub->findCurrentSpineForSource(sourceSpineIndex, static_cast(depth), steps[depth].tag, + static_cast(steps[depth].siblingIndex), currentSpineIndex, + currentSiblingIndex); + if (mapped) matchedDepth = depth; + } + if (!mapped && stepCount == 0) { + mapped = epub->findCurrentSpineForSource(sourceSpineIndex, 0, nullptr, 0, currentSpineIndex, currentSiblingIndex); + } + if (!mapped || !rewriteDocFragment(xpath, currentSpineIndex)) return false; + return matchedDepth < 0 || rewriteXPathStepIndex(xpath, matchedDepth, currentSiblingIndex); +} + class ParagraphStreamer final : public Print { size_t bytesWritten = 0; bool globalInTag = false; @@ -250,7 +344,8 @@ class ParagraphStreamer final : public Print { bool isNonVisibleTag() const { return strcasecmp(tagName, "head") == 0 || strcasecmp(tagName, "style") == 0 || - strcasecmp(tagName, "script") == 0 || strcasecmp(tagName, "title") == 0; + strcasecmp(tagName, "script") == 0 || strcasecmp(tagName, "title") == 0 || strcasecmp(tagName, "rp") == 0 || + strcasecmp(tagName, "rt") == 0; } static bool isAttrWhitespace(uint8_t c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } @@ -701,12 +796,16 @@ bool streamSpine(const std::shared_ptr& epub, int spineIndex, ParagraphStr } } // namespace -KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr& epub, const CrossPointPosition& pos) { +KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr& epub, const CrossPointPosition& pos, + const PositionCoordinateSpace coordinateSpace) { KOReaderPosition result; float intra = (pos.totalPages > 1) ? static_cast(pos.pageNumber) / static_cast(pos.totalPages - 1) : 0.0f; result.percentage = epub->calculateProgress(pos.spineIndex, intra); - if (pos.hasLiIndex && pos.liIndex > 0) { + if (pos.hasVisibleTextOffset) { + result.xpath = ChapterXPathResolver::findXPathForVisibleTextOffset(epub, pos.spineIndex, pos.visibleTextOffset); + } + if (result.xpath.empty() && pos.hasLiIndex && pos.liIndex > 0) { result.xpath = ChapterXPathResolver::findXPathForListItem(epub, pos.spineIndex, pos.liIndex); } if (result.xpath.empty() && pos.hasParagraphIndex && pos.paragraphIndex > 0) { @@ -719,14 +818,84 @@ KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr& epub, c if (result.xpath.empty()) { result.xpath = generateXPath(epub, pos.spineIndex, intra); } + if (coordinateSpace == PositionCoordinateSpace::SourceDocument && + !mapCurrentXPathToSource(epub, pos.spineIndex, result.xpath)) { + LOG_ERR("PM", "Source position map unavailable for optimized spine %d; re-optimize the EPUB", pos.spineIndex); + result.xpath.clear(); + result.valid = false; + return result; + } LOG_DBG("PM", "-> KO: spine=%d page=%d/%d %.2f%% %s", pos.spineIndex, pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str()); return result; } +std::optional ProgressMapper::fromRichPosition(const std::shared_ptr& epub, + const KOReaderRichPosition& rich, + GfxRenderer& renderer) { + const int spineCount = epub->getSpineItemsCount(); + if (static_cast(rich.spineIndex) >= spineCount) { + LOG_DBG("PM", "Rich position spine %u out of range (%d spine items)", rich.spineIndex, spineCount); + return std::nullopt; + } + + CrossPointPosition result{}; + result.spineIndex = rich.spineIndex; + + Section tempSection(epub, result.spineIndex, renderer); + const auto cachedCount = tempSection.getCachedPageCount(); + if (!cachedCount || *cachedCount <= 0) { + // No local layout for the target spine yet; the percentage/xpath mapping + // handles density estimation better than a blind copy of remote pages. + LOG_DBG("PM", "Rich position spine %u has no cached page count", rich.spineIndex); + return std::nullopt; + } + result.totalPages = *cachedCount; + + const int remotePages = rich.totalPages > 0 ? rich.totalPages : 1; + if (result.totalPages == remotePages) { + // Identical layout (same render settings) — the page transfers losslessly. + result.pageNumber = std::min(rich.pageNumber, result.totalPages - 1); + LOG_DBG("PM", "Rich position exact: spine=%d page=%d/%d", result.spineIndex, result.pageNumber, result.totalPages); + return result; + } + + // Layout differs; the paragraph LUT is the most accurate anchor we have. + if (rich.paragraphIndex.has_value()) { + const auto lutPage = tempSection.getPageForParagraphIndex(*rich.paragraphIndex); + if (lutPage.has_value()) { + result.paragraphIndex = *rich.paragraphIndex; + result.hasParagraphIndex = true; + result.pageNumber = std::min(*lutPage, result.totalPages - 1); + LOG_DBG("PM", "Rich position para %u -> spine=%d page=%d/%d", *rich.paragraphIndex, result.spineIndex, + result.pageNumber, result.totalPages); + return result; + } + } + + // Fall back to the intra-spine page fraction. + const float intra = + (remotePages > 1) ? static_cast(rich.pageNumber) / static_cast(remotePages - 1) : 0.0f; + result.pageNumber = std::max( + 0, std::min(static_cast(intra * static_cast(result.totalPages - 1) + 0.5f), result.totalPages - 1)); + LOG_DBG("PM", "Rich position scaled: spine=%d remote %u/%d -> page=%d/%d", result.spineIndex, rich.pageNumber, + remotePages, result.pageNumber, result.totalPages); + return result; +} + CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr& epub, const KOReaderPosition& koPos, - int currentSpineIndex, int totalPagesInCurrentSpine) { + int currentSpineIndex, int totalPagesInCurrentSpine, + const PositionCoordinateSpace coordinateSpace) { CrossPointPosition result{}; + KOReaderPosition mappedKoPos = koPos; + if (coordinateSpace == PositionCoordinateSpace::SourceDocument) { + int mappedSpineIndex = -1; + if (!mapSourceXPathToCurrent(epub, mappedKoPos.xpath, mappedSpineIndex)) { + LOG_ERR("PM", "Cannot map source XPath into this optimized EPUB; re-optimize the EPUB"); + result.valid = false; + return result; + } + } const size_t bookSize = epub->getBookSize(); if (bookSize == 0) return result; @@ -734,14 +903,14 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr& epu const float clampedPercentage = std::max(0.0f, std::min(1.0f, koPos.percentage)); const size_t targetBytes = static_cast(static_cast(bookSize) * clampedPercentage); - const int docFrag = parseIndex(koPos.xpath, "/body/DocFragment["); - const int xpathP = parseIndex(koPos.xpath, "/p[", true); - const int xpathChar = parseCharOffset(koPos.xpath); - const int xpathTextNode = parseTextNodeIndex(koPos.xpath); + const int docFrag = parseIndex(mappedKoPos.xpath, "/body/DocFragment["); + const int xpathP = parseIndex(mappedKoPos.xpath, "/p[", true); + const int xpathChar = parseCharOffset(mappedKoPos.xpath); + const int xpathTextNode = parseTextNodeIndex(mappedKoPos.xpath); const int xpathSpine = (docFrag >= 1) ? (docFrag - 1) : -1; XPathStep xpathSteps[MAX_XPATH_DEPTH]; - const int xpathStepCount = parseXPathSteps(koPos.xpath, xpathSteps); + const int xpathStepCount = parseXPathSteps(mappedKoPos.xpath, xpathSteps); // Use ancestry mode whenever the XPath has a structured path (always more accurate than global counting). const bool useAncestry = xpathStepCount > 0; @@ -778,6 +947,10 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr& epu if (streamSpine(epub, result.spineIndex, s) && s.found()) { intra = s.progress(); resolvedIntra = true; + if (s.getTargetVisChars() > 0 && s.getTargetVisChars() <= UINT32_MAX) { + result.visibleTextOffset = static_cast(s.getTargetVisChars() - 1); + result.hasVisibleTextOffset = true; + } const int pAtMatch = s.getParagraphAtMatch(); if (pAtMatch > 0) { result.paragraphIndex = static_cast(pAtMatch); @@ -804,14 +977,18 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr& epu if (streamSpine(epub, result.spineIndex, s) && s.found()) { intra = s.progress(); resolvedIntra = true; + if (s.getTargetVisChars() > 0 && s.getTargetVisChars() <= UINT32_MAX) { + result.visibleTextOffset = static_cast(s.getTargetVisChars() - 1); + result.hasVisibleTextOffset = true; + } LOG_DBG("PM", "XPath p[%d]/text()[%d]+%d -> %.1f%% (target=%zu total=%zu)", xpathP, xpathTextNode, xpathChar, intra * 100, s.getTargetVisChars(), s.getTotalVisChars()); } } - if (!resolvedIntra && xpathSpine >= 0 && xpathSpine < spineCount && isChapterStartXPath(koPos.xpath)) { + if (!resolvedIntra && xpathSpine >= 0 && xpathSpine < spineCount && isChapterStartXPath(mappedKoPos.xpath)) { intra = 0.0f; resolvedIntra = true; - LOG_DBG("PM", "Chapter-start XPath %s -> spine=%d page start", koPos.xpath.c_str(), result.spineIndex); + LOG_DBG("PM", "Chapter-start XPath %s -> spine=%d page start", mappedKoPos.xpath.c_str(), result.spineIndex); } if (!resolvedIntra) { const size_t bytesIn = (targetBytes > prevCum) ? (targetBytes - prevCum) : 0; @@ -820,7 +997,7 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr& epu result.pageNumber = std::max( 0, std::min(static_cast(intra * static_cast(result.totalPages - 1) + 0.5f), result.totalPages - 1)); - LOG_DBG("PM", "<- KO: %.2f%% %s -> spine=%d page=%d/%d", koPos.percentage * 100, koPos.xpath.c_str(), + LOG_DBG("PM", "<- KO: %.2f%% %s -> spine=%d page=%d/%d", koPos.percentage * 100, mappedKoPos.xpath.c_str(), result.spineIndex, result.pageNumber, result.totalPages); return result; } diff --git a/lib/KOReaderSync/ProgressMapper.h b/lib/KOReaderSync/ProgressMapper.h index bd6c73c12e..49a82a1166 100644 --- a/lib/KOReaderSync/ProgressMapper.h +++ b/lib/KOReaderSync/ProgressMapper.h @@ -2,8 +2,11 @@ #include #include +#include #include +#include "KOReaderSyncClient.h" + /** * CrossPoint position representation. */ @@ -16,6 +19,9 @@ struct CrossPointPosition { uint16_t liIndex = 0; // Running
  • count at the matched XPath element bool hasLiIndex = false; // True when target element is
  • and liIndex was resolved char xpathAnchorId[64] = {}; // First captured inside the matched XPath element + uint32_t visibleTextOffset = 0; // Zero-based visible codepoint offset in the spine item + bool hasVisibleTextOffset = false; + bool valid = true; // False when an exact requested coordinate mapping is unavailable }; /** @@ -24,6 +30,12 @@ struct CrossPointPosition { struct KOReaderPosition { std::string xpath; // XPath-like progress string float percentage; // Progress percentage (0.0 to 1.0) + bool valid = true; // False when an exact requested coordinate mapping is unavailable +}; + +enum class PositionCoordinateSpace : uint8_t { + CurrentDocument, + SourceDocument, }; /** @@ -45,7 +57,9 @@ class ProgressMapper { * @param pos CrossPoint position * @return KOReader position */ - static KOReaderPosition toKOReader(const std::shared_ptr& epub, const CrossPointPosition& pos); + static KOReaderPosition toKOReader( + const std::shared_ptr& epub, const CrossPointPosition& pos, + PositionCoordinateSpace coordinateSpace = PositionCoordinateSpace::CurrentDocument); /** * Convert KOReader position to CrossPoint format. @@ -59,8 +73,24 @@ class ProgressMapper { * @param totalPagesInCurrentSpine Total pages in the current spine item (for density estimation) * @return CrossPoint position */ - static CrossPointPosition toCrossPoint(const std::shared_ptr& epub, const KOReaderPosition& koPos, - int currentSpineIndex = -1, int totalPagesInCurrentSpine = 0); + static CrossPointPosition toCrossPoint( + const std::shared_ptr& epub, const KOReaderPosition& koPos, int currentSpineIndex = -1, + int totalPagesInCurrentSpine = 0, + PositionCoordinateSpace coordinateSpace = PositionCoordinateSpace::CurrentDocument); + + /** + * Convert a rich CrossPoint position (downloaded from a crosspoint-sync + * server) directly to a CrossPoint position, without XPath approximation. + * When the local layout matches the uploader's (same spine page count) the + * page transfers losslessly; otherwise the paragraph LUT or the intra-spine + * page fraction is used. + * + * @return The position, or std::nullopt when the rich position cannot be + * applied (spine out of range, no section cache) and the caller + * should fall back to toCrossPoint(). + */ + static std::optional fromRichPosition(const std::shared_ptr& epub, + const KOReaderRichPosition& rich, GfxRenderer& renderer); private: /** diff --git a/lib/Logging/Logging.cpp b/lib/Logging/Logging.cpp index a8143bebdc..e3b40f7b99 100644 --- a/lib/Logging/Logging.cpp +++ b/lib/Logging/Logging.cpp @@ -1,5 +1,9 @@ #include "Logging.h" +#include +#include + +#include #include #ifdef SIMULATOR @@ -69,9 +73,18 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) { } } va_end(args); +#if defined(SIMULATOR) + std::fputs(buf, stderr); +#elif FREEINK_LOG_TRANSPORT == FREEINK_LOG_TRANSPORT_ROM_PRINTF + // IDF/ROM console path for boards monitored over USB-Serial-JTAG, where the + // HWCDC `operator bool` reads false under `pio device monitor` and logs would + // otherwise be silently dropped (e.g. Sticky). + esp_rom_printf("%s", buf); +#else if (logSerial) { logSerial.print(buf); } +#endif addToLogRingBuffer(buf); } diff --git a/lib/Logging/Logging.h b/lib/Logging/Logging.h index 8784a3418f..dcea1177ae 100644 --- a/lib/Logging/Logging.h +++ b/lib/Logging/Logging.h @@ -1,6 +1,7 @@ #pragma once -#include +#include +#include #include @@ -19,15 +20,16 @@ special formatting), use the underlying logSerial object directly: logSerial.printf("Special case: %d\n", value); logSerial.write(binaryData, length); -The logSerial reference (defined below) points to the real Serial object and -won't trigger deprecation warnings. +The logSerial reference (defined below) points to the board's physical USB +serial transport and won't trigger deprecation warnings. */ #ifndef LOG_LEVEL #define LOG_LEVEL 0 #endif -static HWCDC& logSerial = Serial; +static auto& logSerial = BoardConfig::serialTransport(); +#define LOG_SERIAL_HAS_TX_TIMEOUT FREEINK_SERIAL_HAS_TX_TIMEOUT void logPrintf(const char* level, const char* origin, const char* format, ...); diff --git a/lib/Memory/BuildScratch.cpp b/lib/Memory/BuildScratch.cpp new file mode 100644 index 0000000000..aaae6f0bdd --- /dev/null +++ b/lib/Memory/BuildScratch.cpp @@ -0,0 +1,51 @@ +#include "BuildScratch.h" + +#include + +#include + +namespace buildscratch { +namespace { +uint8_t* block = nullptr; +size_t blockLen = 0; +// atomic exchange so an opportunistic claim from another task can never +// double-hand-out the block (single core, but FreeRTOS preempts). +std::atomic claimed{false}; +} // namespace + +void lend(uint8_t* buf, const size_t len) { + if (block) { + LOG_ERR("SCR", "Build scratch lent twice; ignoring second lend"); + return; + } + block = buf; + blockLen = len; + claimed.store(false); +} + +void reclaim() { + if (claimed.load()) { + // A consumer still holds the block. The storage stays valid (it is the + // framebuffer allocation, never freed) but its contents are about to be + // clobbered; the consumer's output will be garbage. Loud log so a + // lifetime bug is visible instead of a silent corrupt decode. + LOG_ERR("SCR", "Build scratch reclaimed while still claimed"); + } + block = nullptr; + blockLen = 0; + claimed.store(false); +} + +uint8_t* claim(const size_t minLen, size_t* lenOut) { + if (!block || blockLen < minLen) return nullptr; + bool expected = false; + if (!claimed.compare_exchange_strong(expected, true)) return nullptr; + if (lenOut) *lenOut = blockLen; + return block; +} + +void release(const uint8_t* p) { + if (p && p == block) claimed.store(false); +} + +} // namespace buildscratch diff --git a/lib/Memory/BuildScratch.h b/lib/Memory/BuildScratch.h new file mode 100644 index 0000000000..56237fb320 --- /dev/null +++ b/lib/Memory/BuildScratch.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +// Registry for the framebuffer bytes lent out during a build phase +// (GfxRenderer::FrameBufferLoan). The lender (GfxRenderer) deposits the block +// with lend()/reclaim(); a memory-hungry consumer (e.g. InflateStream's ~43KB +// tinfl state + window) may claim() it instead of allocating from the heap. +// +// Exactly one claimant at a time; claim() returns nullptr when the block is +// absent or already claimed, and consumers must fall back to the heap. The +// underlying storage is the framebuffer allocation itself, which is never +// freed -- so even the pathological case (reclaim() while still claimed, which +// logs an error) reads garbage, never freed memory. +namespace buildscratch { + +// Lender side (GfxRenderer only). +void lend(uint8_t* buf, size_t len); +void reclaim(); + +// Consumer side: exclusive claim of the whole block if it is at least minLen +// bytes; nullptr means "use the heap". Release with the same pointer. +uint8_t* claim(size_t minLen, size_t* lenOut = nullptr); +void release(const uint8_t* p); + +} // namespace buildscratch diff --git a/lib/Memory/ScratchWorkspace.cpp b/lib/Memory/ScratchWorkspace.cpp deleted file mode 100644 index 9235bc24b6..0000000000 --- a/lib/Memory/ScratchWorkspace.cpp +++ /dev/null @@ -1,181 +0,0 @@ -#include "ScratchWorkspace.h" - -#include -#include -#include -#include - -#include -#include - -namespace { -uint8_t* gBuffer = nullptr; -size_t gCapacity = 0; -bool gLeased = false; -bool gBorrowed = false; -bool gReleasePending = false; -const char* gLeaseOwner = nullptr; -const char* gBorrowOwner = nullptr; -std::atomic gMutex{nullptr}; - -bool ensureMutex() { - SemaphoreHandle_t mutex = gMutex.load(std::memory_order_acquire); - if (mutex) return true; - - SemaphoreHandle_t created = xSemaphoreCreateMutex(); - if (!created) { - LOG_ERR("SCR", "Failed to create scratch workspace mutex"); - return false; - } - - mutex = nullptr; - if (!gMutex.compare_exchange_strong(mutex, created, std::memory_order_release, std::memory_order_acquire)) { - vSemaphoreDelete(created); - } - return true; -} - -bool lock() { - if (!ensureMutex()) return false; - SemaphoreHandle_t mutex = gMutex.load(std::memory_order_acquire); - return mutex && xSemaphoreTake(mutex, portMAX_DELAY) == pdTRUE; -} - -void unlock() { - SemaphoreHandle_t mutex = gMutex.load(std::memory_order_acquire); - if (mutex) xSemaphoreGive(mutex); -} - -} // namespace - -namespace ScratchWorkspace { - -bool initialize() { return ensureMutex(); } - -void releaseLease(Lease& lease) { - if (!lease.valid) return; - if (!lock()) { - lease.valid = false; - lease.capacity = 0; - return; - } - - if (gBorrowed) { - LOG_ERR("SCR", "Deferring scratch workspace release while borrowed by %s", gBorrowOwner ? gBorrowOwner : "unknown"); - gReleasePending = true; - lease.valid = false; - lease.capacity = 0; - unlock(); - return; - } - free(gBuffer); - gBuffer = nullptr; - gCapacity = 0; - gLeased = false; - gReleasePending = false; - gLeaseOwner = nullptr; - lease.valid = false; - lease.capacity = 0; - unlock(); -} - -void releaseBorrow(Borrow& borrow) { - if (!borrow.buffer) return; - if (lock()) { - gBorrowed = false; - gBorrowOwner = nullptr; - if (gReleasePending) { - free(gBuffer); - gBuffer = nullptr; - gCapacity = 0; - gLeased = false; - gReleasePending = false; - gLeaseOwner = nullptr; - } - unlock(); - } - borrow.buffer = nullptr; - borrow.capacity = 0; -} - -Lease::Lease(Lease&& other) noexcept : valid(other.valid), capacity(other.capacity) { - other.valid = false; - other.capacity = 0; -} - -Lease& Lease::operator=(Lease&& other) noexcept { - if (this != &other) { - releaseLease(*this); - valid = other.valid; - capacity = other.capacity; - other.valid = false; - other.capacity = 0; - } - return *this; -} - -Lease::~Lease() { releaseLease(*this); } - -Borrow::Borrow(Borrow&& other) noexcept : buffer(other.buffer), capacity(other.capacity) { - other.buffer = nullptr; - other.capacity = 0; -} - -Borrow& Borrow::operator=(Borrow&& other) noexcept { - if (this != &other) { - releaseBorrow(*this); - buffer = other.buffer; - capacity = other.capacity; - other.buffer = nullptr; - other.capacity = 0; - } - return *this; -} - -Borrow::~Borrow() { releaseBorrow(*this); } - -Lease acquire(const size_t bytes, const char* owner) { - Lease lease; - if (bytes == 0 || !lock()) return lease; - if (gLeased) { - LOG_DBG("SCR", "Scratch workspace already leased by %s", gLeaseOwner ? gLeaseOwner : "unknown"); - unlock(); - return lease; - } - - gBuffer = static_cast(malloc(bytes)); - if (!gBuffer) { - LOG_ERR("SCR", "Failed to allocate scratch workspace for %s (%u bytes, free=%u, maxAlloc=%u)", - owner ? owner : "unknown", static_cast(bytes), ESP.getFreeHeap(), ESP.getMaxAllocHeap()); - unlock(); - return lease; - } - - gCapacity = bytes; - gLeased = true; - gBorrowed = false; - gReleasePending = false; - gLeaseOwner = owner; - lease.valid = true; - lease.capacity = bytes; - unlock(); - return lease; -} - -Borrow borrow(const size_t minBytes, const char* owner) { - Borrow borrowed; - if (minBytes == 0 || !lock()) return borrowed; - if (!gLeased || gBorrowed || gCapacity < minBytes) { - unlock(); - return borrowed; - } - - gBorrowed = true; - gBorrowOwner = owner; - borrowed.buffer = gBuffer; - borrowed.capacity = gCapacity; - unlock(); - return borrowed; -} - -} // namespace ScratchWorkspace diff --git a/lib/Memory/ScratchWorkspace.h b/lib/Memory/ScratchWorkspace.h deleted file mode 100644 index 1f29465fb0..0000000000 --- a/lib/Memory/ScratchWorkspace.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include -#include - -namespace ScratchWorkspace { - -class Lease final { - public: - Lease() = default; - Lease(Lease&& other) noexcept; - Lease& operator=(Lease&& other) noexcept; - Lease(const Lease&) = delete; - Lease& operator=(const Lease&) = delete; - ~Lease(); - - explicit operator bool() const { return valid; } - bool isValid() const { return valid; } - size_t size() const { return capacity; } - - private: - friend Lease acquire(size_t bytes, const char* owner); - friend void releaseLease(Lease& lease); - bool valid = false; - size_t capacity = 0; -}; - -class Borrow final { - public: - Borrow() = default; - Borrow(Borrow&& other) noexcept; - Borrow& operator=(Borrow&& other) noexcept; - Borrow(const Borrow&) = delete; - Borrow& operator=(const Borrow&) = delete; - ~Borrow(); - - explicit operator bool() const { return buffer != nullptr; } - uint8_t* data() const { return buffer; } - size_t size() const { return capacity; } - - private: - friend Borrow borrow(size_t minBytes, const char* owner); - friend void releaseBorrow(Borrow& borrow); - uint8_t* buffer = nullptr; - size_t capacity = 0; -}; - -Lease acquire(size_t bytes, const char* owner); -Borrow borrow(size_t minBytes, const char* owner); -bool initialize(); -void releaseLease(Lease& lease); -void releaseBorrow(Borrow& borrow); - -} // namespace ScratchWorkspace diff --git a/lib/MemoryBudget/MemoryBudget.h b/lib/MemoryBudget/MemoryBudget.h index 73c0633d51..addb6322a8 100644 --- a/lib/MemoryBudget/MemoryBudget.h +++ b/lib/MemoryBudget/MemoryBudget.h @@ -6,6 +6,10 @@ #include #include +#if defined(ARDUINO_ARCH_ESP32) && !defined(SIMULATOR) +#include +#endif + namespace MemoryBudget { struct HeapSnapshot { @@ -13,6 +17,13 @@ struct HeapSnapshot { uint32_t maxAllocHeap; }; +struct HeapShapeSnapshot { + uint32_t freeHeap; + uint32_t maxAllocHeap; + uint32_t freeBlocks; + uint32_t allocatedBlocks; +}; + struct HeapRequirement { uint32_t minFree; uint32_t minMaxAlloc; @@ -20,10 +31,18 @@ struct HeapRequirement { constexpr uint32_t EPUB_INLINE_IMAGE_MIN_FREE = 72U * 1024U; constexpr uint32_t EPUB_INLINE_IMAGE_MIN_MAX_ALLOC = 48U * 1024U; +constexpr uint32_t EPUB_TEXT_LAYOUT_MIN_FREE = 44U * 1024U; +constexpr uint32_t EPUB_TEXT_LAYOUT_MIN_MAX_ALLOC = 32U * 1024U; constexpr uint32_t EPUB_INLINE_IMAGE_SD_FONT_RELEASE_MIN_FREE = 120U * 1024U; constexpr uint32_t EPUB_INLINE_IMAGE_SD_FONT_RELEASE_MIN_MAX_ALLOC = 80U * 1024U; constexpr uint32_t OPTIONAL_EPUB_REBUILD_MIN_FREE = 96U * 1024U; constexpr uint32_t OPTIONAL_EPUB_REBUILD_MIN_MAX_ALLOC = 48U * 1024U; +constexpr uint32_t OPTIONAL_EPUB_PREFETCH_AFTER_SD_FONT_RELEASE_MIN_FREE = 88U * 1024U; +// Initial C3 guard for switching to a different dictionary .cpfont. Both total +// free heap and contiguous maxAlloc matter because font metadata and prewarm +// arenas are separate allocations. Hardware stress logs should tune these. +constexpr uint32_t DICTIONARY_SD_FONT_MIN_FREE = 64U * 1024U; +constexpr uint32_t DICTIONARY_SD_FONT_MIN_MAX_ALLOC = 32U * 1024U; constexpr uint32_t IMAGE_DECODER_HEADROOM = 16U * 1024U; constexpr uint32_t JPEG_DECODER_APPROX_BYTES = 20U * 1024U; constexpr uint32_t EPUB_INLINE_JPEG_MIN_FREE = JPEG_DECODER_APPROX_BYTES + IMAGE_DECODER_HEADROOM; @@ -31,10 +50,46 @@ constexpr uint32_t EPUB_INLINE_JPEG_MIN_MAX_ALLOC = JPEG_DECODER_APPROX_BYTES; inline HeapSnapshot snapshot() { return {ESP.getFreeHeap(), ESP.getMaxAllocHeap()}; } +inline HeapShapeSnapshot shapeSnapshot() { + const auto heap = snapshot(); +#if defined(ARDUINO_ARCH_ESP32) && !defined(SIMULATOR) + multi_heap_info_t info{}; + heap_caps_get_info(&info, MALLOC_CAP_INTERNAL); + return {heap.freeHeap, heap.maxAllocHeap, static_cast(info.free_blocks), + static_cast(info.allocated_blocks)}; +#else + return {heap.freeHeap, heap.maxAllocHeap, 0, 0}; +#endif +} + +inline void logHeapShape(const char* stage) { +#if defined(ENABLE_SERIAL_LOG) && LOG_LEVEL >= 2 + const auto heap = shapeSnapshot(); + const uint32_t largestPct = heap.freeHeap == 0 ? 0 : heap.maxAllocHeap * 100U / heap.freeHeap; + LOG_DBG("HEAP", "stage=%s free=%u max=%u freeBlocks=%u allocBlocks=%u largestPct=%u", stage, heap.freeHeap, + heap.maxAllocHeap, heap.freeBlocks, heap.allocatedBlocks, largestPct); +#else + (void)stage; +#endif +} + inline bool hasHeap(const HeapSnapshot heap, const uint32_t minFree, const uint32_t minMaxAlloc) { return heap.freeHeap >= minFree && heap.maxAllocHeap >= minMaxAlloc; } +// Text layout starts with small, fallible allocations: a 4 KB scratch arena and +// a ParsedText object. The operations which really need a large contiguous block +// (table buffering, advance prewarming, and image decoding) each have their own +// max-allocation gate. Do not reject a whole chapter merely because fragmentation +// leaves the general 32 KB background-build threshold a few bytes short. +inline bool hasHeapForEpubTextLayoutStart(const HeapSnapshot heap) { + return heap.freeHeap >= EPUB_TEXT_LAYOUT_MIN_FREE; +} + +inline bool hasHeapForDictionarySdFont(const HeapSnapshot heap) { + return hasHeap(heap, DICTIONARY_SD_FONT_MIN_FREE, DICTIONARY_SD_FONT_MIN_MAX_ALLOC); +} + inline char asciiLower(const char c) { return (c >= 'A' && c <= 'Z') ? static_cast(c - 'A' + 'a') : c; } inline bool endsWithIgnoreCase(const char* value, const char* suffix) { @@ -77,15 +132,16 @@ inline bool hasHeapForEpubInlineImage(const char* tag, const char* source) { return false; } -inline bool hasHeapForOptionalEpubRebuild(const char* tag, const char* action, const int spineIndex) { +inline bool hasHeapForOptionalEpubRebuild(const char* tag, const char* action, const int spineIndex, + const uint32_t minFree = OPTIONAL_EPUB_REBUILD_MIN_FREE, + const uint32_t minMaxAlloc = OPTIONAL_EPUB_REBUILD_MIN_MAX_ALLOC) { const auto heap = snapshot(); - if (hasHeap(heap, OPTIONAL_EPUB_REBUILD_MIN_FREE, OPTIONAL_EPUB_REBUILD_MIN_MAX_ALLOC)) { + if (hasHeap(heap, minFree, minMaxAlloc)) { return true; } LOG_DBG(tag, "Skipping %s for spine %d: low heap (free=%u, maxAlloc=%u, need free>=%u maxAlloc>=%u)", action, - spineIndex, heap.freeHeap, heap.maxAllocHeap, OPTIONAL_EPUB_REBUILD_MIN_FREE, - OPTIONAL_EPUB_REBUILD_MIN_MAX_ALLOC); + spineIndex, heap.freeHeap, heap.maxAllocHeap, minFree, minMaxAlloc); return false; } diff --git a/lib/MiniBidi/BidiUtils.cpp b/lib/MiniBidi/BidiUtils.cpp index 07f061f88f..88aabc0cd1 100644 --- a/lib/MiniBidi/BidiUtils.cpp +++ b/lib/MiniBidi/BidiUtils.cpp @@ -11,6 +11,13 @@ extern "C" { #include #include +#include + +// Guards the static bidi_char buffers in applyBidiVisual() and +// computeVisualWordOrder(). The bidi+shaping pipeline is not reentrant; +// this mutex serialises access so multi-core callers don't corrupt each +// other's intermediate state. +static std::mutex bidiMutex; namespace { @@ -27,6 +34,14 @@ bool isNaturalDirectionClass(const uchar cls) { } } +// Shared visual-reorder scratch for applyBidiVisual() and +// computeVisualWordOrder(). Both run on the single render task and never call +// each other, so one buffer is reused instead of a per-function static — saving +// ~1.5 KB of always-resident RAM. Not reentrant (same assumption as the +// previous per-function statics). +bidi_char sharedBidiLine[BIDI_MAX_LINE]; +bidi_char sharedBidiShaped[BIDI_MAX_LINE]; + } // namespace namespace BidiUtils { @@ -68,11 +83,25 @@ int detectParagraphLevel(const char* utf8, const int fallbackLevel, const int ma return fallbackLevel & 1; } +bool isTransparentMark(const uint32_t cp) { + // RTL-script combining marks: Hebrew niqqud/cantillation and Arabic + // harakat/Quranic annotation. Transparent for Arabic joining (do_shape + // skips them), zero-advance for measurement, and rendered as overlays on + // the preceding base glyph when the active font carries their glyphs. + // The cp >= 0x0591 guard keeps Latin combining marks (U+0300-U+036F, also + // NSM) on their existing utf8IsCombiningMark() rendering path. + return cp >= 0x0591 && bidi_class(cp) == NSM; +} + bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel) { if (!utf8 || !*utf8) return false; + const std::lock_guard lock(bidiMutex); - static bidi_char line[BIDI_MAX_LINE]; + bidi_char* const line = sharedBidiLine; + bidi_char* const shaped = sharedBidiShaped; int count = 0; + int lastBase = -1; // last non-formatter character (mintty's ibase) + uint8_t pendingJoiners = 0; // ZWJ/ZWNJ seen since lastBase auto* p = reinterpret_cast(utf8); while (*p) { if (count >= BIDI_MAX_LINE) { @@ -84,18 +113,66 @@ bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel) { if (!cp || cp == REPLACEMENT_GLYPH) break; line[count].origwc = line[count].wc = cp; line[count].index = static_cast(count); + line[count].joiners = 0; + + // Flag Arabic joining formatters mintty-style (termline.c): the ZWJ/ZWNJ + // goes into the low nibble of the character it follows and the high + // nibble of the character it precedes. Flags are assigned in logical + // order here; do_shape() reads them after reordering. + if (cp == 0x200C || cp == 0x200D) { + const uint8_t joiner = (cp == 0x200D) ? ZWJ : ZWNJ; + if (lastBase >= 0) line[lastBase].joiners |= joiner; + pendingJoiners |= joiner; + } else { + line[count].joiners = pendingJoiners << 4; + pendingJoiners = 0; + lastBase = count; + } count++; } if (!count) return false; const bool autodir = (paragraphLevel < 0); const int level = autodir ? 0 : (paragraphLevel & 1); + + // Order matters (mintty does the same): do_bidi() first to obtain visual + // order, then do_shape() — contextual forms are resolved from *visual* + // adjacency, and shaping presentation forms must never be reordered. do_bidi(autodir, level, line, count); + do_shape(line, shaped, count); out.clear(); out.reserve(std::strlen(utf8)); + // Lam-Alef collapse sentinel and zero-width joining formatters have done + // their job during shaping and have no glyphs to render. + const auto filtered = [](const uint32_t cp) { return cp == LIGATURE_PLACEHOLDER || cp == 0x200C || cp == 0x200D; }; for (int i = 0; i < count; i++) { - utf8AppendCodepoint(line[i].wc, out); + const uint32_t cp = shaped[i].wc; + if (filtered(cp)) continue; + if (!isTransparentMark(cp)) { + utf8AppendCodepoint(cp, out); + continue; + } + // UAX#9 rule L3: reversing an RTL run leaves combining marks *before* + // their base character. The renderer overlays a mark on the most + // recently drawn glyph, so emit the base first, then its marks in + // logical order. `index` is the original logical position: a base + // following its marks with a *lower* index means the run was reversed. + int j = i; // [i, j) = the run of marks (and filtered entries) + while (j < count && (filtered(shaped[j].wc) || isTransparentMark(shaped[j].wc))) j++; + if (j < count && shaped[j].index < shaped[i].index) { + utf8AppendCodepoint(shaped[j].wc, out); + for (int k = j - 1; k >= i; k--) { + if (isTransparentMark(shaped[k].wc)) utf8AppendCodepoint(shaped[k].wc, out); + } + i = j; // base already emitted + } else { + // Unreversed (or trailing, base-less) marks already follow their base. + for (int k = i; k < j; k++) { + if (isTransparentMark(shaped[k].wc)) utf8AppendCodepoint(shaped[k].wc, out); + } + i = j - 1; + } } return true; } @@ -105,8 +182,9 @@ bool computeVisualWordOrder(const std::vector& words, bool paragrap visualOrder.clear(); const size_t nWords = words.size(); if (nWords <= 1 || nWords > BIDI_MAX_LINE) return false; + const std::lock_guard lock(bidiMutex); - static bidi_char line[BIDI_MAX_LINE]; + bidi_char* const line = sharedBidiLine; int count = 0; bool truncated = false; @@ -121,6 +199,7 @@ bool computeVisualWordOrder(const std::vector& words, bool paragrap if (!cp || cp == REPLACEMENT_GLYPH) break; line[count].origwc = line[count].wc = cp; line[count].index = static_cast(w); + line[count].joiners = 0; count++; } @@ -131,6 +210,7 @@ bool computeVisualWordOrder(const std::vector& words, bool paragrap } line[count].origwc = line[count].wc = ' '; line[count].index = static_cast(nWords); + line[count].joiners = 0; count++; } } diff --git a/lib/MiniBidi/BidiUtils.h b/lib/MiniBidi/BidiUtils.h index c36c9a36da..466375c84e 100644 --- a/lib/MiniBidi/BidiUtils.h +++ b/lib/MiniBidi/BidiUtils.h @@ -13,6 +13,14 @@ bool startsWithRtl(const char* utf8, int maxStrongChars = RTL_PARAGRAPH_PROBE_DE int detectParagraphLevel(const char* utf8, int fallbackLevel = 0, int maxStrongChars = 64); +// True for RTL-script non-spacing marks (Hebrew niqqud/cantillation, Arabic +// harakat and Quranic annotation): zero-width for measurement, transparent for +// Arabic joining, and rendered as overlays on the preceding base glyph when +// the active font carries their glyphs (SD fonts; built-in fonts don't). +// Latin combining marks (U+0300-U+036F) intentionally return false — they are +// handled by the utf8IsCombiningMark() rendering path. +bool isTransparentMark(uint32_t cp); + // paragraphLevel: -1 = auto-detect, 0 = LTR, 1 = RTL bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel = -1); diff --git a/lib/MiniBidi/bidiclasses.t b/lib/MiniBidi/bidiclasses.t index f300d3aedb..10a0ccf67f 100644 --- a/lib/MiniBidi/bidiclasses.t +++ b/lib/MiniBidi/bidiclasses.t @@ -1,15 +1,19 @@ -/* bidiclasses.t — bidi class table for CrossPoint Hebrew/English epub. +/* bidiclasses.t — bidi class table for CrossPoint RTL (Hebrew/Arabic) epub. * * Coverage rationale: - * Hebrew + English is the primary target. However, CrossPoint renders - * Latin and Cyrillic scripts for many other languages, so these MUST be - * classified as L (not fall through to ON) to avoid regression when they - * appear adjacent to Hebrew runs. + * Hebrew and Arabic-script languages (Arabic, Farsi, Urdu, Sindhi, Pashto, + * Kurdish) are the RTL targets. CrossPoint also renders Latin and Cyrillic + * scripts for many other languages, so these MUST be classified as L (not + * fall through to ON) to avoid regression when they appear adjacent to + * RTL runs. * * Scripts NOT in this table fall through to ON — correct per UAX#9 for - * scripts CrossPoint's fonts don't support (CJK, Arabic, Devanagari, etc.) + * scripts CrossPoint's fonts don't support (CJK, Devanagari, etc.) * ON is the right class for "unknown" — it behaves neutrally. * + * Arabic ranges are sourced from Unicode UCD extracted/DerivedBidiClass.txt + * (values verified against Unicode 17.0.0). + * * Entries sorted ascending by first (binary search requirement). */ @@ -84,6 +88,36 @@ {0x05D0, 0x05EA, R}, /* alef … tav */ {0x05F0, 0x05F4, R}, /* alternative forms + geresh/gershayim */ +/* ── Arabic / Perso-Arabic (DerivedBidiClass.txt) ───────────────────── */ +/* Letters are AL (Arabic Letter), harakat/marks are NSM, Arabic-Indic + digits are AN, extended (Farsi/Urdu) digits are EN. + 0x0606-0x0607, 0x060E-0x060F, 0x06DE, 0x06E9 are ON — fall through. */ +{0x0600, 0x0605, AN}, /* Arabic number signs (Cf) */ +{0x0608, 0x0608, AL}, /* Arabic ray */ +{0x0609, 0x060A, ET}, /* per mille / per ten thousand */ +{0x060B, 0x060B, AL}, /* afghani sign */ +{0x060C, 0x060C, CS}, /* Arabic comma */ +{0x060D, 0x060D, AL}, /* Arabic date separator */ +{0x0610, 0x061A, NSM}, /* honorifics + small marks */ +{0x061B, 0x061F, AL}, /* semicolon, ALM, end-of-text, hamza mark, question mark */ +{0x0620, 0x064A, AL}, /* core Arabic letters + tatweel */ +{0x064B, 0x065F, NSM}, /* harakat (fathatan … wavy hamza below) */ +{0x0660, 0x0669, AN}, /* Arabic-Indic digits ٠-٩ */ +{0x066A, 0x066A, ET}, /* Arabic percent sign */ +{0x066B, 0x066C, AN}, /* decimal / thousands separators */ +{0x066D, 0x066F, AL}, /* five-pointed star, dotless beh/qaf */ +{0x0670, 0x0670, NSM}, /* superscript alef */ +{0x0671, 0x06D5, AL}, /* extended letters: Farsi, Urdu, Sindhi, Pashto, Kurdish */ +{0x06D6, 0x06DC, NSM}, /* Quranic annotation marks */ +{0x06DD, 0x06DD, AN}, /* end of ayah */ +{0x06DF, 0x06E4, NSM}, +{0x06E5, 0x06E6, AL}, /* small waw / small yeh */ +{0x06E7, 0x06E8, NSM}, +{0x06EA, 0x06ED, NSM}, +{0x06EE, 0x06EF, AL}, /* dal/reh with inverted V */ +{0x06F0, 0x06F9, EN}, /* extended Arabic-Indic digits ۰-۹ (Farsi/Urdu) — EN per UCD */ +{0x06FA, 0x06FF, AL}, + /* ── Latin Extended Additional (L) ─────────────────────────────────── */ /* Covers accented chars for Vietnamese, Welsh, Romanian, etc. Not currently rendered by CrossPoint fonts, but costs only 2 table rows. */ @@ -110,6 +144,14 @@ {0x2069, 0x2069, PDI}, {0x206A, 0x206F, BN}, +/* ── Arabic presentation forms (output of do_shape()) ───────────────── */ +/* Contextual/ligature forms emitted by the shaper must classify as AL so + a reshaped line still resolves RTL. Ranges per DerivedBidiClass.txt; + 0xFBC3-0xFBD2 are ON — fall through. */ +{0xFB50, 0xFBC2, AL}, /* Presentation Forms-A: Perso-Arabic contextual forms */ +{0xFBD3, 0xFBFF, AL}, /* Presentation Forms-A: NG … Farsi Yeh forms */ +{0xFE70, 0xFE74, AL}, /* Presentation Forms-B: harakat isolated forms */ +{0xFE76, 0xFEFC, AL}, /* Presentation Forms-B: contextual forms + Lam-Alef ligatures */ /* ── Byte Order Mark ────────────────────────────────────────────────── */ {0xFEFF, 0xFEFF, BN}, diff --git a/lib/MiniBidi/minibidi.c b/lib/MiniBidi/minibidi.c index 262837020d..68d90c26d4 100644 --- a/lib/MiniBidi/minibidi.c +++ b/lib/MiniBidi/minibidi.c @@ -127,6 +127,345 @@ ucschar mirror(ucschar c) { return p ? p->to : c; } +/* ═══════════════════════════════════════════════════════════════════════ + * Arabic contextual shaping — do_shape() + * + * Ported from mintty src/minibidi.c (https://github.com/mintty/mintty), + * original author Ahmad Khalifa (www.arabeyes.org), maintained by + * Thomas Wolff. MIT licence. + * + * CrossPoint deviations from the upstream code, each marked inline: + * 1. Joining-context lookups skip NSM marks (harakat). mintty stores + * combining marks out-of-line in terminal cells, so they never appear + * in its bidi_char array; in CrossPoint they are real array entries + * and must be transparent for joining (Unicode joining type T). + * 2. The Alef absorbed into a Lam-Alef ligature is overwritten with + * LIGATURE_PLACEHOLDER instead of a space: a terminal must keep the + * cell, a proportional-text renderer must drop the character. + * 3. The STYPE/SISOLATED macros became functions backed by a second + * lookup table covering Perso-Arabic letters outside mintty's native + * U+0621–U+064A range (Farsi پ چ ژ گ, Urdu ٹ ڈ ڑ ں ہ ے, plus Sindhi/ + * Pashto/Kurdish letters). Joining types are sourced from Unicode + * ArabicShaping.txt and presentation forms from UnicodeData.txt + * (Arabic Presentation Forms-A), both Unicode 17.0.0. Letters with a + * joining type but no presentation-form codepoints keep their base + * codepoint (neighbours still shape correctly around them). + * U+200C/U+200D also get their ArabicShaping.txt types (U and C) so + * that in-stream ZWJ/ZWNJ — which mintty never has in its array — + * affect adjacency the same way the joiners flags do. + * ═══════════════════════════════════════════════════════════════════════ */ + +/* Shaping Types (mintty) */ +enum { + SL, /* Left-Joining, doesn't exist in U+0600 - U+06FF */ + SR, /* Right-Joining, i.e. has Isolated, Final */ + SD, /* Dual-Joining, i.e. has Isolated, Final, Initial, Medial */ + SU, /* Non-Joining */ + SC /* Join-Causing, like U+0640 (TATWEEL) */ +}; + +typedef struct { + uchar type; + uchar form_b; /* isolated form = 0xFE00 + form_b (Presentation Forms-B) */ +} shape_node; + +/* Kept near the actual table, for verification. (mintty) */ +enum { SHAPE_FIRST = 0x621, SHAPE_LAST = 0x64A }; + +/* mintty's shapetypes[] — verbatim */ +static const shape_node shapetypes[] = { + /* index, Typ, Iso, Ligature Index */ + /* 621 */ {SU, 0x80}, + /* 622 */ {SR, 0x81}, + /* 623 */ {SR, 0x83}, + /* 624 */ {SR, 0x85}, + /* 625 */ {SR, 0x87}, + /* 626 */ {SD, 0x89}, + /* 627 */ {SR, 0x8D}, + /* 628 */ {SD, 0x8F}, + /* 629 */ {SR, 0x93}, + /* 62A */ {SD, 0x95}, + /* 62B */ {SD, 0x99}, + /* 62C */ {SD, 0x9D}, + /* 62D */ {SD, 0xA1}, + /* 62E */ {SD, 0xA5}, + /* 62F */ {SR, 0xA9}, + /* 630 */ {SR, 0xAB}, + /* 631 */ {SR, 0xAD}, + /* 632 */ {SR, 0xAF}, + /* 633 */ {SD, 0xB1}, + /* 634 */ {SD, 0xB5}, + /* 635 */ {SD, 0xB9}, + /* 636 */ {SD, 0xBD}, + /* 637 */ {SD, 0xC1}, + /* 638 */ {SD, 0xC5}, + /* 639 */ {SD, 0xC9}, + /* 63A */ {SD, 0xCD}, + /* 63B */ {SU, 0x0}, + /* 63C */ {SU, 0x0}, + /* 63D */ {SU, 0x0}, + /* 63E */ {SU, 0x0}, + /* 63F */ {SU, 0x0}, + /* 640 */ {SC, 0x0}, + /* 641 */ {SD, 0xD1}, + /* 642 */ {SD, 0xD5}, + /* 643 */ {SD, 0xD9}, + /* 644 */ {SD, 0xDD}, + /* 645 */ {SD, 0xE1}, + /* 646 */ {SD, 0xE5}, + /* 647 */ {SD, 0xE9}, + /* 648 */ {SR, 0xED}, + /* 649 */ {SR, 0xEF}, + /* 64A */ {SD, 0xF1}}; + +/* ── CrossPoint extension: joining types outside U+0621–U+064A ────────── + * Source: Unicode ArabicShaping.txt (joining type column). Ranges of + * letters sharing one type are collapsed. Sorted ascending (binary + * search). Anything not listed here or in shapetypes[] is SU. */ +static const struct { + ucschar first, last; + uchar type; +} xjointypes[] = { + {0x0620, 0x0620, SD}, /* kashmiri yeh */ + {0x066E, 0x066F, SD}, /* dotless beh/qaf */ + {0x0671, 0x0673, SR}, /* alef wasla + wavy-hamza alefs */ + {0x0675, 0x0677, SR}, /* high-hamza alef/waw */ + {0x0678, 0x0687, SD}, /* high-hamza yeh, beh group (ٹ ٺ ٻ … پ), hah group (چ ڇ) */ + {0x0688, 0x0699, SR}, /* dal group (ڈ ڊ ڌ …), reh group (ڑ ړ ژ …) */ + {0x069A, 0x06BF, SD}, /* seen/sad/feh/qaf/kaf/gaf/lam/noon groups (ک گ ں ھ …) */ + {0x06C0, 0x06C0, SR}, /* heh with yeh above */ + {0x06C1, 0x06C2, SD}, /* heh goal (ہ) */ + {0x06C3, 0x06CB, SR}, /* teh marbuta goal, waw group (ۆ ۇ ۋ …) */ + {0x06CC, 0x06CC, SD}, /* farsi yeh (ی) */ + {0x06CD, 0x06CD, SR}, /* yeh with tail */ + {0x06CE, 0x06CE, SD}, /* farsi yeh with V (Kurdish ێ) */ + {0x06CF, 0x06CF, SR}, /* waw with dot above */ + {0x06D0, 0x06D1, SD}, /* e (Pashto ې), yeh three dots below */ + {0x06D2, 0x06D3, SR}, /* yeh barree (ے ۓ) */ + {0x06D5, 0x06D5, SR}, /* ae (Kurdish ە) */ + {0x06EE, 0x06EF, SR}, /* dal/reh with inverted V */ + {0x06FA, 0x06FC, SD}, /* sheen/dad/ghain with dot below */ + {0x06FF, 0x06FF, SD}, /* heh with inverted V */ + {0x200C, 0x200C, SU}, /* ZWNJ — joining type U per ArabicShaping.txt */ + {0x200D, 0x200D, SC}, /* ZWJ — joining type C per ArabicShaping.txt */ +}; + +/* ── CrossPoint extension: presentation forms outside U+0621–U+064A ───── + * Source: UnicodeData.txt, Arabic Presentation Forms-A (U+FB50–U+FBFF). + * forms = number of consecutive presentation forms allocated for the + * letter, always in the order isolated, final, initial, medial (matching + * the SHAPE_* offsets below): 2 = isolated+final, 4 = all. + * Letters absent here have no presentation forms and keep their base + * codepoint. Sorted ascending (binary search). */ +static const struct { + ucschar cp; + ucschar isolated; + uchar forms; +} xshapeforms[] = { + {0x0671, 0xFB50, 2}, /* alef wasla */ + {0x0679, 0xFB66, 4}, /* tteh (Urdu ٹ) */ + {0x067A, 0xFB5E, 4}, /* tteheh */ + {0x067B, 0xFB52, 4}, /* beeh (Sindhi ٻ) */ + {0x067E, 0xFB56, 4}, /* peh (Farsi پ) */ + {0x067F, 0xFB62, 4}, /* teheh */ + {0x0680, 0xFB5A, 4}, /* beheh (Sindhi ڀ) */ + {0x0683, 0xFB76, 4}, /* nyeh (Sindhi ڃ) */ + {0x0684, 0xFB72, 4}, /* dyeh (Sindhi ڄ) */ + {0x0686, 0xFB7A, 4}, /* tcheh (Farsi چ) */ + {0x0687, 0xFB7E, 4}, /* tcheheh (Sindhi ڇ) */ + {0x0688, 0xFB88, 2}, /* ddal (Urdu ڈ) */ + {0x068C, 0xFB84, 2}, /* dahal (Sindhi ڌ) */ + {0x068D, 0xFB82, 2}, /* ddahal (Sindhi ڍ) */ + {0x068E, 0xFB86, 2}, /* dul (Sindhi ڎ) */ + {0x0691, 0xFB8C, 2}, /* rreh (Urdu ڑ) */ + {0x0698, 0xFB8A, 2}, /* jeh (Farsi ژ) */ + {0x06A4, 0xFB6A, 4}, /* veh (Kurdish ڤ) */ + {0x06A6, 0xFB6E, 4}, /* peheh (Sindhi ڦ) */ + {0x06A9, 0xFB8E, 4}, /* keheh (Farsi/Urdu ک) */ + {0x06AD, 0xFBD3, 4}, /* ng */ + {0x06AF, 0xFB92, 4}, /* gaf (Farsi/Urdu گ) */ + {0x06B1, 0xFB9A, 4}, /* ngoeh (Sindhi ڱ) */ + {0x06B3, 0xFB96, 4}, /* gueh (Sindhi ڳ) */ + {0x06BA, 0xFB9E, 2}, /* noon ghunna (Urdu ں) — dual-joining but only + isolated+final forms exist; initial/medial + contexts keep the base codepoint */ + {0x06BB, 0xFBA0, 4}, /* rnoon (Sindhi ڻ) */ + {0x06BE, 0xFBAA, 4}, /* heh doachashmee (Urdu ھ) */ + {0x06C0, 0xFBA4, 2}, /* heh with yeh above */ + {0x06C1, 0xFBA6, 4}, /* heh goal (Urdu ہ) */ + {0x06C5, 0xFBE0, 2}, /* kirghiz oe */ + {0x06C6, 0xFBD9, 2}, /* oe (Kurdish ۆ) */ + {0x06C7, 0xFBD7, 2}, /* u (ۇ) */ + {0x06C8, 0xFBDB, 2}, /* yu */ + {0x06C9, 0xFBE2, 2}, /* kirghiz yu */ + {0x06CB, 0xFBDE, 2}, /* ve */ + {0x06CC, 0xFBFC, 4}, /* farsi yeh (Farsi/Urdu ی) */ + {0x06D0, 0xFBE4, 4}, /* e (Pashto ې) */ + {0x06D2, 0xFBAE, 2}, /* yeh barree (Urdu ے) */ + {0x06D3, 0xFBB0, 2}, /* yeh barree with hamza above (ۓ) */ +}; + +/* Contextual form offsets from the isolated form — identical ordering in + Presentation Forms-A and -B, matching mintty's SFINAL/SINITIAL/SMEDIAL + (+1/+2/+3) macros. */ +enum { SHAPE_ISOLATED = 0, SHAPE_FINAL = 1, SHAPE_INITIAL = 2, SHAPE_MEDIAL = 3 }; + +/* STYPE equivalent (mintty macro → function to add the extended table) */ +static uchar stype(ucschar c) { + if (c >= SHAPE_FIRST && c <= SHAPE_LAST) return shapetypes[c - SHAPE_FIRST].type; + + int i = -1, j = lengthof(xjointypes); + while (j - i > 1) { + int k = (i + j) / 2; + if (c < xjointypes[k].first) + j = k; + else if (c > xjointypes[k].last) + i = k; + else + return xjointypes[k].type; + } + return SU; +} + +/* SISOLATED/SFINAL/SINITIAL/SMEDIAL equivalent. + form is one of the SHAPE_* offsets; returns c unchanged when the letter + has no presentation form allocated for that context. */ +static ucschar shape_form(ucschar c, uchar form) { + if (c >= SHAPE_FIRST && c <= SHAPE_LAST) { + const uchar form_b = shapetypes[c - SHAPE_FIRST].form_b; + return form_b ? 0xFE00 + form_b + form : c; + } + + int i = -1, j = lengthof(xshapeforms); + while (j - i > 1) { + int k = (i + j) / 2; + if (c < xshapeforms[k].cp) + j = k; + else if (c > xshapeforms[k].cp) + i = k; + else + return form < xshapeforms[k].forms ? xshapeforms[k].isolated + form : c; + } + return c; +} + +/* CrossPoint deviation 1: NSM marks (harakat) sit between letters in our + array but are transparent for joining (Unicode joining type T). These + helpers find the effective joining neighbour. */ +static int next_non_nsm(const bidi_char* line, int i, int count) { + int j = i + 1; + while (j < count && bidi_class(line[j].wc) == NSM) j++; + return j; +} + +static int prev_non_nsm(const bidi_char* line, int i) { + int j = i - 1; + while (j >= 0 && bidi_class(line[j].wc) == NSM) j--; + return j; +} + +/* The Main shaping function (mintty, structure preserved). + * + * line: visual-order buffer — must have been passed through do_bidi() first + * to: output buffer for the shaped data + * count: number of characters in line + */ +int do_shape(bidi_char* line, bidi_char* to, int count) { + for (int i = 0; i < count; i++) { + to[i] = line[i]; + int tempShape = stype(line[i].wc); + switch (tempShape) { + case SR: { /* Right-Joining, i.e. has Isolated, Final */ + const int nx = next_non_nsm(line, i, count); /* deviation 1: was i + 1 */ + tempShape = (nx < count) ? stype(line[nx].wc) : SU; + if (tempShape == SL || tempShape == SD || tempShape == SC) + to[i].wc = shape_form(line[i].wc, SHAPE_FINAL); + else + to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED); + break; + } + case SD: { /* Dual-Joining, i.e. has Isolated, Final, Initial, Medial */ + const int nx = next_non_nsm(line, i, count); /* deviation 1: was i + 1 */ + const int pv = prev_non_nsm(line, i); /* deviation 1: was i - 1 */ + + /* Make Ligatures */ + tempShape = (nx < count) ? stype(line[nx].wc) : SU; + if (line[i].wc == 0x644) { /* Lam: the Alef variant is at pv (visually left) */ + int ligFlag = 0; + switch (pv >= 0 ? line[pv].wc : 0) { + case 0x622: /* Alef with Madda above → لآ */ + ligFlag = 1; + to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEF6 : 0xFEF5; + break; + case 0x623: /* Alef with Hamza above → لأ */ + ligFlag = 1; + to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEF8 : 0xFEF7; + break; + case 0x625: /* Alef with Hamza below → لإ */ + ligFlag = 1; + to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEFA : 0xFEF9; + break; + case 0x627: /* Alef → لا */ + ligFlag = 1; + to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEFC : 0xFEFB; + break; + } + if (ligFlag) { + to[pv].wc = LIGATURE_PLACEHOLDER; /* deviation 2: mintty writes 0x20 */ + break; + } + } + + /* Arabic joining formatters: adapt forms (mintty) */ + const uchar joiners = line[i].joiners & 0xF; + const uchar prevjoiners = line[i].joiners >> 4; + if (prevjoiners == ZWNJ) { + /* backward join blocked; initial only if the visually-right + (logically next) neighbour joins, else isolated */ + tempShape = (pv >= 0) ? stype(line[pv].wc) : SU; + if (tempShape == SR || tempShape == SD || tempShape == SC) + to[i].wc = shape_form(line[i].wc, SHAPE_INITIAL); + else + to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED); + } else if (prevjoiners == (ZWJ | ZWNJ)) { + to[i].wc = shape_form(line[i].wc, SHAPE_MEDIAL); + } else if (prevjoiners == ZWJ) { + to[i].wc = shape_form(line[i].wc, SHAPE_FINAL); + } else if (joiners & ZWNJ) { + /* forward join blocked; final only if the visually-left + (logically previous) neighbour joins, else isolated — + tempShape still holds stype(nx) from above */ + if (tempShape == SL || tempShape == SD || tempShape == SC) + to[i].wc = shape_form(line[i].wc, SHAPE_FINAL); + else + to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED); + } else if (tempShape == SL || tempShape == SD || tempShape == SC) { + /* visually-right neighbour joins → final, or medial if the + visually-left neighbour joins too */ + tempShape = (pv >= 0) ? stype(line[pv].wc) : SU; + if (tempShape == SR || tempShape == SD || tempShape == SC) + to[i].wc = shape_form(line[i].wc, SHAPE_MEDIAL); + else + to[i].wc = shape_form(line[i].wc, SHAPE_FINAL); + } else { + /* visually-right neighbour doesn't join → isolated, or initial + if the visually-left neighbour joins */ + tempShape = (pv >= 0) ? stype(line[pv].wc) : SU; + if (tempShape == SR || tempShape == SD || tempShape == SC) + to[i].wc = shape_form(line[i].wc, SHAPE_INITIAL); + else + to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED); + } + break; + } + default: + /* SU, SL, SC and non-Arabic characters pass through unchanged */ + break; + } + } + return 1; +} + /* ═══════════════════════════════════════════════════════════════════════ * Directional Status Stack * (replaces GCC nested functions — ESP32C3 has no executable stack) diff --git a/lib/MiniBidi/minibidi.h b/lib/MiniBidi/minibidi.h index 26555d1b75..7f2aa4a04c 100644 --- a/lib/MiniBidi/minibidi.h +++ b/lib/MiniBidi/minibidi.h @@ -5,8 +5,10 @@ * minibidi.h — standalone header for ESP32C3 BiDi calculations * * Derived from [mintty](https://github.com/mintty/mintty/) (Thomas Wolff, MIT licence). - * Stripped of: Arabic shaping, box-drawing mirror, terminal dependencies, - * GCC nested functions, VLAs, and non-Hebrew/English Unicode data. + * Includes: UAX#9 bidi (do_bidi) and Arabic contextual shaping (do_shape), + * both ported from mintty src/minibidi.c (Ahmad Khalifa, Thomas Wolff). + * Stripped of: box-drawing mirror, terminal dependencies, GCC nested + * functions, VLAs, and Unicode data for scripts CrossPoint doesn't render. */ #include @@ -34,13 +36,18 @@ typedef uint32_t ucschar; /* Unicode codepoint; BMP-only content fits uint16_t #define BIDI_MAX_LINE 128 /* ── bidi_char ───────────────────────────────────────────────────────── */ -/* origwc: the codepoint as it came from the epub text stream - wc: working codepoint (may be replaced by mirrored form after do_bidi) - index: original logical position, so the caller can reorder glyphs */ +/* origwc: the codepoint as it came from the epub text stream + wc: working codepoint (may be replaced by mirrored form after + do_bidi, or by an Arabic contextual form after do_shape) + index: original logical position, so the caller can reorder glyphs + joiners: ZWJ/ZWNJ context for Arabic shaping, mintty layout: + low nibble = joiners that logically FOLLOW this character, + high nibble = joiners that logically PRECEDE this character */ typedef struct { ucschar origwc; ucschar wc; uint16_t index; + uint8_t joiners; } bidi_char; /* ── Bidi character classes (UAX #9) ────────────────────────────────── */ @@ -71,6 +78,20 @@ enum { PDI, /* Pop Directional Isolate */ }; +/* ── Arabic joining formatter flags (bidi_char.joiners nibbles) ─────── */ +/* Values match mintty's minibidi.h: ZWNJ 0x01, ZWJ 0x02. do_shape() + compares nibble values directly, so these must not be changed. */ +enum { + ZWNJ = 0x01, /* U+200C ZERO WIDTH NON-JOINER */ + ZWJ = 0x02, /* U+200D ZERO WIDTH JOINER */ +}; + +/* Sentinel written by do_shape() over the Alef absorbed into a Lam-Alef + ligature. Callers must filter it out when emitting shaped text. + (Upstream mintty writes a space instead — a terminal must keep the cell; + a proportional-text renderer must drop the character entirely.) */ +#define LIGATURE_PLACEHOLDER 0xFFFFu + /* ── Public API ──────────────────────────────────────────────────────── */ /* @@ -94,6 +115,32 @@ bool is_rtl_class(uchar bc); */ ucschar mirror(ucschar ch); +/* + * do_shape(line, to, count) + * + * Applies Arabic contextual shaping (and Lam-Alef ligation) to + * `line[0..count-1]`, writing the result to `to[0..count-1]`. + * + * MUST be called AFTER do_bidi(): the algorithm resolves joining from + * VISUAL adjacency (line[i-1] is the visually-left neighbour, line[i+1] + * the visually-right one), exactly like upstream mintty. + * + * line: visual-order input; the joiners field must be populated by the + * caller (in logical order, before do_bidi) for ZWJ/ZWNJ support + * to: output buffer, same size as line; non-Arabic entries are copied + * through unchanged. An Alef absorbed by a Lam-Alef ligature is + * replaced with LIGATURE_PLACEHOLDER — filter it on emission. + * count: number of characters (≤ BIDI_MAX_LINE) + * + * Returns 1. + * + * Ported from mintty src/minibidi.c (Ahmad Khalifa, Thomas Wolff, + * MIT licence), https://github.com/mintty/mintty — with CrossPoint + * extensions for Perso-Arabic letters and in-stream diacritics, see + * minibidi.c for details. + */ +int do_shape(bidi_char* line, bidi_char* to, int count); + /* * do_bidi(autodir, paragraphLevel, line, count) * diff --git a/lib/OpdsParser/OpdsParser.cpp b/lib/OpdsParser/OpdsParser.cpp index 2a93594757..b63e9fcf3a 100644 --- a/lib/OpdsParser/OpdsParser.cpp +++ b/lib/OpdsParser/OpdsParser.cpp @@ -6,6 +6,15 @@ #include #include +namespace { +constexpr size_t MAX_TITLE_CHARS = 160; +constexpr size_t MAX_AUTHOR_CHARS = 120; +constexpr size_t MAX_ID_CHARS = 128; +constexpr size_t MAX_HREF_CHARS = 768; +constexpr size_t MAX_SEARCH_TEMPLATE_CHARS = 768; +constexpr size_t MAX_PAGE_URL_CHARS = 768; +} // namespace + OpdsParser::OpdsParser(OpdsEntry* entries, const size_t entryCapacity) : entries(entries), entryCapacity(entryCapacity) { if (!entries || entryCapacity == 0) { @@ -136,6 +145,20 @@ const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) { return nullptr; } +void OpdsParser::assignBounded(std::string& target, const char* value, const size_t maxLen) { + if (!value) { + target.clear(); + return; + } + target.assign(value, strnlen(value, maxLen)); +} + +void OpdsParser::appendBounded(std::string& target, const char* value, const size_t len, const size_t maxLen) { + if (target.size() >= maxLen) return; + const size_t remaining = maxLen - target.size(); + target.append(value, len < remaining ? len : remaining); +} + void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { auto* self = static_cast(userData); @@ -146,14 +169,13 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons const char* type = findAttribute(atts, "type"); if (rel && strcmp(rel, "search") == 0) { - std::string sHref(href); - if (sHref.find("{searchTerms}") != std::string::npos) { - self->searchTemplate = sHref; + if (strstr(href, "{searchTerms}") != nullptr) { + assignBounded(self->searchTemplate, href, MAX_SEARCH_TEMPLATE_CHARS); } } else if (rel && strcmp(rel, "next") == 0 && !self->inEntry) { - self->nextPageUrl = href; + assignBounded(self->nextPageUrl, href, MAX_PAGE_URL_CHARS); } else if (rel && strcmp(rel, "previous") == 0 && !self->inEntry) { - self->prevPageUrl = href; + assignBounded(self->prevPageUrl, href, MAX_PAGE_URL_CHARS); } if (self->inEntry) { @@ -167,12 +189,12 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons self->currentEntry.href.find("/epub/") != std::string::npos); if (self->currentEntry.type != OpdsEntryType::BOOK || (isPlainEpub && !alreadyHasPlainEpub)) { self->currentEntry.type = OpdsEntryType::BOOK; - self->currentEntry.href = href; + assignBounded(self->currentEntry.href, href, MAX_HREF_CHARS); } } else if (type && strstr(type, "application/atom+xml") != nullptr) { if (self->currentEntry.type != OpdsEntryType::BOOK) { self->currentEntry.type = OpdsEntryType::NAVIGATION; - self->currentEntry.href = href; + assignBounded(self->currentEntry.href, href, MAX_HREF_CHARS); } } } @@ -231,7 +253,11 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) { void XMLCALL OpdsParser::characterData(void* userData, const XML_Char* s, const int len) { auto* self = static_cast(userData); - if (self->inTitle || self->inAuthorName || self->inId) { - self->currentText.append(s, len); + if (self->inTitle) { + appendBounded(self->currentText, s, len, MAX_TITLE_CHARS); + } else if (self->inAuthorName) { + appendBounded(self->currentText, s, len, MAX_AUTHOR_CHARS); + } else if (self->inId) { + appendBounded(self->currentText, s, len, MAX_ID_CHARS); } } diff --git a/lib/OpdsParser/OpdsParser.h b/lib/OpdsParser/OpdsParser.h index 76d1b1d2be..d562f1c5d3 100644 --- a/lib/OpdsParser/OpdsParser.h +++ b/lib/OpdsParser/OpdsParser.h @@ -176,6 +176,8 @@ class OpdsParser final : public Print { std::string prevPageUrl; // Helper to find attribute value static const char* findAttribute(const XML_Char** atts, const char* name); + static void assignBounded(std::string& target, const char* value, size_t maxLen); + static void appendBounded(std::string& target, const char* value, size_t len, size_t maxLen); XML_Parser parser = nullptr; OpdsEntry* entries = nullptr; diff --git a/lib/PngToBmpConverter/PngToBmpConverter.cpp b/lib/PngToBmpConverter/PngToBmpConverter.cpp index 0bb6c1d276..6c9fee51f6 100644 --- a/lib/PngToBmpConverter/PngToBmpConverter.cpp +++ b/lib/PngToBmpConverter/PngToBmpConverter.cpp @@ -2,8 +2,10 @@ #include #include -#include +#include #include +#include +#include #include #include @@ -74,6 +76,12 @@ enum PngFilter : uint8_t { PNG_FILTER_PAETH = 4, }; +void yieldDuringDecode(uint8_t& rowsSinceYield) { + if (++rowsSinceYield < 8) return; + rowsSinceYield = 0; + vTaskDelay(1); +} + // Read a big-endian 32-bit value from file bool readBE32(FsFile& file, uint32_t& value) { uint8_t buf[4]; @@ -262,9 +270,8 @@ static bool shouldContainAdaptive(const int srcWidth, const int srcHeight, const } // namespace // Context for streaming PNG decompression -// IMPORTANT: reader must be the first field - the uzlib callback casts uzlib_uncomp* to PngDecodeContext* struct PngDecodeContext { - InflateReader reader; // Must be first — callback casts uzlib_uncomp* to PngDecodeContext* + InflateStream reader; FsFile* file; // PNG image properties @@ -283,7 +290,7 @@ struct PngDecodeContext { uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk bool idatFinished; // no more IDAT chunks - // File read buffer for feeding uzlib + // File read buffer for feeding the inflate stream uint8_t readBuf[2048]; // Palette for indexed color (type 3) @@ -317,21 +324,21 @@ static bool findNextIdatChunk(PngDecodeContext& ctx) { } } -// uzlib callback: reads the next batch of IDAT data from the file -static int pngIdatReadCallback(uzlib_uncomp* uncomp) { - auto* ctx = reinterpret_cast(uncomp); +// Fill callback: reads the next batch of IDAT data from the file +static size_t pngIdatFillCallback(void* vctx, const uint8_t** data) { + auto* ctx = static_cast(vctx); - if (ctx->idatFinished) return -1; + if (ctx->idatFinished) return 0; // Skip 4-byte CRC and find next IDAT chunk when current chunk is exhausted while (ctx->chunkBytesRemaining == 0) { if (!ctx->file->seekCur(4)) { // skip 4-byte CRC of previous IDAT ctx->idatFinished = true; - return -1; + return 0; } if (!findNextIdatChunk(*ctx)) { ctx->idatFinished = true; - return -1; + return 0; } } @@ -339,18 +346,15 @@ static int pngIdatReadCallback(uzlib_uncomp* uncomp) { size_t toRead = sizeof(ctx->readBuf); if (toRead > ctx->chunkBytesRemaining) toRead = ctx->chunkBytesRemaining; - int bytesRead = ctx->file->read(ctx->readBuf, toRead); + const int bytesRead = ctx->file->read(ctx->readBuf, toRead); if (bytesRead <= 0) { ctx->idatFinished = true; - return -1; + return 0; } ctx->chunkBytesRemaining -= bytesRead; - - // Give uzlib the buffer (skip first byte since we return it directly) - uncomp->source = ctx->readBuf + 1; - uncomp->source_limit = ctx->readBuf + bytesRead; - return ctx->readBuf[0]; + *data = ctx->readBuf; + return static_cast(bytesRead); } // Decode one scanline: decompress filter byte + raw bytes, then unfilter @@ -485,8 +489,6 @@ static void convertScanlineToGray(const PngDecodeContext& ctx, uint8_t* grayRow) bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight, bool oneBit, bool crop, bool adaptiveContain) { - LOG_DBG("PNG", "Converting PNG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight); - // Verify PNG signature uint8_t sig[8]; if (pngFile.read(sig, 8) != 8 || memcmp(sig, PNG_SIGNATURE, 8) != 0) { @@ -519,8 +521,6 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu // Skip IHDR CRC pngFile.seekCur(4); - LOG_DBG("PNG", "Image: %ux%u, depth=%u, color=%u, interlace=%u", width, height, bitDepth, colorType, interlace); - if (compression != 0 || filter != 0) { LOG_ERR("PNG", "Unsupported compression/filter method"); return false; @@ -643,16 +643,16 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu return false; } - // Initialize streaming decompressor with 32KB ring buffer for back-reference history + // Initialize streaming decompressor with 32KB window for back-reference history if (!ctx.reader.init(true)) { - LOG_ERR("PNG", "Failed to init inflate reader"); + LOG_ERR("PNG", "Failed to init inflate stream"); free(ctx.currentRow); free(ctx.previousRow); return false; } - ctx.reader.setReadCallback(pngIdatReadCallback); - // PNG IDAT data is zlib-wrapped: consume the 2-byte zlib header (CMF + FLG) - ctx.reader.skipZlibHeader(); + ctx.reader.setFill(pngIdatFillCallback, &ctx); + // PNG IDAT data is zlib-wrapped (2-byte header + trailing adler32) + ctx.reader.setZlibWrapped(); // Calculate output dimensions. Crop mode behaves like CSS object-fit: cover: // scale to fill the requested box, then sample a centered source crop before dithering. @@ -665,9 +665,6 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu const int outWidth = geometry.outWidth; const int outHeight = geometry.outHeight; const bool needsScaling = geometry.needsScaling; - LOG_DBG("PNG", "Scaling %ux%u -> %dx%d (target %dx%d, mode=%s, offset %u,%u)", width, height, outWidth, outHeight, - targetWidth, targetHeight, cropOutput ? "cover" : "contain", geometry.srcXOffset_fp >> 16, - geometry.srcYOffset_fp >> 16); // Write BMP header int bytesPerRow; @@ -735,6 +732,7 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu } bool success = true; + uint8_t rowsSinceYield = 0; // Process each scanline for (uint32_t y = 0; y < height; y++) { @@ -786,6 +784,7 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu fsDitherer->nextRow(); } bmpOut.write(rowBuffer, bytesPerRow); + yieldDuringDecode(rowsSinceYield); } else { const uint64_t srcY_fp = static_cast(y + 1) << 16; if (srcY_fp <= geometry.srcYOffset_fp) { @@ -861,6 +860,7 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu bmpOut.write(rowBuffer, bytesPerRow); currentOutY++; + yieldDuringDecode(rowsSinceYield); nextOutY_srcStart = static_cast(static_cast(geometry.srcYOffset_fp) + static_cast(currentOutY + 1) * geometry.scaleY_fp); @@ -895,7 +895,6 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu free(ctx.previousRow); if (success) { - LOG_DBG("PNG", "Successfully converted PNG to BMP"); } return success; } diff --git a/lib/Serialization/BufferedFile.h b/lib/Serialization/BufferedFile.h new file mode 100644 index 0000000000..921c697f63 --- /dev/null +++ b/lib/Serialization/BufferedFile.h @@ -0,0 +1,161 @@ +#pragma once +#include +#include + +#include +#include +#include + +namespace serialization { + +// Sequential buffered wrappers over HalFile. +// +// SdFat keeps ONE shared 512-byte sector cache per volume, so interleaving small +// reads/writes across two or more files evicts and reloads that sector on nearly +// every call -- each 4-byte pod becomes a full SD transaction (measured: 31s to +// stream ~200KB through BookMetadataCache::buildBookBin on a 1,732-spine EPUB). +// Batching into chunk-sized transfers keeps each file at sequential SD speed. +// +// Heap: one fixed buffer per wrapper, allocated once at construction and freed at +// scope exit. If the allocation fails the wrapper degrades to unbuffered +// passthrough -- correct, just slow -- so callers never need an OOM path. +// +// Constraint: the wrapper must be the file's ONLY accessor while alive (it tracks +// the underlying position itself); mixing direct HalFile calls in desynchronizes it. + +class BufferedFileWriter { + public: + BufferedFileWriter(HalFile& file, const size_t capacity) + : file(file), buf(makeUniqueNoThrow(capacity)), cap(buf ? capacity : 0), pos(file.position()) {} + ~BufferedFileWriter() { flush(); } + BufferedFileWriter(const BufferedFileWriter&) = delete; + BufferedFileWriter& operator=(const BufferedFileWriter&) = delete; + + void write(const void* src, const size_t len) { + pos += len; + const auto* p = static_cast(src); + if (fill + len > cap) { + flushBuffer(); + } + if (len >= cap) { // also the cap == 0 passthrough + okFlag &= file.write(p, len) == len; + return; + } + // Typed local: cppcheck misreads unique_ptr::get() arithmetic as void*. + uint8_t* const data = buf.get(); + memcpy(data + fill, p, len); + fill += len; + } + + // Logical write position (bytes written since the file was opened). + size_t position() const { return pos; } + + // Flush buffered bytes; returns false if any write so far has failed short. + bool flush() { + flushBuffer(); + return okFlag; + } + + private: + void flushBuffer() { + if (fill == 0) return; + okFlag &= file.write(buf.get(), fill) == fill; + fill = 0; + } + + HalFile& file; + std::unique_ptr buf; + const size_t cap; + size_t fill = 0; + size_t pos; + bool okFlag = true; +}; + +class BufferedFileReader { + public: + BufferedFileReader(HalFile& file, const size_t capacity) + : file(file), buf(makeUniqueNoThrow(capacity)), cap(buf ? capacity : 0), bufStart(file.position()) {} + BufferedFileReader(const BufferedFileReader&) = delete; + BufferedFileReader& operator=(const BufferedFileReader&) = delete; + + size_t read(void* dst, size_t len) { + auto* p = static_cast(dst); + if (cap == 0) { // passthrough + const int n = file.read(p, len); + const size_t got = n < 0 ? 0 : static_cast(n); + bufStart += got; + return got; + } + size_t total = 0; + while (len > 0) { + if (off == fill) { + bufStart += fill; + off = 0; + const int n = file.read(buf.get(), cap); + fill = n < 0 ? 0 : static_cast(n); + if (fill == 0) break; // EOF or error + } + const size_t chunk = std::min(len, fill - off); + // Typed local: cppcheck misreads unique_ptr::get() arithmetic as void*. + const uint8_t* const data = buf.get(); + memcpy(p, data + off, chunk); + p += chunk; + off += chunk; + len -= chunk; + total += chunk; + } + return total; + } + + // Logical read position. + size_t position() const { return bufStart + off; } + + bool seek(const size_t target) { + // Within the buffered window: just move the cursor. + if (cap != 0 && target >= bufStart && target < bufStart + fill) { + off = target - bufStart; + return true; + } + if (!file.seek(target)) return false; + bufStart = target; + fill = 0; + off = 0; + return true; + } + + private: + HalFile& file; + std::unique_ptr buf; + const size_t cap; + size_t fill = 0; + size_t off = 0; + size_t bufStart; +}; + +// serialization:: overloads mirroring the HalFile ones in Serialization.h. +template +void writePod(BufferedFileWriter& out, const T& value) { + out.write(&value, sizeof(T)); +} + +template +void readPod(BufferedFileReader& in, T& value) { + in.read(&value, sizeof(T)); +} + +inline void writeString(BufferedFileWriter& out, const std::string& s) { + const uint32_t len = s.size(); + writePod(out, len); + out.write(s.data(), len); +} + +inline void readString(BufferedFileReader& in, std::string& s) { + uint32_t len; + readPod(in, len); + s.resize(len); + if (len > 0) { + in.read(&s[0], len); + } +} + +} // namespace serialization diff --git a/lib/Serialization/ObfuscationUtils.cpp b/lib/Serialization/ObfuscationUtils.cpp index 6cee2ddfc9..6fd31a19be 100644 --- a/lib/Serialization/ObfuscationUtils.cpp +++ b/lib/Serialization/ObfuscationUtils.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include namespace obfuscation { @@ -17,15 +19,15 @@ constexpr size_t CHECKSUM_LEN = 4; constexpr uint32_t FNV_OFFSET_BASIS = 2166136261UL; constexpr uint32_t FNV_PRIME = 16777619UL; -// Simple lazy init — no thread-safety concern on single-core ESP32-C3. const uint8_t* getHwKey() { - static uint8_t key[HW_KEY_LEN] = {}; - static bool initialized = false; - if (!initialized) { - esp_efuse_mac_get_default(key); - initialized = true; - } - return key; + // Function-local static initialization is synchronized by C++, including + // on dual-core S3 targets. + static const std::array key = [] { + std::array value{}; + esp_efuse_mac_get_default(value.data()); + return value; + }(); + return key.data(); } void appendUint32LE(std::string& data, uint32_t value) { @@ -115,6 +117,10 @@ std::string deobfuscateFromBase64(const char* encoded, bool* ok) { } std::string deobfuscateFromBase64(const char* encoded, DecodeStatus* status) { + return deobfuscateFromBase64(encoded, std::numeric_limits::max(), status); +} + +std::string deobfuscateFromBase64(const char* encoded, const size_t maxPlaintextLength, DecodeStatus* status) { if (status) *status = DecodeStatus::INVALID; if (encoded == nullptr || encoded[0] == '\0') { if (status) *status = DecodeStatus::EMPTY; @@ -128,6 +134,15 @@ std::string deobfuscateFromBase64(const char* encoded, DecodeStatus* status) { LOG_ERR("OBF", "Base64 decode size query failed (ret=%d)", ret); return ""; } + const size_t maxPayloadLength = + maxPlaintextLength > std::numeric_limits::max() - VALIDATED_PREFIX_LEN - CHECKSUM_LEN + ? std::numeric_limits::max() + : maxPlaintextLength + VALIDATED_PREFIX_LEN + CHECKSUM_LEN; + if (decodedLen > maxPayloadLength) { + LOG_ERR("OBF", "Decoded credential exceeds %zu byte limit", maxPlaintextLength); + if (status) *status = DecodeStatus::TOO_LONG; + return ""; + } std::string result(decodedLen, '\0'); ret = mbedtls_base64_decode(reinterpret_cast(&result[0]), decodedLen, &decodedLen, reinterpret_cast(encoded), encodedLen); @@ -143,11 +158,22 @@ std::string deobfuscateFromBase64(const char* encoded, DecodeStatus* status) { if (!decodeValidatedPayload(result, plaintext)) { return ""; } + if (plaintext.size() > maxPlaintextLength) { + LOG_ERR("OBF", "Decoded credential exceeds %zu byte limit", maxPlaintextLength); + if (status) *status = DecodeStatus::TOO_LONG; + return ""; + } if (status) *status = DecodeStatus::VALIDATED; return plaintext; } + if (result.size() > maxPlaintextLength) { + LOG_ERR("OBF", "Decoded legacy credential exceeds %zu byte limit", maxPlaintextLength); + if (status) *status = DecodeStatus::TOO_LONG; + return ""; + } + if (status) *status = DecodeStatus::LEGACY; return result; } @@ -200,7 +226,6 @@ void selfTest() { allPassed = false; } if (allPassed) { - LOG_DBG("OBF", "Obfuscation self-test PASSED"); } } diff --git a/lib/Serialization/ObfuscationUtils.h b/lib/Serialization/ObfuscationUtils.h index b5931930ee..6e2207e7cf 100644 --- a/lib/Serialization/ObfuscationUtils.h +++ b/lib/Serialization/ObfuscationUtils.h @@ -19,6 +19,7 @@ namespace obfuscation { enum class DecodeStatus : uint8_t { EMPTY, INVALID, + TOO_LONG, LEGACY, VALIDATED, }; @@ -36,6 +37,9 @@ String obfuscateToBase64(const std::string& plaintext); // Returns empty string on invalid input; sets *ok to false if decode or validation fails. std::string deobfuscateFromBase64(const char* encoded, bool* ok = nullptr); std::string deobfuscateFromBase64(const char* encoded, DecodeStatus* status); +// Rejects oversized plaintext before returning it to a credential/settings +// store, and bounds the decoded payload allocation for corrupted input. +std::string deobfuscateFromBase64(const char* encoded, size_t maxPlaintextLength, DecodeStatus* status); // Self-test: verifies round-trip obfuscation with hardware key. Logs PASS/FAIL. void selfTest(); diff --git a/lib/Serialization/PersistableStore.h b/lib/Serialization/PersistableStore.h index 14c6020cd7..8d8b1bb146 100644 --- a/lib/Serialization/PersistableStore.h +++ b/lib/Serialization/PersistableStore.h @@ -2,7 +2,9 @@ #include #include +#include +#include #include /** @@ -19,6 +21,31 @@ class PersistableStoreBase { PersistableStoreBase() = default; ~PersistableStoreBase() = default; + // Serializes saveToFile/loadFromFile against each other across FreeRTOS + // tasks, so the JSON snapshot cannot tear mid-serialize and two concurrent + // saves cannot write their documents out of order. Concurrent saves are + // reachable: the web server task saves settings while the main task can too. + // + // It is deliberately held across the SD write. That is safe only because the + // read path does NOT take it — derived stores build their snapshots (e.g. + // CrossPointSettings::statusBarSpec) unlocked. If you ever lock this mutex on + // a read path, you put it on the render path and stall rendering behind SD + // I/O, and you create a storeMutex/storageMutex ordering hazard. Don't. + mutable std::mutex storeMutex; + + // fromJson() implementations call this (instead of saveToFile()) when the + // on-disk JSON used a legacy shape that was upgraded in memory. + // loadFromFile() performs the save after releasing storeMutex; calling + // saveToFile() from inside fromJson() would deadlock on storeMutex. + void requestResave() { resaveRequested = true; } + + bool resaveRequested = false; + + public: + // Public so non-store JSON files (e.g. per-book bookmarks) can reuse them + // instead of instantiating serializeJson/deserializeJson in their own TU — + // that per-TU duplication is exactly what this class exists to prevent. + // Serializes doc and writes it to path (ensures /.crosspoint exists). Logs on failure. static bool writeDocToFile(const char* path, const JsonDocument& doc); @@ -26,6 +53,7 @@ class PersistableStoreBase { // does not exist (expected on first boot); logs on read/parse failure. static bool readDocFromFile(const char* path, JsonDocument& doc); + protected: /** * Helper function for extracting an obfuscated password from a JSON value. * Accepts JsonVariantConst so callers can pass either a whole JsonDocument @@ -49,6 +77,10 @@ class PersistableStoreBase { * `obj["name"] | ""`), never as `| std::string("")` — ArduinoJson's * std::string converter drags a per-TU copy of the whole JSON serializer * into flash via its serializeJson fallback. + * + * Concurrency: saveToFile/loadFromFile lock storeMutex, so toJson/fromJson + * always run under it. fromJson must signal legacy-shape upgrades with + * requestResave(), never by calling saveToFile() directly (deadlock). */ template class PersistableStore : public PersistableStoreBase { @@ -67,16 +99,31 @@ class PersistableStore : public PersistableStoreBase { } bool saveToFile() const { + std::lock_guard lock(storeMutex); JsonDocument doc; static_cast(this)->toJson(doc); return writeDocToFile(T::getFilePath(), doc); } bool loadFromFile() { - JsonDocument doc; - if (!readDocFromFile(T::getFilePath(), doc)) { - return false; + bool ok; + bool doResave; + { + std::lock_guard lock(storeMutex); + resaveRequested = false; + JsonDocument doc; + if (!readDocFromFile(T::getFilePath(), doc)) { + return false; + } + ok = static_cast(this)->fromJson(doc.as()); + // Read the flag under the lock that guards the fromJson() that set it. + doResave = resaveRequested; + resaveRequested = false; + } + // Deliberately outside the lock: saveToFile() takes storeMutex itself. + if (ok && doResave && !saveToFile()) { + LOG_ERR("PERSIST", "Failed to resave %s after format update", T::getFilePath()); } - return static_cast(this)->fromJson(doc.as()); + return ok; } }; diff --git a/lib/Txt/Txt.cpp b/lib/Txt/Txt.cpp index ef9ee1754d..5c6d319ad3 100644 --- a/lib/Txt/Txt.cpp +++ b/lib/Txt/Txt.cpp @@ -31,7 +31,6 @@ bool Txt::load() { file.close(); loaded = true; - LOG_DBG("TXT", "Loaded TXT file: %s (%zu bytes)", filepath.c_str(), fileSize); return true; } @@ -75,7 +74,6 @@ std::string Txt::findCoverImage() const { for (const auto& ext : extensions) { std::string coverPath = folder + "/" + baseName + ext; if (Storage.exists(coverPath.c_str())) { - LOG_DBG("TXT", "Found matching cover image: %s", coverPath.c_str()); return coverPath; } } @@ -86,7 +84,6 @@ std::string Txt::findCoverImage() const { for (const auto& ext : extensions) { std::string coverPath = folder + "/" + std::string(name) + ext; if (Storage.exists(coverPath.c_str())) { - LOG_DBG("TXT", "Found fallback cover image: %s", coverPath.c_str()); return coverPath; } } @@ -105,7 +102,6 @@ bool Txt::generateCoverBmp() const { std::string coverImagePath = findCoverImage(); if (coverImagePath.empty()) { - LOG_DBG("TXT", "No cover image found for TXT file"); return false; } @@ -114,7 +110,6 @@ bool Txt::generateCoverBmp() const { if (FsHelpers::hasBmpExtension(coverImagePath)) { // Copy BMP file to cache - LOG_DBG("TXT", "Copying BMP cover image to cache"); HalFile src, dst; if (!Storage.openFileForRead("TXT", coverImagePath, src)) { return false; @@ -127,11 +122,9 @@ bool Txt::generateCoverBmp() const { size_t bytesRead = src.read(buffer, sizeof(buffer)); dst.write(buffer, bytesRead); } - LOG_DBG("TXT", "Copied BMP cover to cache"); return true; } else if (FsHelpers::hasJpgExtension(coverImagePath)) { // Convert JPG/JPEG to BMP (same approach as Epub) - LOG_DBG("TXT", "Generating BMP from JPG cover image"); HalFile coverJpg, coverBmp; if (!Storage.openFileForRead("TXT", coverImagePath, coverJpg)) { return false; @@ -145,7 +138,6 @@ bool Txt::generateCoverBmp() const { LOG_ERR("TXT", "Failed to generate BMP from JPG cover image"); Storage.remove(getCoverBmpPath().c_str()); } else { - LOG_DBG("TXT", "Generated BMP from JPG cover image"); } return success; } @@ -157,7 +149,6 @@ bool Txt::generateCoverBmp() const { bool Txt::clearCache() const { if (!Storage.exists(cachePath.c_str())) { - LOG_DBG("TXT", "Cache does not exist, no action needed"); return true; } @@ -166,7 +157,6 @@ bool Txt::clearCache() const { return false; } - LOG_DBG("TXT", "Cache cleared successfully"); return true; } diff --git a/lib/Utf8/Utf8.cpp b/lib/Utf8/Utf8.cpp index 340c4510c3..bf675fe63a 100644 --- a/lib/Utf8/Utf8.cpp +++ b/lib/Utf8/Utf8.cpp @@ -21,6 +21,45 @@ uint32_t utf8ComposePair(const uint32_t base, const uint32_t mark) { } return 0; } + +bool isLookupBoundary(const uint32_t cp) { + if (cp <= 0x7F) { + return !((cp >= '0' && cp <= '9') || (cp >= 'A' && cp <= 'Z') || (cp >= 'a' && cp <= 'z')); + } + + // Controls and Unicode whitespace. + if ((cp >= 0x80 && cp <= 0xA0) || cp == 0x1680 || (cp >= 0x2000 && cp <= 0x200A) || (cp >= 0x2028 && cp <= 0x202F) || + cp == 0x205F || cp == 0x3000) { + return true; + } + + // Latin-1 punctuation and symbols sit immediately before accented Latin. + if (cp >= 0x00A1 && cp <= 0x00BF) return true; + + // Common punctuation and symbol blocks. Keeping this as range checks avoids + // pulling a full Unicode-category library into constrained C3 firmware. + if ((cp >= 0x2000 && cp <= 0x2BFF) || (cp >= 0x2E00 && cp <= 0x2E7F) || (cp >= 0x3000 && cp <= 0x303F) || + (cp >= 0xFE10 && cp <= 0xFE6F) || (cp >= 0x1F000 && cp <= 0x1FAFF)) { + return true; + } + + // Fullwidth punctuation/symbols, excluding fullwidth letters and digits. + if ((cp >= 0xFF01 && cp <= 0xFF0F) || (cp >= 0xFF1A && cp <= 0xFF20) || (cp >= 0xFF3B && cp <= 0xFF40) || + (cp >= 0xFF5B && cp <= 0xFF65) || (cp >= 0xFFE0 && cp <= 0xFFEE)) { + return true; + } + + // Script-specific punctuation that lives beside letters rather than in a + // dedicated punctuation block. + return cp == 0x037E || cp == 0x0387 || (cp >= 0x055A && cp <= 0x055F) || cp == 0x0589 || cp == 0x058A || + cp == 0x05BE || cp == 0x05C0 || cp == 0x05C3 || cp == 0x05C6 || (cp >= 0x0609 && cp <= 0x060D) || + cp == 0x061B || (cp >= 0x061D && cp <= 0x061F) || (cp >= 0x066A && cp <= 0x066D) || cp == 0x06D4 || + (cp >= 0x0700 && cp <= 0x070D) || cp == 0x0964 || cp == 0x0965; +} + +bool isLookupCoreCharacter(const uint32_t cp) { + return cp != 0 && cp != REPLACEMENT_GLYPH && !utf8IsCombiningMark(cp) && !isLookupBoundary(cp); +} } // namespace std::string utf8ComposeNfc(const std::string& in) { @@ -68,6 +107,41 @@ std::string utf8ComposeNfc(const std::string& in) { return out; } +bool utf8ContainsLookupCharacter(const char* text) { + if (!text) return false; + const auto* cursor = reinterpret_cast(text); + while (*cursor) { + if (isLookupCoreCharacter(utf8NextCodepoint(&cursor))) return true; + } + return false; +} + +bool utf8ContainsLookupCharacter(const std::string& text) { return utf8ContainsLookupCharacter(text.c_str()); } + +std::string utf8CleanLookupWord(const std::string& text) { + const auto* begin = reinterpret_cast(text.c_str()); + const auto* cursor = begin; + size_t firstCore = std::string::npos; + size_t lastKeptEnd = 0; + + while (*cursor) { + const auto* cpStart = cursor; + const uint32_t cp = utf8NextCodepoint(&cursor); + if (isLookupCoreCharacter(cp)) { + if (firstCore == std::string::npos) firstCore = static_cast(cpStart - begin); + lastKeptEnd = static_cast(cursor - begin); + } else if (firstCore != std::string::npos && utf8IsCombiningMark(cp) && + static_cast(cpStart - begin) == lastKeptEnd) { + // A trailing mark belongs to the preceding base character. If another + // core character follows, punctuation between them remains internal. + lastKeptEnd = static_cast(cursor - begin); + } + } + + if (firstCore == std::string::npos) return {}; + return utf8ComposeNfc(text.substr(firstCore, lastKeptEnd - firstCore)); +} + int utf8CodepointLen(const unsigned char c) { if (c < 0x80) return 1; // 0xxxxxxx if ((c >> 5) == 0x6) return 2; // 110xxxxx diff --git a/lib/Utf8/Utf8.h b/lib/Utf8/Utf8.h index 64c04808ec..5af70defd3 100644 --- a/lib/Utf8/Utf8.h +++ b/lib/Utf8/Utf8.h @@ -18,6 +18,18 @@ void utf8TruncateChars(std::string& str, size_t numChars); // stored in NFD (e.g. some EPUB chapter titles) otherwise renders broken. std::string utf8ComposeNfc(const std::string& in); +// Returns true when text contains at least one Unicode letter/number-like +// codepoint that can be sent to dictionary lookup. Punctuation, symbols, +// whitespace, and combining marks do not count by themselves. +bool utf8ContainsLookupCharacter(const char* text); +bool utf8ContainsLookupCharacter(const std::string& text); + +// Trim punctuation, symbols, and whitespace from the edges of a UTF-8 lookup +// token while preserving internal punctuation (for example l'ete), non-Latin +// scripts, and trailing combining marks. The result is composed to NFC where +// the firmware's compact composition table has a matching entry. +std::string utf8CleanLookupWord(const std::string& text); + // Truncate a raw char buffer to the last complete UTF-8 codepoint boundary. // Returns the new length (<= len). If the buffer ends mid-sequence, the // incomplete trailing bytes are excluded. @@ -44,6 +56,52 @@ inline bool utf8IsCjkBreakable(const uint32_t cp) { || (cp >= 0x2A700 && cp <= 0x2B73F); // CJK Extension C } +// Returns true for any codepoint in a CJK script block (Han, Kana, Hangul, Bopomofo, +// radicals, and CJK punctuation/compatibility/enclosed forms). Used for fallback font +// selection — deliberately broader than utf8IsCjkBreakable, whose ranges are tuned to +// implicit line-break opportunities and must not grow without rethinking layout. +inline bool utf8IsCjkCodepoint(const uint32_t cp) { + return (cp >= 0x1100 && cp <= 0x11FF) // Hangul Jamo + || (cp >= 0x2E80 && cp <= 0x2FDF) // CJK Radicals Supplement, Kangxi Radicals + || (cp >= 0x3000 && cp <= 0x33FF) // CJK punctuation, Kana, Bopomofo, Hangul Compat + // Jamo, Kanbun, strokes, enclosed + compat forms + || (cp >= 0x3400 && cp <= 0x4DBF) // CJK Extension A + || (cp >= 0x4E00 && cp <= 0x9FFF) // CJK Unified Ideographs + || (cp >= 0xA960 && cp <= 0xA97F) // Hangul Jamo Extended-A + || (cp >= 0xAC00 && cp <= 0xD7FF) // Hangul Syllables, Hangul Jamo Extended-B + || (cp >= 0xF900 && cp <= 0xFAFF) // CJK Compatibility Ideographs + || (cp >= 0xFE10 && cp <= 0xFE1F) // Vertical Forms + || (cp >= 0xFE30 && cp <= 0xFE4F) // CJK Compatibility Forms + || (cp >= 0xFF01 && cp <= 0xFF60) // Fullwidth Latin / Punctuation + || (cp >= 0xFF65 && cp <= 0xFFEF) // Halfwidth Katakana / Hangul + || (cp >= 0x20000 && cp <= 0x2EBEF) // CJK Extensions B-F + || (cp >= 0x2F800 && cp <= 0x2FA1F) // CJK Compatibility Ideographs Supplement + || (cp >= 0x30000 && cp <= 0x323AF); // CJK Extensions G-H +} + +// Returns true for CJK letters, syllables, and digits that can form a dictionary +// lookup. Unlike utf8IsCjkCodepoint(), this deliberately excludes punctuation +// and compatibility symbols so callers can continue to skip standalone marks. +inline bool utf8IsCjkWordCharacter(const uint32_t cp) { + return (cp >= 0x1100 && cp <= 0x11FF) // Hangul Jamo + || (cp >= 0x3040 && cp <= 0x30FF) // Hiragana and Katakana + || (cp >= 0x3100 && cp <= 0x312F) // Bopomofo + || (cp >= 0x3130 && cp <= 0x318F) // Hangul Compatibility Jamo + || (cp >= 0x31A0 && cp <= 0x31FF) // Bopomofo Extended, Katakana Extensions + || (cp >= 0x3400 && cp <= 0x4DBF) // CJK Extension A + || (cp >= 0x4E00 && cp <= 0x9FFF) // CJK Unified Ideographs + || (cp >= 0xA960 && cp <= 0xA97F) // Hangul Jamo Extended-A + || (cp >= 0xAC00 && cp <= 0xD7FF) // Hangul Syllables, Hangul Jamo Extended-B + || (cp >= 0xF900 && cp <= 0xFAFF) // CJK Compatibility Ideographs + || (cp >= 0xFF10 && cp <= 0xFF19) // Fullwidth digits + || (cp >= 0xFF21 && cp <= 0xFF3A) // Fullwidth Latin uppercase + || (cp >= 0xFF41 && cp <= 0xFF5A) // Fullwidth Latin lowercase + || (cp >= 0xFF66 && cp <= 0xFFDC) // Halfwidth Katakana and Hangul + || (cp >= 0x20000 && cp <= 0x2EBEF) // CJK Extensions B-F + || (cp >= 0x2F800 && cp <= 0x2FA1F) // CJK Compatibility Ideographs Supplement + || (cp >= 0x30000 && cp <= 0x323AF); // CJK Extensions G-H +} + // Returns true for Unicode combining diacritical marks that should not advance the cursor. inline bool utf8IsCombiningMark(const uint32_t cp) { return (cp >= 0x0300 && cp <= 0x036F) // Combining Diacritical Marks diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index acc1c5cb1f..4dbcd2792b 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -10,6 +10,16 @@ #include #include #include +#include +#include + +namespace { +void yieldDuringThumbnail(uint8_t& rowsSinceYield) { + if (++rowsSinceYield < 8) return; + rowsSinceYield = 0; + vTaskDelay(1); +} +} // namespace #include @@ -29,8 +39,6 @@ bool thumbnailHasDimensions(const std::string& path, const uint16_t width, const } // namespace bool Xtc::load() { - LOG_DBG("XTC", "Loading XTC: %s", filepath.c_str()); - // Initialize parser parser.reset(new xtc::XtcParser()); @@ -43,13 +51,11 @@ bool Xtc::load() { } loaded = true; - LOG_DBG("XTC", "Loaded XTC: %s (%lu pages)", filepath.c_str(), parser->getPageCount()); return true; } bool Xtc::clearCache() const { if (!Storage.exists(cachePath.c_str())) { - LOG_DBG("XTC", "Cache does not exist, no action needed"); return true; } @@ -58,7 +64,6 @@ bool Xtc::clearCache() const { return false; } - LOG_DBG("XTC", "Cache cleared successfully"); return true; } @@ -272,7 +277,6 @@ bool Xtc::generateCoverBmp() const { free(pageBuffer); - LOG_DBG("XTC", "Generated cover BMP: %s", getCoverBmpPath().c_str()); return true; } @@ -403,6 +407,7 @@ bool Xtc::generateThumbBmp(uint16_t width, uint16_t height) const { const uint8_t* plane2 = (bitDepth == 2) ? pageBuffer + planeSize : nullptr; const size_t colBytes = (bitDepth == 2) ? ((pageInfo.height + 7) / 8) : 0; const size_t srcRowBytes = (bitDepth == 1) ? ((pageInfo.width + 7) / 8) : 0; + uint8_t rowsSinceYield = 0; for (uint16_t dstY = 0; dstY < thumbHeight; dstY++) { memset(rowBuffer, 0xFF, rowSize); @@ -464,12 +469,12 @@ bool Xtc::generateThumbBmp(uint16_t width, uint16_t height) const { } } thumbBmp.write(rowBuffer, rowSize); + yieldDuringThumbnail(rowsSinceYield); } free(rowBuffer); thumbBmp.close(); free(pageBuffer); - LOG_DBG("XTC", "Generated thumb BMP (%dx%d): %s", thumbWidth, thumbHeight, getThumbBmpPath(width, height).c_str()); return true; } diff --git a/lib/Xtc/Xtc/XtcParser.cpp b/lib/Xtc/Xtc/XtcParser.cpp index 7263c68605..a833278d96 100644 --- a/lib/Xtc/Xtc/XtcParser.cpp +++ b/lib/Xtc/Xtc/XtcParser.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -157,10 +158,6 @@ XtcError XtcParser::readHeader() { return XtcError::CORRUPTED_HEADER; } - LOG_DBG("XTC", "Header: magic=0x%08X (%s), ver=%u.%u, pages=%u, bitDepth=%u", m_header.magic, - (m_header.magic == XTCH_MAGIC) ? "XTCH" : "XTC", m_header.versionMajor, m_header.versionMinor, - m_header.pageCount, m_bitDepth); - return XtcError::OK; } @@ -174,7 +171,6 @@ XtcError XtcParser::readTitle() { m_file.read(titleBuf, sizeof(titleBuf) - 1); m_title = titleBuf; - LOG_DBG("XTC", "Title: %s", m_title.c_str()); return XtcError::OK; } @@ -189,7 +185,6 @@ XtcError XtcParser::readAuthor() { m_file.read(authorBuf, sizeof(authorBuf) - 1); m_author = authorBuf; - LOG_DBG("XTC", "Author: %s", m_author.c_str()); return XtcError::OK; } @@ -231,7 +226,6 @@ XtcError XtcParser::readFirstPageInfo() { m_defaultWidth = entry.width; m_defaultHeight = entry.height; - LOG_DBG("XTC", "Page table validated: %u pages, default %dx%d", m_header.pageCount, m_defaultWidth, m_defaultHeight); return XtcError::OK; } @@ -391,7 +385,6 @@ XtcError XtcParser::readChapters() { LOG_DBG("XTC", "Chapter table truncated: %u available, %u loaded", static_cast(chapterCount), static_cast(MAX_XTC_CHAPTERS)); } - LOG_DBG("XTC", "Chapters: %u", static_cast(m_chapterCount)); return XtcError::OK; } @@ -495,25 +488,25 @@ XtcError XtcParser::loadPageStreaming(uint32_t pageIndex, std::function callback, size_t chunkSize) { if (!m_isOpen) { - return XtcError::FILE_NOT_FOUND; + return m_lastError = XtcError::FILE_NOT_FOUND; } if (pageIndex >= m_header.pageCount) { - return XtcError::PAGE_OUT_OF_RANGE; + return m_lastError = XtcError::PAGE_OUT_OF_RANGE; } PageInfo page; if (!readPageTableEntry(pageIndex, page)) { - return XtcError::READ_ERROR; + return m_lastError = XtcError::READ_ERROR; } if (!ensureFileOpen()) { - return XtcError::FILE_NOT_FOUND; + return m_lastError = XtcError::FILE_NOT_FOUND; } // Seek to page data if (!m_file.seek64(page.offset)) { - return XtcError::READ_ERROR; + return m_lastError = XtcError::READ_ERROR; } // Read and skip page header (XTG for 1-bit, XTH for 2-bit) @@ -521,7 +514,7 @@ XtcError XtcParser::loadPageStreaming(uint32_t pageIndex, size_t headerRead = m_file.read(reinterpret_cast(&pageHeader), sizeof(XtgPageHeader)); const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC; if (headerRead != sizeof(XtgPageHeader) || pageHeader.magic != expectedMagic) { - return XtcError::READ_ERROR; + return m_lastError = XtcError::READ_ERROR; } // Calculate bitmap size based on bit depth @@ -534,23 +527,40 @@ XtcError XtcParser::loadPageStreaming(uint32_t pageIndex, bitmapSize = ((pageHeader.width + 7) / 8) * pageHeader.height; } - // Read in chunks - std::vector chunk(chunkSize); + // The chunk size is caller-controlled and too large for the task stack. Keep + // this allocation fallible so a fragmented C3 heap reports an error instead + // of aborting while entering the streaming fallback. + if (chunkSize == 0) { + return m_lastError = XtcError::MEMORY_ERROR; + } + if (m_streamChunkSize < chunkSize) { + auto chunk = makeUniqueNoThrow(chunkSize); + if (!chunk) { + LOG_ERR("XTC", "Failed to allocate streaming page chunk (%u bytes)", static_cast(chunkSize)); + return m_lastError = XtcError::MEMORY_ERROR; + } + m_streamChunk = std::move(chunk); + m_streamChunkSize = chunkSize; + } + if (!m_streamChunk) { + LOG_ERR("XTC", "Failed to allocate streaming page chunk (%u bytes)", static_cast(chunkSize)); + return m_lastError = XtcError::MEMORY_ERROR; + } size_t totalRead = 0; while (totalRead < bitmapSize) { size_t toRead = std::min(chunkSize, bitmapSize - totalRead); - size_t bytesRead = m_file.read(chunk.data(), toRead); + size_t bytesRead = m_file.read(m_streamChunk.get(), toRead); if (bytesRead == 0) { - return XtcError::READ_ERROR; + return m_lastError = XtcError::READ_ERROR; } - callback(chunk.data(), bytesRead, totalRead); + callback(m_streamChunk.get(), bytesRead, totalRead); totalRead += bytesRead; } - return XtcError::OK; + return m_lastError = XtcError::OK; } bool XtcParser::isValidXtcFile(const char* filepath) { diff --git a/lib/Xtc/Xtc/XtcParser.h b/lib/Xtc/Xtc/XtcParser.h index 58a6e852d6..d2afc9d2ad 100644 --- a/lib/Xtc/Xtc/XtcParser.h +++ b/lib/Xtc/Xtc/XtcParser.h @@ -97,6 +97,8 @@ class XtcParser { bool m_chaptersLoaded; size_t m_chapterCount; XtcError m_lastError; + std::unique_ptr m_streamChunk; + size_t m_streamChunkSize = 0; // Internal helper functions XtcError readHeader(); diff --git a/lib/ZipFile/ZipFile.cpp b/lib/ZipFile/ZipFile.cpp index f8a9346317..1292e458c9 100644 --- a/lib/ZipFile/ZipFile.cpp +++ b/lib/ZipFile/ZipFile.cpp @@ -2,14 +2,13 @@ #include #include -#include +#include #include -#include +#include #include struct ZipInflateCtx { - InflateReader reader; // Must be first — callback casts uzlib_uncomp* to ZipInflateCtx* HalFile* file = nullptr; size_t fileRemaining = 0; uint8_t* readBuf = nullptr; @@ -43,22 +42,227 @@ class ScopedOpenClose final { bool ok = true; // true when zip was already open (no open() call needed) }; -int zipReadCallback(uzlib_uncomp* uncomp) { - auto* ctx = reinterpret_cast(uncomp); - if (ctx->fileRemaining == 0) return -1; +size_t zipFillCallback(void* vctx, const uint8_t** data) { + auto* ctx = static_cast(vctx); + if (ctx->fileRemaining == 0) return 0; const size_t toRead = ctx->fileRemaining < ctx->readBufSize ? ctx->fileRemaining : ctx->readBufSize; const size_t bytesRead = ctx->file->read(ctx->readBuf, toRead); ctx->fileRemaining -= bytesRead; - if (bytesRead == 0) return -1; + *data = ctx->readBuf; + return bytesRead; +} + +size_t zipStreamFillCallback(void* vctx, const uint8_t** data) { + auto* ctx = static_cast(vctx); + if (!ctx->file || ctx->fileRemaining == 0) return 0; - uncomp->source = ctx->readBuf + 1; - uncomp->source_limit = ctx->readBuf + bytesRead; - return ctx->readBuf[0]; + const size_t toRead = ctx->fileRemaining < ctx->readBufSize ? ctx->fileRemaining : ctx->readBufSize; + const size_t bytesRead = ctx->file->read(ctx->readBuf, toRead); + ctx->fileRemaining -= bytesRead; + + *data = ctx->readBuf; + return bytesRead; } } // namespace +ZipFileStreamReader::~ZipFileStreamReader() { abort(); } + +bool ZipFileStreamReader::begin(const std::string& zipPathIn, const char* filename, const size_t chunkSizeIn) { + abort(); + if (!filename || filename[0] == '\0' || chunkSizeIn == 0) { + return false; + } + + ZipFile zip(zipPathIn); + const ScopedOpenClose zipOpen{zip}; + if (!zipOpen) return false; + + ZipFile::FileStatSlim fileStat = {}; + if (!zip.loadFileStatSlim(filename, &fileStat)) return false; + + const long offset = zip.getDataOffset(fileStat); + if (offset < 0) return false; + + zipPath = zipPathIn; + method = fileStat.method; + dataOffset = static_cast(offset); + compressedSize = fileStat.compressedSize; + uncompressedSize = fileStat.uncompressedSize; + chunkSize = chunkSizeIn; + totalProduced = 0; + compressedConsumed = 0; + + readBuffer = static_cast(malloc(chunkSize)); + if (!readBuffer) { + LOG_ERR("ZIP", "Failed to allocate cooperative read buffer (free=%u, maxAlloc=%u, chunk=%zu)", ESP.getFreeHeap(), + ESP.getMaxAllocHeap(), chunkSize); + abort(); + return false; + } + outputBuffer = static_cast(malloc(chunkSize)); + if (!outputBuffer) { + LOG_ERR("ZIP", "Failed to allocate cooperative output buffer (free=%u, maxAlloc=%u, chunk=%zu)", ESP.getFreeHeap(), + ESP.getMaxAllocHeap(), chunkSize); + abort(); + return false; + } + + if (method == ZIP_METHOD_DEFLATED) { + inflateCtx.fileRemaining = compressedSize; + inflateCtx.readBuf = readBuffer; + inflateCtx.readBufSize = chunkSize; + if (!inflateCtx.reader.init(true)) { + LOG_ERR("ZIP", "Failed to init cooperative inflate reader (free=%u, maxAlloc=%u, chunk=%zu)", ESP.getFreeHeap(), + ESP.getMaxAllocHeap(), chunkSize); + abort(); + return false; + } + inflateCtx.reader.setFill(zipStreamFillCallback, &inflateCtx); + } else if (method != ZIP_METHOD_STORED) { + LOG_ERR("ZIP", "Unsupported compression method"); + abort(); + return false; + } + + active = true; + return true; +} + +ZipStreamStatus ZipFileStreamReader::pump(Print& out, const size_t maxOutputBytes) { + if (!active) return ZipStreamStatus::Error; + + HalFile zipFile; + if (!Storage.openFileForRead("ZIP", zipPath, zipFile)) { + return ZipStreamStatus::Error; + } + + if (method == ZIP_METHOD_STORED) { + if (!zipFile.seek(dataOffset + totalProduced)) { + zipFile.close(); + return ZipStreamStatus::Error; + } + const size_t remaining = static_cast(uncompressedSize) - totalProduced; + const size_t budget = maxOutputBytes > 0 ? std::min(remaining, maxOutputBytes) : remaining; + const size_t toRead = std::min(chunkSize, budget); + if (toRead == 0) { + zipFile.close(); + active = false; + return ZipStreamStatus::Done; + } + const size_t dataRead = zipFile.read(outputBuffer, toRead); + zipFile.close(); + if (dataRead == 0 || out.write(outputBuffer, dataRead) != dataRead) { + return ZipStreamStatus::Error; + } + totalProduced += dataRead; + if (totalProduced == static_cast(uncompressedSize)) { + active = false; + return ZipStreamStatus::Done; + } + return ZipStreamStatus::More; + } + + if (!zipFile.seek(dataOffset + compressedConsumed)) { + zipFile.close(); + return ZipStreamStatus::Error; + } + + inflateCtx.file = &zipFile; + size_t pumped = 0; + bool success = false; + ZipStreamStatus result = ZipStreamStatus::More; + + while (true) { + size_t produced = 0; + const size_t beforeRemaining = inflateCtx.fileRemaining; + const size_t outputLimit = + maxOutputBytes > 0 ? std::min(chunkSize, maxOutputBytes - pumped) : static_cast(chunkSize); + if (outputLimit == 0) { + success = true; + break; + } + + const InflateStream::Status status = inflateCtx.reader.readAtMost(outputBuffer, outputLimit, &produced); + compressedConsumed += beforeRemaining - inflateCtx.fileRemaining; + inflateCtx.file = nullptr; + + totalProduced += produced; + pumped += produced; + if (totalProduced > static_cast(uncompressedSize)) { + LOG_ERR("ZIP", "Decompressed size exceeds expected (%zu > %zu)", totalProduced, + static_cast(uncompressedSize)); + result = ZipStreamStatus::Error; + break; + } + + if (produced > 0 && out.write(outputBuffer, produced) != produced) { + result = ZipStreamStatus::Error; + break; + } + + if (status == InflateStream::Status::Done) { + if (totalProduced != static_cast(uncompressedSize)) { + LOG_ERR("ZIP", "Decompressed size mismatch (expected %zu, got %zu)", static_cast(uncompressedSize), + totalProduced); + result = ZipStreamStatus::Error; + } else { + active = false; + result = ZipStreamStatus::Done; + } + success = result != ZipStreamStatus::Error; + break; + } + + if (status == InflateStream::Status::Error) { + LOG_ERR("ZIP", "Decompression failed"); + result = ZipStreamStatus::Error; + break; + } + + if (maxOutputBytes > 0 && pumped >= maxOutputBytes) { + success = true; + result = ZipStreamStatus::More; + break; + } + + inflateCtx.file = &zipFile; + } + + inflateCtx.file = nullptr; + zipFile.close(); + if (!success && result == ZipStreamStatus::Error) { + return ZipStreamStatus::Error; + } + return result; +} + +void ZipFileStreamReader::abort() { + active = false; + inflateCtx.reader.deinit(); + inflateCtx.file = nullptr; + inflateCtx.fileRemaining = 0; + inflateCtx.readBuf = nullptr; + inflateCtx.readBufSize = 0; + if (readBuffer) { + free(readBuffer); + readBuffer = nullptr; + } + if (outputBuffer) { + free(outputBuffer); + outputBuffer = nullptr; + } + zipPath.clear(); + method = 0; + dataOffset = 0; + compressedSize = 0; + uncompressedSize = 0; + chunkSize = 0; + totalProduced = 0; + compressedConsumed = 0; +} + bool ZipFile::loadAllFileStatSlims() { const ScopedOpenClose zip{*this}; if (!zip) return false; @@ -369,6 +573,74 @@ int ZipFile::fillUncompressedSizes(const SizeTarget* targets, const size_t targe return matched; } +int ZipFile::fillEntryIdentities(const EntryTarget* targets, const size_t targetCount, EntryIdentity* identities, + const size_t identityCount) { + if (targets == nullptr || identities == nullptr || targetCount == 0) { + return 0; + } + + const ScopedOpenClose zip{*this}; + if (!zip) return 0; + + if (!loadZipDetails()) return 0; + + file.seek(zipDetails.centralDirOffset); + + int matched = 0; + const auto expectedMatches = static_cast(targetCount); + const EntryTarget* const targetEnd = targets + targetCount; + uint32_t sig; + char itemName[256]; + + while (file.available()) { + file.read(&sig, 4); + if (sig != 0x02014b50) break; + + // Skip versions, flags, compression method, and modification time/date. + file.seekCur(12); + EntryIdentity identity; + file.read(&identity.crc32, 4); + file.read(&identity.compressedSize, 4); + file.read(&identity.uncompressedSize, 4); + uint16_t nameLen, extraLen, commentLen; + file.read(&nameLen, 2); + file.read(&extraLen, 2); + file.read(&commentLen, 2); + file.seekCur(12); + + if (nameLen < sizeof(itemName)) { + file.read(itemName, nameLen); + itemName[nameLen] = '\0'; + + const uint64_t hash = fnvHash64(itemName, nameLen); + const EntryTarget key = {hash, nameLen, 0, nullptr}; + auto it = std::lower_bound(targets, targetEnd, key, [](const EntryTarget& a, const EntryTarget& b) { + return a.hash < b.hash || (a.hash == b.hash && a.len < b.len); + }); + + while (it != targetEnd && it->hash == hash && it->len == nameLen) { + if (it->index < identityCount && it->path != nullptr && memcmp(it->path, itemName, nameLen) == 0 && + it->path[nameLen] == '\0') { + identity.found = true; + identities[it->index] = identity; + ++matched; + } + ++it; + } + + if (matched >= expectedMatches) { + break; + } + } else { + file.seekCur(nameLen); + } + + file.seekCur(extraLen + commentLen); + } + + return matched; +} + uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) { const ScopedOpenClose zip{*this}; if (!zip) return nullptr; @@ -414,15 +686,15 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo return nullptr; } - InflateReader reader; - if (!reader.init(false)) { - LOG_ERR("ZIP", "Failed to init one-shot inflate reader"); + InflateStream inflate; + if (!inflate.init(false)) { + LOG_ERR("ZIP", "Failed to init one-shot inflate stream"); free(compressedData); free(data); return nullptr; } - reader.setSource(compressedData, deflatedDataSize); - if (!reader.read(data, inflatedDataSize)) { + inflate.setSource(compressedData, deflatedDataSize); + if (!inflate.read(data, inflatedDataSize)) { LOG_ERR("ZIP", "Failed to inflate file"); free(compressedData); free(data); @@ -445,29 +717,24 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo return nullptr; } - auto inflateScratch = ScratchWorkspace::borrow(InflateReader::STREAMING_DICT_SIZE, "ZIP inflate"); ZipInflateCtx ctx; ctx.file = &file; ctx.fileRemaining = deflatedDataSize; ctx.readBuf = fileReadBuffer; ctx.readBufSize = 1024; - bool readerInitialized = false; - if (inflateScratch) { - readerInitialized = ctx.reader.initWithExternalDictionary(inflateScratch.data(), inflateScratch.size()); - } - if (!readerInitialized) { - readerInitialized = ctx.reader.init(true); - } - if (!readerInitialized) { - LOG_ERR("ZIP", "Failed to init inflate reader"); + // One-shot mode: `data` holds the entire output, so back-references + // resolve inside it and no 32KB window is allocated. + InflateStream inflate; + if (!inflate.init(false)) { + LOG_ERR("ZIP", "Failed to init inflate stream"); free(fileReadBuffer); free(data); return nullptr; } - ctx.reader.setReadCallback(zipReadCallback); + inflate.setFill(zipFillCallback, &ctx); - if (!ctx.reader.read(data, inflatedDataSize)) { + if (!inflate.read(data, inflatedDataSize)) { LOG_ERR("ZIP", "Failed to inflate file"); free(fileReadBuffer); free(data); @@ -488,7 +755,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo return data; } -bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize) { +bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize, const bool allowEarlyStop) { const ScopedOpenClose zip{*this}; if (!zip) return false; @@ -520,8 +787,9 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch } if (out.write(buffer, dataRead) != dataRead) { - LOG_ERR("ZIP", "Failed to write all output bytes to stream"); free(buffer); + if (allowEarlyStop) return true; + LOG_ERR("ZIP", "Failed to write all output bytes to stream"); return false; } remaining -= dataRead; @@ -532,24 +800,10 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch } if (fileStat.method == ZIP_METHOD_DEFLATED) { - auto inflateScratch = ScratchWorkspace::borrow(InflateReader::STREAMING_DICT_SIZE, "ZIP stream inflate"); ZipInflateCtx ctx; ctx.file = &file; ctx.fileRemaining = deflatedDataSize; - bool readerInitialized = false; - if (inflateScratch) { - readerInitialized = ctx.reader.initWithExternalDictionary(inflateScratch.data(), inflateScratch.size()); - } - if (!readerInitialized) { - readerInitialized = ctx.reader.init(true); - } - if (!readerInitialized) { - LOG_ERR("ZIP", "Failed to init inflate reader (free=%u, maxAlloc=%u, chunk=%zu)", ESP.getFreeHeap(), - ESP.getMaxAllocHeap(), chunkSize); - return false; - } - auto* fileReadBuffer = static_cast(malloc(chunkSize)); if (!fileReadBuffer) { LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer (free=%u, maxAlloc=%u, chunk=%zu)", @@ -567,14 +821,23 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch ctx.readBuf = fileReadBuffer; ctx.readBufSize = chunkSize; - ctx.reader.setReadCallback(zipReadCallback); + + InflateStream inflate; + if (!inflate.init(true)) { + LOG_ERR("ZIP", "Failed to init inflate stream (free=%u, maxAlloc=%u, chunk=%zu)", ESP.getFreeHeap(), + ESP.getMaxAllocHeap(), chunkSize); + free(outputBuffer); + free(fileReadBuffer); + return false; + } + inflate.setFill(zipFillCallback, &ctx); bool success = false; size_t totalProduced = 0; while (true) { size_t produced; - const InflateStatus status = ctx.reader.readAtMost(outputBuffer, chunkSize, &produced); + const InflateStream::Status status = inflate.readAtMost(outputBuffer, chunkSize, &produced); totalProduced += produced; if (totalProduced > static_cast(inflatedDataSize)) { @@ -585,34 +848,49 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch if (produced > 0) { if (out.write(outputBuffer, produced) != produced) { - LOG_ERR("ZIP", "Failed to write all output bytes to stream"); + if (allowEarlyStop) { + success = true; + } else { + LOG_ERR("ZIP", "Failed to write all output bytes to stream"); + } break; } } - if (status == InflateStatus::Done) { + if (status == InflateStream::Status::Done) { if (totalProduced != static_cast(inflatedDataSize)) { LOG_ERR("ZIP", "Decompressed size mismatch (expected %zu, got %zu)", static_cast(inflatedDataSize), totalProduced); break; } - LOG_DBG("ZIP", "Decompressed %d bytes into %d bytes", deflatedDataSize, inflatedDataSize); success = true; break; } - if (status == InflateStatus::Error) { + if (status == InflateStream::Status::Error) { LOG_ERR("ZIP", "Decompression failed"); break; } - // InflateStatus::Ok: output buffer full, continue + // InflateStream::Status::Ok: output buffer full, continue } free(outputBuffer); free(fileReadBuffer); - return success; // ctx.reader destructor frees the ring buffer + return success; // inflate destructor frees the decompressor state + window } LOG_ERR("ZIP", "Unsupported compression method"); return false; } + +std::unique_ptr ZipFile::openFileStream(const char* filename, const size_t chunkSize) { + auto reader = makeUniqueNoThrow(); + if (!reader) { + LOG_ERR("ZIP", "Failed to allocate cooperative stream reader"); + return nullptr; + } + if (!reader->begin(filePath, filename, chunkSize)) { + return nullptr; + } + return reader; +} diff --git a/lib/ZipFile/ZipFile.h b/lib/ZipFile/ZipFile.h index 3e2e6c8700..84b4d4814d 100644 --- a/lib/ZipFile/ZipFile.h +++ b/lib/ZipFile/ZipFile.h @@ -1,11 +1,56 @@ #pragma once #include +#include +#include #include #include #include +enum class ZipStreamStatus { + More, + Done, + Error, +}; + +struct ZipStreamInflateCtx { + InflateStream reader; + HalFile* file = nullptr; + size_t fileRemaining = 0; + uint8_t* readBuf = nullptr; + size_t readBufSize = 0; +}; + +class ZipFileStreamReader { + public: + ZipFileStreamReader() = default; + ~ZipFileStreamReader(); + ZipFileStreamReader(const ZipFileStreamReader&) = delete; + ZipFileStreamReader& operator=(const ZipFileStreamReader&) = delete; + + bool begin(const std::string& zipPath, const char* filename, size_t chunkSize); + ZipStreamStatus pump(Print& out, size_t maxOutputBytes = 0); + void abort(); + size_t produced() const { return totalProduced; } + + private: + std::string zipPath; + uint16_t method = 0; + uint32_t dataOffset = 0; + uint32_t compressedSize = 0; + uint32_t uncompressedSize = 0; + size_t chunkSize = 0; + size_t totalProduced = 0; + size_t compressedConsumed = 0; + uint8_t* readBuffer = nullptr; + uint8_t* outputBuffer = nullptr; + ZipStreamInflateCtx inflateCtx; + bool active = false; +}; + class ZipFile { + friend class ZipFileStreamReader; + public: struct FileStatSlim { uint16_t method; // Compression method @@ -27,6 +72,23 @@ class ZipFile { uint16_t index; // Caller's index (e.g. spine index) }; + // Target and result for batch central-directory identity lookup. The full + // path check prevents a hash collision from treating unrelated files as + // byte-identical. + struct EntryTarget { + uint64_t hash; + uint16_t len; + uint16_t index; + const char* path; + }; + + struct EntryIdentity { + uint32_t crc32 = 0; + uint32_t compressedSize = 0; + uint32_t uncompressedSize = 0; + bool found = false; + }; + // FNV-1a 64-bit hash computed from char buffer (no std::string allocation) static uint64_t fnvHash64(const char* s, size_t len) { uint64_t hash = 14695981039346656037ull; @@ -65,10 +127,15 @@ class ZipFile { // targets must be sorted by (hash, len). sizes[target.index] receives uncompressedSize. // Returns number of targets matched. int fillUncompressedSizes(const SizeTarget* targets, size_t targetCount, uint32_t* sizes, size_t sizeCount); + // Batch lookup for duplicate detection. Targets must be sorted by (hash, + // len); identities[target.index] receives the ZIP entry's CRC and sizes. + int fillEntryIdentities(const EntryTarget* targets, size_t targetCount, EntryIdentity* identities, + size_t identityCount); // Due to the memory required to run each of these, it is recommended to not preopen the zip file for multiple // These functions will open and close the zip as needed uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false); - bool readFileToStream(const char* filename, Print& out, size_t chunkSize); + bool readFileToStream(const char* filename, Print& out, size_t chunkSize, bool allowEarlyStop = false); + std::unique_ptr openFileStream(const char* filename, size_t chunkSize); template bool enumerateFilePaths(F&& callback) { diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index 376f736b54..f4a0498804 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -2,29 +2,26 @@ #include #include -#include +#include +#include +#include #include #include HalClock halClock; // Singleton instance -// DS3231 register layout (BCD encoded): -// 0x00: Seconds (bits 6-4 = tens, bits 3-0 = ones) -// 0x01: Minutes (bits 6-4 = tens, bits 3-0 = ones) -// 0x02: Hours (bit 6 = 12/24 mode, bits 5-4 = tens, bits 3-0 = ones) -// 0x03: Day (1-7, not used for display) -// 0x04: Date (day of month) -// 0x05: Month (bit 7 = century, bits 4-0 = month) -// 0x06: Year (00-99, interpreted as 2000-2099) - -static uint8_t bcdToDec(uint8_t bcd) { return ((bcd >> 4) * 10) + (bcd & 0x0F); } -static uint8_t decToBcd(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); } - namespace { constexpr uint16_t kBaseYear = 2000; constexpr const char* kMonthNames[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; +constexpr char kFullMonthNames[][10] = {"January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"}; + +esp_err_t clearDnsCache(void*) { + dns_clear_cache(); + return ESP_OK; +} bool isLeapYear(const uint16_t year) { return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; } @@ -72,33 +69,8 @@ void adjustDateByDays(uint16_t& year, uint8_t& month, uint8_t& day, const int da } // namespace void HalClock::begin() { - if (!gpio.deviceIsX3()) { - _available = false; - return; - } - - // I2C is already initialised by HalPowerManager::begin() for X3. - // Probe the DS3231 by reading the seconds register. - Wire.beginTransmission(I2C_ADDR_DS3231); - Wire.write(DS3231_SEC_REG); - if (Wire.endTransmission(false) != 0) { - LOG_INF("CLK", "DS3231 RTC not found"); - _available = false; - return; - } - Wire.requestFrom(I2C_ADDR_DS3231, (uint8_t)1); - if (Wire.available() < 1) { - _available = false; - return; - } - Wire.read(); // discard — just testing connectivity - - _available = true; - LOG_INF("CLK", "DS3231 RTC found"); - - // Prime the cache with an initial read - uint8_t h, m; - getTime(h, m); + _available = _sdkRtc.begin(); + LOG_INF("CLK", _available ? "SDK RTC found" : "RTC not found"); } bool HalClock::getTime(uint8_t& hour, uint8_t& minute) const { @@ -111,18 +83,8 @@ bool HalClock::getTime(uint8_t& hour, uint8_t& minute) const { return true; } - // Read 3 bytes starting at register 0x00: seconds, minutes, hours - Wire.beginTransmission(I2C_ADDR_DS3231); - Wire.write(DS3231_SEC_REG); - if (Wire.endTransmission(false) != 0) { - if (!_hasCachedTime) return false; - _lastPollMs = now; - hour = _cachedHour; - minute = _cachedMinute; - return true; - } - Wire.requestFrom(I2C_ADDR_DS3231, (uint8_t)3); - if (Wire.available() < 3) { + Rtc::DateTime dt; + if (!_sdkRtc.now(dt)) { if (!_hasCachedTime) return false; _lastPollMs = now; hour = _cachedHour; @@ -130,24 +92,14 @@ bool HalClock::getTime(uint8_t& hour, uint8_t& minute) const { return true; } - Wire.read(); // seconds — not needed - const uint8_t rawMin = Wire.read(); - const uint8_t rawHour = Wire.read(); - - _cachedMinute = bcdToDec(rawMin & 0x7F); - // Handle 12/24h mode: bit 6 high = 12h mode - if (rawHour & 0x40) { - // 12h mode: bit 5 = PM, bits 4-0 = hours (1-12) - uint8_t h12 = bcdToDec(rawHour & 0x1F); - bool pm = rawHour & 0x20; - if (h12 == 12) h12 = 0; - _cachedHour = pm ? (h12 + 12) : h12; - } else { - // 24h mode: bits 5-0 = hours (0-23) - _cachedHour = bcdToDec(rawHour & 0x3F); - } + _cachedHour = dt.hour; + _cachedMinute = dt.minute; + _cachedYear = dt.year; + _cachedMonth = dt.month; + _cachedDay = dt.day; _lastPollMs = now; _hasCachedTime = true; + _hasCachedDate = isValidDate(_cachedYear, _cachedMonth, _cachedDay); hour = _cachedHour; minute = _cachedMinute; @@ -165,7 +117,6 @@ bool HalClock::formatTime(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHou int offsetQuarterHours = static_cast(utcOffsetQuarterHoursBiased) - 48; int totalMinutes = static_cast(h) * 60 + static_cast(m) + offsetQuarterHours * 15; - // Wrap around 24 hours totalMinutes = ((totalMinutes % 1440) + 1440) % 1440; const int hour24 = totalMinutes / 60; @@ -194,20 +145,8 @@ bool HalClock::getDate(uint16_t& year, uint8_t& month, uint8_t& day, uint8_t& ho return true; } - Wire.beginTransmission(I2C_ADDR_DS3231); - Wire.write(DS3231_SEC_REG); - if (Wire.endTransmission(false) != 0) { - if (!_hasCachedDate) return false; - _lastPollMs = now; - year = _cachedYear; - month = _cachedMonth; - day = _cachedDay; - hour = _cachedHour; - minute = _cachedMinute; - return true; - } - Wire.requestFrom(I2C_ADDR_DS3231, (uint8_t)7); - if (Wire.available() < 7) { + Rtc::DateTime dt; + if (!_sdkRtc.now(dt)) { if (!_hasCachedDate) return false; _lastPollMs = now; year = _cachedYear; @@ -218,31 +157,16 @@ bool HalClock::getDate(uint16_t& year, uint8_t& month, uint8_t& day, uint8_t& ho return true; } - Wire.read(); // seconds - const uint8_t rawMin = Wire.read(); - const uint8_t rawHour = Wire.read(); - Wire.read(); // weekday - const uint8_t rawDay = Wire.read(); - const uint8_t rawMonth = Wire.read(); - const uint8_t rawYear = Wire.read(); - - _cachedMinute = bcdToDec(rawMin & 0x7F); - if (rawHour & 0x40) { - uint8_t h12 = bcdToDec(rawHour & 0x1F); - const bool pm = rawHour & 0x20; - if (h12 == 12) h12 = 0; - _cachedHour = pm ? (h12 + 12) : h12; - } else { - _cachedHour = bcdToDec(rawHour & 0x3F); - } - _cachedYear = kBaseYear + bcdToDec(rawYear); - _cachedMonth = bcdToDec(rawMonth & 0x1F); - _cachedDay = bcdToDec(rawDay & 0x3F); + _cachedYear = dt.year; + _cachedMonth = dt.month; + _cachedDay = dt.day; + _cachedHour = dt.hour; + _cachedMinute = dt.minute; _lastPollMs = now; _hasCachedTime = true; _hasCachedDate = isValidDate(_cachedYear, _cachedMonth, _cachedDay); - if (!_hasCachedDate) return false; + year = _cachedYear; month = _cachedMonth; day = _cachedDay; @@ -251,7 +175,8 @@ bool HalClock::getDate(uint16_t& year, uint8_t& month, uint8_t& day, uint8_t& ho return true; } -bool HalClock::formatDate(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased) const { +bool HalClock::formatDate(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased, const DateFormat dateFormat, + const char numericSeparator) const { if (bufSize < 13u) return false; uint16_t year; @@ -265,8 +190,40 @@ bool HalClock::formatDate(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHou adjustDateByDays(year, month, day, dayDelta); if (!isValidDate(year, month, day)) return false; - snprintf(buf, bufSize, "%s %u, %u", kMonthNames[month - 1], static_cast(day), - static_cast(year)); + const unsigned int displayMonth = month; + const unsigned int displayDay = day; + const unsigned int displayYear = year; + const char separator = numericSeparator == '.' || numericSeparator == '-' ? numericSeparator : '/'; + switch (dateFormat) { + case DAY_MONTH_YEAR_LONG: + snprintf(buf, bufSize, "%02u %s %u", displayDay, kMonthNames[month - 1], displayYear); + break; + case MONTH_DAY_YEAR_NUMERIC: + snprintf(buf, bufSize, "%02u%c%02u%c%u", displayMonth, separator, displayDay, separator, displayYear); + break; + case DAY_MONTH_YEAR_NUMERIC: + snprintf(buf, bufSize, "%02u%c%02u%c%u", displayDay, separator, displayMonth, separator, displayYear); + break; + case YEAR_MONTH_DAY_NUMERIC: + snprintf(buf, bufSize, "%u%c%02u%c%02u", displayYear, separator, displayMonth, separator, displayDay); + break; + case MONTH_DAY_NUMERIC: + snprintf(buf, bufSize, "%02u%c%02u", displayMonth, separator, displayDay); + break; + case DAY_MONTH_NUMERIC: + snprintf(buf, bufSize, "%02u%c%02u", displayDay, separator, displayMonth); + break; + case MONTH_DAY_LONG: + snprintf(buf, bufSize, "%s %02u", kFullMonthNames[month - 1], displayDay); + break; + case DAY_MONTH_LONG: + snprintf(buf, bufSize, "%02u %s", displayDay, kFullMonthNames[month - 1]); + break; + case MONTH_DAY_YEAR_LONG: + default: + snprintf(buf, bufSize, "%s %02u, %u", kMonthNames[month - 1], displayDay, displayYear); + break; + } return true; } @@ -277,21 +234,20 @@ bool HalClock::writeDateTimeToRTC(uint16_t year, uint8_t month, uint8_t day, uin assert(second < 60); assert(isValidDate(year, month, day)); assert(weekday >= 1 && weekday <= 7); - Wire.beginTransmission(I2C_ADDR_DS3231); - Wire.write(DS3231_SEC_REG); // Start at register 0x00 - Wire.write(decToBcd(second)); // 0x00: Seconds - Wire.write(decToBcd(minute)); // 0x01: Minutes - Wire.write(decToBcd(hour)); // 0x02: Hours (24h mode, bit 6 = 0) - Wire.write(decToBcd(weekday)); - Wire.write(decToBcd(day)); - Wire.write(decToBcd(month)); - Wire.write(decToBcd(static_cast(year - kBaseYear))); - if (Wire.endTransmission() != 0) { - LOG_ERR("CLK", "Failed to write date/time to DS3231"); + + Rtc::DateTime dt; + dt.year = year; + dt.month = month; + dt.day = day; + dt.weekday = weekday; + dt.hour = hour; + dt.minute = minute; + dt.second = second; + if (!_sdkRtc.set(dt)) { + LOG_ERR("CLK", "Failed to write date/time to SDK RTC"); return false; } - // Invalidate cache so next read fetches fresh data _lastPollMs = 0; _cachedHour = hour; _cachedMinute = minute; @@ -306,36 +262,57 @@ bool HalClock::writeDateTimeToRTC(uint16_t year, uint8_t month, uint8_t day, uin bool HalClock::syncFromNTP() { if (!_available) return false; + if (!syncSystemTimeFromNTP()) return false; + + time_t now = time(nullptr); + struct tm timeinfo; + gmtime_r(&now, &timeinfo); + + const uint16_t year = static_cast(timeinfo.tm_year + 1900); + const uint8_t month = static_cast(timeinfo.tm_mon + 1); + const uint8_t day = static_cast(timeinfo.tm_mday); + const uint8_t weekday = static_cast(timeinfo.tm_wday + 1); + if (!writeDateTimeToRTC(year, month, day, weekday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec)) { + return false; + } + + LOG_INF("CLK", "RTC set to %04d-%02d-%02d %02d:%02d:%02d UTC", year, month, day, timeinfo.tm_hour, timeinfo.tm_min, + timeinfo.tm_sec); + return true; +} + +bool HalClock::syncSystemTimeFromNTP() { if (WiFi.status() != WL_CONNECTED) { LOG_ERR("CLK", "WiFi not connected, cannot sync NTP"); return false; } + esp_netif_sntp_deinit(); + LOG_INF("CLK", "Starting NTP sync..."); - configTzTime("UTC0", "pool.ntp.org", "time.nist.gov"); - - // Wait for SNTP sync to complete (up to 5 seconds) - constexpr int maxAttempts = 50; - for (int i = 0; i < maxAttempts; i++) { - if (sntp_get_sync_status() == SNTP_SYNC_STATUS_COMPLETED) { - time_t now = time(nullptr); - struct tm timeinfo; - gmtime_r(&now, &timeinfo); - - const uint16_t year = static_cast(timeinfo.tm_year + 1900); - const uint8_t month = static_cast(timeinfo.tm_mon + 1); - const uint8_t day = static_cast(timeinfo.tm_mday); - const uint8_t weekday = static_cast(timeinfo.tm_wday + 1); - if (writeDateTimeToRTC(year, month, day, weekday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec)) { - LOG_INF("CLK", "RTC set to %04d-%02d-%02d %02d:%02d:%02d UTC", year, month, day, timeinfo.tm_hour, - timeinfo.tm_min, timeinfo.tm_sec); - return true; - } - return false; - } - delay(100); + // esp-netif owns the small temporary SNTP state/semaphore and routes all + // lwIP timer changes through its core-lock-safe execution path. + esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org"); + const esp_err_t initResult = esp_netif_sntp_init(&config); + if (initResult != ESP_OK) { + LOG_ERR("CLK", "Failed to start NTP sync: %s", esp_err_to_name(initResult)); + return false; } - LOG_ERR("CLK", "NTP sync timed out"); - return false; + const esp_err_t syncResult = esp_netif_sntp_sync_wait(pdMS_TO_TICKS(5000)); + // An SNTP timeout can leave its DNS callback pending. Drain that callback + // under lwIP's core lock before stopping SNTP; the next hostname lookup may + // otherwise invoke it from the application task while clearing the cache. + const esp_err_t clearResult = esp_netif_tcpip_exec(clearDnsCache, nullptr); + esp_netif_sntp_deinit(); + if (clearResult != ESP_OK) { + LOG_ERR("CLK", "Failed to clear DNS after NTP sync: %s", esp_err_to_name(clearResult)); + return false; + } + if (syncResult != ESP_OK) { + LOG_ERR("CLK", "NTP sync failed: %s", esp_err_to_name(syncResult)); + return false; + } + + return true; } diff --git a/lib/hal/HalClock.h b/lib/hal/HalClock.h index 3c78cb78c4..3758018b51 100644 --- a/lib/hal/HalClock.h +++ b/lib/hal/HalClock.h @@ -1,15 +1,14 @@ #pragma once #include -#include - -#include "HalGPIO.h" +#include class HalClock; extern HalClock halClock; // Singleton class HalClock { bool _available = false; + mutable Rtc _sdkRtc; mutable uint8_t _cachedHour = 0; mutable uint8_t _cachedMinute = 0; mutable uint16_t _cachedYear = 2000; @@ -22,10 +21,23 @@ class HalClock { static constexpr unsigned long CLOCK_POLL_MS = 10000; // 10 seconds public: - // Call after gpio.begin() and powerManager.begin() (I2C already initialised for X3) + enum DateFormat : uint8_t { + MONTH_DAY_YEAR_LONG = 0, + DAY_MONTH_YEAR_LONG = 1, + MONTH_DAY_YEAR_NUMERIC = 2, + DAY_MONTH_YEAR_NUMERIC = 3, + YEAR_MONTH_DAY_NUMERIC = 4, + MONTH_DAY_NUMERIC = 5, + DAY_MONTH_NUMERIC = 6, + MONTH_DAY_LONG = 7, + DAY_MONTH_LONG = 8, + DATE_FORMAT_COUNT + }; + + // Call after BoardConfig has selected the active device. void begin(); - // True if the DS3231 RTC is present on this device + // True if an RTC is present on this device bool isAvailable() const { return _available; } // Get current hour (0-23) and minute (0-59). @@ -40,17 +52,18 @@ class HalClock { bool formatTime(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased = 48, bool use12Hour = false) const; // Returns the raw RTC date/time before any user-configured timezone offset is applied. - // The DS3231 is synced in UTC, so callers that need wall-clock local time should apply SETTINGS.clockUtcOffsetQ. + // The RTC is synced in UTC, so callers that need wall-clock local time should apply SETTINGS.clockUtcOffsetQ. bool getDateTime(uint16_t& year, uint8_t& month, uint8_t& day, uint8_t& hour, uint8_t& minute) const { return getDate(year, month, day, hour, minute); } - // Format date into a caller-provided buffer as "Mon D, YYYY". + // Format date into a caller-provided buffer using the requested display format. // utcOffsetQuarterHoursBiased matches formatTime so the date rolls over at local midnight. // Returns false if RTC is not available or the RTC date is invalid. - bool formatDate(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased = 48) const; + bool formatDate(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased = 48, + DateFormat dateFormat = MONTH_DAY_YEAR_LONG, char numericSeparator = '/') const; - // Sync the DS3231 RTC from an NTP server. Requires WiFi to be connected. + // Sync the RTC from an NTP server. Requires WiFi to be connected. // Blocks for up to ~5s while waiting for SNTP response. // Returns true if the RTC was successfully updated. // @@ -58,6 +71,9 @@ class HalClock { // so the HAL stays free of any app-layer settings dependency. bool syncFromNTP(); + // Sync the ESP32 system clock without requiring an external RTC. + bool syncSystemTimeFromNTP(); + private: bool getDate(uint16_t& year, uint8_t& month, uint8_t& day, uint8_t& hour, uint8_t& minute) const; bool writeDateTimeToRTC(uint16_t year, uint8_t month, uint8_t day, uint8_t weekday, uint8_t hour, uint8_t minute, diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index 31909d402c..aa518e4a6f 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -71,6 +71,20 @@ void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen) einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen); } +void HalDisplay::displayBufferAsync(HalDisplay::RefreshMode mode) { + if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) { + einkDisplay.requestResync(1); + } + + einkDisplay.displayBufferAsyncNoShadow(convertRefreshMode(mode)); +} + +void HalDisplay::waitRefreshComplete() { einkDisplay.waitRefreshComplete(); } + +bool HalDisplay::supportsAsyncRefresh() const { return einkDisplay.supportsAsyncRefresh(); } + +bool HalDisplay::supportsAsyncGrayscaleBase() const { return !gpio.deviceIsX3() && einkDisplay.supportsAsyncRefresh(); } + void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) { HalSpiBus::Lock spiLock; @@ -88,19 +102,22 @@ void HalDisplay::deepSleep() { uint8_t* HalDisplay::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } +uint8_t* HalDisplay::lendFrameBufferStorage(uint32_t* sizeOut) { return einkDisplay.lendBuildStorage(sizeOut); } + +void HalDisplay::returnFrameBufferStorage() { einkDisplay.returnBuildStorage(); } + void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) { einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer); } void HalDisplay::displayGrayscaleBase(RefreshMode fallback, bool turnOffScreen) { - // X3: a HALF fallback means the caller wants a clean base (e.g. the sleep - // cover, a full-screen swap from arbitrary prior content). Without this, the - // X3 grayscale base takes its gentle differential happy path and the prior - // home/reader frame ghosts through the soft aa_pre_bw_mid waveform. Forcing a - // resync makes displayGrayscaleBase clear first, matching displayBuffer(HALF). - // The reader's FAST path is deliberately left on the differential path so - // per-page grayscale stays cheap. - if (gpio.deviceIsX3() && fallback == RefreshMode::HALF_REFRESH) { + // X3: a HALF or FULL fallback means the caller wants a clean base (e.g. the + // sleep cover, a full-screen swap from arbitrary prior content). Without + // this, the X3 grayscale base takes its gentle differential happy path and + // the prior home/reader frame ghosts through the soft aa_pre_bw_mid + // waveform. Forcing a resync makes displayGrayscaleBase clear first, + // matching displayBuffer(HALF)/displayBuffer(FULL). + if (gpio.deviceIsX3() && fallback != RefreshMode::FAST_REFRESH) { einkDisplay.requestResync(1); } diff --git a/lib/hal/HalDisplay.h b/lib/hal/HalDisplay.h index 9b35254a9c..f8e91dca86 100644 --- a/lib/hal/HalDisplay.h +++ b/lib/hal/HalDisplay.h @@ -39,6 +39,20 @@ class HalDisplay { bool fromProgmem = false) const; void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false); + // Non-blocking refresh (shadow-free): starts the panel waveform and returns + // while the panel refreshes on its own. The framebuffer must stay untouched + // until waitRefreshComplete(), and the caller must rebuild the differential + // baseline before the next differential update (the tiled grayscale cleanup + // does). Panels without deferral fall back to a blocking refresh. + void displayBufferAsync(RefreshMode mode = RefreshMode::FAST_REFRESH); + // Block until a pending deferred refresh completes (no-op when none is). + void waitRefreshComplete(); + // True when displayBufferAsync() genuinely overlaps (panel driver defers); + // false where it falls back to a blocking refresh. + bool supportsAsyncRefresh() const; + // True when the normal async refresh is equivalent to the panel's grayscale + // base pass. X3 has a dedicated grayscale waveform and must stay blocking. + bool supportsAsyncGrayscaleBase() const; void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false); // Power management @@ -47,6 +61,14 @@ class HalDisplay { // Access to frame buffer uint8_t* getFrameBuffer() const; + // Lend the framebuffer's ~48 KB STORAGE to a memory-hungry phase (chapter + // builds) without freeing it: the allocation never moves, so repeated loans + // cannot fragment the heap (free+realloc measurably did). No display calls + // between lend and return; the panel keeps its last refreshed image. The + // buffer comes back white — redraw fully. Returns nullptr if already lent. + uint8_t* lendFrameBufferStorage(uint32_t* sizeOut); + void returnFrameBufferStorage(); + // X3 grayscale preconditioning (OEM "AA-pre-BW(mid)" settle pass), windowed // to the gray region in physical panel coordinates (no-arg = full frame). // Call after the BW base frame is displayed and before the grayscale planes @@ -57,7 +79,9 @@ class HalDisplay { // Display the framebuffer as the base frame for a grayscale overlay that // follows. X3 uses the OEM differential base waveform ("AA-pre-BW(mid)"); // other panels display normally with `fallback` mode (previous behavior). - // Deliberately does NOT force the X3 resync that displayBuffer(HALF) does. + // Forces the same X3 resync that displayBuffer(HALF)/displayBuffer(FULL) do + // whenever `fallback` isn't FAST_REFRESH, so a HALF/FULL caller gets a clean + // base instead of ghosting through the differential waveform. void displayGrayscaleBase(RefreshMode fallback = HALF_REFRESH, bool turnOffScreen = false); void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer); diff --git a/lib/hal/HalFrontlight.cpp b/lib/hal/HalFrontlight.cpp new file mode 100644 index 0000000000..2abea91af2 --- /dev/null +++ b/lib/hal/HalFrontlight.cpp @@ -0,0 +1,36 @@ +#include "HalFrontlight.h" + +#include + +HalFrontlight HalFrontlight::instance; + +void HalFrontlight::begin(const uint8_t brightness, const uint8_t warmth, const bool on) { + if (!manager.present()) { + return; + } + manager.begin(); + lastBrightness = brightness > 100 ? 100 : brightness; + manager.setColorTemperature(warmth > 100 ? 100 : warmth); + lit = on; + manager.setBrightness(lit ? lastBrightness : 0); + LOG_INF("LIGHT", "Frontlight up: %u%% warm=%u%% %s", lastBrightness, manager.colorTemperature(), lit ? "on" : "off"); +} + +void HalFrontlight::setBrightness(const uint8_t percent) { + lastBrightness = percent > 100 ? 100 : percent; + if (lit) { + manager.setBrightness(lastBrightness); + } +} + +void HalFrontlight::setWarmth(const uint8_t warmPercent) { + manager.setColorTemperature(warmPercent > 100 ? 100 : warmPercent); +} + +void HalFrontlight::setOn(const bool on) { + if (on == lit) { + return; + } + lit = on; + manager.setBrightness(lit ? lastBrightness : 0); +} diff --git a/lib/hal/HalFrontlight.h b/lib/hal/HalFrontlight.h new file mode 100644 index 0000000000..723c767e35 --- /dev/null +++ b/lib/hal/HalFrontlight.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +// Thin HAL over the SDK's FrontlightManager (activities never touch SDK +// classes directly). Inert on boards without a frontlight, so it is always +// safe to call. State (brightness / warmth / on) is owned by the SDK manager; +// persistence policy lives in CrossPointSettings and the frontlight panel. +class HalFrontlight { + public: + static HalFrontlight& getInstance() { return instance; } + + // Bring up the PWM channel(s) and apply the given persisted state. + void begin(uint8_t brightness, uint8_t warmth, bool on); + + bool present() const { return manager.present(); } + bool hasColorTemperature() const { return manager.hasColorTemperature(); } + + // Brightness as 0-100 (total light on a warm/cool pair). Setting a value + // while off leaves the light off; it is applied on the next on(). + void setBrightness(uint8_t percent); + // Warm/cool mix, 0 = cool .. 100 = warm. No-op on single-channel boards. + void setWarmth(uint8_t warmPercent); + void setOn(bool on); + + uint8_t brightness() const { return lastBrightness; } + uint8_t warmth() const { return manager.colorTemperature(); } + bool isOn() const { return lit; } + + private: + HalFrontlight() = default; + + FrontlightManager manager; + // The SDK manager folds "off" into brightness 0; keep the user's level and + // the on/off state separate so toggling restores the previous level. + uint8_t lastBrightness = 60; + bool lit = false; + + static HalFrontlight instance; +}; + +#define Frontlight HalFrontlight::getInstance() diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index 4225113b44..ca3f6ed470 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -1,47 +1,26 @@ +#include #include #include #include +#include #include #include -#include #include #include +#if FREEINK_MCU_S3 +#include +#endif + // Global HalGPIO instance HalGPIO gpio; -namespace X3GPIO { - -bool readI2CReg16LE(uint8_t addr, uint8_t reg, uint16_t* outValue) { - Wire.beginTransmission(addr); - Wire.write(reg); - if (Wire.endTransmission(false) != 0) { - return false; - } - if (Wire.requestFrom(addr, static_cast(2), static_cast(true)) < 2) { - while (Wire.available()) { - Wire.read(); - } - return false; - } - const uint8_t lo = Wire.read(); - const uint8_t hi = Wire.read(); - *outValue = (static_cast(hi) << 8) | lo; - return true; -} - -bool readBQ27220CurrentMA(int16_t* outCurrent) { - uint16_t raw = 0; - if (!readI2CReg16LE(I2C_ADDR_BQ27220, BQ27220_CUR_REG, &raw)) { - return false; - } - *outCurrent = static_cast(raw); - return true; -} - -} // namespace X3GPIO - namespace { +constexpr unsigned long BUTTON_DEBOUNCE_REPOLL_MS = 6; + +// The X3-vs-X4 fingerprint (freeink::detectXteinkVerdict) only makes sense on +// Xteink hardware; other boards keep their compile-time BoardConfig profile. +#if FREEINK_DEVICE_X4 || FREEINK_DEVICE_X3 constexpr char HW_NAMESPACE[] = "cphw"; constexpr char NVS_KEY_DEV_OVERRIDE[] = "dev_ovr"; // 0=auto, 1=x4, 2=x3 constexpr char NVS_KEY_DEV_CACHED[] = "dev_det"; // 0=unknown, 1=x4, 2=x3 @@ -89,60 +68,112 @@ HalGPIO::DeviceType detectDeviceTypeWithFingerprint() { return nvsToDeviceType(cachedValue); } - // No cache yet: use FreeInk's canonical two-pass X3 fingerprint and persist - // only confirmed results. Inconclusive probes deliberately remain uncached. + // No cache yet: run the SDK's X3 fingerprint probe and persist the result. uint8_t score1 = 0; uint8_t score2 = 0; const freeink::XteinkVerdict verdict = freeink::detectXteinkVerdict(&score1, &score2); - LOG_INF("HW", "Xteink probe scores: pass1=%u pass2=%u verdict=%u", score1, score2, static_cast(verdict)); + LOG_INF("HW", "X3 probe scores: pass1=%u pass2=%u", score1, score2); + + switch (verdict) { + case freeink::XteinkVerdict::X3Confirmed: + writeNvsDeviceValue(NVS_KEY_DEV_CACHED, NvsDeviceValue::X3); + return HalGPIO::DeviceType::X3; + case freeink::XteinkVerdict::X4Confirmed: + writeNvsDeviceValue(NVS_KEY_DEV_CACHED, NvsDeviceValue::X4); + return HalGPIO::DeviceType::X4; + case freeink::XteinkVerdict::Inconclusive: + break; + } - if (verdict == freeink::XteinkVerdict::X3Confirmed) { - writeNvsDeviceValue(NVS_KEY_DEV_CACHED, NvsDeviceValue::X3); - return HalGPIO::DeviceType::X3; + // Conservative fallback for first boot with inconclusive probes; not cached, + // so the next boot re-probes. + return HalGPIO::DeviceType::X4; +} + +// --- X3 panel-controller fingerprint (UC8253 vs UC8279) ---------------------- +// Newer X3 production units ship a UC8279d panel controller on the same board, +// glass and pins. The SDK probe reads the UC8279's VER/FLG registers over a +// bit-banged half-duplex SPI on the EPD pins; same override/cache scheme as +// the device fingerprint (values reuse NvsDeviceValue: 1 = UC8253, 2 = UC8279). +constexpr char NVS_KEY_EPD_OVERRIDE[] = "epd_ovr"; // 0=auto, 1=uc8253, 2=uc8279 +constexpr char NVS_KEY_EPD_CACHED[] = "epd_det"; // 0=unknown, 1=uc8253, 2=uc8279 + +bool detectX3DisplayIsUc8279() { + const NvsDeviceValue overrideValue = readNvsDeviceValue(NVS_KEY_EPD_OVERRIDE, NvsDeviceValue::Unknown); + if (overrideValue != NvsDeviceValue::Unknown) { + LOG_INF("HW", "EPD controller override active: %s", overrideValue == NvsDeviceValue::X3 ? "UC8279" : "UC8253"); + return overrideValue == NvsDeviceValue::X3; } - if (verdict == freeink::XteinkVerdict::X4Confirmed) { - writeNvsDeviceValue(NVS_KEY_DEV_CACHED, NvsDeviceValue::X4); - return HalGPIO::DeviceType::X4; + const NvsDeviceValue cachedValue = readNvsDeviceValue(NVS_KEY_EPD_CACHED, NvsDeviceValue::Unknown); + if (cachedValue != NvsDeviceValue::Unknown) { + LOG_INF("HW", "Using cached EPD controller: %s", cachedValue == NvsDeviceValue::X3 ? "UC8279" : "UC8253"); + return cachedValue == NvsDeviceValue::X3; } - // Conservative fallback for first boot with inconclusive probes. - return HalGPIO::DeviceType::X4; + uint8_t ver[5] = {0}; + uint8_t flg = 0; + const freeink::X3DisplayVerdict verdict = freeink::detectX3DisplayController(ver, &flg); + LOG_INF("HW", "EPD probe: ver=%02X %02X %02X %02X %02X flg=%02X verdict=%u", ver[0], ver[1], ver[2], ver[3], ver[4], + flg, static_cast(verdict)); + if (verdict == freeink::X3DisplayVerdict::Uc8279Confirmed) { + writeNvsDeviceValue(NVS_KEY_EPD_CACHED, NvsDeviceValue::X3); + return true; + } + if (verdict == freeink::X3DisplayVerdict::Uc8253Assumed) { + writeNvsDeviceValue(NVS_KEY_EPD_CACHED, NvsDeviceValue::X4); + } + // Inconclusive: run as UC8253 (the shipping controller) but don't persist, + // so a flaky first boot gets re-probed. + return false; } +#endif // FREEINK_DEVICE_X4 || FREEINK_DEVICE_X3 + } // namespace void HalGPIO::begin() { -#if FREEINK_MCU_C3 +#if FREEINK_DEVICE_X4 || FREEINK_DEVICE_X3 #ifdef FORCE_DEVICE_X3 _deviceType = DeviceType::X3; LOG_INF("HW", "Device override active via build flag: X3"); #else _deviceType = detectDeviceTypeWithFingerprint(); #endif - BoardConfig::selectDevice(deviceIsX3() ? BoardConfig::Board::XteinkX3 : BoardConfig::Board::XteinkX4); - - // Resolve the per-batch controller before SPI owns the display pins. - freeink::applyXteinkDisplayController(); - if (deviceIsX3() && BoardConfig::ACTIVE.displayController == BoardConfig::DisplayController::UC8279) { - BoardConfig::selectDevice(BoardConfig::Board::XteinkX3Uc8279); + // The panel-controller probe bit-bangs the EPD pins, so it must run before + // SPI.begin() attaches them to the SPI matrix. I2C-only device fingerprint + // above doesn't care about SPI ordering. + const bool x3IsUc8279 = deviceIsX3() && detectX3DisplayIsUc8279(); + BoardConfig::selectDevice(!deviceIsX3() ? BoardConfig::Board::XteinkX4 + : x3IsUc8279 ? BoardConfig::Board::XteinkX3Uc8279 + : BoardConfig::Board::XteinkX3); + + // CrossInk's X3 override/cache remains authoritative. X4 has no equivalent + // local override, so use the SDK's factory-aware controller selection before + // SPI claims the display pins. + if (deviceIsX4()) { + freeink::applyXteinkDisplayController(); } SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS); -#else - _deviceType = DeviceType::X4; -#endif - - inputMgr.begin(); if (deviceIsX4()) { pinMode(BAT_GPIO0, INPUT); pinMode(UART0_RXD, INPUT); } +#endif + inputMgr.begin(); } void HalGPIO::update() { inputMgr.update(); + if (inputMgr.isDebouncePending()) { + // The SDK commits a state change after it remains stable for more than + // 5 ms. Re-poll before the low-power loop's next ~100 ms sample so short + // physical button presses are not discarded. + delay(BUTTON_DEBOUNCE_REPOLL_MS); + inputMgr.update(); + } const bool connected = isUsbConnected(); usbStateChanged = (connected != lastUsbConnected); lastUsbConnected = connected; @@ -164,29 +195,72 @@ unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); } unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); } -void HalGPIO::startDeepSleep() { - // Ensure that the power button has been released to avoid immediately turning back on if you're holding it - while (inputMgr.isPressed(BTN_POWER)) { - delay(50); - inputMgr.update(); - } - // Arm the wakeup trigger *after* the button is released - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); - // Enter Deep Sleep - esp_deep_sleep_start(); +#if CROSSINK_APP_CAP_TOUCH +bool HalGPIO::hasTouch() const { return inputMgr.hasTouch(); } + +bool HalGPIO::hasHomeKey() const { return BoardConfig::hasHomeKey(); } + +bool HalGPIO::wasHomeKeyPressed() const { return inputMgr.wasHomeKeyPressed(); } + +bool HalGPIO::wasHomeKeyTapped() const { return inputMgr.wasHomeKeyTapped(); } + +bool HalGPIO::wasHomeKeyLongPressed() const { return inputMgr.wasHomeKeyLongPressed(); } + +bool HalGPIO::wasTouchTap(float& nx, float& ny) const { return inputMgr.wasTouchTap(nx, ny); } + +bool HalGPIO::wasTouchDown(float& nx, float& ny) const { return inputMgr.wasTouchPressedAt(nx, ny); } + +bool HalGPIO::wasTouchReleased() const { return inputMgr.wasTouchReleased(); } + +bool HalGPIO::isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const { + return inputMgr.isTouchTapCandidate(nx, ny, heldMs); +} + +bool HalGPIO::isTouchHeldAt(float& nx, float& ny) const { return inputMgr.isTouchHeldAt(nx, ny); } + +unsigned long HalGPIO::lastTouchHeldMs() const { return inputMgr.lastTouchHeldMs(); } + +bool HalGPIO::wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const { + return inputMgr.wasSwipe(nxStart, nyStart, nxEnd, nyEnd); +} + +bool HalGPIO::wasTouchActivity() const { return inputMgr.wasTouchActivity(); } +#endif + +void HalGPIO::setSharedConfirmPowerShortPressEmitsPower(const bool enabled) { + InputManager::setSharedConfirmPowerShortPressEmitsPower(enabled); +} + +bool HalGPIO::isXteinkDevice() const { + return BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX3 || + BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX3Uc8279 || + BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX4; } -void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) { +bool HalGPIO::hasEdgeSideButtons() const { + return BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX3 || + BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX4Pro; +} + +bool HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) { + // Boards without a power button (or M5Paper's latch circuit) cannot verify a + // hold; treat the wake as valid. + if (BoardConfig::ACTIVE.input.power < 0) { + return true; + } +#if defined(FREEINK_DEVICE_M5PAPER) && FREEINK_DEVICE_M5PAPER + return true; +#endif if (shortPressAllowed) { // Fast path - no duration check needed - return; + return true; } // TODO: Intermittent edge case remains: a single tap followed by another single tap // can still power on the device. Tighten wake debounce/state handling here. - // Calibrate: subtract boot time already elapsed, assuming button held since boot - const uint16_t calibration = millis(); - const uint16_t calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1; + // Calibrate: subtract boot time already elapsed, assuming button held since boot. + const unsigned long calibration = millis(); + const unsigned long calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1; const auto start = millis(); inputMgr.update(); @@ -201,28 +275,53 @@ void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPre inputMgr.update(); } while (inputMgr.isPressed(BTN_POWER) && inputMgr.getPowerButtonHeldTime() < calibratedDuration); if (inputMgr.getPowerButtonHeldTime() < calibratedDuration) { - startDeepSleep(); + return false; } } else { - startDeepSleep(); + return false; + } + return true; +} + +#if FREEINK_MCU_S3 +// USB host presence via the USB-Serial-JTAG peripheral's SOF frame counter: a +// connected host clocks 1 kHz start-of-frame packets, so the counter advancing +// between polls means a data-capable host is attached. Held for a short window +// so multiple callers within one loop tick (update() plus header renders) all +// see the same answer. Limitation: a data-less wall charger sends no SOFs and +// stays invisible — on boards with no VBUS line (X4 Pro, see +// xteink-x4pro-support.md) this is the only observable USB signal. +static bool usbHostSofActive() { + static uint32_t lastFrame = 0xFFFFFFFF; + static unsigned long lastAdvanceMs = 0; + const uint32_t frame = REG_READ(USB_SERIAL_JTAG_FRAM_NUM_REG); + if (frame != lastFrame) { + lastFrame = frame; + lastAdvanceMs = millis(); + return true; } + return lastAdvanceMs != 0 && millis() - lastAdvanceMs < 1500; } +#endif bool HalGPIO::isUsbConnected() const { +#if FREEINK_DEVICE_X3 if (deviceIsX3()) { - // X3: infer USB/charging via BQ27220 Current() register (0x0C, signed mA). - // Positive current means charging. - for (uint8_t attempt = 0; attempt < 2; ++attempt) { - int16_t currentMa = 0; - if (X3GPIO::readBQ27220CurrentMA(¤tMa)) { - return currentMa > 0; - } - delay(2); - } - return false; + // X3 has no USB-detect pin; infer external power from the gauge's charge + // current via the SDK's BatteryMonitor (BQ27220 Current() > 0 = charging). + static const BatteryMonitor battery; + return battery.isCharging(); } - // U0RXD/GPIO20 reads HIGH when USB is connected - return digitalRead(UART0_RXD) == HIGH; +#endif + if (BoardConfig::ACTIVE.usbDetect >= 0) { + return digitalRead(BoardConfig::ACTIVE.usbDetect) == HIGH; + } +#if FREEINK_MCU_S3 + // No VBUS line on this board (X4 Pro): fall back to native-USB host detection. + return usbHostSofActive(); +#else + return false; +#endif } HalGPIO::WakeupReason HalGPIO::getWakeupReason() const { diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h index 94e9bcd711..2a9dcb4753 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -3,6 +3,8 @@ #include #include +#include "AppCapabilities.h" + // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) #define EPD_SCLK 8 // SPI Clock #define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) @@ -17,27 +19,6 @@ #define UART0_RXD 20 // Used for USB connection detection -// Xteink X3 Hardware -#define X3_I2C_SDA 20 -#define X3_I2C_SCL 0 -#define X3_I2C_FREQ 400000 - -// TI BQ27220 Fuel gauge I2C -#define I2C_ADDR_BQ27220 0x55 // Fuel gauge I2C address -#define BQ27220_SOC_REG 0x2C // StateOfCharge() command code (%) -#define BQ27220_CUR_REG 0x0C // Current() command code (signed mA) -#define BQ27220_VOLT_REG 0x08 // Voltage() command code (mV) - -// Analog DS3231 RTC I2C -#define I2C_ADDR_DS3231 0x68 // RTC I2C address -#define DS3231_SEC_REG 0x00 // Seconds command code (BCD) - -// QST QMI8658 IMU I2C -#define I2C_ADDR_QMI8658 0x6B // IMU I2C address -#define I2C_ADDR_QMI8658_ALT 0x6A // IMU I2C fallback address -#define QMI8658_WHO_AM_I_REG 0x00 // WHO_AM_I command code -#define QMI8658_WHO_AM_I_VALUE 0x05 // WHO_AM_I expected value - class HalGPIO { #if CROSSPOINT_EMULATED == 0 InputManager inputMgr; @@ -58,6 +39,14 @@ class HalGPIO { // Inline device type helpers for cleaner downstream checks inline bool deviceIsX3() const { return _deviceType == DeviceType::X3; } inline bool deviceIsX4() const { return _deviceType == DeviceType::X4; } + bool isXteinkDevice() const; + + // True when the board's page buttons sit on the left/right screen edges + // (X3, X4 Pro) rather than an off-screen vertical rocker. Drives side-hint + // placement, the flipped large-step direction in selection activities, and + // the keyboard's side-gutter reserve. Keyed off the active BoardConfig + // profile, not the X3/X4 runtime detection. + bool hasEdgeSideButtons() const; // Start button GPIO and setup SPI for screen and SD card void begin(); @@ -71,14 +60,35 @@ class HalGPIO { bool wasAnyReleased() const; unsigned long getHeldTime() const; unsigned long getPowerButtonHeldTime() const; - - // Setup wake up GPIO and enter deep sleep - void startDeepSleep(); +#if CROSSINK_APP_CAP_TOUCH + bool hasTouch() const; + // Capacitive home key under the bezel, reported by the touch controller + // (e.g. X4 Pro's GT911 key). Tap = short press (fires on release, the primary + // "home" action); LongPress = held ~700ms (a hold shortcut, e.g. reader menu). + bool hasHomeKey() const; + bool wasHomeKeyPressed() const; + bool wasHomeKeyTapped() const; + bool wasHomeKeyLongPressed() const; + bool wasTouchTap(float& nx, float& ny) const; + bool wasTouchDown(float& nx, float& ny) const; + // Raw release edge, reported even when the contact was not a tap (swipe end, + // drag-off). Snapshot builders forward it so interaction routing can clear + // pressed state. + bool wasTouchReleased() const; + bool isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const; + bool isTouchHeldAt(float& nx, float& ny) const; + unsigned long lastTouchHeldMs() const; + bool wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const; + bool wasTouchActivity() const; +#else + constexpr bool hasTouch() const { return false; } +#endif + void setSharedConfirmPowerShortPressEmitsPower(bool enabled); // Verify power button was held long enough after wakeup. - // If verification fails, enters deep sleep and does not return. + // Returns true if verification succeeded, false if device should return to sleep. // Should only be called when wakeup reason is PowerButton. - void verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed); + bool verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed); // Check if USB is connected bool isUsbConnected() const; diff --git a/lib/hal/HalPowerManager.cpp b/lib/hal/HalPowerManager.cpp index 325ddb05f3..1310c09d00 100644 --- a/lib/hal/HalPowerManager.cpp +++ b/lib/hal/HalPowerManager.cpp @@ -1,8 +1,11 @@ #include "HalPowerManager.h" +#include #include +#include #include #include +#include #include @@ -31,14 +34,8 @@ void disableWiFiBeforeDeepSleep() { } // namespace void HalPowerManager::begin() { - if (gpio.deviceIsX3()) { - // X3 uses an I2C fuel gauge for battery monitoring. - // I2C init must come AFTER gpio.begin() so early hardware detection/probes are finished. - Wire.begin(X3_I2C_SDA, X3_I2C_SCL, X3_I2C_FREQ); - Wire.setTimeOut(4); - _batteryUseI2C = true; - } else { - pinMode(BAT_GPIO0, INPUT); + if (BoardConfig::ACTIVE.batteryAdc >= 0) { + pinMode(BoardConfig::ACTIVE.batteryAdc, INPUT); } normalFreq = getCpuFrequencyMhz(); modeMutex = xSemaphoreCreateMutex(); @@ -50,20 +47,25 @@ void HalPowerManager::setPowerSaving(bool enabled) { return; // invalid state } + if (modeMutex != nullptr) { + xSemaphoreTake(modeMutex, portMAX_DELAY); + } + auto wifiMode = WiFi.getMode(); if (wifiMode != WIFI_MODE_NULL) { // Wifi is active, force disabling power saving enabled = false; } - // Note: We don't use mutex here to avoid too much overhead, - // it's not very important if we read a slightly stale value for currentLockMode const LockMode mode = currentLockMode; if (mode == None && enabled && !isLowPower) { LOG_DBG("PWR", "Going to low-power mode"); if (!setCpuFrequencyMhz(LOW_POWER_FREQ)) { LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", LOW_POWER_FREQ); + if (modeMutex != nullptr) { + xSemaphoreGive(modeMutex); + } return; } isLowPower = true; @@ -72,23 +74,24 @@ void HalPowerManager::setPowerSaving(bool enabled) { LOG_DBG("PWR", "Restoring normal CPU frequency"); if (!setCpuFrequencyMhz(normalFreq)) { LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", normalFreq); + if (modeMutex != nullptr) { + xSemaphoreGive(modeMutex); + } return; } isLowPower = false; } + if (modeMutex != nullptr) { + xSemaphoreGive(modeMutex); + } + // Otherwise, no change needed } void HalPowerManager::startDeepSleep(HalGPIO& gpio) const { disableWiFiBeforeDeepSleep(); - // Ensure that the power button has been released to avoid immediately turning back on if you're holding it - while (gpio.isPressed(HalGPIO::BTN_POWER)) { - delay(50); - gpio.update(); - } - #ifdef ENABLE_SERIAL_LOG // Tear down HWCDC so the host sees a clean disconnect and the peripheral // doesn't hold power domains that interfere with USB-powered GPIO wake. @@ -97,53 +100,55 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio) const { logSerial.end(); #endif - // Pre-sleep routines from the original firmware - // GPIO13 is connected to battery latch MOSFET, we need to make sure it's low during sleep - // Note that this means the MCU will be completely powered off during sleep, including RTC - constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13; - gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT); - gpio_set_level(GPIO_SPIWP, 0); +#if !SOC_PM_SUPPORT_EXT1_WAKEUP + // Release every configured battery latch. BoardConfig owns the pin mapping; + // the collision guard prevents a stale/mismatched profile from driving a + // display or SD bus pin low and holding it through sleep. + for (const int8_t pin : {BoardConfig::ACTIVE.power.latch0, BoardConfig::ACTIVE.power.latch1}) { + if (pin < 0 || BoardConfig::latchConflictsWithBus(pin)) continue; + const auto latch = static_cast(pin); + gpio_set_direction(latch, GPIO_MODE_OUTPUT); + gpio_set_level(latch, 0); + gpio_hold_en(latch); + } +#endif + + // Cut the gated peripheral rails (touch/SD/EPD on boards like the Sticky) and + // hold the enables off through deep sleep — otherwise the GT911 and SD card + // stay powered all through "off" and drain the battery. No-op on boards with + // no switched rails (X4/X3). Trade-off: no touch-to-wake; wake is the power + // button. Must run after display.deepSleep() so the panel controller gets its + // deep-sleep command while its rail is still up (enterDeepSleep() in main.cpp + // guarantees that ordering). + freeink::PowerManager::powerDownRailsForSleep(); + + // The SDK convenience helper currently isolates every GPIO after arming the + // wake source. On the ESP32-C3 that overwrites the power pin's sleep input + // configuration, so short presses can be missed. Isolate first, then restore + // and arm the board-configured power pin immediately before sleeping. + freeink::PowerManager::waitForPowerButtonRelease(); esp_sleep_config_gpio_isolate(); + freeink::PowerManager::armPowerButtonWakeup(); gpio_deep_sleep_hold_en(); - gpio_hold_en(GPIO_SPIWP); - pinMode(InputManager::POWER_BUTTON_PIN, INPUT_PULLUP); - // Arm the wakeup trigger *after* the button is released - // Note: this is only useful for waking up on USB power. On battery, the MCU will be completely powered off, so the - // power button is hard-wired to briefly provide power to the MCU, waking it up regardless of the wakeup source - // configuration - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); - // Enter Deep Sleep esp_deep_sleep_start(); } uint16_t HalPowerManager::getBatteryPercentage() const { - if (_batteryUseI2C) { + static const BatteryMonitor battery; + if (BoardConfig::ACTIVE.batteryGauge.gaugeAddr != 0) { const unsigned long now = millis(); if (_batteryLastPollMs != 0 && (now - _batteryLastPollMs) < BATTERY_POLL_MS) { return _batteryCachedPercent; } - // Read SOC directly from I2C fuel gauge (16-bit LE register). - // On I2C error, keep last known value to avoid UI jitter/slowdowns. - Wire.beginTransmission(I2C_ADDR_BQ27220); - Wire.write(BQ27220_SOC_REG); - if (Wire.endTransmission(false) != 0) { - _batteryLastPollMs = now; - return _batteryCachedPercent; - } - Wire.requestFrom(I2C_ADDR_BQ27220, (uint8_t)2); - if (Wire.available() < 2) { - _batteryLastPollMs = now; + _batteryLastPollMs = now; + uint16_t percent = 0; + if (!battery.readPercentageChecked(percent)) { return _batteryCachedPercent; } - const uint8_t lo = Wire.read(); - const uint8_t hi = Wire.read(); - const uint16_t soc = (hi << 8) | lo; - _batteryCachedPercent = soc > 100 ? 100 : soc; - _batteryLastPollMs = now; + _batteryCachedPercent = percent; return _batteryCachedPercent; } - static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0); // smooth the battery %. if (_batteryCachedPercent == 0) { diff --git a/lib/hal/HalPowerManager.h b/lib/hal/HalPowerManager.h index 19f6489310..96f49e06de 100644 --- a/lib/hal/HalPowerManager.h +++ b/lib/hal/HalPowerManager.h @@ -29,7 +29,11 @@ class HalPowerManager { SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode public: - static constexpr int LOW_POWER_FREQ = 10; // MHz +#if defined(BOARD_HAS_PSRAM) + static constexpr int LOW_POWER_FREQ = 80; // MHz +#else + static constexpr int LOW_POWER_FREQ = 10; // MHz +#endif static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms static constexpr unsigned long BATTERY_POLL_MS = 1500; // ms diff --git a/lib/hal/HalStorage.cpp b/lib/hal/HalStorage.cpp index 63ae9a6e83..680eafbb8d 100644 --- a/lib/hal/HalStorage.cpp +++ b/lib/hal/HalStorage.cpp @@ -5,10 +5,11 @@ #include // need to be included before SdFat.h for compatibility with FS.h's File class #include #include -#include #include #include +#include +#include #include "HalSpiBus.h" @@ -135,6 +136,10 @@ class HalStorage::StorageLock { HalStorage::StorageLock lock; \ return SDCard.method(__VA_ARGS__); +uint64_t HalStorage::totalBytes() const { return SDCard.sdTotalBytes(); } + +uint64_t HalStorage::usedBytes() { HAL_STORAGE_WRAPPED_CALL(sdUsedBytes); } + std::vector HalStorage::listFiles(const char* path, int maxFiles) { HAL_STORAGE_WRAPPED_CALL(listFiles, path, maxFiles); } @@ -168,9 +173,23 @@ class HalFile::Impl { FsFile file; }; +void HalFile::ImplDeleter::operator()(Impl* const impl) const { + if (!impl) return; + impl->~Impl(); + std::free(impl); +} + +void* HalFile::allocateImplStorage() { + // The ESP32 build disables exceptions, so `new (std::nothrow)` can still + // terminate through libstdc++ when the allocation cannot be satisfied. + // malloc gives this storage wrapper the recoverable failure contract callers + // expect while preserving FsFile's value semantics. + return std::malloc(sizeof(Impl)); +} + HalFile::HalFile() = default; -HalFile::HalFile(std::unique_ptr impl) : impl(std::move(impl)) {} +HalFile::HalFile(ImplPtr impl) : impl(std::move(impl)) {} HalFile::~HalFile() { close(); } @@ -180,6 +199,8 @@ HalFile& HalFile::operator=(HalFile&& other) { if (this == &other) return *this; close(); impl = std::move(other.impl); + allocationFailed_ = other.allocationFailed_; + other.allocationFailed_ = false; return *this; } @@ -192,13 +213,16 @@ HalFile HalStorage::open(const char* path, const oflag_t oflag) { if (!fsFile) { return HalFile(); } - auto impl = makeUniqueNoThrow(std::move(fsFile)); + void* const storage = HalFile::allocateImplStorage(); + HalFile::ImplPtr impl(storage ? ::new (storage) HalFile::Impl(std::move(fsFile)) : nullptr); if (!impl) { LOG_ERR("SD", "OOM: HalFile wrapper for %s (%u free, %u max alloc)", path, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); StorageLock lock; fsFile.close(); - return HalFile(); + HalFile failed; + failed.allocationFailed_ = true; + return failed; } return HalFile(std::move(impl)); } @@ -225,7 +249,8 @@ bool HalStorage::openFileForRead(const char* moduleName, const char* path, HalFi if (!ok) { return false; } - auto impl = makeUniqueNoThrow(std::move(fsFile)); + void* const storage = HalFile::allocateImplStorage(); + HalFile::ImplPtr impl(storage ? ::new (storage) HalFile::Impl(std::move(fsFile)) : nullptr); if (!impl) { LOG_ERR(moduleName, "OOM: HalFile read wrapper for %s (%u free, %u max alloc)", path, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); @@ -256,7 +281,8 @@ bool HalStorage::openFileForWrite(const char* moduleName, const char* path, HalF if (!ok) { return false; } - auto impl = makeUniqueNoThrow(std::move(fsFile)); + void* const storage = HalFile::allocateImplStorage(); + HalFile::ImplPtr impl(storage ? ::new (storage) HalFile::Impl(std::move(fsFile)) : nullptr); if (!impl) { LOG_ERR(moduleName, "OOM: HalFile write wrapper for %s (%u free, %u max alloc)", path, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); @@ -276,7 +302,45 @@ bool HalStorage::openFileForWrite(const char* moduleName, const String& path, Ha return openFileForWrite(moduleName, path.c_str(), file); } -bool HalStorage::removeDir(const char* path) { HAL_STORAGE_WRAPPED_CALL(removeDir, path); } +bool HalStorage::removeDir(const char* path) { + if (!path || path[0] == '\0') { + return false; + } + + HalFile dir = open(path); + if (!dir || !dir.isDirectory()) { + dir.close(); + return false; + } + + char name[128]; + for (HalFile entry = dir.openNextFile(); entry; entry = dir.openNextFile()) { + const bool isDirectory = entry.isDirectory(); + const size_t nameLen = entry.getName(name, sizeof(name)); + + // SdFat cannot reopen or delete this path while its directory entry is open. + entry.close(); + if (nameLen == 0) { + dir.close(); + return false; + } + + std::string entryPath(path); + if (entryPath.back() != '/') { + entryPath += '/'; + } + entryPath += name; + + const bool removed = isDirectory ? removeDir(entryPath.c_str()) : remove(entryPath.c_str()); + if (!removed) { + dir.close(); + return false; + } + } + + dir.close(); + return rmdir(path); +} // HalFile implementation // Allow doing file operations while ensuring thread safety via HalStorage's mutex. @@ -315,17 +379,21 @@ bool HalFile::close() { HalStorage::StorageLock lock; const bool ok = impl->file.close(); impl.reset(); + allocationFailed_ = false; return ok; } HalFile HalFile::openNextFile() { + allocationFailed_ = false; HalStorage::StorageLock lock; assert(impl != nullptr); auto fsFile = impl->file.openNextFile(); if (!fsFile) { return HalFile(); } - auto childImpl = makeUniqueNoThrow(std::move(fsFile)); + void* const storage = allocateImplStorage(); + ImplPtr childImpl(storage ? ::new (storage) Impl(std::move(fsFile)) : nullptr); if (!childImpl) { + allocationFailed_ = true; LOG_ERR("SD", "OOM: HalFile directory entry wrapper (%u free, %u max alloc)", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); fsFile.close(); diff --git a/lib/hal/HalStorage.h b/lib/hal/HalStorage.h index 905d58b841..bb3e395f73 100644 --- a/lib/hal/HalStorage.h +++ b/lib/hal/HalStorage.h @@ -15,6 +15,8 @@ class HalStorage { HalStorage(); bool begin(); bool ready() const; + uint64_t totalBytes() const; + uint64_t usedBytes(); std::vector listFiles(const char* path = "/", int maxFiles = 200); // Read the entire file at `path` into a String. Returns empty string on failure. String readFile(const char* path); @@ -63,8 +65,18 @@ class HalStorage { class HalFile : public Print { friend class HalStorage; class Impl; - std::unique_ptr impl; - explicit HalFile(std::unique_ptr impl); + struct ImplDeleter { + void operator()(Impl* impl) const; + }; + using ImplPtr = std::unique_ptr; + + ImplPtr impl; + // Invalid handles otherwise represent both ordinary EOF/open failure and a + // wrapper-allocation failure. Registry scans need to distinguish them. + bool allocationFailed_ = false; + + explicit HalFile(ImplPtr impl); + static void* allocateImplStorage(); public: HalFile(); @@ -95,6 +107,7 @@ class HalFile : public Print { void rewindDirectory(); bool close(); HalFile openNextFile(); + bool allocationFailed() const { return allocationFailed_; } bool isOpen() const; operator bool() const; }; diff --git a/lib/hal/HalSystem.cpp b/lib/hal/HalSystem.cpp index bdba6d2954..00e1345f29 100644 --- a/lib/hal/HalSystem.cpp +++ b/lib/hal/HalSystem.cpp @@ -39,6 +39,11 @@ void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) { __real_panic_print_backtrace(frame, core); return; } + +#if !__riscv + __real_panic_print_backtrace(frame, core); + return; +#else for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) { panicStack[i].sp = 0; } @@ -66,6 +71,7 @@ void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) { } __real_panic_print_backtrace(frame, core); +#endif } } @@ -117,7 +123,7 @@ std::string getPanicInfo(bool full) { std::string info; info += "CrossInk version: " CROSSINK_VERSION; - info += "\nCrossInk variant: " CROSSINK_FIRMWARE_VARIANT; + info += "\nCrossInk device type: " CROSSINK_FIRMWARE_DEVICE_TYPE; info += "\n\nPanic reason: " + std::string(panicMessage); info += "\n\nLast logs:\n" + getLastLogs(); info += "\n\nStack memory:\n"; @@ -144,7 +150,8 @@ std::string getPanicInfo(bool full) { bool isRebootFromPanic() { const auto resetReason = esp_reset_reason(); - return resetReason == ESP_RST_PANIC || resetReason == ESP_RST_CPU_LOCKUP; + return resetReason == ESP_RST_PANIC || resetReason == ESP_RST_CPU_LOCKUP || resetReason == ESP_RST_INT_WDT || + resetReason == ESP_RST_TASK_WDT || resetReason == ESP_RST_WDT; } } // namespace HalSystem diff --git a/lib/hal/HalTiltSensor.cpp b/lib/hal/HalTiltSensor.cpp index a31d4730ae..7744733d4d 100644 --- a/lib/hal/HalTiltSensor.cpp +++ b/lib/hal/HalTiltSensor.cpp @@ -4,50 +4,12 @@ HalTiltSensor halTiltSensor; // Singleton instance -bool HalTiltSensor::writeReg(uint8_t reg, uint8_t val) const { - Wire.beginTransmission(_i2cAddr); - Wire.write(reg); - Wire.write(val); - return Wire.endTransmission() == 0; -} - -bool HalTiltSensor::readReg(uint8_t reg, uint8_t* val) const { - Wire.beginTransmission(_i2cAddr); - Wire.write(reg); - if (Wire.endTransmission(false) != 0) { - return false; - } - Wire.requestFrom(_i2cAddr, (uint8_t)1); - if (Wire.available() < 1) { - return false; - } - *val = Wire.read(); - return true; -} - bool HalTiltSensor::readGyro(float& gx, float& gy, float& gz) const { - Wire.beginTransmission(_i2cAddr); - Wire.write(REG_GX_L); // Start reading at Gyro X Low - if (Wire.endTransmission(false) != 0) { - return false; - } - - Wire.requestFrom(_i2cAddr, (uint8_t)6); - if (Wire.available() < 6) { - return false; - } - - auto readInt16 = [&]() -> int16_t { - const uint8_t lo = Wire.read(); - const uint8_t hi = Wire.read(); - return static_cast((hi << 8) | lo); - }; - - // If Full Scale is ±512 dps, the scale factor is 32768 / 512 = 64 LSB/dps - constexpr float SCALE = 1.0f / 64.0f; - gx = readInt16() * SCALE; - gy = readInt16() * SCALE; - gz = readInt16() * SCALE; + Imu::Sample sample; + if (!_sdkImu.read(sample)) return false; + gx = sample.gx; + gy = sample.gy; + gz = sample.gz; return true; } @@ -61,36 +23,19 @@ void HalTiltSensor::begin() { return; #endif - if (!gpio.deviceIsX3()) { - _available = false; - return; - } - - // Try primary address, then alternate - uint8_t whoami = 0; - _i2cAddr = I2C_ADDR_QMI8658; - if (!readReg(QMI8658_WHO_AM_I_REG, &whoami) || whoami != QMI8658_WHO_AM_I_VALUE) { - _i2cAddr = I2C_ADDR_QMI8658_ALT; - if (!readReg(QMI8658_WHO_AM_I_REG, &whoami) || whoami != QMI8658_WHO_AM_I_VALUE) { - LOG_ERR("GYR", "QMI8658 IMU not found"); - _available = false; - return; + _available = _sdkImu.begin(); + if (_available) { + _initMs = millis(); + _lastPollMs = millis(); + // begin() leaves the sensors sampling; stand them by until tilt page turn + // actually wakes them, so a disabled IMU doesn't drain the battery. + if (!_sdkImu.sleep()) { + LOG_ERR("GYR", "IMU standby failed"); } - } - - LOG_INF("GYR", "QMI8658 IMU found at 0x%02X", _i2cAddr); - - if (!writeReg(REG_CTRL7, CTRL7_DISABLE_ALL) || !writeReg(REG_CTRL3, CTRL3_FS_512DPS | CTRL3_ODR_28HZ) || - !writeReg(REG_CTRL1, CTRL1_BASE | CTRL1_SENSOR_DISABLE)) { - LOG_ERR("GYR", "QMI8658 register configuration failed"); - _available = false; + LOG_INF("GYR", "SDK IMU initialized"); return; } - - _available = true; - _initMs = millis(); - _lastPollMs = millis(); - LOG_INF("GYR", "QMI8658 gyro initialized and put to sleep"); + LOG_ERR("GYR", "SDK IMU not found"); } bool HalTiltSensor::wake() { @@ -98,28 +43,16 @@ bool HalTiltSensor::wake() { return false; } -#ifdef FORCE_TILT_SENSOR_AVAILABLE + if (!_sdkImu.wake()) { + LOG_ERR("GYR", "IMU wake failed"); + return false; + } + _lastPollMs = millis(); _lastTiltMs = millis(); _wakeMs = millis(); + _isAwake = true; return true; -#endif - - // Wait for init to complete before waking - if ((millis() - _initMs) < SLEEP_STABILIZE_MS) { - return false; - } - - if (writeReg(REG_CTRL1, CTRL1_BASE) && writeReg(REG_CTRL7, CTRL7_GYRO_ENABLE)) { - _lastPollMs = millis(); - _lastTiltMs = millis(); - _wakeMs = millis(); - LOG_INF("GYR", "QMI8658 woke up"); - return true; - } else { - LOG_ERR("GYR", "Failed to wake QMI8658"); - return false; - } } bool HalTiltSensor::deepSleep() { @@ -127,26 +60,15 @@ bool HalTiltSensor::deepSleep() { return false; } -#ifdef FORCE_TILT_SENSOR_AVAILABLE - clearPendingEvents(); - _inTilt = false; - return true; -#endif - - if ((millis() - _wakeMs) < SLEEP_STABILIZE_MS) { + if (!_sdkImu.sleep()) { + LOG_ERR("GYR", "IMU sleep failed"); return false; } - if (writeReg(REG_CTRL7, CTRL7_DISABLE_ALL) && writeReg(REG_CTRL1, CTRL1_BASE | CTRL1_SENSOR_DISABLE)) { - // Clear any residual state so it doesn't immediately trigger upon waking - clearPendingEvents(); - _inTilt = false; - LOG_INF("GYR", "QMI8658 entered sleep mode"); - return true; - } else { - LOG_ERR("GYR", "Failed to put QMI8658 to sleep"); - return false; - } + clearPendingEvents(); + _inTilt = false; + _isAwake = false; + return true; } void HalTiltSensor::update(const uint8_t enabled, const uint8_t direction, const uint8_t orientation, diff --git a/lib/hal/HalTiltSensor.h b/lib/hal/HalTiltSensor.h index 09b5311aa9..3508f9fdf6 100644 --- a/lib/hal/HalTiltSensor.h +++ b/lib/hal/HalTiltSensor.h @@ -1,9 +1,7 @@ #pragma once #include -#include - -#include "HalGPIO.h" +#include // TODO: Move enums into new header and share with CrossPointSettings.h namespace CrossPointOrientation { @@ -28,7 +26,7 @@ extern HalTiltSensor halTiltSensor; // Singleton class HalTiltSensor { bool _available = false; - uint8_t _i2cAddr = 0; + mutable Imu _sdkImu; // Tilt gesture state machine bool _tiltForwardEvent = false; // Consumed by wasTiltedForward() @@ -41,52 +39,31 @@ class HalTiltSensor { unsigned long _wakeMs = 0; // Timestamp of last wake() for stabilization // Tuning constants - static constexpr float RATE_THRESHOLD_DPS = 270.0f; // Deg/sec speed to trigger flick +#if defined(FREEINK_DEVICE_STICKY) && FREEINK_DEVICE_STICKY + static constexpr float RATE_THRESHOLD_DPS = 200.0f; // Stay below the Sticky IMU's ±245 dps range +#else + static constexpr float RATE_THRESHOLD_DPS = 270.0f; // Deg/sec speed to trigger flick +#endif static constexpr float NEUTRAL_RATE_DPS = 50.0f; // Must stop moving below this rate before next trigger static constexpr unsigned long COOLDOWN_MS = 600; // Minimum ms between triggers static constexpr unsigned long POLL_INTERVAL_MS = 50; // 20 Hz polling static constexpr unsigned long WAKE_STABILIZE_MS = 300; // Ignore readings after wake - static constexpr unsigned long SLEEP_STABILIZE_MS = 15; // Sleep turn on/off delay mutable unsigned long _lastPollMs = 0; - // --- QMI8658 registers --- - static constexpr uint8_t REG_CTRL1 = 0x02; - static constexpr uint8_t REG_CTRL3 = 0x04; - static constexpr uint8_t REG_CTRL7 = 0x08; - static constexpr uint8_t REG_GX_L = 0x3B; - - // --- Register Bit Flags --- - - // REG_CTRL1 (0x02) - static constexpr uint8_t CTRL1_BIG_ENDIAN = (1 << 5); // 0x20: Default state (1 = Big Endian) - static constexpr uint8_t CTRL1_AUTO_INC = (1 << 6); // 0x40: Enable address auto-increment - static constexpr uint8_t CTRL1_SENSOR_DISABLE = (1 << 0); // 0x01: Power down sensor engine - static constexpr uint8_t CTRL1_BASE = CTRL1_AUTO_INC | CTRL1_BIG_ENDIAN; // 0x60 - - // REG_CTRL3 (0x04) - Gyro Config - static constexpr uint8_t CTRL3_FS_512DPS = (0b101 << 4); // Bits 6:4 = 101 - static constexpr uint8_t CTRL3_ODR_28HZ = 0b1000; // Bits 3:0 = 1000 (28.025 Hz) - - // REG_CTRL7 (0x08) - Enable - static constexpr uint8_t CTRL7_DISABLE_ALL = 0x00; - static constexpr uint8_t CTRL7_GYRO_ENABLE = (1 << 1); // Bit 1 = 1 - - bool writeReg(uint8_t reg, uint8_t val) const; - bool readReg(uint8_t reg, uint8_t* val) const; bool readGyro(float& gx, float& gy, float& gz) const; public: - // Call after gpio.begin() and powerManager.begin() (I2C already initialised for X3) + // Call after BoardConfig has selected the active device. void begin(); - // Enables the QMI8658 internal sensor engine + // Enables tilt polling state bool wake(); - // Puts the QMI8658 into a low-power standby state + // Puts tilt polling state to sleep bool deepSleep(); - // True if the QMI8658 IMU is present on this device + // True if an IMU is present on this device bool isAvailable() const { return _available; } // Poll the accelerometer and update tilt gesture state. diff --git a/lib/miniz/library.json b/lib/miniz/library.json new file mode 100644 index 0000000000..86e8d0b222 --- /dev/null +++ b/lib/miniz/library.json @@ -0,0 +1,9 @@ +{ + "name": "miniz", + "version": "11.3.2", + "description": "Vendored miniz (tinfl inflate only) + InflateStream wrapper", + "build": { + "srcDir": "src", + "includeDir": "src" + } +} diff --git a/lib/miniz/src/InflateStream.cpp b/lib/miniz/src/InflateStream.cpp new file mode 100644 index 0000000000..b1216d7fdd --- /dev/null +++ b/lib/miniz/src/InflateStream.cpp @@ -0,0 +1,170 @@ +#include "InflateStream.h" + +#include + +#include +#include + +#include "MinizConfig.h" + +namespace { +// tinfl's window must be a power of two; TINFL_LZ_DICT_SIZE is 32768. +constexpr size_t WINDOW_SIZE = InflateStream::STREAMING_WINDOW_SIZE; +static_assert(WINDOW_SIZE == TINFL_LZ_DICT_SIZE, "Inflate window size must match miniz"); +// tinfl_decompressor holds mz_uint32 arrays; 8 keeps the window aligned too. +constexpr size_t STATE_ALIGNED = (sizeof(tinfl_decompressor) + 7) & ~size_t{7}; +} // namespace + +InflateStream::~InflateStream() { deinit(); } + +size_t InflateStream::requiredStorageSize(const bool streaming) { + return STATE_ALIGNED + (streaming ? WINDOW_SIZE : 0); +} + +bool InflateStream::init(const bool streaming) { + // Every consumer constructs a fresh stream per operation, so acquire storage + // from scratch each init (releasing any prior backing first). + deinit(); + + // During a framebuffer loan the lent 48KB is up for grabs: state (~11KB) + + // window (32KB) fit inside it, so a chapter-build inflate costs the heap + // nothing. Absent (or already claimed): plain heap, freed in deinit(). + const size_t needed = requiredStorageSize(streaming); + arenaBase = buildscratch::claim(needed); + if (arenaBase) { + state = reinterpret_cast(arenaBase); + window = streaming ? arenaBase + STATE_ALIGNED : nullptr; + } else { + // Raw malloc (not makeUniqueNoThrow): the header keeps tinfl_decompressor + // an incomplete type so consumers never include miniz; both blocks are + // freed in deinit()/the destructor. + state = static_cast(malloc(sizeof(tinfl_decompressor))); + if (!state) return false; + if (streaming) { + window = static_cast(malloc(WINDOW_SIZE)); + if (!window) return false; // state kept; deinit()/next init reclaims it + } + } + + tinfl_init(state); + windowPos = 0; + pendingStart = 0; + pendingLen = 0; + inPtr = nullptr; + inAvail = 0; + fill = nullptr; + fillCtx = nullptr; + inputExhausted = false; + zlibWrapped = false; + finished = false; + oneShotStart = nullptr; + return true; +} + +void InflateStream::deinit() { + if (arenaBase) { + buildscratch::release(arenaBase); + arenaBase = nullptr; + } else { + free(state); + free(window); + } + state = nullptr; + window = nullptr; +} + +void InflateStream::setSource(const uint8_t* src, const size_t len) { + inPtr = src; + inAvail = len; + inputExhausted = true; // the whole input is present; nothing more will come +} + +void InflateStream::setFill(const FillFn fn, void* ctx) { + fill = fn; + fillCtx = ctx; +} + +InflateStream::Status InflateStream::readAtMost(uint8_t* dest, const size_t maxLen, size_t* produced) { + *produced = 0; + if (!state) return Status::Error; + + const bool streaming = window != nullptr; + if (!streaming && !oneShotStart) oneShotStart = dest; + + for (;;) { + // Drain window bytes left over from a previous tinfl call. In ring mode + // tinfl may produce more than the caller asked for in one shot -- the + // overshoot stays pending in the window until a later readAtMost. + if (pendingLen > 0) { + size_t n = maxLen - *produced; + if (n > pendingLen) n = pendingLen; + memcpy(dest + *produced, window + pendingStart, n); + pendingStart += n; + pendingLen -= n; + *produced += n; + } + if (*produced == maxLen) { + return (finished && pendingLen == 0) ? Status::Done : Status::Ok; + } + if (finished) return Status::Done; + + if (inAvail == 0 && !inputExhausted && fill) { + inAvail = fill(fillCtx, &inPtr); + if (inAvail == 0) inputExhausted = true; + } + + const mz_uint32 flags = (zlibWrapped ? TINFL_FLAG_PARSE_ZLIB_HEADER : 0) | + (inputExhausted ? 0 : TINFL_FLAG_HAS_MORE_INPUT) | + (streaming ? 0 : TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF); + + size_t inBytes = inAvail; + tinfl_status status; + size_t outBytes; + if (streaming) { + // Ring mode invariant: tinfl derives its wrap mask from + // (cursor offset + avail_out), so avail_out MUST always reach the end of + // the 32KB window -- never cap it to the caller's remaining space. + outBytes = WINDOW_SIZE - windowPos; + status = tinfl_decompress(state, inPtr, &inBytes, window, window + windowPos, &outBytes, flags); + pendingStart = windowPos; + pendingLen = outBytes; + windowPos += outBytes; + if (windowPos == WINDOW_SIZE) windowPos = 0; + } else { + // One-shot: back-references resolve directly inside the destination buffer. + outBytes = maxLen - *produced; + status = tinfl_decompress(state, inPtr, &inBytes, oneShotStart, dest + *produced, &outBytes, flags); + *produced += outBytes; + } + inPtr += inBytes; + inAvail -= inBytes; + + if (status == TINFL_STATUS_DONE) { + finished = true; // drain any pending window bytes on the next pass + continue; + } + if (status < TINFL_STATUS_DONE) return Status::Error; // corrupt stream / adler mismatch + // TINFL_STATUS_NEEDS_MORE_INPUT loops back to the fill above; once the fill + // runs dry the HAS_MORE_INPUT flag drops and tinfl either finishes or fails + // (truncated stream) instead of spinning. + if (status == TINFL_STATUS_NEEDS_MORE_INPUT && inputExhausted && inAvail == 0) { + return Status::Error; + } + if (*produced == maxLen) { + return (finished && pendingLen == 0) ? Status::Done : Status::Ok; + } + } +} + +bool InflateStream::read(uint8_t* dest, const size_t len) { + size_t total = 0; + while (total < len) { + size_t produced = 0; + const Status status = readAtMost(dest + total, len - total, &produced); + total += produced; + if (status == Status::Error) return false; + if (status == Status::Done) return total == len; + if (produced == 0) return false; // no progress safeguard + } + return true; +} diff --git a/lib/miniz/src/InflateStream.h b/lib/miniz/src/InflateStream.h new file mode 100644 index 0000000000..6d4cb467d9 --- /dev/null +++ b/lib/miniz/src/InflateStream.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include + +// Forward declaration keeps miniz out of consumer translation units; the +// decompressor state is heap-allocated in the .cpp where the type is complete. +struct tinfl_decompressor_tag; + +// Streaming deflate decompressor wrapping miniz's tinfl. +// +// Replaces the uzlib-backed InflateReader on the throughput paths (EPUB zip +// entries, PNG IDAT). tinfl decodes via lookup tables where uzlib walks the +// Huffman tree bit-by-bit -- several times faster on this CPU -- at the cost +// of a larger decompressor state (~11KB, transient for the scope of the +// stream; taken from the lent framebuffer bytes via buildscratch::claim() +// when a FrameBufferLoan is active, heap otherwise). FontDecompressor +// intentionally stays on InflateReader: +// its one-shot flash-resident group decompressions are tiny, and the render +// path should not carry the extra state allocation. +// +// Two modes: +// init(false) -- one-shot: the destination buffer holds the ENTIRE output, +// so back-references resolve inside it and no 32KB window is +// allocated. read()/readAtMost() must be driven with +// contiguous, forward-only slices of that one buffer +// (a single read(dest, totalSize) is the common case). +// init(true) -- streaming: allocates a 32KB window; output can go to any +// buffer in any-sized chunks across calls. +// +// Input is either a single contiguous buffer (setSource) or pulled on demand +// through a fill callback (setFill): return the number of bytes available and +// point *data at them (valid until the next fill call); return 0 at end of +// input. Call setZlibWrapped() before the first read when the stream has a +// zlib header (e.g. PNG IDAT). +class InflateStream { + public: + static constexpr size_t STREAMING_WINDOW_SIZE = 32768; + + enum class Status { + Ok, // Output buffer full; more decompressed data remains. + Done, // Stream ended cleanly. produced may be < maxLen. + Error, // Corrupt/truncated stream, or decompression failed. + }; + + using FillFn = size_t (*)(void* ctx, const uint8_t** data); + + InflateStream() = default; + ~InflateStream(); + InflateStream(const InflateStream&) = delete; + InflateStream& operator=(const InflateStream&) = delete; + + // Allocate decompressor state (and the 32KB window when streaming) and reset + // stream state, releasing any previous backing first. Returns false on OOM. + bool init(bool streaming); + + // Transient storage needed when the framebuffer scratch loan is unavailable. + static size_t requiredStorageSize(bool streaming); + + // Free the decompressor state and window. + void deinit(); + + // Provide the entire compressed input as one contiguous buffer. + void setSource(const uint8_t* src, size_t len); + + // Provide compressed input on demand. ctx is passed back to fn verbatim. + void setFill(FillFn fn, void* ctx); + + // Declare the input zlib-wrapped (2-byte header + trailing adler32). + void setZlibWrapped() { zlibWrapped = true; } + + // Decompress exactly len bytes into dest. Returns false if the stream ends + // or errors before producing len bytes. + bool read(uint8_t* dest, size_t len); + + // Decompress up to maxLen bytes into dest; *produced gets the byte count. + Status readAtMost(uint8_t* dest, size_t maxLen, size_t* produced); + + private: + tinfl_decompressor_tag* state = nullptr; // ~11KB: heap, or inside the claimed build scratch + uint8_t* window = nullptr; // 32KB ring, streaming mode only + uint8_t* arenaBase = nullptr; // non-null when state/window live in lent framebuffer bytes + size_t windowPos = 0; // ring write cursor + // Decompressed-but-undelivered region of the window (tinfl can overshoot the + // caller's requested length; the overshoot waits here for the next read). + size_t pendingStart = 0; + size_t pendingLen = 0; + + const uint8_t* inPtr = nullptr; + size_t inAvail = 0; + FillFn fill = nullptr; + void* fillCtx = nullptr; + bool inputExhausted = false; + bool zlibWrapped = false; + bool finished = false; + + // One-shot mode: tinfl needs the output buffer start for back-references. + uint8_t* oneShotStart = nullptr; +}; diff --git a/lib/miniz/src/MinizConfig.h b/lib/miniz/src/MinizConfig.h new file mode 100644 index 0000000000..28767fbf69 --- /dev/null +++ b/lib/miniz/src/MinizConfig.h @@ -0,0 +1,35 @@ +/* CrossPoint only needs miniz's low-level streaming inflate (tinfl). The + * archive, deflate, stdio, and zlib-compatibility layers are compiled out so + * the vendored library stays small and never touches the filesystem or clock. + * Include this header instead of so every translation unit sees the + * same configuration. */ +#pragma once + +#define MINIZ_NO_STDIO +#define MINIZ_NO_TIME +#define MINIZ_NO_ARCHIVE_APIS +#define MINIZ_NO_ARCHIVE_WRITING_APIS +#define MINIZ_NO_DEFLATE_APIS +#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES + +// The ESP32 mask ROM exports tinfl_* at fixed addresses via DIRECT linker +// script assignments (e.g. "tinfl_decompress = 0x...;" in the ROM .ld), +// which override object-file definitions -- without these renames the +// firmware silently binds to the ROM's 2021 build (TINFL_LESS_MEMORY, a +// different tinfl_decompressor layout) and corrupts inflate state on real +// data. Rename so the linker can never capture them. The prefix is +// crosspoint_ (NOT freeink_) so a future branch that links FreeInkBook's +// identically-renamed copy does not collide. +#define tinfl_decompress crosspoint_tinfl_decompress +#define tinfl_decompress_mem_to_heap crosspoint_tinfl_decompress_mem_to_heap +#define tinfl_decompress_mem_to_mem crosspoint_tinfl_decompress_mem_to_mem +#define tinfl_decompress_mem_to_callback crosspoint_tinfl_decompress_mem_to_callback +#define mz_crc32 crosspoint_mz_crc32 +#define mz_adler32 crosspoint_mz_adler32 +#define mz_free crosspoint_mz_free + +// Include the vendored miniz by relative path: ESP-IDF ships a ROM miniz.h +// with the SAME include guard but a different (TINFL_LESS_MEMORY) struct +// layout -- resolving through the platform include path would +// silently compile against the wrong structures. +#include "../third_party/miniz.h" diff --git a/lib/miniz/src/miniz_impl.c b/lib/miniz/src/miniz_impl.c new file mode 100644 index 0000000000..4923e30fa1 --- /dev/null +++ b/lib/miniz/src/miniz_impl.c @@ -0,0 +1,7 @@ +/* Compiles the vendored miniz with CrossPoint's configuration. The include + * order is load-bearing (the config defines/renames must be seen first). */ +// clang-format off +#include "MinizConfig.h" + +#include "../third_party/miniz.c" +// clang-format on diff --git a/lib/miniz/third_party/miniz.c b/lib/miniz/third_party/miniz.c new file mode 100644 index 0000000000..54fc5f39ef --- /dev/null +++ b/lib/miniz/third_party/miniz.c @@ -0,0 +1,7922 @@ +#include "miniz.h" +/************************************************************************** + * + * Copyright 2013-2014 RAD Game Tools and Valve Software + * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + **************************************************************************/ + + + +typedef unsigned char mz_validate_uint16[sizeof(mz_uint16) == 2 ? 1 : -1]; +typedef unsigned char mz_validate_uint32[sizeof(mz_uint32) == 4 ? 1 : -1]; +typedef unsigned char mz_validate_uint64[sizeof(mz_uint64) == 8 ? 1 : -1]; + +#ifdef __cplusplus +extern "C" +{ +#endif + + /* ------------------- zlib-style API's */ + + mz_ulong mz_adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len) + { + mz_uint32 i, s1 = (mz_uint32)(adler & 0xffff), s2 = (mz_uint32)(adler >> 16); + size_t block_len = buf_len % 5552; + if (!ptr) + return MZ_ADLER32_INIT; + while (buf_len) + { + for (i = 0; i + 7 < block_len; i += 8, ptr += 8) + { + s1 += ptr[0], s2 += s1; + s1 += ptr[1], s2 += s1; + s1 += ptr[2], s2 += s1; + s1 += ptr[3], s2 += s1; + s1 += ptr[4], s2 += s1; + s1 += ptr[5], s2 += s1; + s1 += ptr[6], s2 += s1; + s1 += ptr[7], s2 += s1; + } + for (; i < block_len; ++i) + s1 += *ptr++, s2 += s1; + s1 %= 65521U, s2 %= 65521U; + buf_len -= block_len; + block_len = 5552; + } + return (s2 << 16) + s1; + } + +/* Karl Malbrain's compact CRC-32. See "A compact CCITT crc16 and crc32 C implementation that balances processor cache usage against speed": http://www.geocities.com/malbrain/ */ +#if 0 + mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len) + { + static const mz_uint32 s_crc32[16] = { 0, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, + 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c }; + mz_uint32 crcu32 = (mz_uint32)crc; + if (!ptr) + return MZ_CRC32_INIT; + crcu32 = ~crcu32; + while (buf_len--) + { + mz_uint8 b = *ptr++; + crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b & 0xF)]; + crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b >> 4)]; + } + return ~crcu32; + } +#elif defined(USE_EXTERNAL_MZCRC) +/* If USE_EXTERNAL_CRC is defined, an external module will export the + * mz_crc32() symbol for us to use, e.g. an SSE-accelerated version. + * Depending on the impl, it may be necessary to ~ the input/output crc values. + */ +mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len); +#else +/* Faster, but larger CPU cache footprint. + */ +mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len) +{ + static const mz_uint32 s_crc_table[256] = { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, + 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, + 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, + 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, + 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, + 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, + 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, + 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, + 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, + 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, + 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, + 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, + 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, + 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, + 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, + 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, + 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, + 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, + 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, + 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, + 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, + 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, + 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, + 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, + 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, + 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, + 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, + 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, + 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, + 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + mz_uint32 crc32 = (mz_uint32)crc ^ 0xFFFFFFFF; + const mz_uint8 *pByte_buf = (const mz_uint8 *)ptr; + + while (buf_len >= 4) + { + crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[0]) & 0xFF]; + crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[1]) & 0xFF]; + crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[2]) & 0xFF]; + crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[3]) & 0xFF]; + pByte_buf += 4; + buf_len -= 4; + } + + while (buf_len) + { + crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[0]) & 0xFF]; + ++pByte_buf; + --buf_len; + } + + return ~crc32; +} +#endif + + void mz_free(void *p) + { + MZ_FREE(p); + } + + MINIZ_EXPORT void *miniz_def_alloc_func(void *opaque, size_t items, size_t size) + { + (void)opaque, (void)items, (void)size; + return MZ_MALLOC(items * size); + } + MINIZ_EXPORT void miniz_def_free_func(void *opaque, void *address) + { + (void)opaque, (void)address; + MZ_FREE(address); + } + MINIZ_EXPORT void *miniz_def_realloc_func(void *opaque, void *address, size_t items, size_t size) + { + (void)opaque, (void)address, (void)items, (void)size; + return MZ_REALLOC(address, items * size); + } + + const char *mz_version(void) + { + return MZ_VERSION; + } + +#ifndef MINIZ_NO_ZLIB_APIS + +#ifndef MINIZ_NO_DEFLATE_APIS + + int mz_deflateInit(mz_streamp pStream, int level) + { + return mz_deflateInit2(pStream, level, MZ_DEFLATED, MZ_DEFAULT_WINDOW_BITS, 9, MZ_DEFAULT_STRATEGY); + } + + int mz_deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy) + { + tdefl_compressor *pComp; + mz_uint comp_flags = TDEFL_COMPUTE_ADLER32 | tdefl_create_comp_flags_from_zip_params(level, window_bits, strategy); + + if (!pStream) + return MZ_STREAM_ERROR; + if ((method != MZ_DEFLATED) || ((mem_level < 1) || (mem_level > 9)) || ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS))) + return MZ_PARAM_ERROR; + + pStream->data_type = 0; + pStream->adler = MZ_ADLER32_INIT; + pStream->msg = NULL; + pStream->reserved = 0; + pStream->total_in = 0; + pStream->total_out = 0; + if (!pStream->zalloc) + pStream->zalloc = miniz_def_alloc_func; + if (!pStream->zfree) + pStream->zfree = miniz_def_free_func; + + pComp = (tdefl_compressor *)pStream->zalloc(pStream->opaque, 1, sizeof(tdefl_compressor)); + if (!pComp) + return MZ_MEM_ERROR; + + pStream->state = (struct mz_internal_state *)pComp; + + if (tdefl_init(pComp, NULL, NULL, comp_flags) != TDEFL_STATUS_OKAY) + { + mz_deflateEnd(pStream); + return MZ_PARAM_ERROR; + } + + return MZ_OK; + } + + int mz_deflateReset(mz_streamp pStream) + { + if ((!pStream) || (!pStream->state) || (!pStream->zalloc) || (!pStream->zfree)) + return MZ_STREAM_ERROR; + pStream->total_in = pStream->total_out = 0; + tdefl_init((tdefl_compressor *)pStream->state, NULL, NULL, ((tdefl_compressor *)pStream->state)->m_flags); + return MZ_OK; + } + + int mz_deflate(mz_streamp pStream, int flush) + { + size_t in_bytes, out_bytes; + mz_ulong orig_total_in, orig_total_out; + int mz_status = MZ_OK; + + if ((!pStream) || (!pStream->state) || (flush < 0) || (flush > MZ_FINISH) || (!pStream->next_out)) + return MZ_STREAM_ERROR; + if (!pStream->avail_out) + return MZ_BUF_ERROR; + + if (flush == MZ_PARTIAL_FLUSH) + flush = MZ_SYNC_FLUSH; + + if (((tdefl_compressor *)pStream->state)->m_prev_return_status == TDEFL_STATUS_DONE) + return (flush == MZ_FINISH) ? MZ_STREAM_END : MZ_BUF_ERROR; + + orig_total_in = pStream->total_in; + orig_total_out = pStream->total_out; + for (;;) + { + tdefl_status defl_status; + in_bytes = pStream->avail_in; + out_bytes = pStream->avail_out; + + defl_status = tdefl_compress((tdefl_compressor *)pStream->state, pStream->next_in, &in_bytes, pStream->next_out, &out_bytes, (tdefl_flush)flush); + pStream->next_in += (mz_uint)in_bytes; + pStream->avail_in -= (mz_uint)in_bytes; + pStream->total_in += (mz_uint)in_bytes; + pStream->adler = tdefl_get_adler32((tdefl_compressor *)pStream->state); + + pStream->next_out += (mz_uint)out_bytes; + pStream->avail_out -= (mz_uint)out_bytes; + pStream->total_out += (mz_uint)out_bytes; + + if (defl_status < 0) + { + mz_status = MZ_STREAM_ERROR; + break; + } + else if (defl_status == TDEFL_STATUS_DONE) + { + mz_status = MZ_STREAM_END; + break; + } + else if (!pStream->avail_out) + break; + else if ((!pStream->avail_in) && (flush != MZ_FINISH)) + { + if ((flush) || (pStream->total_in != orig_total_in) || (pStream->total_out != orig_total_out)) + break; + return MZ_BUF_ERROR; /* Can't make forward progress without some input. + */ + } + } + return mz_status; + } + + int mz_deflateEnd(mz_streamp pStream) + { + if (!pStream) + return MZ_STREAM_ERROR; + if (pStream->state) + { + pStream->zfree(pStream->opaque, pStream->state); + pStream->state = NULL; + } + return MZ_OK; + } + + mz_ulong mz_deflateBound(mz_streamp pStream, mz_ulong source_len) + { + (void)pStream; + /* This is really over conservative. (And lame, but it's actually pretty tricky to compute a true upper bound given the way tdefl's blocking works.) */ + return MZ_MAX(128 + (source_len * 110) / 100, 128 + source_len + ((source_len / (31 * 1024)) + 1) * 5); + } + + int mz_compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level) + { + int status; + mz_stream stream; + memset(&stream, 0, sizeof(stream)); + + /* In case mz_ulong is 64-bits (argh I hate longs). */ + if ((mz_uint64)(source_len | *pDest_len) > 0xFFFFFFFFU) + return MZ_PARAM_ERROR; + + stream.next_in = pSource; + stream.avail_in = (mz_uint32)source_len; + stream.next_out = pDest; + stream.avail_out = (mz_uint32)*pDest_len; + + status = mz_deflateInit(&stream, level); + if (status != MZ_OK) + return status; + + status = mz_deflate(&stream, MZ_FINISH); + if (status != MZ_STREAM_END) + { + mz_deflateEnd(&stream); + return (status == MZ_OK) ? MZ_BUF_ERROR : status; + } + + *pDest_len = stream.total_out; + return mz_deflateEnd(&stream); + } + + int mz_compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len) + { + return mz_compress2(pDest, pDest_len, pSource, source_len, MZ_DEFAULT_COMPRESSION); + } + + mz_ulong mz_compressBound(mz_ulong source_len) + { + return mz_deflateBound(NULL, source_len); + } + +#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ + +#ifndef MINIZ_NO_INFLATE_APIS + + typedef struct + { + tinfl_decompressor m_decomp; + mz_uint m_dict_ofs, m_dict_avail, m_first_call, m_has_flushed; + int m_window_bits; + mz_uint8 m_dict[TINFL_LZ_DICT_SIZE]; + tinfl_status m_last_status; + } inflate_state; + + int mz_inflateInit2(mz_streamp pStream, int window_bits) + { + inflate_state *pDecomp; + if (!pStream) + return MZ_STREAM_ERROR; + if ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS)) + return MZ_PARAM_ERROR; + + pStream->data_type = 0; + pStream->adler = 0; + pStream->msg = NULL; + pStream->total_in = 0; + pStream->total_out = 0; + pStream->reserved = 0; + if (!pStream->zalloc) + pStream->zalloc = miniz_def_alloc_func; + if (!pStream->zfree) + pStream->zfree = miniz_def_free_func; + + pDecomp = (inflate_state *)pStream->zalloc(pStream->opaque, 1, sizeof(inflate_state)); + if (!pDecomp) + return MZ_MEM_ERROR; + + pStream->state = (struct mz_internal_state *)pDecomp; + + tinfl_init(&pDecomp->m_decomp); + pDecomp->m_dict_ofs = 0; + pDecomp->m_dict_avail = 0; + pDecomp->m_last_status = TINFL_STATUS_NEEDS_MORE_INPUT; + pDecomp->m_first_call = 1; + pDecomp->m_has_flushed = 0; + pDecomp->m_window_bits = window_bits; + + return MZ_OK; + } + + int mz_inflateInit(mz_streamp pStream) + { + return mz_inflateInit2(pStream, MZ_DEFAULT_WINDOW_BITS); + } + + int mz_inflateReset(mz_streamp pStream) + { + inflate_state *pDecomp; + if (!pStream) + return MZ_STREAM_ERROR; + + pStream->data_type = 0; + pStream->adler = 0; + pStream->msg = NULL; + pStream->total_in = 0; + pStream->total_out = 0; + pStream->reserved = 0; + + pDecomp = (inflate_state *)pStream->state; + + tinfl_init(&pDecomp->m_decomp); + pDecomp->m_dict_ofs = 0; + pDecomp->m_dict_avail = 0; + pDecomp->m_last_status = TINFL_STATUS_NEEDS_MORE_INPUT; + pDecomp->m_first_call = 1; + pDecomp->m_has_flushed = 0; + /* pDecomp->m_window_bits = window_bits */; + + return MZ_OK; + } + + int mz_inflate(mz_streamp pStream, int flush) + { + inflate_state *pState; + mz_uint n, first_call, decomp_flags = TINFL_FLAG_COMPUTE_ADLER32; + size_t in_bytes, out_bytes, orig_avail_in; + tinfl_status status; + + if ((!pStream) || (!pStream->state)) + return MZ_STREAM_ERROR; + if (flush == MZ_PARTIAL_FLUSH) + flush = MZ_SYNC_FLUSH; + if ((flush) && (flush != MZ_SYNC_FLUSH) && (flush != MZ_FINISH)) + return MZ_STREAM_ERROR; + + pState = (inflate_state *)pStream->state; + if (pState->m_window_bits > 0) + decomp_flags |= TINFL_FLAG_PARSE_ZLIB_HEADER; + orig_avail_in = pStream->avail_in; + + first_call = pState->m_first_call; + pState->m_first_call = 0; + if (pState->m_last_status < 0) + return MZ_DATA_ERROR; + + if (pState->m_has_flushed && (flush != MZ_FINISH)) + return MZ_STREAM_ERROR; + pState->m_has_flushed |= (flush == MZ_FINISH); + + if ((flush == MZ_FINISH) && (first_call)) + { + /* MZ_FINISH on the first call implies that the input and output buffers are large enough to hold the entire compressed/decompressed file. */ + decomp_flags |= TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF; + in_bytes = pStream->avail_in; + out_bytes = pStream->avail_out; + status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pStream->next_out, pStream->next_out, &out_bytes, decomp_flags); + pState->m_last_status = status; + pStream->next_in += (mz_uint)in_bytes; + pStream->avail_in -= (mz_uint)in_bytes; + pStream->total_in += (mz_uint)in_bytes; + pStream->adler = tinfl_get_adler32(&pState->m_decomp); + pStream->next_out += (mz_uint)out_bytes; + pStream->avail_out -= (mz_uint)out_bytes; + pStream->total_out += (mz_uint)out_bytes; + + if (status < 0) + return MZ_DATA_ERROR; + else if (status != TINFL_STATUS_DONE) + { + pState->m_last_status = TINFL_STATUS_FAILED; + return MZ_BUF_ERROR; + } + return MZ_STREAM_END; + } + /* flush != MZ_FINISH then we must assume there's more input. */ + if (flush != MZ_FINISH) + decomp_flags |= TINFL_FLAG_HAS_MORE_INPUT; + + if (pState->m_dict_avail) + { + n = MZ_MIN(pState->m_dict_avail, pStream->avail_out); + memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n); + pStream->next_out += n; + pStream->avail_out -= n; + pStream->total_out += n; + pState->m_dict_avail -= n; + pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1); + return ((pState->m_last_status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK; + } + + for (;;) + { + in_bytes = pStream->avail_in; + out_bytes = TINFL_LZ_DICT_SIZE - pState->m_dict_ofs; + + status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pState->m_dict, pState->m_dict + pState->m_dict_ofs, &out_bytes, decomp_flags); + pState->m_last_status = status; + + pStream->next_in += (mz_uint)in_bytes; + pStream->avail_in -= (mz_uint)in_bytes; + pStream->total_in += (mz_uint)in_bytes; + pStream->adler = tinfl_get_adler32(&pState->m_decomp); + + pState->m_dict_avail = (mz_uint)out_bytes; + + n = MZ_MIN(pState->m_dict_avail, pStream->avail_out); + memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n); + pStream->next_out += n; + pStream->avail_out -= n; + pStream->total_out += n; + pState->m_dict_avail -= n; + pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1); + + if (status < 0) + return MZ_DATA_ERROR; /* Stream is corrupted (there could be some uncompressed data left in the output dictionary - oh well). */ + else if ((status == TINFL_STATUS_NEEDS_MORE_INPUT) && (!orig_avail_in)) + return MZ_BUF_ERROR; /* Signal caller that we can't make forward progress without supplying more input or by setting flush to MZ_FINISH. */ + else if (flush == MZ_FINISH) + { + /* The output buffer MUST be large to hold the remaining uncompressed data when flush==MZ_FINISH. */ + if (status == TINFL_STATUS_DONE) + return pState->m_dict_avail ? MZ_BUF_ERROR : MZ_STREAM_END; + /* status here must be TINFL_STATUS_HAS_MORE_OUTPUT, which means there's at least 1 more byte on the way. If there's no more room left in the output buffer then something is wrong. */ + else if (!pStream->avail_out) + return MZ_BUF_ERROR; + } + else if ((status == TINFL_STATUS_DONE) || (!pStream->avail_in) || (!pStream->avail_out) || (pState->m_dict_avail)) + break; + } + + return ((status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK; + } + + int mz_inflateEnd(mz_streamp pStream) + { + if (!pStream) + return MZ_STREAM_ERROR; + if (pStream->state) + { + pStream->zfree(pStream->opaque, pStream->state); + pStream->state = NULL; + } + return MZ_OK; + } + int mz_uncompress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len) + { + mz_stream stream; + int status; + memset(&stream, 0, sizeof(stream)); + + /* In case mz_ulong is 64-bits (argh I hate longs). */ + if ((mz_uint64)(*pSource_len | *pDest_len) > 0xFFFFFFFFU) + return MZ_PARAM_ERROR; + + stream.next_in = pSource; + stream.avail_in = (mz_uint32)*pSource_len; + stream.next_out = pDest; + stream.avail_out = (mz_uint32)*pDest_len; + + status = mz_inflateInit(&stream); + if (status != MZ_OK) + return status; + + status = mz_inflate(&stream, MZ_FINISH); + *pSource_len = *pSource_len - stream.avail_in; + if (status != MZ_STREAM_END) + { + mz_inflateEnd(&stream); + return ((status == MZ_BUF_ERROR) && (!stream.avail_in)) ? MZ_DATA_ERROR : status; + } + *pDest_len = stream.total_out; + + return mz_inflateEnd(&stream); + } + + int mz_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len) + { + return mz_uncompress2(pDest, pDest_len, pSource, &source_len); + } + +#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ + + const char *mz_error(int err) + { + static struct + { + int m_err; + const char *m_pDesc; + } s_error_descs[] = { + { MZ_OK, "" }, { MZ_STREAM_END, "stream end" }, { MZ_NEED_DICT, "need dictionary" }, { MZ_ERRNO, "file error" }, { MZ_STREAM_ERROR, "stream error" }, { MZ_DATA_ERROR, "data error" }, { MZ_MEM_ERROR, "out of memory" }, { MZ_BUF_ERROR, "buf error" }, { MZ_VERSION_ERROR, "version error" }, { MZ_PARAM_ERROR, "parameter error" } + }; + mz_uint i; + for (i = 0; i < sizeof(s_error_descs) / sizeof(s_error_descs[0]); ++i) + if (s_error_descs[i].m_err == err) + return s_error_descs[i].m_pDesc; + return NULL; + } + +#endif /*MINIZ_NO_ZLIB_APIS */ + +#ifdef __cplusplus +} +#endif + +/* + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + For more information, please refer to +*/ +/************************************************************************** + * + * Copyright 2013-2014 RAD Game Tools and Valve Software + * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + **************************************************************************/ + + + +#ifndef MINIZ_NO_DEFLATE_APIS + +#ifdef __cplusplus +extern "C" +{ +#endif + + /* ------------------- Low-level Compression (independent from all decompression API's) */ + + /* Purposely making these tables static for faster init and thread safety. */ + static const mz_uint16 s_tdefl_len_sym[256] = { + 257, 258, 259, 260, 261, 262, 263, 264, 265, 265, 266, 266, 267, 267, 268, 268, 269, 269, 269, 269, 270, 270, 270, 270, 271, 271, 271, 271, 272, 272, 272, 272, + 273, 273, 273, 273, 273, 273, 273, 273, 274, 274, 274, 274, 274, 274, 274, 274, 275, 275, 275, 275, 275, 275, 275, 275, 276, 276, 276, 276, 276, 276, 276, 276, + 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, + 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, + 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, + 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, + 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, + 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 285 + }; + + static const mz_uint8 s_tdefl_len_extra[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0 + }; + + static const mz_uint8 s_tdefl_small_dist_sym[512] = { + 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, + 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17 + }; + + static const mz_uint8 s_tdefl_small_dist_extra[512] = { + 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7 + }; + + static const mz_uint8 s_tdefl_large_dist_sym[128] = { + 0, 0, 18, 19, 20, 20, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29 + }; + + static const mz_uint8 s_tdefl_large_dist_extra[128] = { + 0, 0, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13 + }; + + /* Radix sorts tdefl_sym_freq[] array by 16-bit key m_key. Returns ptr to sorted values. */ + typedef struct + { + mz_uint16 m_key, m_sym_index; + } tdefl_sym_freq; + static tdefl_sym_freq *tdefl_radix_sort_syms(mz_uint num_syms, tdefl_sym_freq *pSyms0, tdefl_sym_freq *pSyms1) + { + mz_uint32 total_passes = 2, pass_shift, pass, i, hist[256 * 2]; + tdefl_sym_freq *pCur_syms = pSyms0, *pNew_syms = pSyms1; + MZ_CLEAR_ARR(hist); + for (i = 0; i < num_syms; i++) + { + mz_uint freq = pSyms0[i].m_key; + hist[freq & 0xFF]++; + hist[256 + ((freq >> 8) & 0xFF)]++; + } + while ((total_passes > 1) && (num_syms == hist[(total_passes - 1) * 256])) + total_passes--; + for (pass_shift = 0, pass = 0; pass < total_passes; pass++, pass_shift += 8) + { + const mz_uint32 *pHist = &hist[pass << 8]; + mz_uint offsets[256], cur_ofs = 0; + for (i = 0; i < 256; i++) + { + offsets[i] = cur_ofs; + cur_ofs += pHist[i]; + } + for (i = 0; i < num_syms; i++) + pNew_syms[offsets[(pCur_syms[i].m_key >> pass_shift) & 0xFF]++] = pCur_syms[i]; + { + tdefl_sym_freq *t = pCur_syms; + pCur_syms = pNew_syms; + pNew_syms = t; + } + } + return pCur_syms; + } + + /* tdefl_calculate_minimum_redundancy() originally written by: Alistair Moffat, alistair@cs.mu.oz.au, Jyrki Katajainen, jyrki@diku.dk, November 1996. */ + static void tdefl_calculate_minimum_redundancy(tdefl_sym_freq *A, int n) + { + int root, leaf, next, avbl, used, dpth; + if (n == 0) + return; + else if (n == 1) + { + A[0].m_key = 1; + return; + } + A[0].m_key += A[1].m_key; + root = 0; + leaf = 2; + for (next = 1; next < n - 1; next++) + { + if (leaf >= n || A[root].m_key < A[leaf].m_key) + { + A[next].m_key = A[root].m_key; + A[root++].m_key = (mz_uint16)next; + } + else + A[next].m_key = A[leaf++].m_key; + if (leaf >= n || (root < next && A[root].m_key < A[leaf].m_key)) + { + A[next].m_key = (mz_uint16)(A[next].m_key + A[root].m_key); + A[root++].m_key = (mz_uint16)next; + } + else + A[next].m_key = (mz_uint16)(A[next].m_key + A[leaf++].m_key); + } + A[n - 2].m_key = 0; + for (next = n - 3; next >= 0; next--) + A[next].m_key = A[A[next].m_key].m_key + 1; + avbl = 1; + used = dpth = 0; + root = n - 2; + next = n - 1; + while (avbl > 0) + { + while (root >= 0 && (int)A[root].m_key == dpth) + { + used++; + root--; + } + while (avbl > used) + { + A[next--].m_key = (mz_uint16)(dpth); + avbl--; + } + avbl = 2 * used; + dpth++; + used = 0; + } + } + + /* Limits canonical Huffman code table's max code size. */ + enum + { + TDEFL_MAX_SUPPORTED_HUFF_CODESIZE = 32 + }; + static void tdefl_huffman_enforce_max_code_size(int *pNum_codes, int code_list_len, int max_code_size) + { + int i; + mz_uint32 total = 0; + if (code_list_len <= 1) + return; + for (i = max_code_size + 1; i <= TDEFL_MAX_SUPPORTED_HUFF_CODESIZE; i++) + pNum_codes[max_code_size] += pNum_codes[i]; + for (i = max_code_size; i > 0; i--) + total += (((mz_uint32)pNum_codes[i]) << (max_code_size - i)); + while (total != (1UL << max_code_size)) + { + pNum_codes[max_code_size]--; + for (i = max_code_size - 1; i > 0; i--) + if (pNum_codes[i]) + { + pNum_codes[i]--; + pNum_codes[i + 1] += 2; + break; + } + total--; + } + } + + static void tdefl_optimize_huffman_table(tdefl_compressor *d, int table_num, int table_len, int code_size_limit, int static_table) + { + int i, j, l, num_codes[1 + TDEFL_MAX_SUPPORTED_HUFF_CODESIZE]; + mz_uint next_code[TDEFL_MAX_SUPPORTED_HUFF_CODESIZE + 1]; + MZ_CLEAR_ARR(num_codes); + if (static_table) + { + for (i = 0; i < table_len; i++) + num_codes[d->m_huff_code_sizes[table_num][i]]++; + } + else + { + tdefl_sym_freq syms0[TDEFL_MAX_HUFF_SYMBOLS], syms1[TDEFL_MAX_HUFF_SYMBOLS], *pSyms; + int num_used_syms = 0; + const mz_uint16 *pSym_count = &d->m_huff_count[table_num][0]; + for (i = 0; i < table_len; i++) + if (pSym_count[i]) + { + syms0[num_used_syms].m_key = (mz_uint16)pSym_count[i]; + syms0[num_used_syms++].m_sym_index = (mz_uint16)i; + } + + pSyms = tdefl_radix_sort_syms(num_used_syms, syms0, syms1); + tdefl_calculate_minimum_redundancy(pSyms, num_used_syms); + + for (i = 0; i < num_used_syms; i++) + num_codes[pSyms[i].m_key]++; + + tdefl_huffman_enforce_max_code_size(num_codes, num_used_syms, code_size_limit); + + MZ_CLEAR_ARR(d->m_huff_code_sizes[table_num]); + MZ_CLEAR_ARR(d->m_huff_codes[table_num]); + for (i = 1, j = num_used_syms; i <= code_size_limit; i++) + for (l = num_codes[i]; l > 0; l--) + d->m_huff_code_sizes[table_num][pSyms[--j].m_sym_index] = (mz_uint8)(i); + } + + next_code[1] = 0; + for (j = 0, i = 2; i <= code_size_limit; i++) + next_code[i] = j = ((j + num_codes[i - 1]) << 1); + + for (i = 0; i < table_len; i++) + { + mz_uint rev_code = 0, code, code_size; + if ((code_size = d->m_huff_code_sizes[table_num][i]) == 0) + continue; + code = next_code[code_size]++; + for (l = code_size; l > 0; l--, code >>= 1) + rev_code = (rev_code << 1) | (code & 1); + d->m_huff_codes[table_num][i] = (mz_uint16)rev_code; + } + } + +#define TDEFL_PUT_BITS(b, l) \ + do \ + { \ + mz_uint bits = b; \ + mz_uint len = l; \ + MZ_ASSERT(bits <= ((1U << len) - 1U)); \ + d->m_bit_buffer |= (bits << d->m_bits_in); \ + d->m_bits_in += len; \ + while (d->m_bits_in >= 8) \ + { \ + if (d->m_pOutput_buf < d->m_pOutput_buf_end) \ + *d->m_pOutput_buf++ = (mz_uint8)(d->m_bit_buffer); \ + d->m_bit_buffer >>= 8; \ + d->m_bits_in -= 8; \ + } \ + } \ + MZ_MACRO_END + +#define TDEFL_RLE_PREV_CODE_SIZE() \ + { \ + if (rle_repeat_count) \ + { \ + if (rle_repeat_count < 3) \ + { \ + d->m_huff_count[2][prev_code_size] = (mz_uint16)(d->m_huff_count[2][prev_code_size] + rle_repeat_count); \ + while (rle_repeat_count--) \ + packed_code_sizes[num_packed_code_sizes++] = prev_code_size; \ + } \ + else \ + { \ + d->m_huff_count[2][16] = (mz_uint16)(d->m_huff_count[2][16] + 1); \ + packed_code_sizes[num_packed_code_sizes++] = 16; \ + packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_repeat_count - 3); \ + } \ + rle_repeat_count = 0; \ + } \ + } + +#define TDEFL_RLE_ZERO_CODE_SIZE() \ + { \ + if (rle_z_count) \ + { \ + if (rle_z_count < 3) \ + { \ + d->m_huff_count[2][0] = (mz_uint16)(d->m_huff_count[2][0] + rle_z_count); \ + while (rle_z_count--) \ + packed_code_sizes[num_packed_code_sizes++] = 0; \ + } \ + else if (rle_z_count <= 10) \ + { \ + d->m_huff_count[2][17] = (mz_uint16)(d->m_huff_count[2][17] + 1); \ + packed_code_sizes[num_packed_code_sizes++] = 17; \ + packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_z_count - 3); \ + } \ + else \ + { \ + d->m_huff_count[2][18] = (mz_uint16)(d->m_huff_count[2][18] + 1); \ + packed_code_sizes[num_packed_code_sizes++] = 18; \ + packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_z_count - 11); \ + } \ + rle_z_count = 0; \ + } \ + } + + static const mz_uint8 s_tdefl_packed_code_size_syms_swizzle[] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + static void tdefl_start_dynamic_block(tdefl_compressor *d) + { + int num_lit_codes, num_dist_codes, num_bit_lengths; + mz_uint i, total_code_sizes_to_pack, num_packed_code_sizes, rle_z_count, rle_repeat_count, packed_code_sizes_index; + mz_uint8 code_sizes_to_pack[TDEFL_MAX_HUFF_SYMBOLS_0 + TDEFL_MAX_HUFF_SYMBOLS_1], packed_code_sizes[TDEFL_MAX_HUFF_SYMBOLS_0 + TDEFL_MAX_HUFF_SYMBOLS_1], prev_code_size = 0xFF; + + d->m_huff_count[0][256] = 1; + + tdefl_optimize_huffman_table(d, 0, TDEFL_MAX_HUFF_SYMBOLS_0, 15, MZ_FALSE); + tdefl_optimize_huffman_table(d, 1, TDEFL_MAX_HUFF_SYMBOLS_1, 15, MZ_FALSE); + + for (num_lit_codes = 286; num_lit_codes > 257; num_lit_codes--) + if (d->m_huff_code_sizes[0][num_lit_codes - 1]) + break; + for (num_dist_codes = 30; num_dist_codes > 1; num_dist_codes--) + if (d->m_huff_code_sizes[1][num_dist_codes - 1]) + break; + + memcpy(code_sizes_to_pack, &d->m_huff_code_sizes[0][0], num_lit_codes); + memcpy(code_sizes_to_pack + num_lit_codes, &d->m_huff_code_sizes[1][0], num_dist_codes); + total_code_sizes_to_pack = num_lit_codes + num_dist_codes; + num_packed_code_sizes = 0; + rle_z_count = 0; + rle_repeat_count = 0; + + memset(&d->m_huff_count[2][0], 0, sizeof(d->m_huff_count[2][0]) * TDEFL_MAX_HUFF_SYMBOLS_2); + for (i = 0; i < total_code_sizes_to_pack; i++) + { + mz_uint8 code_size = code_sizes_to_pack[i]; + if (!code_size) + { + TDEFL_RLE_PREV_CODE_SIZE(); + if (++rle_z_count == 138) + { + TDEFL_RLE_ZERO_CODE_SIZE(); + } + } + else + { + TDEFL_RLE_ZERO_CODE_SIZE(); + if (code_size != prev_code_size) + { + TDEFL_RLE_PREV_CODE_SIZE(); + d->m_huff_count[2][code_size] = (mz_uint16)(d->m_huff_count[2][code_size] + 1); + packed_code_sizes[num_packed_code_sizes++] = code_size; + } + else if (++rle_repeat_count == 6) + { + TDEFL_RLE_PREV_CODE_SIZE(); + } + } + prev_code_size = code_size; + } + if (rle_repeat_count) + { + TDEFL_RLE_PREV_CODE_SIZE(); + } + else + { + TDEFL_RLE_ZERO_CODE_SIZE(); + } + + tdefl_optimize_huffman_table(d, 2, TDEFL_MAX_HUFF_SYMBOLS_2, 7, MZ_FALSE); + + TDEFL_PUT_BITS(2, 2); + + TDEFL_PUT_BITS(num_lit_codes - 257, 5); + TDEFL_PUT_BITS(num_dist_codes - 1, 5); + + for (num_bit_lengths = 18; num_bit_lengths >= 0; num_bit_lengths--) + if (d->m_huff_code_sizes[2][s_tdefl_packed_code_size_syms_swizzle[num_bit_lengths]]) + break; + num_bit_lengths = MZ_MAX(4, (num_bit_lengths + 1)); + TDEFL_PUT_BITS(num_bit_lengths - 4, 4); + for (i = 0; (int)i < num_bit_lengths; i++) + TDEFL_PUT_BITS(d->m_huff_code_sizes[2][s_tdefl_packed_code_size_syms_swizzle[i]], 3); + + for (packed_code_sizes_index = 0; packed_code_sizes_index < num_packed_code_sizes;) + { + mz_uint code = packed_code_sizes[packed_code_sizes_index++]; + MZ_ASSERT(code < TDEFL_MAX_HUFF_SYMBOLS_2); + TDEFL_PUT_BITS(d->m_huff_codes[2][code], d->m_huff_code_sizes[2][code]); + if (code >= 16) + TDEFL_PUT_BITS(packed_code_sizes[packed_code_sizes_index++], "\02\03\07"[code - 16]); + } + } + + static void tdefl_start_static_block(tdefl_compressor *d) + { + mz_uint i; + mz_uint8 *p = &d->m_huff_code_sizes[0][0]; + + for (i = 0; i <= 143; ++i) + *p++ = 8; + for (; i <= 255; ++i) + *p++ = 9; + for (; i <= 279; ++i) + *p++ = 7; + for (; i <= 287; ++i) + *p++ = 8; + + memset(d->m_huff_code_sizes[1], 5, 32); + + tdefl_optimize_huffman_table(d, 0, 288, 15, MZ_TRUE); + tdefl_optimize_huffman_table(d, 1, 32, 15, MZ_TRUE); + + TDEFL_PUT_BITS(1, 2); + } + + static const mz_uint mz_bitmasks[17] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF }; + +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN && MINIZ_HAS_64BIT_REGISTERS + static mz_bool tdefl_compress_lz_codes(tdefl_compressor *d) + { + mz_uint flags; + mz_uint8 *pLZ_codes; + mz_uint8 *pOutput_buf = d->m_pOutput_buf; + mz_uint8 *pLZ_code_buf_end = d->m_pLZ_code_buf; + mz_uint64 bit_buffer = d->m_bit_buffer; + mz_uint bits_in = d->m_bits_in; + +#define TDEFL_PUT_BITS_FAST(b, l) \ + { \ + bit_buffer |= (((mz_uint64)(b)) << bits_in); \ + bits_in += (l); \ + } + + flags = 1; + for (pLZ_codes = d->m_lz_code_buf; pLZ_codes < pLZ_code_buf_end; flags >>= 1) + { + if (flags == 1) + flags = *pLZ_codes++ | 0x100; + + if (flags & 1) + { + mz_uint s0, s1, n0, n1, sym, num_extra_bits; + mz_uint match_len = pLZ_codes[0]; + mz_uint match_dist = (pLZ_codes[1] | (pLZ_codes[2] << 8)); + pLZ_codes += 3; + + MZ_ASSERT(d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); + TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][s_tdefl_len_sym[match_len]], d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); + TDEFL_PUT_BITS_FAST(match_len & mz_bitmasks[s_tdefl_len_extra[match_len]], s_tdefl_len_extra[match_len]); + + /* This sequence coaxes MSVC into using cmov's vs. jmp's. */ + s0 = s_tdefl_small_dist_sym[match_dist & 511]; + n0 = s_tdefl_small_dist_extra[match_dist & 511]; + s1 = s_tdefl_large_dist_sym[match_dist >> 8]; + n1 = s_tdefl_large_dist_extra[match_dist >> 8]; + sym = (match_dist < 512) ? s0 : s1; + num_extra_bits = (match_dist < 512) ? n0 : n1; + + MZ_ASSERT(d->m_huff_code_sizes[1][sym]); + TDEFL_PUT_BITS_FAST(d->m_huff_codes[1][sym], d->m_huff_code_sizes[1][sym]); + TDEFL_PUT_BITS_FAST(match_dist & mz_bitmasks[num_extra_bits], num_extra_bits); + } + else + { + mz_uint lit = *pLZ_codes++; + MZ_ASSERT(d->m_huff_code_sizes[0][lit]); + TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); + + if (((flags & 2) == 0) && (pLZ_codes < pLZ_code_buf_end)) + { + flags >>= 1; + lit = *pLZ_codes++; + MZ_ASSERT(d->m_huff_code_sizes[0][lit]); + TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); + + if (((flags & 2) == 0) && (pLZ_codes < pLZ_code_buf_end)) + { + flags >>= 1; + lit = *pLZ_codes++; + MZ_ASSERT(d->m_huff_code_sizes[0][lit]); + TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); + } + } + } + + if (pOutput_buf >= d->m_pOutput_buf_end) + return MZ_FALSE; + + memcpy(pOutput_buf, &bit_buffer, sizeof(mz_uint64)); + pOutput_buf += (bits_in >> 3); + bit_buffer >>= (bits_in & ~7); + bits_in &= 7; + } + +#undef TDEFL_PUT_BITS_FAST + + d->m_pOutput_buf = pOutput_buf; + d->m_bits_in = 0; + d->m_bit_buffer = 0; + + while (bits_in) + { + mz_uint32 n = MZ_MIN(bits_in, 16); + TDEFL_PUT_BITS((mz_uint)bit_buffer & mz_bitmasks[n], n); + bit_buffer >>= n; + bits_in -= n; + } + + TDEFL_PUT_BITS(d->m_huff_codes[0][256], d->m_huff_code_sizes[0][256]); + + return (d->m_pOutput_buf < d->m_pOutput_buf_end); + } +#else +static mz_bool tdefl_compress_lz_codes(tdefl_compressor *d) +{ + mz_uint flags; + mz_uint8 *pLZ_codes; + + flags = 1; + for (pLZ_codes = d->m_lz_code_buf; pLZ_codes < d->m_pLZ_code_buf; flags >>= 1) + { + if (flags == 1) + flags = *pLZ_codes++ | 0x100; + if (flags & 1) + { + mz_uint sym, num_extra_bits; + mz_uint match_len = pLZ_codes[0], match_dist = (pLZ_codes[1] | (pLZ_codes[2] << 8)); + pLZ_codes += 3; + + MZ_ASSERT(d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); + TDEFL_PUT_BITS(d->m_huff_codes[0][s_tdefl_len_sym[match_len]], d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); + TDEFL_PUT_BITS(match_len & mz_bitmasks[s_tdefl_len_extra[match_len]], s_tdefl_len_extra[match_len]); + + if (match_dist < 512) + { + sym = s_tdefl_small_dist_sym[match_dist]; + num_extra_bits = s_tdefl_small_dist_extra[match_dist]; + } + else + { + sym = s_tdefl_large_dist_sym[match_dist >> 8]; + num_extra_bits = s_tdefl_large_dist_extra[match_dist >> 8]; + } + MZ_ASSERT(d->m_huff_code_sizes[1][sym]); + TDEFL_PUT_BITS(d->m_huff_codes[1][sym], d->m_huff_code_sizes[1][sym]); + TDEFL_PUT_BITS(match_dist & mz_bitmasks[num_extra_bits], num_extra_bits); + } + else + { + mz_uint lit = *pLZ_codes++; + MZ_ASSERT(d->m_huff_code_sizes[0][lit]); + TDEFL_PUT_BITS(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); + } + } + + TDEFL_PUT_BITS(d->m_huff_codes[0][256], d->m_huff_code_sizes[0][256]); + + return (d->m_pOutput_buf < d->m_pOutput_buf_end); +} +#endif /* MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN && MINIZ_HAS_64BIT_REGISTERS */ + + static mz_bool tdefl_compress_block(tdefl_compressor *d, mz_bool static_block) + { + if (static_block) + tdefl_start_static_block(d); + else + tdefl_start_dynamic_block(d); + return tdefl_compress_lz_codes(d); + } + + static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 }; + + static int tdefl_flush_block(tdefl_compressor *d, int flush) + { + mz_uint saved_bit_buf, saved_bits_in; + mz_uint8 *pSaved_output_buf; + mz_bool comp_block_succeeded = MZ_FALSE; + int n, use_raw_block = ((d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS) != 0) && (d->m_lookahead_pos - d->m_lz_code_buf_dict_pos) <= d->m_dict_size; + mz_uint8 *pOutput_buf_start = ((d->m_pPut_buf_func == NULL) && ((*d->m_pOut_buf_size - d->m_out_buf_ofs) >= TDEFL_OUT_BUF_SIZE)) ? ((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs) : d->m_output_buf; + + d->m_pOutput_buf = pOutput_buf_start; + d->m_pOutput_buf_end = d->m_pOutput_buf + TDEFL_OUT_BUF_SIZE - 16; + + MZ_ASSERT(!d->m_output_flush_remaining); + d->m_output_flush_ofs = 0; + d->m_output_flush_remaining = 0; + + *d->m_pLZ_flags = (mz_uint8)(*d->m_pLZ_flags >> d->m_num_flags_left); + d->m_pLZ_code_buf -= (d->m_num_flags_left == 8); + + if ((d->m_flags & TDEFL_WRITE_ZLIB_HEADER) && (!d->m_block_index)) + { + const mz_uint8 cmf = 0x78; + mz_uint8 flg, flevel = 3; + mz_uint header, i, mz_un = sizeof(s_tdefl_num_probes) / sizeof(mz_uint); + + /* Determine compression level by reversing the process in tdefl_create_comp_flags_from_zip_params() */ + for (i = 0; i < mz_un; i++) + if (s_tdefl_num_probes[i] == (d->m_flags & 0xFFF)) + break; + + if (i < 2) + flevel = 0; + else if (i < 6) + flevel = 1; + else if (i == 6) + flevel = 2; + + header = cmf << 8 | (flevel << 6); + header += 31 - (header % 31); + flg = header & 0xFF; + + TDEFL_PUT_BITS(cmf, 8); + TDEFL_PUT_BITS(flg, 8); + } + + TDEFL_PUT_BITS(flush == TDEFL_FINISH, 1); + + pSaved_output_buf = d->m_pOutput_buf; + saved_bit_buf = d->m_bit_buffer; + saved_bits_in = d->m_bits_in; + + if (!use_raw_block) + comp_block_succeeded = tdefl_compress_block(d, (d->m_flags & TDEFL_FORCE_ALL_STATIC_BLOCKS) || (d->m_total_lz_bytes < 48)); + + /* If the block gets expanded, forget the current contents of the output buffer and send a raw block instead. */ + if (((use_raw_block) || ((d->m_total_lz_bytes) && ((d->m_pOutput_buf - pSaved_output_buf + 1U) >= d->m_total_lz_bytes))) && + ((d->m_lookahead_pos - d->m_lz_code_buf_dict_pos) <= d->m_dict_size)) + { + mz_uint i; + d->m_pOutput_buf = pSaved_output_buf; + d->m_bit_buffer = saved_bit_buf, d->m_bits_in = saved_bits_in; + TDEFL_PUT_BITS(0, 2); + if (d->m_bits_in) + { + TDEFL_PUT_BITS(0, 8 - d->m_bits_in); + } + for (i = 2; i; --i, d->m_total_lz_bytes ^= 0xFFFF) + { + TDEFL_PUT_BITS(d->m_total_lz_bytes & 0xFFFF, 16); + } + for (i = 0; i < d->m_total_lz_bytes; ++i) + { + TDEFL_PUT_BITS(d->m_dict[(d->m_lz_code_buf_dict_pos + i) & TDEFL_LZ_DICT_SIZE_MASK], 8); + } + } + /* Check for the extremely unlikely (if not impossible) case of the compressed block not fitting into the output buffer when using dynamic codes. */ + else if (!comp_block_succeeded) + { + d->m_pOutput_buf = pSaved_output_buf; + d->m_bit_buffer = saved_bit_buf, d->m_bits_in = saved_bits_in; + tdefl_compress_block(d, MZ_TRUE); + } + + if (flush) + { + if (flush == TDEFL_FINISH) + { + if (d->m_bits_in) + { + TDEFL_PUT_BITS(0, 8 - d->m_bits_in); + } + if (d->m_flags & TDEFL_WRITE_ZLIB_HEADER) + { + mz_uint i, a = d->m_adler32; + for (i = 0; i < 4; i++) + { + TDEFL_PUT_BITS((a >> 24) & 0xFF, 8); + a <<= 8; + } + } + } + else + { + mz_uint i, z = 0; + TDEFL_PUT_BITS(0, 3); + if (d->m_bits_in) + { + TDEFL_PUT_BITS(0, 8 - d->m_bits_in); + } + for (i = 2; i; --i, z ^= 0xFFFF) + { + TDEFL_PUT_BITS(z & 0xFFFF, 16); + } + } + } + + MZ_ASSERT(d->m_pOutput_buf < d->m_pOutput_buf_end); + + memset(&d->m_huff_count[0][0], 0, sizeof(d->m_huff_count[0][0]) * TDEFL_MAX_HUFF_SYMBOLS_0); + memset(&d->m_huff_count[1][0], 0, sizeof(d->m_huff_count[1][0]) * TDEFL_MAX_HUFF_SYMBOLS_1); + + d->m_pLZ_code_buf = d->m_lz_code_buf + 1; + d->m_pLZ_flags = d->m_lz_code_buf; + d->m_num_flags_left = 8; + d->m_lz_code_buf_dict_pos += d->m_total_lz_bytes; + d->m_total_lz_bytes = 0; + d->m_block_index++; + + if ((n = (int)(d->m_pOutput_buf - pOutput_buf_start)) != 0) + { + if (d->m_pPut_buf_func) + { + *d->m_pIn_buf_size = d->m_pSrc - (const mz_uint8 *)d->m_pIn_buf; + if (!(*d->m_pPut_buf_func)(d->m_output_buf, n, d->m_pPut_buf_user)) + return (d->m_prev_return_status = TDEFL_STATUS_PUT_BUF_FAILED); + } + else if (pOutput_buf_start == d->m_output_buf) + { + int bytes_to_copy = (int)MZ_MIN((size_t)n, (size_t)(*d->m_pOut_buf_size - d->m_out_buf_ofs)); + memcpy((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs, d->m_output_buf, bytes_to_copy); + d->m_out_buf_ofs += bytes_to_copy; + if ((n -= bytes_to_copy) != 0) + { + d->m_output_flush_ofs = bytes_to_copy; + d->m_output_flush_remaining = n; + } + } + else + { + d->m_out_buf_ofs += n; + } + } + + return d->m_output_flush_remaining; + } + +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES +#ifdef MINIZ_UNALIGNED_USE_MEMCPY + static mz_uint16 TDEFL_READ_UNALIGNED_WORD(const mz_uint8 *p) + { + mz_uint16 ret; + memcpy(&ret, p, sizeof(mz_uint16)); + return ret; + } + static mz_uint16 TDEFL_READ_UNALIGNED_WORD2(const mz_uint16 *p) + { + mz_uint16 ret; + memcpy(&ret, p, sizeof(mz_uint16)); + return ret; + } +#else +#define TDEFL_READ_UNALIGNED_WORD(p) *(const mz_uint16 *)(p) +#define TDEFL_READ_UNALIGNED_WORD2(p) *(const mz_uint16 *)(p) +#endif + static MZ_FORCEINLINE void tdefl_find_match(tdefl_compressor *d, mz_uint lookahead_pos, mz_uint max_dist, mz_uint max_match_len, mz_uint *pMatch_dist, mz_uint *pMatch_len) + { + mz_uint dist, pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK, match_len = *pMatch_len, probe_pos = pos, next_probe_pos, probe_len; + mz_uint num_probes_left = d->m_max_probes[match_len >= 32]; + const mz_uint16 *s = (const mz_uint16 *)(d->m_dict + pos), *p, *q; + mz_uint16 c01 = TDEFL_READ_UNALIGNED_WORD(&d->m_dict[pos + match_len - 1]), s01 = TDEFL_READ_UNALIGNED_WORD2(s); + MZ_ASSERT(max_match_len <= TDEFL_MAX_MATCH_LEN); + if (max_match_len <= match_len) + return; + for (;;) + { + for (;;) + { + if (--num_probes_left == 0) + return; +#define TDEFL_PROBE \ + next_probe_pos = d->m_next[probe_pos]; \ + if ((!next_probe_pos) || ((dist = (mz_uint16)(lookahead_pos - next_probe_pos)) > max_dist)) \ + return; \ + probe_pos = next_probe_pos & TDEFL_LZ_DICT_SIZE_MASK; \ + if (TDEFL_READ_UNALIGNED_WORD(&d->m_dict[probe_pos + match_len - 1]) == c01) \ + break; + TDEFL_PROBE; + TDEFL_PROBE; + TDEFL_PROBE; + } + if (!dist) + break; + q = (const mz_uint16 *)(d->m_dict + probe_pos); + if (TDEFL_READ_UNALIGNED_WORD2(q) != s01) + continue; + p = s; + probe_len = 32; + do + { + } while ((TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && + (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (--probe_len > 0)); + if (!probe_len) + { + *pMatch_dist = dist; + *pMatch_len = MZ_MIN(max_match_len, (mz_uint)TDEFL_MAX_MATCH_LEN); + break; + } + else if ((probe_len = ((mz_uint)(p - s) * 2) + (mz_uint)(*(const mz_uint8 *)p == *(const mz_uint8 *)q)) > match_len) + { + *pMatch_dist = dist; + if ((*pMatch_len = match_len = MZ_MIN(max_match_len, probe_len)) == max_match_len) + break; + c01 = TDEFL_READ_UNALIGNED_WORD(&d->m_dict[pos + match_len - 1]); + } + } + } +#else +static MZ_FORCEINLINE void tdefl_find_match(tdefl_compressor *d, mz_uint lookahead_pos, mz_uint max_dist, mz_uint max_match_len, mz_uint *pMatch_dist, mz_uint *pMatch_len) +{ + mz_uint dist, pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK, match_len = *pMatch_len, probe_pos = pos, next_probe_pos, probe_len; + mz_uint num_probes_left = d->m_max_probes[match_len >= 32]; + const mz_uint8 *s = d->m_dict + pos, *p, *q; + mz_uint8 c0 = d->m_dict[pos + match_len], c1 = d->m_dict[pos + match_len - 1]; + MZ_ASSERT(max_match_len <= TDEFL_MAX_MATCH_LEN); + if (max_match_len <= match_len) + return; + for (;;) + { + for (;;) + { + if (--num_probes_left == 0) + return; +#define TDEFL_PROBE \ + next_probe_pos = d->m_next[probe_pos]; \ + if ((!next_probe_pos) || ((dist = (mz_uint16)(lookahead_pos - next_probe_pos)) > max_dist)) \ + return; \ + probe_pos = next_probe_pos & TDEFL_LZ_DICT_SIZE_MASK; \ + if ((d->m_dict[probe_pos + match_len] == c0) && (d->m_dict[probe_pos + match_len - 1] == c1)) \ + break; + TDEFL_PROBE; + TDEFL_PROBE; + TDEFL_PROBE; + } + if (!dist) + break; + p = s; + q = d->m_dict + probe_pos; + for (probe_len = 0; probe_len < max_match_len; probe_len++) + if (*p++ != *q++) + break; + if (probe_len > match_len) + { + *pMatch_dist = dist; + if ((*pMatch_len = match_len = probe_len) == max_match_len) + return; + c0 = d->m_dict[pos + match_len]; + c1 = d->m_dict[pos + match_len - 1]; + } + } +} +#endif /* #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES */ + +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN +#ifdef MINIZ_UNALIGNED_USE_MEMCPY + static mz_uint32 TDEFL_READ_UNALIGNED_WORD32(const mz_uint8 *p) + { + mz_uint32 ret; + memcpy(&ret, p, sizeof(mz_uint32)); + return ret; + } +#else +#define TDEFL_READ_UNALIGNED_WORD32(p) *(const mz_uint32 *)(p) +#endif + static mz_bool tdefl_compress_fast(tdefl_compressor *d) + { + /* Faster, minimally featured LZRW1-style match+parse loop with better register utilization. Intended for applications where raw throughput is valued more highly than ratio. */ + mz_uint lookahead_pos = d->m_lookahead_pos, lookahead_size = d->m_lookahead_size, dict_size = d->m_dict_size, total_lz_bytes = d->m_total_lz_bytes, num_flags_left = d->m_num_flags_left; + mz_uint8 *pLZ_code_buf = d->m_pLZ_code_buf, *pLZ_flags = d->m_pLZ_flags; + mz_uint cur_pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK; + + while ((d->m_src_buf_left) || ((d->m_flush) && (lookahead_size))) + { + const mz_uint TDEFL_COMP_FAST_LOOKAHEAD_SIZE = 4096; + mz_uint dst_pos = (lookahead_pos + lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK; + mz_uint num_bytes_to_process = (mz_uint)MZ_MIN(d->m_src_buf_left, TDEFL_COMP_FAST_LOOKAHEAD_SIZE - lookahead_size); + d->m_src_buf_left -= num_bytes_to_process; + lookahead_size += num_bytes_to_process; + + while (num_bytes_to_process) + { + mz_uint32 n = MZ_MIN(TDEFL_LZ_DICT_SIZE - dst_pos, num_bytes_to_process); + memcpy(d->m_dict + dst_pos, d->m_pSrc, n); + if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1)) + memcpy(d->m_dict + TDEFL_LZ_DICT_SIZE + dst_pos, d->m_pSrc, MZ_MIN(n, (TDEFL_MAX_MATCH_LEN - 1) - dst_pos)); + d->m_pSrc += n; + dst_pos = (dst_pos + n) & TDEFL_LZ_DICT_SIZE_MASK; + num_bytes_to_process -= n; + } + + dict_size = MZ_MIN(TDEFL_LZ_DICT_SIZE - lookahead_size, dict_size); + if ((!d->m_flush) && (lookahead_size < TDEFL_COMP_FAST_LOOKAHEAD_SIZE)) + break; + + while (lookahead_size >= 4) + { + mz_uint cur_match_dist, cur_match_len = 1; + mz_uint8 *pCur_dict = d->m_dict + cur_pos; + mz_uint first_trigram = TDEFL_READ_UNALIGNED_WORD32(pCur_dict) & 0xFFFFFF; + mz_uint hash = (first_trigram ^ (first_trigram >> (24 - (TDEFL_LZ_HASH_BITS - 8)))) & TDEFL_LEVEL1_HASH_SIZE_MASK; + mz_uint probe_pos = d->m_hash[hash]; + d->m_hash[hash] = (mz_uint16)lookahead_pos; + + if (((cur_match_dist = (mz_uint16)(lookahead_pos - probe_pos)) <= dict_size) && ((TDEFL_READ_UNALIGNED_WORD32(d->m_dict + (probe_pos &= TDEFL_LZ_DICT_SIZE_MASK)) & 0xFFFFFF) == first_trigram)) + { + const mz_uint16 *p = (const mz_uint16 *)pCur_dict; + const mz_uint16 *q = (const mz_uint16 *)(d->m_dict + probe_pos); + mz_uint32 probe_len = 32; + do + { + } while ((TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && + (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (--probe_len > 0)); + cur_match_len = ((mz_uint)(p - (const mz_uint16 *)pCur_dict) * 2) + (mz_uint)(*(const mz_uint8 *)p == *(const mz_uint8 *)q); + if (!probe_len) + cur_match_len = cur_match_dist ? TDEFL_MAX_MATCH_LEN : 0; + + if ((cur_match_len < TDEFL_MIN_MATCH_LEN) || ((cur_match_len == TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 8U * 1024U))) + { + cur_match_len = 1; + *pLZ_code_buf++ = (mz_uint8)first_trigram; + *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1); + d->m_huff_count[0][(mz_uint8)first_trigram]++; + } + else + { + mz_uint32 s0, s1; + cur_match_len = MZ_MIN(cur_match_len, lookahead_size); + + MZ_ASSERT((cur_match_len >= TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 1) && (cur_match_dist <= TDEFL_LZ_DICT_SIZE)); + + cur_match_dist--; + + pLZ_code_buf[0] = (mz_uint8)(cur_match_len - TDEFL_MIN_MATCH_LEN); +#ifdef MINIZ_UNALIGNED_USE_MEMCPY + memcpy(&pLZ_code_buf[1], &cur_match_dist, sizeof(cur_match_dist)); +#else + *(mz_uint16 *)(&pLZ_code_buf[1]) = (mz_uint16)cur_match_dist; +#endif + pLZ_code_buf += 3; + *pLZ_flags = (mz_uint8)((*pLZ_flags >> 1) | 0x80); + + s0 = s_tdefl_small_dist_sym[cur_match_dist & 511]; + s1 = s_tdefl_large_dist_sym[cur_match_dist >> 8]; + d->m_huff_count[1][(cur_match_dist < 512) ? s0 : s1]++; + + d->m_huff_count[0][s_tdefl_len_sym[cur_match_len - TDEFL_MIN_MATCH_LEN]]++; + } + } + else + { + *pLZ_code_buf++ = (mz_uint8)first_trigram; + *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1); + d->m_huff_count[0][(mz_uint8)first_trigram]++; + } + + if (--num_flags_left == 0) + { + num_flags_left = 8; + pLZ_flags = pLZ_code_buf++; + } + + total_lz_bytes += cur_match_len; + lookahead_pos += cur_match_len; + dict_size = MZ_MIN(dict_size + cur_match_len, (mz_uint)TDEFL_LZ_DICT_SIZE); + cur_pos = (cur_pos + cur_match_len) & TDEFL_LZ_DICT_SIZE_MASK; + MZ_ASSERT(lookahead_size >= cur_match_len); + lookahead_size -= cur_match_len; + + if (pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8]) + { + int n; + d->m_lookahead_pos = lookahead_pos; + d->m_lookahead_size = lookahead_size; + d->m_dict_size = dict_size; + d->m_total_lz_bytes = total_lz_bytes; + d->m_pLZ_code_buf = pLZ_code_buf; + d->m_pLZ_flags = pLZ_flags; + d->m_num_flags_left = num_flags_left; + if ((n = tdefl_flush_block(d, 0)) != 0) + return (n < 0) ? MZ_FALSE : MZ_TRUE; + total_lz_bytes = d->m_total_lz_bytes; + pLZ_code_buf = d->m_pLZ_code_buf; + pLZ_flags = d->m_pLZ_flags; + num_flags_left = d->m_num_flags_left; + } + } + + while (lookahead_size) + { + mz_uint8 lit = d->m_dict[cur_pos]; + + total_lz_bytes++; + *pLZ_code_buf++ = lit; + *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1); + if (--num_flags_left == 0) + { + num_flags_left = 8; + pLZ_flags = pLZ_code_buf++; + } + + d->m_huff_count[0][lit]++; + + lookahead_pos++; + dict_size = MZ_MIN(dict_size + 1, (mz_uint)TDEFL_LZ_DICT_SIZE); + cur_pos = (cur_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK; + lookahead_size--; + + if (pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8]) + { + int n; + d->m_lookahead_pos = lookahead_pos; + d->m_lookahead_size = lookahead_size; + d->m_dict_size = dict_size; + d->m_total_lz_bytes = total_lz_bytes; + d->m_pLZ_code_buf = pLZ_code_buf; + d->m_pLZ_flags = pLZ_flags; + d->m_num_flags_left = num_flags_left; + if ((n = tdefl_flush_block(d, 0)) != 0) + return (n < 0) ? MZ_FALSE : MZ_TRUE; + total_lz_bytes = d->m_total_lz_bytes; + pLZ_code_buf = d->m_pLZ_code_buf; + pLZ_flags = d->m_pLZ_flags; + num_flags_left = d->m_num_flags_left; + } + } + } + + d->m_lookahead_pos = lookahead_pos; + d->m_lookahead_size = lookahead_size; + d->m_dict_size = dict_size; + d->m_total_lz_bytes = total_lz_bytes; + d->m_pLZ_code_buf = pLZ_code_buf; + d->m_pLZ_flags = pLZ_flags; + d->m_num_flags_left = num_flags_left; + return MZ_TRUE; + } +#endif /* MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN */ + + static MZ_FORCEINLINE void tdefl_record_literal(tdefl_compressor *d, mz_uint8 lit) + { + d->m_total_lz_bytes++; + *d->m_pLZ_code_buf++ = lit; + *d->m_pLZ_flags = (mz_uint8)(*d->m_pLZ_flags >> 1); + if (--d->m_num_flags_left == 0) + { + d->m_num_flags_left = 8; + d->m_pLZ_flags = d->m_pLZ_code_buf++; + } + d->m_huff_count[0][lit]++; + } + + static MZ_FORCEINLINE void tdefl_record_match(tdefl_compressor *d, mz_uint match_len, mz_uint match_dist) + { + mz_uint32 s0, s1; + + MZ_ASSERT((match_len >= TDEFL_MIN_MATCH_LEN) && (match_dist >= 1) && (match_dist <= TDEFL_LZ_DICT_SIZE)); + + d->m_total_lz_bytes += match_len; + + d->m_pLZ_code_buf[0] = (mz_uint8)(match_len - TDEFL_MIN_MATCH_LEN); + + match_dist -= 1; + d->m_pLZ_code_buf[1] = (mz_uint8)(match_dist & 0xFF); + d->m_pLZ_code_buf[2] = (mz_uint8)(match_dist >> 8); + d->m_pLZ_code_buf += 3; + + *d->m_pLZ_flags = (mz_uint8)((*d->m_pLZ_flags >> 1) | 0x80); + if (--d->m_num_flags_left == 0) + { + d->m_num_flags_left = 8; + d->m_pLZ_flags = d->m_pLZ_code_buf++; + } + + s0 = s_tdefl_small_dist_sym[match_dist & 511]; + s1 = s_tdefl_large_dist_sym[(match_dist >> 8) & 127]; + d->m_huff_count[1][(match_dist < 512) ? s0 : s1]++; + d->m_huff_count[0][s_tdefl_len_sym[match_len - TDEFL_MIN_MATCH_LEN]]++; + } + + static mz_bool tdefl_compress_normal(tdefl_compressor *d) + { + const mz_uint8 *pSrc = d->m_pSrc; + size_t src_buf_left = d->m_src_buf_left; + tdefl_flush flush = d->m_flush; + + while ((src_buf_left) || ((flush) && (d->m_lookahead_size))) + { + mz_uint len_to_move, cur_match_dist, cur_match_len, cur_pos; + /* Update dictionary and hash chains. Keeps the lookahead size equal to TDEFL_MAX_MATCH_LEN. */ + if ((d->m_lookahead_size + d->m_dict_size) >= (TDEFL_MIN_MATCH_LEN - 1)) + { + mz_uint dst_pos = (d->m_lookahead_pos + d->m_lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK, ins_pos = d->m_lookahead_pos + d->m_lookahead_size - 2; + mz_uint hash = (d->m_dict[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] << TDEFL_LZ_HASH_SHIFT) ^ d->m_dict[(ins_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK]; + mz_uint num_bytes_to_process = (mz_uint)MZ_MIN(src_buf_left, TDEFL_MAX_MATCH_LEN - d->m_lookahead_size); + const mz_uint8 *pSrc_end = pSrc ? pSrc + num_bytes_to_process : NULL; + src_buf_left -= num_bytes_to_process; + d->m_lookahead_size += num_bytes_to_process; + while (pSrc != pSrc_end) + { + mz_uint8 c = *pSrc++; + d->m_dict[dst_pos] = c; + if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1)) + d->m_dict[TDEFL_LZ_DICT_SIZE + dst_pos] = c; + hash = ((hash << TDEFL_LZ_HASH_SHIFT) ^ c) & (TDEFL_LZ_HASH_SIZE - 1); + d->m_next[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] = d->m_hash[hash]; + d->m_hash[hash] = (mz_uint16)(ins_pos); + dst_pos = (dst_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK; + ins_pos++; + } + } + else + { + while ((src_buf_left) && (d->m_lookahead_size < TDEFL_MAX_MATCH_LEN)) + { + mz_uint8 c = *pSrc++; + mz_uint dst_pos = (d->m_lookahead_pos + d->m_lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK; + src_buf_left--; + d->m_dict[dst_pos] = c; + if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1)) + d->m_dict[TDEFL_LZ_DICT_SIZE + dst_pos] = c; + if ((++d->m_lookahead_size + d->m_dict_size) >= TDEFL_MIN_MATCH_LEN) + { + mz_uint ins_pos = d->m_lookahead_pos + (d->m_lookahead_size - 1) - 2; + mz_uint hash = ((d->m_dict[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] << (TDEFL_LZ_HASH_SHIFT * 2)) ^ (d->m_dict[(ins_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK] << TDEFL_LZ_HASH_SHIFT) ^ c) & (TDEFL_LZ_HASH_SIZE - 1); + d->m_next[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] = d->m_hash[hash]; + d->m_hash[hash] = (mz_uint16)(ins_pos); + } + } + } + d->m_dict_size = MZ_MIN(TDEFL_LZ_DICT_SIZE - d->m_lookahead_size, d->m_dict_size); + if ((!flush) && (d->m_lookahead_size < TDEFL_MAX_MATCH_LEN)) + break; + + /* Simple lazy/greedy parsing state machine. */ + len_to_move = 1; + cur_match_dist = 0; + cur_match_len = d->m_saved_match_len ? d->m_saved_match_len : (TDEFL_MIN_MATCH_LEN - 1); + cur_pos = d->m_lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK; + if (d->m_flags & (TDEFL_RLE_MATCHES | TDEFL_FORCE_ALL_RAW_BLOCKS)) + { + if ((d->m_dict_size) && (!(d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS))) + { + mz_uint8 c = d->m_dict[(cur_pos - 1) & TDEFL_LZ_DICT_SIZE_MASK]; + cur_match_len = 0; + while (cur_match_len < d->m_lookahead_size) + { + if (d->m_dict[cur_pos + cur_match_len] != c) + break; + cur_match_len++; + } + if (cur_match_len < TDEFL_MIN_MATCH_LEN) + cur_match_len = 0; + else + cur_match_dist = 1; + } + } + else + { + tdefl_find_match(d, d->m_lookahead_pos, d->m_dict_size, d->m_lookahead_size, &cur_match_dist, &cur_match_len); + } + if (((cur_match_len == TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 8U * 1024U)) || (cur_pos == cur_match_dist) || ((d->m_flags & TDEFL_FILTER_MATCHES) && (cur_match_len <= 5))) + { + cur_match_dist = cur_match_len = 0; + } + if (d->m_saved_match_len) + { + if (cur_match_len > d->m_saved_match_len) + { + tdefl_record_literal(d, (mz_uint8)d->m_saved_lit); + if (cur_match_len >= 128) + { + tdefl_record_match(d, cur_match_len, cur_match_dist); + d->m_saved_match_len = 0; + len_to_move = cur_match_len; + } + else + { + d->m_saved_lit = d->m_dict[cur_pos]; + d->m_saved_match_dist = cur_match_dist; + d->m_saved_match_len = cur_match_len; + } + } + else + { + tdefl_record_match(d, d->m_saved_match_len, d->m_saved_match_dist); + len_to_move = d->m_saved_match_len - 1; + d->m_saved_match_len = 0; + } + } + else if (!cur_match_dist) + tdefl_record_literal(d, d->m_dict[MZ_MIN(cur_pos, sizeof(d->m_dict) - 1)]); + else if ((d->m_greedy_parsing) || (d->m_flags & TDEFL_RLE_MATCHES) || (cur_match_len >= 128)) + { + tdefl_record_match(d, cur_match_len, cur_match_dist); + len_to_move = cur_match_len; + } + else + { + d->m_saved_lit = d->m_dict[MZ_MIN(cur_pos, sizeof(d->m_dict) - 1)]; + d->m_saved_match_dist = cur_match_dist; + d->m_saved_match_len = cur_match_len; + } + /* Move the lookahead forward by len_to_move bytes. */ + d->m_lookahead_pos += len_to_move; + MZ_ASSERT(d->m_lookahead_size >= len_to_move); + d->m_lookahead_size -= len_to_move; + d->m_dict_size = MZ_MIN(d->m_dict_size + len_to_move, (mz_uint)TDEFL_LZ_DICT_SIZE); + /* Check if it's time to flush the current LZ codes to the internal output buffer. */ + if ((d->m_pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8]) || + ((d->m_total_lz_bytes > 31 * 1024) && (((((mz_uint)(d->m_pLZ_code_buf - d->m_lz_code_buf) * 115) >> 7) >= d->m_total_lz_bytes) || (d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS)))) + { + int n; + d->m_pSrc = pSrc; + d->m_src_buf_left = src_buf_left; + if ((n = tdefl_flush_block(d, 0)) != 0) + return (n < 0) ? MZ_FALSE : MZ_TRUE; + } + } + + d->m_pSrc = pSrc; + d->m_src_buf_left = src_buf_left; + return MZ_TRUE; + } + + static tdefl_status tdefl_flush_output_buffer(tdefl_compressor *d) + { + if (d->m_pIn_buf_size) + { + *d->m_pIn_buf_size = d->m_pSrc - (const mz_uint8 *)d->m_pIn_buf; + } + + if (d->m_pOut_buf_size) + { + size_t n = MZ_MIN(*d->m_pOut_buf_size - d->m_out_buf_ofs, d->m_output_flush_remaining); + memcpy((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs, d->m_output_buf + d->m_output_flush_ofs, n); + d->m_output_flush_ofs += (mz_uint)n; + d->m_output_flush_remaining -= (mz_uint)n; + d->m_out_buf_ofs += n; + + *d->m_pOut_buf_size = d->m_out_buf_ofs; + } + + return (d->m_finished && !d->m_output_flush_remaining) ? TDEFL_STATUS_DONE : TDEFL_STATUS_OKAY; + } + + tdefl_status tdefl_compress(tdefl_compressor *d, const void *pIn_buf, size_t *pIn_buf_size, void *pOut_buf, size_t *pOut_buf_size, tdefl_flush flush) + { + if (!d) + { + if (pIn_buf_size) + *pIn_buf_size = 0; + if (pOut_buf_size) + *pOut_buf_size = 0; + return TDEFL_STATUS_BAD_PARAM; + } + + d->m_pIn_buf = pIn_buf; + d->m_pIn_buf_size = pIn_buf_size; + d->m_pOut_buf = pOut_buf; + d->m_pOut_buf_size = pOut_buf_size; + d->m_pSrc = (const mz_uint8 *)(pIn_buf); + d->m_src_buf_left = pIn_buf_size ? *pIn_buf_size : 0; + d->m_out_buf_ofs = 0; + d->m_flush = flush; + + if (((d->m_pPut_buf_func != NULL) == ((pOut_buf != NULL) || (pOut_buf_size != NULL))) || (d->m_prev_return_status != TDEFL_STATUS_OKAY) || + (d->m_wants_to_finish && (flush != TDEFL_FINISH)) || (pIn_buf_size && *pIn_buf_size && !pIn_buf) || (pOut_buf_size && *pOut_buf_size && !pOut_buf)) + { + if (pIn_buf_size) + *pIn_buf_size = 0; + if (pOut_buf_size) + *pOut_buf_size = 0; + return (d->m_prev_return_status = TDEFL_STATUS_BAD_PARAM); + } + d->m_wants_to_finish |= (flush == TDEFL_FINISH); + + if ((d->m_output_flush_remaining) || (d->m_finished)) + return (d->m_prev_return_status = tdefl_flush_output_buffer(d)); + +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN + if (((d->m_flags & TDEFL_MAX_PROBES_MASK) == 1) && + ((d->m_flags & TDEFL_GREEDY_PARSING_FLAG) != 0) && + ((d->m_flags & (TDEFL_FILTER_MATCHES | TDEFL_FORCE_ALL_RAW_BLOCKS | TDEFL_RLE_MATCHES)) == 0)) + { + if (!tdefl_compress_fast(d)) + return d->m_prev_return_status; + } + else +#endif /* #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN */ + { + if (!tdefl_compress_normal(d)) + return d->m_prev_return_status; + } + + if ((d->m_flags & (TDEFL_WRITE_ZLIB_HEADER | TDEFL_COMPUTE_ADLER32)) && (pIn_buf)) + d->m_adler32 = (mz_uint32)mz_adler32(d->m_adler32, (const mz_uint8 *)pIn_buf, d->m_pSrc - (const mz_uint8 *)pIn_buf); + + if ((flush) && (!d->m_lookahead_size) && (!d->m_src_buf_left) && (!d->m_output_flush_remaining)) + { + if (tdefl_flush_block(d, flush) < 0) + return d->m_prev_return_status; + d->m_finished = (flush == TDEFL_FINISH); + if (flush == TDEFL_FULL_FLUSH) + { + MZ_CLEAR_ARR(d->m_hash); + MZ_CLEAR_ARR(d->m_next); + d->m_dict_size = 0; + } + } + + return (d->m_prev_return_status = tdefl_flush_output_buffer(d)); + } + + tdefl_status tdefl_compress_buffer(tdefl_compressor *d, const void *pIn_buf, size_t in_buf_size, tdefl_flush flush) + { + MZ_ASSERT(d->m_pPut_buf_func); + return tdefl_compress(d, pIn_buf, &in_buf_size, NULL, NULL, flush); + } + + tdefl_status tdefl_init(tdefl_compressor *d, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags) + { + d->m_pPut_buf_func = pPut_buf_func; + d->m_pPut_buf_user = pPut_buf_user; + d->m_flags = (mz_uint)(flags); + d->m_max_probes[0] = 1 + ((flags & 0xFFF) + 2) / 3; + d->m_greedy_parsing = (flags & TDEFL_GREEDY_PARSING_FLAG) != 0; + d->m_max_probes[1] = 1 + (((flags & 0xFFF) >> 2) + 2) / 3; + if (!(flags & TDEFL_NONDETERMINISTIC_PARSING_FLAG)) + MZ_CLEAR_ARR(d->m_hash); + d->m_lookahead_pos = d->m_lookahead_size = d->m_dict_size = d->m_total_lz_bytes = d->m_lz_code_buf_dict_pos = d->m_bits_in = 0; + d->m_output_flush_ofs = d->m_output_flush_remaining = d->m_finished = d->m_block_index = d->m_bit_buffer = d->m_wants_to_finish = 0; + d->m_pLZ_code_buf = d->m_lz_code_buf + 1; + d->m_pLZ_flags = d->m_lz_code_buf; + *d->m_pLZ_flags = 0; + d->m_num_flags_left = 8; + d->m_pOutput_buf = d->m_output_buf; + d->m_pOutput_buf_end = d->m_output_buf; + d->m_prev_return_status = TDEFL_STATUS_OKAY; + d->m_saved_match_dist = d->m_saved_match_len = d->m_saved_lit = 0; + d->m_adler32 = 1; + d->m_pIn_buf = NULL; + d->m_pOut_buf = NULL; + d->m_pIn_buf_size = NULL; + d->m_pOut_buf_size = NULL; + d->m_flush = TDEFL_NO_FLUSH; + d->m_pSrc = NULL; + d->m_src_buf_left = 0; + d->m_out_buf_ofs = 0; + if (!(flags & TDEFL_NONDETERMINISTIC_PARSING_FLAG)) + MZ_CLEAR_ARR(d->m_dict); + memset(&d->m_huff_count[0][0], 0, sizeof(d->m_huff_count[0][0]) * TDEFL_MAX_HUFF_SYMBOLS_0); + memset(&d->m_huff_count[1][0], 0, sizeof(d->m_huff_count[1][0]) * TDEFL_MAX_HUFF_SYMBOLS_1); + return TDEFL_STATUS_OKAY; + } + + tdefl_status tdefl_get_prev_return_status(tdefl_compressor *d) + { + return d->m_prev_return_status; + } + + mz_uint32 tdefl_get_adler32(tdefl_compressor *d) + { + return d->m_adler32; + } + + mz_bool tdefl_compress_mem_to_output(const void *pBuf, size_t buf_len, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags) + { + tdefl_compressor *pComp; + mz_bool succeeded; + if (((buf_len) && (!pBuf)) || (!pPut_buf_func)) + return MZ_FALSE; + pComp = (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor)); + if (!pComp) + return MZ_FALSE; + succeeded = (tdefl_init(pComp, pPut_buf_func, pPut_buf_user, flags) == TDEFL_STATUS_OKAY); + succeeded = succeeded && (tdefl_compress_buffer(pComp, pBuf, buf_len, TDEFL_FINISH) == TDEFL_STATUS_DONE); + MZ_FREE(pComp); + return succeeded; + } + + typedef struct + { + size_t m_size, m_capacity; + mz_uint8 *m_pBuf; + mz_bool m_expandable; + } tdefl_output_buffer; + + static mz_bool tdefl_output_buffer_putter(const void *pBuf, int len, void *pUser) + { + tdefl_output_buffer *p = (tdefl_output_buffer *)pUser; + size_t new_size = p->m_size + len; + if (new_size > p->m_capacity) + { + size_t new_capacity = p->m_capacity; + mz_uint8 *pNew_buf; + if (!p->m_expandable) + return MZ_FALSE; + do + { + new_capacity = MZ_MAX(128U, new_capacity << 1U); + } while (new_size > new_capacity); + pNew_buf = (mz_uint8 *)MZ_REALLOC(p->m_pBuf, new_capacity); + if (!pNew_buf) + return MZ_FALSE; + p->m_pBuf = pNew_buf; + p->m_capacity = new_capacity; + } + memcpy((mz_uint8 *)p->m_pBuf + p->m_size, pBuf, len); + p->m_size = new_size; + return MZ_TRUE; + } + + void *tdefl_compress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags) + { + tdefl_output_buffer out_buf; + MZ_CLEAR_OBJ(out_buf); + if (!pOut_len) + return MZ_FALSE; + else + *pOut_len = 0; + out_buf.m_expandable = MZ_TRUE; + if (!tdefl_compress_mem_to_output(pSrc_buf, src_buf_len, tdefl_output_buffer_putter, &out_buf, flags)) + return NULL; + *pOut_len = out_buf.m_size; + return out_buf.m_pBuf; + } + + size_t tdefl_compress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags) + { + tdefl_output_buffer out_buf; + MZ_CLEAR_OBJ(out_buf); + if (!pOut_buf) + return 0; + out_buf.m_pBuf = (mz_uint8 *)pOut_buf; + out_buf.m_capacity = out_buf_len; + if (!tdefl_compress_mem_to_output(pSrc_buf, src_buf_len, tdefl_output_buffer_putter, &out_buf, flags)) + return 0; + return out_buf.m_size; + } + + /* level may actually range from [0,10] (10 is a "hidden" max level, where we want a bit more compression and it's fine if throughput to fall off a cliff on some files). */ + mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy) + { + mz_uint comp_flags = s_tdefl_num_probes[(level >= 0) ? MZ_MIN(10, level) : MZ_DEFAULT_LEVEL] | ((level <= 3) ? TDEFL_GREEDY_PARSING_FLAG : 0); + if (window_bits > 0) + comp_flags |= TDEFL_WRITE_ZLIB_HEADER; + + if (!level) + comp_flags |= TDEFL_FORCE_ALL_RAW_BLOCKS; + else if (strategy == MZ_FILTERED) + comp_flags |= TDEFL_FILTER_MATCHES; + else if (strategy == MZ_HUFFMAN_ONLY) + comp_flags &= ~TDEFL_MAX_PROBES_MASK; + else if (strategy == MZ_FIXED) + comp_flags |= TDEFL_FORCE_ALL_STATIC_BLOCKS; + else if (strategy == MZ_RLE) + comp_flags |= TDEFL_RLE_MATCHES; + + return comp_flags; + } + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4204) /* nonstandard extension used : non-constant aggregate initializer (also supported by GNU C and C99, so no big deal) */ +#endif + + /* Simple PNG writer function by Alex Evans, 2011. Released into the public domain: https://gist.github.com/908299, more context at + http://altdevblogaday.org/2011/04/06/a-smaller-jpg-encoder/. + This is actually a modification of Alex's original code so PNG files generated by this function pass pngcheck. */ + void *tdefl_write_image_to_png_file_in_memory_ex(const void *pImage, int w, int h, int num_chans, size_t *pLen_out, mz_uint level, mz_bool flip) + { + /* Using a local copy of this array here in case MINIZ_NO_ZLIB_APIS was defined. */ + static const mz_uint s_tdefl_png_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 }; + tdefl_compressor *pComp = (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor)); + tdefl_output_buffer out_buf; + int i, bpl = w * num_chans, y, z; + mz_uint32 c; + *pLen_out = 0; + if (!pComp) + return NULL; + if (w <= 0 || h <= 0 || w > 0xFFFF || h > 0xFFFF || num_chans < 1 || num_chans > 4) + { + MZ_FREE(pComp); + return NULL; + } + MZ_CLEAR_OBJ(out_buf); + out_buf.m_expandable = MZ_TRUE; + out_buf.m_capacity = 57 + MZ_MAX(64, (1 + bpl) * h); + if (NULL == (out_buf.m_pBuf = (mz_uint8 *)MZ_MALLOC(out_buf.m_capacity))) + { + MZ_FREE(pComp); + return NULL; + } + /* write dummy header */ + for (z = 41; z; --z) + tdefl_output_buffer_putter(&z, 1, &out_buf); + /* compress image data */ + tdefl_init(pComp, tdefl_output_buffer_putter, &out_buf, s_tdefl_png_num_probes[MZ_MIN(10, level)] | TDEFL_WRITE_ZLIB_HEADER); + for (y = 0; y < h; ++y) + { + tdefl_compress_buffer(pComp, &z, 1, TDEFL_NO_FLUSH); + tdefl_compress_buffer(pComp, (mz_uint8 *)pImage + (flip ? (h - 1 - y) : y) * bpl, bpl, TDEFL_NO_FLUSH); + } + if (tdefl_compress_buffer(pComp, NULL, 0, TDEFL_FINISH) != TDEFL_STATUS_DONE) + { + MZ_FREE(pComp); + MZ_FREE(out_buf.m_pBuf); + return NULL; + } + /* write real header */ + *pLen_out = out_buf.m_size - 41; + { + static const mz_uint8 chans[] = { 0x00, 0x00, 0x04, 0x02, 0x06 }; + mz_uint8 pnghdr[41] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, + 0x0a, 0x1a, 0x0a, 0x00, 0x00, + 0x00, 0x0d, 0x49, 0x48, 0x44, + 0x52, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x49, 0x44, 0x41, + 0x54 }; + pnghdr[18] = (mz_uint8)(w >> 8); + pnghdr[19] = (mz_uint8)w; + pnghdr[22] = (mz_uint8)(h >> 8); + pnghdr[23] = (mz_uint8)h; + pnghdr[25] = chans[num_chans]; + pnghdr[33] = (mz_uint8)(*pLen_out >> 24); + pnghdr[34] = (mz_uint8)(*pLen_out >> 16); + pnghdr[35] = (mz_uint8)(*pLen_out >> 8); + pnghdr[36] = (mz_uint8)*pLen_out; + c = (mz_uint32)mz_crc32(MZ_CRC32_INIT, pnghdr + 12, 17); + for (i = 0; i < 4; ++i, c <<= 8) + ((mz_uint8 *)(pnghdr + 29))[i] = (mz_uint8)(c >> 24); + memcpy(out_buf.m_pBuf, pnghdr, 41); + } + /* write footer (IDAT CRC-32, followed by IEND chunk) */ + if (!tdefl_output_buffer_putter("\0\0\0\0\0\0\0\0\x49\x45\x4e\x44\xae\x42\x60\x82", 16, &out_buf)) + { + *pLen_out = 0; + MZ_FREE(pComp); + MZ_FREE(out_buf.m_pBuf); + return NULL; + } + c = (mz_uint32)mz_crc32(MZ_CRC32_INIT, out_buf.m_pBuf + 41 - 4, *pLen_out + 4); + for (i = 0; i < 4; ++i, c <<= 8) + (out_buf.m_pBuf + out_buf.m_size - 16)[i] = (mz_uint8)(c >> 24); + /* compute final size of file, grab compressed data buffer and return */ + *pLen_out += 57; + MZ_FREE(pComp); + return out_buf.m_pBuf; + } + void *tdefl_write_image_to_png_file_in_memory(const void *pImage, int w, int h, int num_chans, size_t *pLen_out) + { + /* Level 6 corresponds to TDEFL_DEFAULT_MAX_PROBES or MZ_DEFAULT_LEVEL (but we can't depend on MZ_DEFAULT_LEVEL being available in case the zlib API's where #defined out) */ + return tdefl_write_image_to_png_file_in_memory_ex(pImage, w, h, num_chans, pLen_out, 6, MZ_FALSE); + } + +#ifndef MINIZ_NO_MALLOC + /* Allocate the tdefl_compressor and tinfl_decompressor structures in C so that */ + /* non-C language bindings to tdefL_ and tinfl_ API don't need to worry about */ + /* structure size and allocation mechanism. */ + tdefl_compressor *tdefl_compressor_alloc(void) + { + return (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor)); + } + + void tdefl_compressor_free(tdefl_compressor *pComp) + { + MZ_FREE(pComp); + } +#endif + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ + /************************************************************************** + * + * Copyright 2013-2014 RAD Game Tools and Valve Software + * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + **************************************************************************/ + + + +#ifndef MINIZ_NO_INFLATE_APIS + +#ifdef __cplusplus +extern "C" +{ +#endif + + /* ------------------- Low-level Decompression (completely independent from all compression API's) */ + +#define TINFL_MEMCPY(d, s, l) memcpy(d, s, l) +#define TINFL_MEMSET(p, c, l) memset(p, c, l) + +#define TINFL_CR_BEGIN \ + switch (r->m_state) \ + { \ + case 0: +#define TINFL_CR_RETURN(state_index, result) \ + do \ + { \ + status = result; \ + r->m_state = state_index; \ + goto common_exit; \ + case state_index:; \ + } \ + MZ_MACRO_END +#define TINFL_CR_RETURN_FOREVER(state_index, result) \ + do \ + { \ + for (;;) \ + { \ + TINFL_CR_RETURN(state_index, result); \ + } \ + } \ + MZ_MACRO_END +#define TINFL_CR_FINISH } + +#define TINFL_GET_BYTE(state_index, c) \ + do \ + { \ + while (pIn_buf_cur >= pIn_buf_end) \ + { \ + TINFL_CR_RETURN(state_index, (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) ? TINFL_STATUS_NEEDS_MORE_INPUT : TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS); \ + } \ + c = *pIn_buf_cur++; \ + } \ + MZ_MACRO_END + +#define TINFL_NEED_BITS(state_index, n) \ + do \ + { \ + mz_uint c; \ + TINFL_GET_BYTE(state_index, c); \ + bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); \ + num_bits += 8; \ + } while (num_bits < (mz_uint)(n)) +#define TINFL_SKIP_BITS(state_index, n) \ + do \ + { \ + if (num_bits < (mz_uint)(n)) \ + { \ + TINFL_NEED_BITS(state_index, n); \ + } \ + bit_buf >>= (n); \ + num_bits -= (n); \ + } \ + MZ_MACRO_END +#define TINFL_GET_BITS(state_index, b, n) \ + do \ + { \ + if (num_bits < (mz_uint)(n)) \ + { \ + TINFL_NEED_BITS(state_index, n); \ + } \ + b = bit_buf & ((1 << (n)) - 1); \ + bit_buf >>= (n); \ + num_bits -= (n); \ + } \ + MZ_MACRO_END + +/* TINFL_HUFF_BITBUF_FILL() is only used rarely, when the number of bytes remaining in the input buffer falls below 2. */ +/* It reads just enough bytes from the input stream that are needed to decode the next Huffman code (and absolutely no more). It works by trying to fully decode a */ +/* Huffman code by using whatever bits are currently present in the bit buffer. If this fails, it reads another byte, and tries again until it succeeds or until the */ +/* bit buffer contains >=15 bits (deflate's max. Huffman code size). */ +#define TINFL_HUFF_BITBUF_FILL(state_index, pLookUp, pTree) \ + do \ + { \ + temp = pLookUp[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]; \ + if (temp >= 0) \ + { \ + code_len = temp >> 9; \ + if ((code_len) && (num_bits >= code_len)) \ + break; \ + } \ + else if (num_bits > TINFL_FAST_LOOKUP_BITS) \ + { \ + code_len = TINFL_FAST_LOOKUP_BITS; \ + do \ + { \ + temp = pTree[~temp + ((bit_buf >> code_len++) & 1)]; \ + } while ((temp < 0) && (num_bits >= (code_len + 1))); \ + if (temp >= 0) \ + break; \ + } \ + TINFL_GET_BYTE(state_index, c); \ + bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); \ + num_bits += 8; \ + } while (num_bits < 15); + +/* TINFL_HUFF_DECODE() decodes the next Huffman coded symbol. It's more complex than you would initially expect because the zlib API expects the decompressor to never read */ +/* beyond the final byte of the deflate stream. (In other words, when this macro wants to read another byte from the input, it REALLY needs another byte in order to fully */ +/* decode the next Huffman code.) Handling this properly is particularly important on raw deflate (non-zlib) streams, which aren't followed by a byte aligned adler-32. */ +/* The slow path is only executed at the very end of the input buffer. */ +/* v1.16: The original macro handled the case at the very end of the passed-in input buffer, but we also need to handle the case where the user passes in 1+zillion bytes */ +/* following the deflate data and our non-conservative read-ahead path won't kick in here on this code. This is much trickier. */ +#define TINFL_HUFF_DECODE(state_index, sym, pLookUp, pTree) \ + do \ + { \ + int temp; \ + mz_uint code_len, c; \ + if (num_bits < 15) \ + { \ + if ((pIn_buf_end - pIn_buf_cur) < 2) \ + { \ + TINFL_HUFF_BITBUF_FILL(state_index, pLookUp, pTree); \ + } \ + else \ + { \ + bit_buf |= (((tinfl_bit_buf_t)pIn_buf_cur[0]) << num_bits) | (((tinfl_bit_buf_t)pIn_buf_cur[1]) << (num_bits + 8)); \ + pIn_buf_cur += 2; \ + num_bits += 16; \ + } \ + } \ + if ((temp = pLookUp[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) \ + code_len = temp >> 9, temp &= 511; \ + else \ + { \ + code_len = TINFL_FAST_LOOKUP_BITS; \ + do \ + { \ + temp = pTree[~temp + ((bit_buf >> code_len++) & 1)]; \ + } while (temp < 0); \ + } \ + sym = temp; \ + bit_buf >>= code_len; \ + num_bits -= code_len; \ + } \ + MZ_MACRO_END + + static void tinfl_clear_tree(tinfl_decompressor *r) + { + if (r->m_type == 0) + MZ_CLEAR_ARR(r->m_tree_0); + else if (r->m_type == 1) + MZ_CLEAR_ARR(r->m_tree_1); + else + MZ_CLEAR_ARR(r->m_tree_2); + } + + tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags) + { + static const mz_uint16 s_length_base[31] = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 }; + static const mz_uint8 s_length_extra[31] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0 }; + static const mz_uint16 s_dist_base[32] = { 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, 0, 0 }; + static const mz_uint8 s_dist_extra[32] = { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 }; + static const mz_uint8 s_length_dezigzag[19] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + static const mz_uint16 s_min_table_sizes[3] = { 257, 1, 4 }; + + mz_int16 *pTrees[3]; + mz_uint8 *pCode_sizes[3]; + + tinfl_status status = TINFL_STATUS_FAILED; + mz_uint32 num_bits, dist, counter, num_extra; + tinfl_bit_buf_t bit_buf; + const mz_uint8 *pIn_buf_cur = pIn_buf_next, *const pIn_buf_end = pIn_buf_next + *pIn_buf_size; + mz_uint8 *pOut_buf_cur = pOut_buf_next, *const pOut_buf_end = pOut_buf_next ? pOut_buf_next + *pOut_buf_size : NULL; + size_t out_buf_size_mask = (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF) ? (size_t)-1 : ((pOut_buf_next - pOut_buf_start) + *pOut_buf_size) - 1, dist_from_out_buf_start; + + /* Ensure the output buffer's size is a power of 2, unless the output buffer is large enough to hold the entire output file (in which case it doesn't matter). */ + if (((out_buf_size_mask + 1) & out_buf_size_mask) || (pOut_buf_next < pOut_buf_start)) + { + *pIn_buf_size = *pOut_buf_size = 0; + return TINFL_STATUS_BAD_PARAM; + } + + pTrees[0] = r->m_tree_0; + pTrees[1] = r->m_tree_1; + pTrees[2] = r->m_tree_2; + pCode_sizes[0] = r->m_code_size_0; + pCode_sizes[1] = r->m_code_size_1; + pCode_sizes[2] = r->m_code_size_2; + + num_bits = r->m_num_bits; + bit_buf = r->m_bit_buf; + dist = r->m_dist; + counter = r->m_counter; + num_extra = r->m_num_extra; + dist_from_out_buf_start = r->m_dist_from_out_buf_start; + TINFL_CR_BEGIN + + bit_buf = num_bits = dist = counter = num_extra = r->m_zhdr0 = r->m_zhdr1 = 0; + r->m_z_adler32 = r->m_check_adler32 = 1; + if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) + { + TINFL_GET_BYTE(1, r->m_zhdr0); + TINFL_GET_BYTE(2, r->m_zhdr1); + counter = (((r->m_zhdr0 * 256 + r->m_zhdr1) % 31 != 0) || (r->m_zhdr1 & 32) || ((r->m_zhdr0 & 15) != 8)); + if (!(decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF)) + counter |= (((1U << (8U + (r->m_zhdr0 >> 4))) > 32768U) || ((out_buf_size_mask + 1) < (size_t)((size_t)1 << (8U + (r->m_zhdr0 >> 4))))); + if (counter) + { + TINFL_CR_RETURN_FOREVER(36, TINFL_STATUS_FAILED); + } + } + + do + { + TINFL_GET_BITS(3, r->m_final, 3); + r->m_type = r->m_final >> 1; + if (r->m_type == 0) + { + TINFL_SKIP_BITS(5, num_bits & 7); + for (counter = 0; counter < 4; ++counter) + { + if (num_bits) + TINFL_GET_BITS(6, r->m_raw_header[counter], 8); + else + TINFL_GET_BYTE(7, r->m_raw_header[counter]); + } + if ((counter = (r->m_raw_header[0] | (r->m_raw_header[1] << 8))) != (mz_uint)(0xFFFF ^ (r->m_raw_header[2] | (r->m_raw_header[3] << 8)))) + { + TINFL_CR_RETURN_FOREVER(39, TINFL_STATUS_FAILED); + } + while ((counter) && (num_bits)) + { + TINFL_GET_BITS(51, dist, 8); + while (pOut_buf_cur >= pOut_buf_end) + { + TINFL_CR_RETURN(52, TINFL_STATUS_HAS_MORE_OUTPUT); + } + *pOut_buf_cur++ = (mz_uint8)dist; + counter--; + } + while (counter) + { + size_t n; + while (pOut_buf_cur >= pOut_buf_end) + { + TINFL_CR_RETURN(9, TINFL_STATUS_HAS_MORE_OUTPUT); + } + while (pIn_buf_cur >= pIn_buf_end) + { + TINFL_CR_RETURN(38, (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) ? TINFL_STATUS_NEEDS_MORE_INPUT : TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS); + } + n = MZ_MIN(MZ_MIN((size_t)(pOut_buf_end - pOut_buf_cur), (size_t)(pIn_buf_end - pIn_buf_cur)), counter); + TINFL_MEMCPY(pOut_buf_cur, pIn_buf_cur, n); + pIn_buf_cur += n; + pOut_buf_cur += n; + counter -= (mz_uint)n; + } + } + else if (r->m_type == 3) + { + TINFL_CR_RETURN_FOREVER(10, TINFL_STATUS_FAILED); + } + else + { + if (r->m_type == 1) + { + mz_uint8 *p = r->m_code_size_0; + mz_uint i; + r->m_table_sizes[0] = 288; + r->m_table_sizes[1] = 32; + TINFL_MEMSET(r->m_code_size_1, 5, 32); + for (i = 0; i <= 143; ++i) + *p++ = 8; + for (; i <= 255; ++i) + *p++ = 9; + for (; i <= 279; ++i) + *p++ = 7; + for (; i <= 287; ++i) + *p++ = 8; + } + else + { + for (counter = 0; counter < 3; counter++) + { + TINFL_GET_BITS(11, r->m_table_sizes[counter], "\05\05\04"[counter]); + r->m_table_sizes[counter] += s_min_table_sizes[counter]; + } + MZ_CLEAR_ARR(r->m_code_size_2); + for (counter = 0; counter < r->m_table_sizes[2]; counter++) + { + mz_uint s; + TINFL_GET_BITS(14, s, 3); + r->m_code_size_2[s_length_dezigzag[counter]] = (mz_uint8)s; + } + r->m_table_sizes[2] = 19; + } + for (; (int)r->m_type >= 0; r->m_type--) + { + int tree_next, tree_cur; + mz_int16 *pLookUp; + mz_int16 *pTree; + mz_uint8 *pCode_size; + mz_uint i, j, used_syms, total, sym_index, next_code[17], total_syms[16]; + pLookUp = r->m_look_up[r->m_type]; + pTree = pTrees[r->m_type]; + pCode_size = pCode_sizes[r->m_type]; + MZ_CLEAR_ARR(total_syms); + TINFL_MEMSET(pLookUp, 0, sizeof(r->m_look_up[0])); + tinfl_clear_tree(r); + for (i = 0; i < r->m_table_sizes[r->m_type]; ++i) + total_syms[pCode_size[i]]++; + used_syms = 0, total = 0; + next_code[0] = next_code[1] = 0; + for (i = 1; i <= 15; ++i) + { + used_syms += total_syms[i]; + next_code[i + 1] = (total = ((total + total_syms[i]) << 1)); + } + if ((65536 != total) && (used_syms > 1)) + { + TINFL_CR_RETURN_FOREVER(35, TINFL_STATUS_FAILED); + } + for (tree_next = -1, sym_index = 0; sym_index < r->m_table_sizes[r->m_type]; ++sym_index) + { + mz_uint rev_code = 0, l, cur_code, code_size = pCode_size[sym_index]; + if (!code_size) + continue; + cur_code = next_code[code_size]++; + for (l = code_size; l > 0; l--, cur_code >>= 1) + rev_code = (rev_code << 1) | (cur_code & 1); + if (code_size <= TINFL_FAST_LOOKUP_BITS) + { + mz_int16 k = (mz_int16)((code_size << 9) | sym_index); + while (rev_code < TINFL_FAST_LOOKUP_SIZE) + { + pLookUp[rev_code] = k; + rev_code += (1 << code_size); + } + continue; + } + if (0 == (tree_cur = pLookUp[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)])) + { + pLookUp[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)] = (mz_int16)tree_next; + tree_cur = tree_next; + tree_next -= 2; + } + rev_code >>= (TINFL_FAST_LOOKUP_BITS - 1); + for (j = code_size; j > (TINFL_FAST_LOOKUP_BITS + 1); j--) + { + tree_cur -= ((rev_code >>= 1) & 1); + if (!pTree[-tree_cur - 1]) + { + pTree[-tree_cur - 1] = (mz_int16)tree_next; + tree_cur = tree_next; + tree_next -= 2; + } + else + tree_cur = pTree[-tree_cur - 1]; + } + tree_cur -= ((rev_code >>= 1) & 1); + pTree[-tree_cur - 1] = (mz_int16)sym_index; + } + if (r->m_type == 2) + { + for (counter = 0; counter < (r->m_table_sizes[0] + r->m_table_sizes[1]);) + { + mz_uint s; + TINFL_HUFF_DECODE(16, dist, r->m_look_up[2], r->m_tree_2); + if (dist < 16) + { + r->m_len_codes[counter++] = (mz_uint8)dist; + continue; + } + if ((dist == 16) && (!counter)) + { + TINFL_CR_RETURN_FOREVER(17, TINFL_STATUS_FAILED); + } + num_extra = "\02\03\07"[dist - 16]; + TINFL_GET_BITS(18, s, num_extra); + s += "\03\03\013"[dist - 16]; + TINFL_MEMSET(r->m_len_codes + counter, (dist == 16) ? r->m_len_codes[counter - 1] : 0, s); + counter += s; + } + if ((r->m_table_sizes[0] + r->m_table_sizes[1]) != counter) + { + TINFL_CR_RETURN_FOREVER(21, TINFL_STATUS_FAILED); + } + TINFL_MEMCPY(r->m_code_size_0, r->m_len_codes, r->m_table_sizes[0]); + TINFL_MEMCPY(r->m_code_size_1, r->m_len_codes + r->m_table_sizes[0], r->m_table_sizes[1]); + } + } + for (;;) + { + mz_uint8 *pSrc; + for (;;) + { + if (((pIn_buf_end - pIn_buf_cur) < 4) || ((pOut_buf_end - pOut_buf_cur) < 2)) + { + TINFL_HUFF_DECODE(23, counter, r->m_look_up[0], r->m_tree_0); + if (counter >= 256) + break; + while (pOut_buf_cur >= pOut_buf_end) + { + TINFL_CR_RETURN(24, TINFL_STATUS_HAS_MORE_OUTPUT); + } + *pOut_buf_cur++ = (mz_uint8)counter; + } + else + { + int sym2; + mz_uint code_len; +#if TINFL_USE_64BIT_BITBUF + if (num_bits < 30) + { + bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE32(pIn_buf_cur)) << num_bits); + pIn_buf_cur += 4; + num_bits += 32; + } +#else + if (num_bits < 15) + { + bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits); + pIn_buf_cur += 2; + num_bits += 16; + } +#endif + if ((sym2 = r->m_look_up[0][bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) + code_len = sym2 >> 9; + else + { + code_len = TINFL_FAST_LOOKUP_BITS; + do + { + sym2 = r->m_tree_0[~sym2 + ((bit_buf >> code_len++) & 1)]; + } while (sym2 < 0); + } + counter = sym2; + bit_buf >>= code_len; + num_bits -= code_len; + if (code_len == 0) + { + TINFL_CR_RETURN_FOREVER(40, TINFL_STATUS_FAILED); + } + if (counter & 256) + break; + +#if !TINFL_USE_64BIT_BITBUF + if (num_bits < 15) + { + bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits); + pIn_buf_cur += 2; + num_bits += 16; + } +#endif + if ((sym2 = r->m_look_up[0][bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) + code_len = sym2 >> 9; + else + { + code_len = TINFL_FAST_LOOKUP_BITS; + do + { + sym2 = r->m_tree_0[~sym2 + ((bit_buf >> code_len++) & 1)]; + } while (sym2 < 0); + } + bit_buf >>= code_len; + num_bits -= code_len; + if (code_len == 0) + { + TINFL_CR_RETURN_FOREVER(54, TINFL_STATUS_FAILED); + } + + pOut_buf_cur[0] = (mz_uint8)counter; + if (sym2 & 256) + { + pOut_buf_cur++; + counter = sym2; + break; + } + pOut_buf_cur[1] = (mz_uint8)sym2; + pOut_buf_cur += 2; + } + } + if ((counter &= 511) == 256) + break; + + num_extra = s_length_extra[counter - 257]; + counter = s_length_base[counter - 257]; + if (num_extra) + { + mz_uint extra_bits; + TINFL_GET_BITS(25, extra_bits, num_extra); + counter += extra_bits; + } + + TINFL_HUFF_DECODE(26, dist, r->m_look_up[1], r->m_tree_1); + num_extra = s_dist_extra[dist]; + dist = s_dist_base[dist]; + if (num_extra) + { + mz_uint extra_bits; + TINFL_GET_BITS(27, extra_bits, num_extra); + dist += extra_bits; + } + + dist_from_out_buf_start = pOut_buf_cur - pOut_buf_start; + if ((dist == 0 || dist > dist_from_out_buf_start || dist_from_out_buf_start == 0) && (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF)) + { + TINFL_CR_RETURN_FOREVER(37, TINFL_STATUS_FAILED); + } + + pSrc = pOut_buf_start + ((dist_from_out_buf_start - dist) & out_buf_size_mask); + + if ((MZ_MAX(pOut_buf_cur, pSrc) + counter) > pOut_buf_end) + { + while (counter--) + { + while (pOut_buf_cur >= pOut_buf_end) + { + TINFL_CR_RETURN(53, TINFL_STATUS_HAS_MORE_OUTPUT); + } + *pOut_buf_cur++ = pOut_buf_start[(dist_from_out_buf_start++ - dist) & out_buf_size_mask]; + } + continue; + } +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES + else if ((counter >= 9) && (counter <= dist)) + { + const mz_uint8 *pSrc_end = pSrc + (counter & ~7); + do + { +#ifdef MINIZ_UNALIGNED_USE_MEMCPY + memcpy(pOut_buf_cur, pSrc, sizeof(mz_uint32) * 2); +#else + ((mz_uint32 *)pOut_buf_cur)[0] = ((const mz_uint32 *)pSrc)[0]; + ((mz_uint32 *)pOut_buf_cur)[1] = ((const mz_uint32 *)pSrc)[1]; +#endif + pOut_buf_cur += 8; + } while ((pSrc += 8) < pSrc_end); + if ((counter &= 7) < 3) + { + if (counter) + { + pOut_buf_cur[0] = pSrc[0]; + if (counter > 1) + pOut_buf_cur[1] = pSrc[1]; + pOut_buf_cur += counter; + } + continue; + } + } +#endif + while (counter > 2) + { + pOut_buf_cur[0] = pSrc[0]; + pOut_buf_cur[1] = pSrc[1]; + pOut_buf_cur[2] = pSrc[2]; + pOut_buf_cur += 3; + pSrc += 3; + counter -= 3; + } + if (counter > 0) + { + pOut_buf_cur[0] = pSrc[0]; + if (counter > 1) + pOut_buf_cur[1] = pSrc[1]; + pOut_buf_cur += counter; + } + } + } + } while (!(r->m_final & 1)); + + /* Ensure byte alignment and put back any bytes from the bitbuf if we've looked ahead too far on gzip, or other Deflate streams followed by arbitrary data. */ + /* I'm being super conservative here. A number of simplifications can be made to the byte alignment part, and the Adler32 check shouldn't ever need to worry about reading from the bitbuf now. */ + TINFL_SKIP_BITS(32, num_bits & 7); + while ((pIn_buf_cur > pIn_buf_next) && (num_bits >= 8)) + { + --pIn_buf_cur; + num_bits -= 8; + } + bit_buf &= ~(~(tinfl_bit_buf_t)0 << num_bits); + MZ_ASSERT(!num_bits); /* if this assert fires then we've read beyond the end of non-deflate/zlib streams with following data (such as gzip streams). */ + + if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) + { + for (counter = 0; counter < 4; ++counter) + { + mz_uint s; + if (num_bits) + TINFL_GET_BITS(41, s, 8); + else + TINFL_GET_BYTE(42, s); + r->m_z_adler32 = (r->m_z_adler32 << 8) | s; + } + } + TINFL_CR_RETURN_FOREVER(34, TINFL_STATUS_DONE); + + TINFL_CR_FINISH + + common_exit: + /* As long as we aren't telling the caller that we NEED more input to make forward progress: */ + /* Put back any bytes from the bitbuf in case we've looked ahead too far on gzip, or other Deflate streams followed by arbitrary data. */ + /* We need to be very careful here to NOT push back any bytes we definitely know we need to make forward progress, though, or we'll lock the caller up into an inf loop. */ + if ((status != TINFL_STATUS_NEEDS_MORE_INPUT) && (status != TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS)) + { + while ((pIn_buf_cur > pIn_buf_next) && (num_bits >= 8)) + { + --pIn_buf_cur; + num_bits -= 8; + } + } + r->m_num_bits = num_bits; + r->m_bit_buf = bit_buf & ~(~(tinfl_bit_buf_t)0 << num_bits); + r->m_dist = dist; + r->m_counter = counter; + r->m_num_extra = num_extra; + r->m_dist_from_out_buf_start = dist_from_out_buf_start; + *pIn_buf_size = pIn_buf_cur - pIn_buf_next; + *pOut_buf_size = pOut_buf_cur - pOut_buf_next; + if ((decomp_flags & (TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_COMPUTE_ADLER32)) && (status >= 0)) + { + const mz_uint8 *ptr = pOut_buf_next; + size_t buf_len = *pOut_buf_size; + mz_uint32 i, s1 = r->m_check_adler32 & 0xffff, s2 = r->m_check_adler32 >> 16; + size_t block_len = buf_len % 5552; + while (buf_len) + { + for (i = 0; i + 7 < block_len; i += 8, ptr += 8) + { + s1 += ptr[0], s2 += s1; + s1 += ptr[1], s2 += s1; + s1 += ptr[2], s2 += s1; + s1 += ptr[3], s2 += s1; + s1 += ptr[4], s2 += s1; + s1 += ptr[5], s2 += s1; + s1 += ptr[6], s2 += s1; + s1 += ptr[7], s2 += s1; + } + for (; i < block_len; ++i) + s1 += *ptr++, s2 += s1; + s1 %= 65521U, s2 %= 65521U; + buf_len -= block_len; + block_len = 5552; + } + r->m_check_adler32 = (s2 << 16) + s1; + if ((status == TINFL_STATUS_DONE) && (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) && (r->m_check_adler32 != r->m_z_adler32)) + status = TINFL_STATUS_ADLER32_MISMATCH; + } + return status; + } + + /* Higher level helper functions. */ + void *tinfl_decompress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags) + { + tinfl_decompressor decomp; + void *pBuf = NULL, *pNew_buf; + size_t src_buf_ofs = 0, out_buf_capacity = 0; + *pOut_len = 0; + tinfl_init(&decomp); + for (;;) + { + size_t src_buf_size = src_buf_len - src_buf_ofs, dst_buf_size = out_buf_capacity - *pOut_len, new_out_buf_capacity; + tinfl_status status = tinfl_decompress(&decomp, (const mz_uint8 *)pSrc_buf + src_buf_ofs, &src_buf_size, (mz_uint8 *)pBuf, pBuf ? (mz_uint8 *)pBuf + *pOut_len : NULL, &dst_buf_size, + (flags & ~TINFL_FLAG_HAS_MORE_INPUT) | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF); + if ((status < 0) || (status == TINFL_STATUS_NEEDS_MORE_INPUT)) + { + MZ_FREE(pBuf); + *pOut_len = 0; + return NULL; + } + src_buf_ofs += src_buf_size; + *pOut_len += dst_buf_size; + if (status == TINFL_STATUS_DONE) + break; + new_out_buf_capacity = out_buf_capacity * 2; + if (new_out_buf_capacity < 128) + new_out_buf_capacity = 128; + pNew_buf = MZ_REALLOC(pBuf, new_out_buf_capacity); + if (!pNew_buf) + { + MZ_FREE(pBuf); + *pOut_len = 0; + return NULL; + } + pBuf = pNew_buf; + out_buf_capacity = new_out_buf_capacity; + } + return pBuf; + } + + size_t tinfl_decompress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags) + { + tinfl_decompressor decomp; + tinfl_status status; + tinfl_init(&decomp); + status = tinfl_decompress(&decomp, (const mz_uint8 *)pSrc_buf, &src_buf_len, (mz_uint8 *)pOut_buf, (mz_uint8 *)pOut_buf, &out_buf_len, (flags & ~TINFL_FLAG_HAS_MORE_INPUT) | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF); + return (status != TINFL_STATUS_DONE) ? TINFL_DECOMPRESS_MEM_TO_MEM_FAILED : out_buf_len; + } + + int tinfl_decompress_mem_to_callback(const void *pIn_buf, size_t *pIn_buf_size, tinfl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags) + { + int result = 0; + tinfl_decompressor decomp; + mz_uint8 *pDict = (mz_uint8 *)MZ_MALLOC(TINFL_LZ_DICT_SIZE); + size_t in_buf_ofs = 0, dict_ofs = 0; + if (!pDict) + return TINFL_STATUS_FAILED; + memset(pDict, 0, TINFL_LZ_DICT_SIZE); + tinfl_init(&decomp); + for (;;) + { + size_t in_buf_size = *pIn_buf_size - in_buf_ofs, dst_buf_size = TINFL_LZ_DICT_SIZE - dict_ofs; + tinfl_status status = tinfl_decompress(&decomp, (const mz_uint8 *)pIn_buf + in_buf_ofs, &in_buf_size, pDict, pDict + dict_ofs, &dst_buf_size, + (flags & ~(TINFL_FLAG_HAS_MORE_INPUT | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF))); + in_buf_ofs += in_buf_size; + if ((dst_buf_size) && (!(*pPut_buf_func)(pDict + dict_ofs, (int)dst_buf_size, pPut_buf_user))) + break; + if (status != TINFL_STATUS_HAS_MORE_OUTPUT) + { + result = (status == TINFL_STATUS_DONE); + break; + } + dict_ofs = (dict_ofs + dst_buf_size) & (TINFL_LZ_DICT_SIZE - 1); + } + MZ_FREE(pDict); + *pIn_buf_size = in_buf_ofs; + return result; + } + +#ifndef MINIZ_NO_MALLOC + tinfl_decompressor *tinfl_decompressor_alloc(void) + { + tinfl_decompressor *pDecomp = (tinfl_decompressor *)MZ_MALLOC(sizeof(tinfl_decompressor)); + if (pDecomp) + tinfl_init(pDecomp); + return pDecomp; + } + + void tinfl_decompressor_free(tinfl_decompressor *pDecomp) + { + MZ_FREE(pDecomp); + } +#endif + +#ifdef __cplusplus +} +#endif + +#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ + /************************************************************************** + * + * Copyright 2013-2014 RAD Game Tools and Valve Software + * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + * Copyright 2016 Martin Raiber + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + **************************************************************************/ + + +#ifndef MINIZ_NO_ARCHIVE_APIS + +#ifdef __cplusplus +extern "C" +{ +#endif + + /* ------------------- .ZIP archive reading */ + +#ifdef MINIZ_NO_STDIO +#define MZ_FILE void * +#else +#include + +#if defined(_MSC_VER) || defined(__MINGW64__) || defined(__MINGW32__) + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef __cplusplus +#define MICROSOFT_WINDOWS_WINBASE_H_DEFINE_INTERLOCKED_CPLUSPLUS_OVERLOADS 0 +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +static WCHAR *mz_utf8z_to_widechar(const char *str) +{ + int reqChars = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + WCHAR *wStr = (WCHAR *)malloc(reqChars * sizeof(WCHAR)); + MultiByteToWideChar(CP_UTF8, 0, str, -1, wStr, reqChars); + return wStr; +} + +static FILE *mz_fopen(const char *pFilename, const char *pMode) +{ + WCHAR *wFilename = mz_utf8z_to_widechar(pFilename); + WCHAR *wMode = mz_utf8z_to_widechar(pMode); + FILE *pFile = NULL; + errno_t err = _wfopen_s(&pFile, wFilename, wMode); + free(wFilename); + free(wMode); + return err ? NULL : pFile; +} + +static FILE *mz_freopen(const char *pPath, const char *pMode, FILE *pStream) +{ + WCHAR *wPath = mz_utf8z_to_widechar(pPath); + WCHAR *wMode = mz_utf8z_to_widechar(pMode); + FILE *pFile = NULL; + errno_t err = _wfreopen_s(&pFile, wPath, wMode, pStream); + free(wPath); + free(wMode); + return err ? NULL : pFile; +} + +#if defined(__MINGW32__) +static int mz_stat(const char *path, struct _stat *buffer) +{ + WCHAR *wPath = mz_utf8z_to_widechar(path); + int res = _wstat(wPath, buffer); + free(wPath); + return res; +} +#else +static int mz_stat64(const char *path, struct __stat64 *buffer) +{ + WCHAR *wPath = mz_utf8z_to_widechar(path); + int res = _wstat64(wPath, buffer); + free(wPath); + return res; +} +#endif + +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN mz_fopen +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#define MZ_FTELL64 _ftelli64 +#define MZ_FSEEK64 _fseeki64 +#if defined(__MINGW32__) +#define MZ_FILE_STAT_STRUCT _stat +#define MZ_FILE_STAT mz_stat +#else +#define MZ_FILE_STAT_STRUCT _stat64 +#define MZ_FILE_STAT mz_stat64 +#endif +#define MZ_FFLUSH fflush +#define MZ_FREOPEN mz_freopen +#define MZ_DELETE_FILE remove + +#elif defined(__WATCOMC__) +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN(f, m) fopen(f, m) +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#define MZ_FTELL64 _ftelli64 +#define MZ_FSEEK64 _fseeki64 +#define MZ_FILE_STAT_STRUCT stat +#define MZ_FILE_STAT stat +#define MZ_FFLUSH fflush +#define MZ_FREOPEN(f, m, s) freopen(f, m, s) +#define MZ_DELETE_FILE remove + +#elif defined(__TINYC__) +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN(f, m) fopen(f, m) +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#define MZ_FTELL64 ftell +#define MZ_FSEEK64 fseek +#define MZ_FILE_STAT_STRUCT stat +#define MZ_FILE_STAT stat +#define MZ_FFLUSH fflush +#define MZ_FREOPEN(f, m, s) freopen(f, m, s) +#define MZ_DELETE_FILE remove + +#elif defined(__USE_LARGEFILE64) /* gcc, clang */ +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN(f, m) fopen64(f, m) +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#define MZ_FTELL64 ftello64 +#define MZ_FSEEK64 fseeko64 +#define MZ_FILE_STAT_STRUCT stat64 +#define MZ_FILE_STAT stat64 +#define MZ_FFLUSH fflush +#define MZ_FREOPEN(p, m, s) freopen64(p, m, s) +#define MZ_DELETE_FILE remove + +#elif defined(__APPLE__) || defined(__FreeBSD__) || (defined(__linux__) && defined(__x86_64__)) +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN(f, m) fopen(f, m) +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#define MZ_FTELL64 ftello +#define MZ_FSEEK64 fseeko +#define MZ_FILE_STAT_STRUCT stat +#define MZ_FILE_STAT stat +#define MZ_FFLUSH fflush +#define MZ_FREOPEN(p, m, s) freopen(p, m, s) +#define MZ_DELETE_FILE remove + +#else +#pragma message("Using fopen, ftello, fseeko, stat() etc. path for file I/O - this path may not support large files.") +#ifndef MINIZ_NO_TIME +#include +#endif +#define MZ_FOPEN(f, m) fopen(f, m) +#define MZ_FCLOSE fclose +#define MZ_FREAD fread +#define MZ_FWRITE fwrite +#ifdef __STRICT_ANSI__ +#define MZ_FTELL64 ftell +#define MZ_FSEEK64 fseek +#else +#define MZ_FTELL64 ftello +#define MZ_FSEEK64 fseeko +#endif +#define MZ_FILE_STAT_STRUCT stat +#define MZ_FILE_STAT stat +#define MZ_FFLUSH fflush +#define MZ_FREOPEN(f, m, s) freopen(f, m, s) +#define MZ_DELETE_FILE remove +#endif /* #ifdef _MSC_VER */ +#endif /* #ifdef MINIZ_NO_STDIO */ + +#define MZ_TOLOWER(c) ((((c) >= 'A') && ((c) <= 'Z')) ? ((c) - 'A' + 'a') : (c)) + + /* Various ZIP archive enums. To completely avoid cross platform compiler alignment and platform endian issues, miniz.c doesn't use structs for any of this stuff. */ + enum + { + /* ZIP archive identifiers and record sizes */ + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG = 0x06054b50, + MZ_ZIP_CENTRAL_DIR_HEADER_SIG = 0x02014b50, + MZ_ZIP_LOCAL_DIR_HEADER_SIG = 0x04034b50, + MZ_ZIP_LOCAL_DIR_HEADER_SIZE = 30, + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE = 46, + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE = 22, + + /* ZIP64 archive identifier and record sizes */ + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG = 0x06064b50, + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG = 0x07064b50, + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE = 56, + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE = 20, + MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID = 0x0001, + MZ_ZIP_DATA_DESCRIPTOR_ID = 0x08074b50, + MZ_ZIP_DATA_DESCRIPTER_SIZE64 = 24, + MZ_ZIP_DATA_DESCRIPTER_SIZE32 = 16, + + /* Central directory header record offsets */ + MZ_ZIP_CDH_SIG_OFS = 0, + MZ_ZIP_CDH_VERSION_MADE_BY_OFS = 4, + MZ_ZIP_CDH_VERSION_NEEDED_OFS = 6, + MZ_ZIP_CDH_BIT_FLAG_OFS = 8, + MZ_ZIP_CDH_METHOD_OFS = 10, + MZ_ZIP_CDH_FILE_TIME_OFS = 12, + MZ_ZIP_CDH_FILE_DATE_OFS = 14, + MZ_ZIP_CDH_CRC32_OFS = 16, + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS = 20, + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS = 24, + MZ_ZIP_CDH_FILENAME_LEN_OFS = 28, + MZ_ZIP_CDH_EXTRA_LEN_OFS = 30, + MZ_ZIP_CDH_COMMENT_LEN_OFS = 32, + MZ_ZIP_CDH_DISK_START_OFS = 34, + MZ_ZIP_CDH_INTERNAL_ATTR_OFS = 36, + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS = 38, + MZ_ZIP_CDH_LOCAL_HEADER_OFS = 42, + + /* Local directory header offsets */ + MZ_ZIP_LDH_SIG_OFS = 0, + MZ_ZIP_LDH_VERSION_NEEDED_OFS = 4, + MZ_ZIP_LDH_BIT_FLAG_OFS = 6, + MZ_ZIP_LDH_METHOD_OFS = 8, + MZ_ZIP_LDH_FILE_TIME_OFS = 10, + MZ_ZIP_LDH_FILE_DATE_OFS = 12, + MZ_ZIP_LDH_CRC32_OFS = 14, + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS = 18, + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS = 22, + MZ_ZIP_LDH_FILENAME_LEN_OFS = 26, + MZ_ZIP_LDH_EXTRA_LEN_OFS = 28, + MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR = 1 << 3, + + /* End of central directory offsets */ + MZ_ZIP_ECDH_SIG_OFS = 0, + MZ_ZIP_ECDH_NUM_THIS_DISK_OFS = 4, + MZ_ZIP_ECDH_NUM_DISK_CDIR_OFS = 6, + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS = 8, + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS = 10, + MZ_ZIP_ECDH_CDIR_SIZE_OFS = 12, + MZ_ZIP_ECDH_CDIR_OFS_OFS = 16, + MZ_ZIP_ECDH_COMMENT_SIZE_OFS = 20, + + /* ZIP64 End of central directory locator offsets */ + MZ_ZIP64_ECDL_SIG_OFS = 0, /* 4 bytes */ + MZ_ZIP64_ECDL_NUM_DISK_CDIR_OFS = 4, /* 4 bytes */ + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS = 8, /* 8 bytes */ + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS = 16, /* 4 bytes */ + + /* ZIP64 End of central directory header offsets */ + MZ_ZIP64_ECDH_SIG_OFS = 0, /* 4 bytes */ + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS = 4, /* 8 bytes */ + MZ_ZIP64_ECDH_VERSION_MADE_BY_OFS = 12, /* 2 bytes */ + MZ_ZIP64_ECDH_VERSION_NEEDED_OFS = 14, /* 2 bytes */ + MZ_ZIP64_ECDH_NUM_THIS_DISK_OFS = 16, /* 4 bytes */ + MZ_ZIP64_ECDH_NUM_DISK_CDIR_OFS = 20, /* 4 bytes */ + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS = 24, /* 8 bytes */ + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS = 32, /* 8 bytes */ + MZ_ZIP64_ECDH_CDIR_SIZE_OFS = 40, /* 8 bytes */ + MZ_ZIP64_ECDH_CDIR_OFS_OFS = 48, /* 8 bytes */ + MZ_ZIP_VERSION_MADE_BY_DOS_FILESYSTEM_ID = 0, + MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG = 0x10, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED = 1, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG = 32, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION = 64, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_LOCAL_DIR_IS_MASKED = 8192, + MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8 = 1 << 11 + }; + + typedef struct + { + void *m_p; + size_t m_size, m_capacity; + mz_uint m_element_size; + } mz_zip_array; + + struct mz_zip_internal_state_tag + { + mz_zip_array m_central_dir; + mz_zip_array m_central_dir_offsets; + mz_zip_array m_sorted_central_dir_offsets; + + /* The flags passed in when the archive is initially opened. */ + mz_uint32 m_init_flags; + + /* MZ_TRUE if the archive has a zip64 end of central directory headers, etc. */ + mz_bool m_zip64; + + /* MZ_TRUE if we found zip64 extended info in the central directory (m_zip64 will also be slammed to true too, even if we didn't find a zip64 end of central dir header, etc.) */ + mz_bool m_zip64_has_extended_info_fields; + + /* These fields are used by the file, FILE, memory, and memory/heap read/write helpers. */ + MZ_FILE *m_pFile; + mz_uint64 m_file_archive_start_ofs; + + void *m_pMem; + size_t m_mem_size; + size_t m_mem_capacity; + }; + +#define MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(array_ptr, element_size) (array_ptr)->m_element_size = element_size + +#if defined(DEBUG) || defined(_DEBUG) + static MZ_FORCEINLINE mz_uint mz_zip_array_range_check(const mz_zip_array *pArray, mz_uint index) + { + MZ_ASSERT(index < pArray->m_size); + return index; + } +#define MZ_ZIP_ARRAY_ELEMENT(array_ptr, element_type, index) ((element_type *)((array_ptr)->m_p))[mz_zip_array_range_check(array_ptr, index)] +#else +#define MZ_ZIP_ARRAY_ELEMENT(array_ptr, element_type, index) ((element_type *)((array_ptr)->m_p))[index] +#endif + + static MZ_FORCEINLINE void mz_zip_array_init(mz_zip_array *pArray, mz_uint32 element_size) + { + memset(pArray, 0, sizeof(mz_zip_array)); + pArray->m_element_size = element_size; + } + + static MZ_FORCEINLINE void mz_zip_array_clear(mz_zip_archive *pZip, mz_zip_array *pArray) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pArray->m_p); + memset(pArray, 0, sizeof(mz_zip_array)); + } + + static mz_bool mz_zip_array_ensure_capacity(mz_zip_archive *pZip, mz_zip_array *pArray, size_t min_new_capacity, mz_uint growing) + { + void *pNew_p; + size_t new_capacity = min_new_capacity; + MZ_ASSERT(pArray->m_element_size); + if (pArray->m_capacity >= min_new_capacity) + return MZ_TRUE; + if (growing) + { + new_capacity = MZ_MAX(1, pArray->m_capacity); + while (new_capacity < min_new_capacity) + new_capacity *= 2; + } + if (NULL == (pNew_p = pZip->m_pRealloc(pZip->m_pAlloc_opaque, pArray->m_p, pArray->m_element_size, new_capacity))) + return MZ_FALSE; + pArray->m_p = pNew_p; + pArray->m_capacity = new_capacity; + return MZ_TRUE; + } + + static MZ_FORCEINLINE mz_bool mz_zip_array_reserve(mz_zip_archive *pZip, mz_zip_array *pArray, size_t new_capacity, mz_uint growing) + { + if (new_capacity > pArray->m_capacity) + { + if (!mz_zip_array_ensure_capacity(pZip, pArray, new_capacity, growing)) + return MZ_FALSE; + } + return MZ_TRUE; + } + + static MZ_FORCEINLINE mz_bool mz_zip_array_resize(mz_zip_archive *pZip, mz_zip_array *pArray, size_t new_size, mz_uint growing) + { + if (new_size > pArray->m_capacity) + { + if (!mz_zip_array_ensure_capacity(pZip, pArray, new_size, growing)) + return MZ_FALSE; + } + pArray->m_size = new_size; + return MZ_TRUE; + } + + static MZ_FORCEINLINE mz_bool mz_zip_array_ensure_room(mz_zip_archive *pZip, mz_zip_array *pArray, size_t n) + { + return mz_zip_array_reserve(pZip, pArray, pArray->m_size + n, MZ_TRUE); + } + + static MZ_FORCEINLINE mz_bool mz_zip_array_push_back(mz_zip_archive *pZip, mz_zip_array *pArray, const void *pElements, size_t n) + { + size_t orig_size = pArray->m_size; + if (!mz_zip_array_resize(pZip, pArray, orig_size + n, MZ_TRUE)) + return MZ_FALSE; + if (n > 0) + memcpy((mz_uint8 *)pArray->m_p + orig_size * pArray->m_element_size, pElements, n * pArray->m_element_size); + return MZ_TRUE; + } + +#ifndef MINIZ_NO_TIME + static MZ_TIME_T mz_zip_dos_to_time_t(int dos_time, int dos_date) + { + struct tm tm; + memset(&tm, 0, sizeof(tm)); + tm.tm_isdst = -1; + tm.tm_year = ((dos_date >> 9) & 127) + 1980 - 1900; + tm.tm_mon = ((dos_date >> 5) & 15) - 1; + tm.tm_mday = dos_date & 31; + tm.tm_hour = (dos_time >> 11) & 31; + tm.tm_min = (dos_time >> 5) & 63; + tm.tm_sec = (dos_time << 1) & 62; + return mktime(&tm); + } + +#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS + static void mz_zip_time_t_to_dos_time(MZ_TIME_T time, mz_uint16 *pDOS_time, mz_uint16 *pDOS_date) + { +#ifdef _MSC_VER + struct tm tm_struct; + struct tm *tm = &tm_struct; + errno_t err = localtime_s(tm, &time); + if (err) + { + *pDOS_date = 0; + *pDOS_time = 0; + return; + } +#else + struct tm *tm = localtime(&time); +#endif /* #ifdef _MSC_VER */ + + *pDOS_time = (mz_uint16)(((tm->tm_hour) << 11) + ((tm->tm_min) << 5) + ((tm->tm_sec) >> 1)); + *pDOS_date = (mz_uint16)(((tm->tm_year + 1900 - 1980) << 9) + ((tm->tm_mon + 1) << 5) + tm->tm_mday); + } +#endif /* MINIZ_NO_ARCHIVE_WRITING_APIS */ + +#ifndef MINIZ_NO_STDIO +#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS + static mz_bool mz_zip_get_file_modified_time(const char *pFilename, MZ_TIME_T *pTime) + { + struct MZ_FILE_STAT_STRUCT file_stat; + + /* On Linux with x86 glibc, this call will fail on large files (I think >= 0x80000000 bytes) unless you compiled with _LARGEFILE64_SOURCE. Argh. */ + if (MZ_FILE_STAT(pFilename, &file_stat) != 0) + return MZ_FALSE; + + *pTime = file_stat.st_mtime; + + return MZ_TRUE; + } +#endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS*/ + + static mz_bool mz_zip_set_file_times(const char *pFilename, MZ_TIME_T access_time, MZ_TIME_T modified_time) + { + struct utimbuf t; + + memset(&t, 0, sizeof(t)); + t.actime = access_time; + t.modtime = modified_time; + + return !utime(pFilename, &t); + } +#endif /* #ifndef MINIZ_NO_STDIO */ +#endif /* #ifndef MINIZ_NO_TIME */ + + static MZ_FORCEINLINE mz_bool mz_zip_set_error(mz_zip_archive *pZip, mz_zip_error err_num) + { + if (pZip) + pZip->m_last_error = err_num; + return MZ_FALSE; + } + + static mz_bool mz_zip_reader_init_internal(mz_zip_archive *pZip, mz_uint flags) + { + (void)flags; + if ((!pZip) || (pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_INVALID)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!pZip->m_pAlloc) + pZip->m_pAlloc = miniz_def_alloc_func; + if (!pZip->m_pFree) + pZip->m_pFree = miniz_def_free_func; + if (!pZip->m_pRealloc) + pZip->m_pRealloc = miniz_def_realloc_func; + + pZip->m_archive_size = 0; + pZip->m_central_directory_file_ofs = 0; + pZip->m_total_files = 0; + pZip->m_last_error = MZ_ZIP_NO_ERROR; + + if (NULL == (pZip->m_pState = (mz_zip_internal_state *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_internal_state)))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + memset(pZip->m_pState, 0, sizeof(mz_zip_internal_state)); + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir, sizeof(mz_uint8)); + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir_offsets, sizeof(mz_uint32)); + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_sorted_central_dir_offsets, sizeof(mz_uint32)); + pZip->m_pState->m_init_flags = flags; + pZip->m_pState->m_zip64 = MZ_FALSE; + pZip->m_pState->m_zip64_has_extended_info_fields = MZ_FALSE; + + pZip->m_zip_mode = MZ_ZIP_MODE_READING; + + return MZ_TRUE; + } + + static MZ_FORCEINLINE mz_bool mz_zip_reader_filename_less(const mz_zip_array *pCentral_dir_array, const mz_zip_array *pCentral_dir_offsets, mz_uint l_index, mz_uint r_index) + { + const mz_uint8 *pL = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, l_index)), *pE; + const mz_uint8 *pR = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, r_index)); + mz_uint l_len = MZ_READ_LE16(pL + MZ_ZIP_CDH_FILENAME_LEN_OFS), r_len = MZ_READ_LE16(pR + MZ_ZIP_CDH_FILENAME_LEN_OFS); + mz_uint8 l = 0, r = 0; + pL += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; + pR += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; + pE = pL + MZ_MIN(l_len, r_len); + while (pL < pE) + { + if ((l = MZ_TOLOWER(*pL)) != (r = MZ_TOLOWER(*pR))) + break; + pL++; + pR++; + } + return (pL == pE) ? (l_len < r_len) : (l < r); + } + +#define MZ_SWAP_UINT32(a, b) \ + do \ + { \ + mz_uint32 t = a; \ + a = b; \ + b = t; \ + } \ + MZ_MACRO_END + + /* Heap sort of lowercased filenames, used to help accelerate plain central directory searches by mz_zip_reader_locate_file(). (Could also use qsort(), but it could allocate memory.) */ + static void mz_zip_reader_sort_central_dir_offsets_by_filename(mz_zip_archive *pZip) + { + mz_zip_internal_state *pState = pZip->m_pState; + const mz_zip_array *pCentral_dir_offsets = &pState->m_central_dir_offsets; + const mz_zip_array *pCentral_dir = &pState->m_central_dir; + mz_uint32 *pIndices; + mz_uint32 start, end; + const mz_uint32 size = pZip->m_total_files; + + if (size <= 1U) + return; + + pIndices = &MZ_ZIP_ARRAY_ELEMENT(&pState->m_sorted_central_dir_offsets, mz_uint32, 0); + + start = (size - 2U) >> 1U; + for (;;) + { + mz_uint64 child, root = start; + for (;;) + { + if ((child = (root << 1U) + 1U) >= size) + break; + child += (((child + 1U) < size) && (mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[child], pIndices[child + 1U]))); + if (!mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[root], pIndices[child])) + break; + MZ_SWAP_UINT32(pIndices[root], pIndices[child]); + root = child; + } + if (!start) + break; + start--; + } + + end = size - 1; + while (end > 0) + { + mz_uint64 child, root = 0; + MZ_SWAP_UINT32(pIndices[end], pIndices[0]); + for (;;) + { + if ((child = (root << 1U) + 1U) >= end) + break; + child += (((child + 1U) < end) && mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[child], pIndices[child + 1U])); + if (!mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[root], pIndices[child])) + break; + MZ_SWAP_UINT32(pIndices[root], pIndices[child]); + root = child; + } + end--; + } + } + + static mz_bool mz_zip_reader_locate_header_sig(mz_zip_archive *pZip, mz_uint32 record_sig, mz_uint32 record_size, mz_int64 *pOfs) + { + mz_int64 cur_file_ofs; + mz_uint32 buf_u32[4096 / sizeof(mz_uint32)]; + mz_uint8 *pBuf = (mz_uint8 *)buf_u32; + + /* Basic sanity checks - reject files which are too small */ + if (pZip->m_archive_size < record_size) + return MZ_FALSE; + + /* Find the record by scanning the file from the end towards the beginning. */ + cur_file_ofs = MZ_MAX((mz_int64)pZip->m_archive_size - (mz_int64)sizeof(buf_u32), 0); + for (;;) + { + int i, n = (int)MZ_MIN(sizeof(buf_u32), pZip->m_archive_size - cur_file_ofs); + + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, n) != (mz_uint)n) + return MZ_FALSE; + + for (i = n - 4; i >= 0; --i) + { + mz_uint s = MZ_READ_LE32(pBuf + i); + if (s == record_sig) + { + if ((pZip->m_archive_size - (cur_file_ofs + i)) >= record_size) + break; + } + } + + if (i >= 0) + { + cur_file_ofs += i; + break; + } + + /* Give up if we've searched the entire file, or we've gone back "too far" (~64kb) */ + if ((!cur_file_ofs) || ((pZip->m_archive_size - cur_file_ofs) >= ((mz_uint64)(MZ_UINT16_MAX) + record_size))) + return MZ_FALSE; + + cur_file_ofs = MZ_MAX(cur_file_ofs - (sizeof(buf_u32) - 3), 0); + } + + *pOfs = cur_file_ofs; + return MZ_TRUE; + } + + static mz_bool mz_zip_reader_eocd64_valid(mz_zip_archive *pZip, uint64_t offset, uint8_t *buf) + { + if (pZip->m_pRead(pZip->m_pIO_opaque, offset, buf, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) == MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) + { + if (MZ_READ_LE32(buf + MZ_ZIP64_ECDH_SIG_OFS) == MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG) + { + return MZ_TRUE; + } + } + + return MZ_FALSE; + } + + static mz_bool mz_zip_reader_read_central_dir(mz_zip_archive *pZip, mz_uint flags) + { + mz_uint cdir_size = 0, cdir_entries_on_this_disk = 0, num_this_disk = 0, cdir_disk_index = 0; + mz_uint64 cdir_ofs = 0, eocd_ofs = 0, archive_ofs = 0; + mz_int64 cur_file_ofs = 0; + const mz_uint8 *p; + + mz_uint32 buf_u32[4096 / sizeof(mz_uint32)]; + mz_uint8 *pBuf = (mz_uint8 *)buf_u32; + mz_bool sort_central_dir = ((flags & MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY) == 0); + mz_uint32 zip64_end_of_central_dir_locator_u32[(MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pZip64_locator = (mz_uint8 *)zip64_end_of_central_dir_locator_u32; + + mz_uint32 zip64_end_of_central_dir_header_u32[(MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pZip64_end_of_central_dir = (mz_uint8 *)zip64_end_of_central_dir_header_u32; + + mz_uint64 zip64_end_of_central_dir_ofs = 0; + + /* Basic sanity checks - reject files which are too small, and check the first 4 bytes of the file to make sure a local header is there. */ + if (pZip->m_archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + if (!mz_zip_reader_locate_header_sig(pZip, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE, &cur_file_ofs)) + return mz_zip_set_error(pZip, MZ_ZIP_FAILED_FINDING_CENTRAL_DIR); + + eocd_ofs = cur_file_ofs; + /* Read and verify the end of central directory record. */ + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + if (MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_SIG_OFS) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + if (cur_file_ofs >= (MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE)) + { + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs - MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE, pZip64_locator, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) == MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) + { + if (MZ_READ_LE32(pZip64_locator + MZ_ZIP64_ECDL_SIG_OFS) == MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG) + { + pZip->m_pState->m_zip64 = MZ_TRUE; + } + } + } + + if (pZip->m_pState->m_zip64) + { + /* Try locating the EOCD64 right before the EOCD64 locator. This works even + * when the effective start of the zip header is not yet known. */ + if (cur_file_ofs < MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE + + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + zip64_end_of_central_dir_ofs = cur_file_ofs - + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE - + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE; + + if (!mz_zip_reader_eocd64_valid(pZip, zip64_end_of_central_dir_ofs, + pZip64_end_of_central_dir)) + { + /* That failed, try reading where the locator tells us to. */ + zip64_end_of_central_dir_ofs = MZ_READ_LE64( + pZip64_locator + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS); + + if (zip64_end_of_central_dir_ofs > + (pZip->m_archive_size - MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE)) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + if (!mz_zip_reader_eocd64_valid(pZip, zip64_end_of_central_dir_ofs, + pZip64_end_of_central_dir)) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + } + } + + pZip->m_total_files = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS); + cdir_entries_on_this_disk = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS); + num_this_disk = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_NUM_THIS_DISK_OFS); + cdir_disk_index = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_NUM_DISK_CDIR_OFS); + cdir_size = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_SIZE_OFS); + cdir_ofs = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_OFS_OFS); + + if (pZip->m_pState->m_zip64) + { + mz_uint32 zip64_total_num_of_disks = MZ_READ_LE32(pZip64_locator + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS); + mz_uint64 zip64_cdir_total_entries = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS); + mz_uint64 zip64_cdir_total_entries_on_this_disk = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS); + mz_uint64 zip64_size_of_end_of_central_dir_record = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS); + mz_uint64 zip64_size_of_central_directory = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_SIZE_OFS); + + if (zip64_size_of_end_of_central_dir_record < (MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE - 12)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if (zip64_total_num_of_disks != 1U) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); + + /* Check for miniz's practical limits */ + if (zip64_cdir_total_entries > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + + pZip->m_total_files = (mz_uint32)zip64_cdir_total_entries; + + if (zip64_cdir_total_entries_on_this_disk > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + + cdir_entries_on_this_disk = (mz_uint32)zip64_cdir_total_entries_on_this_disk; + + /* Check for miniz's current practical limits (sorry, this should be enough for millions of files) */ + if (zip64_size_of_central_directory > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + + cdir_size = (mz_uint32)zip64_size_of_central_directory; + + num_this_disk = MZ_READ_LE32(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_NUM_THIS_DISK_OFS); + + cdir_disk_index = MZ_READ_LE32(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_NUM_DISK_CDIR_OFS); + + cdir_ofs = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_OFS_OFS); + } + + if (pZip->m_total_files != cdir_entries_on_this_disk) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); + + if (((num_this_disk | cdir_disk_index) != 0) && ((num_this_disk != 1) || (cdir_disk_index != 1))) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); + + if (cdir_size < (mz_uint64)pZip->m_total_files * MZ_ZIP_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if (cdir_size> pZip->m_archive_size || cdir_ofs > pZip->m_archive_size - cdir_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if (eocd_ofs < cdir_ofs + cdir_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + /* The end of central dir follows the central dir, unless the zip file has + * some trailing data (e.g. it is appended to an executable file). */ + archive_ofs = eocd_ofs - (cdir_ofs + cdir_size); + if (pZip->m_pState->m_zip64) + { + if (archive_ofs < MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE + + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + archive_ofs -= MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE + + MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE; + } + + /* Update the archive start position, but only if not specified. */ + if ((pZip->m_zip_type == MZ_ZIP_TYPE_FILE || pZip->m_zip_type == MZ_ZIP_TYPE_CFILE || + pZip->m_zip_type == MZ_ZIP_TYPE_USER) && pZip->m_pState->m_file_archive_start_ofs == 0) + { + pZip->m_pState->m_file_archive_start_ofs = archive_ofs; + pZip->m_archive_size -= archive_ofs; + } + + pZip->m_central_directory_file_ofs = cdir_ofs; + + if (pZip->m_total_files) + { + mz_uint i, n; + /* Read the entire central directory into a heap block, and allocate another heap block to hold the unsorted central dir file record offsets, and possibly another to hold the sorted indices. */ + if ((!mz_zip_array_resize(pZip, &pZip->m_pState->m_central_dir, cdir_size, MZ_FALSE)) || + (!mz_zip_array_resize(pZip, &pZip->m_pState->m_central_dir_offsets, pZip->m_total_files, MZ_FALSE))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + if (sort_central_dir) + { + if (!mz_zip_array_resize(pZip, &pZip->m_pState->m_sorted_central_dir_offsets, pZip->m_total_files, MZ_FALSE)) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (pZip->m_pRead(pZip->m_pIO_opaque, cdir_ofs, pZip->m_pState->m_central_dir.m_p, cdir_size) != cdir_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + /* Now create an index into the central directory file records, do some basic sanity checking on each record */ + p = (const mz_uint8 *)pZip->m_pState->m_central_dir.m_p; + for (n = cdir_size, i = 0; i < pZip->m_total_files; ++i) + { + mz_uint total_header_size, disk_index, bit_flags, filename_size, ext_data_size; + mz_uint64 comp_size, decomp_size, local_header_ofs; + + if ((n < MZ_ZIP_CENTRAL_DIR_HEADER_SIZE) || (MZ_READ_LE32(p) != MZ_ZIP_CENTRAL_DIR_HEADER_SIG)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, i) = (mz_uint32)(p - (const mz_uint8 *)pZip->m_pState->m_central_dir.m_p); + + if (sort_central_dir) + MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_sorted_central_dir_offsets, mz_uint32, i) = i; + + comp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS); + decomp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS); + local_header_ofs = MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS); + filename_size = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + ext_data_size = MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS); + + if ((!pZip->m_pState->m_zip64_has_extended_info_fields) && + (ext_data_size) && + (MZ_MAX(MZ_MAX(comp_size, decomp_size), local_header_ofs) == MZ_UINT32_MAX)) + { + /* Attempt to find zip64 extended information field in the entry's extra data */ + mz_uint32 extra_size_remaining = ext_data_size; + + if (extra_size_remaining) + { + const mz_uint8 *pExtra_data; + void *buf = NULL; + + if (MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size + ext_data_size > n) + { + buf = MZ_MALLOC(ext_data_size); + if (buf == NULL) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + if (pZip->m_pRead(pZip->m_pIO_opaque, cdir_ofs + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size, buf, ext_data_size) != ext_data_size) + { + MZ_FREE(buf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + + pExtra_data = (mz_uint8 *)buf; + } + else + { + pExtra_data = p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size; + } + + do + { + mz_uint32 field_id; + mz_uint32 field_data_size; + + if (extra_size_remaining < (sizeof(mz_uint16) * 2)) + { + MZ_FREE(buf); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + field_id = MZ_READ_LE16(pExtra_data); + field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); + + if ((field_data_size + sizeof(mz_uint16) * 2) > extra_size_remaining) + { + MZ_FREE(buf); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) + { + /* Ok, the archive didn't have any zip64 headers but it uses a zip64 extended information field so mark it as zip64 anyway (this can occur with infozip's zip util when it reads compresses files from stdin). */ + pZip->m_pState->m_zip64 = MZ_TRUE; + pZip->m_pState->m_zip64_has_extended_info_fields = MZ_TRUE; + break; + } + + pExtra_data += sizeof(mz_uint16) * 2 + field_data_size; + extra_size_remaining = extra_size_remaining - sizeof(mz_uint16) * 2 - field_data_size; + } while (extra_size_remaining); + + MZ_FREE(buf); + } + } + + /* I've seen archives that aren't marked as zip64 that uses zip64 ext data, argh */ + if ((comp_size != MZ_UINT32_MAX) && (decomp_size != MZ_UINT32_MAX)) + { + if (((!MZ_READ_LE32(p + MZ_ZIP_CDH_METHOD_OFS)) && (decomp_size != comp_size)) || (decomp_size && !comp_size)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + disk_index = MZ_READ_LE16(p + MZ_ZIP_CDH_DISK_START_OFS); + if ((disk_index == MZ_UINT16_MAX) || ((disk_index != num_this_disk) && (disk_index != 1))) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); + + if (comp_size != MZ_UINT32_MAX) + { + if (((mz_uint64)MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS) + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + comp_size) > pZip->m_archive_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + bit_flags = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); + if (bit_flags & MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_LOCAL_DIR_IS_MASKED) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + + if ((total_header_size = MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_COMMENT_LEN_OFS)) > n) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + n -= total_header_size; + p += total_header_size; + } + } + + if (sort_central_dir) + mz_zip_reader_sort_central_dir_offsets_by_filename(pZip); + + return MZ_TRUE; + } + + void mz_zip_zero_struct(mz_zip_archive *pZip) + { + if (pZip) + MZ_CLEAR_PTR(pZip); + } + + static mz_bool mz_zip_reader_end_internal(mz_zip_archive *pZip, mz_bool set_last_error) + { + mz_bool status = MZ_TRUE; + + if (!pZip) + return MZ_FALSE; + + if ((!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (pZip->m_zip_mode != MZ_ZIP_MODE_READING)) + { + if (set_last_error) + pZip->m_last_error = MZ_ZIP_INVALID_PARAMETER; + + return MZ_FALSE; + } + + if (pZip->m_pState) + { + mz_zip_internal_state *pState = pZip->m_pState; + pZip->m_pState = NULL; + + mz_zip_array_clear(pZip, &pState->m_central_dir); + mz_zip_array_clear(pZip, &pState->m_central_dir_offsets); + mz_zip_array_clear(pZip, &pState->m_sorted_central_dir_offsets); + +#ifndef MINIZ_NO_STDIO + if (pState->m_pFile) + { + if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE) + { + if (MZ_FCLOSE(pState->m_pFile) == EOF) + { + if (set_last_error) + pZip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED; + status = MZ_FALSE; + } + } + pState->m_pFile = NULL; + } +#endif /* #ifndef MINIZ_NO_STDIO */ + + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + } + pZip->m_zip_mode = MZ_ZIP_MODE_INVALID; + + return status; + } + + mz_bool mz_zip_reader_end(mz_zip_archive *pZip) + { + return mz_zip_reader_end_internal(pZip, MZ_TRUE); + } + mz_bool mz_zip_reader_init(mz_zip_archive *pZip, mz_uint64 size, mz_uint flags) + { + if ((!pZip) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!mz_zip_reader_init_internal(pZip, flags)) + return MZ_FALSE; + + pZip->m_zip_type = MZ_ZIP_TYPE_USER; + pZip->m_archive_size = size; + + if (!mz_zip_reader_read_central_dir(pZip, flags)) + { + mz_zip_reader_end_internal(pZip, MZ_FALSE); + return MZ_FALSE; + } + + return MZ_TRUE; + } + + static size_t mz_zip_mem_read_func(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n) + { + mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; + size_t s = (file_ofs >= pZip->m_archive_size) ? 0 : (size_t)MZ_MIN(pZip->m_archive_size - file_ofs, n); + memcpy(pBuf, (const mz_uint8 *)pZip->m_pState->m_pMem + file_ofs, s); + return s; + } + + mz_bool mz_zip_reader_init_mem(mz_zip_archive *pZip, const void *pMem, size_t size, mz_uint flags) + { + if (!pMem) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + + if (!mz_zip_reader_init_internal(pZip, flags)) + return MZ_FALSE; + + pZip->m_zip_type = MZ_ZIP_TYPE_MEMORY; + pZip->m_archive_size = size; + pZip->m_pRead = mz_zip_mem_read_func; + pZip->m_pIO_opaque = pZip; + pZip->m_pNeeds_keepalive = NULL; + +#ifdef __cplusplus + pZip->m_pState->m_pMem = const_cast(pMem); +#else + pZip->m_pState->m_pMem = (void *)pMem; +#endif + + pZip->m_pState->m_mem_size = size; + + if (!mz_zip_reader_read_central_dir(pZip, flags)) + { + mz_zip_reader_end_internal(pZip, MZ_FALSE); + return MZ_FALSE; + } + + return MZ_TRUE; + } + +#ifndef MINIZ_NO_STDIO + static size_t mz_zip_file_read_func(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n) + { + mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; + mz_int64 cur_ofs = MZ_FTELL64(pZip->m_pState->m_pFile); + + file_ofs += pZip->m_pState->m_file_archive_start_ofs; + + if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pZip->m_pState->m_pFile, (mz_int64)file_ofs, SEEK_SET)))) + return 0; + + return MZ_FREAD(pBuf, 1, n, pZip->m_pState->m_pFile); + } + + mz_bool mz_zip_reader_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint32 flags) + { + return mz_zip_reader_init_file_v2(pZip, pFilename, flags, 0, 0); + } + + mz_bool mz_zip_reader_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags, mz_uint64 file_start_ofs, mz_uint64 archive_size) + { + mz_uint64 file_size; + MZ_FILE *pFile; + + if ((!pZip) || (!pFilename) || ((archive_size) && (archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE))) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pFile = MZ_FOPEN(pFilename, (flags & MZ_ZIP_FLAG_READ_ALLOW_WRITING ) ? "r+b" : "rb"); + if (!pFile) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + + file_size = archive_size; + if (!file_size) + { + if (MZ_FSEEK64(pFile, 0, SEEK_END)) + { + MZ_FCLOSE(pFile); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED); + } + + file_size = MZ_FTELL64(pFile); + } + + /* TODO: Better sanity check archive_size and the # of actual remaining bytes */ + + if (file_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + { + MZ_FCLOSE(pFile); + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + } + + if (!mz_zip_reader_init_internal(pZip, flags)) + { + MZ_FCLOSE(pFile); + return MZ_FALSE; + } + + pZip->m_zip_type = MZ_ZIP_TYPE_FILE; + pZip->m_pRead = mz_zip_file_read_func; + pZip->m_pIO_opaque = pZip; + pZip->m_pState->m_pFile = pFile; + pZip->m_archive_size = file_size; + pZip->m_pState->m_file_archive_start_ofs = file_start_ofs; + + if (!mz_zip_reader_read_central_dir(pZip, flags)) + { + mz_zip_reader_end_internal(pZip, MZ_FALSE); + return MZ_FALSE; + } + + return MZ_TRUE; + } + + mz_bool mz_zip_reader_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint64 archive_size, mz_uint flags) + { + mz_uint64 cur_file_ofs; + + if ((!pZip) || (!pFile)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + + cur_file_ofs = MZ_FTELL64(pFile); + + if (!archive_size) + { + if (MZ_FSEEK64(pFile, 0, SEEK_END)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED); + + archive_size = MZ_FTELL64(pFile) - cur_file_ofs; + + if (archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); + } + + if (!mz_zip_reader_init_internal(pZip, flags)) + return MZ_FALSE; + + pZip->m_zip_type = MZ_ZIP_TYPE_CFILE; + pZip->m_pRead = mz_zip_file_read_func; + + pZip->m_pIO_opaque = pZip; + pZip->m_pState->m_pFile = pFile; + pZip->m_archive_size = archive_size; + pZip->m_pState->m_file_archive_start_ofs = cur_file_ofs; + + if (!mz_zip_reader_read_central_dir(pZip, flags)) + { + mz_zip_reader_end_internal(pZip, MZ_FALSE); + return MZ_FALSE; + } + + return MZ_TRUE; + } + +#endif /* #ifndef MINIZ_NO_STDIO */ + + static MZ_FORCEINLINE const mz_uint8 *mz_zip_get_cdh(mz_zip_archive *pZip, mz_uint file_index) + { + if ((!pZip) || (!pZip->m_pState) || (file_index >= pZip->m_total_files)) + return NULL; + return &MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index)); + } + + mz_bool mz_zip_reader_is_file_encrypted(mz_zip_archive *pZip, mz_uint file_index) + { + mz_uint m_bit_flag; + const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); + if (!p) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return MZ_FALSE; + } + + m_bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); + return (m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION)) != 0; + } + + mz_bool mz_zip_reader_is_file_supported(mz_zip_archive *pZip, mz_uint file_index) + { + mz_uint bit_flag; + mz_uint method; + + const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); + if (!p) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return MZ_FALSE; + } + + method = MZ_READ_LE16(p + MZ_ZIP_CDH_METHOD_OFS); + bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); + + if ((method != 0) && (method != MZ_DEFLATED)) + { + mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); + return MZ_FALSE; + } + + if (bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION)) + { + mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + return MZ_FALSE; + } + + if (bit_flag & MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG) + { + mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); + return MZ_FALSE; + } + + return MZ_TRUE; + } + + mz_bool mz_zip_reader_is_file_a_directory(mz_zip_archive *pZip, mz_uint file_index) + { + mz_uint filename_len, attribute_mapping_id, external_attr; + const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); + if (!p) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return MZ_FALSE; + } + + filename_len = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + if (filename_len) + { + if (*(p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_len - 1) == '/') + return MZ_TRUE; + } + + /* Bugfix: This code was also checking if the internal attribute was non-zero, which wasn't correct. */ + /* Most/all zip writers (hopefully) set DOS file/directory attributes in the low 16-bits, so check for the DOS directory flag and ignore the source OS ID in the created by field. */ + /* FIXME: Remove this check? Is it necessary - we already check the filename. */ + attribute_mapping_id = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_MADE_BY_OFS) >> 8; + (void)attribute_mapping_id; + + external_attr = MZ_READ_LE32(p + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS); + if ((external_attr & MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG) != 0) + { + return MZ_TRUE; + } + + return MZ_FALSE; + } + + static mz_bool mz_zip_file_stat_internal(mz_zip_archive *pZip, mz_uint file_index, const mz_uint8 *pCentral_dir_header, mz_zip_archive_file_stat *pStat, mz_bool *pFound_zip64_extra_data) + { + mz_uint n; + const mz_uint8 *p = pCentral_dir_header; + + if (pFound_zip64_extra_data) + *pFound_zip64_extra_data = MZ_FALSE; + + if ((!p) || (!pStat)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + /* Extract fields from the central directory record. */ + pStat->m_file_index = file_index; + pStat->m_central_dir_ofs = MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index); + pStat->m_version_made_by = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_MADE_BY_OFS); + pStat->m_version_needed = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_NEEDED_OFS); + pStat->m_bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); + pStat->m_method = MZ_READ_LE16(p + MZ_ZIP_CDH_METHOD_OFS); +#ifndef MINIZ_NO_TIME + pStat->m_time = mz_zip_dos_to_time_t(MZ_READ_LE16(p + MZ_ZIP_CDH_FILE_TIME_OFS), MZ_READ_LE16(p + MZ_ZIP_CDH_FILE_DATE_OFS)); +#endif + pStat->m_crc32 = MZ_READ_LE32(p + MZ_ZIP_CDH_CRC32_OFS); + pStat->m_comp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS); + pStat->m_uncomp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS); + pStat->m_internal_attr = MZ_READ_LE16(p + MZ_ZIP_CDH_INTERNAL_ATTR_OFS); + pStat->m_external_attr = MZ_READ_LE32(p + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS); + pStat->m_local_header_ofs = MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS); + + /* Copy as much of the filename and comment as possible. */ + n = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + n = MZ_MIN(n, MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE - 1); + memcpy(pStat->m_filename, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, n); + pStat->m_filename[n] = '\0'; + + n = MZ_READ_LE16(p + MZ_ZIP_CDH_COMMENT_LEN_OFS); + n = MZ_MIN(n, MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE - 1); + pStat->m_comment_size = n; + memcpy(pStat->m_comment, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS), n); + pStat->m_comment[n] = '\0'; + + /* Set some flags for convienance */ + pStat->m_is_directory = mz_zip_reader_is_file_a_directory(pZip, file_index); + pStat->m_is_encrypted = mz_zip_reader_is_file_encrypted(pZip, file_index); + pStat->m_is_supported = mz_zip_reader_is_file_supported(pZip, file_index); + + /* See if we need to read any zip64 extended information fields. */ + /* Confusingly, these zip64 fields can be present even on non-zip64 archives (Debian zip on a huge files from stdin piped to stdout creates them). */ + if (MZ_MAX(MZ_MAX(pStat->m_comp_size, pStat->m_uncomp_size), pStat->m_local_header_ofs) == MZ_UINT32_MAX) + { + /* Attempt to find zip64 extended information field in the entry's extra data */ + mz_uint32 extra_size_remaining = MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS); + + if (extra_size_remaining) + { + const mz_uint8 *pExtra_data = p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + + do + { + mz_uint32 field_id; + mz_uint32 field_data_size; + + if (extra_size_remaining < (sizeof(mz_uint16) * 2)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + field_id = MZ_READ_LE16(pExtra_data); + field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); + + if ((field_data_size + sizeof(mz_uint16) * 2) > extra_size_remaining) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) + { + const mz_uint8 *pField_data = pExtra_data + sizeof(mz_uint16) * 2; + mz_uint32 field_data_remaining = field_data_size; + + if (pFound_zip64_extra_data) + *pFound_zip64_extra_data = MZ_TRUE; + + if (pStat->m_uncomp_size == MZ_UINT32_MAX) + { + if (field_data_remaining < sizeof(mz_uint64)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + pStat->m_uncomp_size = MZ_READ_LE64(pField_data); + pField_data += sizeof(mz_uint64); + field_data_remaining -= sizeof(mz_uint64); + } + + if (pStat->m_comp_size == MZ_UINT32_MAX) + { + if (field_data_remaining < sizeof(mz_uint64)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + pStat->m_comp_size = MZ_READ_LE64(pField_data); + pField_data += sizeof(mz_uint64); + field_data_remaining -= sizeof(mz_uint64); + } + + if (pStat->m_local_header_ofs == MZ_UINT32_MAX) + { + if (field_data_remaining < sizeof(mz_uint64)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + pStat->m_local_header_ofs = MZ_READ_LE64(pField_data); + pField_data += sizeof(mz_uint64); + field_data_remaining -= sizeof(mz_uint64); + } + + break; + } + + pExtra_data += sizeof(mz_uint16) * 2 + field_data_size; + extra_size_remaining = extra_size_remaining - sizeof(mz_uint16) * 2 - field_data_size; + } while (extra_size_remaining); + } + } + + return MZ_TRUE; + } + + static MZ_FORCEINLINE mz_bool mz_zip_string_equal(const char *pA, const char *pB, mz_uint len, mz_uint flags) + { + mz_uint i; + if (flags & MZ_ZIP_FLAG_CASE_SENSITIVE) + return 0 == memcmp(pA, pB, len); + for (i = 0; i < len; ++i) + if (MZ_TOLOWER(pA[i]) != MZ_TOLOWER(pB[i])) + return MZ_FALSE; + return MZ_TRUE; + } + + static MZ_FORCEINLINE int mz_zip_filename_compare(const mz_zip_array *pCentral_dir_array, const mz_zip_array *pCentral_dir_offsets, mz_uint l_index, const char *pR, mz_uint r_len) + { + const mz_uint8 *pL = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, l_index)), *pE; + mz_uint l_len = MZ_READ_LE16(pL + MZ_ZIP_CDH_FILENAME_LEN_OFS); + mz_uint8 l = 0, r = 0; + pL += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; + pE = pL + MZ_MIN(l_len, r_len); + while (pL < pE) + { + if ((l = MZ_TOLOWER(*pL)) != (r = MZ_TOLOWER(*pR))) + break; + pL++; + pR++; + } + return (pL == pE) ? (int)(l_len - r_len) : (l - r); + } + + static mz_bool mz_zip_locate_file_binary_search(mz_zip_archive *pZip, const char *pFilename, mz_uint32 *pIndex) + { + mz_zip_internal_state *pState = pZip->m_pState; + const mz_zip_array *pCentral_dir_offsets = &pState->m_central_dir_offsets; + const mz_zip_array *pCentral_dir = &pState->m_central_dir; + mz_uint32 *pIndices = &MZ_ZIP_ARRAY_ELEMENT(&pState->m_sorted_central_dir_offsets, mz_uint32, 0); + const mz_uint32 size = pZip->m_total_files; + const mz_uint filename_len = (mz_uint)strlen(pFilename); + + if (pIndex) + *pIndex = 0; + + if (size) + { + /* yes I could use uint32_t's, but then we would have to add some special case checks in the loop, argh, and */ + /* honestly the major expense here on 32-bit CPU's will still be the filename compare */ + mz_int64 l = 0, h = (mz_int64)size - 1; + + while (l <= h) + { + mz_int64 m = l + ((h - l) >> 1); + mz_uint32 file_index = pIndices[(mz_uint32)m]; + + int comp = mz_zip_filename_compare(pCentral_dir, pCentral_dir_offsets, file_index, pFilename, filename_len); + if (!comp) + { + if (pIndex) + *pIndex = file_index; + return MZ_TRUE; + } + else if (comp < 0) + l = m + 1; + else + h = m - 1; + } + } + + return mz_zip_set_error(pZip, MZ_ZIP_FILE_NOT_FOUND); + } + + int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags) + { + mz_uint32 index; + if (!mz_zip_reader_locate_file_v2(pZip, pName, pComment, flags, &index)) + return -1; + else + return (int)index; + } + + mz_bool mz_zip_reader_locate_file_v2(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags, mz_uint32 *pIndex) + { + mz_uint file_index; + size_t name_len, comment_len; + + if (pIndex) + *pIndex = 0; + + if ((!pZip) || (!pZip->m_pState) || (!pName)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + /* See if we can use a binary search */ + if (((pZip->m_pState->m_init_flags & MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY) == 0) && + (pZip->m_zip_mode == MZ_ZIP_MODE_READING) && + ((flags & (MZ_ZIP_FLAG_IGNORE_PATH | MZ_ZIP_FLAG_CASE_SENSITIVE)) == 0) && (!pComment) && (pZip->m_pState->m_sorted_central_dir_offsets.m_size)) + { + return mz_zip_locate_file_binary_search(pZip, pName, pIndex); + } + + /* Locate the entry by scanning the entire central directory */ + name_len = strlen(pName); + if (name_len > MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + comment_len = pComment ? strlen(pComment) : 0; + if (comment_len > MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + for (file_index = 0; file_index < pZip->m_total_files; file_index++) + { + const mz_uint8 *pHeader = &MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index)); + mz_uint filename_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_FILENAME_LEN_OFS); + const char *pFilename = (const char *)pHeader + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; + if (filename_len < name_len) + continue; + if (comment_len) + { + mz_uint file_extra_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_EXTRA_LEN_OFS), file_comment_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_COMMENT_LEN_OFS); + const char *pFile_comment = pFilename + filename_len + file_extra_len; + if ((file_comment_len != comment_len) || (!mz_zip_string_equal(pComment, pFile_comment, file_comment_len, flags))) + continue; + } + if ((flags & MZ_ZIP_FLAG_IGNORE_PATH) && (filename_len)) + { + int ofs = filename_len - 1; + do + { + if ((pFilename[ofs] == '/') || (pFilename[ofs] == '\\') || (pFilename[ofs] == ':')) + break; + } while (--ofs >= 0); + ofs++; + pFilename += ofs; + filename_len -= ofs; + } + if ((filename_len == name_len) && (mz_zip_string_equal(pName, pFilename, filename_len, flags))) + { + if (pIndex) + *pIndex = file_index; + return MZ_TRUE; + } + } + + return mz_zip_set_error(pZip, MZ_ZIP_FILE_NOT_FOUND); + } + + static mz_bool mz_zip_reader_extract_to_mem_no_alloc1(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size, const mz_zip_archive_file_stat *st) + { + int status = TINFL_STATUS_DONE; + mz_uint64 needed_size, cur_file_ofs, comp_remaining, out_buf_ofs = 0, read_buf_size, read_buf_ofs = 0, read_buf_avail; + mz_zip_archive_file_stat file_stat; + void *pRead_buf; + mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; + tinfl_decompressor inflator; + + if ((!pZip) || (!pZip->m_pState) || ((buf_size) && (!pBuf)) || ((user_read_buf_size) && (!pUser_read_buf)) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (st) + { + file_stat = *st; + } + else if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) + return MZ_FALSE; + + /* A directory or zero length file */ + if ((file_stat.m_is_directory) || (!file_stat.m_comp_size)) + return MZ_TRUE; + + /* Encryption and patch files are not supported. */ + if (file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + + /* This function only supports decompressing stored and deflate. */ + if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); + + /* Ensure supplied output buffer is large enough. */ + needed_size = (flags & MZ_ZIP_FLAG_COMPRESSED_DATA) ? file_stat.m_comp_size : file_stat.m_uncomp_size; + if (buf_size < needed_size) + return mz_zip_set_error(pZip, MZ_ZIP_BUF_TOO_SMALL); + + /* Read and parse the local directory entry. */ + cur_file_ofs = file_stat.m_local_header_ofs; + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + cur_file_ofs += (mz_uint64)(MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); + if ((cur_file_ofs + file_stat.m_comp_size) > pZip->m_archive_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!file_stat.m_method)) + { + /* The file is stored or the caller has requested the compressed data. */ + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, (size_t)needed_size) != needed_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) == 0) + { + if (mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, (size_t)file_stat.m_uncomp_size) != file_stat.m_crc32) + return mz_zip_set_error(pZip, MZ_ZIP_CRC_CHECK_FAILED); + } +#endif + + return MZ_TRUE; + } + + /* Decompress the file either directly from memory or from a file input buffer. */ + tinfl_init(&inflator); + + if (pZip->m_pState->m_pMem) + { + /* Read directly from the archive in memory. */ + pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + cur_file_ofs; + read_buf_size = read_buf_avail = file_stat.m_comp_size; + comp_remaining = 0; + } + else if (pUser_read_buf) + { + /* Use a user provided read buffer. */ + if (!user_read_buf_size) + return MZ_FALSE; + pRead_buf = (mz_uint8 *)pUser_read_buf; + read_buf_size = user_read_buf_size; + read_buf_avail = 0; + comp_remaining = file_stat.m_comp_size; + } + else + { + /* Temporarily allocate a read buffer. */ + read_buf_size = MZ_MIN(file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE); + if (((sizeof(size_t) == sizeof(mz_uint32))) && (read_buf_size > 0x7FFFFFFF)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (NULL == (pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)read_buf_size))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + read_buf_avail = 0; + comp_remaining = file_stat.m_comp_size; + } + + do + { + /* The size_t cast here should be OK because we've verified that the output buffer is >= file_stat.m_uncomp_size above */ + size_t in_buf_size, out_buf_size = (size_t)(file_stat.m_uncomp_size - out_buf_ofs); + if ((!read_buf_avail) && (!pZip->m_pState->m_pMem)) + { + read_buf_avail = MZ_MIN(read_buf_size, comp_remaining); + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) + { + status = TINFL_STATUS_FAILED; + mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED); + break; + } + cur_file_ofs += read_buf_avail; + comp_remaining -= read_buf_avail; + read_buf_ofs = 0; + } + in_buf_size = (size_t)read_buf_avail; + status = tinfl_decompress(&inflator, (mz_uint8 *)pRead_buf + read_buf_ofs, &in_buf_size, (mz_uint8 *)pBuf, (mz_uint8 *)pBuf + out_buf_ofs, &out_buf_size, TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF | (comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0)); + read_buf_avail -= in_buf_size; + read_buf_ofs += in_buf_size; + out_buf_ofs += out_buf_size; + } while (status == TINFL_STATUS_NEEDS_MORE_INPUT); + + if (status == TINFL_STATUS_DONE) + { + /* Make sure the entire file was decompressed, and check its CRC. */ + if (out_buf_ofs != file_stat.m_uncomp_size) + { + mz_zip_set_error(pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE); + status = TINFL_STATUS_FAILED; + } +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + else if (mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, (size_t)file_stat.m_uncomp_size) != file_stat.m_crc32) + { + mz_zip_set_error(pZip, MZ_ZIP_CRC_CHECK_FAILED); + status = TINFL_STATUS_FAILED; + } +#endif + } + + if ((!pZip->m_pState->m_pMem) && (!pUser_read_buf)) + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + + return status == TINFL_STATUS_DONE; + } + + mz_bool mz_zip_reader_extract_to_mem_no_alloc(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size) + { + return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, pUser_read_buf, user_read_buf_size, NULL); + } + + mz_bool mz_zip_reader_extract_file_to_mem_no_alloc(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size) + { + mz_uint32 file_index; + if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) + return MZ_FALSE; + return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, pUser_read_buf, user_read_buf_size, NULL); + } + + mz_bool mz_zip_reader_extract_to_mem(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags) + { + return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, NULL, 0, NULL); + } + + mz_bool mz_zip_reader_extract_file_to_mem(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags) + { + return mz_zip_reader_extract_file_to_mem_no_alloc(pZip, pFilename, pBuf, buf_size, flags, NULL, 0); + } + + void *mz_zip_reader_extract_to_heap(mz_zip_archive *pZip, mz_uint file_index, size_t *pSize, mz_uint flags) + { + mz_zip_archive_file_stat file_stat; + mz_uint64 alloc_size; + void *pBuf; + + if (pSize) + *pSize = 0; + + if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) + return NULL; + + alloc_size = (flags & MZ_ZIP_FLAG_COMPRESSED_DATA) ? file_stat.m_comp_size : file_stat.m_uncomp_size; + if (((sizeof(size_t) == sizeof(mz_uint32))) && (alloc_size > 0x7FFFFFFF)) + { + mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + return NULL; + } + + if (NULL == (pBuf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)alloc_size))) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + return NULL; + } + + if (!mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, (size_t)alloc_size, flags, NULL, 0, &file_stat)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return NULL; + } + + if (pSize) + *pSize = (size_t)alloc_size; + return pBuf; + } + + void *mz_zip_reader_extract_file_to_heap(mz_zip_archive *pZip, const char *pFilename, size_t *pSize, mz_uint flags) + { + mz_uint32 file_index; + if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) + { + if (pSize) + *pSize = 0; + return MZ_FALSE; + } + return mz_zip_reader_extract_to_heap(pZip, file_index, pSize, flags); + } + + mz_bool mz_zip_reader_extract_to_callback(mz_zip_archive *pZip, mz_uint file_index, mz_file_write_func pCallback, void *pOpaque, mz_uint flags) + { + int status = TINFL_STATUS_DONE; +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + mz_uint file_crc32 = MZ_CRC32_INIT; +#endif + mz_uint64 read_buf_size, read_buf_ofs = 0, read_buf_avail, comp_remaining, out_buf_ofs = 0, cur_file_ofs; + mz_zip_archive_file_stat file_stat; + void *pRead_buf = NULL; + void *pWrite_buf = NULL; + mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; + + if ((!pZip) || (!pZip->m_pState) || (!pCallback) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) + return MZ_FALSE; + + /* A directory or zero length file */ + if ((file_stat.m_is_directory) || (!file_stat.m_comp_size)) + return MZ_TRUE; + + /* Encryption and patch files are not supported. */ + if (file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + + /* This function only supports decompressing stored and deflate. */ + if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); + + /* Read and do some minimal validation of the local directory entry (this doesn't crack the zip64 stuff, which we already have from the central dir) */ + cur_file_ofs = file_stat.m_local_header_ofs; + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + cur_file_ofs += (mz_uint64)(MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); + if ((cur_file_ofs + file_stat.m_comp_size) > pZip->m_archive_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + /* Decompress the file either directly from memory or from a file input buffer. */ + if (pZip->m_pState->m_pMem) + { + pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + cur_file_ofs; + read_buf_size = read_buf_avail = file_stat.m_comp_size; + comp_remaining = 0; + } + else + { + read_buf_size = MZ_MIN(file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE); + if (NULL == (pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)read_buf_size))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + read_buf_avail = 0; + comp_remaining = file_stat.m_comp_size; + } + + if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!file_stat.m_method)) + { + /* The file is stored or the caller has requested the compressed data. */ + if (pZip->m_pState->m_pMem) + { + if (((sizeof(size_t) == sizeof(mz_uint32))) && (file_stat.m_comp_size > MZ_UINT32_MAX)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (pCallback(pOpaque, out_buf_ofs, pRead_buf, (size_t)file_stat.m_comp_size) != file_stat.m_comp_size) + { + mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED); + status = TINFL_STATUS_FAILED; + } + else if (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) + { +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + file_crc32 = (mz_uint32)mz_crc32(file_crc32, (const mz_uint8 *)pRead_buf, (size_t)file_stat.m_comp_size); +#endif + } + + cur_file_ofs += file_stat.m_comp_size; + out_buf_ofs += file_stat.m_comp_size; + comp_remaining = 0; + } + else + { + while (comp_remaining) + { + read_buf_avail = MZ_MIN(read_buf_size, comp_remaining); + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + status = TINFL_STATUS_FAILED; + break; + } + +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + if (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) + { + file_crc32 = (mz_uint32)mz_crc32(file_crc32, (const mz_uint8 *)pRead_buf, (size_t)read_buf_avail); + } +#endif + + if (pCallback(pOpaque, out_buf_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) + { + mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED); + status = TINFL_STATUS_FAILED; + break; + } + + cur_file_ofs += read_buf_avail; + out_buf_ofs += read_buf_avail; + comp_remaining -= read_buf_avail; + } + } + } + else + { + tinfl_decompressor inflator; + tinfl_init(&inflator); + + if (NULL == (pWrite_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, TINFL_LZ_DICT_SIZE))) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + status = TINFL_STATUS_FAILED; + } + else + { + do + { + mz_uint8 *pWrite_buf_cur = (mz_uint8 *)pWrite_buf + (out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); + size_t in_buf_size, out_buf_size = TINFL_LZ_DICT_SIZE - (out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); + if ((!read_buf_avail) && (!pZip->m_pState->m_pMem)) + { + read_buf_avail = MZ_MIN(read_buf_size, comp_remaining); + if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + status = TINFL_STATUS_FAILED; + break; + } + cur_file_ofs += read_buf_avail; + comp_remaining -= read_buf_avail; + read_buf_ofs = 0; + } + + in_buf_size = (size_t)read_buf_avail; + status = tinfl_decompress(&inflator, (const mz_uint8 *)pRead_buf + read_buf_ofs, &in_buf_size, (mz_uint8 *)pWrite_buf, pWrite_buf_cur, &out_buf_size, comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0); + read_buf_avail -= in_buf_size; + read_buf_ofs += in_buf_size; + + if (out_buf_size) + { + if (pCallback(pOpaque, out_buf_ofs, pWrite_buf_cur, out_buf_size) != out_buf_size) + { + mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED); + status = TINFL_STATUS_FAILED; + break; + } + +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + file_crc32 = (mz_uint32)mz_crc32(file_crc32, pWrite_buf_cur, out_buf_size); +#endif + if ((out_buf_ofs += out_buf_size) > file_stat.m_uncomp_size) + { + mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED); + status = TINFL_STATUS_FAILED; + break; + } + } + } while ((status == TINFL_STATUS_NEEDS_MORE_INPUT) || (status == TINFL_STATUS_HAS_MORE_OUTPUT)); + } + } + + if ((status == TINFL_STATUS_DONE) && (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA))) + { + /* Make sure the entire file was decompressed, and check its CRC. */ + if (out_buf_ofs != file_stat.m_uncomp_size) + { + mz_zip_set_error(pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE); + status = TINFL_STATUS_FAILED; + } +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + else if (file_crc32 != file_stat.m_crc32) + { + mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED); + status = TINFL_STATUS_FAILED; + } +#endif + } + + if (!pZip->m_pState->m_pMem) + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + + if (pWrite_buf) + pZip->m_pFree(pZip->m_pAlloc_opaque, pWrite_buf); + + return status == TINFL_STATUS_DONE; + } + + mz_bool mz_zip_reader_extract_file_to_callback(mz_zip_archive *pZip, const char *pFilename, mz_file_write_func pCallback, void *pOpaque, mz_uint flags) + { + mz_uint32 file_index; + if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) + return MZ_FALSE; + + return mz_zip_reader_extract_to_callback(pZip, file_index, pCallback, pOpaque, flags); + } + + mz_zip_reader_extract_iter_state *mz_zip_reader_extract_iter_new(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags) + { + mz_zip_reader_extract_iter_state *pState; + mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; + + /* Argument sanity check */ + if ((!pZip) || (!pZip->m_pState)) + return NULL; + + /* Allocate an iterator status structure */ + pState = (mz_zip_reader_extract_iter_state *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_reader_extract_iter_state)); + if (!pState) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + return NULL; + } + + /* Fetch file details */ + if (!mz_zip_reader_file_stat(pZip, file_index, &pState->file_stat)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + /* Encryption and patch files are not supported. */ + if (pState->file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG)) + { + mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + /* This function only supports decompressing stored and deflate. */ + if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (pState->file_stat.m_method != 0) && (pState->file_stat.m_method != MZ_DEFLATED)) + { + mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + /* Init state - save args */ + pState->pZip = pZip; + pState->flags = flags; + + /* Init state - reset variables to defaults */ + pState->status = TINFL_STATUS_DONE; +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + pState->file_crc32 = MZ_CRC32_INIT; +#endif + pState->read_buf_ofs = 0; + pState->out_buf_ofs = 0; + pState->pRead_buf = NULL; + pState->pWrite_buf = NULL; + pState->out_blk_remain = 0; + + /* Read and parse the local directory entry. */ + pState->cur_file_ofs = pState->file_stat.m_local_header_ofs; + if (pZip->m_pRead(pZip->m_pIO_opaque, pState->cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + pState->cur_file_ofs += (mz_uint64)(MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); + if ((pState->cur_file_ofs + pState->file_stat.m_comp_size) > pZip->m_archive_size) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + + /* Decompress the file either directly from memory or from a file input buffer. */ + if (pZip->m_pState->m_pMem) + { + pState->pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + pState->cur_file_ofs; + pState->read_buf_size = pState->read_buf_avail = pState->file_stat.m_comp_size; + pState->comp_remaining = pState->file_stat.m_comp_size; + } + else + { + if (!((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method))) + { + /* Decompression required, therefore intermediate read buffer required */ + pState->read_buf_size = MZ_MIN(pState->file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE); + if (NULL == (pState->pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)pState->read_buf_size))) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + } + else + { + /* Decompression not required - we will be reading directly into user buffer, no temp buf required */ + pState->read_buf_size = 0; + } + pState->read_buf_avail = 0; + pState->comp_remaining = pState->file_stat.m_comp_size; + } + + if (!((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method))) + { + /* Decompression required, init decompressor */ + tinfl_init(&pState->inflator); + + /* Allocate write buffer */ + if (NULL == (pState->pWrite_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, TINFL_LZ_DICT_SIZE))) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + if (pState->pRead_buf) + pZip->m_pFree(pZip->m_pAlloc_opaque, pState->pRead_buf); + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + return NULL; + } + } + + return pState; + } + + mz_zip_reader_extract_iter_state *mz_zip_reader_extract_file_iter_new(mz_zip_archive *pZip, const char *pFilename, mz_uint flags) + { + mz_uint32 file_index; + + /* Locate file index by name */ + if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) + return NULL; + + /* Construct iterator */ + return mz_zip_reader_extract_iter_new(pZip, file_index, flags); + } + + size_t mz_zip_reader_extract_iter_read(mz_zip_reader_extract_iter_state *pState, void *pvBuf, size_t buf_size) + { + size_t copied_to_caller = 0; + + /* Argument sanity check */ + if ((!pState) || (!pState->pZip) || (!pState->pZip->m_pState) || (!pvBuf)) + return 0; + + if ((pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method)) + { + /* The file is stored or the caller has requested the compressed data, calc amount to return. */ + copied_to_caller = (size_t)MZ_MIN(buf_size, pState->comp_remaining); + + /* Zip is in memory....or requires reading from a file? */ + if (pState->pZip->m_pState->m_pMem) + { + /* Copy data to caller's buffer */ + memcpy(pvBuf, pState->pRead_buf, copied_to_caller); + pState->pRead_buf = ((mz_uint8 *)pState->pRead_buf) + copied_to_caller; + } + else + { + /* Read directly into caller's buffer */ + if (pState->pZip->m_pRead(pState->pZip->m_pIO_opaque, pState->cur_file_ofs, pvBuf, copied_to_caller) != copied_to_caller) + { + /* Failed to read all that was asked for, flag failure and alert user */ + mz_zip_set_error(pState->pZip, MZ_ZIP_FILE_READ_FAILED); + pState->status = TINFL_STATUS_FAILED; + copied_to_caller = 0; + } + } + +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + /* Compute CRC if not returning compressed data only */ + if (!(pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) + pState->file_crc32 = (mz_uint32)mz_crc32(pState->file_crc32, (const mz_uint8 *)pvBuf, copied_to_caller); +#endif + + /* Advance offsets, dec counters */ + pState->cur_file_ofs += copied_to_caller; + pState->out_buf_ofs += copied_to_caller; + pState->comp_remaining -= copied_to_caller; + } + else + { + do + { + /* Calc ptr to write buffer - given current output pos and block size */ + mz_uint8 *pWrite_buf_cur = (mz_uint8 *)pState->pWrite_buf + (pState->out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); + + /* Calc max output size - given current output pos and block size */ + size_t in_buf_size, out_buf_size = TINFL_LZ_DICT_SIZE - (pState->out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); + + if (!pState->out_blk_remain) + { + /* Read more data from file if none available (and reading from file) */ + if ((!pState->read_buf_avail) && (!pState->pZip->m_pState->m_pMem)) + { + /* Calc read size */ + pState->read_buf_avail = MZ_MIN(pState->read_buf_size, pState->comp_remaining); + if (pState->pZip->m_pRead(pState->pZip->m_pIO_opaque, pState->cur_file_ofs, pState->pRead_buf, (size_t)pState->read_buf_avail) != pState->read_buf_avail) + { + mz_zip_set_error(pState->pZip, MZ_ZIP_FILE_READ_FAILED); + pState->status = TINFL_STATUS_FAILED; + break; + } + + /* Advance offsets, dec counters */ + pState->cur_file_ofs += pState->read_buf_avail; + pState->comp_remaining -= pState->read_buf_avail; + pState->read_buf_ofs = 0; + } + + /* Perform decompression */ + in_buf_size = (size_t)pState->read_buf_avail; + pState->status = tinfl_decompress(&pState->inflator, (const mz_uint8 *)pState->pRead_buf + pState->read_buf_ofs, &in_buf_size, (mz_uint8 *)pState->pWrite_buf, pWrite_buf_cur, &out_buf_size, pState->comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0); + pState->read_buf_avail -= in_buf_size; + pState->read_buf_ofs += in_buf_size; + + /* Update current output block size remaining */ + pState->out_blk_remain = out_buf_size; + } + + if (pState->out_blk_remain) + { + /* Calc amount to return. */ + size_t to_copy = MZ_MIN((buf_size - copied_to_caller), pState->out_blk_remain); + + /* Copy data to caller's buffer */ + memcpy((mz_uint8 *)pvBuf + copied_to_caller, pWrite_buf_cur, to_copy); + +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + /* Perform CRC */ + pState->file_crc32 = (mz_uint32)mz_crc32(pState->file_crc32, pWrite_buf_cur, to_copy); +#endif + + /* Decrement data consumed from block */ + pState->out_blk_remain -= to_copy; + + /* Inc output offset, while performing sanity check */ + if ((pState->out_buf_ofs += to_copy) > pState->file_stat.m_uncomp_size) + { + mz_zip_set_error(pState->pZip, MZ_ZIP_DECOMPRESSION_FAILED); + pState->status = TINFL_STATUS_FAILED; + break; + } + + /* Increment counter of data copied to caller */ + copied_to_caller += to_copy; + } + } while ((copied_to_caller < buf_size) && ((pState->status == TINFL_STATUS_NEEDS_MORE_INPUT) || (pState->status == TINFL_STATUS_HAS_MORE_OUTPUT))); + } + + /* Return how many bytes were copied into user buffer */ + return copied_to_caller; + } + + mz_bool mz_zip_reader_extract_iter_free(mz_zip_reader_extract_iter_state *pState) + { + int status; + + /* Argument sanity check */ + if ((!pState) || (!pState->pZip) || (!pState->pZip->m_pState)) + return MZ_FALSE; + + /* Was decompression completed and requested? */ + if ((pState->status == TINFL_STATUS_DONE) && (!(pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA))) + { + /* Make sure the entire file was decompressed, and check its CRC. */ + if (pState->out_buf_ofs != pState->file_stat.m_uncomp_size) + { + mz_zip_set_error(pState->pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE); + pState->status = TINFL_STATUS_FAILED; + } +#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + else if (pState->file_crc32 != pState->file_stat.m_crc32) + { + mz_zip_set_error(pState->pZip, MZ_ZIP_DECOMPRESSION_FAILED); + pState->status = TINFL_STATUS_FAILED; + } +#endif + } + + /* Free buffers */ + if (!pState->pZip->m_pState->m_pMem) + pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState->pRead_buf); + if (pState->pWrite_buf) + pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState->pWrite_buf); + + /* Save status */ + status = pState->status; + + /* Free context */ + pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState); + + return status == TINFL_STATUS_DONE; + } + +#ifndef MINIZ_NO_STDIO + static size_t mz_zip_file_write_callback(void *pOpaque, mz_uint64 ofs, const void *pBuf, size_t n) + { + (void)ofs; + + return MZ_FWRITE(pBuf, 1, n, (MZ_FILE *)pOpaque); + } + + mz_bool mz_zip_reader_extract_to_file(mz_zip_archive *pZip, mz_uint file_index, const char *pDst_filename, mz_uint flags) + { + mz_bool status; + mz_zip_archive_file_stat file_stat; + MZ_FILE *pFile; + + if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) + return MZ_FALSE; + + if ((file_stat.m_is_directory) || (!file_stat.m_is_supported)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); + + pFile = MZ_FOPEN(pDst_filename, "wb"); + if (!pFile) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + + status = mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_file_write_callback, pFile, flags); + + if (MZ_FCLOSE(pFile) == EOF) + { + if (status) + mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED); + + status = MZ_FALSE; + } + +#if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_STDIO) + if (status) + mz_zip_set_file_times(pDst_filename, file_stat.m_time, file_stat.m_time); +#endif + + return status; + } + + mz_bool mz_zip_reader_extract_file_to_file(mz_zip_archive *pZip, const char *pArchive_filename, const char *pDst_filename, mz_uint flags) + { + mz_uint32 file_index; + if (!mz_zip_reader_locate_file_v2(pZip, pArchive_filename, NULL, flags, &file_index)) + return MZ_FALSE; + + return mz_zip_reader_extract_to_file(pZip, file_index, pDst_filename, flags); + } + + mz_bool mz_zip_reader_extract_to_cfile(mz_zip_archive *pZip, mz_uint file_index, MZ_FILE *pFile, mz_uint flags) + { + mz_zip_archive_file_stat file_stat; + + if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) + return MZ_FALSE; + + if ((file_stat.m_is_directory) || (!file_stat.m_is_supported)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); + + return mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_file_write_callback, pFile, flags); + } + + mz_bool mz_zip_reader_extract_file_to_cfile(mz_zip_archive *pZip, const char *pArchive_filename, MZ_FILE *pFile, mz_uint flags) + { + mz_uint32 file_index; + if (!mz_zip_reader_locate_file_v2(pZip, pArchive_filename, NULL, flags, &file_index)) + return MZ_FALSE; + + return mz_zip_reader_extract_to_cfile(pZip, file_index, pFile, flags); + } +#endif /* #ifndef MINIZ_NO_STDIO */ + + static size_t mz_zip_compute_crc32_callback(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n) + { + mz_uint32 *p = (mz_uint32 *)pOpaque; + (void)file_ofs; + *p = (mz_uint32)mz_crc32(*p, (const mz_uint8 *)pBuf, n); + return n; + } + + mz_bool mz_zip_validate_file(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags) + { + mz_zip_archive_file_stat file_stat; + mz_zip_internal_state *pState; + const mz_uint8 *pCentral_dir_header; + mz_bool found_zip64_ext_data_in_cdir = MZ_FALSE; + mz_bool found_zip64_ext_data_in_ldir = MZ_FALSE; + mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; + mz_uint64 local_header_ofs = 0; + mz_uint32 local_header_filename_len, local_header_extra_len, local_header_crc32; + mz_uint64 local_header_comp_size, local_header_uncomp_size; + mz_uint32 uncomp_crc32 = MZ_CRC32_INIT; + mz_bool has_data_descriptor; + mz_uint32 local_header_bit_flags; + + mz_zip_array file_data_array; + mz_zip_array_init(&file_data_array, 1); + + if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (file_index > pZip->m_total_files) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + pCentral_dir_header = mz_zip_get_cdh(pZip, file_index); + + if (!mz_zip_file_stat_internal(pZip, file_index, pCentral_dir_header, &file_stat, &found_zip64_ext_data_in_cdir)) + return MZ_FALSE; + + /* A directory or zero length file */ + if ((file_stat.m_is_directory) || (!file_stat.m_uncomp_size)) + return MZ_TRUE; + + /* Encryption and patch files are not supported. */ + if (file_stat.m_is_encrypted) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); + + /* This function only supports stored and deflate. */ + if ((file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED)) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); + + if (!file_stat.m_is_supported) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); + + /* Read and parse the local directory entry. */ + local_header_ofs = file_stat.m_local_header_ofs; + if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + local_header_filename_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS); + local_header_extra_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); + local_header_comp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS); + local_header_uncomp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS); + local_header_crc32 = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_CRC32_OFS); + local_header_bit_flags = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_BIT_FLAG_OFS); + has_data_descriptor = (local_header_bit_flags & 8) != 0; + + if (local_header_filename_len != strlen(file_stat.m_filename)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if ((local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len + local_header_extra_len + file_stat.m_comp_size) > pZip->m_archive_size) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if (!mz_zip_array_resize(pZip, &file_data_array, MZ_MAX(local_header_filename_len, local_header_extra_len), MZ_FALSE)) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + goto handle_failure; + } + + if (local_header_filename_len) + { + if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE, file_data_array.m_p, local_header_filename_len) != local_header_filename_len) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + goto handle_failure; + } + + /* I've seen 1 archive that had the same pathname, but used backslashes in the local dir and forward slashes in the central dir. Do we care about this? For now, this case will fail validation. */ + if (memcmp(file_stat.m_filename, file_data_array.m_p, local_header_filename_len) != 0) + { + mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); + goto handle_failure; + } + } + + if ((local_header_extra_len) && ((local_header_comp_size == MZ_UINT32_MAX) || (local_header_uncomp_size == MZ_UINT32_MAX))) + { + mz_uint32 extra_size_remaining = local_header_extra_len; + const mz_uint8 *pExtra_data = (const mz_uint8 *)file_data_array.m_p; + + if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len, file_data_array.m_p, local_header_extra_len) != local_header_extra_len) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + goto handle_failure; + } + + do + { + mz_uint32 field_id, field_data_size, field_total_size; + + if (extra_size_remaining < (sizeof(mz_uint16) * 2)) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + goto handle_failure; + } + + field_id = MZ_READ_LE16(pExtra_data); + field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); + field_total_size = field_data_size + sizeof(mz_uint16) * 2; + + if (field_total_size > extra_size_remaining) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + goto handle_failure; + } + + if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) + { + const mz_uint8 *pSrc_field_data = pExtra_data + sizeof(mz_uint32); + + if (field_data_size < sizeof(mz_uint64) * 2) + { + mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + goto handle_failure; + } + + local_header_uncomp_size = MZ_READ_LE64(pSrc_field_data); + local_header_comp_size = MZ_READ_LE64(pSrc_field_data + sizeof(mz_uint64)); + + found_zip64_ext_data_in_ldir = MZ_TRUE; + break; + } + + pExtra_data += field_total_size; + extra_size_remaining -= field_total_size; + } while (extra_size_remaining); + } + + /* TODO: parse local header extra data when local_header_comp_size is 0xFFFFFFFF! (big_descriptor.zip) */ + /* I've seen zips in the wild with the data descriptor bit set, but proper local header values and bogus data descriptors */ + if ((has_data_descriptor) && (!local_header_comp_size) && (!local_header_crc32)) + { + mz_uint8 descriptor_buf[32]; + mz_bool has_id; + const mz_uint8 *pSrc; + mz_uint32 file_crc32; + mz_uint64 comp_size = 0, uncomp_size = 0; + + mz_uint32 num_descriptor_uint32s = ((pState->m_zip64) || (found_zip64_ext_data_in_ldir)) ? 6 : 4; + + if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len + local_header_extra_len + file_stat.m_comp_size, descriptor_buf, sizeof(mz_uint32) * num_descriptor_uint32s) != (sizeof(mz_uint32) * num_descriptor_uint32s)) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + goto handle_failure; + } + + has_id = (MZ_READ_LE32(descriptor_buf) == MZ_ZIP_DATA_DESCRIPTOR_ID); + pSrc = has_id ? (descriptor_buf + sizeof(mz_uint32)) : descriptor_buf; + + file_crc32 = MZ_READ_LE32(pSrc); + + if ((pState->m_zip64) || (found_zip64_ext_data_in_ldir)) + { + comp_size = MZ_READ_LE64(pSrc + sizeof(mz_uint32)); + uncomp_size = MZ_READ_LE64(pSrc + sizeof(mz_uint32) + sizeof(mz_uint64)); + } + else + { + comp_size = MZ_READ_LE32(pSrc + sizeof(mz_uint32)); + uncomp_size = MZ_READ_LE32(pSrc + sizeof(mz_uint32) + sizeof(mz_uint32)); + } + + if ((file_crc32 != file_stat.m_crc32) || (comp_size != file_stat.m_comp_size) || (uncomp_size != file_stat.m_uncomp_size)) + { + mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); + goto handle_failure; + } + } + else + { + if ((local_header_crc32 != file_stat.m_crc32) || (local_header_comp_size != file_stat.m_comp_size) || (local_header_uncomp_size != file_stat.m_uncomp_size)) + { + mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); + goto handle_failure; + } + } + + mz_zip_array_clear(pZip, &file_data_array); + + if ((flags & MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY) == 0) + { + if (!mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_compute_crc32_callback, &uncomp_crc32, 0)) + return MZ_FALSE; + + /* 1 more check to be sure, although the extract checks too. */ + if (uncomp_crc32 != file_stat.m_crc32) + { + mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); + return MZ_FALSE; + } + } + + return MZ_TRUE; + + handle_failure: + mz_zip_array_clear(pZip, &file_data_array); + return MZ_FALSE; + } + + mz_bool mz_zip_validate_archive(mz_zip_archive *pZip, mz_uint flags) + { + mz_zip_internal_state *pState; + mz_uint32 i; + + if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + /* Basic sanity checks */ + if (!pState->m_zip64) + { + if (pZip->m_total_files > MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + + if (pZip->m_archive_size > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + } + else + { + if (pState->m_central_dir.m_size >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + } + + for (i = 0; i < pZip->m_total_files; i++) + { + if (MZ_ZIP_FLAG_VALIDATE_LOCATE_FILE_FLAG & flags) + { + mz_uint32 found_index; + mz_zip_archive_file_stat stat; + + if (!mz_zip_reader_file_stat(pZip, i, &stat)) + return MZ_FALSE; + + if (!mz_zip_reader_locate_file_v2(pZip, stat.m_filename, NULL, 0, &found_index)) + return MZ_FALSE; + + /* This check can fail if there are duplicate filenames in the archive (which we don't check for when writing - that's up to the user) */ + if (found_index != i) + return mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); + } + + if (!mz_zip_validate_file(pZip, i, flags)) + return MZ_FALSE; + } + + return MZ_TRUE; + } + + mz_bool mz_zip_validate_mem_archive(const void *pMem, size_t size, mz_uint flags, mz_zip_error *pErr) + { + mz_bool success = MZ_TRUE; + mz_zip_archive zip; + mz_zip_error actual_err = MZ_ZIP_NO_ERROR; + + if ((!pMem) || (!size)) + { + if (pErr) + *pErr = MZ_ZIP_INVALID_PARAMETER; + return MZ_FALSE; + } + + mz_zip_zero_struct(&zip); + + if (!mz_zip_reader_init_mem(&zip, pMem, size, flags)) + { + if (pErr) + *pErr = zip.m_last_error; + return MZ_FALSE; + } + + if (!mz_zip_validate_archive(&zip, flags)) + { + actual_err = zip.m_last_error; + success = MZ_FALSE; + } + + if (!mz_zip_reader_end_internal(&zip, success)) + { + if (!actual_err) + actual_err = zip.m_last_error; + success = MZ_FALSE; + } + + if (pErr) + *pErr = actual_err; + + return success; + } + +#ifndef MINIZ_NO_STDIO + mz_bool mz_zip_validate_file_archive(const char *pFilename, mz_uint flags, mz_zip_error *pErr) + { + mz_bool success = MZ_TRUE; + mz_zip_archive zip; + mz_zip_error actual_err = MZ_ZIP_NO_ERROR; + + if (!pFilename) + { + if (pErr) + *pErr = MZ_ZIP_INVALID_PARAMETER; + return MZ_FALSE; + } + + mz_zip_zero_struct(&zip); + + if (!mz_zip_reader_init_file_v2(&zip, pFilename, flags, 0, 0)) + { + if (pErr) + *pErr = zip.m_last_error; + return MZ_FALSE; + } + + if (!mz_zip_validate_archive(&zip, flags)) + { + actual_err = zip.m_last_error; + success = MZ_FALSE; + } + + if (!mz_zip_reader_end_internal(&zip, success)) + { + if (!actual_err) + actual_err = zip.m_last_error; + success = MZ_FALSE; + } + + if (pErr) + *pErr = actual_err; + + return success; + } +#endif /* #ifndef MINIZ_NO_STDIO */ + + /* ------------------- .ZIP archive writing */ + +#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS + + static MZ_FORCEINLINE void mz_write_le16(mz_uint8 *p, mz_uint16 v) + { + p[0] = (mz_uint8)v; + p[1] = (mz_uint8)(v >> 8); + } + static MZ_FORCEINLINE void mz_write_le32(mz_uint8 *p, mz_uint32 v) + { + p[0] = (mz_uint8)v; + p[1] = (mz_uint8)(v >> 8); + p[2] = (mz_uint8)(v >> 16); + p[3] = (mz_uint8)(v >> 24); + } + static MZ_FORCEINLINE void mz_write_le64(mz_uint8 *p, mz_uint64 v) + { + mz_write_le32(p, (mz_uint32)v); + mz_write_le32(p + sizeof(mz_uint32), (mz_uint32)(v >> 32)); + } + +#define MZ_WRITE_LE16(p, v) mz_write_le16((mz_uint8 *)(p), (mz_uint16)(v)) +#define MZ_WRITE_LE32(p, v) mz_write_le32((mz_uint8 *)(p), (mz_uint32)(v)) +#define MZ_WRITE_LE64(p, v) mz_write_le64((mz_uint8 *)(p), (mz_uint64)(v)) + + static size_t mz_zip_heap_write_func(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n) + { + mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; + mz_zip_internal_state *pState = pZip->m_pState; + mz_uint64 new_size = MZ_MAX(file_ofs + n, pState->m_mem_size); + + if (!n) + return 0; + + /* An allocation this big is likely to just fail on 32-bit systems, so don't even go there. */ + if ((sizeof(size_t) == sizeof(mz_uint32)) && (new_size > 0x7FFFFFFF)) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE); + return 0; + } + + if (new_size > pState->m_mem_capacity) + { + void *pNew_block; + size_t new_capacity = MZ_MAX(64, pState->m_mem_capacity); + + while (new_capacity < new_size) + new_capacity *= 2; + + if (NULL == (pNew_block = pZip->m_pRealloc(pZip->m_pAlloc_opaque, pState->m_pMem, 1, new_capacity))) + { + mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + return 0; + } + + pState->m_pMem = pNew_block; + pState->m_mem_capacity = new_capacity; + } + memcpy((mz_uint8 *)pState->m_pMem + file_ofs, pBuf, n); + pState->m_mem_size = (size_t)new_size; + return n; + } + + static mz_bool mz_zip_writer_end_internal(mz_zip_archive *pZip, mz_bool set_last_error) + { + mz_zip_internal_state *pState; + mz_bool status = MZ_TRUE; + + if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || ((pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) && (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED))) + { + if (set_last_error) + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return MZ_FALSE; + } + + pState = pZip->m_pState; + pZip->m_pState = NULL; + mz_zip_array_clear(pZip, &pState->m_central_dir); + mz_zip_array_clear(pZip, &pState->m_central_dir_offsets); + mz_zip_array_clear(pZip, &pState->m_sorted_central_dir_offsets); + +#ifndef MINIZ_NO_STDIO + if (pState->m_pFile) + { + if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE) + { + if (MZ_FCLOSE(pState->m_pFile) == EOF) + { + if (set_last_error) + mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED); + status = MZ_FALSE; + } + } + + pState->m_pFile = NULL; + } +#endif /* #ifndef MINIZ_NO_STDIO */ + + if ((pZip->m_pWrite == mz_zip_heap_write_func) && (pState->m_pMem)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pState->m_pMem); + pState->m_pMem = NULL; + } + + pZip->m_pFree(pZip->m_pAlloc_opaque, pState); + pZip->m_zip_mode = MZ_ZIP_MODE_INVALID; + return status; + } + + mz_bool mz_zip_writer_init_v2(mz_zip_archive *pZip, mz_uint64 existing_size, mz_uint flags) + { + mz_bool zip64 = (flags & MZ_ZIP_FLAG_WRITE_ZIP64) != 0; + + if ((!pZip) || (pZip->m_pState) || (!pZip->m_pWrite) || (pZip->m_zip_mode != MZ_ZIP_MODE_INVALID)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) + { + if (!pZip->m_pRead) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + } + + if (pZip->m_file_offset_alignment) + { + /* Ensure user specified file offset alignment is a power of 2. */ + if (pZip->m_file_offset_alignment & (pZip->m_file_offset_alignment - 1)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + } + + if (!pZip->m_pAlloc) + pZip->m_pAlloc = miniz_def_alloc_func; + if (!pZip->m_pFree) + pZip->m_pFree = miniz_def_free_func; + if (!pZip->m_pRealloc) + pZip->m_pRealloc = miniz_def_realloc_func; + + pZip->m_archive_size = existing_size; + pZip->m_central_directory_file_ofs = 0; + pZip->m_total_files = 0; + + if (NULL == (pZip->m_pState = (mz_zip_internal_state *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_internal_state)))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + memset(pZip->m_pState, 0, sizeof(mz_zip_internal_state)); + + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir, sizeof(mz_uint8)); + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir_offsets, sizeof(mz_uint32)); + MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_sorted_central_dir_offsets, sizeof(mz_uint32)); + + pZip->m_pState->m_zip64 = zip64; + pZip->m_pState->m_zip64_has_extended_info_fields = zip64; + + pZip->m_zip_type = MZ_ZIP_TYPE_USER; + pZip->m_zip_mode = MZ_ZIP_MODE_WRITING; + + return MZ_TRUE; + } + + mz_bool mz_zip_writer_init(mz_zip_archive *pZip, mz_uint64 existing_size) + { + return mz_zip_writer_init_v2(pZip, existing_size, 0); + } + + mz_bool mz_zip_writer_init_heap_v2(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size, mz_uint flags) + { + pZip->m_pWrite = mz_zip_heap_write_func; + pZip->m_pNeeds_keepalive = NULL; + + if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) + pZip->m_pRead = mz_zip_mem_read_func; + + pZip->m_pIO_opaque = pZip; + + if (!mz_zip_writer_init_v2(pZip, size_to_reserve_at_beginning, flags)) + return MZ_FALSE; + + pZip->m_zip_type = MZ_ZIP_TYPE_HEAP; + + if (0 != (initial_allocation_size = MZ_MAX(initial_allocation_size, size_to_reserve_at_beginning))) + { + if (NULL == (pZip->m_pState->m_pMem = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, initial_allocation_size))) + { + mz_zip_writer_end_internal(pZip, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + pZip->m_pState->m_mem_capacity = initial_allocation_size; + } + + return MZ_TRUE; + } + + mz_bool mz_zip_writer_init_heap(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size) + { + return mz_zip_writer_init_heap_v2(pZip, size_to_reserve_at_beginning, initial_allocation_size, 0); + } + +#ifndef MINIZ_NO_STDIO + static size_t mz_zip_file_write_func(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n) + { + mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; + mz_int64 cur_ofs = MZ_FTELL64(pZip->m_pState->m_pFile); + + file_ofs += pZip->m_pState->m_file_archive_start_ofs; + + if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pZip->m_pState->m_pFile, (mz_int64)file_ofs, SEEK_SET)))) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED); + return 0; + } + + return MZ_FWRITE(pBuf, 1, n, pZip->m_pState->m_pFile); + } + + mz_bool mz_zip_writer_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning) + { + return mz_zip_writer_init_file_v2(pZip, pFilename, size_to_reserve_at_beginning, 0); + } + + mz_bool mz_zip_writer_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning, mz_uint flags) + { + MZ_FILE *pFile; + + pZip->m_pWrite = mz_zip_file_write_func; + pZip->m_pNeeds_keepalive = NULL; + + if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) + pZip->m_pRead = mz_zip_file_read_func; + + pZip->m_pIO_opaque = pZip; + + if (!mz_zip_writer_init_v2(pZip, size_to_reserve_at_beginning, flags)) + return MZ_FALSE; + + if (NULL == (pFile = MZ_FOPEN(pFilename, (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) ? "w+b" : "wb"))) + { + mz_zip_writer_end(pZip); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + } + + pZip->m_pState->m_pFile = pFile; + pZip->m_zip_type = MZ_ZIP_TYPE_FILE; + + if (size_to_reserve_at_beginning) + { + mz_uint64 cur_ofs = 0; + char buf[4096]; + + MZ_CLEAR_ARR(buf); + + do + { + size_t n = (size_t)MZ_MIN(sizeof(buf), size_to_reserve_at_beginning); + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_ofs, buf, n) != n) + { + mz_zip_writer_end(pZip); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + cur_ofs += n; + size_to_reserve_at_beginning -= n; + } while (size_to_reserve_at_beginning); + } + + return MZ_TRUE; + } + + mz_bool mz_zip_writer_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint flags) + { + pZip->m_pWrite = mz_zip_file_write_func; + pZip->m_pNeeds_keepalive = NULL; + + if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) + pZip->m_pRead = mz_zip_file_read_func; + + pZip->m_pIO_opaque = pZip; + + if (!mz_zip_writer_init_v2(pZip, 0, flags)) + return MZ_FALSE; + + pZip->m_pState->m_pFile = pFile; + pZip->m_pState->m_file_archive_start_ofs = MZ_FTELL64(pZip->m_pState->m_pFile); + pZip->m_zip_type = MZ_ZIP_TYPE_CFILE; + + return MZ_TRUE; + } +#endif /* #ifndef MINIZ_NO_STDIO */ + + mz_bool mz_zip_writer_init_from_reader_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags) + { + mz_zip_internal_state *pState; + + if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_READING)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (flags & MZ_ZIP_FLAG_WRITE_ZIP64) + { + /* We don't support converting a non-zip64 file to zip64 - this seems like more trouble than it's worth. (What about the existing 32-bit data descriptors that could follow the compressed data?) */ + if (!pZip->m_pState->m_zip64) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + } + + /* No sense in trying to write to an archive that's already at the support max size */ + if (pZip->m_pState->m_zip64) + { + if (pZip->m_total_files == MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + else + { + if (pZip->m_total_files == MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + + if ((pZip->m_archive_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_ZIP_LOCAL_DIR_HEADER_SIZE) > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE); + } + + pState = pZip->m_pState; + + if (pState->m_pFile) + { +#ifdef MINIZ_NO_STDIO + (void)pFilename; + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); +#else + if (pZip->m_pIO_opaque != pZip) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE && + !(flags & MZ_ZIP_FLAG_READ_ALLOW_WRITING) ) + { + if (!pFilename) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + /* Archive is being read from stdio and was originally opened only for reading. Try to reopen as writable. */ + if (NULL == (pState->m_pFile = MZ_FREOPEN(pFilename, "r+b", pState->m_pFile))) + { + /* The mz_zip_archive is now in a bogus state because pState->m_pFile is NULL, so just close it. */ + mz_zip_reader_end_internal(pZip, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + } + } + + pZip->m_pWrite = mz_zip_file_write_func; + pZip->m_pNeeds_keepalive = NULL; +#endif /* #ifdef MINIZ_NO_STDIO */ + } + else if (pState->m_pMem) + { + /* Archive lives in a memory block. Assume it's from the heap that we can resize using the realloc callback. */ + if (pZip->m_pIO_opaque != pZip) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState->m_mem_capacity = pState->m_mem_size; + pZip->m_pWrite = mz_zip_heap_write_func; + pZip->m_pNeeds_keepalive = NULL; + } + /* Archive is being read via a user provided read function - make sure the user has specified a write function too. */ + else if (!pZip->m_pWrite) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + /* Start writing new files at the archive's current central directory location. */ + /* TODO: We could add a flag that lets the user start writing immediately AFTER the existing central dir - this would be safer. */ + pZip->m_archive_size = pZip->m_central_directory_file_ofs; + pZip->m_central_directory_file_ofs = 0; + + /* Clear the sorted central dir offsets, they aren't useful or maintained now. */ + /* Even though we're now in write mode, files can still be extracted and verified, but file locates will be slow. */ + /* TODO: We could easily maintain the sorted central directory offsets. */ + mz_zip_array_clear(pZip, &pZip->m_pState->m_sorted_central_dir_offsets); + + pZip->m_zip_mode = MZ_ZIP_MODE_WRITING; + + return MZ_TRUE; + } + + mz_bool mz_zip_writer_init_from_reader(mz_zip_archive *pZip, const char *pFilename) + { + return mz_zip_writer_init_from_reader_v2(pZip, pFilename, 0); + } + + /* TODO: pArchive_name is a terrible name here! */ + mz_bool mz_zip_writer_add_mem(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, mz_uint level_and_flags) + { + return mz_zip_writer_add_mem_ex(pZip, pArchive_name, pBuf, buf_size, NULL, 0, level_and_flags, 0, 0); + } + + typedef struct + { + mz_zip_archive *m_pZip; + mz_uint64 m_cur_archive_file_ofs; + mz_uint64 m_comp_size; + } mz_zip_writer_add_state; + + static mz_bool mz_zip_writer_add_put_buf_callback(const void *pBuf, int len, void *pUser) + { + mz_zip_writer_add_state *pState = (mz_zip_writer_add_state *)pUser; + if ((int)pState->m_pZip->m_pWrite(pState->m_pZip->m_pIO_opaque, pState->m_cur_archive_file_ofs, pBuf, len) != len) + return MZ_FALSE; + + pState->m_cur_archive_file_ofs += len; + pState->m_comp_size += len; + return MZ_TRUE; + } + +#define MZ_ZIP64_MAX_LOCAL_EXTRA_FIELD_SIZE (sizeof(mz_uint16) * 2 + sizeof(mz_uint64) * 2) +#define MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE (sizeof(mz_uint16) * 2 + sizeof(mz_uint64) * 3) + static mz_uint32 mz_zip_writer_create_zip64_extra_data(mz_uint8 *pBuf, mz_uint64 *pUncomp_size, mz_uint64 *pComp_size, mz_uint64 *pLocal_header_ofs) + { + mz_uint8 *pDst = pBuf; + mz_uint32 field_size = 0; + + MZ_WRITE_LE16(pDst + 0, MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID); + MZ_WRITE_LE16(pDst + 2, 0); + pDst += sizeof(mz_uint16) * 2; + + if (pUncomp_size) + { + MZ_WRITE_LE64(pDst, *pUncomp_size); + pDst += sizeof(mz_uint64); + field_size += sizeof(mz_uint64); + } + + if (pComp_size) + { + MZ_WRITE_LE64(pDst, *pComp_size); + pDst += sizeof(mz_uint64); + field_size += sizeof(mz_uint64); + } + + if (pLocal_header_ofs) + { + MZ_WRITE_LE64(pDst, *pLocal_header_ofs); + pDst += sizeof(mz_uint64); + field_size += sizeof(mz_uint64); + } + + MZ_WRITE_LE16(pBuf + 2, field_size); + + return (mz_uint32)(pDst - pBuf); + } + + static mz_bool mz_zip_writer_create_local_dir_header(mz_zip_archive *pZip, mz_uint8 *pDst, mz_uint16 filename_size, mz_uint16 extra_size, mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32, mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date) + { + (void)pZip; + memset(pDst, 0, MZ_ZIP_LOCAL_DIR_HEADER_SIZE); + MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_SIG_OFS, MZ_ZIP_LOCAL_DIR_HEADER_SIG); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_VERSION_NEEDED_OFS, method ? 20 : 0); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_BIT_FLAG_OFS, bit_flags); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_METHOD_OFS, method); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILE_TIME_OFS, dos_time); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILE_DATE_OFS, dos_date); + MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_CRC32_OFS, uncomp_crc32); + MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS, MZ_MIN(comp_size, MZ_UINT32_MAX)); + MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS, MZ_MIN(uncomp_size, MZ_UINT32_MAX)); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILENAME_LEN_OFS, filename_size); + MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_EXTRA_LEN_OFS, extra_size); + return MZ_TRUE; + } + + static mz_bool mz_zip_writer_create_central_dir_header(mz_zip_archive *pZip, mz_uint8 *pDst, + mz_uint16 filename_size, mz_uint16 extra_size, mz_uint16 comment_size, + mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32, + mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date, + mz_uint64 local_header_ofs, mz_uint32 ext_attributes) + { + (void)pZip; + memset(pDst, 0, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_SIG_OFS, MZ_ZIP_CENTRAL_DIR_HEADER_SIG); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_VERSION_NEEDED_OFS, method ? 20 : 0); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_BIT_FLAG_OFS, bit_flags); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_METHOD_OFS, method); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILE_TIME_OFS, dos_time); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILE_DATE_OFS, dos_date); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_CRC32_OFS, uncomp_crc32); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS, MZ_MIN(comp_size, MZ_UINT32_MAX)); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS, MZ_MIN(uncomp_size, MZ_UINT32_MAX)); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILENAME_LEN_OFS, filename_size); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_EXTRA_LEN_OFS, extra_size); + MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_COMMENT_LEN_OFS, comment_size); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS, ext_attributes); + MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_LOCAL_HEADER_OFS, MZ_MIN(local_header_ofs, MZ_UINT32_MAX)); + return MZ_TRUE; + } + + static mz_bool mz_zip_writer_add_to_central_dir(mz_zip_archive *pZip, const char *pFilename, mz_uint16 filename_size, + const void *pExtra, mz_uint16 extra_size, const void *pComment, mz_uint16 comment_size, + mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32, + mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date, + mz_uint64 local_header_ofs, mz_uint32 ext_attributes, + const char *user_extra_data, mz_uint user_extra_data_len) + { + mz_zip_internal_state *pState = pZip->m_pState; + mz_uint32 central_dir_ofs = (mz_uint32)pState->m_central_dir.m_size; + size_t orig_central_dir_size = pState->m_central_dir.m_size; + mz_uint8 central_dir_header[MZ_ZIP_CENTRAL_DIR_HEADER_SIZE]; + + if (!pZip->m_pState->m_zip64) + { + if (local_header_ofs > 0xFFFFFFFF) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE); + } + + /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */ + if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size + extra_size + user_extra_data_len + comment_size) >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + + if (!mz_zip_writer_create_central_dir_header(pZip, central_dir_header, filename_size, (mz_uint16)(extra_size + user_extra_data_len), comment_size, uncomp_size, comp_size, uncomp_crc32, method, bit_flags, dos_time, dos_date, local_header_ofs, ext_attributes)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if ((!mz_zip_array_push_back(pZip, &pState->m_central_dir, central_dir_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE)) || + (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pFilename, filename_size)) || + (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pExtra, extra_size)) || + (!mz_zip_array_push_back(pZip, &pState->m_central_dir, user_extra_data, user_extra_data_len)) || + (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pComment, comment_size)) || + (!mz_zip_array_push_back(pZip, &pState->m_central_dir_offsets, ¢ral_dir_ofs, 1))) + { + /* Try to resize the central directory array back into its original state. */ + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + return MZ_TRUE; + } + + static mz_bool mz_zip_writer_validate_archive_name(const char *pArchive_name) + { + /* Basic ZIP archive filename validity checks: Valid filenames cannot start with a forward slash, cannot contain a drive letter, and cannot use DOS-style backward slashes. */ + if (*pArchive_name == '/') + return MZ_FALSE; + + /* Making sure the name does not contain drive letters or DOS style backward slashes is the responsibility of the program using miniz*/ + + return MZ_TRUE; + } + + static mz_uint mz_zip_writer_compute_padding_needed_for_file_alignment(mz_zip_archive *pZip) + { + mz_uint32 n; + if (!pZip->m_file_offset_alignment) + return 0; + n = (mz_uint32)(pZip->m_archive_size & (pZip->m_file_offset_alignment - 1)); + return (mz_uint)((pZip->m_file_offset_alignment - n) & (pZip->m_file_offset_alignment - 1)); + } + + static mz_bool mz_zip_writer_write_zeros(mz_zip_archive *pZip, mz_uint64 cur_file_ofs, mz_uint32 n) + { + char buf[4096]; + memset(buf, 0, MZ_MIN(sizeof(buf), n)); + while (n) + { + mz_uint32 s = MZ_MIN(sizeof(buf), n); + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_file_ofs, buf, s) != s) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_file_ofs += s; + n -= s; + } + return MZ_TRUE; + } + + mz_bool mz_zip_writer_add_mem_ex(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, + mz_uint64 uncomp_size, mz_uint32 uncomp_crc32) + { + return mz_zip_writer_add_mem_ex_v2(pZip, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, uncomp_size, uncomp_crc32, NULL, NULL, 0, NULL, 0); + } + + mz_bool mz_zip_writer_add_mem_ex_v2(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, + mz_uint level_and_flags, mz_uint64 uncomp_size, mz_uint32 uncomp_crc32, MZ_TIME_T *last_modified, + const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len) + { + mz_uint16 method = 0, dos_time = 0, dos_date = 0; + mz_uint level, ext_attributes = 0, num_alignment_padding_bytes; + mz_uint64 local_dir_header_ofs = pZip->m_archive_size, cur_archive_file_ofs = pZip->m_archive_size, comp_size = 0; + size_t archive_name_size; + mz_uint8 local_dir_header[MZ_ZIP_LOCAL_DIR_HEADER_SIZE]; + tdefl_compressor *pComp = NULL; + mz_bool store_data_uncompressed; + mz_zip_internal_state *pState; + mz_uint8 *pExtra_data = NULL; + mz_uint32 extra_size = 0; + mz_uint8 extra_data[MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE]; + mz_uint16 bit_flags = 0; + + if ((int)level_and_flags < 0) + level_and_flags = MZ_DEFAULT_LEVEL; + + if (uncomp_size || (buf_size && !(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA))) + bit_flags |= MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR; + + if (!(level_and_flags & MZ_ZIP_FLAG_ASCII_FILENAME)) + bit_flags |= MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8; + + level = level_and_flags & 0xF; + store_data_uncompressed = ((!level) || (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)); + + if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || ((buf_size) && (!pBuf)) || (!pArchive_name) || ((comment_size) && (!pComment)) || (level > MZ_UBER_COMPRESSION)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + if (pState->m_zip64) + { + if (pZip->m_total_files == MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + else + { + if (pZip->m_total_files == MZ_UINT16_MAX) + { + pState->m_zip64 = MZ_TRUE; + /*return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); */ + } + if (((mz_uint64)buf_size > 0xFFFFFFFF) || (uncomp_size > 0xFFFFFFFF)) + { + pState->m_zip64 = MZ_TRUE; + /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ + } + } + + if ((!(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (uncomp_size)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!mz_zip_writer_validate_archive_name(pArchive_name)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); + +#ifndef MINIZ_NO_TIME + if (last_modified != NULL) + { + mz_zip_time_t_to_dos_time(*last_modified, &dos_time, &dos_date); + } + else + { + MZ_TIME_T cur_time; + time(&cur_time); + mz_zip_time_t_to_dos_time(cur_time, &dos_time, &dos_date); + } +#else + (void)last_modified; +#endif /* #ifndef MINIZ_NO_TIME */ + + if (!(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) + { + uncomp_crc32 = (mz_uint32)mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, buf_size); + uncomp_size = buf_size; + if (uncomp_size <= 3) + { + level = 0; + store_data_uncompressed = MZ_TRUE; + } + } + + archive_name_size = strlen(pArchive_name); + if (archive_name_size > MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); + + num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip); + + /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */ + if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE + comment_size) >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + + if (!pState->m_zip64) + { + /* Bail early if the archive would obviously become too large */ + if ((pZip->m_archive_size + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + comment_size + user_extra_data_len + + pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + user_extra_data_central_len + MZ_ZIP_DATA_DESCRIPTER_SIZE32) > 0xFFFFFFFF) + { + pState->m_zip64 = MZ_TRUE; + /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ + } + } + + if ((archive_name_size) && (pArchive_name[archive_name_size - 1] == '/')) + { + /* Set DOS Subdirectory attribute bit. */ + ext_attributes |= MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG; + + /* Subdirectories cannot contain data. */ + if ((buf_size) || (uncomp_size)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + } + + /* Try to do any allocations before writing to the archive, so if an allocation fails the file remains unmodified. (A good idea if we're doing an in-place modification.) */ + if ((!mz_zip_array_ensure_room(pZip, &pState->m_central_dir, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + comment_size + (pState->m_zip64 ? MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE : 0))) || (!mz_zip_array_ensure_room(pZip, &pState->m_central_dir_offsets, 1))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + if ((!store_data_uncompressed) && (buf_size)) + { + if (NULL == (pComp = (tdefl_compressor *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(tdefl_compressor)))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (!mz_zip_writer_write_zeros(pZip, cur_archive_file_ofs, num_alignment_padding_bytes)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + return MZ_FALSE; + } + + local_dir_header_ofs += num_alignment_padding_bytes; + if (pZip->m_file_offset_alignment) + { + MZ_ASSERT((local_dir_header_ofs & (pZip->m_file_offset_alignment - 1)) == 0); + } + cur_archive_file_ofs += num_alignment_padding_bytes; + + MZ_CLEAR_ARR(local_dir_header); + + if (!store_data_uncompressed || (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) + { + method = MZ_DEFLATED; + } + + if (pState->m_zip64) + { + if (uncomp_size >= MZ_UINT32_MAX || local_dir_header_ofs >= MZ_UINT32_MAX) + { + pExtra_data = extra_data; + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, + (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + } + + if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len), 0, 0, 0, method, bit_flags, dos_time, dos_date)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, local_dir_header_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += sizeof(local_dir_header); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + cur_archive_file_ofs += archive_name_size; + + if (pExtra_data != NULL) + { + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, extra_data, extra_size) != extra_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += extra_size; + } + } + else + { + if ((comp_size > MZ_UINT32_MAX) || (cur_archive_file_ofs > MZ_UINT32_MAX)) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)user_extra_data_len, 0, 0, 0, method, bit_flags, dos_time, dos_date)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, local_dir_header_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += sizeof(local_dir_header); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + cur_archive_file_ofs += archive_name_size; + } + + if (user_extra_data_len > 0) + { + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, user_extra_data, user_extra_data_len) != user_extra_data_len) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += user_extra_data_len; + } + + if (store_data_uncompressed) + { + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pBuf, buf_size) != buf_size) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_archive_file_ofs += buf_size; + comp_size = buf_size; + } + else if (buf_size) + { + mz_zip_writer_add_state state; + + state.m_pZip = pZip; + state.m_cur_archive_file_ofs = cur_archive_file_ofs; + state.m_comp_size = 0; + + if ((tdefl_init(pComp, mz_zip_writer_add_put_buf_callback, &state, tdefl_create_comp_flags_from_zip_params(level, -15, MZ_DEFAULT_STRATEGY)) != TDEFL_STATUS_OKAY) || + (tdefl_compress_buffer(pComp, pBuf, buf_size, TDEFL_FINISH) != TDEFL_STATUS_DONE)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + return mz_zip_set_error(pZip, MZ_ZIP_COMPRESSION_FAILED); + } + + comp_size = state.m_comp_size; + cur_archive_file_ofs = state.m_cur_archive_file_ofs; + } + + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + pComp = NULL; + + if (uncomp_size) + { + mz_uint8 local_dir_footer[MZ_ZIP_DATA_DESCRIPTER_SIZE64]; + mz_uint32 local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE32; + + MZ_ASSERT(bit_flags & MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR); + + MZ_WRITE_LE32(local_dir_footer + 0, MZ_ZIP_DATA_DESCRIPTOR_ID); + MZ_WRITE_LE32(local_dir_footer + 4, uncomp_crc32); + if (pExtra_data == NULL) + { + if (comp_size > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + + MZ_WRITE_LE32(local_dir_footer + 8, comp_size); + MZ_WRITE_LE32(local_dir_footer + 12, uncomp_size); + } + else + { + MZ_WRITE_LE64(local_dir_footer + 8, comp_size); + MZ_WRITE_LE64(local_dir_footer + 16, uncomp_size); + local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE64; + } + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_footer, local_dir_footer_size) != local_dir_footer_size) + return MZ_FALSE; + + cur_archive_file_ofs += local_dir_footer_size; + } + + if (pExtra_data != NULL) + { + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, + (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + } + + if (!mz_zip_writer_add_to_central_dir(pZip, pArchive_name, (mz_uint16)archive_name_size, pExtra_data, (mz_uint16)extra_size, pComment, + comment_size, uncomp_size, comp_size, uncomp_crc32, method, bit_flags, dos_time, dos_date, local_dir_header_ofs, ext_attributes, + user_extra_data_central, user_extra_data_central_len)) + return MZ_FALSE; + + pZip->m_total_files++; + pZip->m_archive_size = cur_archive_file_ofs; + + return MZ_TRUE; + } + + mz_bool mz_zip_writer_add_read_buf_callback(mz_zip_archive *pZip, const char *pArchive_name, mz_file_read_func read_callback, void *callback_opaque, mz_uint64 max_size, const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, + const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len) + { + mz_uint16 gen_flags; + mz_uint uncomp_crc32 = MZ_CRC32_INIT, level, num_alignment_padding_bytes; + mz_uint16 method = 0, dos_time = 0, dos_date = 0, ext_attributes = 0; + mz_uint64 local_dir_header_ofs, cur_archive_file_ofs = pZip->m_archive_size, uncomp_size = 0, comp_size = 0; + size_t archive_name_size; + mz_uint8 local_dir_header[MZ_ZIP_LOCAL_DIR_HEADER_SIZE]; + mz_uint8 *pExtra_data = NULL; + mz_uint32 extra_size = 0; + mz_uint8 extra_data[MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE]; + mz_zip_internal_state *pState; + mz_uint64 file_ofs = 0, cur_archive_header_file_ofs; + + if ((int)level_and_flags < 0) + level_and_flags = MZ_DEFAULT_LEVEL; + level = level_and_flags & 0xF; + + gen_flags = (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE) ? 0 : MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR; + + if (!(level_and_flags & MZ_ZIP_FLAG_ASCII_FILENAME)) + gen_flags |= MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8; + + /* Sanity checks */ + if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || (!pArchive_name) || ((comment_size) && (!pComment)) || (level > MZ_UBER_COMPRESSION)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + if ((!pState->m_zip64) && (max_size > MZ_UINT32_MAX)) + { + /* Source file is too large for non-zip64 */ + /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ + pState->m_zip64 = MZ_TRUE; + } + + /* We could support this, but why? */ + if (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!mz_zip_writer_validate_archive_name(pArchive_name)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); + + if (pState->m_zip64) + { + if (pZip->m_total_files == MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + else + { + if (pZip->m_total_files == MZ_UINT16_MAX) + { + pState->m_zip64 = MZ_TRUE; + /*return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); */ + } + } + + archive_name_size = strlen(pArchive_name); + if (archive_name_size > MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); + + num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip); + + /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */ + if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE + comment_size) >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + + if (!pState->m_zip64) + { + /* Bail early if the archive would obviously become too large */ + if ((pZip->m_archive_size + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + comment_size + user_extra_data_len + pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + 1024 + MZ_ZIP_DATA_DESCRIPTER_SIZE32 + user_extra_data_central_len) > 0xFFFFFFFF) + { + pState->m_zip64 = MZ_TRUE; + /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ + } + } + +#ifndef MINIZ_NO_TIME + if (pFile_time) + { + mz_zip_time_t_to_dos_time(*pFile_time, &dos_time, &dos_date); + } +#else + (void)pFile_time; +#endif + + if (max_size <= 3) + level = 0; + + if (!mz_zip_writer_write_zeros(pZip, cur_archive_file_ofs, num_alignment_padding_bytes)) + { + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_archive_file_ofs += num_alignment_padding_bytes; + local_dir_header_ofs = cur_archive_file_ofs; + + if (pZip->m_file_offset_alignment) + { + MZ_ASSERT((cur_archive_file_ofs & (pZip->m_file_offset_alignment - 1)) == 0); + } + + if (max_size && level) + { + method = MZ_DEFLATED; + } + + MZ_CLEAR_ARR(local_dir_header); + if (pState->m_zip64) + { + if (max_size >= MZ_UINT32_MAX || local_dir_header_ofs >= MZ_UINT32_MAX) + { + pExtra_data = extra_data; + if (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE) + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (max_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, + (max_size >= MZ_UINT32_MAX) ? &comp_size : NULL, + (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + else + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, NULL, + NULL, + (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + } + + if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len), 0, 0, 0, method, gen_flags, dos_time, dos_date)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += sizeof(local_dir_header); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) + { + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_archive_file_ofs += archive_name_size; + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, extra_data, extra_size) != extra_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += extra_size; + } + else + { + if ((comp_size > MZ_UINT32_MAX) || (cur_archive_file_ofs > MZ_UINT32_MAX)) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)user_extra_data_len, 0, 0, 0, method, gen_flags, dos_time, dos_date)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += sizeof(local_dir_header); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) + { + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_archive_file_ofs += archive_name_size; + } + + if (user_extra_data_len > 0) + { + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, user_extra_data, user_extra_data_len) != user_extra_data_len) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_file_ofs += user_extra_data_len; + } + + if (max_size) + { + void *pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, MZ_ZIP_MAX_IO_BUF_SIZE); + if (!pRead_buf) + { + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (!level) + { + while (1) + { + size_t n = read_callback(callback_opaque, file_ofs, pRead_buf, MZ_ZIP_MAX_IO_BUF_SIZE); + if (n == 0) + break; + + if ((n > MZ_ZIP_MAX_IO_BUF_SIZE) || (file_ofs + n > max_size)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pRead_buf, n) != n) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + file_ofs += n; + uncomp_crc32 = (mz_uint32)mz_crc32(uncomp_crc32, (const mz_uint8 *)pRead_buf, n); + cur_archive_file_ofs += n; + } + uncomp_size = file_ofs; + comp_size = uncomp_size; + } + else + { + mz_bool result = MZ_FALSE; + mz_zip_writer_add_state state; + tdefl_compressor *pComp = (tdefl_compressor *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(tdefl_compressor)); + if (!pComp) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + state.m_pZip = pZip; + state.m_cur_archive_file_ofs = cur_archive_file_ofs; + state.m_comp_size = 0; + + if (tdefl_init(pComp, mz_zip_writer_add_put_buf_callback, &state, tdefl_create_comp_flags_from_zip_params(level, -15, MZ_DEFAULT_STRATEGY)) != TDEFL_STATUS_OKAY) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + } + + for (;;) + { + tdefl_status status; + tdefl_flush flush = TDEFL_NO_FLUSH; + + size_t n = read_callback(callback_opaque, file_ofs, pRead_buf, MZ_ZIP_MAX_IO_BUF_SIZE); + if ((n > MZ_ZIP_MAX_IO_BUF_SIZE) || (file_ofs + n > max_size)) + { + mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + break; + } + + file_ofs += n; + uncomp_crc32 = (mz_uint32)mz_crc32(uncomp_crc32, (const mz_uint8 *)pRead_buf, n); + + if (pZip->m_pNeeds_keepalive != NULL && pZip->m_pNeeds_keepalive(pZip->m_pIO_opaque)) + flush = TDEFL_FULL_FLUSH; + + if (n == 0) + flush = TDEFL_FINISH; + + status = tdefl_compress_buffer(pComp, pRead_buf, n, flush); + if (status == TDEFL_STATUS_DONE) + { + result = MZ_TRUE; + break; + } + else if (status != TDEFL_STATUS_OKAY) + { + mz_zip_set_error(pZip, MZ_ZIP_COMPRESSION_FAILED); + break; + } + } + + pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); + + if (!result) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + return MZ_FALSE; + } + + uncomp_size = file_ofs; + comp_size = state.m_comp_size; + cur_archive_file_ofs = state.m_cur_archive_file_ofs; + } + + pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); + } + + if (!(level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE)) + { + mz_uint8 local_dir_footer[MZ_ZIP_DATA_DESCRIPTER_SIZE64]; + mz_uint32 local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE32; + + MZ_WRITE_LE32(local_dir_footer + 0, MZ_ZIP_DATA_DESCRIPTOR_ID); + MZ_WRITE_LE32(local_dir_footer + 4, uncomp_crc32); + if (pExtra_data == NULL) + { + if (comp_size > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + + MZ_WRITE_LE32(local_dir_footer + 8, comp_size); + MZ_WRITE_LE32(local_dir_footer + 12, uncomp_size); + } + else + { + MZ_WRITE_LE64(local_dir_footer + 8, comp_size); + MZ_WRITE_LE64(local_dir_footer + 16, uncomp_size); + local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE64; + } + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_footer, local_dir_footer_size) != local_dir_footer_size) + return MZ_FALSE; + + cur_archive_file_ofs += local_dir_footer_size; + } + + if (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE) + { + if (pExtra_data != NULL) + { + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (max_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, + (max_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + } + + if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, + (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len), + (max_size >= MZ_UINT32_MAX) ? MZ_UINT32_MAX : uncomp_size, + (max_size >= MZ_UINT32_MAX) ? MZ_UINT32_MAX : comp_size, + uncomp_crc32, method, gen_flags, dos_time, dos_date)) + return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); + + cur_archive_header_file_ofs = local_dir_header_ofs; + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + if (pExtra_data != NULL) + { + cur_archive_header_file_ofs += sizeof(local_dir_header); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, pArchive_name, archive_name_size) != archive_name_size) + { + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_archive_header_file_ofs += archive_name_size; + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, extra_data, extra_size) != extra_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_archive_header_file_ofs += extra_size; + } + } + + if (pExtra_data != NULL) + { + extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, + (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); + } + + if (!mz_zip_writer_add_to_central_dir(pZip, pArchive_name, (mz_uint16)archive_name_size, pExtra_data, (mz_uint16)extra_size, pComment, comment_size, + uncomp_size, comp_size, uncomp_crc32, method, gen_flags, dos_time, dos_date, local_dir_header_ofs, ext_attributes, + user_extra_data_central, user_extra_data_central_len)) + return MZ_FALSE; + + pZip->m_total_files++; + pZip->m_archive_size = cur_archive_file_ofs; + + return MZ_TRUE; + } + +#ifndef MINIZ_NO_STDIO + + static size_t mz_file_read_func_stdio(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n) + { + MZ_FILE *pSrc_file = (MZ_FILE *)pOpaque; + mz_int64 cur_ofs = MZ_FTELL64(pSrc_file); + + if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pSrc_file, (mz_int64)file_ofs, SEEK_SET)))) + return 0; + + return MZ_FREAD(pBuf, 1, n, pSrc_file); + } + + mz_bool mz_zip_writer_add_cfile(mz_zip_archive *pZip, const char *pArchive_name, MZ_FILE *pSrc_file, mz_uint64 max_size, const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, + const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len) + { + return mz_zip_writer_add_read_buf_callback(pZip, pArchive_name, mz_file_read_func_stdio, pSrc_file, max_size, pFile_time, pComment, comment_size, level_and_flags, + user_extra_data, user_extra_data_len, user_extra_data_central, user_extra_data_central_len); + } + + mz_bool mz_zip_writer_add_file(mz_zip_archive *pZip, const char *pArchive_name, const char *pSrc_filename, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags) + { + MZ_FILE *pSrc_file = NULL; + mz_uint64 uncomp_size = 0; + MZ_TIME_T file_modified_time; + MZ_TIME_T *pFile_time = NULL; + mz_bool status; + + memset(&file_modified_time, 0, sizeof(file_modified_time)); + +#if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_STDIO) + pFile_time = &file_modified_time; + if (!mz_zip_get_file_modified_time(pSrc_filename, &file_modified_time)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_STAT_FAILED); +#endif + + pSrc_file = MZ_FOPEN(pSrc_filename, "rb"); + if (!pSrc_file) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); + + MZ_FSEEK64(pSrc_file, 0, SEEK_END); + uncomp_size = MZ_FTELL64(pSrc_file); + MZ_FSEEK64(pSrc_file, 0, SEEK_SET); + + status = mz_zip_writer_add_cfile(pZip, pArchive_name, pSrc_file, uncomp_size, pFile_time, pComment, comment_size, level_and_flags, NULL, 0, NULL, 0); + + MZ_FCLOSE(pSrc_file); + + return status; + } +#endif /* #ifndef MINIZ_NO_STDIO */ + + static mz_bool mz_zip_writer_update_zip64_extension_block(mz_zip_array *pNew_ext, mz_zip_archive *pZip, const mz_uint8 *pExt, mz_uint32 ext_len, mz_uint64 *pComp_size, mz_uint64 *pUncomp_size, mz_uint64 *pLocal_header_ofs, mz_uint32 *pDisk_start) + { + /* + 64 should be enough for any new zip64 data */ + if (!mz_zip_array_reserve(pZip, pNew_ext, ext_len + 64, MZ_FALSE)) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + mz_zip_array_resize(pZip, pNew_ext, 0, MZ_FALSE); + + if ((pUncomp_size) || (pComp_size) || (pLocal_header_ofs) || (pDisk_start)) + { + mz_uint8 new_ext_block[64]; + mz_uint8 *pDst = new_ext_block; + mz_write_le16(pDst, MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID); + mz_write_le16(pDst + sizeof(mz_uint16), 0); + pDst += sizeof(mz_uint16) * 2; + + if (pUncomp_size) + { + mz_write_le64(pDst, *pUncomp_size); + pDst += sizeof(mz_uint64); + } + + if (pComp_size) + { + mz_write_le64(pDst, *pComp_size); + pDst += sizeof(mz_uint64); + } + + if (pLocal_header_ofs) + { + mz_write_le64(pDst, *pLocal_header_ofs); + pDst += sizeof(mz_uint64); + } + + if (pDisk_start) + { + mz_write_le32(pDst, *pDisk_start); + pDst += sizeof(mz_uint32); + } + + mz_write_le16(new_ext_block + sizeof(mz_uint16), (mz_uint16)((pDst - new_ext_block) - sizeof(mz_uint16) * 2)); + + if (!mz_zip_array_push_back(pZip, pNew_ext, new_ext_block, pDst - new_ext_block)) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if ((pExt) && (ext_len)) + { + mz_uint32 extra_size_remaining = ext_len; + const mz_uint8 *pExtra_data = pExt; + + do + { + mz_uint32 field_id, field_data_size, field_total_size; + + if (extra_size_remaining < (sizeof(mz_uint16) * 2)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + field_id = MZ_READ_LE16(pExtra_data); + field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); + field_total_size = field_data_size + sizeof(mz_uint16) * 2; + + if (field_total_size > extra_size_remaining) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + if (field_id != MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) + { + if (!mz_zip_array_push_back(pZip, pNew_ext, pExtra_data, field_total_size)) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + pExtra_data += field_total_size; + extra_size_remaining -= field_total_size; + } while (extra_size_remaining); + } + + return MZ_TRUE; + } + + /* TODO: This func is now pretty freakin complex due to zip64, split it up? */ + mz_bool mz_zip_writer_add_from_zip_reader(mz_zip_archive *pZip, mz_zip_archive *pSource_zip, mz_uint src_file_index) + { + mz_uint n, bit_flags, num_alignment_padding_bytes, src_central_dir_following_data_size; + mz_uint64 src_archive_bytes_remaining, local_dir_header_ofs; + mz_uint64 cur_src_file_ofs, cur_dst_file_ofs; + mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; + mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; + mz_uint8 new_central_header[MZ_ZIP_CENTRAL_DIR_HEADER_SIZE]; + size_t orig_central_dir_size; + mz_zip_internal_state *pState; + void *pBuf; + const mz_uint8 *pSrc_central_header; + mz_zip_archive_file_stat src_file_stat; + mz_uint32 src_filename_len, src_comment_len, src_ext_len; + mz_uint32 local_header_filename_size, local_header_extra_len; + mz_uint64 local_header_comp_size, local_header_uncomp_size; + mz_bool found_zip64_ext_data_in_ldir = MZ_FALSE; + + /* Sanity checks */ + if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || (!pSource_zip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + /* Don't support copying files from zip64 archives to non-zip64, even though in some cases this is possible */ + if ((pSource_zip->m_pState->m_zip64) && (!pZip->m_pState->m_zip64)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + /* Get pointer to the source central dir header and crack it */ + if (NULL == (pSrc_central_header = mz_zip_get_cdh(pSource_zip, src_file_index))) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (MZ_READ_LE32(pSrc_central_header + MZ_ZIP_CDH_SIG_OFS) != MZ_ZIP_CENTRAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + src_filename_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_FILENAME_LEN_OFS); + src_comment_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_COMMENT_LEN_OFS); + src_ext_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_EXTRA_LEN_OFS); + src_central_dir_following_data_size = src_filename_len + src_ext_len + src_comment_len; + + /* TODO: We don't support central dir's >= MZ_UINT32_MAX bytes right now (+32 fudge factor in case we need to add more extra data) */ + if ((pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_central_dir_following_data_size + 32) >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + + num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip); + + if (!pState->m_zip64) + { + if (pZip->m_total_files == MZ_UINT16_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + else + { + /* TODO: Our zip64 support still has some 32-bit limits that may not be worth fixing. */ + if (pZip->m_total_files == MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + + if (!mz_zip_file_stat_internal(pSource_zip, src_file_index, pSrc_central_header, &src_file_stat, NULL)) + return MZ_FALSE; + + cur_src_file_ofs = src_file_stat.m_local_header_ofs; + cur_dst_file_ofs = pZip->m_archive_size; + + /* Read the source archive's local dir header */ + if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + + if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + + cur_src_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE; + + /* Compute the total size we need to copy (filename+extra data+compressed data) */ + local_header_filename_size = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS); + local_header_extra_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); + local_header_comp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS); + local_header_uncomp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS); + src_archive_bytes_remaining = src_file_stat.m_comp_size + local_header_filename_size + local_header_extra_len; + + /* Try to find a zip64 extended information field */ + if ((local_header_extra_len) && ((local_header_comp_size == MZ_UINT32_MAX) || (local_header_uncomp_size == MZ_UINT32_MAX))) + { + mz_zip_array file_data_array; + const mz_uint8 *pExtra_data; + mz_uint32 extra_size_remaining = local_header_extra_len; + + mz_zip_array_init(&file_data_array, 1); + if (!mz_zip_array_resize(pZip, &file_data_array, local_header_extra_len, MZ_FALSE)) + { + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, src_file_stat.m_local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_size, file_data_array.m_p, local_header_extra_len) != local_header_extra_len) + { + mz_zip_array_clear(pZip, &file_data_array); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + + pExtra_data = (const mz_uint8 *)file_data_array.m_p; + + do + { + mz_uint32 field_id, field_data_size, field_total_size; + + if (extra_size_remaining < (sizeof(mz_uint16) * 2)) + { + mz_zip_array_clear(pZip, &file_data_array); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + field_id = MZ_READ_LE16(pExtra_data); + field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); + field_total_size = field_data_size + sizeof(mz_uint16) * 2; + + if (field_total_size > extra_size_remaining) + { + mz_zip_array_clear(pZip, &file_data_array); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) + { + const mz_uint8 *pSrc_field_data = pExtra_data + sizeof(mz_uint32); + + if (field_data_size < sizeof(mz_uint64) * 2) + { + mz_zip_array_clear(pZip, &file_data_array); + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); + } + + local_header_uncomp_size = MZ_READ_LE64(pSrc_field_data); + local_header_comp_size = MZ_READ_LE64(pSrc_field_data + sizeof(mz_uint64)); /* may be 0 if there's a descriptor */ + + found_zip64_ext_data_in_ldir = MZ_TRUE; + break; + } + + pExtra_data += field_total_size; + extra_size_remaining -= field_total_size; + } while (extra_size_remaining); + + mz_zip_array_clear(pZip, &file_data_array); + } + + if (!pState->m_zip64) + { + /* Try to detect if the new archive will most likely wind up too big and bail early (+(sizeof(mz_uint32) * 4) is for the optional descriptor which could be present, +64 is a fudge factor). */ + /* We also check when the archive is finalized so this doesn't need to be perfect. */ + mz_uint64 approx_new_archive_size = cur_dst_file_ofs + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + src_archive_bytes_remaining + (sizeof(mz_uint32) * 4) + + pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_central_dir_following_data_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + 64; + + if (approx_new_archive_size >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + } + + /* Write dest archive padding */ + if (!mz_zip_writer_write_zeros(pZip, cur_dst_file_ofs, num_alignment_padding_bytes)) + return MZ_FALSE; + + cur_dst_file_ofs += num_alignment_padding_bytes; + + local_dir_header_ofs = cur_dst_file_ofs; + if (pZip->m_file_offset_alignment) + { + MZ_ASSERT((local_dir_header_ofs & (pZip->m_file_offset_alignment - 1)) == 0); + } + + /* The original zip's local header+ext block doesn't change, even with zip64, so we can just copy it over to the dest zip */ + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + cur_dst_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE; + + /* Copy over the source archive bytes to the dest archive, also ensure we have enough buf space to handle optional data descriptor */ + if (NULL == (pBuf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)MZ_MAX(32U, MZ_MIN((mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE, src_archive_bytes_remaining))))) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + while (src_archive_bytes_remaining) + { + n = (mz_uint)MZ_MIN((mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE, src_archive_bytes_remaining); + if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, n) != n) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + cur_src_file_ofs += n; + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pBuf, n) != n) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + cur_dst_file_ofs += n; + + src_archive_bytes_remaining -= n; + } + + /* Now deal with the optional data descriptor */ + bit_flags = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_BIT_FLAG_OFS); + if (bit_flags & 8) + { + /* Copy data descriptor */ + if ((pSource_zip->m_pState->m_zip64) || (found_zip64_ext_data_in_ldir)) + { + /* src is zip64, dest must be zip64 */ + + /* name uint32_t's */ + /* id 1 (optional in zip64?) */ + /* crc 1 */ + /* comp_size 2 */ + /* uncomp_size 2 */ + if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, (sizeof(mz_uint32) * 6)) != (sizeof(mz_uint32) * 6)) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + + n = sizeof(mz_uint32) * ((MZ_READ_LE32(pBuf) == MZ_ZIP_DATA_DESCRIPTOR_ID) ? 6 : 5); + } + else + { + /* src is NOT zip64 */ + mz_bool has_id; + + if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, sizeof(mz_uint32) * 4) != sizeof(mz_uint32) * 4) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); + } + + has_id = (MZ_READ_LE32(pBuf) == MZ_ZIP_DATA_DESCRIPTOR_ID); + + if (pZip->m_pState->m_zip64) + { + /* dest is zip64, so upgrade the data descriptor */ + const mz_uint8 *pSrc_descriptor = (const mz_uint8 *)pBuf + (has_id ? sizeof(mz_uint32) : 0); + const mz_uint32 src_crc32 = MZ_READ_LE32(pSrc_descriptor); + const mz_uint64 src_comp_size = MZ_READ_LE32(pSrc_descriptor + sizeof(mz_uint32)); + const mz_uint64 src_uncomp_size = MZ_READ_LE32(pSrc_descriptor + 2 * sizeof(mz_uint32)); + + mz_write_le32((mz_uint8 *)pBuf, MZ_ZIP_DATA_DESCRIPTOR_ID); + mz_write_le32((mz_uint8 *)pBuf + sizeof(mz_uint32) * 1, src_crc32); + mz_write_le64((mz_uint8 *)pBuf + sizeof(mz_uint32) * 2, src_comp_size); + mz_write_le64((mz_uint8 *)pBuf + sizeof(mz_uint32) * 4, src_uncomp_size); + + n = sizeof(mz_uint32) * 6; + } + else + { + /* dest is NOT zip64, just copy it as-is */ + n = sizeof(mz_uint32) * (has_id ? 4 : 3); + } + } + + if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pBuf, n) != n) + { + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + } + + cur_src_file_ofs += n; + cur_dst_file_ofs += n; + } + pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); + + /* Finally, add the new central dir header */ + orig_central_dir_size = pState->m_central_dir.m_size; + + memcpy(new_central_header, pSrc_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE); + + if (pState->m_zip64) + { + /* This is the painful part: We need to write a new central dir header + ext block with updated zip64 fields, and ensure the old fields (if any) are not included. */ + const mz_uint8 *pSrc_ext = pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_filename_len; + mz_zip_array new_ext_block; + + mz_zip_array_init(&new_ext_block, sizeof(mz_uint8)); + + MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS, MZ_UINT32_MAX); + MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS, MZ_UINT32_MAX); + MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_LOCAL_HEADER_OFS, MZ_UINT32_MAX); + + if (!mz_zip_writer_update_zip64_extension_block(&new_ext_block, pZip, pSrc_ext, src_ext_len, &src_file_stat.m_comp_size, &src_file_stat.m_uncomp_size, &local_dir_header_ofs, NULL)) + { + mz_zip_array_clear(pZip, &new_ext_block); + return MZ_FALSE; + } + + MZ_WRITE_LE16(new_central_header + MZ_ZIP_CDH_EXTRA_LEN_OFS, new_ext_block.m_size); + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE)) + { + mz_zip_array_clear(pZip, &new_ext_block); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, src_filename_len)) + { + mz_zip_array_clear(pZip, &new_ext_block); + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_ext_block.m_p, new_ext_block.m_size)) + { + mz_zip_array_clear(pZip, &new_ext_block); + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_filename_len + src_ext_len, src_comment_len)) + { + mz_zip_array_clear(pZip, &new_ext_block); + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + mz_zip_array_clear(pZip, &new_ext_block); + } + else + { + /* sanity checks */ + if (cur_dst_file_ofs > MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + + if (local_dir_header_ofs >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); + + MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_LOCAL_HEADER_OFS, local_dir_header_ofs); + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE)) + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, src_central_dir_following_data_size)) + { + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + } + + /* This shouldn't trigger unless we screwed up during the initial sanity checks */ + if (pState->m_central_dir.m_size >= MZ_UINT32_MAX) + { + /* TODO: Support central dirs >= 32-bits in size */ + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); + } + + n = (mz_uint32)orig_central_dir_size; + if (!mz_zip_array_push_back(pZip, &pState->m_central_dir_offsets, &n, 1)) + { + mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); + return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); + } + + pZip->m_total_files++; + pZip->m_archive_size = cur_dst_file_ofs; + + return MZ_TRUE; + } + + mz_bool mz_zip_writer_finalize_archive(mz_zip_archive *pZip) + { + mz_zip_internal_state *pState; + mz_uint64 central_dir_ofs, central_dir_size; + mz_uint8 hdr[256]; + + if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + pState = pZip->m_pState; + + if (pState->m_zip64) + { + if ((mz_uint64)pState->m_central_dir.m_size >= MZ_UINT32_MAX) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + else + { + if ((pZip->m_total_files > MZ_UINT16_MAX) || ((pZip->m_archive_size + pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) > MZ_UINT32_MAX)) + return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); + } + + central_dir_ofs = 0; + central_dir_size = 0; + if (pZip->m_total_files) + { + /* Write central directory */ + central_dir_ofs = pZip->m_archive_size; + central_dir_size = pState->m_central_dir.m_size; + pZip->m_central_directory_file_ofs = central_dir_ofs; + if (pZip->m_pWrite(pZip->m_pIO_opaque, central_dir_ofs, pState->m_central_dir.m_p, (size_t)central_dir_size) != central_dir_size) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + pZip->m_archive_size += central_dir_size; + } + + if (pState->m_zip64) + { + /* Write zip64 end of central directory header */ + mz_uint64 rel_ofs_to_zip64_ecdr = pZip->m_archive_size; + + MZ_CLEAR_ARR(hdr); + MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDH_SIG_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE - sizeof(mz_uint32) - sizeof(mz_uint64)); + MZ_WRITE_LE16(hdr + MZ_ZIP64_ECDH_VERSION_MADE_BY_OFS, 0x031E); /* TODO: always Unix */ + MZ_WRITE_LE16(hdr + MZ_ZIP64_ECDH_VERSION_NEEDED_OFS, 0x002D); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS, pZip->m_total_files); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS, pZip->m_total_files); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_SIZE_OFS, central_dir_size); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_OFS_OFS, central_dir_ofs); + if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + pZip->m_archive_size += MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE; + + /* Write zip64 end of central directory locator */ + MZ_CLEAR_ARR(hdr); + MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDL_SIG_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG); + MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS, rel_ofs_to_zip64_ecdr); + MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS, 1); + if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) != MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + + pZip->m_archive_size += MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE; + } + + /* Write end of central directory record */ + MZ_CLEAR_ARR(hdr); + MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_SIG_OFS, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG); + MZ_WRITE_LE16(hdr + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS, MZ_MIN(MZ_UINT16_MAX, pZip->m_total_files)); + MZ_WRITE_LE16(hdr + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS, MZ_MIN(MZ_UINT16_MAX, pZip->m_total_files)); + MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_CDIR_SIZE_OFS, MZ_MIN(MZ_UINT32_MAX, central_dir_size)); + MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_CDIR_OFS_OFS, MZ_MIN(MZ_UINT32_MAX, central_dir_ofs)); + + if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); + +#ifndef MINIZ_NO_STDIO + if ((pState->m_pFile) && (MZ_FFLUSH(pState->m_pFile) == EOF)) + return mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED); +#endif /* #ifndef MINIZ_NO_STDIO */ + + pZip->m_archive_size += MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE; + + pZip->m_zip_mode = MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED; + return MZ_TRUE; + } + + mz_bool mz_zip_writer_finalize_heap_archive(mz_zip_archive *pZip, void **ppBuf, size_t *pSize) + { + if ((!ppBuf) || (!pSize)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + *ppBuf = NULL; + *pSize = 0; + + if ((!pZip) || (!pZip->m_pState)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (pZip->m_pWrite != mz_zip_heap_write_func) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + if (!mz_zip_writer_finalize_archive(pZip)) + return MZ_FALSE; + + *ppBuf = pZip->m_pState->m_pMem; + *pSize = pZip->m_pState->m_mem_size; + pZip->m_pState->m_pMem = NULL; + pZip->m_pState->m_mem_size = pZip->m_pState->m_mem_capacity = 0; + + return MZ_TRUE; + } + + mz_bool mz_zip_writer_end(mz_zip_archive *pZip) + { + return mz_zip_writer_end_internal(pZip, MZ_TRUE); + } + +#ifndef MINIZ_NO_STDIO + mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags) + { + return mz_zip_add_mem_to_archive_file_in_place_v2(pZip_filename, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, NULL); + } + + mz_bool mz_zip_add_mem_to_archive_file_in_place_v2(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_zip_error *pErr) + { + mz_bool status, created_new_archive = MZ_FALSE; + mz_zip_archive zip_archive; + struct MZ_FILE_STAT_STRUCT file_stat; + mz_zip_error actual_err = MZ_ZIP_NO_ERROR; + + mz_zip_zero_struct(&zip_archive); + if ((int)level_and_flags < 0) + level_and_flags = MZ_DEFAULT_LEVEL; + + if ((!pZip_filename) || (!pArchive_name) || ((buf_size) && (!pBuf)) || ((comment_size) && (!pComment)) || ((level_and_flags & 0xF) > MZ_UBER_COMPRESSION)) + { + if (pErr) + *pErr = MZ_ZIP_INVALID_PARAMETER; + return MZ_FALSE; + } + + if (!mz_zip_writer_validate_archive_name(pArchive_name)) + { + if (pErr) + *pErr = MZ_ZIP_INVALID_FILENAME; + return MZ_FALSE; + } + + /* Important: The regular non-64 bit version of stat() can fail here if the file is very large, which could cause the archive to be overwritten. */ + /* So be sure to compile with _LARGEFILE64_SOURCE 1 */ + if (MZ_FILE_STAT(pZip_filename, &file_stat) != 0) + { + /* Create a new archive. */ + if (!mz_zip_writer_init_file_v2(&zip_archive, pZip_filename, 0, level_and_flags)) + { + if (pErr) + *pErr = zip_archive.m_last_error; + return MZ_FALSE; + } + + created_new_archive = MZ_TRUE; + } + else + { + /* Append to an existing archive. */ + if (!mz_zip_reader_init_file_v2(&zip_archive, pZip_filename, level_and_flags | MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY | MZ_ZIP_FLAG_READ_ALLOW_WRITING, 0, 0)) + { + if (pErr) + *pErr = zip_archive.m_last_error; + return MZ_FALSE; + } + + if (!mz_zip_writer_init_from_reader_v2(&zip_archive, pZip_filename, level_and_flags | MZ_ZIP_FLAG_READ_ALLOW_WRITING)) + { + if (pErr) + *pErr = zip_archive.m_last_error; + + mz_zip_reader_end_internal(&zip_archive, MZ_FALSE); + + return MZ_FALSE; + } + } + + status = mz_zip_writer_add_mem_ex(&zip_archive, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, 0, 0); + actual_err = zip_archive.m_last_error; + + /* Always finalize, even if adding failed for some reason, so we have a valid central directory. (This may not always succeed, but we can try.) */ + if (!mz_zip_writer_finalize_archive(&zip_archive)) + { + if (!actual_err) + actual_err = zip_archive.m_last_error; + + status = MZ_FALSE; + } + + if (!mz_zip_writer_end_internal(&zip_archive, status)) + { + if (!actual_err) + actual_err = zip_archive.m_last_error; + + status = MZ_FALSE; + } + + if ((!status) && (created_new_archive)) + { + /* It's a new archive and something went wrong, so just delete it. */ + int ignoredStatus = MZ_DELETE_FILE(pZip_filename); + (void)ignoredStatus; + } + + if (pErr) + *pErr = actual_err; + + return status; + } + + void *mz_zip_extract_archive_file_to_heap_v2(const char *pZip_filename, const char *pArchive_name, const char *pComment, size_t *pSize, mz_uint flags, mz_zip_error *pErr) + { + mz_uint32 file_index; + mz_zip_archive zip_archive; + void *p = NULL; + + if (pSize) + *pSize = 0; + + if ((!pZip_filename) || (!pArchive_name)) + { + if (pErr) + *pErr = MZ_ZIP_INVALID_PARAMETER; + + return NULL; + } + + mz_zip_zero_struct(&zip_archive); + if (!mz_zip_reader_init_file_v2(&zip_archive, pZip_filename, flags | MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY, 0, 0)) + { + if (pErr) + *pErr = zip_archive.m_last_error; + + return NULL; + } + + if (mz_zip_reader_locate_file_v2(&zip_archive, pArchive_name, pComment, flags, &file_index)) + { + p = mz_zip_reader_extract_to_heap(&zip_archive, file_index, pSize, flags); + } + + mz_zip_reader_end_internal(&zip_archive, p != NULL); + + if (pErr) + *pErr = zip_archive.m_last_error; + + return p; + } + + void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, size_t *pSize, mz_uint flags) + { + return mz_zip_extract_archive_file_to_heap_v2(pZip_filename, pArchive_name, NULL, pSize, flags, NULL); + } + +#endif /* #ifndef MINIZ_NO_STDIO */ + +#endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS */ + + /* ------------------- Misc utils */ + + mz_zip_mode mz_zip_get_mode(mz_zip_archive *pZip) + { + return pZip ? pZip->m_zip_mode : MZ_ZIP_MODE_INVALID; + } + + mz_zip_type mz_zip_get_type(mz_zip_archive *pZip) + { + return pZip ? pZip->m_zip_type : MZ_ZIP_TYPE_INVALID; + } + + mz_zip_error mz_zip_set_last_error(mz_zip_archive *pZip, mz_zip_error err_num) + { + mz_zip_error prev_err; + + if (!pZip) + return MZ_ZIP_INVALID_PARAMETER; + + prev_err = pZip->m_last_error; + + pZip->m_last_error = err_num; + return prev_err; + } + + mz_zip_error mz_zip_peek_last_error(mz_zip_archive *pZip) + { + if (!pZip) + return MZ_ZIP_INVALID_PARAMETER; + + return pZip->m_last_error; + } + + mz_zip_error mz_zip_clear_last_error(mz_zip_archive *pZip) + { + return mz_zip_set_last_error(pZip, MZ_ZIP_NO_ERROR); + } + + mz_zip_error mz_zip_get_last_error(mz_zip_archive *pZip) + { + mz_zip_error prev_err; + + if (!pZip) + return MZ_ZIP_INVALID_PARAMETER; + + prev_err = pZip->m_last_error; + + pZip->m_last_error = MZ_ZIP_NO_ERROR; + return prev_err; + } + + const char *mz_zip_get_error_string(mz_zip_error mz_err) + { + switch (mz_err) + { + case MZ_ZIP_NO_ERROR: + return "no error"; + case MZ_ZIP_UNDEFINED_ERROR: + return "undefined error"; + case MZ_ZIP_TOO_MANY_FILES: + return "too many files"; + case MZ_ZIP_FILE_TOO_LARGE: + return "file too large"; + case MZ_ZIP_UNSUPPORTED_METHOD: + return "unsupported method"; + case MZ_ZIP_UNSUPPORTED_ENCRYPTION: + return "unsupported encryption"; + case MZ_ZIP_UNSUPPORTED_FEATURE: + return "unsupported feature"; + case MZ_ZIP_FAILED_FINDING_CENTRAL_DIR: + return "failed finding central directory"; + case MZ_ZIP_NOT_AN_ARCHIVE: + return "not a ZIP archive"; + case MZ_ZIP_INVALID_HEADER_OR_CORRUPTED: + return "invalid header or archive is corrupted"; + case MZ_ZIP_UNSUPPORTED_MULTIDISK: + return "unsupported multidisk archive"; + case MZ_ZIP_DECOMPRESSION_FAILED: + return "decompression failed or archive is corrupted"; + case MZ_ZIP_COMPRESSION_FAILED: + return "compression failed"; + case MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE: + return "unexpected decompressed size"; + case MZ_ZIP_CRC_CHECK_FAILED: + return "CRC-32 check failed"; + case MZ_ZIP_UNSUPPORTED_CDIR_SIZE: + return "unsupported central directory size"; + case MZ_ZIP_ALLOC_FAILED: + return "allocation failed"; + case MZ_ZIP_FILE_OPEN_FAILED: + return "file open failed"; + case MZ_ZIP_FILE_CREATE_FAILED: + return "file create failed"; + case MZ_ZIP_FILE_WRITE_FAILED: + return "file write failed"; + case MZ_ZIP_FILE_READ_FAILED: + return "file read failed"; + case MZ_ZIP_FILE_CLOSE_FAILED: + return "file close failed"; + case MZ_ZIP_FILE_SEEK_FAILED: + return "file seek failed"; + case MZ_ZIP_FILE_STAT_FAILED: + return "file stat failed"; + case MZ_ZIP_INVALID_PARAMETER: + return "invalid parameter"; + case MZ_ZIP_INVALID_FILENAME: + return "invalid filename"; + case MZ_ZIP_BUF_TOO_SMALL: + return "buffer too small"; + case MZ_ZIP_INTERNAL_ERROR: + return "internal error"; + case MZ_ZIP_FILE_NOT_FOUND: + return "file not found"; + case MZ_ZIP_ARCHIVE_TOO_LARGE: + return "archive is too large"; + case MZ_ZIP_VALIDATION_FAILED: + return "validation failed"; + case MZ_ZIP_WRITE_CALLBACK_FAILED: + return "write callback failed"; + case MZ_ZIP_TOTAL_ERRORS: + return "total errors"; + default: + break; + } + + return "unknown error"; + } + + /* Note: Just because the archive is not zip64 doesn't necessarily mean it doesn't have Zip64 extended information extra field, argh. */ + mz_bool mz_zip_is_zip64(mz_zip_archive *pZip) + { + if ((!pZip) || (!pZip->m_pState)) + return MZ_FALSE; + + return pZip->m_pState->m_zip64; + } + + size_t mz_zip_get_central_dir_size(mz_zip_archive *pZip) + { + if ((!pZip) || (!pZip->m_pState)) + return 0; + + return pZip->m_pState->m_central_dir.m_size; + } + + mz_uint mz_zip_reader_get_num_files(mz_zip_archive *pZip) + { + return pZip ? pZip->m_total_files : 0; + } + + mz_uint64 mz_zip_get_archive_size(mz_zip_archive *pZip) + { + if (!pZip) + return 0; + return pZip->m_archive_size; + } + + mz_uint64 mz_zip_get_archive_file_start_offset(mz_zip_archive *pZip) + { + if ((!pZip) || (!pZip->m_pState)) + return 0; + return pZip->m_pState->m_file_archive_start_ofs; + } + + MZ_FILE *mz_zip_get_cfile(mz_zip_archive *pZip) + { + if ((!pZip) || (!pZip->m_pState)) + return 0; + return pZip->m_pState->m_pFile; + } + + size_t mz_zip_read_archive_data(mz_zip_archive *pZip, mz_uint64 file_ofs, void *pBuf, size_t n) + { + if ((!pZip) || (!pZip->m_pState) || (!pBuf) || (!pZip->m_pRead)) + return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + + return pZip->m_pRead(pZip->m_pIO_opaque, file_ofs, pBuf, n); + } + + mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, char *pFilename, mz_uint filename_buf_size) + { + mz_uint n; + const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); + if (!p) + { + if (filename_buf_size) + pFilename[0] = '\0'; + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return 0; + } + n = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + if (filename_buf_size) + { + n = MZ_MIN(n, filename_buf_size - 1); + memcpy(pFilename, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, n); + pFilename[n] = '\0'; + } + return n + 1; + } + + mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat) + { + return mz_zip_file_stat_internal(pZip, file_index, mz_zip_get_cdh(pZip, file_index), pStat, NULL); + } + + mz_bool mz_zip_end(mz_zip_archive *pZip) + { + if (!pZip) + return MZ_FALSE; + + if (pZip->m_zip_mode == MZ_ZIP_MODE_READING) + return mz_zip_reader_end(pZip); +#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS + else if ((pZip->m_zip_mode == MZ_ZIP_MODE_WRITING) || (pZip->m_zip_mode == MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED)) + return mz_zip_writer_end(pZip); +#endif + + return MZ_FALSE; + } + +#ifdef __cplusplus +} +#endif + +#endif /*#ifndef MINIZ_NO_ARCHIVE_APIS*/ diff --git a/lib/miniz/third_party/miniz.h b/lib/miniz/third_party/miniz.h new file mode 100644 index 0000000000..059d812945 --- /dev/null +++ b/lib/miniz/third_party/miniz.h @@ -0,0 +1,1510 @@ +#ifndef MINIZ_EXPORT +#define MINIZ_EXPORT +#endif +/* miniz.c 3.1.2 - public domain deflate/inflate, zlib-subset, ZIP reading/writing/appending, PNG writing + See "unlicense" statement at the end of this file. + Rich Geldreich , last updated Oct. 13, 2013 + Implements RFC 1950: http://www.ietf.org/rfc/rfc1950.txt and RFC 1951: http://www.ietf.org/rfc/rfc1951.txt + + Most API's defined in miniz.c are optional. For example, to disable the archive related functions just define + MINIZ_NO_ARCHIVE_APIS, or to get rid of all stdio usage define MINIZ_NO_STDIO (see the list below for more macros). + + * Low-level Deflate/Inflate implementation notes: + + Compression: Use the "tdefl" API's. The compressor supports raw, static, and dynamic blocks, lazy or + greedy parsing, match length filtering, RLE-only, and Huffman-only streams. It performs and compresses + approximately as well as zlib. + + Decompression: Use the "tinfl" API's. The entire decompressor is implemented as a single function + coroutine: see tinfl_decompress(). It supports decompression into a 32KB (or larger power of 2) wrapping buffer, or into a memory + block large enough to hold the entire file. + + The low-level tdefl/tinfl API's do not make any use of dynamic memory allocation. + + * zlib-style API notes: + + miniz.c implements a fairly large subset of zlib. There's enough functionality present for it to be a drop-in + zlib replacement in many apps: + The z_stream struct, optional memory allocation callbacks + deflateInit/deflateInit2/deflate/deflateReset/deflateEnd/deflateBound + inflateInit/inflateInit2/inflate/inflateReset/inflateEnd + compress, compress2, compressBound, uncompress + CRC-32, Adler-32 - Using modern, minimal code size, CPU cache friendly routines. + Supports raw deflate streams or standard zlib streams with adler-32 checking. + + Limitations: + The callback API's are not implemented yet. No support for gzip headers or zlib static dictionaries. + I've tried to closely emulate zlib's various flavors of stream flushing and return status codes, but + there are no guarantees that miniz.c pulls this off perfectly. + + * PNG writing: See the tdefl_write_image_to_png_file_in_memory() function, originally written by + Alex Evans. Supports 1-4 bytes/pixel images. + + * ZIP archive API notes: + + The ZIP archive API's where designed with simplicity and efficiency in mind, with just enough abstraction to + get the job done with minimal fuss. There are simple API's to retrieve file information, read files from + existing archives, create new archives, append new files to existing archives, or clone archive data from + one archive to another. It supports archives located in memory or the heap, on disk (using stdio.h), + or you can specify custom file read/write callbacks. + + - Archive reading: Just call this function to read a single file from a disk archive: + + void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, + size_t *pSize, mz_uint zip_flags); + + For more complex cases, use the "mz_zip_reader" functions. Upon opening an archive, the entire central + directory is located and read as-is into memory, and subsequent file access only occurs when reading individual files. + + - Archives file scanning: The simple way is to use this function to scan a loaded archive for a specific file: + + int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags); + + The locate operation can optionally check file comments too, which (as one example) can be used to identify + multiple versions of the same file in an archive. This function uses a simple linear search through the central + directory, so it's not very fast. + + Alternately, you can iterate through all the files in an archive (using mz_zip_reader_get_num_files()) and + retrieve detailed info on each file by calling mz_zip_reader_file_stat(). + + - Archive creation: Use the "mz_zip_writer" functions. The ZIP writer immediately writes compressed file data + to disk and builds an exact image of the central directory in memory. The central directory image is written + all at once at the end of the archive file when the archive is finalized. + + The archive writer can optionally align each file's local header and file data to any power of 2 alignment, + which can be useful when the archive will be read from optical media. Also, the writer supports placing + arbitrary data blobs at the very beginning of ZIP archives. Archives written using either feature are still + readable by any ZIP tool. + + - Archive appending: The simple way to add a single file to an archive is to call this function: + + mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, + const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags); + + The archive will be created if it doesn't already exist, otherwise it'll be appended to. + Note the appending is done in-place and is not an atomic operation, so if something goes wrong + during the operation it's possible the archive could be left without a central directory (although the local + file headers and file data will be fine, so the archive will be recoverable). + + For more complex archive modification scenarios: + 1. The safest way is to use a mz_zip_reader to read the existing archive, cloning only those bits you want to + preserve into a new archive using using the mz_zip_writer_add_from_zip_reader() function (which compiles the + compressed file data as-is). When you're done, delete the old archive and rename the newly written archive, and + you're done. This is safe but requires a bunch of temporary disk space or heap memory. + + 2. Or, you can convert an mz_zip_reader in-place to an mz_zip_writer using mz_zip_writer_init_from_reader(), + append new files as needed, then finalize the archive which will write an updated central directory to the + original archive. (This is basically what mz_zip_add_mem_to_archive_file_in_place() does.) There's a + possibility that the archive's central directory could be lost with this method if anything goes wrong, though. + + - ZIP archive support limitations: + No spanning support. Extraction functions can only handle unencrypted, stored or deflated files. + Requires streams capable of seeking. + + * This is a header file library, like stb_image.c. To get only a header file, either cut and paste the + below header, or create miniz.h, #define MINIZ_HEADER_FILE_ONLY, and then include miniz.c from it. + + * Important: For best perf. be sure to customize the below macros for your target platform: + #define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 1 + #define MINIZ_LITTLE_ENDIAN 1 + #define MINIZ_HAS_64BIT_REGISTERS 1 + + * On platforms using glibc, Be sure to "#define _LARGEFILE64_SOURCE 1" before including miniz.c to ensure miniz + uses the 64-bit variants: fopen64(), stat64(), etc. Otherwise you won't be able to process large files + (i.e. 32-bit stat() fails for me on files > 0x7FFFFFFF bytes). +*/ +#pragma once + + + +#if defined(__STRICT_ANSI__) +#define MZ_FORCEINLINE +#elif defined(_MSC_VER) +#define MZ_FORCEINLINE __forceinline +#elif defined(__GNUC__) +#define MZ_FORCEINLINE __inline__ __attribute__((__always_inline__)) +#else +#define MZ_FORCEINLINE inline +#endif + +/* Defines to completely disable specific portions of miniz.c: + If all macros here are defined the only functionality remaining will be CRC-32 and adler-32. */ + +/* Define MINIZ_NO_STDIO to disable all usage and any functions which rely on stdio for file I/O. */ +/*#define MINIZ_NO_STDIO */ + +/* If MINIZ_NO_TIME is specified then the ZIP archive functions will not be able to get the current time, or */ +/* get/set file times, and the C run-time funcs that get/set times won't be called. */ +/* The current downside is the times written to your archives will be from 1979. */ +/*#define MINIZ_NO_TIME */ + +/* Define MINIZ_NO_DEFLATE_APIS to disable all compression API's. */ +/*#define MINIZ_NO_DEFLATE_APIS */ + +/* Define MINIZ_NO_INFLATE_APIS to disable all decompression API's. */ +/*#define MINIZ_NO_INFLATE_APIS */ + +/* Define MINIZ_NO_ARCHIVE_APIS to disable all ZIP archive API's. */ +/*#define MINIZ_NO_ARCHIVE_APIS */ + +/* Define MINIZ_NO_ARCHIVE_WRITING_APIS to disable all writing related ZIP archive API's. */ +/*#define MINIZ_NO_ARCHIVE_WRITING_APIS */ + +/* Define MINIZ_NO_ZLIB_APIS to remove all ZLIB-style compression/decompression API's. */ +/*#define MINIZ_NO_ZLIB_APIS */ + +/* Define MINIZ_NO_ZLIB_COMPATIBLE_NAME to disable zlib names, to prevent conflicts against stock zlib. */ +/*#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES */ + +/* Define MINIZ_NO_MALLOC to disable all calls to malloc, free, and realloc. + Note if MINIZ_NO_MALLOC is defined then the user must always provide custom user alloc/free/realloc + callbacks to the zlib and archive API's, and a few stand-alone helper API's which don't provide custom user + functions (such as tdefl_compress_mem_to_heap() and tinfl_decompress_mem_to_heap()) won't work. */ +/*#define MINIZ_NO_MALLOC */ + +#ifdef MINIZ_NO_INFLATE_APIS +#define MINIZ_NO_ARCHIVE_APIS +#endif + +#ifdef MINIZ_NO_DEFLATE_APIS +#define MINIZ_NO_ARCHIVE_WRITING_APIS +#endif + +#if defined(__TINYC__) && (defined(__linux) || defined(__linux__)) +/* TODO: Work around "error: include file 'sys\utime.h' when compiling with tcc on Linux */ +#define MINIZ_NO_TIME +#endif + +#include + +#if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_ARCHIVE_APIS) +#include +#endif + +#if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__i386) || defined(__i486__) || defined(__i486) || defined(i386) || defined(__ia64__) || defined(__x86_64__) +/* MINIZ_X86_OR_X64_CPU is only used to help set the below macros. */ +#define MINIZ_X86_OR_X64_CPU 1 +#else +#define MINIZ_X86_OR_X64_CPU 0 +#endif + +/* Set MINIZ_LITTLE_ENDIAN only if not set */ +#if !defined(MINIZ_LITTLE_ENDIAN) +#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) + +#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) +/* Set MINIZ_LITTLE_ENDIAN to 1 if the processor is little endian. */ +#define MINIZ_LITTLE_ENDIAN 1 +#else +#define MINIZ_LITTLE_ENDIAN 0 +#endif + +#else + +#if MINIZ_X86_OR_X64_CPU +#define MINIZ_LITTLE_ENDIAN 1 +#else +#define MINIZ_LITTLE_ENDIAN 0 +#endif + +#endif +#endif + +/* Using unaligned loads and stores causes errors when using UBSan */ +#if defined(__has_feature) +#if __has_feature(undefined_behavior_sanitizer) +#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0 +#endif +#endif + +/* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES only if not set */ +#if !defined(MINIZ_USE_UNALIGNED_LOADS_AND_STORES) +#if MINIZ_X86_OR_X64_CPU +/* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES to 1 on CPU's that permit efficient integer loads and stores from unaligned addresses. */ +#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0 +#define MINIZ_UNALIGNED_USE_MEMCPY +#else +#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0 +#endif +#endif + +#if defined(_M_X64) || defined(_WIN64) || defined(__MINGW64__) || defined(_LP64) || defined(__LP64__) || defined(__ia64__) || defined(__x86_64__) +/* Set MINIZ_HAS_64BIT_REGISTERS to 1 if operations on 64-bit integers are reasonably fast (and don't involve compiler generated calls to helper functions). */ +#define MINIZ_HAS_64BIT_REGISTERS 1 +#else +#define MINIZ_HAS_64BIT_REGISTERS 0 +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + + /* ------------------- zlib-style API Definitions. */ + + /* For more compatibility with zlib, miniz.c uses unsigned long for some parameters/struct members. Beware: mz_ulong can be either 32 or 64-bits! */ + typedef unsigned long mz_ulong; + + /* mz_free() internally uses the MZ_FREE() macro (which by default calls free() unless you've modified the MZ_MALLOC macro) to release a block allocated from the heap. */ + MINIZ_EXPORT void mz_free(void *p); + +#define MZ_ADLER32_INIT (1) + /* mz_adler32() returns the initial adler-32 value to use when called with ptr==NULL. */ + MINIZ_EXPORT mz_ulong mz_adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len); + +#define MZ_CRC32_INIT (0) + /* mz_crc32() returns the initial CRC-32 value to use when called with ptr==NULL. */ + MINIZ_EXPORT mz_ulong mz_crc32(mz_ulong crc, const unsigned char *ptr, size_t buf_len); + + /* Compression strategies. */ + enum + { + MZ_DEFAULT_STRATEGY = 0, + MZ_FILTERED = 1, + MZ_HUFFMAN_ONLY = 2, + MZ_RLE = 3, + MZ_FIXED = 4 + }; + +/* Method */ +#define MZ_DEFLATED 8 + + /* Heap allocation callbacks. + Note that mz_alloc_func parameter types purposely differ from zlib's: items/size is size_t, not unsigned long. */ + typedef void *(*mz_alloc_func)(void *opaque, size_t items, size_t size); + typedef void (*mz_free_func)(void *opaque, void *address); + typedef void *(*mz_realloc_func)(void *opaque, void *address, size_t items, size_t size); + + /* Compression levels: 0-9 are the standard zlib-style levels, 10 is best possible compression (not zlib compatible, and may be very slow), MZ_DEFAULT_COMPRESSION=MZ_DEFAULT_LEVEL. */ + enum + { + MZ_NO_COMPRESSION = 0, + MZ_BEST_SPEED = 1, + MZ_BEST_COMPRESSION = 9, + MZ_UBER_COMPRESSION = 10, + MZ_DEFAULT_LEVEL = 6, + MZ_DEFAULT_COMPRESSION = -1 + }; + +#define MZ_VERSION "11.3.2" +#define MZ_VERNUM 0xB302 +#define MZ_VER_MAJOR 11 +#define MZ_VER_MINOR 3 +#define MZ_VER_REVISION 2 +#define MZ_VER_SUBREVISION 0 + +#ifndef MINIZ_NO_ZLIB_APIS + + /* Flush values. For typical usage you only need MZ_NO_FLUSH and MZ_FINISH. The other values are for advanced use (refer to the zlib docs). */ + enum + { + MZ_NO_FLUSH = 0, + MZ_PARTIAL_FLUSH = 1, + MZ_SYNC_FLUSH = 2, + MZ_FULL_FLUSH = 3, + MZ_FINISH = 4, + MZ_BLOCK = 5 + }; + + /* Return status codes. MZ_PARAM_ERROR is non-standard. */ + enum + { + MZ_OK = 0, + MZ_STREAM_END = 1, + MZ_NEED_DICT = 2, + MZ_ERRNO = -1, + MZ_STREAM_ERROR = -2, + MZ_DATA_ERROR = -3, + MZ_MEM_ERROR = -4, + MZ_BUF_ERROR = -5, + MZ_VERSION_ERROR = -6, + MZ_PARAM_ERROR = -10000 + }; + +/* Window bits */ +#define MZ_DEFAULT_WINDOW_BITS 15 + + struct mz_internal_state; + + /* Compression/decompression stream struct. */ + typedef struct mz_stream_s + { + const unsigned char *next_in; /* pointer to next byte to read */ + unsigned int avail_in; /* number of bytes available at next_in */ + mz_ulong total_in; /* total number of bytes consumed so far */ + + unsigned char *next_out; /* pointer to next byte to write */ + unsigned int avail_out; /* number of bytes that can be written to next_out */ + mz_ulong total_out; /* total number of bytes produced so far */ + + char *msg; /* error msg (unused) */ + struct mz_internal_state *state; /* internal state, allocated by zalloc/zfree */ + + mz_alloc_func zalloc; /* optional heap allocation function (defaults to malloc) */ + mz_free_func zfree; /* optional heap free function (defaults to free) */ + void *opaque; /* heap alloc function user pointer */ + + int data_type; /* data_type (unused) */ + mz_ulong adler; /* adler32 of the source or uncompressed data */ + mz_ulong reserved; /* not used */ + } mz_stream; + + typedef mz_stream *mz_streamp; + + /* Returns the version string of miniz.c. */ + MINIZ_EXPORT const char *mz_version(void); + +#ifndef MINIZ_NO_DEFLATE_APIS + + /* mz_deflateInit() initializes a compressor with default options: */ + /* Parameters: */ + /* pStream must point to an initialized mz_stream struct. */ + /* level must be between [MZ_NO_COMPRESSION, MZ_BEST_COMPRESSION]. */ + /* level 1 enables a specially optimized compression function that's been optimized purely for performance, not ratio. */ + /* (This special func. is currently only enabled when MINIZ_USE_UNALIGNED_LOADS_AND_STORES and MINIZ_LITTLE_ENDIAN are defined.) */ + /* Return values: */ + /* MZ_OK on success. */ + /* MZ_STREAM_ERROR if the stream is bogus. */ + /* MZ_PARAM_ERROR if the input parameters are bogus. */ + /* MZ_MEM_ERROR on out of memory. */ + MINIZ_EXPORT int mz_deflateInit(mz_streamp pStream, int level); + + /* mz_deflateInit2() is like mz_deflate(), except with more control: */ + /* Additional parameters: */ + /* method must be MZ_DEFLATED */ + /* window_bits must be MZ_DEFAULT_WINDOW_BITS (to wrap the deflate stream with zlib header/adler-32 footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate/no header or footer) */ + /* mem_level must be between [1, 9] (it's checked but ignored by miniz.c) */ + MINIZ_EXPORT int mz_deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy); + + /* Quickly resets a compressor without having to reallocate anything. Same as calling mz_deflateEnd() followed by mz_deflateInit()/mz_deflateInit2(). */ + MINIZ_EXPORT int mz_deflateReset(mz_streamp pStream); + + /* mz_deflate() compresses the input to output, consuming as much of the input and producing as much output as possible. */ + /* Parameters: */ + /* pStream is the stream to read from and write to. You must initialize/update the next_in, avail_in, next_out, and avail_out members. */ + /* flush may be MZ_NO_FLUSH, MZ_PARTIAL_FLUSH/MZ_SYNC_FLUSH, MZ_FULL_FLUSH, or MZ_FINISH. */ + /* Return values: */ + /* MZ_OK on success (when flushing, or if more input is needed but not available, and/or there's more output to be written but the output buffer is full). */ + /* MZ_STREAM_END if all input has been consumed and all output bytes have been written. Don't call mz_deflate() on the stream anymore. */ + /* MZ_STREAM_ERROR if the stream is bogus. */ + /* MZ_PARAM_ERROR if one of the parameters is invalid. */ + /* MZ_BUF_ERROR if no forward progress is possible because the input and/or output buffers are empty. (Fill up the input buffer or free up some output space and try again.) */ + MINIZ_EXPORT int mz_deflate(mz_streamp pStream, int flush); + + /* mz_deflateEnd() deinitializes a compressor: */ + /* Return values: */ + /* MZ_OK on success. */ + /* MZ_STREAM_ERROR if the stream is bogus. */ + MINIZ_EXPORT int mz_deflateEnd(mz_streamp pStream); + + /* mz_deflateBound() returns a (very) conservative upper bound on the amount of data that could be generated by deflate(), assuming flush is set to only MZ_NO_FLUSH or MZ_FINISH. */ + MINIZ_EXPORT mz_ulong mz_deflateBound(mz_streamp pStream, mz_ulong source_len); + + /* Single-call compression functions mz_compress() and mz_compress2(): */ + /* Returns MZ_OK on success, or one of the error codes from mz_deflate() on failure. */ + MINIZ_EXPORT int mz_compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len); + MINIZ_EXPORT int mz_compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level); + + /* mz_compressBound() returns a (very) conservative upper bound on the amount of data that could be generated by calling mz_compress(). */ + MINIZ_EXPORT mz_ulong mz_compressBound(mz_ulong source_len); + +#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ + +#ifndef MINIZ_NO_INFLATE_APIS + + /* Initializes a decompressor. */ + MINIZ_EXPORT int mz_inflateInit(mz_streamp pStream); + + /* mz_inflateInit2() is like mz_inflateInit() with an additional option that controls the window size and whether or not the stream has been wrapped with a zlib header/footer: */ + /* window_bits must be MZ_DEFAULT_WINDOW_BITS (to parse zlib header/footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate). */ + MINIZ_EXPORT int mz_inflateInit2(mz_streamp pStream, int window_bits); + + /* Quickly resets a compressor without having to reallocate anything. Same as calling mz_inflateEnd() followed by mz_inflateInit()/mz_inflateInit2(). */ + MINIZ_EXPORT int mz_inflateReset(mz_streamp pStream); + + /* Decompresses the input stream to the output, consuming only as much of the input as needed, and writing as much to the output as possible. */ + /* Parameters: */ + /* pStream is the stream to read from and write to. You must initialize/update the next_in, avail_in, next_out, and avail_out members. */ + /* flush may be MZ_NO_FLUSH, MZ_SYNC_FLUSH, or MZ_FINISH. */ + /* On the first call, if flush is MZ_FINISH it's assumed the input and output buffers are both sized large enough to decompress the entire stream in a single call (this is slightly faster). */ + /* MZ_FINISH implies that there are no more source bytes available beside what's already in the input buffer, and that the output buffer is large enough to hold the rest of the decompressed data. */ + /* Return values: */ + /* MZ_OK on success. Either more input is needed but not available, and/or there's more output to be written but the output buffer is full. */ + /* MZ_STREAM_END if all needed input has been consumed and all output bytes have been written. For zlib streams, the adler-32 of the decompressed data has also been verified. */ + /* MZ_STREAM_ERROR if the stream is bogus. */ + /* MZ_DATA_ERROR if the deflate stream is invalid. */ + /* MZ_PARAM_ERROR if one of the parameters is invalid. */ + /* MZ_BUF_ERROR if no forward progress is possible because the input buffer is empty but the inflater needs more input to continue, or if the output buffer is not large enough. Call mz_inflate() again */ + /* with more input data, or with more room in the output buffer (except when using single call decompression, described above). */ + MINIZ_EXPORT int mz_inflate(mz_streamp pStream, int flush); + + /* Deinitializes a decompressor. */ + MINIZ_EXPORT int mz_inflateEnd(mz_streamp pStream); + + /* Single-call decompression. */ + /* Returns MZ_OK on success, or one of the error codes from mz_inflate() on failure. */ + MINIZ_EXPORT int mz_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len); + MINIZ_EXPORT int mz_uncompress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len); +#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ + + /* Returns a string description of the specified error code, or NULL if the error code is invalid. */ + MINIZ_EXPORT const char *mz_error(int err); + +/* Redefine zlib-compatible names to miniz equivalents, so miniz.c can be used as a drop-in replacement for the subset of zlib that miniz.c supports. */ +/* Define MINIZ_NO_ZLIB_COMPATIBLE_NAMES to disable zlib-compatibility if you use zlib in the same project. */ +#ifndef MINIZ_NO_ZLIB_COMPATIBLE_NAMES + typedef unsigned char Byte; + typedef unsigned int uInt; + typedef mz_ulong uLong; + typedef Byte Bytef; + typedef uInt uIntf; + typedef char charf; + typedef int intf; + typedef void *voidpf; + typedef uLong uLongf; + typedef void *voidp; + typedef void *const voidpc; +#define Z_NULL 0 +#define Z_NO_FLUSH MZ_NO_FLUSH +#define Z_PARTIAL_FLUSH MZ_PARTIAL_FLUSH +#define Z_SYNC_FLUSH MZ_SYNC_FLUSH +#define Z_FULL_FLUSH MZ_FULL_FLUSH +#define Z_FINISH MZ_FINISH +#define Z_BLOCK MZ_BLOCK +#define Z_OK MZ_OK +#define Z_STREAM_END MZ_STREAM_END +#define Z_NEED_DICT MZ_NEED_DICT +#define Z_ERRNO MZ_ERRNO +#define Z_STREAM_ERROR MZ_STREAM_ERROR +#define Z_DATA_ERROR MZ_DATA_ERROR +#define Z_MEM_ERROR MZ_MEM_ERROR +#define Z_BUF_ERROR MZ_BUF_ERROR +#define Z_VERSION_ERROR MZ_VERSION_ERROR +#define Z_PARAM_ERROR MZ_PARAM_ERROR +#define Z_NO_COMPRESSION MZ_NO_COMPRESSION +#define Z_BEST_SPEED MZ_BEST_SPEED +#define Z_BEST_COMPRESSION MZ_BEST_COMPRESSION +#define Z_DEFAULT_COMPRESSION MZ_DEFAULT_COMPRESSION +#define Z_DEFAULT_STRATEGY MZ_DEFAULT_STRATEGY +#define Z_FILTERED MZ_FILTERED +#define Z_HUFFMAN_ONLY MZ_HUFFMAN_ONLY +#define Z_RLE MZ_RLE +#define Z_FIXED MZ_FIXED +#define Z_DEFLATED MZ_DEFLATED +#define Z_DEFAULT_WINDOW_BITS MZ_DEFAULT_WINDOW_BITS + /* See mz_alloc_func */ + typedef void *(*alloc_func)(void *opaque, size_t items, size_t size); + /* See mz_free_func */ + typedef void (*free_func)(void *opaque, void *address); + +#define internal_state mz_internal_state +#define z_stream mz_stream + +#ifndef MINIZ_NO_DEFLATE_APIS + /* Compatiblity with zlib API. See called functions for documentation */ + static MZ_FORCEINLINE int deflateInit(mz_streamp pStream, int level) + { + return mz_deflateInit(pStream, level); + } + static MZ_FORCEINLINE int deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy) + { + return mz_deflateInit2(pStream, level, method, window_bits, mem_level, strategy); + } + static MZ_FORCEINLINE int deflateReset(mz_streamp pStream) + { + return mz_deflateReset(pStream); + } + static MZ_FORCEINLINE int deflate(mz_streamp pStream, int flush) + { + return mz_deflate(pStream, flush); + } + static MZ_FORCEINLINE int deflateEnd(mz_streamp pStream) + { + return mz_deflateEnd(pStream); + } + static MZ_FORCEINLINE mz_ulong deflateBound(mz_streamp pStream, mz_ulong source_len) + { + return mz_deflateBound(pStream, source_len); + } + static MZ_FORCEINLINE int compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len) + { + return mz_compress(pDest, pDest_len, pSource, source_len); + } + static MZ_FORCEINLINE int compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level) + { + return mz_compress2(pDest, pDest_len, pSource, source_len, level); + } + static MZ_FORCEINLINE mz_ulong compressBound(mz_ulong source_len) + { + return mz_compressBound(source_len); + } +#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ + +#ifndef MINIZ_NO_INFLATE_APIS + /* Compatiblity with zlib API. See called functions for documentation */ + static MZ_FORCEINLINE int inflateInit(mz_streamp pStream) + { + return mz_inflateInit(pStream); + } + + static MZ_FORCEINLINE int inflateInit2(mz_streamp pStream, int window_bits) + { + return mz_inflateInit2(pStream, window_bits); + } + + static MZ_FORCEINLINE int inflateReset(mz_streamp pStream) + { + return mz_inflateReset(pStream); + } + + static MZ_FORCEINLINE int inflate(mz_streamp pStream, int flush) + { + return mz_inflate(pStream, flush); + } + + static MZ_FORCEINLINE int inflateEnd(mz_streamp pStream) + { + return mz_inflateEnd(pStream); + } + + static MZ_FORCEINLINE int uncompress(unsigned char* pDest, mz_ulong* pDest_len, const unsigned char* pSource, mz_ulong source_len) + { + return mz_uncompress(pDest, pDest_len, pSource, source_len); + } + + static MZ_FORCEINLINE int uncompress2(unsigned char* pDest, mz_ulong* pDest_len, const unsigned char* pSource, mz_ulong* pSource_len) + { + return mz_uncompress2(pDest, pDest_len, pSource, pSource_len); + } +#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ + + static MZ_FORCEINLINE mz_ulong crc32(mz_ulong crc, const unsigned char *ptr, size_t buf_len) + { + return mz_crc32(crc, ptr, buf_len); + } + + static MZ_FORCEINLINE mz_ulong adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len) + { + return mz_adler32(adler, ptr, buf_len); + } + +#define MAX_WBITS 15 +#define MAX_MEM_LEVEL 9 + + static MZ_FORCEINLINE const char* zError(int err) + { + return mz_error(err); + } +#define ZLIB_VERSION MZ_VERSION +#define ZLIB_VERNUM MZ_VERNUM +#define ZLIB_VER_MAJOR MZ_VER_MAJOR +#define ZLIB_VER_MINOR MZ_VER_MINOR +#define ZLIB_VER_REVISION MZ_VER_REVISION +#define ZLIB_VER_SUBREVISION MZ_VER_SUBREVISION + +#define zlibVersion mz_version +#define zlib_version mz_version() +#endif /* #ifndef MINIZ_NO_ZLIB_COMPATIBLE_NAMES */ + +#endif /* MINIZ_NO_ZLIB_APIS */ + +#ifdef __cplusplus +} +#endif + + + + + +#pragma once +#include +#include +#include +#include + + + +/* ------------------- Types and macros */ +typedef unsigned char mz_uint8; +typedef int16_t mz_int16; +typedef uint16_t mz_uint16; +typedef uint32_t mz_uint32; +typedef uint32_t mz_uint; +typedef int64_t mz_int64; +typedef uint64_t mz_uint64; +typedef int mz_bool; + +#define MZ_FALSE (0) +#define MZ_TRUE (1) + +/* Works around MSVC's spammy "warning C4127: conditional expression is constant" message. */ +#ifdef _MSC_VER +#define MZ_MACRO_END while (0, 0) +#else +#define MZ_MACRO_END while (0) +#endif + +#ifdef MINIZ_NO_STDIO +#define MZ_FILE void * +#else +#include +#define MZ_FILE FILE +#endif /* #ifdef MINIZ_NO_STDIO */ + +#ifdef MINIZ_NO_TIME +typedef struct mz_dummy_time_t_tag +{ + mz_uint32 m_dummy1; + mz_uint32 m_dummy2; +} mz_dummy_time_t; +#define MZ_TIME_T mz_dummy_time_t +#else +#define MZ_TIME_T time_t +#endif + +#define MZ_ASSERT(x) assert(x) + +#ifdef MINIZ_NO_MALLOC +#define MZ_MALLOC(x) NULL +#define MZ_FREE(x) (void)x, ((void)0) +#define MZ_REALLOC(p, x) NULL +#else +#define MZ_MALLOC(x) malloc(x) +#define MZ_FREE(x) free(x) +#define MZ_REALLOC(p, x) realloc(p, x) +#endif + +#define MZ_MAX(a, b) (((a) > (b)) ? (a) : (b)) +#define MZ_MIN(a, b) (((a) < (b)) ? (a) : (b)) +#define MZ_CLEAR_OBJ(obj) memset(&(obj), 0, sizeof(obj)) +#define MZ_CLEAR_ARR(obj) memset((obj), 0, sizeof(obj)) +#define MZ_CLEAR_PTR(obj) memset((obj), 0, sizeof(*obj)) + +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN +#define MZ_READ_LE16(p) *((const mz_uint16 *)(p)) +#define MZ_READ_LE32(p) *((const mz_uint32 *)(p)) +#else +#define MZ_READ_LE16(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U)) +#define MZ_READ_LE32(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U) | ((mz_uint32)(((const mz_uint8 *)(p))[2]) << 16U) | ((mz_uint32)(((const mz_uint8 *)(p))[3]) << 24U)) +#endif + +#define MZ_READ_LE64(p) (((mz_uint64)MZ_READ_LE32(p)) | (((mz_uint64)MZ_READ_LE32((const mz_uint8 *)(p) + sizeof(mz_uint32))) << 32U)) + +#ifdef __cplusplus +extern "C" +{ +#endif + + extern MINIZ_EXPORT void *miniz_def_alloc_func(void *opaque, size_t items, size_t size); + extern MINIZ_EXPORT void miniz_def_free_func(void *opaque, void *address); + extern MINIZ_EXPORT void *miniz_def_realloc_func(void *opaque, void *address, size_t items, size_t size); + +#define MZ_UINT16_MAX (0xFFFFU) +#define MZ_UINT32_MAX (0xFFFFFFFFU) + +#ifdef __cplusplus +} +#endif + #pragma once + + +#ifndef MINIZ_NO_DEFLATE_APIS + +#ifdef __cplusplus +extern "C" +{ +#endif +/* ------------------- Low-level Compression API Definitions */ + +/* Set TDEFL_LESS_MEMORY to 1 to use less memory (compression will be slightly slower, and raw/dynamic blocks will be output more frequently). */ +#ifndef TDEFL_LESS_MEMORY +#define TDEFL_LESS_MEMORY 0 +#endif + + /* tdefl_init() compression flags logically OR'd together (low 12 bits contain the max. number of probes per dictionary search): */ + /* TDEFL_DEFAULT_MAX_PROBES: The compressor defaults to 128 dictionary probes per dictionary search. 0=Huffman only, 1=Huffman+LZ (fastest/crap compression), 4095=Huffman+LZ (slowest/best compression). */ + enum + { + TDEFL_HUFFMAN_ONLY = 0, + TDEFL_DEFAULT_MAX_PROBES = 128, + TDEFL_MAX_PROBES_MASK = 0xFFF + }; + + /* TDEFL_WRITE_ZLIB_HEADER: If set, the compressor outputs a zlib header before the deflate data, and the Adler-32 of the source data at the end. Otherwise, you'll get raw deflate data. */ + /* TDEFL_COMPUTE_ADLER32: Always compute the adler-32 of the input data (even when not writing zlib headers). */ + /* TDEFL_GREEDY_PARSING_FLAG: Set to use faster greedy parsing, instead of more efficient lazy parsing. */ + /* TDEFL_NONDETERMINISTIC_PARSING_FLAG: Enable to decrease the compressor's initialization time to the minimum, but the output may vary from run to run given the same input (depending on the contents of memory). */ + /* TDEFL_RLE_MATCHES: Only look for RLE matches (matches with a distance of 1) */ + /* TDEFL_FILTER_MATCHES: Discards matches <= 5 chars if enabled. */ + /* TDEFL_FORCE_ALL_STATIC_BLOCKS: Disable usage of optimized Huffman tables. */ + /* TDEFL_FORCE_ALL_RAW_BLOCKS: Only use raw (uncompressed) deflate blocks. */ + /* The low 12 bits are reserved to control the max # of hash probes per dictionary lookup (see TDEFL_MAX_PROBES_MASK). */ + enum + { + TDEFL_WRITE_ZLIB_HEADER = 0x01000, + TDEFL_COMPUTE_ADLER32 = 0x02000, + TDEFL_GREEDY_PARSING_FLAG = 0x04000, + TDEFL_NONDETERMINISTIC_PARSING_FLAG = 0x08000, + TDEFL_RLE_MATCHES = 0x10000, + TDEFL_FILTER_MATCHES = 0x20000, + TDEFL_FORCE_ALL_STATIC_BLOCKS = 0x40000, + TDEFL_FORCE_ALL_RAW_BLOCKS = 0x80000 + }; + + /* High level compression functions: */ + /* tdefl_compress_mem_to_heap() compresses a block in memory to a heap block allocated via malloc(). */ + /* On entry: */ + /* pSrc_buf, src_buf_len: Pointer and size of source block to compress. */ + /* flags: The max match finder probes (default is 128) logically OR'd against the above flags. Higher probes are slower but improve compression. */ + /* On return: */ + /* Function returns a pointer to the compressed data, or NULL on failure. */ + /* *pOut_len will be set to the compressed data's size, which could be larger than src_buf_len on uncompressible data. */ + /* The caller must free() the returned block when it's no longer needed. */ + MINIZ_EXPORT void *tdefl_compress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags); + + /* tdefl_compress_mem_to_mem() compresses a block in memory to another block in memory. */ + /* Returns 0 on failure. */ + MINIZ_EXPORT size_t tdefl_compress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags); + + /* Compresses an image to a compressed PNG file in memory. */ + /* On entry: */ + /* pImage, w, h, and num_chans describe the image to compress. num_chans may be 1, 2, 3, or 4. */ + /* The image pitch in bytes per scanline will be w*num_chans. The leftmost pixel on the top scanline is stored first in memory. */ + /* level may range from [0,10], use MZ_NO_COMPRESSION, MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc. or a decent default is MZ_DEFAULT_LEVEL */ + /* If flip is true, the image will be flipped on the Y axis (useful for OpenGL apps). */ + /* On return: */ + /* Function returns a pointer to the compressed data, or NULL on failure. */ + /* *pLen_out will be set to the size of the PNG image file. */ + /* The caller must mz_free() the returned heap block (which will typically be larger than *pLen_out) when it's no longer needed. */ + MINIZ_EXPORT void *tdefl_write_image_to_png_file_in_memory_ex(const void *pImage, int w, int h, int num_chans, size_t *pLen_out, mz_uint level, mz_bool flip); + MINIZ_EXPORT void *tdefl_write_image_to_png_file_in_memory(const void *pImage, int w, int h, int num_chans, size_t *pLen_out); + + /* Output stream interface. The compressor uses this interface to write compressed data. It'll typically be called TDEFL_OUT_BUF_SIZE at a time. */ + typedef mz_bool (*tdefl_put_buf_func_ptr)(const void *pBuf, int len, void *pUser); + + /* tdefl_compress_mem_to_output() compresses a block to an output stream. The above helpers use this function internally. */ + MINIZ_EXPORT mz_bool tdefl_compress_mem_to_output(const void *pBuf, size_t buf_len, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags); + + enum + { + TDEFL_MAX_HUFF_TABLES = 3, + TDEFL_MAX_HUFF_SYMBOLS_0 = 288, + TDEFL_MAX_HUFF_SYMBOLS_1 = 32, + TDEFL_MAX_HUFF_SYMBOLS_2 = 19, + TDEFL_LZ_DICT_SIZE = 32768, + TDEFL_LZ_DICT_SIZE_MASK = TDEFL_LZ_DICT_SIZE - 1, + TDEFL_MIN_MATCH_LEN = 3, + TDEFL_MAX_MATCH_LEN = 258 + }; + +/* TDEFL_OUT_BUF_SIZE MUST be large enough to hold a single entire compressed output block (using static/fixed Huffman codes). */ +#if TDEFL_LESS_MEMORY + enum + { + TDEFL_LZ_CODE_BUF_SIZE = 24 * 1024, + TDEFL_OUT_BUF_SIZE = (TDEFL_LZ_CODE_BUF_SIZE * 13) / 10, + TDEFL_MAX_HUFF_SYMBOLS = 288, + TDEFL_LZ_HASH_BITS = 12, + TDEFL_LEVEL1_HASH_SIZE_MASK = 4095, + TDEFL_LZ_HASH_SHIFT = (TDEFL_LZ_HASH_BITS + 2) / 3, + TDEFL_LZ_HASH_SIZE = 1 << TDEFL_LZ_HASH_BITS + }; +#else +enum +{ + TDEFL_LZ_CODE_BUF_SIZE = 64 * 1024, + TDEFL_OUT_BUF_SIZE = (mz_uint)((TDEFL_LZ_CODE_BUF_SIZE * 13) / 10), + TDEFL_MAX_HUFF_SYMBOLS = 288, + TDEFL_LZ_HASH_BITS = 15, + TDEFL_LEVEL1_HASH_SIZE_MASK = 4095, + TDEFL_LZ_HASH_SHIFT = (TDEFL_LZ_HASH_BITS + 2) / 3, + TDEFL_LZ_HASH_SIZE = 1 << TDEFL_LZ_HASH_BITS +}; +#endif + + /* The low-level tdefl functions below may be used directly if the above helper functions aren't flexible enough. The low-level functions don't make any heap allocations, unlike the above helper functions. */ + typedef enum + { + TDEFL_STATUS_BAD_PARAM = -2, + TDEFL_STATUS_PUT_BUF_FAILED = -1, + TDEFL_STATUS_OKAY = 0, + TDEFL_STATUS_DONE = 1 + } tdefl_status; + + /* Must map to MZ_NO_FLUSH, MZ_SYNC_FLUSH, etc. enums */ + typedef enum + { + TDEFL_NO_FLUSH = 0, + TDEFL_SYNC_FLUSH = 2, + TDEFL_FULL_FLUSH = 3, + TDEFL_FINISH = 4 + } tdefl_flush; + + /* tdefl's compression state structure. */ + typedef struct + { + tdefl_put_buf_func_ptr m_pPut_buf_func; + void *m_pPut_buf_user; + mz_uint m_flags, m_max_probes[2]; + int m_greedy_parsing; + mz_uint m_adler32, m_lookahead_pos, m_lookahead_size, m_dict_size; + mz_uint8 *m_pLZ_code_buf, *m_pLZ_flags, *m_pOutput_buf, *m_pOutput_buf_end; + mz_uint m_num_flags_left, m_total_lz_bytes, m_lz_code_buf_dict_pos, m_bits_in, m_bit_buffer; + mz_uint m_saved_match_dist, m_saved_match_len, m_saved_lit, m_output_flush_ofs, m_output_flush_remaining, m_finished, m_block_index, m_wants_to_finish; + tdefl_status m_prev_return_status; + const void *m_pIn_buf; + void *m_pOut_buf; + size_t *m_pIn_buf_size, *m_pOut_buf_size; + tdefl_flush m_flush; + const mz_uint8 *m_pSrc; + size_t m_src_buf_left, m_out_buf_ofs; + mz_uint8 m_dict[TDEFL_LZ_DICT_SIZE + TDEFL_MAX_MATCH_LEN - 1]; + mz_uint16 m_huff_count[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS]; + mz_uint16 m_huff_codes[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS]; + mz_uint8 m_huff_code_sizes[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS]; + mz_uint8 m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE]; + mz_uint16 m_next[TDEFL_LZ_DICT_SIZE]; + mz_uint16 m_hash[TDEFL_LZ_HASH_SIZE]; + mz_uint8 m_output_buf[TDEFL_OUT_BUF_SIZE]; + } tdefl_compressor; + + /* Initializes the compressor. */ + /* There is no corresponding deinit() function because the tdefl API's do not dynamically allocate memory. */ + /* pBut_buf_func: If NULL, output data will be supplied to the specified callback. In this case, the user should call the tdefl_compress_buffer() API for compression. */ + /* If pBut_buf_func is NULL the user should always call the tdefl_compress() API. */ + /* flags: See the above enums (TDEFL_HUFFMAN_ONLY, TDEFL_WRITE_ZLIB_HEADER, etc.) */ + MINIZ_EXPORT tdefl_status tdefl_init(tdefl_compressor *d, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags); + + /* Compresses a block of data, consuming as much of the specified input buffer as possible, and writing as much compressed data to the specified output buffer as possible. */ + MINIZ_EXPORT tdefl_status tdefl_compress(tdefl_compressor *d, const void *pIn_buf, size_t *pIn_buf_size, void *pOut_buf, size_t *pOut_buf_size, tdefl_flush flush); + + /* tdefl_compress_buffer() is only usable when the tdefl_init() is called with a non-NULL tdefl_put_buf_func_ptr. */ + /* tdefl_compress_buffer() always consumes the entire input buffer. */ + MINIZ_EXPORT tdefl_status tdefl_compress_buffer(tdefl_compressor *d, const void *pIn_buf, size_t in_buf_size, tdefl_flush flush); + + MINIZ_EXPORT tdefl_status tdefl_get_prev_return_status(tdefl_compressor *d); + MINIZ_EXPORT mz_uint32 tdefl_get_adler32(tdefl_compressor *d); + + /* Create tdefl_compress() flags given zlib-style compression parameters. */ + /* level may range from [0,10] (where 10 is absolute max compression, but may be much slower on some files) */ + /* window_bits may be -15 (raw deflate) or 15 (zlib) */ + /* strategy may be either MZ_DEFAULT_STRATEGY, MZ_FILTERED, MZ_HUFFMAN_ONLY, MZ_RLE, or MZ_FIXED */ + MINIZ_EXPORT mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy); + +#ifndef MINIZ_NO_MALLOC + /* Allocate the tdefl_compressor structure in C so that */ + /* non-C language bindings to tdefl_ API don't need to worry about */ + /* structure size and allocation mechanism. */ + MINIZ_EXPORT tdefl_compressor *tdefl_compressor_alloc(void); + MINIZ_EXPORT void tdefl_compressor_free(tdefl_compressor *pComp); +#endif + +#ifdef __cplusplus +} +#endif + +#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ + #pragma once + +/* ------------------- Low-level Decompression API Definitions */ + +#ifndef MINIZ_NO_INFLATE_APIS + +#ifdef __cplusplus +extern "C" +{ +#endif + /* Decompression flags used by tinfl_decompress(). */ + /* TINFL_FLAG_PARSE_ZLIB_HEADER: If set, the input has a valid zlib header and ends with an adler32 checksum (it's a valid zlib stream). Otherwise, the input is a raw deflate stream. */ + /* TINFL_FLAG_HAS_MORE_INPUT: If set, there are more input bytes available beyond the end of the supplied input buffer. If clear, the input buffer contains all remaining input. */ + /* TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF: If set, the output buffer is large enough to hold the entire decompressed stream. If clear, the output buffer is at least the size of the dictionary (typically 32KB). */ + /* TINFL_FLAG_COMPUTE_ADLER32: Force adler-32 checksum computation of the decompressed bytes. */ + enum + { + TINFL_FLAG_PARSE_ZLIB_HEADER = 1, + TINFL_FLAG_HAS_MORE_INPUT = 2, + TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF = 4, + TINFL_FLAG_COMPUTE_ADLER32 = 8 + }; + + /* High level decompression functions: */ + /* tinfl_decompress_mem_to_heap() decompresses a block in memory to a heap block allocated via malloc(). */ + /* On entry: */ + /* pSrc_buf, src_buf_len: Pointer and size of the Deflate or zlib source data to decompress. */ + /* On return: */ + /* Function returns a pointer to the decompressed data, or NULL on failure. */ + /* *pOut_len will be set to the decompressed data's size, which could be larger than src_buf_len on uncompressible data. */ + /* The caller must call mz_free() on the returned block when it's no longer needed. */ + MINIZ_EXPORT void *tinfl_decompress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags); + +/* tinfl_decompress_mem_to_mem() decompresses a block in memory to another block in memory. */ +/* Returns TINFL_DECOMPRESS_MEM_TO_MEM_FAILED on failure, or the number of bytes written on success. */ +#define TINFL_DECOMPRESS_MEM_TO_MEM_FAILED ((size_t)(-1)) + MINIZ_EXPORT size_t tinfl_decompress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags); + + /* tinfl_decompress_mem_to_callback() decompresses a block in memory to an internal 32KB buffer, and a user provided callback function will be called to flush the buffer. */ + /* Returns 1 on success or 0 on failure. */ + typedef int (*tinfl_put_buf_func_ptr)(const void *pBuf, int len, void *pUser); + MINIZ_EXPORT int tinfl_decompress_mem_to_callback(const void *pIn_buf, size_t *pIn_buf_size, tinfl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags); + + struct tinfl_decompressor_tag; + typedef struct tinfl_decompressor_tag tinfl_decompressor; + +#ifndef MINIZ_NO_MALLOC + /* Allocate the tinfl_decompressor structure in C so that */ + /* non-C language bindings to tinfl_ API don't need to worry about */ + /* structure size and allocation mechanism. */ + MINIZ_EXPORT tinfl_decompressor *tinfl_decompressor_alloc(void); + MINIZ_EXPORT void tinfl_decompressor_free(tinfl_decompressor *pDecomp); +#endif + +/* Max size of LZ dictionary. */ +#define TINFL_LZ_DICT_SIZE 32768 + + /* Return status. */ + typedef enum + { + /* This flags indicates the inflator needs 1 or more input bytes to make forward progress, but the caller is indicating that no more are available. The compressed data */ + /* is probably corrupted. If you call the inflator again with more bytes it'll try to continue processing the input but this is a BAD sign (either the data is corrupted or you called it incorrectly). */ + /* If you call it again with no input you'll just get TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS again. */ + TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS = -4, + + /* This flag indicates that one or more of the input parameters was obviously bogus. (You can try calling it again, but if you get this error the calling code is wrong.) */ + TINFL_STATUS_BAD_PARAM = -3, + + /* This flags indicate the inflator is finished but the adler32 check of the uncompressed data didn't match. If you call it again it'll return TINFL_STATUS_DONE. */ + TINFL_STATUS_ADLER32_MISMATCH = -2, + + /* This flags indicate the inflator has somehow failed (bad code, corrupted input, etc.). If you call it again without resetting via tinfl_init() it it'll just keep on returning the same status failure code. */ + TINFL_STATUS_FAILED = -1, + + /* Any status code less than TINFL_STATUS_DONE must indicate a failure. */ + + /* This flag indicates the inflator has returned every byte of uncompressed data that it can, has consumed every byte that it needed, has successfully reached the end of the deflate stream, and */ + /* if zlib headers and adler32 checking enabled that it has successfully checked the uncompressed data's adler32. If you call it again you'll just get TINFL_STATUS_DONE over and over again. */ + TINFL_STATUS_DONE = 0, + + /* This flag indicates the inflator MUST have more input data (even 1 byte) before it can make any more forward progress, or you need to clear the TINFL_FLAG_HAS_MORE_INPUT */ + /* flag on the next call if you don't have any more source data. If the source data was somehow corrupted it's also possible (but unlikely) for the inflator to keep on demanding input to */ + /* proceed, so be sure to properly set the TINFL_FLAG_HAS_MORE_INPUT flag. */ + TINFL_STATUS_NEEDS_MORE_INPUT = 1, + + /* This flag indicates the inflator definitely has 1 or more bytes of uncompressed data available, but it cannot write this data into the output buffer. */ + /* Note if the source compressed data was corrupted it's possible for the inflator to return a lot of uncompressed data to the caller. I've been assuming you know how much uncompressed data to expect */ + /* (either exact or worst case) and will stop calling the inflator and fail after receiving too much. In pure streaming scenarios where you have no idea how many bytes to expect this may not be possible */ + /* so I may need to add some code to address this. */ + TINFL_STATUS_HAS_MORE_OUTPUT = 2 + } tinfl_status; + +/* Initializes the decompressor to its initial state. */ +#define tinfl_init(r) \ + do \ + { \ + (r)->m_state = 0; \ + } \ + MZ_MACRO_END +#define tinfl_get_adler32(r) (r)->m_check_adler32 + + /* Main low-level decompressor coroutine function. This is the only function actually needed for decompression. All the other functions are just high-level helpers for improved usability. */ + /* This is a universal API, i.e. it can be used as a building block to build any desired higher level decompression API. In the limit case, it can be called once per every byte input or output. */ + MINIZ_EXPORT tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags); + + /* Internal/private bits follow. */ + enum + { + TINFL_MAX_HUFF_TABLES = 3, + TINFL_MAX_HUFF_SYMBOLS_0 = 288, + TINFL_MAX_HUFF_SYMBOLS_1 = 32, + TINFL_MAX_HUFF_SYMBOLS_2 = 19, + TINFL_FAST_LOOKUP_BITS = 10, + TINFL_FAST_LOOKUP_SIZE = 1 << TINFL_FAST_LOOKUP_BITS + }; + +#if MINIZ_HAS_64BIT_REGISTERS +#define TINFL_USE_64BIT_BITBUF 1 +#else +#define TINFL_USE_64BIT_BITBUF 0 +#endif + +#if TINFL_USE_64BIT_BITBUF + typedef mz_uint64 tinfl_bit_buf_t; +#define TINFL_BITBUF_SIZE (64) +#else +typedef mz_uint32 tinfl_bit_buf_t; +#define TINFL_BITBUF_SIZE (32) +#endif + + struct tinfl_decompressor_tag + { + mz_uint32 m_state, m_num_bits, m_zhdr0, m_zhdr1, m_z_adler32, m_final, m_type, m_check_adler32, m_dist, m_counter, m_num_extra, m_table_sizes[TINFL_MAX_HUFF_TABLES]; + tinfl_bit_buf_t m_bit_buf; + size_t m_dist_from_out_buf_start; + mz_int16 m_look_up[TINFL_MAX_HUFF_TABLES][TINFL_FAST_LOOKUP_SIZE]; + mz_int16 m_tree_0[TINFL_MAX_HUFF_SYMBOLS_0 * 2]; + mz_int16 m_tree_1[TINFL_MAX_HUFF_SYMBOLS_1 * 2]; + mz_int16 m_tree_2[TINFL_MAX_HUFF_SYMBOLS_2 * 2]; + mz_uint8 m_code_size_0[TINFL_MAX_HUFF_SYMBOLS_0]; + mz_uint8 m_code_size_1[TINFL_MAX_HUFF_SYMBOLS_1]; + mz_uint8 m_code_size_2[TINFL_MAX_HUFF_SYMBOLS_2]; + mz_uint8 m_raw_header[4], m_len_codes[TINFL_MAX_HUFF_SYMBOLS_0 + TINFL_MAX_HUFF_SYMBOLS_1 + 137]; + }; + +#ifdef __cplusplus +} +#endif + +#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ + +#pragma once + + +/* ------------------- ZIP archive reading/writing */ + +#ifndef MINIZ_NO_ARCHIVE_APIS + +#ifdef __cplusplus +extern "C" +{ +#endif + + enum + { + /* Note: These enums can be reduced as needed to save memory or stack space - they are pretty conservative. */ + MZ_ZIP_MAX_IO_BUF_SIZE = 64 * 1024, + MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE = 512, + MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE = 512 + }; + + typedef struct + { + /* Central directory file index. */ + mz_uint32 m_file_index; + + /* Byte offset of this entry in the archive's central directory. Note we currently only support up to UINT_MAX or less bytes in the central dir. */ + mz_uint64 m_central_dir_ofs; + + /* These fields are copied directly from the zip's central dir. */ + mz_uint16 m_version_made_by; + mz_uint16 m_version_needed; + mz_uint16 m_bit_flag; + mz_uint16 m_method; + + /* CRC-32 of uncompressed data. */ + mz_uint32 m_crc32; + + /* File's compressed size. */ + mz_uint64 m_comp_size; + + /* File's uncompressed size. Note, I've seen some old archives where directory entries had 512 bytes for their uncompressed sizes, but when you try to unpack them you actually get 0 bytes. */ + mz_uint64 m_uncomp_size; + + /* Zip internal and external file attributes. */ + mz_uint16 m_internal_attr; + mz_uint32 m_external_attr; + + /* Entry's local header file offset in bytes. */ + mz_uint64 m_local_header_ofs; + + /* Size of comment in bytes. */ + mz_uint32 m_comment_size; + + /* MZ_TRUE if the entry appears to be a directory. */ + mz_bool m_is_directory; + + /* MZ_TRUE if the entry uses encryption/strong encryption (which miniz_zip doesn't support) */ + mz_bool m_is_encrypted; + + /* MZ_TRUE if the file is not encrypted, a patch file, and if it uses a compression method we support. */ + mz_bool m_is_supported; + + /* Filename. If string ends in '/' it's a subdirectory entry. */ + /* Guaranteed to be zero terminated, may be truncated to fit. */ + char m_filename[MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE]; + + /* Comment field. */ + /* Guaranteed to be zero terminated, may be truncated to fit. */ + char m_comment[MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE]; + +#ifdef MINIZ_NO_TIME + MZ_TIME_T m_padding; +#else + MZ_TIME_T m_time; +#endif + } mz_zip_archive_file_stat; + + typedef size_t (*mz_file_read_func)(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n); + typedef size_t (*mz_file_write_func)(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n); + typedef mz_bool (*mz_file_needs_keepalive)(void *pOpaque); + + struct mz_zip_internal_state_tag; + typedef struct mz_zip_internal_state_tag mz_zip_internal_state; + + typedef enum + { + MZ_ZIP_MODE_INVALID = 0, + MZ_ZIP_MODE_READING = 1, + MZ_ZIP_MODE_WRITING = 2, + MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED = 3 + } mz_zip_mode; + + typedef enum + { + MZ_ZIP_FLAG_CASE_SENSITIVE = 0x0100, + MZ_ZIP_FLAG_IGNORE_PATH = 0x0200, + MZ_ZIP_FLAG_COMPRESSED_DATA = 0x0400, + MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY = 0x0800, + MZ_ZIP_FLAG_VALIDATE_LOCATE_FILE_FLAG = 0x1000, /* if enabled, mz_zip_reader_locate_file() will be called on each file as its validated to ensure the func finds the file in the central dir (intended for testing) */ + MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY = 0x2000, /* validate the local headers, but don't decompress the entire file and check the crc32 */ + MZ_ZIP_FLAG_WRITE_ZIP64 = 0x4000, /* always use the zip64 file format, instead of the original zip file format with automatic switch to zip64. Use as flags parameter with mz_zip_writer_init*_v2 */ + MZ_ZIP_FLAG_WRITE_ALLOW_READING = 0x8000, + MZ_ZIP_FLAG_ASCII_FILENAME = 0x10000, + /*After adding a compressed file, seek back + to local file header and set the correct sizes*/ + MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE = 0x20000, + MZ_ZIP_FLAG_READ_ALLOW_WRITING = 0x40000 + } mz_zip_flags; + + typedef enum + { + MZ_ZIP_TYPE_INVALID = 0, + MZ_ZIP_TYPE_USER, + MZ_ZIP_TYPE_MEMORY, + MZ_ZIP_TYPE_HEAP, + MZ_ZIP_TYPE_FILE, + MZ_ZIP_TYPE_CFILE, + MZ_ZIP_TOTAL_TYPES + } mz_zip_type; + + /* miniz error codes. Be sure to update mz_zip_get_error_string() if you add or modify this enum. */ + typedef enum + { + MZ_ZIP_NO_ERROR = 0, + MZ_ZIP_UNDEFINED_ERROR, + MZ_ZIP_TOO_MANY_FILES, + MZ_ZIP_FILE_TOO_LARGE, + MZ_ZIP_UNSUPPORTED_METHOD, + MZ_ZIP_UNSUPPORTED_ENCRYPTION, + MZ_ZIP_UNSUPPORTED_FEATURE, + MZ_ZIP_FAILED_FINDING_CENTRAL_DIR, + MZ_ZIP_NOT_AN_ARCHIVE, + MZ_ZIP_INVALID_HEADER_OR_CORRUPTED, + MZ_ZIP_UNSUPPORTED_MULTIDISK, + MZ_ZIP_DECOMPRESSION_FAILED, + MZ_ZIP_COMPRESSION_FAILED, + MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE, + MZ_ZIP_CRC_CHECK_FAILED, + MZ_ZIP_UNSUPPORTED_CDIR_SIZE, + MZ_ZIP_ALLOC_FAILED, + MZ_ZIP_FILE_OPEN_FAILED, + MZ_ZIP_FILE_CREATE_FAILED, + MZ_ZIP_FILE_WRITE_FAILED, + MZ_ZIP_FILE_READ_FAILED, + MZ_ZIP_FILE_CLOSE_FAILED, + MZ_ZIP_FILE_SEEK_FAILED, + MZ_ZIP_FILE_STAT_FAILED, + MZ_ZIP_INVALID_PARAMETER, + MZ_ZIP_INVALID_FILENAME, + MZ_ZIP_BUF_TOO_SMALL, + MZ_ZIP_INTERNAL_ERROR, + MZ_ZIP_FILE_NOT_FOUND, + MZ_ZIP_ARCHIVE_TOO_LARGE, + MZ_ZIP_VALIDATION_FAILED, + MZ_ZIP_WRITE_CALLBACK_FAILED, + MZ_ZIP_TOTAL_ERRORS + } mz_zip_error; + + typedef struct + { + mz_uint64 m_archive_size; + mz_uint64 m_central_directory_file_ofs; + + /* We only support up to UINT32_MAX files in zip64 mode. */ + mz_uint32 m_total_files; + mz_zip_mode m_zip_mode; + mz_zip_type m_zip_type; + mz_zip_error m_last_error; + + mz_uint64 m_file_offset_alignment; + + mz_alloc_func m_pAlloc; + mz_free_func m_pFree; + mz_realloc_func m_pRealloc; + void *m_pAlloc_opaque; + + mz_file_read_func m_pRead; + mz_file_write_func m_pWrite; + mz_file_needs_keepalive m_pNeeds_keepalive; + void *m_pIO_opaque; + + mz_zip_internal_state *m_pState; + + } mz_zip_archive; + + typedef struct + { + mz_zip_archive *pZip; + mz_uint flags; + + int status; + + mz_uint64 read_buf_size, read_buf_ofs, read_buf_avail, comp_remaining, out_buf_ofs, cur_file_ofs; + mz_zip_archive_file_stat file_stat; + void *pRead_buf; + void *pWrite_buf; + + size_t out_blk_remain; + + tinfl_decompressor inflator; + +#ifdef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS + mz_uint padding; +#else + mz_uint file_crc32; +#endif + + } mz_zip_reader_extract_iter_state; + + /* -------- ZIP reading */ + + /* Inits a ZIP archive reader. */ + /* These functions read and validate the archive's central directory. */ + MINIZ_EXPORT mz_bool mz_zip_reader_init(mz_zip_archive *pZip, mz_uint64 size, mz_uint flags); + + MINIZ_EXPORT mz_bool mz_zip_reader_init_mem(mz_zip_archive *pZip, const void *pMem, size_t size, mz_uint flags); + +#ifndef MINIZ_NO_STDIO + /* Read a archive from a disk file. */ + /* file_start_ofs is the file offset where the archive actually begins, or 0. */ + /* actual_archive_size is the true total size of the archive, which may be smaller than the file's actual size on disk. If zero the entire file is treated as the archive. */ + MINIZ_EXPORT mz_bool mz_zip_reader_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint32 flags); + MINIZ_EXPORT mz_bool mz_zip_reader_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags, mz_uint64 file_start_ofs, mz_uint64 archive_size); + + /* Read an archive from an already opened FILE, beginning at the current file position. */ + /* The archive is assumed to be archive_size bytes long. If archive_size is 0, then the entire rest of the file is assumed to contain the archive. */ + /* The FILE will NOT be closed when mz_zip_reader_end() is called. */ + MINIZ_EXPORT mz_bool mz_zip_reader_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint64 archive_size, mz_uint flags); +#endif + + /* Ends archive reading, freeing all allocations, and closing the input archive file if mz_zip_reader_init_file() was used. */ + MINIZ_EXPORT mz_bool mz_zip_reader_end(mz_zip_archive *pZip); + + /* -------- ZIP reading or writing */ + + /* Clears a mz_zip_archive struct to all zeros. */ + /* Important: This must be done before passing the struct to any mz_zip functions. */ + MINIZ_EXPORT void mz_zip_zero_struct(mz_zip_archive *pZip); + + MINIZ_EXPORT mz_zip_mode mz_zip_get_mode(mz_zip_archive *pZip); + MINIZ_EXPORT mz_zip_type mz_zip_get_type(mz_zip_archive *pZip); + + /* Returns the total number of files in the archive. */ + MINIZ_EXPORT mz_uint mz_zip_reader_get_num_files(mz_zip_archive *pZip); + + MINIZ_EXPORT mz_uint64 mz_zip_get_archive_size(mz_zip_archive *pZip); + MINIZ_EXPORT mz_uint64 mz_zip_get_archive_file_start_offset(mz_zip_archive *pZip); + MINIZ_EXPORT MZ_FILE *mz_zip_get_cfile(mz_zip_archive *pZip); + + /* Reads n bytes of raw archive data, starting at file offset file_ofs, to pBuf. */ + MINIZ_EXPORT size_t mz_zip_read_archive_data(mz_zip_archive *pZip, mz_uint64 file_ofs, void *pBuf, size_t n); + + /* All mz_zip funcs set the m_last_error field in the mz_zip_archive struct. These functions retrieve/manipulate this field. */ + /* Note that the m_last_error functionality is not thread safe. */ + MINIZ_EXPORT mz_zip_error mz_zip_set_last_error(mz_zip_archive *pZip, mz_zip_error err_num); + MINIZ_EXPORT mz_zip_error mz_zip_peek_last_error(mz_zip_archive *pZip); + MINIZ_EXPORT mz_zip_error mz_zip_clear_last_error(mz_zip_archive *pZip); + MINIZ_EXPORT mz_zip_error mz_zip_get_last_error(mz_zip_archive *pZip); + MINIZ_EXPORT const char *mz_zip_get_error_string(mz_zip_error mz_err); + + /* MZ_TRUE if the archive file entry is a directory entry. */ + MINIZ_EXPORT mz_bool mz_zip_reader_is_file_a_directory(mz_zip_archive *pZip, mz_uint file_index); + + /* MZ_TRUE if the file is encrypted/strong encrypted. */ + MINIZ_EXPORT mz_bool mz_zip_reader_is_file_encrypted(mz_zip_archive *pZip, mz_uint file_index); + + /* MZ_TRUE if the compression method is supported, and the file is not encrypted, and the file is not a compressed patch file. */ + MINIZ_EXPORT mz_bool mz_zip_reader_is_file_supported(mz_zip_archive *pZip, mz_uint file_index); + + /* Retrieves the filename of an archive file entry. */ + /* Returns the number of bytes written to pFilename, or if filename_buf_size is 0 this function returns the number of bytes needed to fully store the filename. */ + MINIZ_EXPORT mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, char *pFilename, mz_uint filename_buf_size); + + /* Attempts to locates a file in the archive's central directory. */ + /* Valid flags: MZ_ZIP_FLAG_CASE_SENSITIVE, MZ_ZIP_FLAG_IGNORE_PATH */ + /* Returns -1 if the file cannot be found. */ + MINIZ_EXPORT int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags); + MINIZ_EXPORT mz_bool mz_zip_reader_locate_file_v2(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags, mz_uint32 *file_index); + + /* Returns detailed information about an archive file entry. */ + MINIZ_EXPORT mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat); + + /* MZ_TRUE if the file is in zip64 format. */ + /* A file is considered zip64 if it contained a zip64 end of central directory marker, or if it contained any zip64 extended file information fields in the central directory. */ + MINIZ_EXPORT mz_bool mz_zip_is_zip64(mz_zip_archive *pZip); + + /* Returns the total central directory size in bytes. */ + /* The current max supported size is <= MZ_UINT32_MAX. */ + MINIZ_EXPORT size_t mz_zip_get_central_dir_size(mz_zip_archive *pZip); + + /* Extracts a archive file to a memory buffer using no memory allocation. */ + /* There must be at least enough room on the stack to store the inflator's state (~34KB or so). */ + MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_mem_no_alloc(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size); + MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_mem_no_alloc(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size); + + /* Extracts a archive file to a memory buffer. */ + MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_mem(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags); + MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_mem(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags); + + /* Extracts a archive file to a dynamically allocated heap buffer. */ + /* The memory will be allocated via the mz_zip_archive's alloc/realloc functions. */ + /* Returns NULL and sets the last error on failure. */ + MINIZ_EXPORT void *mz_zip_reader_extract_to_heap(mz_zip_archive *pZip, mz_uint file_index, size_t *pSize, mz_uint flags); + MINIZ_EXPORT void *mz_zip_reader_extract_file_to_heap(mz_zip_archive *pZip, const char *pFilename, size_t *pSize, mz_uint flags); + + /* Extracts a archive file using a callback function to output the file's data. */ + MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_callback(mz_zip_archive *pZip, mz_uint file_index, mz_file_write_func pCallback, void *pOpaque, mz_uint flags); + MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_callback(mz_zip_archive *pZip, const char *pFilename, mz_file_write_func pCallback, void *pOpaque, mz_uint flags); + + /* Extract a file iteratively */ + MINIZ_EXPORT mz_zip_reader_extract_iter_state *mz_zip_reader_extract_iter_new(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags); + MINIZ_EXPORT mz_zip_reader_extract_iter_state *mz_zip_reader_extract_file_iter_new(mz_zip_archive *pZip, const char *pFilename, mz_uint flags); + MINIZ_EXPORT size_t mz_zip_reader_extract_iter_read(mz_zip_reader_extract_iter_state *pState, void *pvBuf, size_t buf_size); + MINIZ_EXPORT mz_bool mz_zip_reader_extract_iter_free(mz_zip_reader_extract_iter_state *pState); + +#ifndef MINIZ_NO_STDIO + /* Extracts a archive file to a disk file and sets its last accessed and modified times. */ + /* This function only extracts files, not archive directory records. */ + MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_file(mz_zip_archive *pZip, mz_uint file_index, const char *pDst_filename, mz_uint flags); + MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_file(mz_zip_archive *pZip, const char *pArchive_filename, const char *pDst_filename, mz_uint flags); + + /* Extracts a archive file starting at the current position in the destination FILE stream. */ + MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_cfile(mz_zip_archive *pZip, mz_uint file_index, MZ_FILE *File, mz_uint flags); + MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_cfile(mz_zip_archive *pZip, const char *pArchive_filename, MZ_FILE *pFile, mz_uint flags); +#endif + +#if 0 +/* TODO */ + typedef void *mz_zip_streaming_extract_state_ptr; + mz_zip_streaming_extract_state_ptr mz_zip_streaming_extract_begin(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags); + mz_uint64 mz_zip_streaming_extract_get_size(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState); + mz_uint64 mz_zip_streaming_extract_get_cur_ofs(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState); + mz_bool mz_zip_streaming_extract_seek(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState, mz_uint64 new_ofs); + size_t mz_zip_streaming_extract_read(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState, void *pBuf, size_t buf_size); + mz_bool mz_zip_streaming_extract_end(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState); +#endif + + /* This function compares the archive's local headers, the optional local zip64 extended information block, and the optional descriptor following the compressed data vs. the data in the central directory. */ + /* It also validates that each file can be successfully uncompressed unless the MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY is specified. */ + MINIZ_EXPORT mz_bool mz_zip_validate_file(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags); + + /* Validates an entire archive by calling mz_zip_validate_file() on each file. */ + MINIZ_EXPORT mz_bool mz_zip_validate_archive(mz_zip_archive *pZip, mz_uint flags); + + /* Misc utils/helpers, valid for ZIP reading or writing */ + MINIZ_EXPORT mz_bool mz_zip_validate_mem_archive(const void *pMem, size_t size, mz_uint flags, mz_zip_error *pErr); +#ifndef MINIZ_NO_STDIO + MINIZ_EXPORT mz_bool mz_zip_validate_file_archive(const char *pFilename, mz_uint flags, mz_zip_error *pErr); +#endif + + /* Universal end function - calls either mz_zip_reader_end() or mz_zip_writer_end(). */ + MINIZ_EXPORT mz_bool mz_zip_end(mz_zip_archive *pZip); + + /* -------- ZIP writing */ + +#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS + + /* Inits a ZIP archive writer. */ + /*Set pZip->m_pWrite (and pZip->m_pIO_opaque) before calling mz_zip_writer_init or mz_zip_writer_init_v2*/ + /*The output is streamable, i.e. file_ofs in mz_file_write_func always increases only by n*/ + MINIZ_EXPORT mz_bool mz_zip_writer_init(mz_zip_archive *pZip, mz_uint64 existing_size); + MINIZ_EXPORT mz_bool mz_zip_writer_init_v2(mz_zip_archive *pZip, mz_uint64 existing_size, mz_uint flags); + + MINIZ_EXPORT mz_bool mz_zip_writer_init_heap(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size); + MINIZ_EXPORT mz_bool mz_zip_writer_init_heap_v2(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size, mz_uint flags); + +#ifndef MINIZ_NO_STDIO + MINIZ_EXPORT mz_bool mz_zip_writer_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning); + MINIZ_EXPORT mz_bool mz_zip_writer_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning, mz_uint flags); + MINIZ_EXPORT mz_bool mz_zip_writer_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint flags); +#endif + + /* Converts a ZIP archive reader object into a writer object, to allow efficient in-place file appends to occur on an existing archive. */ + /* For archives opened using mz_zip_reader_init_file, pFilename must be the archive's filename so it can be reopened for writing. If the file can't be reopened, mz_zip_reader_end() will be called. */ + /* For archives opened using mz_zip_reader_init_mem, the memory block must be growable using the realloc callback (which defaults to realloc unless you've overridden it). */ + /* Finally, for archives opened using mz_zip_reader_init, the mz_zip_archive's user provided m_pWrite function cannot be NULL. */ + /* Note: In-place archive modification is not recommended unless you know what you're doing, because if execution stops or something goes wrong before */ + /* the archive is finalized the file's central directory will be hosed. */ + MINIZ_EXPORT mz_bool mz_zip_writer_init_from_reader(mz_zip_archive *pZip, const char *pFilename); + MINIZ_EXPORT mz_bool mz_zip_writer_init_from_reader_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags); + + /* Adds the contents of a memory buffer to an archive. These functions record the current local time into the archive. */ + /* To add a directory entry, call this method with an archive name ending in a forwardslash with an empty buffer. */ + /* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */ + MINIZ_EXPORT mz_bool mz_zip_writer_add_mem(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, mz_uint level_and_flags); + + /* Like mz_zip_writer_add_mem(), except you can specify a file comment field, and optionally supply the function with already compressed data. */ + /* uncomp_size/uncomp_crc32 are only used if the MZ_ZIP_FLAG_COMPRESSED_DATA flag is specified. */ + MINIZ_EXPORT mz_bool mz_zip_writer_add_mem_ex(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, + mz_uint64 uncomp_size, mz_uint32 uncomp_crc32); + + MINIZ_EXPORT mz_bool mz_zip_writer_add_mem_ex_v2(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, + mz_uint64 uncomp_size, mz_uint32 uncomp_crc32, MZ_TIME_T *last_modified, const char *user_extra_data_local, mz_uint user_extra_data_local_len, + const char *user_extra_data_central, mz_uint user_extra_data_central_len); + + /* Adds the contents of a file to an archive. This function also records the disk file's modified time into the archive. */ + /* File data is supplied via a read callback function. User mz_zip_writer_add_(c)file to add a file directly.*/ + MINIZ_EXPORT mz_bool mz_zip_writer_add_read_buf_callback(mz_zip_archive *pZip, const char *pArchive_name, mz_file_read_func read_callback, void *callback_opaque, mz_uint64 max_size, + const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data_local, mz_uint user_extra_data_local_len, + const char *user_extra_data_central, mz_uint user_extra_data_central_len); + +#ifndef MINIZ_NO_STDIO + /* Adds the contents of a disk file to an archive. This function also records the disk file's modified time into the archive. */ + /* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */ + MINIZ_EXPORT mz_bool mz_zip_writer_add_file(mz_zip_archive *pZip, const char *pArchive_name, const char *pSrc_filename, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags); + + /* Like mz_zip_writer_add_file(), except the file data is read from the specified FILE stream. */ + MINIZ_EXPORT mz_bool mz_zip_writer_add_cfile(mz_zip_archive *pZip, const char *pArchive_name, MZ_FILE *pSrc_file, mz_uint64 max_size, + const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data_local, mz_uint user_extra_data_local_len, + const char *user_extra_data_central, mz_uint user_extra_data_central_len); +#endif + + /* Adds a file to an archive by fully cloning the data from another archive. */ + /* This function fully clones the source file's compressed data (no recompression), along with its full filename, extra data (it may add or modify the zip64 local header extra data field), and the optional descriptor following the compressed data. */ + MINIZ_EXPORT mz_bool mz_zip_writer_add_from_zip_reader(mz_zip_archive *pZip, mz_zip_archive *pSource_zip, mz_uint src_file_index); + + /* Finalizes the archive by writing the central directory records followed by the end of central directory record. */ + /* After an archive is finalized, the only valid call on the mz_zip_archive struct is mz_zip_writer_end(). */ + /* An archive must be manually finalized by calling this function for it to be valid. */ + MINIZ_EXPORT mz_bool mz_zip_writer_finalize_archive(mz_zip_archive *pZip); + + /* Finalizes a heap archive, returning a pointer to the heap block and its size. */ + /* The heap block will be allocated using the mz_zip_archive's alloc/realloc callbacks. */ + MINIZ_EXPORT mz_bool mz_zip_writer_finalize_heap_archive(mz_zip_archive *pZip, void **ppBuf, size_t *pSize); + + /* Ends archive writing, freeing all allocations, and closing the output file if mz_zip_writer_init_file() was used. */ + /* Note for the archive to be valid, it *must* have been finalized before ending (this function will not do it for you). */ + MINIZ_EXPORT mz_bool mz_zip_writer_end(mz_zip_archive *pZip); + + /* -------- Misc. high-level helper functions: */ + + /* mz_zip_add_mem_to_archive_file_in_place() efficiently (but not atomically) appends a memory blob to a ZIP archive. */ + /* Note this is NOT a fully safe operation. If it crashes or dies in some way your archive can be left in a screwed up state (without a central directory). */ + /* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */ + /* TODO: Perhaps add an option to leave the existing central dir in place in case the add dies? We could then truncate the file (so the old central dir would be at the end) if something goes wrong. */ + MINIZ_EXPORT mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags); + MINIZ_EXPORT mz_bool mz_zip_add_mem_to_archive_file_in_place_v2(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_zip_error *pErr); + +#ifndef MINIZ_NO_STDIO + /* Reads a single file from an archive into a heap block. */ + /* If pComment is not NULL, only the file with the specified comment will be extracted. */ + /* Returns NULL on failure. */ + MINIZ_EXPORT void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, size_t *pSize, mz_uint flags); + MINIZ_EXPORT void *mz_zip_extract_archive_file_to_heap_v2(const char *pZip_filename, const char *pArchive_name, const char *pComment, size_t *pSize, mz_uint flags, mz_zip_error *pErr); +#endif + +#endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS */ + +#ifdef __cplusplus +} +#endif + +#endif /* MINIZ_NO_ARCHIVE_APIS */ diff --git a/nix/default.nix b/nix/default.nix new file mode 120000 index 0000000000..593a000b67 --- /dev/null +++ b/nix/default.nix @@ -0,0 +1 @@ +shell.nix \ No newline at end of file diff --git a/nix/flake.lock b/nix/flake.lock new file mode 100644 index 0000000000..2d51f2ec38 --- /dev/null +++ b/nix/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "flake-compat": { + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1784356753, + "narHash": "sha256-12KrbMiWLcf8m7pCvAtZh1ZrgF85ZXDXvfR/fWTKy84=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "61b7c44c4073f0b827768aff0049561b5110ea5a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/flake.nix b/nix/flake.nix new file mode 100644 index 0000000000..3a0c6b3f07 --- /dev/null +++ b/nix/flake.nix @@ -0,0 +1,85 @@ +{ + description = "CrossPoint Reader development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-compat.url = "github:NixOS/flake-compat"; + }; + + outputs = + { nixpkgs, ... }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + forAllSystems = nixpkgs.lib.genAttrs systems; + in + { + devShells = forAllSystems ( + system: + let + pkgs = import nixpkgs { inherit system; }; + # Detect the project root from wherever the user entered the shell, + # so commands work from the repository root or any subdirectory. + # Using `git rev-parse` to do so (assuming git is installed + # system-wide); user can overwrite this by setting PROJECT_ROOT env. + setEnvs = '' + PROJECT_ROOT="''${PROJECT_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" + export PROJECT_ROOT + export PLATFORMIO_CORE_DIR="$PROJECT_ROOT/.cache/platformio" + ''; + fhsEnv = pkgs.buildFHSEnv { + name = "crosspoint-reader-shell"; + + targetPkgs = + pkgs: with pkgs; [ + python3 + uv + + # Runtime libraries used by PlatformIO's downloaded ESP32 toolchain binaries. + stdenv.cc.cc.lib + zlib + ncurses + ]; + + profile = '' + ${setEnvs} + export PATH="$PROJECT_ROOT/.venv/bin:$PATH" + + if [ ! -x "$PROJECT_ROOT/.venv/bin/pio" ]; then + echo "Creating .venv and installing pioarduino PlatformIO Core..." + # Forcing python3 from fhsEnv, otherwise we get the following + # exception while running `pip check` + # ModuleNotFoundError: No module named 'littlefs' + uv venv --python /usr/bin/python3 "$PROJECT_ROOT/.venv" && + uv pip install --python "$PROJECT_ROOT/.venv/bin/python" \ + -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip \ + -r "$PROJECT_ROOT/requirements.txt" || + echo "Failed to install pioarduino PlatformIO Core" >&2 + fi + ''; + }; + pio = pkgs.writeShellScriptBin "pio" '' + exec ${fhsEnv}/bin/crosspoint-reader-shell -c 'exec pio "$@"' pio "$@" + ''; + in + { + default = pkgs.mkShell { + packages = with pkgs; [ + pio + fhsEnv + clang-tools # for clang-format + (python3.withPackages (ps: with ps; [ # for debugging monitor + matplotlib + pyserial + colorama + ])) + ]; + + shellHook = setEnvs; + }; + } + ); + }; +} diff --git a/nix/shell.nix b/nix/shell.nix new file mode 100644 index 0000000000..692cd4df89 --- /dev/null +++ b/nix/shell.nix @@ -0,0 +1,12 @@ +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + nodeName = lock.nodes.root.inputs.flake-compat; + in + fetchTarball { + url = + lock.nodes.${nodeName}.locked.url + or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz"; + sha256 = lock.nodes.${nodeName}.locked.narHash; + } +) { src = ./.; }).shellNix diff --git a/platformio.ini b/platformio.ini index b92c410c1a..cb3de35da0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,10 +1,9 @@ [platformio] -default_envs = tiny, xlarge +default_envs = default, sticky extra_configs = platformio.local.ini -[crosspoint] -version = 1.3.0 -crossink_version = 1.4.0 +[crossink] +version = 1.5.0 [base] platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip @@ -20,7 +19,33 @@ board_upload.flash_size = 16MB board_upload.maximum_size = 6553600 board_upload.offset_address = 0x10000 +; Rebuild the Arduino/ESP-IDF SDK with lower runtime RAM reservations. Timer +; stacks are still substantially above their measured high-water marks, while +; disabling WiFi IRAM placement trades some throughput for more usable heap. +custom_sdkconfig = + CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096 + CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2560 + CONFIG_ESP_WIFI_IRAM_OPT=n + CONFIG_ESP_WIFI_RX_IRAM_OPT=n + CONFIG_ARDUINO_SELECTIVE_COMPILATION=y + CONFIG_ARDUINO_SELECTIVE_RainMaker=n + CONFIG_ARDUINO_SELECTIVE_Insights=n + CONFIG_ARDUINO_SELECTIVE_ESP_SR=n + +; These managed ESP-IDF components support Arduino cloud, diagnostics, and +; radio-co-processor features that CrossInk does not use. +custom_component_remove = + espressif/esp_insights + espressif/esp_rainmaker + espressif/esp_diagnostics + espressif/esp_diag_data_store + espressif/esp_schedule + espressif/esp_rcp_update + espressif/esp_secure_cert_mgr + espressif/cbor + build_flags = + -Iinclude -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DEINK_DISPLAY_SINGLE_BUFFER_MODE=1 @@ -35,8 +60,19 @@ build_flags = # Increase PNG scanline buffer to support up to 2048px wide images # Default is (320*4+1)*2=2562, we need more for larger images -DPNG_MAX_BUFFERED_PIXELS=16416 - -DFREEINK_DEVICE_X4=1 - -DFREEINK_DEVICE_X3=1 + -DFREEINK_NET_WOLFSSL=1 + -DWOLFSSL_USER_SETTINGS + -DWOLFSSL_OPTIONS_H + -DWOLFSSL_CLIENT_EXAMPLE + -DWOLFSSL_TLS13 + -DWOLFSSL_HAVE_SP_ECC + -DWOLFSSL_SP_SMALL +# Arduino-wolfSSL's user_settings.h enables TLS extensions, supported curves, +# HKDF, and RSA-PSS when WOLFSSL_TLS13 is set. Do not repeat those defines here: +# GCC reports every library translation unit as a macro redefinition. + -DHAVE_FFDHE_2048 + -DHAVE_CURVE25519 + -DHAVE_SNI -Wno-bidi-chars -Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity -fno-exceptions @@ -51,12 +87,14 @@ board_build.flash_size = 16MB board_build.partitions = partitions.csv extra_scripts = + pre:scripts/patch_wolfssl.py pre:scripts/build_web.py pre:scripts/gen_i18n.py pre:scripts/git_branch.py pre:scripts/patch_jpegdec.py pre:scripts/patch_websockets.py post:scripts/check_firmware_size.py + post:scripts/check_app_touch_gate.py post:scripts/rename_firmware.py ; Libraries @@ -68,6 +106,12 @@ lib_deps = BoardConfig=symlink://freeink-sdk/libs/hardware/BoardConfig XteinkDetect=symlink://freeink-sdk/libs/hardware/XteinkDetect PowerManager=symlink://freeink-sdk/libs/hardware/PowerManager + Rtc=symlink://freeink-sdk/libs/hardware/Rtc + Imu=symlink://freeink-sdk/libs/hardware/Imu + FrontlightManager=symlink://freeink-sdk/libs/hardware/FrontlightManager + XteinkDetect=symlink://freeink-sdk/libs/hardware/XteinkDetect + SecureNet=symlink://freeink-sdk/libs/network/SecureNet + NearbyTransfer=symlink://freeink-sdk/libs/network/NearbyTransfer FreeInkUI=symlink://freeink-sdk/libs/ui/FreeInkUI Icons=symlink://freeink-sdk/libs/assets/Icons bblanchon/ArduinoJson @ 7.4.2 @@ -75,88 +119,59 @@ lib_deps = bitbank2/PNGdec @ 1.1.6 https://github.com/bitbank2/JPEGDEC.git#86282979224c8a32fd51e091ed5a35b0c699a52b links2004/WebSockets @ 2.7.3 + wolfssl/Arduino-wolfSSL @ 5.7.2 lib_ignore = + BLE HalClockSim [env:default] extends = base +custom_firmware_device_type = x3-x4 build_flags = ${base.build_flags} + -DFREEINK_DEVICE_X4=1 + -DFREEINK_DEVICE_X3=1 + -DCROSSINK_APP_CAP_TOUCH=0 ; CROSSINK_VERSION is set by scripts/git_branch.py (includes current branch) - -DCROSSPOINT_FIRMWARE_VARIANT=\"tiny\" + -DCROSSINK_FIRMWARE_DEVICE_TYPE=\"x3-x4\" -DENABLE_SERIAL_LOG -DLOG_LEVEL=1 ; Set log level to debug for development builds - -DOMIT_TEENSY_FONT - -DOMIT_ITTY_BITTY_FONT - -DOMIT_XLARGE_FONT - -DOMIT_HUGE_FONT [env:debug] extends = base +custom_firmware_device_type = x3-x4 build_flags = ${base.build_flags} + -DFREEINK_DEVICE_X4=1 + -DFREEINK_DEVICE_X3=1 + -DCROSSINK_APP_CAP_TOUCH=0 ; CROSSINK_VERSION injected by scripts/git_branch.py - ; (uses $CROSSPOINT_RC_HASH env var in CI, or git short hash locally) - -DCROSSPOINT_FIRMWARE_VARIANT=\"tiny\" + ; (uses $CROSSINK_RC_HASH env var in CI, or git short hash locally) + -DCROSSINK_FIRMWARE_DEVICE_TYPE=\"x3-x4\" -DCROSSINK_BUILD_ENV=\"debug\" -DCROSSINK_ENABLE_READING_STATS_TOGGLE -DCROSSINK_SHOW_SLEEP_BUILD_INFO -DENABLE_SERIAL_LOG -DLOG_LEVEL=2 ; Set log level to debug for development builds - -DOMIT_TEENSY_FONT - -DOMIT_ITTY_BITTY_FONT - -DOMIT_XLARGE_FONT - -DOMIT_HUGE_FONT - -; Omits two smallest and largest font sizes -[env:tiny] -extends = base -build_flags = - ${base.build_flags} - -DCROSSINK_VERSION=\"${crosspoint.crossink_version}-tiny\" - -DCROSSPOINT_FIRMWARE_VARIANT=\"tiny\" - -DENABLE_SERIAL_LOG - -DLOG_LEVEL=1 - -DOMIT_TEENSY_FONT - -DOMIT_ITTY_BITTY_FONT - -DOMIT_XLARGE_FONT - -DOMIT_HUGE_FONT -; Omits smaller font sizes and only includes the three largest font sizes -[env:xlarge] -extends = base -build_flags = - ${base.build_flags} - -DCROSSINK_VERSION=\"${crosspoint.crossink_version}-xlarge\" - -DCROSSPOINT_FIRMWARE_VARIANT=\"xlarge\" - -DENABLE_SERIAL_LOG - -DLOG_LEVEL=1 - -DOMIT_TEENSY_FONT - -DOMIT_ITTY_BITTY_FONT - -DOMIT_TINY_FONT - -DOMIT_SMALL_FONT - -DOMIT_MEDIUM_FONT - -[env:simulator] +[simulator-base] platform = native custom_run_simulator_target_owner = project lib_compat_mode = off build_src_filter = +<*> - - - - - - - - build_flags = -std=gnu++2a !sdl2-config --cflags --libs -DSIMULATOR + -DCROSSPOINT_SIMULATOR_PROJECT_WEBSERVER -Wno-c++11-narrowing - -DCROSSINK_VERSION=\"${crosspoint.crossink_version}\" - -DCROSSPOINT_FIRMWARE_VARIANT=\"simulator\" + -DCROSSINK_VERSION=\"${crossink.version}\" -DENABLE_SERIAL_LOG -DLOG_LEVEL=2 -DEINK_DISPLAY_SINGLE_BUFFER_MODE=1 @@ -168,16 +183,9 @@ build_flags = -DDISABLE_FS_H_WARNING=1 -DDESTRUCTOR_CLOSES_FILE=1 ;-DSIMULATOR_DEVICE_X3 ; uncomment to simulate X3 screen size + -Iinclude -Isrc ;-DOMIT_EMOJI_FONTS ; uncomment to omit emoji/symbols glyphs from reading fonts - ;-DOMIT_TEENSY_FONT ; uncomment to omit teensy (8px) font size - ;-DOMIT_ITTY_BITTY_FONT ; uncomment to omit itty bitty (9px) font size - ;-DOMIT_TINY_FONT ; uncomment to omit tiny (10px) font size - ;-DOMIT_SMALL_FONT ; uncomment to omit small (12px) font size - ;-DOMIT_MEDIUM_FONT ; uncomment to omit medium (14px) font size - ;-DOMIT_LARGE_FONT ; uncomment to omit large (16px) font size - ;-DOMIT_XLARGE_FONT ; uncomment to omit extra large (18px) font size - ;-DOMIT_HUGE_FONT ; uncomment to omit huge (20px) font size -DCROSSPOINT_SIM_USE_NATIVE_DECODERS -Dmemcpy_P=memcpy ; For native JPEGDEC lib_ignore = hal, WebSockets @@ -187,9 +195,72 @@ extra_scripts = pre:scripts/build_web.py post:.pio/libdeps/$PIOENV/simulator/run_simulator_project.py lib_deps = - simulator=https://github.com/uxjulia/crosspoint-simulator + simulator=https://github.com/uxjulia/crossink-simulator + FreeInkUI=symlink://freeink-sdk/libs/ui/FreeInkUI + Icons=symlink://freeink-sdk/libs/assets/Icons + NearbyTransfer=symlink://freeink-sdk/libs/network/NearbyTransfer bblanchon/ArduinoJson @ 7.4.2 ricmoo/QRCode @ ^0.0.1 bitbank2/PNGdec @ 1.1.6 https://github.com/bitbank2/JPEGDEC.git#86282979224c8a32fd51e091ed5a35b0c699a52b links2004/WebSockets @ 2.7.3 + +[env:simulator] +extends = simulator-base +build_flags = + ${simulator-base.build_flags} + -DCROSSINK_APP_CAP_TOUCH=0 + -DCROSSINK_FIRMWARE_DEVICE_TYPE=\"x3-x4\" + +[env:simulator-X3] +extends = simulator-base +build_flags = + ${simulator-base.build_flags} + -DCROSSINK_APP_CAP_TOUCH=0 + -DCROSSINK_FIRMWARE_DEVICE_TYPE=\"x3-x4\" + -DSIMULATOR_DEVICE_X3 + +[env:sticky-simulator] +extends = simulator-base +build_flags = + ${simulator-base.build_flags} + -DSIMULATOR_DEVICE_STICKY + -DFREEINK_DEVICE_STICKY=1 + -DCROSSINK_APP_CAP_TOUCH=1 + -DCROSSINK_FIRMWARE_DEVICE_TYPE=\"sticky\" +; pio run -e sticky-simulator -t run_simulator + +; --- Seeed Sticky - ESP32-S3R8, 3.97" 800x480 SSD1677 + GT911 touch ----------- +; Different MCU family than the C3 envs (one binary per family). The SDK +; auto-enables CAP_TOUCH (GT911) and the BQ27220 gauge for this device. The +; framebuffer is placed in PSRAM to leave more internal DRAM for EPUB rendering. +; pio run -e sticky -t upload +[env:sticky] +extends = base +custom_firmware_device_type = sticky +board = esp32-s3-devkitc1-n16r8 +board_build.mcu = esp32s3 +build_flags = + ${base.build_flags} + -DFREEINK_DEVICE_STICKY=1 + -DCROSSINK_APP_CAP_TOUCH=1 + -DFREEINK_FB_PSRAM=1 + -DCROSSINK_VERSION=\"${crossink.version}-sticky\" + -DCROSSINK_FIRMWARE_DEVICE_TYPE=\"sticky\" + -DENABLE_SERIAL_LOG + -DLOG_LEVEL=1 + +[env:sticky-debug] +extends = base +custom_firmware_device_type = sticky +board = esp32-s3-devkitc1-n16r8 +board_build.mcu = esp32s3 +build_flags = + ${base.build_flags} + -DFREEINK_DEVICE_STICKY=1 + -DCROSSINK_APP_CAP_TOUCH=1 + -DFREEINK_FB_PSRAM=1 + ; CROSSINK_VERSION injected by scripts/git_branch.py + -DCROSSINK_FIRMWARE_DEVICE_TYPE=\"sticky\" + -DENABLE_SERIAL_LOG + -DLOG_LEVEL=2 diff --git a/scripts/check_app_touch_gate.py b/scripts/check_app_touch_gate.py new file mode 100644 index 0000000000..b4f76427b6 --- /dev/null +++ b/scripts/check_app_touch_gate.py @@ -0,0 +1,57 @@ +"""Reject app touch symbols from firmware configured without touch UI.""" + +import os +import subprocess + + +FORBIDDEN_SYMBOLS = ( + "TouchRegistry", + "HalGPIO::hasTouch", + "HalGPIO::wasTouch", + "HalGPIO::isTouch", + "HalGPIO::lastTouch", + "HalGPIO::wasSwipe", + "MappedInputManager::hasTouch", + "MappedInputManager::wasScreen", + "MappedInputManager::isScreenTouch", + "MappedInputManager::wasItemTap", + "MappedInputManager::wasTabTap", + "MappedInputManager::wasCoverTap", + "MappedInputManager::wasSwipe", + "MappedInputManager::wasLeftEdgeGesture", + "MappedInputManager::wasHomeGesture", + "MappedInputManager::wasMenuGesture", + "ReaderUtils::detectTouchPageTurn", +) + + +def _capability_enabled(env): + for define in env.get("CPPDEFINES", []): + if isinstance(define, (list, tuple)): + name, value = define[0], define[1] + else: + name, value = define, None + if name == "CROSSINK_APP_CAP_TOUCH": + return str(value) == "1" + raise RuntimeError("CROSSINK_APP_CAP_TOUCH is missing from CPPDEFINES") + + +def audit_app_touch_gate(target, source, env): + del source + if _capability_enabled(env): + return + + toolchain_dir = env.PioPlatform().get_package_dir("toolchain-riscv32-esp") + nm = os.path.join(toolchain_dir, "bin", "riscv32-esp-elf-nm") + output = subprocess.check_output([nm, "--demangle", "--defined-only", str(target[0])], text=True) + leaked = [line for line in output.splitlines() if any(symbol in line for symbol in FORBIDDEN_SYMBOLS)] + if leaked: + print("Non-touch firmware retained app touch symbols:") + print("\n".join(leaked)) + env.Exit(1) + + print("App touch gate audit passed: no app touch symbols in non-touch firmware") + + +Import("env") +env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", audit_app_touch_gate) diff --git a/scripts/debugging_monitor.py b/scripts/debugging_monitor.py index fa2b8547fa..b9f57e4e7d 100755 --- a/scripts/debugging_monitor.py +++ b/scripts/debugging_monitor.py @@ -170,7 +170,7 @@ def build_arg_parser() -> argparse.ArgumentParser: "LOAD:", "ENTRY", "[SD]", - "STARTING CROSSPOINT", + "STARTING CROSSINK", "VERSION", ], Fore.LIGHTCYAN_EX: ["[RBS]"], diff --git a/scripts/dictionary_tools.py b/scripts/dictionary_tools.py new file mode 100644 index 0000000000..7fd18ede8f --- /dev/null +++ b/scripts/dictionary_tools.py @@ -0,0 +1,583 @@ +#!/usr/bin/env python3 +""" +dict_tools.py — Offline StarDict dictionary tools for CrossPoint Reader. + +Subcommands: + prep — Pre-process a dictionary (decompress, generate offset files) + lookup — Look up a word in a prepared dictionary + merge — Merge multiple StarDict dictionaries into one + +Run 'python3 scripts/dictionary_tools.py --help' for details. +""" + +import argparse +import gzip +import shutil +import struct +import sys +from pathlib import Path + +# --------------------------------------------------------------------------- +# .oft constants — matches StarDict cache format and DictPrepareActivity +# --------------------------------------------------------------------------- + +_OFT_HEADER = b"StarDict's Cache, Version: 0.2" + b"\xc1\xd1\xa4\x51\x00\x00\x00\x00" +assert len(_OFT_HEADER) == 38 +_STRIDE = 32 + + +def _build_oft(data: bytes, skip_bytes_after_null: int) -> bytes: + """Build .oft index bytes for .idx (skip=8) or .syn (skip=4).""" + offsets = [] + entry_count = 0 + pos = 0 + while pos < len(data): + null = data.index(b"\x00", pos) + pos = null + 1 + skip_bytes_after_null + entry_count += 1 + if entry_count % _STRIDE == 0: + offsets.append(pos) + offsets.append(len(data)) # sentinel: total byte size of the source file + result = _OFT_HEADER + for off in offsets: + result += struct.pack(" bytes: + """Build .cspt from .idx/.syn data and matching .oft data. + + skip_per_entry: bytes after the null-terminated word in src_data + (8 for .idx = offset+size; 4 for .syn = original_word_index). + """ + # Parse OFT: header (38 bytes) + LE uint32 page offsets + sentinel. + table_bytes = oft_data[len(_OFT_HEADER):] + # Last entry is sentinel (total file size); preceding entries are page boundaries. + num_oft_entries = len(table_bytes) // 4 + if num_oft_entries > 0: + num_oft_entries -= 1 # exclude sentinel + + # Collect page start offsets: page 0 is implicit at byte 0, + # remaining pages from OFT entries. + page_offsets = [0] + for i in range(num_oft_entries): + off = struct.unpack_from("= len(src_data): + break + try: + null = src_data.index(b"\x00", pos) + except ValueError: + break + word = src_data[pos:null] + prefix = word[:_CSPT_PREFIX_LEN].ljust(_CSPT_PREFIX_LEN, b"\x00") + entries.append(prefix + struct.pack("= len(src_data): + continue + try: + null = src_data.index(b"\x00", scan_pos) + except ValueError: + continue + word = src_data[scan_pos:null] + prefix = word[:_CSPT_PREFIX_LEN].ljust(_CSPT_PREFIX_LEN, b"\x00") + entries.append(prefix + struct.pack(" list[tuple[str, int, int]]: + """Parse all entries from .idx data. Returns [(word, offset, size), ...].""" + entries = [] + pos = 0 + while pos < len(idx_data): + null = idx_data.index(b"\x00", pos) + word = idx_data[pos:null].decode("utf-8") + offset, size = struct.unpack_from(">II", idx_data, null + 1) + entries.append((word, offset, size)) + pos = null + 1 + 8 + return entries + + +def _parse_all_syn(syn_data: bytes) -> list[tuple[str, int]]: + """Parse all entries from .syn data. Returns [(synonym, word_index), ...].""" + entries = [] + pos = 0 + while pos < len(syn_data): + null = syn_data.index(b"\x00", pos) + word = syn_data[pos:null].decode("utf-8") + (idx,) = struct.unpack_from(">I", syn_data, null + 1) + entries.append((word, idx)) + pos = null + 1 + 4 + return entries + + +def _parse_ifo(ifo_path: Path) -> dict[str, str]: + """Parse .ifo file into a dict of key=value pairs.""" + result = {} + for line in ifo_path.read_text(encoding="utf-8").splitlines(): + if "=" in line: + k, v = line.split("=", 1) + result[k] = v + return result + + +def _write_ifo(path: Path, fields: dict[str, str]) -> None: + """Write a StarDict .ifo file.""" + lines = ["StarDict's dict ifo file", "version=2.4.2"] + for k, v in fields.items(): + lines.append(f"{k}={v}") + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def _find_stem(folder: Path) -> str: + """Return the StarDict stem by locating the .ifo file in the folder.""" + ifo_files = list(folder.glob("*.ifo")) + if not ifo_files: + print(f"ERROR: no .ifo file found in {folder}", file=sys.stderr) + sys.exit(1) + if len(ifo_files) > 1: + print(f"ERROR: multiple .ifo files in {folder} -- cannot determine stem", file=sys.stderr) + sys.exit(1) + return ifo_files[0].stem + + +def _decompress(src: Path, dst: Path) -> None: + """Gzip-decompress src into dst.""" + with gzip.open(src, "rb") as f_in, open(dst, "wb") as f_out: + shutil.copyfileobj(f_in, f_out) + + +# --------------------------------------------------------------------------- +# prep subcommand +# --------------------------------------------------------------------------- + +def prep(source_folder: Path) -> None: + if not source_folder.is_dir(): + print(f"ERROR: not a directory: {source_folder}", file=sys.stderr) + sys.exit(1) + + stem = _find_stem(source_folder) + out_dir = source_folder + + print(f"Preparing {source_folder}") + + steps_run = 0 + + # Step 1: Extract .dict.dz -> .dict + dz = out_dir / f"{stem}.dict.dz" + dict_ = out_dir / f"{stem}.dict" + if dz.exists() and not dict_.exists(): + print(f" Extracting {dz.name} -> {dict_.name} ...", end=" ", flush=True) + _decompress(dz, dict_) + print(f"{dict_.stat().st_size / 1024 / 1024:.2f} MB") + steps_run += 1 + + # Step 2: Extract .syn.dz -> .syn + syn_dz = out_dir / f"{stem}.syn.dz" + syn = out_dir / f"{stem}.syn" + if syn_dz.exists() and not syn.exists(): + print(f" Extracting {syn_dz.name} -> {syn.name} ...", end=" ", flush=True) + _decompress(syn_dz, syn) + print(f"{syn.stat().st_size / 1024 / 1024:.2f} MB") + steps_run += 1 + + # Step 3: Generate .idx.oft + idx = out_dir / f"{stem}.idx" + idx_oft = out_dir / f"{stem}.idx.oft" + idx_oft_regenerated = False + if idx.exists() and not idx_oft.exists(): + print(f" Generating {idx_oft.name} ...", end=" ", flush=True) + idx_oft.write_bytes(_build_oft(idx.read_bytes(), skip_bytes_after_null=8)) + print(f"{idx_oft.stat().st_size} bytes") + steps_run += 1 + idx_oft_regenerated = True + + # Step 4: Generate .syn.oft (.syn may have just been created in step 2) + syn_oft = out_dir / f"{stem}.syn.oft" + syn_oft_regenerated = False + if syn.exists() and not syn_oft.exists(): + print(f" Generating {syn_oft.name} ...", end=" ", flush=True) + syn_oft.write_bytes(_build_oft(syn.read_bytes(), skip_bytes_after_null=4)) + print(f"{syn_oft.stat().st_size} bytes") + steps_run += 1 + syn_oft_regenerated = True + + # Step 5: Generate .idx.oft.cspt (requires .idx and .idx.oft). + # Regenerate if .cspt is missing OR if .idx.oft was just rebuilt this run + # (stale .cspt). Mirrors DictPrepareActivity::detectSteps on-device. + idx_cspt = out_dir / f"{stem}.idx.oft.cspt" + if idx.exists() and idx_oft.exists() and (not idx_cspt.exists() or idx_oft_regenerated): + if idx_cspt.exists(): + idx_cspt.unlink() + print(f" Generating {idx_cspt.name} ...", end=" ", flush=True) + idx_cspt.write_bytes(_build_cspt(idx.read_bytes(), idx_oft.read_bytes(), skip_per_entry=8)) + print(f"{idx_cspt.stat().st_size} bytes") + steps_run += 1 + + # Step 6: Generate .syn.oft.cspt (requires .syn and .syn.oft). + # Regenerate if .cspt is missing OR if .syn.oft was just rebuilt this run. + syn_cspt = out_dir / f"{stem}.syn.oft.cspt" + if syn.exists() and syn_oft.exists() and (not syn_cspt.exists() or syn_oft_regenerated): + if syn_cspt.exists(): + syn_cspt.unlink() + print(f" Generating {syn_cspt.name} ...", end=" ", flush=True) + syn_cspt.write_bytes(_build_cspt(syn.read_bytes(), syn_oft.read_bytes(), skip_per_entry=4)) + print(f"{syn_cspt.stat().st_size} bytes") + steps_run += 1 + + if steps_run == 0: + print(" No preparation steps needed -- dictionary already processed.") + else: + print(f" Done. {steps_run} step(s) completed.") + print(f" Output: {out_dir}") + + +# --------------------------------------------------------------------------- +# lookup subcommand +# --------------------------------------------------------------------------- + +def _scan_idx(idx_data: bytes, idx_oft_path: Path, word: str) -> tuple[int, int] | None: + """ + Search idx_data for an exact match of word. + Returns (dict_offset, size) on match, None if not found. + Uses .idx.oft.cspt if present, then .idx.oft as fallback. + """ + target = word.encode("utf-8") + start_pos = 0 + end_pos = len(idx_data) + + # Try .cspt first; on bad magic / truncation, fall through to .oft. + cspt_used = False + cspt_path = Path(str(idx_oft_path) + ".cspt") + if cspt_path.exists(): + cspt_data = cspt_path.read_bytes() + if len(cspt_data) >= _CSPT_HEADER_SIZE and cspt_data[:4] == _CSPT_MAGIC: + prefix_len = cspt_data[4 + 1] # byte 5 + entry_count = struct.unpack_from(" 0: + lo, hi = 0, entry_count - 1 + while lo < hi: + mid = lo + (hi - lo + 1) // 2 + off = _CSPT_HEADER_SIZE + mid * entry_size + prefix = cspt_data[off:off + prefix_len].rstrip(b"\x00") + if prefix.lower() > target.lower(): + hi = mid - 1 + else: + lo = mid + + off = _CSPT_HEADER_SIZE + lo * entry_size + start_pos = struct.unpack_from("= len(idx_data): + break + try: + null = idx_data.index(b"\x00", pos) + except ValueError: + break + if idx_data[pos:null].lower() <= target.lower(): + best = pos + else: + break + start_pos = best + + pos = start_pos + while pos < end_pos: + try: + null = idx_data.index(b"\x00", pos) + except ValueError: + break + entry_word = idx_data[pos:null] + entry_offset, entry_size = struct.unpack_from(">II", idx_data, null + 1) + pos = null + 1 + 8 + + if entry_word == target: + return entry_offset, entry_size + + return None + + +def lookup(folder: Path, word: str) -> None: + if not folder.is_dir(): + print(f"ERROR: not a directory: {folder}", file=sys.stderr) + sys.exit(1) + + stem = _find_stem(folder) + dict_path = folder / f"{stem}.dict" + dict_dz_path = folder / f"{stem}.dict.dz" + idx_path = folder / f"{stem}.idx" + idx_oft_path = folder / f"{stem}.idx.oft" + + if not dict_path.exists(): + if dict_dz_path.exists(): + print( + f"ERROR: dictionary is still compressed. " + f"Run: python3 scripts/dict_tools.py prep {folder}", + file=sys.stderr, + ) + else: + print(f"ERROR: {dict_path.name} not found in {folder}", file=sys.stderr) + sys.exit(1) + + if not idx_path.exists(): + print(f"ERROR: {idx_path.name} not found in {folder}", file=sys.stderr) + sys.exit(1) + + result = _scan_idx(idx_path.read_bytes(), idx_oft_path, word) + + if result is None: + print(f"Not found: {word}", file=sys.stderr) + sys.exit(1) + + entry_offset, entry_size = result + with open(dict_path, "rb") as f: + f.seek(entry_offset) + print(f.read(entry_size).decode("utf-8")) + + +# --------------------------------------------------------------------------- +# merge subcommand +# --------------------------------------------------------------------------- + +def merge(sources: list[Path], output: Path) -> None: + """Merge multiple StarDict dictionaries into a single dictionary.""" + if len(sources) < 2: + print("ERROR: merge requires at least 2 source folders", file=sys.stderr) + sys.exit(1) + + # Dict-based merge: accumulate definitions per headword (no sort needed) + # word -> list of (src_idx, word_idx) for definition retrieval + synonym remapping + word_entries: dict[str, list[tuple[int, int]]] = {} + all_syns: list[tuple[str, int, int]] = [] # (synonym, orig_word_idx, src_idx) + source_dicts: list[bytes] = [] + source_offsets: list[list[tuple[int, int]]] = [] + sametypesequence: str | None = None + + for src_idx, src_folder in enumerate(sources): + if not src_folder.is_dir(): + print(f"ERROR: not a directory: {src_folder}", file=sys.stderr) + sys.exit(1) + + stem = _find_stem(src_folder) + idx_path = src_folder / f"{stem}.idx" + dict_path = src_folder / f"{stem}.dict" + dict_dz = src_folder / f"{stem}.dict.dz" + syn_path = src_folder / f"{stem}.syn" + ifo_path = src_folder / f"{stem}.ifo" + + if not dict_path.exists(): + if dict_dz.exists(): + print( + f"ERROR: {dict_path.name} not found but .dict.dz exists in {src_folder}. " + f"Run: python3 scripts/dictionary_tools.py prep {src_folder}", + file=sys.stderr, + ) + else: + print(f"ERROR: {dict_path.name} not found in {src_folder}", file=sys.stderr) + sys.exit(1) + + if not idx_path.exists(): + print(f"ERROR: {idx_path.name} not found in {src_folder}", file=sys.stderr) + sys.exit(1) + + if ifo_path.exists(): + ifo = _parse_ifo(ifo_path) + sts = ifo.get("sametypesequence", "") + if sametypesequence is None: + sametypesequence = sts + elif sts and sts != sametypesequence: + print( + f"WARNING: sametypesequence mismatch: " + f"'{sametypesequence}' vs '{sts}' in {src_folder}", + file=sys.stderr, + ) + + idx_data = idx_path.read_bytes() + dict_data = dict_path.read_bytes() + entries = _parse_all_idx(idx_data) + + source_dicts.append(dict_data) + source_offsets.append([(offset, size) for _, offset, size in entries]) + + for word_idx, (word, _, _) in enumerate(entries): + word_entries.setdefault(word, []).append((src_idx, word_idx)) + + if syn_path.exists(): + for syn_word, target_idx in _parse_all_syn(syn_path.read_bytes()): + all_syns.append((syn_word, target_idx, src_idx)) + + # Sort headwords once (just strings, no payloads) + sorted_words = sorted(word_entries.keys(), key=lambda w: (w.lower(), w)) + + # Build merged output + merged_defs: list[bytes] = [] + index_remap: dict[tuple[int, int], int] = {} + + for new_idx, word in enumerate(sorted_words): + parts: list[bytes] = [] + for src_idx, orig_idx in word_entries[word]: + index_remap[(src_idx, orig_idx)] = new_idx + offset, size = source_offsets[src_idx][orig_idx] + parts.append(source_dicts[src_idx][offset:offset + size]) + merged_defs.append(b"".join(parts)) + + # Write output + output.mkdir(parents=True, exist_ok=True) + out_stem = output.name + + # .dict + dict_bytes = bytearray() + dict_offsets: list[tuple[int, int]] = [] + for defn in merged_defs: + dict_offsets.append((len(dict_bytes), len(defn))) + dict_bytes += defn + (output / f"{out_stem}.dict").write_bytes(dict_bytes) + + # .idx + idx_parts: list[bytes] = [] + for word, (offset, size) in zip(sorted_words, dict_offsets): + idx_parts.append(word.encode("utf-8") + b"\x00" + struct.pack(">II", offset, size)) + idx_bytes = b"".join(idx_parts) + (output / f"{out_stem}.idx").write_bytes(idx_bytes) + + # .syn (with remapped indices, sorted) + syn_count = 0 + if all_syns: + remapped: list[tuple[str, int]] = [] + for syn_word, orig_target, src_idx in all_syns: + new_target = index_remap.get((src_idx, orig_target)) + if new_target is not None: + remapped.append((syn_word, new_target)) + remapped.sort(key=lambda e: (e[0].lower(), e[0])) + if remapped: + syn_bytes = b"" + for syn_word, target in remapped: + syn_bytes += syn_word.encode("utf-8") + b"\x00" + struct.pack(">I", target) + (output / f"{out_stem}.syn").write_bytes(syn_bytes) + syn_count = len(remapped) + + # .ifo + ifo_fields = { + "bookname": out_stem, + "wordcount": str(len(sorted_words)), + "idxfilesize": str(len(idx_bytes)), + } + if sametypesequence: + ifo_fields["sametypesequence"] = sametypesequence + if syn_count: + ifo_fields["synwordcount"] = str(syn_count) + _write_ifo(output / f"{out_stem}.ifo", ifo_fields) + + # .oft files + .cspt files + idx_oft_bytes = _build_oft(idx_bytes, skip_bytes_after_null=8) + (output / f"{out_stem}.idx.oft").write_bytes(idx_oft_bytes) + (output / f"{out_stem}.idx.oft.cspt").write_bytes( + _build_cspt(idx_bytes, idx_oft_bytes, skip_per_entry=8) + ) + if syn_count: + syn_data = (output / f"{out_stem}.syn").read_bytes() + syn_oft_bytes = _build_oft(syn_data, skip_bytes_after_null=4) + (output / f"{out_stem}.syn.oft").write_bytes(syn_oft_bytes) + (output / f"{out_stem}.syn.oft.cspt").write_bytes( + _build_cspt(syn_data, syn_oft_bytes, skip_per_entry=4) + ) + + print(f"Merged {len(sources)} dictionaries -> {output}") + print(f" Words: {len(sorted_words)}, Synonyms: {syn_count}") + print(f" Dict: {len(dict_bytes)} bytes, Idx: {len(idx_bytes)} bytes") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser( + description="Offline StarDict dictionary tools for CrossPoint Reader.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + sub = parser.add_subparsers(dest="command", required=True) + + p_prep = sub.add_parser("prep", help="Pre-process a dictionary folder") + p_prep.add_argument("folder", help="Path to the dictionary folder to process") + + p_lookup = sub.add_parser("lookup", help="Look up a word in a prepared dictionary") + p_lookup.add_argument("folder", help="Path to the prepared dictionary folder") + p_lookup.add_argument("word", help="Word to look up (exact match, case-sensitive)") + + p_merge = sub.add_parser("merge", help="Merge multiple StarDict dictionaries into one") + p_merge.add_argument( + "--source", action="append", required=True, dest="sources", + help="Path to a source dictionary folder (specify multiple times)", + ) + p_merge.add_argument( + "--output", required=True, + help="Path to the output dictionary folder", + ) + + args = parser.parse_args() + + if args.command == "prep": + prep(Path(args.folder)) + elif args.command == "lookup": + lookup(Path(args.folder), args.word) + elif args.command == "merge": + merge([Path(s) for s in args.sources], Path(args.output)) + + +if __name__ == "__main__": + main() diff --git a/scripts/gen_i18n.py b/scripts/gen_i18n.py index 03943c0539..93fa45fd92 100755 --- a/scripts/gen_i18n.py +++ b/scripts/gen_i18n.py @@ -29,9 +29,10 @@ python gen_i18n.py --strip-unused --src-dirs src lib/EpdFont """ -import sys +import hashlib import os import re +import sys from pathlib import Path from typing import Dict, List, Optional, Set, Tuple @@ -527,14 +528,14 @@ def generate_keys_header( lines.append("") # Sorted language indices for display order - # (English first, then by language code alphabetically) + # (English first, then by native language name alphabetically) english_idx = languages.index("EN") rest = sorted( (i for i in range(len(languages)) if i != english_idx), - key=lambda i: languages[i], + key=lambda i: language_names[i], ) sorted_indices = [english_idx] + rest - lines.append("// Sorted language indices by code (auto-generated by gen_i18n.py)") + lines.append("// Sorted language indices by native name (auto-generated by gen_i18n.py)") for rank, idx in enumerate(sorted_indices): lines.append(f"// {rank:>2}: {languages[idx]:<4} {language_names[idx]}") lines.append( @@ -999,5 +1000,11 @@ def main( try: Import("env") main(strip_unused=True) + keys_path = Path("lib/I18n/I18nKeys.h") + layout_hash = hashlib.sha256(keys_path.read_bytes()).hexdigest()[:16] + # StrId values are compiled into every tr(...) call. Include the + # generated layout in the command signature so PlatformIO rebuilds + # consumers when IDs move, not only the generated string table. + env.Append(CPPDEFINES=[f"I18N_LAYOUT_{layout_hash}"]) except NameError: pass diff --git a/scripts/generate_br_section_break_epub.py b/scripts/generate_br_section_break_epub.py index a22d44625c..1733c2e95c 100644 --- a/scripts/generate_br_section_break_epub.py +++ b/scripts/generate_br_section_break_epub.py @@ -177,7 +177,7 @@ def build_epub(path): test-epub-br-section-break - Test: br Section Break + Test: br Section Break & li/p en diff --git a/scripts/generate_kerning_ligature_epub.py b/scripts/generate_kerning_ligature_epub.py index 1ba640ca62..7d833d8f67 100644 --- a/scripts/generate_kerning_ligature_epub.py +++ b/scripts/generate_kerning_ligature_epub.py @@ -205,7 +205,7 @@ def create_cover_image(): _draw_text_centered(draw, subtitle_y, "A Typographer\u2019s Compendium", font_subtitle, text_color, width) - _draw_text_centered(draw, height - 70, "CROSSPOINT TEST FIXTURES", + _draw_text_centered(draw, height - 70, "CROSSINK TEST FIXTURES", font_author, text_color, width) buf = io.BytesIO() diff --git a/scripts/generate_release_catalog.py b/scripts/generate_release_catalog.py index a1b7954174..e9b4541201 100755 --- a/scripts/generate_release_catalog.py +++ b/scripts/generate_release_catalog.py @@ -3,7 +3,7 @@ Generate public firmware manifests consumed by external apps and OTA. The catalog follows the simple schema requested by downstream clients and now -emits one entry per firmware build variant. +emits one entry per firmware device type. """ import argparse @@ -14,7 +14,11 @@ from pathlib import Path -VARIANT_ORDER = ('tiny', 'xlarge') +DEVICE_TYPE_ORDER = ('x3-x4', 'sticky') +DEFAULT_SUPPORTED_DEVICES = { + 'x3-x4': ['x4', 'x3'], + 'sticky': ['sticky'], +} FIRMWARE_NAME_PATTERN = re.compile(r'^firmware-(?P.+?)-v[^/]+\.bin$') @@ -42,7 +46,7 @@ def parse_args(): required=True, action='append', type=Path, - help='Path to a firmware .bin artifact. Pass once per build variant.', + help='Path to a firmware .bin artifact. Pass once per firmware device type.', ) parser.add_argument('--output', required=True, type=Path, help='Output catalog path. Use "catalog" for /catalog.') parser.add_argument('--repo', required=True, help='GitHub repository in owner/name form.') @@ -65,52 +69,52 @@ def parse_args(): return parser.parse_args() -def parse_variant(firmware_path): +def parse_device_type(firmware_path): match = FIRMWARE_NAME_PATTERN.match(firmware_path.name) if match: return match.group('variant') return firmware_path.parent.name -def sort_key_for_variant(variant): +def sort_key_for_device_type(device_type): try: - return (VARIANT_ORDER.index(variant), variant) + return (DEVICE_TYPE_ORDER.index(device_type), device_type) except ValueError: - return (len(VARIANT_ORDER), variant) + return (len(DEVICE_TYPE_ORDER), device_type) def main(): args = parse_args() version = normalize_version(args.version) - supported_devices = args.supported_devices or ['x4', 'x3'] notes = args.notes or f'CrossInk {version} {args.channel} firmware' firmware_base_url = args.firmware_base_url or f'https://github.com/{args.repo}/releases/download/v{version}/' firmware_base_url = firmware_base_url.rstrip('/') + '/' releases = [] - seen_variants = set() - firmware_paths = sorted(args.firmware, key=lambda path: sort_key_for_variant(parse_variant(path))) + seen_device_types = set() + firmware_paths = sorted(args.firmware, key=lambda path: sort_key_for_device_type(parse_device_type(path))) for firmware_path in firmware_paths: if not firmware_path.is_file(): raise SystemExit(f'Firmware artifact not found: {firmware_path}') filename = firmware_path.name - variant = parse_variant(firmware_path) + device_type = parse_device_type(firmware_path) + supported_devices = args.supported_devices or DEFAULT_SUPPORTED_DEVICES.get(device_type, []) firmware_url = f'{firmware_base_url}{filename}' firmware_sha256 = sha256_file(firmware_path) firmware_size = firmware_path.stat().st_size - if variant in seen_variants: - raise SystemExit(f'Duplicate firmware variant supplied: {variant}') - seen_variants.add(variant) + if device_type in seen_device_types: + raise SystemExit(f'Duplicate firmware device type supplied: {device_type}') + seen_device_types.add(device_type) releases.append( { - 'id': f'{args.channel}-{version}-{variant}', + 'id': f'{args.channel}-{version}-{device_type}', 'channel': args.channel, 'name': version, 'version': version, - 'variant': variant, + 'variant': device_type, 'released_at': args.released_at, 'notes': notes, 'firmware_url': firmware_url, diff --git a/scripts/git_branch.py b/scripts/git_branch.py index 6d844c6682..882e8d8e2c 100644 --- a/scripts/git_branch.py +++ b/scripts/git_branch.py @@ -1,10 +1,12 @@ """ PlatformIO pre-build script: inject git info into version defines. - default: 1.1.0-dev+ + default: 1.1.0-dev+ (local development builds) + production: 1.1.0 (when $CROSSINK_RELEASE_VERSION is set) + default RC: 1.1.0-rc+ (when $CROSSINK_RC_HASH is set) test & debug: 1.2.6-+<5-char-hash> - gh_release_rc: 1.1.0-rc+<5-char-hash> (hash from $CROSSPOINT_RC_HASH in - CI, or from git locally) + gh_release_rc: 1.1.0-rc+ (hash from $CROSSINK_RC_HASH in CI, + or from git locally) All other environments set CROSSINK_VERSION directly in platformio.ini. """ @@ -88,28 +90,41 @@ def get_git_short_sha(project_dir): def _read_ini(project_dir): ini_path = os.path.join(project_dir, 'platformio.ini') + local_ini_path = os.path.join(project_dir, 'platformio.local.ini') config = configparser.ConfigParser() if os.path.isfile(ini_path): - config.read(ini_path) + config_paths = [ini_path] + if os.path.isfile(local_ini_path): + # Match PlatformIO's local override convention: values from the + # optional local file take precedence over the tracked config. + config_paths.append(local_ini_path) + config.read(config_paths) else: warn(f'platformio.ini not found at {ini_path}') return config -def get_base_version(project_dir): +def get_crossink_version(project_dir): config = _read_ini(project_dir) - if not config.has_option('crosspoint', 'version'): - warn('No [crosspoint] version in platformio.ini; base version will be "0.0.0"') + if not config.has_option('crossink', 'version'): + warn( + 'No [crossink] version in platformio.ini or platformio.local.ini; ' + 'build version will be "0.0.0"' + ) return '0.0.0' - return config.get('crosspoint', 'version') + return config.get('crossink', 'version') -def get_crossink_version(project_dir): - config = _read_ini(project_dir) - if not config.has_option('crosspoint', 'crossink_version'): - warn('No [crosspoint] crossink_version in platformio.ini; falling back to version') - return get_base_version(project_dir) - return config.get('crosspoint', 'crossink_version') +def get_release_candidate_version(project_dir): + short_hash = os.environ.get('CROSSINK_RC_HASH') or get_git_short_hash(project_dir) + return f'{get_crossink_version(project_dir)}-rc+{sanitize_version_component(short_hash)}' + + +def get_production_version(project_dir): + release_version = os.environ.get('CROSSINK_RELEASE_VERSION') + if release_version: + return sanitize_version_component(release_version.lstrip('v')) + return get_crossink_version(project_dir) def inject_version(env): @@ -117,11 +132,18 @@ def inject_version(env): pioenv = env['PIOENV'] if pioenv == 'default': - base_version = get_crossink_version(project_dir) - branch = get_git_branch(project_dir) - version_string = f'{base_version}-dev+{branch}' + if os.environ.get('CROSSINK_RC_HASH'): + version_string = get_release_candidate_version(project_dir) + print(f'CrossInk RC build version: {version_string}') + elif os.environ.get('CROSSINK_RELEASE_VERSION'): + version_string = get_production_version(project_dir) + print(f'CrossInk production build version: {version_string}') + else: + base_version = get_crossink_version(project_dir) + branch = get_git_branch(project_dir) + version_string = f'{base_version}-dev+{branch}' + print(f'CrossInk build version: {version_string}') env.Append(CPPDEFINES=[('CROSSINK_VERSION', f'\\"{version_string}\\"')]) - print(f'CrossInk build version: {version_string}') elif pioenv == 'debug': branch = get_git_branch(project_dir) @@ -135,6 +157,18 @@ def inject_version(env): ]) print(f'CrossInk test build version: {ci_version}{suffix}') + elif pioenv == 'sticky-debug': + branch = get_git_branch(project_dir) + short_hash = get_git_short_hash(project_dir) + ci_version = get_crossink_version(project_dir) + suffix = f'-{branch}+{short_hash}' + env.Append(CPPDEFINES=[ + ('CROSSINK_VERSION', f'\\"{ci_version}{suffix}\\"'), + ('CROSSINK_BUILD_ENV', '\\"debug\\"'), + 'CROSSINK_SHOW_SLEEP_BUILD_INFO', + ]) + print(f'CrossInk test build version: {ci_version}{suffix}') + elif pioenv == 'test': branch = get_git_branch(project_dir) short_hash = get_git_short_hash(project_dir) @@ -146,14 +180,12 @@ def inject_version(env): print(f'CrossInk test build version: {ci_version}{suffix}') elif pioenv == 'gh_release_rc': - # CI passes CROSSPOINT_RC_HASH as an env var; locally we derive it from git. - short_hash = os.environ.get('CROSSPOINT_RC_HASH') or get_git_short_hash(project_dir) - ci_version = get_crossink_version(project_dir) - rc_suffix = f'-rc+{short_hash}' + # CI passes CROSSINK_RC_HASH as an env var; locally we derive it from git. + version_string = get_release_candidate_version(project_dir) env.Append(CPPDEFINES=[ - ('CROSSINK_VERSION', f'\\"{ci_version}{rc_suffix}\\"'), + ('CROSSINK_VERSION', f'\\"{version_string}\\"'), ]) - print(f'CrossInk RC build version: {ci_version}{rc_suffix}') + print(f'CrossInk RC build version: {version_string}') # PlatformIO/SCons entry point — Import and env are SCons builtins injected at runtime. diff --git a/scripts/patch_wolfssl.py b/scripts/patch_wolfssl.py new file mode 100644 index 0000000000..4c6283c075 --- /dev/null +++ b/scripts/patch_wolfssl.py @@ -0,0 +1,33 @@ +from pathlib import Path + +Import("env") + + +PROJECT_DIR = Path(env.subst("$PROJECT_DIR")) +MARKER = "/* CrossPoint wolfSSL compatibility overrides */" +OVERRIDES = f""" + +{MARKER} +#undef NO_DH +#ifndef HAVE_FFDHE_2048 +#define HAVE_FFDHE_2048 +#endif +#undef FP_MAX_BITS +#define FP_MAX_BITS 8192 +""" + + +def patch_user_settings(path: Path) -> None: + original_text = path.read_text() + text = original_text + if MARKER in text: + text = text.split(MARKER, 1)[0].rstrip() + patched_text = text + OVERRIDES + "\n" + if original_text == patched_text: + return + path.write_text(patched_text) + print(f"Patched wolfSSL settings: {path.relative_to(PROJECT_DIR)}") + + +for settings in PROJECT_DIR.glob(".pio/libdeps/*/Arduino-wolfSSL/src/user_settings.h"): + patch_user_settings(settings) diff --git a/scripts/preview_web.py b/scripts/preview_web.py index d0f5225f99..ddd5f67838 100644 --- a/scripts/preview_web.py +++ b/scripts/preview_web.py @@ -71,8 +71,8 @@ def render_page(slug): ], "/api/settings": [ {"key": "darkMode", "name": "Dark Mode", "category": "Display", "type": "toggle", "value": 1}, - {"key": "fontSize", "name": "Font Size", "category": "Display", "type": "enum", - "value": 1, "options": ["Small", "Medium", "Large"]}, + {"key": "fontSize", "name": "Font Size", "category": "Reader", "type": "enum", + "value": 2, "options": ["10 pt", "12 pt", "14 pt", "16 pt"]}, {"key": "margin", "name": "Page Margin", "category": "Reading", "type": "value", "value": 20, "min": 0, "max": 50, "step": 5}, {"key": "sleepScreen", "name": "Sleep Screen", "category": "Reading", "type": "enum", diff --git a/scripts/release.sh b/scripts/release.sh index 36f35e285b..4ace98b0db 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Usage: ./scripts/release.sh 1.2.7 -# Bumps crossink_version in platformio.ini, commits, and creates the git tag. +# Bumps [crossink] version in platformio.ini, commits, and creates the git tag. set -euo pipefail VERSION="${1:-}" @@ -9,8 +9,8 @@ if [[ -z "$VERSION" ]]; then exit 1 fi -sed -i '' "s/crossink_version = .*/crossink_version = $VERSION/" platformio.ini +sed -i '' "/^\\[crossink\\]/,/^\\[/ s/^version = .*/version = $VERSION/" platformio.ini git add platformio.ini -git commit -m "Update crossink_version to $VERSION" +git commit -m "Update crossink version to $VERSION" git tag "v$VERSION" echo "Tagged v$VERSION — push with: git push && git push origin v$VERSION" diff --git a/scripts/rename_firmware.py b/scripts/rename_firmware.py index 4c9f8d2c83..2358bd3f42 100644 --- a/scripts/rename_firmware.py +++ b/scripts/rename_firmware.py @@ -3,16 +3,15 @@ in the same build directory. Default outputs: - .pio/build/tiny/firmware-tiny.bin - .pio/build/xlarge/firmware-xlarge.bin + .pio/build/default/firmware-x3-x4.bin + .pio/build/sticky/firmware-sticky.bin -Release-candidate outputs when CROSSPOINT_RC_ARTIFACTS=1: - .pio/build/tiny/firmware-tiny---RC.bin - .pio/build/xlarge/firmware-xlarge---RC.bin +Release-candidate outputs when CROSSINK_RC_ARTIFACTS=1: + .pio/build/default/firmware-x3-x4---RC.bin -Release outputs when CROSSPOINT_RELEASE_VERSION is set: - .pio/build/tiny/firmware-tiny-v.bin - .pio/build/xlarge/firmware-xlarge-v.bin +Release outputs when CROSSINK_RELEASE_VERSION is set: + .pio/build/default/firmware-x3-x4-v.bin + .pio/build/sticky/firmware-sticky-v.bin """ import os @@ -77,16 +76,20 @@ def _sanitize_branch(branch): return branch or 'unknown' +def _get_firmware_device_type(env): + return _get_project_option(env, 'custom_firmware_device_type') or env['PIOENV'] + + def _get_rc_artifact_name(project_dir, env): - env_name = _get_project_option(env, 'custom_rc_variant') or env['PIOENV'] + device_type = _get_firmware_device_type(env) branch = ( _get_project_option(env, 'custom_rc_branch') - or os.environ.get('CROSSPOINT_RC_BRANCH') + or os.environ.get('CROSSINK_RC_BRANCH') or _get_git_branch(project_dir) ) short_hash = ( _get_project_option(env, 'custom_rc_hash') - or os.environ.get('CROSSPOINT_RC_HASH') + or os.environ.get('CROSSINK_RC_HASH') or _get_git_value( project_dir, 'rev-parse', @@ -97,16 +100,16 @@ def _get_rc_artifact_name(project_dir, env): ) branch = _sanitize_branch(branch) short_hash = re.sub(r'[^A-Za-z0-9]+', '', short_hash)[:12] or '00000' - return f'firmware-{env_name}-{branch}-{short_hash}-RC.bin' + return f'firmware-{device_type}-{branch}-{short_hash}-RC.bin' def _is_rc_artifact_build(env): - flag = _get_project_option(env, 'custom_rc_artifacts') or os.environ.get('CROSSPOINT_RC_ARTIFACTS') + flag = _get_project_option(env, 'custom_rc_artifacts') or os.environ.get('CROSSINK_RELEASE_VERSION') return str(flag).strip().lower() in {'1', 'true', 'yes', 'on'} def _get_release_version(env): - version = _get_project_option(env, 'custom_release_version') or os.environ.get('CROSSPOINT_RELEASE_VERSION') + version = _get_project_option(env, 'custom_release_version') or os.environ.get('CROSSINK_RELEASE_VERSION') if not version: return None version = version.strip() @@ -118,12 +121,12 @@ def _get_release_version(env): def rename_firmware(source, target, env): - env_name = env['PIOENV'] project_dir = env['PROJECT_DIR'] src = str(target[0]) build_dir = os.path.dirname(src) - default_dst = os.path.join(build_dir, f'firmware-{env_name}.bin') + device_type = _get_firmware_device_type(env) + default_dst = os.path.join(build_dir, f'firmware-{device_type}.bin') _copy_artifact(src, default_dst) if _is_rc_artifact_build(env): @@ -132,8 +135,7 @@ def rename_firmware(source, target, env): release_version = _get_release_version(env) if release_version: - release_env_name = _get_project_option(env, 'custom_rc_variant') or env_name - release_dst = os.path.join(build_dir, f'firmware-{release_env_name}-{release_version}.bin') + release_dst = os.path.join(build_dir, f'firmware-{device_type}-{release_version}.bin') _copy_artifact(src, release_dst) diff --git a/scripts/run_simulator_smoke_test.py b/scripts/run_simulator_smoke_test.py index d5d07f180c..b75238b092 100755 --- a/scripts/run_simulator_smoke_test.py +++ b/scripts/run_simulator_smoke_test.py @@ -13,7 +13,6 @@ ROOT = Path(__file__).resolve().parents[1] -PROGRAM = ROOT / ".pio" / "build" / "simulator" / "program" DEFAULT_BOOK = ROOT / "test" / "epubs" / "test_reader_rendering_matrix.epub" CRASH_PATTERNS = ( "std::bad_alloc", @@ -35,12 +34,17 @@ "lyra-carousel": 4, "lyra_carousel": 4, "carousel": 4, + "dashboard": 6, } -def build_simulator() -> None: - print("Building simulator...", flush=True) - proc = subprocess.run(["pio", "run", "-e", "simulator"], cwd=ROOT) +def program_path(env_name: str) -> Path: + return ROOT / ".pio" / "build" / env_name / "program" + + +def build_simulator(env_name: str) -> None: + print(f"Building {env_name} simulator...", flush=True) + proc = subprocess.run(["pio", "run", "-e", env_name], cwd=ROOT) if proc.returncode != 0: raise SystemExit(proc.returncode) @@ -61,11 +65,12 @@ def run_smoke(args: argparse.Namespace) -> int: return 2 if args.build: - build_simulator() + build_simulator(args.env) - if not PROGRAM.exists(): - print(f"Simulator binary not found: {PROGRAM}", file=sys.stderr) - print("Run: pio run -e simulator", file=sys.stderr) + program = program_path(args.env) + if not program.exists(): + print(f"Simulator binary not found: {program}", file=sys.stderr) + print(f"Run: pio run -e {args.env}", file=sys.stderr) return 2 with tempfile.TemporaryDirectory(prefix="crossink-sim-smoke-") as temp_dir_name: @@ -83,7 +88,7 @@ def run_smoke(args: argparse.Namespace) -> int: print(f"Running simulator smoke test with isolated fs_: {temp_root / 'fs_'}", flush=True) proc = subprocess.run( - [str(PROGRAM)], + [str(program)], cwd=temp_root, env=env, text=True, @@ -113,6 +118,8 @@ def run_smoke(args: argparse.Namespace) -> int: def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--book", default=str(DEFAULT_BOOK), help="EPUB fixture to copy into the isolated simulator fs_") + parser.add_argument("--env", choices=("simulator", "sticky-simulator", "x4-pro-simulator"), default="simulator", + help="PlatformIO simulator environment to build and run") parser.add_argument("--timeout", type=int, default=45, help="Seconds before the simulator run is treated as hung") parser.add_argument("--page-turns", type=int, default=2, help="Number of EPUB page-forward taps to run") parser.add_argument("--theme", choices=sorted(THEMES), help="UI theme to use during the smoke test") diff --git a/site/astro.config.mjs b/site/astro.config.mjs new file mode 100644 index 0000000000..d116223337 --- /dev/null +++ b/site/astro.config.mjs @@ -0,0 +1,65 @@ +import { fileURLToPath } from "node:url"; +import { defineConfig } from "astro/config"; +import { remarkAlert } from "remark-github-blockquote-alert"; + +// Docs live at the repo root (../docs), outside the Astro project root, so Vite's +// file watcher never sees them and edits to docs/**/*.md don't hot reload in dev. +// Add the docs directory to the dev watcher so content edits invalidate and refresh. +const docsDir = fileURLToPath(new URL("../docs", import.meta.url)); + +function watchExternalDocs() { + return { + name: "crossink:watch-external-docs", + apply: "serve", + configureServer(server) { + server.watcher.add(docsDir); + }, + }; +} + +function rewriteMarkdownLinks() { + return (tree) => { + visit(tree, (node) => { + if (node.type !== "link" && node.type !== "image") { + return; + } + + if (typeof node.url !== "string") { + return; + } + + const rootDocUrl = node.url + .replace(/^(\.\.\/)+SCOPE\.md(#.*)?$/i, "https://github.com/uxjulia/CrossInk/blob/main/SCOPE.md$2") + .replace(/^(\.\.\/)+GOVERNANCE\.md(#.*)?$/i, "https://github.com/uxjulia/CrossInk/blob/main/GOVERNANCE.md$2"); + if (rootDocUrl !== node.url) { + node.url = rootDocUrl; + return; + } + + node.url = node.url.replace(/(^|\/)README\.md(#.*)?$/i, "$1index.html$2").replace(/\.md(#.*)?$/i, ".html$1"); + }); + }; +} + +function visit(node, visitor) { + visitor(node); + if (!Array.isArray(node.children)) { + return; + } + for (const child of node.children) { + visit(child, visitor); + } +} + +export default defineConfig({ + build: { + format: "file", + }, + markdown: { + remarkPlugins: [remarkAlert, rewriteMarkdownLinks], + }, + vite: { + plugins: [watchExternalDocs()], + }, + site: "https://www.crossink.dev", +}); diff --git a/site/package-lock.json b/site/package-lock.json new file mode 100644 index 0000000000..00c8b59539 --- /dev/null +++ b/site/package-lock.json @@ -0,0 +1,6486 @@ +{ + "name": "site", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@astrojs/check": "^0.9.4", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "astro": "^5.0.0", + "remark-github-blockquote-alert": "^2.1.0", + "typescript": "^5.6.0" + } + }, + "node_modules/@astrojs/check": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.9.tgz", + "integrity": "sha512-A5UW8uIuErLWEoRQvzgXpO1gTjUFtK8r7nU2Z7GewAMxUb7bPvpk11qaKKgxqXlHJWlAvaaxy+Xg28A6bmQ1Tg==", + "license": "MIT", + "dependencies": { + "@astrojs/language-server": "^2.16.7", + "chokidar": "^4.0.3", + "kleur": "^4.1.5", + "yargs": "^17.7.2" + }, + "bin": { + "astro-check": "bin/astro-check.js" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/@astrojs/compiler": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.1.tgz", + "integrity": "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.6.tgz", + "integrity": "sha512-GOle7smBWKfMSP8osUIGOlB5kaHdQLV3foCsf+5Q9Wsuu+C6Fs3Ez/ttXmhjZ1HkSgsogcM1RXSjjOVieHq16Q==", + "license": "MIT" + }, + "node_modules/@astrojs/language-server": { + "version": "2.16.10", + "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.16.10.tgz", + "integrity": "sha512-87VQ/5GSdHlRnUA+hGuerYyIGAj+9RbZmATyuKLEUePinUXhQ5YkRnRrHhOD9sSi5JOErLjrLkHnfZFEvGrV8w==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.13.1", + "@astrojs/yaml2ts": "^0.2.4", + "@jridgewell/sourcemap-codec": "^1.5.5", + "@volar/kit": "~2.4.28", + "@volar/language-core": "~2.4.28", + "@volar/language-server": "~2.4.28", + "@volar/language-service": "~2.4.28", + "muggle-string": "^0.4.1", + "tinyglobby": "^0.2.16", + "volar-service-css": "0.0.70", + "volar-service-emmet": "0.0.70", + "volar-service-html": "0.0.70", + "volar-service-prettier": "0.0.70", + "volar-service-typescript": "0.0.70", + "volar-service-typescript-twoslash-queries": "0.0.70", + "volar-service-yaml": "0.0.70", + "vscode-html-languageservice": "^5.6.2", + "vscode-uri": "^3.1.0" + }, + "bin": { + "astro-ls": "bin/nodeServer.js" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "prettier-plugin-astro": ">=0.11.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + } + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "6.3.11", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.3.11.tgz", + "integrity": "sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.7.6", + "@astrojs/prism": "3.3.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "shiki": "^3.21.0", + "smol-toml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/prism": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.3.0.tgz", + "integrity": "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz", + "integrity": "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.2.0", + "debug": "^4.4.0", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "is-docker": "^3.0.0", + "is-wsl": "^3.1.0", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@astrojs/yaml2ts": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@astrojs/yaml2ts/-/yaml2ts-0.2.4.tgz", + "integrity": "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A==", + "license": "MIT", + "dependencies": { + "yaml": "^2.8.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", + "integrity": "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@emmetio/abbreviation": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz", + "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==", + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-abbreviation": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz", + "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==", + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-parser": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@emmetio/css-parser/-/css-parser-0.4.1.tgz", + "integrity": "sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ==", + "license": "MIT", + "dependencies": { + "@emmetio/stream-reader": "^2.2.0", + "@emmetio/stream-reader-utils": "^0.1.0" + } + }, + "node_modules/@emmetio/html-matcher": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emmetio/html-matcher/-/html-matcher-1.3.0.tgz", + "integrity": "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==", + "license": "ISC", + "dependencies": { + "@emmetio/scanner": "^1.0.0" + } + }, + "node_modules/@emmetio/scanner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz", + "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==", + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz", + "integrity": "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==", + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz", + "integrity": "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==", + "license": "MIT" + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource-variable/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, + "node_modules/@volar/kit": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.4.28.tgz", + "integrity": "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==", + "license": "MIT", + "dependencies": { + "@volar/language-service": "2.4.28", + "@volar/typescript": "2.4.28", + "typesafe-path": "^0.2.2", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/language-server": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-server/-/language-server-2.4.28.tgz", + "integrity": "sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw==", + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@volar/language-service": "2.4.28", + "@volar/typescript": "2.4.28", + "path-browserify": "^1.0.1", + "request-light": "^0.7.0", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/language-service": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.28.tgz", + "integrity": "sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw==", + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vscode/emmet-helper": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.11.0.tgz", + "integrity": "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==", + "license": "MIT", + "dependencies": { + "emmet": "^2.4.3", + "jsonc-parser": "^2.3.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.15.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astro": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/astro/-/astro-5.18.2.tgz", + "integrity": "sha512-TnFwLnAXty5MXKPDGuKXqK4AMBXG+FH6RUdK7Oyc3gyfNoFIthT+4eRbzOK43bdRlLaZuxgciDSjgtggZ3OtGQ==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.13.0", + "@astrojs/internal-helpers": "0.7.6", + "@astrojs/markdown-remark": "6.3.11", + "@astrojs/telemetry": "3.3.0", + "@capsizecss/unpack": "^4.0.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "acorn": "^8.15.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "boxen": "8.0.1", + "ci-info": "^4.3.1", + "clsx": "^2.1.1", + "common-ancestor-path": "^1.0.1", + "cookie": "^1.1.1", + "cssesc": "^3.0.0", + "debug": "^4.4.3", + "deterministic-object-hash": "^2.0.2", + "devalue": "^5.6.2", + "diff": "^8.0.3", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "es-module-lexer": "^1.7.0", + "esbuild": "^0.27.3", + "estree-walker": "^3.0.3", + "flattie": "^1.1.1", + "fontace": "~0.4.0", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.1", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "p-limit": "^6.2.0", + "p-queue": "^8.1.1", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.3", + "prompts": "^2.4.2", + "rehype": "^13.0.2", + "semver": "^7.7.3", + "shiki": "^3.21.0", + "smol-toml": "^1.6.0", + "svgo": "^4.0.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tsconfck": "^3.1.6", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.3", + "unist-util-visit": "^5.0.0", + "unstorage": "^1.17.4", + "vfile": "^6.0.3", + "vite": "^6.4.1", + "vitefu": "^1.1.1", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^21.1.1", + "yocto-spinner": "^0.2.3", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1", + "zod-to-ts": "^1.2.0" + }, + "bin": { + "astro": "astro.js" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.34.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", + "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", + "license": "ISC" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/deterministic-object-hash": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deterministic-object-hash/-/deterministic-object-hash-2.0.2.tgz", + "integrity": "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==", + "license": "MIT", + "dependencies": { + "base-64": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/emmet": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.11.tgz", + "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==", + "license": "MIT", + "workspaces": [ + "./packages/scanner", + "./packages/abbreviation", + "./packages/css-abbreviation", + "./" + ], + "dependencies": { + "@emmetio/abbreviation": "^2.3.3", + "@emmetio/css-abbreviation": "^2.1.8" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", + "license": "MIT" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", + "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", + "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-github-blockquote-alert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/remark-github-blockquote-alert/-/remark-github-blockquote-alert-2.1.0.tgz", + "integrity": "sha512-J392jmIP684d7iGsENN0uguL10IGbRdc8bTUSrd/jOLzdWkwg721Fj3JPQGN8tF6fTIrE5HHOIA3nBuwuaeuPQ==", + "license": "MIT", + "dependencies": { + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/request-light": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz", + "integrity": "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", + "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/engine-javascript": "3.23.0", + "@shikijs/engine-oniguruma": "3.23.0", + "@shikijs/langs": "3.23.0", + "@shikijs/themes": "3.23.0", + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/svgo": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", + "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typesafe-path": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz", + "integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-auto-import-cache": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz", + "integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.8" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", + "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ofetch": "^1.5.1", + "ohash": "^2.0.11" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/unstorage/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/unstorage/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/volar-service-css": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.70.tgz", + "integrity": "sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw==", + "license": "MIT", + "dependencies": { + "vscode-css-languageservice": "^6.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-emmet": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.70.tgz", + "integrity": "sha512-xi5bC4m/VyE3zy/n2CXspKeDZs3qA41tHLTw275/7dNWM/RqE2z3BnDICQybHIVp/6G1iOQj5c1qXMgQC08TNg==", + "license": "MIT", + "dependencies": { + "@emmetio/css-parser": "^0.4.1", + "@emmetio/html-matcher": "^1.3.0", + "@vscode/emmet-helper": "^2.9.3", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-html": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.70.tgz", + "integrity": "sha512-eR6vCgMdmYAo4n+gcT7DSyBQbwB8S3HZZvSagTf0sxNaD4WppMCFfpqWnkrlGStPKMZvMiejRRVmqsX9dYcTvQ==", + "license": "MIT", + "dependencies": { + "vscode-html-languageservice": "^5.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-prettier": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.70.tgz", + "integrity": "sha512-Z6BCFSpGVCd8BPAsZ785Kce1BGlWd5ODqmqZGVuB14MJvrR4+CYz6cDy4F+igmE1gMifqfvMhdgT8Aud4M5ngg==", + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0", + "prettier": "^2.2 || ^3.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + }, + "prettier": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.70.tgz", + "integrity": "sha512-l46Bx4cokkUedTd74ojO5H/zqHZJ8SUuyZ0IB8JN4jfRqUM3bQFBHoOwlZCyZmOeO0A3RQNkMnFclxO4c++gsg==", + "license": "MIT", + "dependencies": { + "path-browserify": "^1.0.1", + "semver": "^7.6.2", + "typescript-auto-import-cache": "^0.3.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-nls": "^5.2.0", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript-twoslash-queries": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.70.tgz", + "integrity": "sha512-IdD13Z9N2Bu8EM6CM0fDV1E69olEYGHDU25X51YXmq8Y0CmJ2LNj6gOiBJgpS5JGUqFzECVhMNBW7R0sPdRTMQ==", + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-yaml": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-yaml/-/volar-service-yaml-0.0.70.tgz", + "integrity": "sha512-0c8bXDBeoATF9F6iPIlOuYTuZAC4c+yi0siQo920u7eiBJk8oQmUmg9cDUbR4+Gl++bvGP4plj3fErbJuPqdcQ==", + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8", + "yaml-language-server": "~1.20.0" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/vscode-css-languageservice": { + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.10.tgz", + "integrity": "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-css-languageservice/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-html-languageservice": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.2.tgz", + "integrity": "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-json-languageservice": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.1.8.tgz", + "integrity": "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==", + "license": "MIT", + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.2" + }, + "engines": { + "npm": ">=7.0.0" + } + }, + "node_modules/vscode-json-languageservice/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0.tgz", + "integrity": "sha512-+VvMmQPJhtvJ+8O+zu2JKIRiLxXF8NW7krWgyMGeOHrp4Cn23T5hc0v2LknNeopDOB70wghHAds7mKtcZ0I4Sg==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.0.tgz", + "integrity": "sha512-Zdz+kJ12Iz6tc11xfZyEo501bBATHXrCjmMfnaR3pMnf1CoqZBKIynba3P+/bi9VEdrMbNtAVKYpKhbODvqy+Q==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.0", + "vscode-languageserver-types": "3.18.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz", + "integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==", + "license": "MIT" + }, + "node_modules/vscode-languageserver/node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yaml-language-server": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/yaml-language-server/-/yaml-language-server-1.20.0.tgz", + "integrity": "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA==", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "prettier": "^3.5.0", + "request-light": "^0.5.7", + "vscode-json-languageservice": "4.1.8", + "vscode-languageserver": "^9.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-uri": "^3.0.2", + "yaml": "2.7.1" + }, + "bin": { + "yaml-language-server": "bin/yaml-language-server" + } + }, + "node_modules/yaml-language-server/node_modules/request-light": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.5.8.tgz", + "integrity": "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==", + "license": "MIT" + }, + "node_modules/yaml-language-server/node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-spinner": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-0.2.3.tgz", + "integrity": "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==", + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/zod-to-ts": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zod-to-ts/-/zod-to-ts-1.2.0.tgz", + "integrity": "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==", + "peerDependencies": { + "typescript": "^4.9.4 || ^5.0.2", + "zod": "^3" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000000..b5257617e8 --- /dev/null +++ b/site/package.json @@ -0,0 +1,14 @@ +{ + "scripts": { + "dev": "node scripts/sync_site_assets.mjs && astro dev --host", + "build": "node scripts/sync_site_assets.mjs && astro build", + "preview": "astro preview" + }, + "dependencies": { + "@astrojs/check": "^0.9.4", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "astro": "^5.0.0", + "remark-github-blockquote-alert": "^2.1.0", + "typescript": "^5.6.0" + } +} diff --git a/site/public/favicon.svg b/site/public/favicon.svg new file mode 100644 index 0000000000..ea4a583863 --- /dev/null +++ b/site/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/site/scripts/sync_site_assets.mjs b/site/scripts/sync_site_assets.mjs new file mode 100644 index 0000000000..43766b314b --- /dev/null +++ b/site/scripts/sync_site_assets.mjs @@ -0,0 +1,47 @@ +import { cp, mkdir, readdir, stat } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; + +const siteRoot = resolve(new URL('..', import.meta.url).pathname); +const repoRoot = resolve(siteRoot, '..'); + +async function copyIfPresent(from, to) { + try { + await stat(from); + } catch { + return; + } + await mkdir(dirname(to), { recursive: true }); + await cp(from, to, { recursive: true, force: true }); +} + +async function copyDirectoryWithoutMacMetadata(from, to) { + try { + const sourceStat = await stat(from); + if (!sourceStat.isDirectory()) { + return; + } + } catch { + return; + } + + await mkdir(to, { recursive: true }); + const entries = await readdir(from, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name === '.DS_Store') { + continue; + } + const source = join(from, entry.name); + const target = join(to, entry.name); + if (entry.isDirectory()) { + await copyDirectoryWithoutMacMetadata(source, target); + } else if (entry.isFile()) { + await mkdir(dirname(target), { recursive: true }); + await cp(source, target, { force: true }); + } + } +} + +await copyDirectoryWithoutMacMetadata(resolve(repoRoot, 'docs/images'), resolve(siteRoot, 'public/images')); +await copyIfPresent(resolve(repoRoot, 'web/assets/logo.png'), resolve(siteRoot, 'public/logo.png')); +await copyIfPresent(resolve(repoRoot, 'docs/catalog'), resolve(siteRoot, 'public/catalog')); +await copyIfPresent(resolve(repoRoot, 'docs/CNAME'), resolve(siteRoot, 'public/CNAME')); diff --git a/site/src/layouts/SiteLayout.astro b/site/src/layouts/SiteLayout.astro new file mode 100644 index 0000000000..74e13a569b --- /dev/null +++ b/site/src/layouts/SiteLayout.astro @@ -0,0 +1,139 @@ +--- +import '../styles/site.css'; +import '@fontsource-variable/jetbrains-mono'; +import { getDocs } from '../lib/docs'; + +type Props = { + title?: string; + description?: string; + pageClass?: string; +}; + +const docs = await getDocs(); +const { title = 'CrossInk', description = 'CrossInk firmware documentation', pageClass = '' } = Astro.props; +const pageTitle = title === 'CrossInk' ? title : `${title} | CrossInk`; +const currentDoc = docs.find((doc) => Astro.url.pathname === doc.url); +--- + + + + + + + + + + + + + {pageTitle} + + + + +
    + +
    +
    + + Docs + {currentDoc && {currentDoc.title}} + + +
    + +
    +
    + + + diff --git a/site/src/lib/docs.ts b/site/src/lib/docs.ts new file mode 100644 index 0000000000..46c440838b --- /dev/null +++ b/site/src/lib/docs.ts @@ -0,0 +1,92 @@ +type DocModule = { + frontmatter: { + title?: string; + nav_order?: number; + parent?: string; + has_children?: boolean; + }; + Content: any; +}; + +export type DocEntry = { + path: string; + slug: string; + url: string; + title: string; + navOrder: number; + parent?: string; + hasChildren: boolean; + load: () => Promise; +}; + +const modules = import.meta.glob("../../../docs/**/*.md"); + +// Slugs that are too technical/internal for the public docs site. These are +// dropped from both the nav and route generation, so the pages are not built. +const HIDDEN_SLUGS = new Set([ + "hyphenation-trie-format", + "activity-manager", + "i18n", + "translators", + "webserver-endpoints", + "simulator", + "dictionary-development", + "file-formats", +]); + +const visibleModules = Object.fromEntries( + Object.entries(modules).filter( + ([path]) => !path.includes("/docs/development/") && !HIDDEN_SLUGS.has(slugFromPath(path)), + ), +); + +function titleFromSlug(slug: string) { + const leaf = slug.split("/").pop() ?? slug; + return leaf.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()); +} + +function slugFromPath(path: string) { + let slug = path.replace("../../../docs/", "").replace(/\.md$/, ""); + if (slug.endsWith("/README")) { + slug = slug.replace(/\/README$/, "/index"); + } + if (slug === "index") { + slug = "docs"; + } + return slug; +} + +function urlFromSlug(slug: string) { + return `/${slug}.html`; +} + +export async function getDocs(): Promise { + const entries = await Promise.all( + Object.entries(visibleModules).map(async ([path, load]) => { + const mod = await load(); + const slug = slugFromPath(path); + return { + path, + slug, + url: urlFromSlug(slug), + title: mod.frontmatter.title ?? titleFromSlug(slug), + navOrder: mod.frontmatter.nav_order ?? 999, + parent: mod.frontmatter.parent, + hasChildren: mod.frontmatter.has_children ?? false, + load, + }; + }), + ); + + return entries.sort((a, b) => { + if (a.navOrder !== b.navOrder) { + return a.navOrder - b.navOrder; + } + return a.title.localeCompare(b.title); + }); +} + +export async function getDocBySlug(slug: string) { + const docs = await getDocs(); + return docs.find((doc) => doc.slug === slug); +} diff --git a/site/src/pages/[...slug].astro b/site/src/pages/[...slug].astro new file mode 100644 index 0000000000..4530451bd6 --- /dev/null +++ b/site/src/pages/[...slug].astro @@ -0,0 +1,26 @@ +--- +import SiteLayout from '../layouts/SiteLayout.astro'; +import { getDocBySlug, getDocs } from '../lib/docs'; + +export async function getStaticPaths() { + const docs = await getDocs(); + return docs.map((doc) => ({ + params: { slug: doc.slug }, + props: { doc } + })); +} + +const { doc } = Astro.props; +const loadedDoc = await getDocBySlug(doc.slug); +if (!loadedDoc) { + return Astro.redirect('/404.html'); +} +const module = await loadedDoc.load(); +const Content = module.Content; +--- + + +
    + +
    +
    diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro new file mode 100644 index 0000000000..43cfe36aed --- /dev/null +++ b/site/src/pages/index.astro @@ -0,0 +1,49 @@ +--- +import SiteLayout from '../layouts/SiteLayout.astro'; +import { getDocs } from '../lib/docs'; + +const docs = await getDocs(); +const featuredDocs = docs.filter((doc) => + ['user-guide', 'installation', 'reader-features', 'webserver'].includes(doc.slug) +); +--- + + +
    +
    +

    A personal fork of CrossPoint

    +

    CrossInk

    +

    + Minimal reading stats and improved UX. +

    + +
    + CrossInk on an X4 using 10pt Bitter font +
    + +
    +

    Start here

    +

    Useful docs

    +
    + +
    diff --git a/site/src/styles/site.css b/site/src/styles/site.css new file mode 100644 index 0000000000..f41c793815 --- /dev/null +++ b/site/src/styles/site.css @@ -0,0 +1,698 @@ +:root { + color-scheme: light; + --ink: #18221f; + --muted: #63706a; + --paper: #fbfaf6; + --panel: #ffffff; + --line: #dfe4de; + --accent: #4e4e4e; + --accent-strong: #313131; + --warm: #6d192d; + --code: #f2f4ef; + --text-soft: #314039; + --header-bg: rgba(251, 250, 246, 0.92); + --header-border: rgba(24, 34, 31, 0.12); + --hero-bg: + linear-gradient(135deg, rgba(255, 255, 255, 0.86), rgba(247, 244, 235, 0.62)), + radial-gradient(circle at 74% 18%, rgba(216, 111, 69, 0.16), transparent 34%); + --th-bg: #f5f6f1; + --active-bg: rgba(38, 107, 85, 0.08); + --btn-primary-fg: #ffffff; + --logo-filter: brightness(0); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; +} + +:root[data-theme="dark"] { + color-scheme: dark; + --ink: #e6eae7; + --muted: #93a09a; + --paper: #14181a; + --panel: #1c2123; + --line: #2c3337; + --accent: #d7dcd8; + --accent-strong: #f0f3f0; + --warm: #e07a8e; + --code: #23282b; + --text-soft: #c2ccc6; + --header-bg: rgba(20, 24, 26, 0.92); + --header-border: rgba(255, 255, 255, 0.1); + --hero-bg: + linear-gradient(135deg, rgba(28, 33, 35, 0.9), rgba(20, 24, 26, 0.6)), + radial-gradient(circle at 74% 18%, rgba(224, 122, 142, 0.12), transparent 36%); + --th-bg: #232a2d; + --active-bg: rgba(215, 220, 216, 0.1); + --btn-primary-fg: #15181a; + --logo-filter: none; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--paper); + color: var(--ink); + line-height: 1.6; +} + +a { + color: var(--accent-strong); + text-decoration-thickness: 0.08em; + text-underline-offset: 0.16em; +} + +img { + max-width: 100%; + height: auto; +} + +.site-header { + position: sticky; + top: 0; + z-index: 10; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 14px clamp(18px, 4vw, 56px); + border-bottom: 1px solid var(--header-border); + background: var(--header-bg); + backdrop-filter: blur(14px); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 12px; + color: var(--ink); + text-decoration: none; +} + +.brand-mark { + height: 38px; + width: auto; + flex-shrink: 0; + filter: var(--logo-filter); +} + +.brand strong, +.brand small { + display: block; +} + +.brand small { + color: var(--muted); + font-size: 0.78rem; + line-height: 1.2; +} + +.top-nav { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 8px 18px; +} + +.top-nav a { + color: var(--ink); + font-weight: 650; + text-decoration: none; +} + +.theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + padding: 0; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + color: var(--ink); + line-height: 0; + cursor: pointer; +} + +.theme-toggle:hover, +.theme-toggle:focus-visible { + border-color: var(--accent); +} + +.theme-toggle svg { + width: 18px; + height: 18px; +} + +.theme-toggle .icon-sun { + display: none; +} + +.theme-toggle .icon-moon { + display: block; +} + +:root[data-theme="dark"] .theme-toggle .icon-sun { + display: block; +} + +:root[data-theme="dark"] .theme-toggle .icon-moon { + display: none; +} + +.hero { + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(320px, 0.85fr); + align-items: center; + gap: clamp(28px, 5vw, 72px); + min-height: min(720px, calc(100vh - 67px)); + padding: clamp(42px, 7vw, 96px) clamp(18px, 5vw, 72px) clamp(34px, 5vw, 72px); + background: var(--hero-bg); +} + +.hero-copy { + max-width: 720px; + min-width: 0; +} + +.eyebrow { + margin: 0 0 10px; + color: var(--warm); + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.crossink-serif { + font-family: Georgia, "Times New Roman", Times, serif; +} + +h1, +h2, +h3 { + line-height: 1.12; +} + +h4 { + color: #828282; + font-weight: 500; +} + +h1 { + margin: 0; + font-size: clamp(4.8rem, 10vw, 10.5rem); + letter-spacing: 0; +} + +.hero-text { + max-width: 670px; + margin: 18px 0 0; + color: var(--text-soft); + font-size: clamp(1.12rem, 2.2vw, 1.42rem); +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 30px; +} + +.button { + display: inline-flex; + align-items: center; + min-height: 44px; + padding: 10px 16px; + border: 1px solid var(--accent); + border-radius: 6px; + color: var(--accent-strong); + font-weight: 800; + text-decoration: none; +} + +.button.primary { + background: var(--accent); + color: var(--btn-primary-fg); +} + +.hero-image { + width: min(100%, 512px); + justify-self: center; + border: 1px solid rgba(24, 34, 31, 0.16); + border-radius: 8px; + box-shadow: 0 24px 80px rgba(23, 75, 61, 0.18); +} + +@media (min-width: 901px) and (max-width: 1180px) { + .hero { + grid-template-columns: minmax(0, 1fr) minmax(300px, 0.78fr); + gap: clamp(30px, 4vw, 48px); + } + + .hero-copy h1 { + font-size: clamp(4.6rem, 8vw, 6.6rem); + } + + .hero-text { + max-width: 560px; + } + + .hero-image { + width: min(100%, 430px); + } +} + +.shell { + display: grid; + grid-template-columns: 280px minmax(0, 860px); + gap: clamp(24px, 4vw, 56px); + max-width: 1240px; + margin: 0 auto; + padding: 34px clamp(18px, 4vw, 56px) 72px; +} + +.home .shell { + display: block; + max-width: 1180px; +} + +.home .docs-sidebar { + display: none; +} + +.home .mobile-docs-nav { + display: none; +} + +.docs-sidebar { + position: sticky; + top: 88px; + align-self: start; + max-height: calc(100vh - 112px); + overflow: auto; + padding-right: 10px; +} + +.sidebar-title { + margin-bottom: 10px; + color: var(--muted); + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.sidebar-link { + display: block; + padding: 7px 10px; + border-left: 3px solid transparent; + color: var(--text-soft); + font-size: 0.94rem; + text-decoration: none; +} + +.sidebar-link.active { + border-left-color: var(--accent); + background: var(--active-bg); + color: var(--accent-strong); + font-weight: 800; +} + +.content { + min-width: 0; +} + +.mobile-docs-nav { + display: none; +} + +.mobile-docs-nav summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + min-height: 48px; + padding: 11px 14px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + color: var(--accent-strong); + cursor: pointer; + font-weight: 850; + list-style: none; +} + +.mobile-docs-nav summary::-webkit-details-marker { + display: none; +} + +.mobile-docs-nav summary::after { + content: ">"; + flex: 0 0 auto; + color: var(--warm); + font-weight: 900; + transform: rotate(90deg); + transition: transform 140ms ease; +} + +.mobile-docs-nav[open] summary::after { + transform: rotate(-90deg); +} + +.mobile-docs-nav summary strong { + min-width: 0; + overflow: hidden; + color: var(--muted); + font-size: 0.9rem; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mobile-docs-nav nav { + display: grid; + grid-template-columns: 1fr; + margin: 8px 0 18px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + overflow: hidden; +} + +.mobile-docs-link { + display: block; + padding: 11px 14px; + border-bottom: 1px solid var(--line); + color: var(--ink); + font-weight: 700; + text-decoration: none; +} + +.mobile-docs-link:last-child { + border-bottom: 0; +} + +.mobile-docs-link.active { + background: var(--active-bg); + color: var(--accent-strong); +} + +.doc-article { + padding: 28px clamp(18px, 4vw, 44px) 42px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); +} + +.doc-article h1 { + margin: 0 0 22px; + font-size: clamp(2rem, 5vw, 4rem); +} + +.doc-article h2 { + margin-top: 40px; + padding-top: 8px; + border-top: 1px solid var(--line); +} + +.doc-article h3 { + margin-top: 30px; +} + +.doc-article .markdown-alert { + --alert-color: var(--accent-strong); + --alert-background: var(--th-bg); + margin: 20px 0; + padding: 12px 16px; + border: 1px solid var(--line); + border-left: 4px solid var(--alert-color); + border-radius: 6px; + background: var(--alert-background); + color: var(--text-soft); +} + +.doc-article .markdown-alert > :last-child { + margin-bottom: 0; +} + +.doc-article .markdown-alert .markdown-alert-title { + display: flex; + align-items: center; + gap: 8px; + margin: 0 0 8px; + color: var(--alert-color); + font-size: 0.82rem; + font-weight: 850; + line-height: 1.2; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.doc-article .markdown-alert .markdown-alert-title svg { + width: 16px; + height: 16px; + flex: 0 0 auto; + fill: currentColor; +} + +.doc-article .markdown-alert-note { + --alert-color: #1d4ed8; + --alert-background: rgba(37, 99, 235, 0.06); +} + +.doc-article .markdown-alert-tip { + --alert-color: #15803d; + --alert-background: rgba(22, 163, 74, 0.06); +} + +.doc-article .markdown-alert-important { + --alert-color: #7c3aed; + --alert-background: rgba(124, 58, 237, 0.06); +} + +.doc-article .markdown-alert-warning { + --alert-color: #a16207; + --alert-background: rgba(202, 138, 4, 0.08); +} + +.doc-article .markdown-alert-caution { + --alert-color: #b91c1c; + --alert-background: rgba(220, 38, 38, 0.06); +} + +:root[data-theme="dark"] .doc-article .markdown-alert-note { + --alert-color: #60a5fa; + --alert-background: rgba(96, 165, 250, 0.1); +} + +:root[data-theme="dark"] .doc-article .markdown-alert-tip { + --alert-color: #4ade80; + --alert-background: rgba(74, 222, 128, 0.1); +} + +:root[data-theme="dark"] .doc-article .markdown-alert-important { + --alert-color: #c084fc; + --alert-background: rgba(192, 132, 252, 0.1); +} + +:root[data-theme="dark"] .doc-article .markdown-alert-warning { + --alert-color: #facc15; + --alert-background: rgba(250, 204, 21, 0.1); +} + +:root[data-theme="dark"] .doc-article .markdown-alert-caution { + --alert-color: #fb7185; + --alert-background: rgba(251, 113, 133, 0.1); +} + +.doc-article table { + display: block; + width: 100%; + overflow-x: auto; + border-collapse: collapse; +} + +.doc-article th, +.doc-article td { + padding: 10px 12px; + border: 1px solid var(--line); + text-align: left; + vertical-align: top; +} + +.doc-article th { + background: var(--th-bg); +} + +pre, +code { + font-size: 0.9rem; + border-radius: 2px; + /* background: var(--code); */ + font-family: "JetBrains Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace; +} + +code { + padding: 0.12em 0.28em; +} + +pre { + overflow-x: auto; + padding: 16px; +} + +pre code { + padding: 0.12em 0.28em; +} + +.intro-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +.intro-grid { + margin: 12px 0 54px; +} + +.intro-grid div { + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); +} + +.intro-grid div { + padding: 20px; +} + +.intro-grid span { + display: block; + color: var(--warm); + font-size: 0.78rem; + font-weight: 850; + text-transform: uppercase; +} + +.intro-grid strong { + display: block; + margin-top: 8px; + font-size: 1.04rem; + line-height: 1.35; +} + +.section-heading { + margin: 0 0 18px; +} + +.section-heading h2 { + margin: 0; + font-size: clamp(2rem, 4vw, 3rem); +} + +.useful-docs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0; + border-top: 1px solid var(--line); +} + +.useful-docs a { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 60px; + padding: 16px 0; + border-bottom: 1px solid var(--line); + color: var(--ink); + font-size: 1.08rem; + font-weight: 800; + text-decoration: none; +} + +.useful-docs a:nth-child(odd) { + margin-right: 24px; +} + +.useful-docs a:nth-child(even) { + margin-left: 24px; +} + +.useful-docs a::after { + content: ">"; + color: var(--warm); + font-weight: 900; +} + +.useful-docs a:hover, +.useful-docs a:focus-visible { + color: var(--accent-strong); +} + +@media (max-width: 900px) { + .hero, + .shell, + .intro-grid, + .useful-docs { + grid-template-columns: 1fr; + } + + .useful-docs a:nth-child(odd), + .useful-docs a:nth-child(even) { + margin-right: 0; + margin-left: 0; + } + + .hero { + min-height: auto; + } + + .docs-sidebar { + display: none; + } + + .mobile-docs-nav { + display: block; + margin-bottom: 16px; + position: sticky; + top: 74px; + z-index: 8; + } +} + +@media (max-width: 620px) { + .site-header { + align-items: flex-start; + flex-direction: column; + } + + .top-nav { + justify-content: flex-start; + } + + .mobile-docs-nav { + top: 124px; + } + + .hero-actions { + flex-direction: column; + } + + .button { + justify-content: center; + width: 100%; + } + + .doc-article { + padding: 22px 16px 34px; + } +} diff --git a/site/tsconfig.json b/site/tsconfig.json new file mode 100644 index 0000000000..9a9bbea106 --- /dev/null +++ b/site/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "src/**/*", "astro.config.mjs"] +} diff --git a/src/BookmarkStore.cpp b/src/BookmarkStore.cpp index e970dc9176..1a7f75ac37 100644 --- a/src/BookmarkStore.cpp +++ b/src/BookmarkStore.cpp @@ -257,7 +257,6 @@ bool BookmarkStore::loadForBook(const std::string& filePath, const std::string& } } - LOG_DBG("BKS", "No bookmark file for this book"); return true; } @@ -406,7 +405,6 @@ void BookmarkStore::clearAll() { LOG_ERR("BKS", "Failed to delete bookmark file"); return; } - LOG_DBG("BKS", "Bookmark file deleted"); } bookmarks.clear(); dirty = false; @@ -423,7 +421,6 @@ bool BookmarkStore::readFromFile() { saveToFile(); LOG_DBG("BKS", "Migrated bookmark file to version %u", VERSION); } - LOG_DBG("BKS", "Loaded %u bookmark(s)", static_cast(bookmarks.size())); return true; } @@ -552,7 +549,6 @@ bool BookmarkStore::writeToFile() const { } f.close(); - LOG_DBG("BKS", "Saved %u bookmark(s)", count); return true; } @@ -569,7 +565,6 @@ void BookmarkStore::deleteForFilePath(const std::string& filePath, const std::st } if (deletedAny) { - LOG_DBG("BKS", "Deleted bookmark file for: %s", filePath.c_str()); } } diff --git a/src/ClippingStore.cpp b/src/ClippingStore.cpp index e5f15e1037..d93e659c46 100644 --- a/src/ClippingStore.cpp +++ b/src/ClippingStore.cpp @@ -7,13 +7,17 @@ #include #include +#include #include #include namespace { -constexpr uint8_t VERSION = 1; +constexpr uint8_t LEGACY_VERSION = 1; +constexpr uint8_t TEXT_OFFSET_VERSION = 2; +constexpr uint8_t VERSION = 3; constexpr size_t INITIAL_CLIPPING_RESERVE = 4; constexpr char CLIPPINGS_DIR[] = "/.crosspoint/clippings"; +constexpr size_t TEXT_COPY_BUFFER_SIZE = 128; struct ClippingFileHeader { std::string title; @@ -42,14 +46,18 @@ bool readClippingFileHeader(const std::string& fullPath, const char* name, Clipp uint8_t version = 0; uint16_t count = 0; - if (!serialization::tryReadPod(f, version) || version != VERSION || !serialization::tryReadPod(f, count) || - !serialization::tryReadString(f, header.title) || !serialization::tryReadString(f, header.author) || - !serialization::tryReadString(f, header.path)) { + if (!serialization::tryReadPod(f, version) || + (version != LEGACY_VERSION && version != TEXT_OFFSET_VERSION && version != VERSION) || + !serialization::tryReadPod(f, count) || !serialization::tryReadString(f, header.title) || + !serialization::tryReadString(f, header.author) || !serialization::tryReadString(f, header.path)) { f.close(); return false; } f.close(); + if (count > CLIPPING_MAX_PER_BOOK) { + return false; + } header.count = count; header.bookType = "epub"; const std::string nameStr = name ? name : ""; @@ -59,6 +67,21 @@ bool readClippingFileHeader(const std::string& fullPath, const char* name, Clipp } return true; } + +bool copyBytes(FsFile& in, FsFile& out, uint16_t length) { + std::array buffer{}; + while (length > 0) { + const size_t chunk = std::min(length, buffer.size()); + if (in.read(buffer.data(), chunk) != static_cast(chunk)) { + return false; + } + if (out.write(buffer.data(), chunk) != chunk) { + return false; + } + length = static_cast(length - chunk); + } + return true; +} } // namespace ClippingStore ClippingStore::instance; @@ -101,7 +124,8 @@ ClippingStore::AddResult ClippingStore::addClipping(const uint16_t spineIndex, c const uint16_t endPage, const uint16_t pageCount, const uint16_t startWordIndex, const uint16_t endWordIndex, const uint16_t wordCount, const char* chapterTitle, - const uint16_t paragraphIndex, const std::string& text) { + const uint16_t paragraphIndex, const std::string& text, + const uint32_t layoutSignature) { if (clippings.size() >= CLIPPING_MAX_PER_BOOK) { LOG_ERR("CLIP", "Clipping limit (%u) reached", CLIPPING_MAX_PER_BOOK); return AddResult::LimitReached; @@ -117,20 +141,41 @@ ClippingStore::AddResult ClippingStore::addClipping(const uint16_t spineIndex, c clipping.wordCount = wordCount; clipping.paragraphIndex = paragraphIndex; clipping.timestamp = static_cast(millis() / 1000UL); + clipping.layoutSignature = layoutSignature; copyBounded(clipping.chapterTitle, sizeof(clipping.chapterTitle), chapterTitle); - // Keep the in-app store bounded. The full Kindle-style export is still written separately. - clipping.text.assign(text.data(), std::min(text.size(), CLIPPING_TEXT_MAX)); + clipping.textLength = static_cast(std::min(text.size(), CLIPPING_TEXT_MAX)); clippings.push_back(std::move(clipping)); dirty = true; - if (!saveToFile()) { + if (!writeToFile(&text, clippings.size() - 1)) { clippings.pop_back(); dirty = true; return AddResult::SaveFailed; } + dirty = false; return AddResult::Added; } +bool ClippingStore::stampMissingLayoutSignature(const uint32_t layoutSignature) { + if (layoutSignature == 0) return true; + + bool changed = false; + for (Clipping& clipping : clippings) { + if (clipping.layoutSignature == 0) { + clipping.layoutSignature = layoutSignature; + changed = true; + } + } + if (!changed) return true; + + dirty = true; + if (writeToFile()) { + dirty = false; + return true; + } + return false; +} + bool ClippingStore::removeClippingAt(const size_t index) { if (index >= clippings.size()) return false; Clipping clipping = std::move(clippings[index]); @@ -150,6 +195,42 @@ bool ClippingStore::hasClippingForPage(const uint16_t spineIndex, const uint16_t }); } +const Clipping* ClippingStore::clippingAt(const size_t index) const { + if (index >= clippings.size()) return nullptr; + return &clippings[index]; +} + +bool ClippingStore::readClippingText(const size_t index, std::string& out) const { + const Clipping* clipping = clippingAt(index); + if (!clipping) return false; + return readClippingText(*clipping, out); +} + +bool ClippingStore::readClippingText(const Clipping& clipping, std::string& out) const { + out.clear(); + if (clipping.textLength == 0) return true; + if (storeFilePath.empty()) return false; + + FsFile f; + if (!Storage.openFileForRead("CLIP", storeFilePath, f)) { + return false; + } + if (!f.seek(clipping.textOffset)) { + f.close(); + LOG_ERR("CLIP", "Failed to seek clipping text at %u: %s", clipping.textOffset, storeFilePath.c_str()); + return false; + } + out.resize(clipping.textLength); + const int expected = static_cast(clipping.textLength); + const bool ok = f.read(&out[0], clipping.textLength) == expected; + f.close(); + if (!ok) { + out.clear(); + LOG_ERR("CLIP", "Failed to read clipping text at %u: %s", clipping.textOffset, storeFilePath.c_str()); + } + return ok; +} + bool ClippingStore::saveToFile() { if (!dirty) return true; if (writeToFile()) { @@ -181,9 +262,10 @@ bool ClippingStore::readFromFile(const std::string& path, std::vector& std::string title; std::string author; std::string storedPath; - if (!serialization::tryReadPod(f, version) || version != VERSION || !serialization::tryReadPod(f, count) || - !serialization::tryReadString(f, title) || !serialization::tryReadString(f, author) || - !serialization::tryReadString(f, storedPath)) { + if (!serialization::tryReadPod(f, version) || + (version != LEGACY_VERSION && version != TEXT_OFFSET_VERSION && version != VERSION) || + !serialization::tryReadPod(f, count) || !serialization::tryReadString(f, title) || + !serialization::tryReadString(f, author) || !serialization::tryReadString(f, storedPath)) { f.close(); LOG_ERR("CLIP", "Failed to read clipping header: %s", path.c_str()); return false; @@ -207,6 +289,11 @@ bool ClippingStore::readFromFile(const std::string& path, std::vector& LOG_ERR("CLIP", "Clipping file truncated at record %u: %s", i, path.c_str()); return false; } + if (version >= VERSION && !serialization::tryReadPod(f, clipping.layoutSignature)) { + f.close(); + LOG_ERR("CLIP", "Clipping file truncated at layout signature, record %u: %s", i, path.c_str()); + return false; + } if (f.read(reinterpret_cast(clipping.chapterTitle), sizeof(clipping.chapterTitle)) != sizeof(clipping.chapterTitle)) { f.close(); @@ -214,13 +301,37 @@ bool ClippingStore::readFromFile(const std::string& path, std::vector& return false; } clipping.chapterTitle[sizeof(clipping.chapterTitle) - 1] = '\0'; - if (!serialization::tryReadString(f, clipping.text)) { - f.close(); - LOG_ERR("CLIP", "Clipping file truncated at text, record %u: %s", i, path.c_str()); - return false; - } - if (clipping.text.size() > CLIPPING_TEXT_MAX) { - clipping.text.resize(CLIPPING_TEXT_MAX); + if (version == LEGACY_VERSION) { + uint32_t textLen = 0; + if (!serialization::tryReadPod(f, textLen)) { + f.close(); + LOG_ERR("CLIP", "Clipping file truncated at text length, record %u: %s", i, path.c_str()); + return false; + } + clipping.textOffset = static_cast(f.position()); + clipping.textLength = static_cast(std::min(textLen, CLIPPING_TEXT_MAX)); + if (textLen > 0 && !f.seekCur(textLen)) { + f.close(); + LOG_ERR("CLIP", "Clipping file truncated at text, record %u: %s", i, path.c_str()); + return false; + } + } else { + if (!serialization::tryReadPod(f, clipping.textLength)) { + f.close(); + LOG_ERR("CLIP", "Clipping file truncated at text length, record %u: %s", i, path.c_str()); + return false; + } + if (clipping.textLength > CLIPPING_TEXT_MAX) { + f.close(); + LOG_ERR("CLIP", "Clipping text length %u exceeds max, record %u: %s", clipping.textLength, i, path.c_str()); + return false; + } + clipping.textOffset = static_cast(f.position()); + if (clipping.textLength > 0 && !f.seekCur(clipping.textLength)) { + f.close(); + LOG_ERR("CLIP", "Clipping file truncated at text, record %u: %s", i, path.c_str()); + return false; + } } out.push_back(std::move(clipping)); } @@ -229,22 +340,48 @@ bool ClippingStore::readFromFile(const std::string& path, std::vector& return true; } -bool ClippingStore::writeToFile() const { +bool ClippingStore::writeToFile(const std::string* replacementText, const size_t replacementIndex) { Storage.mkdir("/.crosspoint"); Storage.mkdir(CLIPPINGS_DIR); - FsFile f = Storage.open(storeFilePath.c_str(), O_WRONLY | O_CREAT | O_TRUNC); + const std::string tmpPath = storeFilePath + ".tmp"; + const std::string backupPath = storeFilePath + ".bak"; + if (!Storage.exists(storeFilePath.c_str()) && Storage.exists(backupPath.c_str())) { + if (!Storage.rename(backupPath.c_str(), storeFilePath.c_str())) { + LOG_ERR("CLIP", "Failed to recover clipping backup: %s", backupPath.c_str()); + return false; + } + LOG_INF("CLIP", "Recovered clipping backup: %s", storeFilePath.c_str()); + } + if (Storage.exists(tmpPath.c_str())) Storage.remove(tmpPath.c_str()); + if (Storage.exists(backupPath.c_str()) && Storage.exists(storeFilePath.c_str())) Storage.remove(backupPath.c_str()); + + FsFile source; + const bool hasSource = Storage.exists(storeFilePath.c_str()); + if (hasSource && !Storage.openFileForRead("CLIP", storeFilePath, source)) { + LOG_ERR("CLIP", "Failed to open clipping source for rewrite: %s", storeFilePath.c_str()); + return false; + } + + FsFile f = Storage.open(tmpPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC); if (!f) { - LOG_ERR("CLIP", "Failed to open clipping file for write: %s", storeFilePath.c_str()); + if (source) source.close(); + LOG_ERR("CLIP", "Failed to open clipping temp file for write: %s", tmpPath.c_str()); return false; } const uint16_t count = static_cast(std::min(clippings.size(), CLIPPING_MAX_PER_BOOK)); + std::vector newTextOffsets; + newTextOffsets.reserve(count); + std::vector newTextLengths; + newTextLengths.reserve(count); if (!serialization::tryWritePod(f, VERSION) || !serialization::tryWritePod(f, count) || !serialization::tryWriteString(f, bookTitle) || !serialization::tryWriteString(f, bookAuthor) || !serialization::tryWriteString(f, bookFilePath)) { - LOG_ERR("CLIP", "Failed to write clipping header: %s", storeFilePath.c_str()); + LOG_ERR("CLIP", "Failed to write clipping header: %s", tmpPath.c_str()); f.close(); + if (source) source.close(); + Storage.remove(tmpPath.c_str()); return false; } @@ -255,21 +392,74 @@ bool ClippingStore::writeToFile() const { !serialization::tryWritePod(f, clipping.startWordIndex) || !serialization::tryWritePod(f, clipping.endWordIndex) || !serialization::tryWritePod(f, clipping.wordCount) || !serialization::tryWritePod(f, clipping.paragraphIndex) || !serialization::tryWritePod(f, clipping.timestamp) || + !serialization::tryWritePod(f, clipping.layoutSignature) || f.write(reinterpret_cast(clipping.chapterTitle), sizeof(clipping.chapterTitle)) != - sizeof(clipping.chapterTitle) || - !serialization::tryWriteString(f, clipping.text)) { + sizeof(clipping.chapterTitle)) { LOG_ERR("CLIP", "Failed to write clipping record %u: %s", i, storeFilePath.c_str()); f.close(); + if (source) source.close(); + Storage.remove(tmpPath.c_str()); + return false; + } + + const bool useReplacement = replacementText && i == replacementIndex; + const uint16_t textLen = useReplacement + ? static_cast(std::min(replacementText->size(), CLIPPING_TEXT_MAX)) + : clipping.textLength; + if (!serialization::tryWritePod(f, textLen)) { + LOG_ERR("CLIP", "Failed to write clipping text length %u: %s", i, tmpPath.c_str()); + f.close(); + if (source) source.close(); + Storage.remove(tmpPath.c_str()); + return false; + } + + const uint32_t newTextOffset = static_cast(f.position()); + bool wroteText = true; + if (textLen > 0 && useReplacement) { + wroteText = f.write(reinterpret_cast(replacementText->data()), textLen) == textLen; + } else if (textLen > 0) { + wroteText = source && source.seek(clipping.textOffset) && copyBytes(source, f, textLen); + } + if (!wroteText) { + LOG_ERR("CLIP", "Failed to write clipping text %u: %s", i, tmpPath.c_str()); + f.close(); + if (source) source.close(); + Storage.remove(tmpPath.c_str()); return false; } + newTextOffsets.push_back(newTextOffset); + newTextLengths.push_back(textLen); } if (!f.sync()) { - LOG_ERR("CLIP", "Failed to sync clipping file: %s", storeFilePath.c_str()); + LOG_ERR("CLIP", "Failed to sync clipping file: %s", tmpPath.c_str()); f.close(); + if (source) source.close(); + Storage.remove(tmpPath.c_str()); return false; } f.close(); + if (source) source.close(); + + if (hasSource && !Storage.rename(storeFilePath.c_str(), backupPath.c_str())) { + LOG_ERR("CLIP", "Failed to back up clipping file: %s", storeFilePath.c_str()); + Storage.remove(tmpPath.c_str()); + return false; + } + if (!Storage.rename(tmpPath.c_str(), storeFilePath.c_str())) { + LOG_ERR("CLIP", "Failed to replace clipping file: %s", storeFilePath.c_str()); + Storage.remove(tmpPath.c_str()); + if (hasSource) Storage.rename(backupPath.c_str(), storeFilePath.c_str()); + return false; + } + if (hasSource && Storage.exists(backupPath.c_str())) { + Storage.remove(backupPath.c_str()); + } + for (uint16_t i = 0; i < count; ++i) { + clippings[i].textOffset = newTextOffsets[i]; + clippings[i].textLength = newTextLengths[i]; + } return true; } @@ -326,12 +516,38 @@ bool ClippingStore::migrateForFilePath(const std::string& oldFilePath, const std writer.bookFilePath = newFilePath; writer.bookTitle = title; writer.bookAuthor = author; - writer.storeFilePath = storeFilePathForBook(newFilePath, bookType); + writer.storeFilePath = oldStorePath; writer.clippings = std::move(migratedClippings); if (!writer.writeToFile()) { return false; } - Storage.remove(oldStorePath.c_str()); + const std::string newStorePath = storeFilePathForBook(newFilePath, bookType); + if (oldStorePath == newStorePath) { + return true; + } + + const std::string backupPath = newStorePath + ".bak"; + const bool hasDestination = Storage.exists(newStorePath.c_str()); + if (hasDestination) { + if (Storage.exists(backupPath.c_str()) && !Storage.remove(backupPath.c_str())) { + LOG_ERR("CLIP", "Failed to remove stale clipping migration backup: %s", backupPath.c_str()); + return false; + } + if (!Storage.rename(newStorePath.c_str(), backupPath.c_str())) { + LOG_ERR("CLIP", "Failed to back up destination clippings: %s", newStorePath.c_str()); + return false; + } + } + if (!Storage.rename(oldStorePath.c_str(), newStorePath.c_str())) { + LOG_ERR("CLIP", "Failed to rename migrated clippings: %s -> %s", oldStorePath.c_str(), newStorePath.c_str()); + if (hasDestination && !Storage.rename(backupPath.c_str(), newStorePath.c_str())) { + LOG_ERR("CLIP", "Failed to restore destination clipping backup: %s", backupPath.c_str()); + } + return false; + } + if (hasDestination && Storage.exists(backupPath.c_str())) { + Storage.remove(backupPath.c_str()); + } return true; } diff --git a/src/ClippingStore.h b/src/ClippingStore.h index 78c6b1c2d9..0b4c410c0b 100644 --- a/src/ClippingStore.h +++ b/src/ClippingStore.h @@ -1,12 +1,14 @@ #pragma once +#include #include #include #include inline constexpr size_t CLIPPING_CHAPTER_TITLE_MAX = 48; inline constexpr size_t CLIPPING_TEXT_MAX = 512; -inline constexpr uint16_t CLIPPING_MAX_PER_BOOK = 64; +inline constexpr uint16_t CLIPPING_MAX_PER_BOOK = 256; +inline constexpr uint16_t CLIPPING_MAX_PAGE_MATCHES = 16; struct Clipping { uint16_t spineIndex = 0; @@ -18,8 +20,10 @@ struct Clipping { uint16_t wordCount = 0; uint16_t paragraphIndex = UINT16_MAX; uint32_t timestamp = 0; + uint32_t layoutSignature = 0; + uint32_t textOffset = 0; + uint16_t textLength = 0; char chapterTitle[CLIPPING_CHAPTER_TITLE_MAX] = {}; - std::string text; }; struct ClippedBookEntry { @@ -46,14 +50,19 @@ class ClippingStore { AddResult addClipping(uint16_t spineIndex, uint16_t startPage, uint16_t endPage, uint16_t pageCount, uint16_t startWordIndex, uint16_t endWordIndex, uint16_t wordCount, const char* chapterTitle, - uint16_t paragraphIndex, const std::string& text); + uint16_t paragraphIndex, const std::string& text, uint32_t layoutSignature); + bool stampMissingLayoutSignature(uint32_t layoutSignature); bool removeClippingAt(size_t index); bool saveToFile(); void clearAll(); bool hasClippings() const { return !clippings.empty(); } bool hasClippingForPage(uint16_t spineIndex, uint16_t page) const; + size_t clippingCount() const { return clippings.size(); } + const Clipping* clippingAt(size_t index) const; const std::vector& getClippings() const { return clippings; } + bool readClippingText(size_t index, std::string& out) const; + bool readClippingText(const Clipping& clipping, std::string& out) const; static bool hasAnyClippings(); static bool getAllClippedBooks(std::vector& out); @@ -73,7 +82,16 @@ class ClippingStore { bool readFromFile(); bool readFromFile(const std::string& path, std::vector& out) const; - bool writeToFile() const; + bool writeToFile(const std::string* replacementText = nullptr, size_t replacementIndex = SIZE_MAX); }; +inline bool clippingStoredRangeMatchesLayout(const Clipping& clipping, const uint16_t currentPageCount, + const uint32_t currentLayoutSignature) { + if (clipping.pageCount != currentPageCount) return false; + // Versions 1-2 predate layout signatures. Preserve their fast path until a + // reader relayout stamps the layout they were displayed with. + return clipping.layoutSignature == 0 || currentLayoutSignature == 0 || + clipping.layoutSignature == currentLayoutSignature; +} + #define CLIPPINGS ClippingStore::getInstance() diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 6002a8f5fb..05ac88d699 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -1,9 +1,13 @@ #include "CrossPointSettings.h" +#include +#include #include #include -#include +#include #include +#include +#include #include #include @@ -13,11 +17,9 @@ #include #include "I18nKeys.h" +#include "SettingsList.h" #include "fontIds.h" -// Initialize the static instance -CrossPointSettings CrossPointSettings::instance; - void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) { uint8_t tempValue; serialization::readPod(file, tempValue); @@ -35,6 +37,10 @@ constexpr char SETTINGS_FILE_BAK[] = "/.crosspoint/settings.bin.bak"; constexpr char LANG_FILE_BIN[] = "/.crosspoint/language.bin"; constexpr char LANG_FILE_BAK[] = "/.crosspoint/language.bin.bak"; constexpr uint8_t INVALID_READER_FONT_SIZE = 0xFF; +constexpr uint8_t TILT_DIRECTION_SCHEMA_CURRENT = 2; +// X3 hardware predates this migration by less than a year. Reject the RTC's +// factory/default year while preserving dates written by released firmware. +constexpr uint16_t MIN_TRUSTED_MIGRATED_RTC_YEAR = 2025; constexpr uint8_t SLEEP_SCREEN_STORAGE_ORDER[] = { static_cast(CrossPointSettings::DARK), static_cast(CrossPointSettings::LIGHT), @@ -54,22 +60,26 @@ constexpr uint8_t SLEEP_SCREEN_STORAGE_ORDER_COUNT = static_assert(SLEEP_SCREEN_STORAGE_ORDER_COUNT == CrossPointSettings::SLEEP_SCREEN_MODE_COUNT, "Update sleep screen persisted-value mapping when adding modes"); constexpr CrossPointSettings::FONT_SIZE READER_FONT_SIZE_STORAGE_ORDER[] = { - CrossPointSettings::TINY, CrossPointSettings::SMALL, CrossPointSettings::MEDIUM, - CrossPointSettings::LARGE, CrossPointSettings::EXTRA_LARGE, CrossPointSettings::TEENSY, - CrossPointSettings::HUGE_SIZE, CrossPointSettings::ITTY_BITTY}; + CrossPointSettings::TINY, + CrossPointSettings::SMALL, + CrossPointSettings::MEDIUM, + CrossPointSettings::LARGE, +}; constexpr CrossPointSettings::FONT_SIZE READER_FONT_SIZE_CYCLE_ORDER[] = { - CrossPointSettings::TEENSY, CrossPointSettings::ITTY_BITTY, CrossPointSettings::TINY, - CrossPointSettings::SMALL, CrossPointSettings::MEDIUM, CrossPointSettings::LARGE, - CrossPointSettings::EXTRA_LARGE, CrossPointSettings::HUGE_SIZE}; + CrossPointSettings::TINY, + CrossPointSettings::SMALL, + CrossPointSettings::MEDIUM, + CrossPointSettings::LARGE, +}; constexpr uint8_t SD_FONT_RANGE_POINT_SIZES[CrossPointSettings::SD_FONT_SIZE_RANGE_COUNT] [CrossPointSettings::SD_FONT_MAX_SIZE_STEPS] = { {8, 9, 10, 12}, {10, 12, 14, 16}, - {16, 18, 20}, - {10, 12, 14, 16, 18}, + {14, 16, 18, 20}, + {8, 9, 10, 12, 14, 16, 18, 20}, {8, 9, 10, 12, 14, 16, 18, 20}, }; -constexpr uint8_t SD_FONT_RANGE_STEP_COUNTS[CrossPointSettings::SD_FONT_SIZE_RANGE_COUNT] = {4, 4, 3, 5, 8}; +constexpr uint8_t SD_FONT_RANGE_STEP_COUNTS[CrossPointSettings::SD_FONT_SIZE_RANGE_COUNT] = {4, 4, 4, 8, 8}; bool isValidDeviceName(const char* name) { if (!name) return false; @@ -77,115 +87,47 @@ bool isValidDeviceName(const char* name) { return len >= CrossPointSettings::MIN_DEVICE_NAME_LENGTH && len <= CrossPointSettings::MAX_DEVICE_NAME_LENGTH; } +bool restoreLegacyRtcDateSyncState(CrossPointSettings& settings) { + if (!settings.clockHasBeenSynced || settings.clockDateHasBeenSynced || !halClock.isAvailable()) return false; + + uint16_t year = 0; + uint8_t month = 0; + uint8_t day = 0; + uint8_t hour = 0; + uint8_t minute = 0; + if (!halClock.getDateTime(year, month, day, hour, minute) || year < MIN_TRUSTED_MIGRATED_RTC_YEAR) return false; + + settings.clockDateHasBeenSynced = 1; + LOG_INF("CPS", "Restored RTC date sync state from valid persisted date: %04u-%02u-%02u", static_cast(year), + static_cast(month), static_cast(day)); + return true; +} + uint8_t normalizedSdFontRange(uint8_t range) { + if (range == CrossPointSettings::SD_FONT_RANGE_NO_EMOJI_LEGACY) { + return CrossPointSettings::SD_FONT_RANGE_ALL; + } return range < CrossPointSettings::SD_FONT_SIZE_RANGE_COUNT ? range : CrossPointSettings::SD_FONT_RANGE_TINY; } bool isReaderFontSizeAvailable(const CrossPointSettings::FONT_SIZE size) { - switch (size) { - case CrossPointSettings::TEENSY: -#ifdef OMIT_TEENSY_FONT - return false; -#else - return true; -#endif - case CrossPointSettings::ITTY_BITTY: -#ifdef OMIT_ITTY_BITTY_FONT - return false; -#else - return true; -#endif - case CrossPointSettings::TINY: -#ifdef OMIT_TINY_FONT - return false; -#else - return true; -#endif - case CrossPointSettings::SMALL: -#ifdef OMIT_SMALL_FONT - return false; -#else - return true; -#endif - case CrossPointSettings::MEDIUM: -#ifdef OMIT_MEDIUM_FONT - return false; -#else - return true; -#endif - case CrossPointSettings::EXTRA_LARGE: -#ifdef OMIT_XLARGE_FONT - return false; -#else - return true; -#endif - case CrossPointSettings::LARGE: -#ifdef OMIT_LARGE_FONT - return false; -#else - return true; -#endif - case CrossPointSettings::HUGE_SIZE: -#ifdef OMIT_HUGE_FONT - return false; -#else - return true; -#endif - default: - return true; - } + return size < CrossPointSettings::FONT_SIZE_COUNT; } CrossPointSettings::FONT_SIZE firstAvailableReaderFontSize() { const auto it = std::find_if(std::begin(READER_FONT_SIZE_STORAGE_ORDER), std::end(READER_FONT_SIZE_STORAGE_ORDER), [](const CrossPointSettings::FONT_SIZE size) { return isReaderFontSizeAvailable(size); }); - return (it != std::end(READER_FONT_SIZE_STORAGE_ORDER)) ? *it : CrossPointSettings::LARGE; + return (it != std::end(READER_FONT_SIZE_STORAGE_ORDER)) ? *it : CrossPointSettings::TINY; } int getFallbackReaderFontIdForFamily(const CrossPointSettings::FONT_FAMILY family) { switch (family) { case CrossPointSettings::BITTER: -#ifndef OMIT_TINY_FONT return BITTER_10_FONT_ID; -#elif !defined(OMIT_SMALL_FONT) - return BITTER_12_FONT_ID; -#elif !defined(OMIT_MEDIUM_FONT) - return BITTER_14_FONT_ID; -#elif !defined(OMIT_LARGE_FONT) - return BITTER_16_FONT_ID; -#elif !defined(OMIT_XLARGE_FONT) - return BITTER_18_FONT_ID; -#elif !defined(OMIT_HUGE_FONT) - return BITTER_20_FONT_ID; -#elif !defined(OMIT_TEENSY_FONT) - return BITTER_8_FONT_ID; -#elif !defined(OMIT_ITTY_BITTY_FONT) - return BITTER_9_FONT_ID; -#else -#error "No reader fonts enabled for BITTER" -#endif case CrossPointSettings::LEXENDDECA: default: -#ifndef OMIT_TINY_FONT return LEXENDDECA_10_FONT_ID; -#elif !defined(OMIT_SMALL_FONT) - return LEXENDDECA_12_FONT_ID; -#elif !defined(OMIT_MEDIUM_FONT) - return LEXENDDECA_14_FONT_ID; -#elif !defined(OMIT_LARGE_FONT) - return LEXENDDECA_16_FONT_ID; -#elif !defined(OMIT_XLARGE_FONT) - return LEXENDDECA_18_FONT_ID; -#elif !defined(OMIT_HUGE_FONT) - return LEXENDDECA_20_FONT_ID; -#elif !defined(OMIT_TEENSY_FONT) - return LEXENDDECA_8_FONT_ID; -#elif !defined(OMIT_ITTY_BITTY_FONT) - return LEXENDDECA_9_FONT_ID; -#else -#error "No reader fonts enabled for LEXENDDECA" -#endif } } @@ -220,9 +162,76 @@ void applyLegacyFrontButtonLayout(CrossPointSettings& settings) { } } +void applyLegacyStatusBarSettings(CrossPointSettings& settings) { + switch (static_cast(settings.statusBar)) { + case CrossPointSettings::NONE: + settings.statusBarChapterPageCount = 0; + settings.statusBarBookProgressPercentage = 0; + settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; + settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; + settings.statusBarBattery = 0; + break; + case CrossPointSettings::NO_PROGRESS: + settings.statusBarChapterPageCount = 0; + settings.statusBarBookProgressPercentage = 0; + settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; + settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; + settings.statusBarBattery = 1; + break; + case CrossPointSettings::BOOK_PROGRESS_BAR: + settings.statusBarChapterPageCount = 1; + settings.statusBarBookProgressPercentage = 0; + settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; + settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; + settings.statusBarBattery = 1; + break; + case CrossPointSettings::ONLY_BOOK_PROGRESS_BAR: + settings.statusBarChapterPageCount = 1; + settings.statusBarBookProgressPercentage = 0; + settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; + settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; + settings.statusBarBattery = 0; + break; + case CrossPointSettings::CHAPTER_PROGRESS_BAR: + settings.statusBarChapterPageCount = 0; + settings.statusBarBookProgressPercentage = 1; + settings.statusBarProgressBar = CrossPointSettings::CHAPTER_PROGRESS; + settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; + settings.statusBarBattery = 1; + break; + case CrossPointSettings::FULL: + default: + settings.statusBarChapterPageCount = 1; + settings.statusBarBookProgressPercentage = 1; + settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; + settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; + settings.statusBarBattery = 1; + break; + } +} + +bool isEnumRawValueAllowed(const SettingInfo& info, const uint8_t value) { + if (info.enumRawValues.empty()) return value < settingEnumOptionCount(info); + return std::find(info.enumRawValues.begin(), info.enumRawValues.end(), value) != info.enumRawValues.end(); +} + +uint8_t defaultEnumRawValue(const SettingInfo& info, const uint8_t fieldDefault) { + if (isEnumRawValueAllowed(info, fieldDefault)) return fieldDefault; + return info.enumRawValues.empty() ? 0 : info.enumRawValues.front(); +} + +bool isSleepScreenSetting(const SettingInfo& info) { return info.key && strcmp(info.key, "sleepScreen") == 0; } + +uint8_t migrateTiltDirectionValue(const uint8_t direction) { + if (direction == CrossPointSettings::TILT_LEFT_RIGHT) return CrossPointSettings::TILT_LEFT_RIGHT_INVERTED; + if (direction == CrossPointSettings::TILT_LEFT_RIGHT_INVERTED) return CrossPointSettings::TILT_LEFT_RIGHT; + return direction; +} + } // namespace const char* CrossPointSettings::getDefaultDeviceName() { + if (BoardConfig::isSticky()) return "Sticky"; if (gpio.deviceIsX3()) return "CrossInk X3"; if (gpio.deviceIsX4()) return "CrossInk X4"; return "CrossInk"; @@ -264,6 +273,8 @@ void CrossPointSettings::validateReaderFrontButtonMapping(CrossPointSettings& se } } +uint8_t CrossPointSettings::defaultUiScale() { return UI_SCALE_SMALL; } + uint8_t CrossPointSettings::sleepTimeoutEnumToMinutes(const uint8_t legacyValue) { switch (legacyValue) { case SLEEP_1_MIN: @@ -358,10 +369,229 @@ uint16_t CrossPointSettings::getReadingIdleTimeThresholdSeconds() const { return readingIdleTimeThresholdSecondsForUnits(readingIdleTimeThresholdUnits); } -bool CrossPointSettings::saveToFile() const { +void CrossPointSettings::toJson(JsonDocument& doc) const { std::lock_guard lock(_mutex); - Storage.mkdir("/.crosspoint"); - return JsonSettingsIO::saveSettings(*this, SETTINGS_FILE_JSON); + for (const auto& info : getBaseSettingsList()) { + if (!info.key || (!info.valuePtr && !info.stringOffset)) continue; + if (info.stringOffset) { + const char* value = reinterpret_cast(this) + info.stringOffset; + if (info.obfuscated) { + char key[64]; + snprintf(key, sizeof(key), "%s_obf", info.key); + doc[key] = obfuscation::obfuscateToBase64(value); + } else { + doc[info.key] = value; + } + } else { + uint8_t value = this->*(info.valuePtr); + if (isSleepScreenSetting(info)) value = sleepScreenModeToStorage(value); + doc[info.key] = value; + } + } + + doc["frontButtonBack"] = frontButtonBack; + doc["frontButtonConfirm"] = frontButtonConfirm; + doc["frontButtonLeft"] = frontButtonLeft; + doc["frontButtonRight"] = frontButtonRight; + doc["readerFrontButtonsEnabled"] = readerFrontButtonsEnabled; + doc["readerFrontButtonBack"] = readerFrontButtonBack; + doc["readerFrontButtonConfirm"] = readerFrontButtonConfirm; + doc["readerFrontButtonLeft"] = readerFrontButtonLeft; + doc["readerFrontButtonRight"] = readerFrontButtonRight; + doc["fontFamily"] = fontFamily; + if (sdFontFamilyName[0] != '\0') doc["sdFontFamilyName"] = sdFontFamilyName; + if (dictionarySdFontFamilyName[0] != '\0') doc["dictionaryFont"] = dictionarySdFontFamilyName; + doc["dictionaryFontSize"] = dictionaryFontPointSize; + doc["language"] = (language < getLanguageCount()) ? LANGUAGE_CODES[language] : "EN"; + doc["tiltPageTurnDirectionSchema"] = TILT_DIRECTION_SCHEMA_CURRENT; + doc["clockDateHasBeenSynced"] = clockDateHasBeenSynced; +} + +bool CrossPointSettings::fromJson(JsonVariantConst doc) { + std::lock_guard lock(_mutex); + bool needsResave = false; + auto clamp = [](const uint8_t value, const uint8_t maxValue, const uint8_t fallback) { + return value < maxValue ? value : fallback; + }; + const bool migrateLegacyTiltMode = !doc["tiltPageTurn"].isNull() && doc["tiltPageTurnDirection"].isNull(); + const uint8_t legacyTiltMode = doc["tiltPageTurn"] | static_cast(TILT_OFF); + const bool migrateTiltDirectionSchema = + !doc["tiltPageTurnDirection"].isNull() && + ((doc["tiltPageTurnDirectionSchema"] | static_cast(1)) < TILT_DIRECTION_SCHEMA_CURRENT); + + if (doc["statusBarChapterPageCount"].isNull()) applyLegacyStatusBarSettings(*this); + + for (const auto& info : getBaseSettingsList()) { + if (!info.key || (!info.valuePtr && !info.stringOffset)) continue; + if (info.stringOffset) { + char* destination = reinterpret_cast(this) + info.stringOffset; + if (info.stringMaxLen == 0) { + LOG_ERR("CPS", "Misconfigured SettingInfo: stringMaxLen is 0 for key '%s'", info.key); + destination[0] = '\0'; + needsResave = true; + continue; + } + const std::string fieldDefault = destination; + std::string value; + if (info.obfuscated) { + char key[64]; + snprintf(key, sizeof(key), "%s_obf", info.key); + obfuscation::DecodeStatus status = obfuscation::DecodeStatus::INVALID; + value = obfuscation::deobfuscateFromBase64(doc[key] | "", info.stringMaxLen - 1, &status); + if (status == obfuscation::DecodeStatus::LEGACY && !value.empty()) needsResave = true; + if (status == obfuscation::DecodeStatus::TOO_LONG) { + LOG_ERR("CPS", "Oversized obfuscated value for key '%s'", info.key); + needsResave = true; + } + if (status == obfuscation::DecodeStatus::INVALID || status == obfuscation::DecodeStatus::EMPTY || + status == obfuscation::DecodeStatus::TOO_LONG || value.empty()) { + const char* legacyValue = doc[info.key] | fieldDefault.c_str(); + if (strlen(legacyValue) >= info.stringMaxLen) { + LOG_ERR("CPS", "Oversized plaintext value for key '%s'", info.key); + value = fieldDefault; + needsResave = true; + } else { + value = legacyValue; + if (value != fieldDefault) needsResave = true; + } + } + } else { + value = doc[info.key] | fieldDefault; + } + strncpy(destination, value.c_str(), info.stringMaxLen - 1); + destination[info.stringMaxLen - 1] = '\0'; + if (strcmp(info.key, "deviceName") == 0 && strlen(destination) < MIN_DEVICE_NAME_LENGTH) { + destination[0] = '\0'; + needsResave = true; + } + continue; + } + + const uint8_t fieldDefault = this->*(info.valuePtr); + uint8_t value = doc[info.key] | fieldDefault; + if (strcmp(info.key, "sdFontSizeRange") == 0 && value == SD_FONT_RANGE_NO_EMOJI_LEGACY) { + value = SD_FONT_RANGE_ALL; + needsResave = true; + } + if (isSleepScreenSetting(info)) { + const uint8_t storedDefault = sleepScreenModeToStorage(fieldDefault); + const uint8_t storedValue = doc[info.key] | storedDefault; + value = sleepScreenStorageToMode(storedValue); + if (sleepScreenModeToStorage(value) != storedValue) needsResave = true; + this->*(info.valuePtr) = value; + continue; + } + if (strcmp(info.key, "fontSize") == 0 && !doc["fontSize"].isNull() && value < MIN_READER_FONT_POINT_SIZE) { + if (doc["sdFontFamilyName"].is() && doc["sdFontFamilyName"].as()[0] != '\0') { + legacySdFontSizeStep = value; + } else { + value = getReaderFontPointSize(static_cast(value)); + needsResave = true; + } + } else if (info.type == SettingType::ENUM && + !(strcmp(info.key, "fontSize") == 0 && doc["sdFontFamilyName"].is() && + doc["sdFontFamilyName"].as()[0] != '\0' && value >= MIN_READER_FONT_POINT_SIZE) && + !isEnumRawValueAllowed(info, value)) { + value = defaultEnumRawValue(info, fieldDefault); + needsResave = true; + } else if (info.type == SettingType::TOGGLE) { + value = clamp(value, 2, fieldDefault); + } else if (info.type == SettingType::VALUE) { + value = std::clamp(value, info.valueRange.min, info.valueRange.max); + } + this->*(info.valuePtr) = value; + } + + if (doc["fileBrowserDisplay"].isNull()) { + if (!doc["fileBrowserPreview"].isNull()) { + fileBrowserDisplay = clamp(doc["fileBrowserPreview"] | static_cast(FILE_BROWSER_DISPLAY_1_LINE), + FILE_BROWSER_DISPLAY_COUNT, FILE_BROWSER_DISPLAY_1_LINE); + needsResave = true; + } else if (uiTheme == MINIMAL) { + fileBrowserDisplay = FILE_BROWSER_DISPLAY_2_LINES; + needsResave = true; + } + } + + if (migrateLegacyTiltMode) { + if (legacyTiltMode == 1 || legacyTiltMode == 2) { + tiltPageTurn = TILT_ON; + tiltPageTurnDirection = legacyTiltMode == 2 ? TILT_LEFT_RIGHT : TILT_LEFT_RIGHT_INVERTED; + needsResave = true; + } else { + tiltPageTurn = TILT_OFF; + } + } else if (migrateTiltDirectionSchema) { + tiltPageTurnDirection = migrateTiltDirectionValue(tiltPageTurnDirection); + needsResave = true; + } + if (!doc["tiltPageTurnDirection"].isNull() && doc["tiltPageTurnDirectionSchema"].isNull()) needsResave = true; + + if (doc["hideClock"].isNull() && !doc["statusBarClock"].isNull()) { + constexpr uint8_t LEGACY_SHOW_CLOCK_NEVER = 0; + const uint8_t legacyShowClock = + clamp(doc["statusBarClock"] | LEGACY_SHOW_CLOCK_NEVER, HIDE_CLOCK_MODE_COUNT, LEGACY_SHOW_CLOCK_NEVER); + hideClock = legacyShowClock == LEGACY_SHOW_CLOCK_NEVER ? HIDE_CLOCK_ALWAYS : HIDE_CLOCK_NEVER; + needsResave = true; + } + if (doc["sleepTimeoutMinutes"].isNull() && !doc["sleepTimeout"].isNull()) { + const uint8_t legacyValue = + clamp(doc["sleepTimeout"] | static_cast(SLEEP_10_MIN), SLEEP_TIMEOUT_COUNT, SLEEP_10_MIN); + sleepTimeoutMinutes = sleepTimeoutEnumToMinutes(legacyValue); + needsResave = true; + } + + frontButtonBack = + clamp(doc["frontButtonBack"] | static_cast(FRONT_HW_BACK), FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_BACK); + frontButtonConfirm = clamp(doc["frontButtonConfirm"] | static_cast(FRONT_HW_CONFIRM), + FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_CONFIRM); + frontButtonLeft = + clamp(doc["frontButtonLeft"] | static_cast(FRONT_HW_LEFT), FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_LEFT); + frontButtonRight = clamp(doc["frontButtonRight"] | static_cast(FRONT_HW_RIGHT), FRONT_BUTTON_HARDWARE_COUNT, + FRONT_HW_RIGHT); + validateFrontButtonMapping(*this); + readerFrontButtonsEnabled = clamp(doc["readerFrontButtonsEnabled"] | static_cast(0), 2, 0); + readerFrontButtonBack = clamp(doc["readerFrontButtonBack"] | static_cast(FRONT_HW_BACK), + FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_BACK); + readerFrontButtonConfirm = clamp(doc["readerFrontButtonConfirm"] | static_cast(FRONT_HW_CONFIRM), + FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_CONFIRM); + readerFrontButtonLeft = clamp(doc["readerFrontButtonLeft"] | static_cast(FRONT_HW_LEFT), + FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_LEFT); + readerFrontButtonRight = clamp(doc["readerFrontButtonRight"] | static_cast(FRONT_HW_RIGHT), + FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_RIGHT); + validateReaderFrontButtonMapping(*this); + + const uint8_t storedFontFamily = doc["fontFamily"] | static_cast(0); + fontFamily = clamp(storedFontFamily, BUILTIN_FONT_COUNT, 0); + const char* sdFamily = doc["sdFontFamilyName"] | ""; + strncpy(sdFontFamilyName, sdFamily, sizeof(sdFontFamilyName) - 1); + sdFontFamilyName[sizeof(sdFontFamilyName) - 1] = '\0'; + const char* dictionaryFamily = doc["dictionaryFont"] | ""; + strncpy(dictionarySdFontFamilyName, dictionaryFamily, sizeof(dictionarySdFontFamilyName) - 1); + dictionarySdFontFamilyName[sizeof(dictionarySdFontFamilyName) - 1] = '\0'; + dictionaryFontPointSize = doc["dictionaryFontSize"] | static_cast(0); + if (storedFontFamily >= BUILTIN_FONT_COUNT) needsResave = true; + if (doc["lineHeightPercent"].isNull() && !doc["lineSpacing"].isNull()) { + const uint8_t legacySpacing = + clamp(doc["lineSpacing"] | static_cast(NORMAL), LINE_COMPRESSION_COUNT, static_cast(NORMAL)); + lineHeightPercent = legacyLineSpacingToPercent(legacySpacing, fontFamily, sdFontFamilyName[0] != '\0'); + needsResave = true; + } + if (doc["language"].is()) { + language = static_cast(I18n::languageFromCode(doc["language"].as())); + } + clockDateHasBeenSynced = clamp(doc["clockDateHasBeenSynced"] | static_cast(0), 2, 0); + + if (needsResave) requestResave(); + LOG_DBG("CPS", "Settings loaded from file"); + return true; +} + +bool CrossPointSettings::saveToFile() const { + std::lock_guard lock(storeMutex); + JsonDocument doc; + toJson(doc); + return PersistableStoreBase::writeDocToFile(SETTINGS_FILE_JSON, doc); } bool CrossPointSettings::loadFromFile() { @@ -370,13 +600,20 @@ bool CrossPointSettings::loadFromFile() { auto loadJsonSettings = [this](const char* path, bool migrateToCurrentPath) -> JsonLoadStatus { if (!Storage.exists(path)) return JsonLoadStatus::MissingOrEmpty; - String json = Storage.readFile(path); - if (!json.isEmpty()) { + JsonDocument doc; + if (PersistableStoreBase::readDocFromFile(path, doc)) { + bool result = false; bool resave = false; - bool result; { - std::lock_guard lock(_mutex); - result = JsonSettingsIO::loadSettings(*this, json.c_str(), &resave); + std::lock_guard storeLock(storeMutex); + resaveRequested = false; + result = fromJson(doc.as()); + resave = resaveRequested; + resaveRequested = false; + } + if (result) { + std::lock_guard settingsLock(_mutex); + if (restoreLegacyRtcDateSyncState(*this)) resave = true; } if (result && (resave || migrateToCurrentPath)) { if (saveToFile()) { @@ -487,7 +724,9 @@ bool CrossPointSettings::loadFromBinaryFile() { } } if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, fontSize, getActiveReaderFontSizeCount()); + uint8_t legacyFontSize = MEDIUM; + readAndValidate(inputFile, legacyFontSize, getActiveReaderFontSizeCount()); + readerFontPointSize = getReaderFontPointSize(static_cast(legacyFontSize)); if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, lineSpacing, LINE_COMPRESSION_COUNT); if (++settingsRead >= fileSettingsCount) break; @@ -547,10 +786,46 @@ bool CrossPointSettings::loadFromBinaryFile() { lineHeightPercent = legacyLineSpacingToPercent(lineSpacing, fontFamily, sdFontFamilyName[0] != '\0'); - LOG_DBG("CPS", "Settings loaded from binary file"); return true; } +CrossPointSettings::StatusBarSpec CrossPointSettings::statusBarSpec() const { + StatusBarSpec spec; + spec.showChapterPageCount = statusBarChapterPageCount != 0; + spec.showBookProgressPercent = statusBarBookProgressPercentage != 0; + spec.showStablePageNumbers = stablePageNumbers != 0; + spec.titleMode = statusBarTitle; + spec.timeLeftMode = statusBarTimeLeft; + spec.showBattery = statusBarBattery != 0; + spec.showBatteryPercent = hideBatteryPercentage == HIDE_NEVER; + spec.showClock = hideClock == HIDE_CLOCK_NEVER; + spec.progressBarMode = statusBarProgressBar; + spec.progressBarHeightPx = + statusBarProgressBar != HIDE_PROGRESS ? static_cast((statusBarProgressBarThickness + 1) * 2) : 0; + spec.xtcMode = xtcStatusBarMode; + return spec; +} + +ReaderRenderSpec CrossPointSettings::readerRenderSpec(const uint16_t viewportWidth, const uint16_t viewportHeight, + const EpubRenderMode renderMode) const { + ReaderRenderSpec spec; + spec.fontId = getReaderFontId(); + spec.lineCompression = getReaderLineCompression(); + spec.extraParagraphSpacing = extraParagraphSpacing != 0; + spec.forceParagraphIndents = forceParagraphIndents != 0; + spec.paragraphAlignment = paragraphAlignment; + spec.viewportWidth = viewportWidth; + spec.viewportHeight = viewportHeight; + spec.hyphenationEnabled = hyphenationEnabled != 0; + spec.embeddedStyle = embeddedStyle != 0; + spec.imageRendering = imageRendering; + spec.bionicReadingEnabled = bionicReadingEnabled != 0; + spec.guideReadingEnabled = guideReadingEnabled != 0; + spec.wordSpacing = wordSpacing; + spec.renderMode = renderMode; + return spec; +} + float CrossPointSettings::getReaderLineCompression() const { return static_cast(clampedLineHeightPercent(lineHeightPercent)) / 100.0f; } @@ -635,10 +910,6 @@ uint8_t CrossPointSettings::getStoredReaderFontSize(const FONT_SIZE size) { uint8_t CrossPointSettings::getReaderFontPointSize(const FONT_SIZE size) { switch (size) { - case TEENSY: - return 8; - case ITTY_BITTY: - return 9; case TINY: return 10; case SMALL: @@ -648,10 +919,6 @@ uint8_t CrossPointSettings::getReaderFontPointSize(const FONT_SIZE size) { return 14; case LARGE: return 16; - case EXTRA_LARGE: - return 18; - case HUGE_SIZE: - return 20; } } @@ -672,18 +939,22 @@ bool CrossPointSettings::isSdFontPointSizeAllowedForRange(const uint8_t pointSiz } CrossPointSettings::FONT_SIZE CrossPointSettings::getEffectiveReaderFontSize() const { - uint8_t stored = 0; + FONT_SIZE best = firstAvailableReaderFontSize(); + uint8_t bestDiff = UINT8_MAX; for (const FONT_SIZE size : READER_FONT_SIZE_STORAGE_ORDER) { if (!isReaderFontSizeAvailable(size)) continue; - if (fontSize == stored) return size; - stored++; + const uint8_t pointSize = getReaderFontPointSize(size); + const uint8_t diff = + pointSize > readerFontPointSize ? pointSize - readerFontPointSize : readerFontPointSize - pointSize; + if (diff < bestDiff || (diff == bestDiff && pointSize < getReaderFontPointSize(best))) { + best = size; + bestDiff = diff; + } } - return firstAvailableReaderFontSize(); + return best; } -uint8_t CrossPointSettings::getSdFontTargetPointSize() const { - return getSdFontRangePointSize(sdFontSizeRange, fontSize); -} +uint8_t CrossPointSettings::getSdFontTargetPointSize() const { return readerFontPointSize; } bool CrossPointSettings::changeReaderFontSize(const bool larger) { const FONT_SIZE currentSize = getEffectiveReaderFontSize(); @@ -702,7 +973,7 @@ bool CrossPointSettings::changeReaderFontSize(const bool larger) { (currentIndex + direction * static_cast(step) + static_cast(sizeCount)) % sizeCount; const uint8_t stored = getStoredReaderFontSize(READER_FONT_SIZE_CYCLE_ORDER[nextIndex]); if (stored != INVALID_READER_FONT_SIZE) { - fontSize = stored; + readerFontPointSize = getReaderFontPointSize(READER_FONT_SIZE_CYCLE_ORDER[nextIndex]); return true; } } @@ -712,7 +983,7 @@ bool CrossPointSettings::changeReaderFontSize(const bool larger) { int CrossPointSettings::getReaderFontId() const { // Check SD card font first if (sdFontFamilyName[0] != '\0' && sdFontIdResolver) { - int id = sdFontIdResolver(sdFontResolverCtx, sdFontFamilyName, fontSize); + int id = sdFontIdResolver(sdFontResolverCtx, sdFontFamilyName, readerFontPointSize); if (id != 0) return id; // Fall through to built-in if SD font not found } @@ -727,82 +998,28 @@ int CrossPointSettings::getBuiltInReaderFontId() const { case LEXENDDECA: default: switch (effectiveSize) { -#ifndef OMIT_TEENSY_FONT - case TEENSY: - return LEXENDDECA_8_FONT_ID; -#endif -#ifndef OMIT_ITTY_BITTY_FONT - case ITTY_BITTY: - return LEXENDDECA_9_FONT_ID; -#endif -#ifndef OMIT_TINY_FONT case TINY: return LEXENDDECA_10_FONT_ID; -#endif -#ifndef OMIT_SMALL_FONT case SMALL: return LEXENDDECA_12_FONT_ID; -#endif -#ifndef OMIT_MEDIUM_FONT case MEDIUM: default: return LEXENDDECA_14_FONT_ID; -#endif -#ifndef OMIT_LARGE_FONT case LARGE: -#ifdef OMIT_MEDIUM_FONT - default: -#endif return LEXENDDECA_16_FONT_ID; -#endif -#ifndef OMIT_XLARGE_FONT - case EXTRA_LARGE: - return LEXENDDECA_18_FONT_ID; -#endif -#ifndef OMIT_HUGE_FONT - case HUGE_SIZE: - return LEXENDDECA_20_FONT_ID; -#endif } return getFallbackReaderFontIdForFamily(LEXENDDECA); case BITTER: switch (effectiveSize) { -#ifndef OMIT_TEENSY_FONT - case TEENSY: - return BITTER_8_FONT_ID; -#endif -#ifndef OMIT_ITTY_BITTY_FONT - case ITTY_BITTY: - return BITTER_9_FONT_ID; -#endif -#ifndef OMIT_TINY_FONT case TINY: return BITTER_10_FONT_ID; -#endif -#ifndef OMIT_SMALL_FONT case SMALL: return BITTER_12_FONT_ID; -#endif -#ifndef OMIT_MEDIUM_FONT case MEDIUM: default: return BITTER_14_FONT_ID; -#endif -#ifndef OMIT_LARGE_FONT case LARGE: -#ifdef OMIT_MEDIUM_FONT - default: -#endif return BITTER_16_FONT_ID; -#endif -#ifndef OMIT_XLARGE_FONT - case EXTRA_LARGE: - return BITTER_18_FONT_ID; -#endif -#ifndef OMIT_HUGE_FONT - case HUGE_SIZE: - return BITTER_20_FONT_ID; -#endif } return getFallbackReaderFontIdForFamily(BITTER); } diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index cc1047bdcc..58d6d2c3b4 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -1,26 +1,22 @@ #pragma once +#include +#include #include +#include #include #include #include #include -class CrossPointSettings { +class CrossPointSettings : public PersistableStore { private: mutable std::mutex _mutex; - // Private constructor for singleton CrossPointSettings() = default; - - // Static instance - static CrossPointSettings instance; + friend class PersistableStore; public: - // Delete copy constructor and assignment - CrossPointSettings(const CrossPointSettings&) = delete; - CrossPointSettings& operator=(const CrossPointSettings&) = delete; - // Access the settings mutex for protecting multi-field reads/writes from other cores. // Callers must not re-enter SETTINGS methods that lock _mutex while holding it. std::mutex& getMutex() const { return _mutex; } @@ -84,6 +80,25 @@ class CrossPointSettings { XTC_STATUS_BAR_MODE_COUNT }; enum HIDE_CLOCK_MODE { HIDE_CLOCK_NEVER = 0, HIDE_CLOCK_IN_READER = 1, HIDE_CLOCK_ALWAYS = 2, HIDE_CLOCK_MODE_COUNT }; + // Persisted date-format values mirror HalClock::DateFormat. + enum DATE_FORMAT { + DATE_FORMAT_MONTH_DAY_YEAR_LONG = 0, + DATE_FORMAT_DAY_MONTH_YEAR_LONG = 1, + DATE_FORMAT_MONTH_DAY_YEAR_NUMERIC = 2, + DATE_FORMAT_DAY_MONTH_YEAR_NUMERIC = 3, + DATE_FORMAT_YEAR_MONTH_DAY_NUMERIC = 4, + DATE_FORMAT_MONTH_DAY_NUMERIC = 5, + DATE_FORMAT_DAY_MONTH_NUMERIC = 6, + DATE_FORMAT_MONTH_DAY_LONG = 7, + DATE_FORMAT_DAY_MONTH_LONG = 8, + DATE_FORMAT_COUNT + }; + enum DATE_SEPARATOR { + DATE_SEPARATOR_PERIOD = 0, + DATE_SEPARATOR_HYPHEN = 1, + DATE_SEPARATOR_SLASH = 2, + DATE_SEPARATOR_COUNT + }; enum ORIENTATION { PORTRAIT = 0, // 480x800 logical coordinates (current default) @@ -143,22 +158,12 @@ class CrossPointSettings { enum FONT_FAMILY { LEXENDDECA = 0, BITTER = 1, FONT_FAMILY_COUNT }; static constexpr uint8_t BUILTIN_FONT_COUNT = FONT_FAMILY_COUNT; // Font size options - enum FONT_SIZE { - TINY = 0, - SMALL = 1, - MEDIUM = 2, - LARGE = 3, - EXTRA_LARGE = 4, - TEENSY = 5, - HUGE_SIZE = 6, - ITTY_BITTY = 7, - FONT_SIZE_COUNT - }; + enum FONT_SIZE { TINY = 0, SMALL = 1, MEDIUM = 2, LARGE = 3, FONT_SIZE_COUNT }; enum SD_FONT_SIZE_RANGE { SD_FONT_RANGE_TEENSY = 0, SD_FONT_RANGE_TINY = 1, SD_FONT_RANGE_XLARGE = 2, - SD_FONT_RANGE_NO_EMOJI = 3, + SD_FONT_RANGE_NO_EMOJI_LEGACY = 3, SD_FONT_RANGE_ALL = 4, SD_FONT_SIZE_RANGE_COUNT }; @@ -223,6 +228,7 @@ class CrossPointSettings { JOIN_NETWORK = 19, CREATE_HOTSPOT = 20, CREATE_CLIPPING = 21, + LOOKUP_WORD = 22, SHORT_PWRBTN_COUNT }; @@ -253,6 +259,9 @@ class CrossPointSettings { // Image rendering in EPUB reader enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT }; + enum TOUCH_READER_CONTROLS { TOUCH_READER_OFF = 0, TOUCH_READER_ON = 1, TOUCH_READER_CONTROLS_COUNT }; + + enum INDEXING_METHOD { INDEXING_INCREMENTAL = 0, INDEXING_FULL_SECTION = 1, INDEXING_METHOD_COUNT }; enum TILT_PAGE_TURN { TILT_OFF = 0, TILT_ON = 1, TILT_PAGE_TURN_COUNT }; enum TILT_PAGE_TURN_DIRECTION { @@ -286,6 +295,7 @@ class CrossPointSettings { LONG_MENU_JOIN_NETWORK = 18, LONG_MENU_CREATE_HOTSPOT = 19, LONG_MENU_CREATE_CLIPPING = 20, + LONG_MENU_LOOKUP_WORD = 21, LONG_PRESS_MENU_ACTION_COUNT }; @@ -302,6 +312,11 @@ class CrossPointSettings { QUICK_RESUME_SLEEP_SCREEN_COUNT }; + // UI scale for list-style screens: sizes list fonts and row heights + // together so touch targets grow uniformly. + enum UI_SCALE { UI_SCALE_SMALL = 0, UI_SCALE_LARGE = 1, UI_SCALE_COUNT }; + static uint8_t defaultUiScale(); + // Sleep screen settings uint8_t sleepScreen = DARK; // Sleep screen cover mode settings @@ -319,7 +334,7 @@ class CrossPointSettings { uint8_t statusBarTimeLeft = TIME_LEFT_HIDE; uint8_t statusBarBattery = 1; uint8_t xtcStatusBarMode = XTC_STATUS_BAR_HIDE; - // Clock visibility mode (X3 only, requires DS3231 RTC) + // Clock visibility mode (requires an RTC-backed clock). uint8_t hideClock = HIDE_CLOCK_ALWAYS; // Clock UTC offset in quarter-hour steps, biased by 48 so it fits in uint8_t. // Value 48 = UTC+0, 0 = UTC-12:00, 104 = UTC+14:00. @@ -327,6 +342,10 @@ class CrossPointSettings { uint8_t clockUtcOffsetQ = 48; // Clock display format: 0 = 24-hour, 1 = 12-hour uint8_t clockFormat = 0; + // Date display format. Values match HalClock::DateFormat; 0 preserves the existing "Jan 01, 2026" default. + uint8_t dateFormat = DATE_FORMAT_MONTH_DAY_YEAR_LONG; + // Separator for numeric dates. Text-based date formats do not use it. + uint8_t dateSeparator = DATE_SEPARATOR_SLASH; // Set once an NTP sync succeeds. Used to skip re-syncing on every WiFi connect. // Resetting to 0 (e.g. via the web UI) forces a re-sync on next WiFi connect. uint8_t clockHasBeenSynced = 0; @@ -338,6 +357,10 @@ class CrossPointSettings { uint8_t forceParagraphIndents = 0; uint8_t textAntiAliasing = 1; uint8_t readerDarkMode = 0; + // Touch screen reader zones/gestures on boards with a touch controller. + uint8_t touchReaderControls = TOUCH_READER_ON; + // Disables all touchscreen input while a reader is active. Reader menus temporarily override this. + uint8_t disableReaderTouchscreen = 0; // Short power button action behaviour uint8_t shortPwrBtn = IGNORE; // Long power button action behaviour @@ -367,18 +390,17 @@ class CrossPointSettings { uint8_t readerFrontButtonRight = FRONT_HW_RIGHT; // Reader font settings uint8_t fontFamily = LEXENDDECA; - uint8_t fontSize = MEDIUM; -#if defined(OMIT_EMOJI_FONTS) - uint8_t sdFontSizeRange = SD_FONT_RANGE_NO_EMOJI; -#elif defined(OMIT_TINY_FONT) && defined(OMIT_SMALL_FONT) - uint8_t sdFontSizeRange = SD_FONT_RANGE_XLARGE; -#elif defined(OMIT_MEDIUM_FONT) && defined(OMIT_LARGE_FONT) && defined(OMIT_XLARGE_FONT) && defined(OMIT_HUGE_FONT) - uint8_t sdFontSizeRange = SD_FONT_RANGE_TEENSY; -#else + // The physical reader size selected by the user. Built-in and SD-card font + // families resolve this to their closest available file. + uint8_t readerFontPointSize = 14; + // Transient compatibility state for JSON settings written before fontSize + // stored a point size. SdCardFontSystem resolves it once the family catalog + // is available, then persists readerFontPointSize. + uint8_t legacySdFontSizeStep = UINT8_MAX; uint8_t sdFontSizeRange = SD_FONT_RANGE_TINY; -#endif uint8_t lineSpacing = NORMAL; // migration only; new saves use lineHeightPercent uint8_t lineHeightPercent = 100; + uint8_t wordSpacing = 0; uint8_t paragraphAlignment = JUSTIFIED; // Auto-sleep timeout setting (default 10 minutes). Legacy sleepTimeout enum values are migration-only. uint8_t sleepTimeoutMinutes = 10; @@ -394,6 +416,10 @@ class CrossPointSettings { char opdsServerUrl[128] = ""; char opdsUsername[64] = ""; char opdsPassword[64] = ""; + // OPDS download destination (empty = SD root). Edited from the OPDS server list. + char opdsDownloadFolder[64] = ""; + // Nearby file receive destination (empty = SD root). + char nearbyReceiveFolder[64] = ""; // Hide battery percentage uint8_t hideBatteryPercentage = HIDE_NEVER; // Long-press page turn button behavior @@ -402,12 +428,16 @@ class CrossPointSettings { uint8_t uiTheme = LYRA; // Recent Books screen layout uint8_t recentBooksView = RECENT_BOOKS_LIST; + // UI scale (list fonts + row heights); touch boards default one step larger + uint8_t uiScale = defaultUiScale(); // Sunlight fading compensation uint8_t fadingFix = 0; // Quick-return from footnotes when a footnote shortcut is active. uint8_t pwrBtnFootnoteBack = 1; // Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled) uint8_t embeddedStyle = 1; + // EPUB section indexing policy. The current chapter keeps its active build. + uint8_t indexingMethod = INDEXING_FULL_SECTION; // Focus Reading - emphasizes the first part of words with bold uint8_t bionicReadingEnabled = 0; // Guide Dots - places a middle dot between words to guide the eye @@ -416,6 +446,10 @@ class CrossPointSettings { uint8_t epubRenderMode = 0; // SD card font family name, including optional range suffix (empty = use built-in fontFamily) char sdFontFamilyName[64] = ""; + // Global dictionary SD-card font (empty = use the reader font). + char dictionarySdFontFamilyName[64] = ""; + // Zero follows the active reader size. + uint8_t dictionaryFontPointSize = 0; // Show hidden files/directories (starting with '.') in the file browser (0 = hidden, 1 = show) uint8_t showHiddenFiles = 0; // Hide file extensions in the file browser right-side value column (0 = show, 1 = hide) @@ -426,7 +460,7 @@ class CrossPointSettings { uint8_t removeReadBooksFromRecents = 0; // Move epub to /Read/ folder on SD card when marked as finished (0 = disabled, 1 = enabled) uint8_t moveFinishedToReadFolder = 0; - // Automatically write a dated global reading-stats backup on X3 sleep (0 = off, 1 = on). + // Automatically write a dated global reading-stats backup before sleep when an RTC is available (0 = off, 1 = on). uint8_t autoBackupStats = 1; // Idle threshold for reading stats, stored in 10-second units to fit uint8_t. uint8_t readingIdleTimeThresholdUnits = 30; @@ -436,9 +470,20 @@ class CrossPointSettings { uint8_t longPressMenuAction = LONG_MENU_OFF; // Long-press Back quick action in reader (defaults to the historical file browser shortcut) uint8_t longPressBackAction = LONG_MENU_FILE_BROWSER; - // Tilt-based page turning (X3 only — requires QMI8658 IMU) + // Tilt-based page turning on devices with a supported IMU (X3 and Sticky). uint8_t tiltPageTurn = TILT_OFF; uint8_t tiltPageTurnDirection = TILT_LEFT_RIGHT; + // Frontlight quick-panel state (boards with FREEINK_CAP_FRONTLIGHT, e.g. X4 + // Pro). Applied at boot, edited only from the frontlight panel; persisted as + // category-less entries so they stay out of the Settings screen. Writes are + // debounced by the panel (saved once on exit), not per slider tick. + uint8_t frontlightBrightness = 60; + uint8_t frontlightWarmth = 50; // 0 = cool .. 100 = warm + uint8_t frontlightOn = 0; + // When 0 (default), the frontlight always comes up OFF after a wake/boot (brightness + // and warmth are still remembered for when it's switched on). When 1, the on/off + // state from before sleep is restored too. Shown in Display settings on frontlight boards. + uint8_t frontlightRestoreOnWake = 0; // Language setting (Language enum index, default 0 = EN) uint8_t language = 0; // Custom KOReader sync device display name. Empty means use the hardware default. @@ -452,9 +497,6 @@ class CrossPointSettings { ~CrossPointSettings() = default; - // Get singleton instance - static CrossPointSettings& getInstance() { return instance; } - static constexpr uint16_t POWER_BUTTON_WAKE_SHORT_MS = 10; static constexpr uint16_t POWER_BUTTON_WAKE_LONG_MS = 200; static constexpr uint16_t POWER_BUTTON_LONG_PRESS_MS = 400; @@ -462,9 +504,11 @@ class CrossPointSettings { static constexpr uint8_t SLEEP_TIMEOUT_NEVER_MINUTES = 31; static constexpr uint8_t MAX_SLEEP_TIMEOUT_MINUTES = SLEEP_TIMEOUT_NEVER_MINUTES; static constexpr uint8_t SD_FONT_MAX_SIZE_STEPS = 8; + static constexpr uint8_t MIN_READER_FONT_POINT_SIZE = 8; static constexpr uint8_t MIN_LINE_HEIGHT_PERCENT = 70; static constexpr uint8_t MAX_LINE_HEIGHT_PERCENT = 200; static constexpr uint8_t LINE_HEIGHT_PERCENT_STEP = 1; + static constexpr uint8_t MAX_WORD_SPACING = 4; static constexpr uint16_t DEFAULT_READING_IDLE_TIME_THRESHOLD_SECONDS = 5 * 60; static constexpr uint16_t MIN_READING_IDLE_TIME_THRESHOLD_SECONDS = 30; static constexpr uint16_t MAX_READING_IDLE_TIME_THRESHOLD_SECONDS = 10 * 60; @@ -498,7 +542,7 @@ class CrossPointSettings { // Callback to resolve SD card font IDs. Set by SdCardFontSystem::begin(). // Returns font ID or 0 if not found. - using SdFontIdResolver = int (*)(void* ctx, const char* familyName, uint8_t fontSize); + using SdFontIdResolver = int (*)(void* ctx, const char* familyName, uint8_t pointSize); SdFontIdResolver sdFontIdResolver = nullptr; void* sdFontResolverCtx = nullptr; @@ -520,6 +564,34 @@ class CrossPointSettings { bool saveToFile() const; bool loadFromFile(); + static const char* getFilePath() { return "/.crosspoint/crossink-settings.json"; } + void toJson(JsonDocument& doc) const; + bool fromJson(JsonVariantConst doc); + + struct StatusBarSpec { + bool showChapterPageCount = false; + bool showBookProgressPercent = false; + bool showStablePageNumbers = false; + uint8_t titleMode = HIDE_TITLE; + uint8_t timeLeftMode = TIME_LEFT_HIDE; + bool showBattery = false; + bool showBatteryPercent = false; + bool showClock = false; + uint8_t progressBarMode = HIDE_PROGRESS; + uint8_t progressBarHeightPx = 0; + uint8_t xtcMode = XTC_STATUS_BAR_HIDE; + + bool textLaneVisible(bool clockAvailable) const { + return showChapterPageCount || showBookProgressPercent || showStablePageNumbers || titleMode != HIDE_TITLE || + timeLeftMode != TIME_LEFT_HIDE || showBattery || (showClock && clockAvailable); + } + bool showsProgressBar() const { return progressBarMode != HIDE_PROGRESS; } + bool showsTitle() const { return titleMode != HIDE_TITLE; } + }; + + StatusBarSpec statusBarSpec() const; + ReaderRenderSpec readerRenderSpec(uint16_t viewportWidth, uint16_t viewportHeight, + EpubRenderMode renderMode = EpubRenderMode::CrossInkDefault) const; static void validateFrontButtonMapping(CrossPointSettings& settings); static void validateReaderFrontButtonMapping(CrossPointSettings& settings); diff --git a/src/CrossPointState.cpp b/src/CrossPointState.cpp index c6d365852c..417d691f8a 100644 --- a/src/CrossPointState.cpp +++ b/src/CrossPointState.cpp @@ -1,8 +1,8 @@ #include "CrossPointState.h" #include -#include #include +#include #include #include @@ -15,8 +15,6 @@ constexpr char STATE_FILE_JSON[] = "/.crosspoint/state.json"; constexpr char STATE_FILE_BAK[] = "/.crosspoint/state.bin.bak"; } // namespace -CrossPointState CrossPointState::instance; - bool CrossPointState::isRecentSleep(uint16_t idx, uint8_t checkCount) const { const uint8_t effectiveCount = std::min(checkCount, recentSleepFill); for (uint8_t i = 0; i < effectiveCount; i++) { @@ -39,18 +37,21 @@ void CrossPointState::clearRecentSleepHistory() { } bool CrossPointState::saveToFile() const { - std::lock_guard lock(_mutex); - Storage.mkdir("/.crosspoint"); - return JsonSettingsIO::saveState(*this, STATE_FILE_JSON); + std::lock_guard storeLock(storeMutex); + std::lock_guard stateLock(_mutex); + JsonDocument doc; + toJson(doc); + return PersistableStoreBase::writeDocToFile(STATE_FILE_JSON, doc); } bool CrossPointState::loadFromFile() { // Try JSON first if (Storage.exists(STATE_FILE_JSON)) { - String json = Storage.readFile(STATE_FILE_JSON); - if (!json.isEmpty()) { - std::lock_guard lock(_mutex); - return JsonSettingsIO::loadState(*this, json.c_str()); + std::lock_guard storeLock(storeMutex); + JsonDocument doc; + if (PersistableStoreBase::readDocFromFile(STATE_FILE_JSON, doc)) { + std::lock_guard stateLock(_mutex); + return fromJson(doc.as()); } } @@ -71,6 +72,52 @@ bool CrossPointState::loadFromFile() { return false; } +void CrossPointState::toJson(JsonDocument& doc) const { + doc["openEpubPath"] = openEpubPath; + doc["favoriteSleepImagePath"] = favoriteSleepImagePath; + doc["preferredSleepFolderPath"] = preferredSleepFolderPath; + JsonArray recentArr = doc["recentSleepImages"].to(); + for (int i = 0; i < SLEEP_RECENT_COUNT; i++) recentArr.add(recentSleepImages[i]); + doc["recentSleepPos"] = recentSleepPos; + doc["recentSleepFill"] = recentSleepFill; + doc["readerActivityLoadCount"] = readerActivityLoadCount; + doc["lastSleepFromReader"] = lastSleepFromReader; + doc["pendingBookmarkSpine"] = pendingBookmarkSpine; + doc["pendingBookmarkProgress"] = pendingBookmarkProgress; + doc["pendingBookmarkParagraphIndex"] = pendingBookmarkParagraphIndex; + doc["pendingClippingIndex"] = pendingClippingIndex; + doc["showBootScreen"] = showBootScreen; +} + +bool CrossPointState::fromJson(JsonVariantConst doc) { + openEpubPath = doc["openEpubPath"] | ""; + favoriteSleepImagePath = doc["favoriteSleepImagePath"] | ""; + preferredSleepFolderPath = doc["preferredSleepFolderPath"] | ""; + std::fill_n(recentSleepImages, SLEEP_RECENT_COUNT, static_cast(0)); + JsonArrayConst recentArr = doc["recentSleepImages"]; + const int actualCount = + recentArr.isNull() ? 0 : std::min(static_cast(recentArr.size()), static_cast(SLEEP_RECENT_COUNT)); + for (int i = 0; i < actualCount; i++) recentSleepImages[i] = recentArr[i] | static_cast(0); + recentSleepPos = doc["recentSleepPos"] | static_cast(0); + if (recentSleepPos >= SLEEP_RECENT_COUNT) { + recentSleepPos = actualCount > 0 ? recentSleepPos % SLEEP_RECENT_COUNT : 0; + } + recentSleepFill = doc["recentSleepFill"] | static_cast(0); + recentSleepFill = static_cast(std::min(static_cast(recentSleepFill), actualCount)); + if (recentSleepFill == 0 && !doc["lastSleepImage"].isNull()) { + const uint8_t legacy = doc["lastSleepImage"] | static_cast(UINT8_MAX); + if (legacy != UINT8_MAX) pushRecentSleep(static_cast(legacy)); + } + readerActivityLoadCount = doc["readerActivityLoadCount"] | static_cast(0); + lastSleepFromReader = doc["lastSleepFromReader"] | false; + pendingBookmarkSpine = doc["pendingBookmarkSpine"] | static_cast(UINT16_MAX); + pendingBookmarkProgress = doc["pendingBookmarkProgress"] | static_cast(-1.0f); + pendingBookmarkParagraphIndex = doc["pendingBookmarkParagraphIndex"] | static_cast(UINT16_MAX); + pendingClippingIndex = doc["pendingClippingIndex"] | static_cast(UINT16_MAX); + showBootScreen = doc["showBootScreen"] | true; + return true; +} + bool CrossPointState::loadFromBinaryFile() { HalFile inputFile; if (!Storage.openFileForRead("CPS", STATE_FILE_BIN, inputFile)) { diff --git a/src/CrossPointState.h b/src/CrossPointState.h index b52ba92b2a..8d29f7754c 100644 --- a/src/CrossPointState.h +++ b/src/CrossPointState.h @@ -1,14 +1,16 @@ #pragma once +#include +#include + #include #include #include #include -class CrossPointState { +class CrossPointState : public PersistableStore { mutable std::mutex _mutex; - - // Static instance - static CrossPointState instance; + CrossPointState() = default; + friend class PersistableStore; public: // Access the state mutex for protecting multi-field reads/writes from other cores. @@ -34,12 +36,12 @@ class CrossPointState { void clearRecentSleepHistory(); ~CrossPointState() = default; - // Get singleton instance - static CrossPointState& getInstance() { return instance; } - bool saveToFile() const; bool loadFromFile(); + static const char* getFilePath() { return "/.crosspoint/state.json"; } + void toJson(JsonDocument& doc) const; + bool fromJson(JsonVariantConst doc); uint16_t pendingBookmarkSpine = UINT16_MAX; float pendingBookmarkProgress = -1.0f; uint16_t pendingBookmarkParagraphIndex = UINT16_MAX; diff --git a/src/FontInstaller.cpp b/src/FontInstaller.cpp index 3e431c1aec..bbe6347d3f 100644 --- a/src/FontInstaller.cpp +++ b/src/FontInstaller.cpp @@ -157,7 +157,10 @@ FontInstaller::Error FontInstaller::deleteFamily(const char* familyName) { return Error::OK; } -void FontInstaller::refreshRegistry() { registry_.discover(); } +bool FontInstaller::refreshRegistry() { + registry_.discover(); + return !registry_.lastDiscoveryFailed(); +} bool FontInstaller::isFamilyInstalled(const char* familyName) const { return registry_.findFamily(familyName) != nullptr; diff --git a/src/FontInstaller.h b/src/FontInstaller.h index d074bd157e..38b1991b15 100644 --- a/src/FontInstaller.h +++ b/src/FontInstaller.h @@ -45,7 +45,8 @@ class FontInstaller { Error deleteFamily(const char* familyName); /// Re-run registry discovery to pick up new/removed fonts. - void refreshRegistry(); + /// Returns false only when discovery could not complete due to low memory. + bool refreshRegistry(); /// Check whether a family name already exists in the registry. bool isFamilyInstalled(const char* familyName) const; diff --git a/src/GlobalActions.h b/src/GlobalActions.h index a860eddd93..00f5580cd7 100644 --- a/src/GlobalActions.h +++ b/src/GlobalActions.h @@ -27,6 +27,7 @@ inline bool isPowerButtonActionAvailableOutsideReader(const CrossPointSettings:: case CrossPointSettings::SHORT_PWRBTN::FOOTNOTES: case CrossPointSettings::SHORT_PWRBTN::FILE_BROWSER: case CrossPointSettings::SHORT_PWRBTN::CREATE_CLIPPING: + case CrossPointSettings::SHORT_PWRBTN::LOOKUP_WORD: case CrossPointSettings::SHORT_PWRBTN::SHORT_PWRBTN_COUNT: default: return false; diff --git a/src/JsonSettingsIO.cpp b/src/JsonSettingsIO.cpp deleted file mode 100644 index 8a8622c949..0000000000 --- a/src/JsonSettingsIO.cpp +++ /dev/null @@ -1,416 +0,0 @@ -#include "JsonSettingsIO.h" - -#include -#ifdef SIMULATOR -#include -#endif -#include -#include -#include - -#include -#include -#include -#include - -#include "CrossPointSettings.h" -#include "CrossPointState.h" -#include "OpdsServerStore.h" -#include "RecentBooksStore.h" -#include "SettingsList.h" -#include "WifiCredentialStore.h" - -// Convert legacy settings. -void applyLegacyStatusBarSettings(CrossPointSettings& settings) { - switch (static_cast(settings.statusBar)) { - case CrossPointSettings::NONE: - settings.statusBarChapterPageCount = 0; - settings.statusBarBookProgressPercentage = 0; - settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; - settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; - settings.statusBarBattery = 0; - break; - case CrossPointSettings::NO_PROGRESS: - settings.statusBarChapterPageCount = 0; - settings.statusBarBookProgressPercentage = 0; - settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; - settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; - settings.statusBarBattery = 1; - break; - case CrossPointSettings::BOOK_PROGRESS_BAR: - settings.statusBarChapterPageCount = 1; - settings.statusBarBookProgressPercentage = 0; - settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; - settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; - settings.statusBarBattery = 1; - break; - case CrossPointSettings::ONLY_BOOK_PROGRESS_BAR: - settings.statusBarChapterPageCount = 1; - settings.statusBarBookProgressPercentage = 0; - settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; - settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; - settings.statusBarBattery = 0; - break; - case CrossPointSettings::CHAPTER_PROGRESS_BAR: - settings.statusBarChapterPageCount = 0; - settings.statusBarBookProgressPercentage = 1; - settings.statusBarProgressBar = CrossPointSettings::CHAPTER_PROGRESS; - settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; - settings.statusBarBattery = 1; - break; - case CrossPointSettings::FULL: - default: - settings.statusBarChapterPageCount = 1; - settings.statusBarBookProgressPercentage = 1; - settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; - settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; - settings.statusBarBattery = 1; - break; - } -} - -bool isEnumRawValueAllowed(const SettingInfo& info, uint8_t value) { - if (info.enumRawValues.empty()) { - return value < settingEnumOptionCount(info); - } - return std::find(info.enumRawValues.begin(), info.enumRawValues.end(), value) != info.enumRawValues.end(); -} - -uint8_t defaultEnumRawValue(const SettingInfo& info, uint8_t fieldDefault) { - if (isEnumRawValueAllowed(info, fieldDefault)) { - return fieldDefault; - } - if (!info.enumRawValues.empty()) { - return info.enumRawValues.front(); - } - return 0; -} - -bool isSleepScreenSetting(const SettingInfo& info) { return info.key && strcmp(info.key, "sleepScreen") == 0; } - -constexpr uint8_t TILT_DIRECTION_SCHEMA_CURRENT = 2; - -uint8_t migrateTiltDirectionValue(uint8_t direction) { - if (direction == CrossPointSettings::TILT_LEFT_RIGHT) { - return CrossPointSettings::TILT_LEFT_RIGHT_INVERTED; - } - if (direction == CrossPointSettings::TILT_LEFT_RIGHT_INVERTED) { - return CrossPointSettings::TILT_LEFT_RIGHT; - } - return direction; -} - -// ---- CrossPointState ---- - -bool JsonSettingsIO::saveState(const CrossPointState& s, const char* path) { - JsonDocument doc; - doc["openEpubPath"] = s.openEpubPath; - doc["favoriteSleepImagePath"] = s.favoriteSleepImagePath; - doc["preferredSleepFolderPath"] = s.preferredSleepFolderPath; - JsonArray recentArr = doc["recentSleepImages"].to(); - for (int i = 0; i < CrossPointState::SLEEP_RECENT_COUNT; i++) recentArr.add(s.recentSleepImages[i]); - doc["recentSleepPos"] = s.recentSleepPos; - doc["recentSleepFill"] = s.recentSleepFill; - doc["readerActivityLoadCount"] = s.readerActivityLoadCount; - doc["lastSleepFromReader"] = s.lastSleepFromReader; - doc["pendingBookmarkSpine"] = s.pendingBookmarkSpine; - doc["pendingBookmarkProgress"] = s.pendingBookmarkProgress; - doc["pendingBookmarkParagraphIndex"] = s.pendingBookmarkParagraphIndex; - doc["pendingClippingIndex"] = s.pendingClippingIndex; - doc["showBootScreen"] = s.showBootScreen; - - String json; - serializeJson(doc, json); - return Storage.writeFile(path, json); -} - -bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) { - JsonDocument doc; - auto error = deserializeJson(doc, json); - if (error) { - LOG_ERR("CPS", "JSON parse error: %s", error.c_str()); - return false; - } - - s.openEpubPath = doc["openEpubPath"] | std::string(""); - s.favoriteSleepImagePath = doc["favoriteSleepImagePath"] | std::string(""); - s.preferredSleepFolderPath = doc["preferredSleepFolderPath"] | std::string(""); - memset(s.recentSleepImages, 0, sizeof(s.recentSleepImages)); - JsonArrayConst recentArr = doc["recentSleepImages"]; - const int actualCount = recentArr.isNull() ? 0 - : std::min(static_cast(recentArr.size()), - static_cast(CrossPointState::SLEEP_RECENT_COUNT)); - for (int i = 0; i < actualCount; i++) s.recentSleepImages[i] = recentArr[i] | static_cast(0); - s.recentSleepPos = doc["recentSleepPos"] | static_cast(0); - if (s.recentSleepPos >= CrossPointState::SLEEP_RECENT_COUNT) - s.recentSleepPos = actualCount > 0 ? s.recentSleepPos % CrossPointState::SLEEP_RECENT_COUNT : 0; - s.recentSleepFill = doc["recentSleepFill"] | static_cast(0); - s.recentSleepFill = static_cast(std::min(static_cast(s.recentSleepFill), actualCount)); - // Migrate legacy single-image field from old state.json (pre-recency-buffer). - // Only seeds the buffer if the new buffer is empty (fresh migration, not a resave). - if (s.recentSleepFill == 0 && !doc["lastSleepImage"].isNull()) { - const uint8_t legacy = doc["lastSleepImage"] | static_cast(UINT8_MAX); - if (legacy != UINT8_MAX) s.pushRecentSleep(static_cast(legacy)); - } - s.readerActivityLoadCount = doc["readerActivityLoadCount"] | static_cast(0); - s.lastSleepFromReader = doc["lastSleepFromReader"] | false; - s.pendingBookmarkSpine = doc["pendingBookmarkSpine"] | static_cast(UINT16_MAX); - s.pendingBookmarkProgress = doc["pendingBookmarkProgress"] | static_cast(-1.0f); - s.pendingBookmarkParagraphIndex = doc["pendingBookmarkParagraphIndex"] | static_cast(UINT16_MAX); - s.pendingClippingIndex = doc["pendingClippingIndex"] | static_cast(UINT16_MAX); - s.showBootScreen = doc["showBootScreen"] | true; - return true; -} - -// ---- CrossPointSettings ---- - -bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path) { - JsonDocument doc; - - for (const auto& info : getSettingsList()) { - if (!info.key) continue; - // Dynamic entries (KOReader etc.) are stored in their own files — skip. - if (!info.valuePtr && !info.stringOffset) continue; - - if (info.stringOffset) { - const char* strPtr = (const char*)&s + info.stringOffset; - if (info.obfuscated) { - doc[std::string(info.key) + "_obf"] = obfuscation::obfuscateToBase64(strPtr); - } else { - doc[info.key] = strPtr; - } - } else { - uint8_t value = s.*(info.valuePtr); - if (isSleepScreenSetting(info)) { - value = CrossPointSettings::sleepScreenModeToStorage(value); - } - doc[info.key] = value; - } - } - - // Front button remap — managed by RemapFrontButtons sub-activity, not in SettingsList. - doc["frontButtonBack"] = s.frontButtonBack; - doc["frontButtonConfirm"] = s.frontButtonConfirm; - doc["frontButtonLeft"] = s.frontButtonLeft; - doc["frontButtonRight"] = s.frontButtonRight; - // Reader-specific front button remap. - doc["readerFrontButtonsEnabled"] = s.readerFrontButtonsEnabled; - doc["readerFrontButtonBack"] = s.readerFrontButtonBack; - doc["readerFrontButtonConfirm"] = s.readerFrontButtonConfirm; - doc["readerFrontButtonLeft"] = s.readerFrontButtonLeft; - doc["readerFrontButtonRight"] = s.readerFrontButtonRight; - // Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it. - doc["fontFamily"] = s.fontFamily; - // SD card font family name — not in SettingsList, save manually - if (s.sdFontFamilyName[0] != '\0') { - doc["sdFontFamilyName"] = s.sdFontFamilyName; - } - - // Language -- managed by LanguageSelectActivity, not in SettingsList. - // Stored as ISO code string ("EN", "DE", ...) for stability across enum reorders. - doc["language"] = (s.language < getLanguageCount()) ? LANGUAGE_CODES[s.language] : "EN"; - doc["tiltPageTurnDirectionSchema"] = TILT_DIRECTION_SCHEMA_CURRENT; - // Separate from the legacy clock sync flag because older builds synced time - // only, leaving the RTC date registers at their placeholder/default value. - doc["clockDateHasBeenSynced"] = s.clockDateHasBeenSynced; - - String json; - serializeJson(doc, json); - return Storage.writeFile(path, json); -} - -bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool* needsResave) { - if (needsResave) *needsResave = false; - JsonDocument doc; - auto error = deserializeJson(doc, json); - if (error) { - LOG_ERR("CPS", "JSON parse error: %s", error.c_str()); - return false; - } - - auto clamp = [](uint8_t val, uint8_t maxVal, uint8_t def) -> uint8_t { return val < maxVal ? val : def; }; - const bool migrateLegacyTiltMode = !doc["tiltPageTurn"].isNull() && doc["tiltPageTurnDirection"].isNull(); - const uint8_t legacyTiltMode = doc["tiltPageTurn"] | static_cast(CrossPointSettings::TILT_OFF); - const bool migrateTiltDirectionSchema = - !doc["tiltPageTurnDirection"].isNull() && - ((doc["tiltPageTurnDirectionSchema"] | static_cast(1)) < TILT_DIRECTION_SCHEMA_CURRENT); - - // Legacy migration: if statusBarChapterPageCount is absent this is a pre-refactor settings file. - // Populate s with migrated values now so the generic loop below picks them up as defaults and clamps them. - if (doc["statusBarChapterPageCount"].isNull()) { - applyLegacyStatusBarSettings(s); - } - for (const auto& info : getSettingsList()) { - if (!info.key) continue; - // Dynamic entries (KOReader etc.) are stored in their own files — skip. - if (!info.valuePtr && !info.stringOffset) continue; - - if (info.stringOffset) { - const char* strPtr = (const char*)&s + info.stringOffset; - const std::string fieldDefault = strPtr; // current buffer = struct-initializer default - std::string val; - if (info.obfuscated) { - obfuscation::DecodeStatus status = obfuscation::DecodeStatus::INVALID; - val = obfuscation::deobfuscateFromBase64(doc[std::string(info.key) + "_obf"] | "", &status); - if (status == obfuscation::DecodeStatus::LEGACY && !val.empty() && needsResave) { - *needsResave = true; - } - if (status == obfuscation::DecodeStatus::INVALID || status == obfuscation::DecodeStatus::EMPTY || val.empty()) { - val = doc[info.key] | fieldDefault; - if (val != fieldDefault && needsResave) *needsResave = true; - } - } else { - val = doc[info.key] | fieldDefault; - } - char* destPtr = (char*)&s + info.stringOffset; - if (info.stringMaxLen == 0) { - LOG_ERR("CPS", "Misconfigured SettingInfo: stringMaxLen is 0 for key '%s'", info.key); - destPtr[0] = '\0'; - if (needsResave) *needsResave = true; - continue; - } - strncpy(destPtr, val.c_str(), info.stringMaxLen - 1); - destPtr[info.stringMaxLen - 1] = '\0'; - if (std::strcmp(info.key, "deviceName") == 0 && - std::strlen(destPtr) < CrossPointSettings::MIN_DEVICE_NAME_LENGTH) { - destPtr[0] = '\0'; - if (needsResave) *needsResave = true; - } - } else { - const uint8_t fieldDefault = s.*(info.valuePtr); // struct-initializer default, read before we overwrite it - uint8_t v = doc[info.key] | fieldDefault; - if (isSleepScreenSetting(info)) { - const uint8_t storedDefault = CrossPointSettings::sleepScreenModeToStorage(fieldDefault); - const uint8_t storedValue = doc[info.key] | storedDefault; - v = CrossPointSettings::sleepScreenStorageToMode(storedValue); - if (CrossPointSettings::sleepScreenModeToStorage(v) != storedValue && needsResave) *needsResave = true; - s.*(info.valuePtr) = v; - continue; - } - if (info.type == SettingType::ENUM) { - const bool isSdFontSize = info.key && strcmp(info.key, "fontSize") == 0 && - doc["sdFontFamilyName"].is() && - doc["sdFontFamilyName"].as()[0] != '\0'; - if (isSdFontSize && v < CrossPointSettings::SD_FONT_MAX_SIZE_STEPS) { - // Keep a saved SD-family size index even when this build's built-in - // font list has fewer choices; the registry-aware settings list will - // clamp it to the selected family once SD fonts are discovered. - } else if (!isEnumRawValueAllowed(info, v)) { - v = defaultEnumRawValue(info, fieldDefault); - if (needsResave) *needsResave = true; - } - } else if (info.type == SettingType::TOGGLE) { - v = clamp(v, (uint8_t)2, fieldDefault); - } else if (info.type == SettingType::VALUE) { - if (v < info.valueRange.min) - v = info.valueRange.min; - else if (v > info.valueRange.max) - v = info.valueRange.max; - } - s.*(info.valuePtr) = v; - } - } - - // Migration: preserve Minimal users' old two-line file browser default when the new - // fileBrowserDisplay setting is not present yet. - if (doc["fileBrowserDisplay"].isNull()) { - if (!doc["fileBrowserPreview"].isNull()) { - s.fileBrowserDisplay = - clamp(doc["fileBrowserPreview"] | static_cast(CrossPointSettings::FILE_BROWSER_DISPLAY_1_LINE), - static_cast(CrossPointSettings::FILE_BROWSER_DISPLAY_COUNT), - static_cast(CrossPointSettings::FILE_BROWSER_DISPLAY_1_LINE)); - if (needsResave) *needsResave = true; - } else if (s.uiTheme == CrossPointSettings::MINIMAL) { - s.fileBrowserDisplay = CrossPointSettings::FILE_BROWSER_DISPLAY_2_LINES; - if (needsResave) *needsResave = true; - } - } - - if (migrateLegacyTiltMode) { - if (legacyTiltMode == 1 || legacyTiltMode == 2) { - s.tiltPageTurn = CrossPointSettings::TILT_ON; - s.tiltPageTurnDirection = - legacyTiltMode == 2 ? CrossPointSettings::TILT_LEFT_RIGHT : CrossPointSettings::TILT_LEFT_RIGHT_INVERTED; - if (needsResave) *needsResave = true; - } else { - s.tiltPageTurn = CrossPointSettings::TILT_OFF; - } - } else if (migrateTiltDirectionSchema) { - s.tiltPageTurnDirection = migrateTiltDirectionValue(s.tiltPageTurnDirection); - if (needsResave) *needsResave = true; - } - - if (!doc["tiltPageTurnDirection"].isNull() && doc["tiltPageTurnDirectionSchema"].isNull()) { - if (needsResave) *needsResave = true; - } - - if (doc["hideClock"].isNull() && !doc["statusBarClock"].isNull()) { - constexpr uint8_t LEGACY_SHOW_CLOCK_NEVER = 0; - const uint8_t legacyShowClock = - clamp(doc["statusBarClock"] | LEGACY_SHOW_CLOCK_NEVER, - static_cast(CrossPointSettings::HIDE_CLOCK_MODE_COUNT), LEGACY_SHOW_CLOCK_NEVER); - s.hideClock = legacyShowClock == LEGACY_SHOW_CLOCK_NEVER ? CrossPointSettings::HIDE_CLOCK_ALWAYS - : CrossPointSettings::HIDE_CLOCK_NEVER; - if (needsResave) *needsResave = true; - } - - if (doc["sleepTimeoutMinutes"].isNull() && !doc["sleepTimeout"].isNull()) { - const uint8_t legacyValue = - clamp(doc["sleepTimeout"] | (uint8_t)CrossPointSettings::SLEEP_10_MIN, CrossPointSettings::SLEEP_TIMEOUT_COUNT, - (uint8_t)CrossPointSettings::SLEEP_10_MIN); - s.sleepTimeoutMinutes = CrossPointSettings::sleepTimeoutEnumToMinutes(legacyValue); - if (needsResave) *needsResave = true; - } - // Front button remap — managed by RemapFrontButtons sub-activity, not in SettingsList. - using S = CrossPointSettings; - s.frontButtonBack = - clamp(doc["frontButtonBack"] | (uint8_t)S::FRONT_HW_BACK, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_BACK); - s.frontButtonConfirm = clamp(doc["frontButtonConfirm"] | (uint8_t)S::FRONT_HW_CONFIRM, S::FRONT_BUTTON_HARDWARE_COUNT, - S::FRONT_HW_CONFIRM); - s.frontButtonLeft = - clamp(doc["frontButtonLeft"] | (uint8_t)S::FRONT_HW_LEFT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_LEFT); - s.frontButtonRight = - clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT); - CrossPointSettings::validateFrontButtonMapping(s); - // Reader-specific front button remap. - s.readerFrontButtonsEnabled = clamp(doc["readerFrontButtonsEnabled"] | (uint8_t)0, (uint8_t)2, (uint8_t)0); - s.readerFrontButtonBack = - clamp(doc["readerFrontButtonBack"] | (uint8_t)S::FRONT_HW_BACK, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_BACK); - s.readerFrontButtonConfirm = clamp(doc["readerFrontButtonConfirm"] | (uint8_t)S::FRONT_HW_CONFIRM, - S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_CONFIRM); - s.readerFrontButtonLeft = - clamp(doc["readerFrontButtonLeft"] | (uint8_t)S::FRONT_HW_LEFT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_LEFT); - s.readerFrontButtonRight = clamp(doc["readerFrontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, - S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT); - CrossPointSettings::validateReaderFrontButtonMapping(s); - - // Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it. - const uint8_t storedFontFamily = doc["fontFamily"] | (uint8_t)0; - s.fontFamily = clamp(storedFontFamily, CrossPointSettings::BUILTIN_FONT_COUNT, 0); - // SD card font family name — not in SettingsList, load manually - const char* sfn = doc["sdFontFamilyName"] | ""; - strncpy(s.sdFontFamilyName, sfn, sizeof(s.sdFontFamilyName) - 1); - s.sdFontFamilyName[sizeof(s.sdFontFamilyName) - 1] = '\0'; - if (storedFontFamily >= CrossPointSettings::BUILTIN_FONT_COUNT) { - if (needsResave) *needsResave = true; - } - - if (doc["lineHeightPercent"].isNull() && !doc["lineSpacing"].isNull()) { - const uint8_t legacyLineSpacing = clamp(doc["lineSpacing"] | static_cast(CrossPointSettings::NORMAL), - static_cast(CrossPointSettings::LINE_COMPRESSION_COUNT), - static_cast(CrossPointSettings::NORMAL)); - s.lineHeightPercent = - CrossPointSettings::legacyLineSpacingToPercent(legacyLineSpacing, s.fontFamily, s.sdFontFamilyName[0] != '\0'); - if (needsResave) *needsResave = true; - } - - // Language -- stored as code string for stability across enum reorders. - if (doc["language"].is()) { - s.language = static_cast(I18n::languageFromCode(doc["language"].as())); - } - s.clockDateHasBeenSynced = clamp(doc["clockDateHasBeenSynced"] | (uint8_t)0, (uint8_t)2, (uint8_t)0); - - LOG_DBG("CPS", "Settings loaded from file"); - - return true; -} diff --git a/src/JsonSettingsIO.h b/src/JsonSettingsIO.h deleted file mode 100644 index a85ddb9243..0000000000 --- a/src/JsonSettingsIO.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -class CrossPointSettings; -class CrossPointState; - -namespace JsonSettingsIO { - -// CrossPointSettings -bool saveSettings(const CrossPointSettings& s, const char* path); -bool loadSettings(CrossPointSettings& s, const char* json, bool* needsResave = nullptr); - -// CrossPointState -bool saveState(const CrossPointState& s, const char* path); -bool loadState(CrossPointState& s, const char* json); - -} // namespace JsonSettingsIO diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index ee42ac5fe4..221742f75b 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -1,14 +1,33 @@ #include "MappedInputManager.h" +#include + #include +#include #include #include "CrossPointSettings.h" #include "GlobalActions.h" +#if CROSSINK_APP_CAP_TOUCH +#include "components/TouchRegistry.h" +#endif +#include "components/UITheme.h" +#ifdef SIMULATOR +#include "simulator/SimulatorHomeKeyInput.h" +#endif namespace { using ButtonIndex = uint8_t; constexpr ButtonIndex kNoButton = UINT8_MAX; +constexpr float LEFT_EDGE_BACK_GESTURE_FRAC_X = 0.25f; +constexpr float BOTTOM_EDGE_HOME_GESTURE_FRAC_Y = 0.06f; +constexpr float READER_BOTTOM_EDGE_HOME_GESTURE_FRAC_Y = 0.12f; +constexpr float TOP_EDGE_MENU_GESTURE_FRAC_Y = 0.14f; +constexpr unsigned long TOUCH_HELD_OVERRIDE_WINDOW_MS = 250; + +constexpr float bottomEdgeHomeGestureFraction(const bool readerMode) { + return readerMode ? READER_BOTTOM_EDGE_HOME_GESTURE_FRAC_Y : BOTTOM_EDGE_HOME_GESTURE_FRAC_Y; +} struct SideLayoutMap { ButtonIndex pageBackPrimary; @@ -120,25 +139,14 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint const auto sideLayout = static_cast(SETTINGS.sideButtonLayout); const auto side = mapSideLayoutForReaderOrientation(kSideLayouts[sideLayout], readerMode); - const bool useReaderMapping = readerMode && SETTINGS.readerFrontButtonsEnabled; - const ButtonIndex btnBack = useReaderMapping ? SETTINGS.readerFrontButtonBack : SETTINGS.frontButtonBack; - const ButtonIndex btnConfirm = useReaderMapping ? SETTINGS.readerFrontButtonConfirm : SETTINGS.frontButtonConfirm; - const ButtonIndex btnLeft = useReaderMapping ? SETTINGS.readerFrontButtonLeft : SETTINGS.frontButtonLeft; - const ButtonIndex btnRight = useReaderMapping ? SETTINGS.readerFrontButtonRight : SETTINGS.frontButtonRight; - const ButtonIndex mappedBack = mapFrontButtonForReaderOrientation(btnBack, btnLeft, btnRight, readerMode); - const ButtonIndex mappedConfirm = mapFrontButtonForReaderOrientation(btnConfirm, btnLeft, btnRight, readerMode); - const ButtonIndex mappedLeft = mapFrontButtonForReaderOrientation(btnLeft, btnLeft, btnRight, readerMode); - const ButtonIndex mappedRight = mapFrontButtonForReaderOrientation(btnRight, btnLeft, btnRight, readerMode); + const ButtonIndex frontButton = mappedFrontButtonFor(button); switch (button) { case Button::Back: - return (gpio.*fn)(mappedBack); case Button::Confirm: - return (gpio.*fn)(mappedConfirm); case Button::Left: - return (gpio.*fn)(mappedLeft); case Button::Right: - return (gpio.*fn)(mappedRight); + return frontButton != kNoButton && (gpio.*fn)(frontButton); case Button::Up: // Reader menus should follow the same top/bottom side-button orientation as reader page turns. return (gpio.*fn)(mapSideButtonForReaderOrientation(HalGPIO::BTN_UP, readerMode)); @@ -159,6 +167,31 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint return false; } +uint8_t MappedInputManager::mappedFrontButtonFor(const Button button) const { + const bool useReaderMapping = readerMode && SETTINGS.readerFrontButtonsEnabled; + const ButtonIndex btnBack = useReaderMapping ? SETTINGS.readerFrontButtonBack : SETTINGS.frontButtonBack; + const ButtonIndex btnConfirm = useReaderMapping ? SETTINGS.readerFrontButtonConfirm : SETTINGS.frontButtonConfirm; + const ButtonIndex btnLeft = useReaderMapping ? SETTINGS.readerFrontButtonLeft : SETTINGS.frontButtonLeft; + const ButtonIndex btnRight = useReaderMapping ? SETTINGS.readerFrontButtonRight : SETTINGS.frontButtonRight; + const ButtonIndex mappedBack = mapFrontButtonForReaderOrientation(btnBack, btnLeft, btnRight, readerMode); + const ButtonIndex mappedConfirm = mapFrontButtonForReaderOrientation(btnConfirm, btnLeft, btnRight, readerMode); + const ButtonIndex mappedLeft = mapFrontButtonForReaderOrientation(btnLeft, btnLeft, btnRight, readerMode); + const ButtonIndex mappedRight = mapFrontButtonForReaderOrientation(btnRight, btnLeft, btnRight, readerMode); + + switch (button) { + case Button::Back: + return mappedBack; + case Button::Confirm: + return mappedConfirm; + case Button::Left: + return mappedLeft; + case Button::Right: + return mappedRight; + default: + return kNoButton; + } +} + bool MappedInputManager::shouldUsePowerAsConfirmFallback() const { return !readerMode || powerAsConfirmInReaderMode; } bool MappedInputManager::shouldMirrorPowerAsConfirmHold() const { @@ -166,6 +199,404 @@ bool MappedInputManager::shouldMirrorPowerAsConfirmHold() const { !isPowerButtonActionAvailableOutsideReader(static_cast(SETTINGS.longPwrBtn)); } +#if CROSSINK_APP_CAP_TOUCH +bool MappedInputManager::touchInputEnabled() const { + return gpio.hasTouch() && (!readerMode || !SETTINGS.disableReaderTouchscreen || readerTouchscreenOverride); +} + +bool MappedInputManager::hasTouch() const { return touchInputEnabled(); } + +bool MappedInputManager::hasTouchHardware() const { return gpio.hasTouch(); } + +void MappedInputManager::rememberTouchHeldTime() const { + touchHeldOverrideValid = true; + touchHeldOverrideMs = gpio.lastTouchHeldMs(); + touchHeldOverrideAt = millis(); +} + +bool MappedInputManager::wasScreenTapped(int& x, int& y) const { + if (!touchInputEnabled()) return false; + if (suppressTouchTap) { + // The release edge may be observed after the new activity's first render. Clear + // on either edge so a missed release cannot suppress a later deliberate tap. + if (gpio.wasTouchActivity()) suppressTouchTap = false; + return false; + } +#ifdef SIMULATOR + if (simulatorTouch.releasedThisFrame) { + x = simulatorTouch.startX; + y = simulatorTouch.startY; + rememberTouchHeldTime(); + return true; + } +#endif + float nx = 0.0f; + float ny = 0.0f; + if (!gpio.wasTouchTap(nx, ny)) return false; + renderer.tapToLogical(nx, ny, x, y); + rememberTouchHeldTime(); + return true; +} + +bool MappedInputManager::wasScreenTapped(int& x, int& y, unsigned long& heldMs) const { + if (!wasScreenTapped(x, y)) return false; + heldMs = touchHeldOverrideMs; + return true; +} + +bool MappedInputManager::isScreenTouchLongPress(int& x, int& y, const unsigned long thresholdMs) const { + unsigned long heldMs = 0; + return isScreenTouchTapCandidate(x, y, heldMs) && heldMs >= thresholdMs; +} + +bool MappedInputManager::isInVerticalEdgeGestureZone(const int y) const { + const int screenHeight = renderer.getScreenHeight(); + if (screenHeight <= 0) return false; + const int topEdgeBottom = static_cast(screenHeight * TOP_EDGE_MENU_GESTURE_FRAC_Y); + const int bottomEdgeTop = screenHeight - static_cast(screenHeight * bottomEdgeHomeGestureFraction(readerMode)); + return y <= topEdgeBottom || y >= bottomEdgeTop; +} + +bool MappedInputManager::wasScreenTouchDown(int& x, int& y) const { + if (!touchInputEnabled()) return false; +#ifdef SIMULATOR + if (simulatorTouch.pressedThisFrame) { + x = simulatorTouch.startX; + y = simulatorTouch.startY; + return true; + } +#endif + float nx = 0.0f; + float ny = 0.0f; + if (!gpio.wasTouchDown(nx, ny)) return false; + renderer.tapToLogical(nx, ny, x, y); + return true; +} + +bool MappedInputManager::isScreenTouchTapCandidate(int& x, int& y, unsigned long& heldMs) const { + if (!touchInputEnabled()) return false; +#ifdef SIMULATOR + if (simulatorTouch.pressed) { + x = simulatorTouch.startX; + y = simulatorTouch.startY; + heldMs = millis() - simulatorTouch.startedAt; + return true; + } +#endif + float nx = 0.0f; + float ny = 0.0f; + if (!gpio.isTouchTapCandidate(nx, ny, heldMs)) return false; + renderer.tapToLogical(nx, ny, x, y); + return true; +} + +bool MappedInputManager::isScreenTouchHeld(int& x, int& y) const { + if (!touchInputEnabled()) return false; +#ifdef SIMULATOR + if (simulatorTouch.pressed) { + x = simulatorTouch.currentX; + y = simulatorTouch.currentY; + return true; + } +#endif + // Live contact position while the finger is down (no tap-slop gate) — drag tracking. + float nx = 0.0f; + float ny = 0.0f; + if (!gpio.isTouchHeldAt(nx, ny)) return false; + renderer.tapToLogical(nx, ny, x, y); + return true; +} + +bool MappedInputManager::wasRegistryTargetTapped(const uint8_t kind, int& id) const { + int tx = 0; + int ty = 0; + return wasScreenTapped(tx, ty) && + TouchRegistry::getInstance().hitTest(tx, ty, static_cast(kind), id); +} + +bool MappedInputManager::wasRegistryTargetTouchedDown(const uint8_t kind, int& id) const { + int tx = 0; + int ty = 0; + return wasScreenTouchDown(tx, ty) && + TouchRegistry::getInstance().hitTest(tx, ty, static_cast(kind), id); +} + +bool MappedInputManager::wasItemTapped(int& id) const { return wasRegistryTargetTapped(TouchRegistry::Kind::Item, id); } + +bool MappedInputManager::wasItemTouchedDown(int& id) const { + return wasRegistryTargetTouchedDown(TouchRegistry::Kind::Item, id); +} + +bool MappedInputManager::wasTabTapped(int& id) const { return wasRegistryTargetTapped(TouchRegistry::Kind::Tab, id); } + +bool MappedInputManager::wasTabTouchedDown(int& id) const { + return wasRegistryTargetTouchedDown(TouchRegistry::Kind::Tab, id); +} + +bool MappedInputManager::wasCoverTapped(int& id) const { + return wasRegistryTargetTapped(TouchRegistry::Kind::Cover, id); +} + +bool MappedInputManager::wasCoverTouchedDown(int& id) const { + return wasRegistryTargetTouchedDown(TouchRegistry::Kind::Cover, id); +} + +bool MappedInputManager::wasFrontButtonHintTapped(const uint8_t buttonIndex) const { + int id = -1; + return wasRegistryTargetTapped(TouchRegistry::Kind::Button, id) && id == buttonIndex; +} + +bool MappedInputManager::wasFrontButtonHintTouchedDown(const uint8_t buttonIndex) const { + int id = -1; + return wasRegistryTargetTouchedDown(TouchRegistry::Kind::Button, id) && id == buttonIndex; +} + +bool MappedInputManager::wasScreenTouchReleased() const { return gpio.wasTouchReleased(); } + +bool MappedInputManager::wasTapInRect(const int x, const int y, const int width, const int height) const { + int tx = 0; + int ty = 0; + return wasScreenTapped(tx, ty) && tx >= x && tx < x + width && ty >= y && ty < y + height; +} + +bool MappedInputManager::listItemFromPoint(const int x, const int y, int& index, const int itemCount, + const int selectedIndex, const int listTop, const int listHeight, + const bool hasSubtitle) const { + (void)x; + if (itemCount <= 0) return false; + if (y < listTop || y >= listTop + listHeight) return false; + + const auto& theme = UITheme::getInstance().getTheme(); + const int rowStep = theme.getListRowStep(hasSubtitle); + if (rowStep <= 0) return false; + + const int pageItems = theme.getListPageItems(listHeight, hasSubtitle); + if (pageItems <= 0) return false; + const int pageStart = std::max(0, selectedIndex / pageItems) * pageItems; + const int row = (y - listTop) / rowStep; + const int tapped = pageStart + row; + if (row < 0 || row >= pageItems || tapped >= itemCount) return false; + index = tapped; + return true; +} + +bool MappedInputManager::wasListItemTapped(int& index, const int itemCount, const int selectedIndex, const int listTop, + const int listHeight, const bool hasSubtitle) const { + int tx = 0; + int ty = 0; + if (!wasScreenTapped(tx, ty)) return false; + if (TouchRegistry::getInstance().hitTest(tx, ty, TouchRegistry::Item, index) && index >= 0 && index < itemCount) { + return true; + } + return listItemFromPoint(tx, ty, index, itemCount, selectedIndex, listTop, listHeight, hasSubtitle); +} + +bool MappedInputManager::wasListItemTouchedDown(int& index, const int itemCount, const int selectedIndex, + const int listTop, const int listHeight, const bool hasSubtitle) const { + int tx = 0; + int ty = 0; + if (!wasScreenTouchDown(tx, ty)) return false; + if (TouchRegistry::getInstance().hitTest(tx, ty, TouchRegistry::Item, index) && index >= 0 && index < itemCount) { + return true; + } + return listItemFromPoint(tx, ty, index, itemCount, selectedIndex, listTop, listHeight, hasSubtitle); +} + +bool MappedInputManager::isListItemTouchLongPressed(int& index, const int itemCount, const int selectedIndex, + const int listTop, const int listHeight, const bool hasSubtitle, + const unsigned long thresholdMs) const { + int tx = 0; + int ty = 0; + if (!isScreenTouchLongPress(tx, ty, thresholdMs)) return false; + if (TouchRegistry::getInstance().hitTest(tx, ty, TouchRegistry::Item, index) && index >= 0 && index < itemCount) { + return true; + } + return listItemFromPoint(tx, ty, index, itemCount, selectedIndex, listTop, listHeight, hasSubtitle); +} + +MappedInputManager::RowTouch MappedInputManager::rowTouch(int& row, const int top, const int rowStep, + const int rowCount, const int xStart, const int xEnd, + const int rowHeight) const { + if (rowStep <= 0 || rowCount <= 0) return RowTouch::None; + const auto hit = [&](const int x, const int y) { + if (x < xStart || x >= xEnd || y < top) return false; + const int r = (y - top) / rowStep; + if (r >= rowCount) return false; + if (rowHeight > 0 && (y - top) % rowStep >= rowHeight) return false; + row = r; + return true; + }; + int x = 0; + int y = 0; + if (wasScreenTouchDown(x, y) && hit(x, y)) return RowTouch::Down; + if (wasScreenTapped(x, y) && hit(x, y)) return RowTouch::Tap; + return RowTouch::None; +} + +MappedInputManager::RowTouch MappedInputManager::colTouch(int& col, const int left, const int colStep, + const int colCount, const int yStart, const int yEnd, + const int colWidth) const { + if (colStep <= 0 || colCount <= 0) return RowTouch::None; + const auto hit = [&](const int x, const int y) { + if (y < yStart || y >= yEnd || x < left) return false; + const int c = (x - left) / colStep; + if (c >= colCount) return false; + if (colWidth > 0 && (x - left) % colStep >= colWidth) return false; + col = c; + return true; + }; + int x = 0; + int y = 0; + if (wasScreenTouchDown(x, y) && hit(x, y)) return RowTouch::Down; + if (wasScreenTapped(x, y) && hit(x, y)) return RowTouch::Tap; + return RowTouch::None; +} + +bool MappedInputManager::decodeSwipe(int& sx, int& sy, int& ex, int& ey) const { + if (!touchInputEnabled()) return false; +#ifdef SIMULATOR + if (simulatorTouch.releasedThisFrame) { + sx = simulatorTouch.startX; + sy = simulatorTouch.startY; + ex = simulatorTouch.currentX; + ey = simulatorTouch.currentY; + return sx != ex || sy != ey; + } +#endif + float nxs = 0.0f; + float nys = 0.0f; + float nxe = 0.0f; + float nye = 0.0f; + if (!gpio.wasSwipe(nxs, nys, nxe, nye)) return false; + renderer.tapToLogical(nxs, nys, sx, sy); + renderer.tapToLogical(nxe, nye, ex, ey); + return true; +} + +bool MappedInputManager::wasSwipeWithPoints(SwipeDir& direction, int& startX, int& startY, int& endX, int& endY) const { + if (!decodeSwipe(startX, startY, endX, endY)) { + direction = SwipeDir::None; + return false; + } + + const int dx = endX - startX; + const int dy = endY - startY; + if (std::abs(dx) >= std::abs(dy)) { + direction = dx < 0 ? SwipeDir::Left : SwipeDir::Right; + } else { + direction = dy < 0 ? SwipeDir::Up : SwipeDir::Down; + } + return true; +} + +MappedInputManager::SwipeDir MappedInputManager::wasSwipe() const { + SwipeDir direction = SwipeDir::None; + int sx = 0; + int sy = 0; + int ex = 0; + int ey = 0; + wasSwipeWithPoints(direction, sx, sy, ex, ey); + return direction; +} + +bool MappedInputManager::wasBackGesture() const { + if (!touchInputEnabled()) return false; + // Back = left-to-right swipe starting near the left edge. Edge-anchored so that + // mid-screen horizontal swipes stay available to activities that consume + // SwipeDir::Left/Right (e.g. percent selection, image viewer). + int sx = 0; + int sy = 0; + int ex = 0; + int ey = 0; + if (!decodeSwipe(sx, sy, ex, ey)) return false; + const bool hit = sx <= renderer.getScreenWidth() * LEFT_EDGE_BACK_GESTURE_FRAC_X && ex > sx && + std::abs(ex - sx) > std::abs(ey - sy); + if (hit) rememberTouchHeldTime(); + return hit; +} + +bool MappedInputManager::wasLeftEdgeGesture() const { return wasBackGesture(); } + +bool MappedInputManager::hasHomeKeyHardware() const { +#ifdef SIMULATOR +#ifdef SIMULATOR_DEVICE_X4PRO + return true; +#else + return false; +#endif +#else + return gpio.hasHomeKey(); +#endif +} + +bool MappedInputManager::wasTopEdgeDownSwipe() const { + // Downward swipe starting at the top edge (mirror of the bottom-edge swipe). + int sx = 0; + int sy = 0; + int ex = 0; + int ey = 0; + if (!decodeSwipe(sx, sy, ex, ey)) return false; + const int topEdgeBottom = static_cast(renderer.getScreenHeight() * TOP_EDGE_MENU_GESTURE_FRAC_Y); + const bool hit = sy <= topEdgeBottom && ey > sy && std::abs(ey - sy) > std::abs(ex - sx); + if (hit) rememberTouchHeldTime(); + return hit; +} + +bool MappedInputManager::wasBottomEdgeUpSwipe() const { + int sx = 0; + int sy = 0; + int ex = 0; + int ey = 0; + if (decodeSwipe(sx, sy, ex, ey)) { + const int screenHeight = renderer.getScreenHeight(); + const int bottomEdgeTop = screenHeight - static_cast(screenHeight * bottomEdgeHomeGestureFraction(readerMode)); + if (sy >= bottomEdgeTop && ey < sy && std::abs(ey - sy) > std::abs(ex - sx)) { + rememberTouchHeldTime(); + return true; + } + } + return false; +} + +// Home-key boards (X4 Pro) rearrange the edge gestures: the capacitive key +// takes over "exit to home", the bottom edge (freed by the key) becomes the +// menu gesture, and the top edge opens the frontlight quick panel. +bool MappedInputManager::wasMenuGesture() const { + return hasHomeKeyHardware() ? wasBottomEdgeUpSwipe() : wasTopEdgeDownSwipe(); +} + +bool MappedInputManager::wasReaderMenuGesture() const { + const SwipeDir direction = wasSwipe(); + return hasHomeKeyHardware() ? direction == SwipeDir::Up : direction == SwipeDir::Down; +} + +bool MappedInputManager::wasReaderHomeGesture() const { return !hasHomeKeyHardware() && wasSwipe() == SwipeDir::Up; } + +bool MappedInputManager::wasReaderLightPanelGesture() const { + return hasHomeKeyHardware() && wasSwipe() == SwipeDir::Down; +} + +bool MappedInputManager::wasHomeGesture() const { + if (!hasHomeKeyHardware()) return wasBottomEdgeUpSwipe(); +#ifdef SIMULATOR + return simulatorHomeKeyInput.wasTapped(); +#else + return gpio.wasHomeKeyTapped(); +#endif +} + +bool MappedInputManager::wasReaderMenuHold() const { + if (!hasHomeKeyHardware()) return false; +#ifdef SIMULATOR + return simulatorHomeKeyInput.wasLongPressed(); +#else + return gpio.wasHomeKeyLongPressed(); +#endif +} + +bool MappedInputManager::wasLightPanelGesture() const { return hasHomeKeyHardware() && wasTopEdgeDownSwipe(); } +#endif + bool MappedInputManager::wasPressed(const Button button) const { #ifdef SIMULATOR if (simulatorPressed[buttonIndex(button)]) { @@ -177,6 +608,9 @@ bool MappedInputManager::wasPressed(const Button button) const { if (mapButton(button, &HalGPIO::wasPressed)) { return true; } + if (wasFrontButtonHintTouchedDown(mappedFrontButtonFor(button))) { + return true; + } return shouldUsePowerAsConfirmFallback() && !isPowerButtonActionAvailableOutsideReader( @@ -184,7 +618,13 @@ bool MappedInputManager::wasPressed(const Button button) const { gpio.wasPressed(HalGPIO::BTN_POWER); } - return mapButton(button, &HalGPIO::wasPressed); + if (button == Button::Back && wasBackGesture()) { + return true; + } + + const uint8_t frontButton = mappedFrontButtonFor(button); + return mapButton(button, &HalGPIO::wasPressed) || + (frontButton != kNoButton && wasFrontButtonHintTouchedDown(frontButton)); } bool MappedInputManager::wasReleased(const Button button) const { @@ -195,9 +635,19 @@ bool MappedInputManager::wasReleased(const Button button) const { #endif if (button == Button::Back) { + if (wasBackGesture()) { + return true; + } + +#if CROSSINK_APP_CAP_TOUCH + if (!mapButton(button, &HalGPIO::wasReleased) && !wasFrontButtonHintTapped(mappedFrontButtonFor(button))) { + return false; + } +#else if (!mapButton(button, &HalGPIO::wasReleased)) { return false; } +#endif if (suppressBackRelease) { suppressBackRelease = false; @@ -208,7 +658,7 @@ bool MappedInputManager::wasReleased(const Button button) const { } if (button == Button::Confirm) { - if (mapButton(button, &HalGPIO::wasReleased)) { + if (mapButton(button, &HalGPIO::wasReleased) || wasFrontButtonHintTapped(mappedFrontButtonFor(button))) { if (suppressConfirmRelease) { suppressConfirmRelease = false; return false; @@ -250,7 +700,9 @@ bool MappedInputManager::wasReleased(const Button button) const { return true; } - return mapButton(button, &HalGPIO::wasReleased); + const uint8_t frontButton = mappedFrontButtonFor(button); + return mapButton(button, &HalGPIO::wasReleased) || + (frontButton != kNoButton && wasFrontButtonHintTapped(frontButton)); } bool MappedInputManager::isPressed(const Button button) const { @@ -282,6 +734,12 @@ bool MappedInputManager::wasAnyPressed() const { if (std::any_of(simulatorPressed.begin(), simulatorPressed.end(), [](bool pressed) { return pressed; })) { return true; } +#endif +#if CROSSINK_APP_CAP_TOUCH + int id = -1; + if (wasRegistryTargetTouchedDown(TouchRegistry::Kind::Button, id)) { + return true; + } #endif return gpio.wasAnyPressed(); } @@ -291,12 +749,26 @@ bool MappedInputManager::wasAnyReleased() const { if (std::any_of(simulatorReleased.begin(), simulatorReleased.end(), [](bool released) { return released; })) { return true; } +#endif +#if CROSSINK_APP_CAP_TOUCH + int id = -1; + if (wasRegistryTargetTapped(TouchRegistry::Kind::Button, id)) { + return true; + } #endif return gpio.wasAnyReleased(); } unsigned long MappedInputManager::getHeldTime() const { unsigned long heldTime = gpio.getHeldTime(); +#if CROSSINK_APP_CAP_TOUCH + if (!gpio.wasAnyPressed() && !gpio.wasAnyReleased() && touchHeldOverrideValid && + millis() - touchHeldOverrideAt <= TOUCH_HELD_OVERRIDE_WINDOW_MS) { + heldTime = touchHeldOverrideMs; + } else { + touchHeldOverrideValid = false; + } +#endif #ifdef SIMULATOR const unsigned long now = millis(); for (size_t i = 0; i < BUTTON_COUNT; i++) { @@ -348,6 +820,13 @@ int MappedInputManager::getPressedFrontButton() const { if (gpio.wasPressed(HalGPIO::BTN_RIGHT)) { return HalGPIO::BTN_RIGHT; } +#if CROSSINK_APP_CAP_TOUCH + int id = -1; + if (wasRegistryTargetTouchedDown(TouchRegistry::Kind::Button, id) && id >= HalGPIO::BTN_BACK && + id <= HalGPIO::BTN_RIGHT) { + return id; + } +#endif return -1; } @@ -366,6 +845,12 @@ int MappedInputManager::getReleasedFrontButton() const { if (gpio.wasReleased(HalGPIO::BTN_RIGHT)) { return HalGPIO::BTN_RIGHT; } +#if CROSSINK_APP_CAP_TOUCH + int id = -1; + if (wasRegistryTargetTapped(TouchRegistry::Kind::Button, id) && id >= HalGPIO::BTN_BACK && id <= HalGPIO::BTN_RIGHT) { + return id; + } +#endif return -1; } @@ -390,5 +875,36 @@ void MappedInputManager::simulatorInjectRelease(Button button) { void MappedInputManager::simulatorClearInputFrame() { simulatorPressed.fill(false); simulatorReleased.fill(false); +#if CROSSINK_APP_CAP_TOUCH + simulatorTouch.pressedThisFrame = false; + simulatorTouch.releasedThisFrame = false; +#endif +} + +#if CROSSINK_APP_CAP_TOUCH +void MappedInputManager::simulatorInjectTouchDown(const int x, const int y) { + simulatorTouch.pressed = true; + simulatorTouch.pressedThisFrame = true; + simulatorTouch.releasedThisFrame = false; + simulatorTouch.startX = x; + simulatorTouch.startY = y; + simulatorTouch.currentX = x; + simulatorTouch.currentY = y; + simulatorTouch.startedAt = millis(); +} + +void MappedInputManager::simulatorInjectTouchMove(const int x, const int y) { + if (!simulatorTouch.pressed) return; + simulatorTouch.currentX = x; + simulatorTouch.currentY = y; } + +void MappedInputManager::simulatorInjectTouchRelease(const int x, const int y) { + if (!simulatorTouch.pressed) return; + simulatorTouch.currentX = x; + simulatorTouch.currentY = y; + simulatorTouch.pressed = false; + simulatorTouch.releasedThisFrame = true; +} +#endif #endif diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index 4ff6e65dad..69f181244a 100644 --- a/src/MappedInputManager.h +++ b/src/MappedInputManager.h @@ -4,10 +4,13 @@ #include +class GfxRenderer; + class MappedInputManager { public: enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward }; static constexpr size_t BUTTON_COUNT = static_cast(Button::PageForward) + 1; + enum class SwipeDir { None, Left, Right, Up, Down }; struct Labels { const char* btn1; @@ -16,12 +19,17 @@ class MappedInputManager { const char* btn4; }; - explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {} + explicit MappedInputManager(HalGPIO& gpio, const GfxRenderer& renderer) : gpio(gpio), renderer(renderer) {} // Enable/disable reader-specific front button mapping. // Call with true in reader activity onEnter(), false in onExit(). void setReaderMode(bool enabled) { readerMode = enabled; } void setPowerAsConfirmInReaderMode(bool enabled) { powerAsConfirmInReaderMode = enabled; } +#if CROSSINK_APP_CAP_TOUCH + void setReaderTouchscreenOverride(bool enabled) { readerTouchscreenOverride = enabled; } +#else + constexpr void setReaderTouchscreenOverride(bool) {} +#endif void update() const { gpio.update(); } void suppressNextBackRelease() { suppressBackRelease = true; } @@ -31,6 +39,102 @@ class MappedInputManager { bool wasPressed(Button button) const; bool wasReleased(Button button) const; bool isPressed(Button button) const; + const GfxRenderer& getRenderer() const { return renderer; } + enum class RowTouch : uint8_t { None, Down, Tap }; +#if CROSSINK_APP_CAP_TOUCH + bool hasTouch() const; + bool hasTouchHardware() const; + // True on boards with a capacitive home key (X4 Pro), where the bottom-edge + // up-swipe is the reader-menu gesture rather than the exit-to-home gesture. + bool hasHomeKey() const { return hasHomeKeyHardware(); } + bool wasScreenTapped(int& x, int& y) const; + // Also reports how long the finger was held before release. + bool wasScreenTapped(int& x, int& y, unsigned long& heldMs) const; + bool isScreenTouchLongPress(int& x, int& y, unsigned long thresholdMs) const; + bool isInVerticalEdgeGestureZone(int y) const; + bool wasScreenTouchDown(int& x, int& y) const; + bool isScreenTouchTapCandidate(int& x, int& y, unsigned long& heldMs) const; + bool isScreenTouchHeld(int& x, int& y) const; + bool wasItemTapped(int& id) const; + bool wasItemTouchedDown(int& id) const; + bool wasTabTapped(int& id) const; + bool wasTabTouchedDown(int& id) const; + bool wasCoverTapped(int& id) const; + bool wasCoverTouchedDown(int& id) const; + // Raw release edge, also true when the contact ended in a swipe or drag-off + // (which wasScreenTapped never reports). InputSnapshot builders forward it + // off-target so FreeInkUI routing clears its pressed-element state. + bool wasScreenTouchReleased() const; + bool wasTapInRect(int x, int y, int width, int height) const; + bool wasListItemTapped(int& index, int itemCount, int selectedIndex, int listTop, int listHeight, + bool hasSubtitle) const; + bool wasListItemTouchedDown(int& index, int itemCount, int selectedIndex, int listTop, int listHeight, + bool hasSubtitle) const; + bool isListItemTouchLongPressed(int& index, int itemCount, int selectedIndex, int listTop, int listHeight, + bool hasSubtitle, unsigned long thresholdMs) const; + void suppressNextTouchTap() { suppressTouchTap = true; } + RowTouch rowTouch(int& row, int top, int rowStep, int rowCount, int xStart = 0, int xEnd = INT32_MAX, + int rowHeight = 0) const; + RowTouch colTouch(int& col, int left, int colStep, int colCount, int yStart, int yEnd, int colWidth = 0) const; + SwipeDir wasSwipe() const; + bool wasSwipeWithPoints(SwipeDir& direction, int& startX, int& startY, int& endX, int& endY) const; + bool wasLeftEdgeGesture() const; + // Exit-to-home intent. Boards with a capacitive home key (X4 Pro) use the + // key's press edge; everywhere else it's the bottom-edge up-swipe. + bool wasHomeGesture() const; + // Contextual menu intent (the reader menu). Home-key boards move this to the + // bottom-edge up-swipe (freed by the home key); others keep the top-edge + // down-swipe. + bool wasMenuGesture() const; + // Frontlight quick panel: top-edge down-swipe, only on home-key boards where + // that edge is no longer the menu gesture. + bool wasLightPanelGesture() const; + // Full-screen vertical reader gestures. These preserve each board's existing + // action mapping, but intentionally do not apply to reader submenus or lists. + bool wasReaderMenuGesture() const; + bool wasReaderHomeGesture() const; + bool wasReaderLightPanelGesture() const; + // Reader-menu shortcut: a long press of the capacitive home key. + bool wasReaderMenuHold() const; +#else + constexpr bool hasTouch() const { return false; } + constexpr bool hasTouchHardware() const { return false; } + constexpr bool hasHomeKey() const { return false; } + constexpr bool wasScreenTapped(int&, int&) const { return false; } + constexpr bool wasScreenTapped(int&, int&, unsigned long&) const { return false; } + constexpr bool isScreenTouchLongPress(int&, int&, unsigned long) const { return false; } + constexpr bool isInVerticalEdgeGestureZone(int) const { return false; } + constexpr bool wasScreenTouchDown(int&, int&) const { return false; } + constexpr bool isScreenTouchTapCandidate(int&, int&, unsigned long&) const { return false; } + constexpr bool isScreenTouchHeld(int&, int&) const { return false; } + constexpr bool wasItemTapped(int&) const { return false; } + constexpr bool wasItemTouchedDown(int&) const { return false; } + constexpr bool wasTabTapped(int&) const { return false; } + constexpr bool wasTabTouchedDown(int&) const { return false; } + constexpr bool wasCoverTapped(int&) const { return false; } + constexpr bool wasCoverTouchedDown(int&) const { return false; } + constexpr bool wasScreenTouchReleased() const { return false; } + constexpr bool wasTapInRect(int, int, int, int) const { return false; } + constexpr bool wasListItemTapped(int&, int, int, int, int, bool) const { return false; } + constexpr bool wasListItemTouchedDown(int&, int, int, int, int, bool) const { return false; } + constexpr bool isListItemTouchLongPressed(int&, int, int, int, int, bool, unsigned long) const { return false; } + constexpr void suppressNextTouchTap() {} + constexpr RowTouch rowTouch(int&, int, int, int, int = 0, int = INT32_MAX, int = 0) const { return RowTouch::None; } + constexpr RowTouch colTouch(int&, int, int, int, int, int, int = 0) const { return RowTouch::None; } + constexpr SwipeDir wasSwipe() const { return SwipeDir::None; } + constexpr bool wasSwipeWithPoints(SwipeDir& direction, int&, int&, int&, int&) const { + direction = SwipeDir::None; + return false; + } + constexpr bool wasLeftEdgeGesture() const { return false; } + constexpr bool wasHomeGesture() const { return false; } + constexpr bool wasMenuGesture() const { return false; } + constexpr bool wasLightPanelGesture() const { return false; } + constexpr bool wasReaderMenuGesture() const { return false; } + constexpr bool wasReaderHomeGesture() const { return false; } + constexpr bool wasReaderLightPanelGesture() const { return false; } + constexpr bool wasReaderMenuHold() const { return false; } +#endif bool wasAnyPressed() const; bool wasAnyReleased() const; unsigned long getHeldTime() const; @@ -45,24 +149,73 @@ class MappedInputManager { void simulatorInjectPress(Button button); void simulatorInjectRelease(Button button); void simulatorClearInputFrame(); +#if CROSSINK_APP_CAP_TOUCH + void simulatorInjectTouchDown(int x, int y); + void simulatorInjectTouchMove(int x, int y); + void simulatorInjectTouchRelease(int x, int y); +#endif #endif private: HalGPIO& gpio; + const GfxRenderer& renderer; bool readerMode = false; bool powerAsConfirmInReaderMode = false; +#if CROSSINK_APP_CAP_TOUCH + bool readerTouchscreenOverride = false; +#endif mutable bool suppressBackRelease = false; mutable bool suppressConfirmRelease = false; mutable bool suppressPowerRelease = false; mutable bool suppressPowerConfirmRelease = false; +#if CROSSINK_APP_CAP_TOUCH + mutable bool suppressTouchTap = false; +#endif #ifdef SIMULATOR std::array simulatorPressed{}; std::array simulatorReleased{}; std::array simulatorHeld{}; std::array simulatorPressStart{}; +#if CROSSINK_APP_CAP_TOUCH + struct SimulatorTouch { + bool pressed = false; + bool pressedThisFrame = false; + bool releasedThisFrame = false; + int startX = 0; + int startY = 0; + int currentX = 0; + int currentY = 0; + unsigned long startedAt = 0; + }; + mutable SimulatorTouch simulatorTouch; +#endif #endif bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const; + uint8_t mappedFrontButtonFor(Button button) const; bool shouldUsePowerAsConfirmFallback() const; bool shouldMirrorPowerAsConfirmHold() const; +#if CROSSINK_APP_CAP_TOUCH + bool touchInputEnabled() const; + bool hasHomeKeyHardware() const; + bool wasBackGesture() const; + bool wasTopEdgeDownSwipe() const; + bool wasBottomEdgeUpSwipe() const; + // Fetch the pending swipe (if any) and map both endpoints to logical screen coords. + bool decodeSwipe(int& sx, int& sy, int& ex, int& ey) const; + bool listItemFromPoint(int x, int y, int& index, int itemCount, int selectedIndex, int listTop, int listHeight, + bool hasSubtitle) const; + bool wasRegistryTargetTapped(uint8_t kind, int& id) const; + bool wasRegistryTargetTouchedDown(uint8_t kind, int& id) const; + bool wasFrontButtonHintTapped(uint8_t buttonIndex) const; + bool wasFrontButtonHintTouchedDown(uint8_t buttonIndex) const; + void rememberTouchHeldTime() const; + mutable bool touchHeldOverrideValid = false; + mutable unsigned long touchHeldOverrideMs = 0; + mutable unsigned long touchHeldOverrideAt = 0; +#else + constexpr bool wasBackGesture() const { return false; } + constexpr bool wasFrontButtonHintTapped(uint8_t) const { return false; } + constexpr bool wasFrontButtonHintTouchedDown(uint8_t) const { return false; } +#endif }; diff --git a/src/OpdsServerStore.cpp b/src/OpdsServerStore.cpp index 498db54fd9..08e8a8e5ec 100644 --- a/src/OpdsServerStore.cpp +++ b/src/OpdsServerStore.cpp @@ -75,17 +75,17 @@ bool OpdsServerStore::fromJson(JsonVariantConst doc) { servers.push_back(std::move(server)); } - LOG_DBG("OPS", "Loaded %zu OPDS servers from file", servers.size()); - if (needsResave) { LOG_DBG("OPS", "Resaving JSON with obfuscated passwords"); - saveToFile(); + requestResave(); } return true; } bool OpdsServerStore::loadFromFile() { + servers.clear(); + loaded_ = true; const bool hasStoreFile = Storage.exists(getFilePath()); if (PersistableStore::loadFromFile()) { return true; @@ -102,6 +102,16 @@ bool OpdsServerStore::loadFromFile() { return false; } +void OpdsServerStore::ensureLoaded() const { + if (loaded_) return; + const_cast(this)->loadFromFile(); +} + +void OpdsServerStore::release() { + std::vector().swap(servers); + loaded_ = false; +} + bool OpdsServerStore::migrateFromSettings() { if (strlen(SETTINGS.opdsServerUrl) == 0) { return false; @@ -130,37 +140,38 @@ bool OpdsServerStore::migrateFromSettings() { } bool OpdsServerStore::addServer(const OpdsServer& server) { + ensureLoaded(); if (servers.size() >= MAX_SERVERS) { LOG_DBG("OPS", "Cannot add more servers, limit of %zu reached", MAX_SERVERS); return false; } servers.push_back(server); - LOG_DBG("OPS", "Added server: %s", server.name.c_str()); return saveToFile(); } bool OpdsServerStore::updateServer(size_t index, const OpdsServer& server) { + ensureLoaded(); if (index >= servers.size()) { return false; } servers[index] = server; - LOG_DBG("OPS", "Updated server: %s", server.name.c_str()); return saveToFile(); } bool OpdsServerStore::removeServer(size_t index) { + ensureLoaded(); if (index >= servers.size()) { return false; } - LOG_DBG("OPS", "Removed server: %s", servers[index].name.c_str()); servers.erase(servers.begin() + static_cast(index)); return saveToFile(); } const OpdsServer* OpdsServerStore::getServer(size_t index) const { + ensureLoaded(); if (index >= servers.size()) { return nullptr; } diff --git a/src/OpdsServerStore.h b/src/OpdsServerStore.h index 6586b1ed0e..f186106b1a 100644 --- a/src/OpdsServerStore.h +++ b/src/OpdsServerStore.h @@ -30,6 +30,7 @@ struct OpdsServer { class OpdsServerStore : public PersistableStore { private: std::vector servers; + bool loaded_ = false; static constexpr size_t MAX_SERVERS = 8; @@ -43,15 +44,26 @@ class OpdsServerStore : public PersistableStore { void toJson(JsonDocument& doc) const; bool fromJson(JsonVariantConst doc); bool loadFromFile(); + void ensureLoaded() const; + void release(); bool addServer(const OpdsServer& server); bool updateServer(size_t index, const OpdsServer& server); bool removeServer(size_t index); - const std::vector& getServers() const { return servers; } + const std::vector& getServers() const { + ensureLoaded(); + return servers; + } const OpdsServer* getServer(size_t index) const; - size_t getCount() const { return servers.size(); } - bool hasServers() const { return !servers.empty(); } + size_t getCount() const { + ensureLoaded(); + return servers.size(); + } + bool hasServers() const { + ensureLoaded(); + return !servers.empty(); + } }; #define OPDS_STORE OpdsServerStore::getInstance() diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 8a13f49312..06ce0c25ab 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -25,6 +25,7 @@ void RecentBooksStore::toJson(JsonDocument& doc) const { obj["title"] = book.title; obj["author"] = book.author; obj["coverBmpPath"] = book.coverBmpPath; + obj["coverState"] = static_cast(book.coverState); } } @@ -41,10 +42,13 @@ bool RecentBooksStore::fromJson(JsonVariantConst doc) { book.title = obj["title"] | ""; book.author = obj["author"] | ""; book.coverBmpPath = obj["coverBmpPath"] | ""; + const int storedCoverState = obj["coverState"] | 0; + if (storedCoverState == static_cast(RecentBook::CoverState::Missing)) { + book.coverState = RecentBook::CoverState::Missing; + } recentBooks.push_back(book); } - LOG_DBG("RBS", "Recent books loaded from file (%d entries)", getCount()); return true; } @@ -54,7 +58,7 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title } void RecentBooksStore::addOrUpdateBook(const std::string& path, const std::string& title, const std::string& author, - const std::string& coverBmpPath) { + const std::string& coverBmpPath, const RecentBook::CoverState coverState) { // Drop stale entries first so a new add can't evict a valid book in their stead. pruneMissing(); @@ -65,13 +69,14 @@ void RecentBooksStore::addOrUpdateBook(const std::string& path, const std::strin it->title = title; it->author = author; it->coverBmpPath = coverBmpPath; + it->coverState = coverState; if (it != recentBooks.begin()) { RecentBook book = std::move(*it); recentBooks.erase(it); recentBooks.insert(recentBooks.begin(), std::move(book)); } } else { - recentBooks.insert(recentBooks.begin(), {path, title, author, coverBmpPath}); + recentBooks.insert(recentBooks.begin(), {path, title, author, coverBmpPath, coverState}); if (recentBooks.size() > MAX_RECENT_BOOKS) { recentBooks.resize(MAX_RECENT_BOOKS); } @@ -80,7 +85,7 @@ void RecentBooksStore::addOrUpdateBook(const std::string& path, const std::strin } bool RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author, - const std::string& coverBmpPath) { + const std::string& coverBmpPath, const RecentBook::CoverState coverState) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it == recentBooks.end()) { @@ -90,6 +95,7 @@ bool RecentBooksStore::updateBook(const std::string& path, const std::string& ti book.title = title; book.author = author; book.coverBmpPath = coverBmpPath; + book.coverState = coverState; saveToFile(); return true; } @@ -136,14 +142,12 @@ RecentBook RecentBooksStore::getDataFromBook(std::string path) const { lastBookFileName = path.substr(lastSlash + 1); } - LOG_DBG("RBS", "Loading recent book: %s", path.c_str()); - // If epub, try to load the metadata for title/author and cover. // Use buildIfMissing=false to avoid heavy epub loading on boot; getTitle()/getAuthor() may be // blank until the book is opened, and entries with missing title are omitted from recent list. if (FsHelpers::hasEpubExtension(lastBookFileName)) { Epub epub(path, "/.crosspoint"); - epub.load(false, true); + epub.load(false, true, Epub::XLocationLoadMode::Skip); return RecentBook{path, epub.getTitle(), epub.getAuthor(), epub.getThumbBmpPath()}; } else if (FsHelpers::hasXtcExtension(lastBookFileName)) { // Handle XTC file @@ -241,6 +245,5 @@ bool RecentBooksStore::loadFromBinaryFile() { return false; } - LOG_DBG("RBS", "Recent books loaded from binary file (%d entries)", static_cast(recentBooks.size())); return true; } diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index cee77d5c91..82fddd6542 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -6,10 +6,14 @@ #include struct RecentBook { + // Unknown failures stay retryable; Missing is set only after EPUB metadata has no cover image. + enum class CoverState : uint8_t { Unknown = 0, Missing = 1 }; + std::string path; std::string title; std::string author; std::string coverBmpPath; + CoverState coverState = CoverState::Unknown; bool operator==(const RecentBook& other) const { return path == other.path; } }; @@ -40,13 +44,14 @@ class RecentBooksStore : public PersistableStore { // Add a new book to the front, or refresh an existing entry and promote it // to the front. void addOrUpdateBook(const std::string& path, const std::string& title, const std::string& author, - const std::string& coverBmpPath); + const std::string& coverBmpPath, + RecentBook::CoverState coverState = RecentBook::CoverState::Unknown); // updateBook updates metadata for an existing book only and must not change // recent-books ordering. Use addOrUpdateBook when the touched book should // become most recent. Returns false if the book does not exist. [[nodiscard]] bool updateBook(const std::string& path, const std::string& title, const std::string& author, - const std::string& coverBmpPath); + const std::string& coverBmpPath, RecentBook::CoverState coverState); // Remove the entry whose path matches (used when a book is removed from recents or finished/read). // Returns true if an entry was found and removed (no-op + false otherwise). diff --git a/src/SdCardFontSystem.cpp b/src/SdCardFontSystem.cpp index 145bd451e1..d7388e3be3 100644 --- a/src/SdCardFontSystem.cpp +++ b/src/SdCardFontSystem.cpp @@ -1,39 +1,121 @@ #include "SdCardFontSystem.h" #include +#include #include +#include + +#include +#include #include "CrossPointSettings.h" +#include "fontIds.h" -void SdCardFontSystem::begin(GfxRenderer& renderer) { - registry_.discover(); +namespace { + +struct UiFontSize { + int fontId; + uint8_t pointSize; +}; + +constexpr UiFontSize kUiFontSizes[] = { + {SMALL_FONT_ID, 8}, + {UI_10_FONT_ID, 10}, + {UI_12_FONT_ID, 12}, +}; + +enum class FontFileSelection : uint8_t { Closest, Exact }; + +// This is a cold setup path, not a render loop. The 320-byte stack footprint +// (path and filename) replace a heap-allocated whole-font catalog +// during every dictionary swap, avoiding persistent fragmentation on the C3. +bool findInstalledFontFile(const char* familyName, const uint8_t targetPointSize, const FontFileSelection selection, + char* path, const size_t pathSize, uint8_t& selectedPointSize) { + if (!familyName || familyName[0] == '\0' || !path || pathSize == 0) return false; + + const char* root = SdCardFontRegistry::findFamilyRoot(familyName); + if (!root) return false; + const int directoryLength = std::snprintf(path, pathSize, "%s/%s", root, familyName); + if (directoryLength <= 0 || static_cast(directoryLength) >= pathSize) return false; + + HalFile dir = Storage.open(path); + if (!dir || !dir.isDirectory()) return false; + + uint8_t closestSize = 0; + uint8_t closestDiff = UINT8_MAX; + char filename[128] = {}; + while (true) { + HalFile entry = dir.openNextFile(); + if (!entry) break; + const bool isDirectory = entry.isDirectory(); + if (!isDirectory) entry.getName(filename, sizeof(filename)); + entry.close(); + if (isDirectory) continue; + + uint8_t pointSize = 0; + uint8_t style = 0; + if (!SdCardFontRegistry::parseFilename(filename, pointSize, style) || style != 0) continue; + + if (selection == FontFileSelection::Closest) { + const uint8_t diff = pointSize > targetPointSize ? pointSize - targetPointSize : targetPointSize - pointSize; + if (closestDiff == UINT8_MAX || diff < closestDiff || (diff == closestDiff && pointSize < closestSize)) { + closestSize = pointSize; + closestDiff = diff; + } + } + } + dir.close(); + + if (selection == FontFileSelection::Closest) { + selectedPointSize = closestSize; + } else if (selection == FontFileSelection::Exact) { + selectedPointSize = targetPointSize; + } + + if (selectedPointSize == 0) return false; + // Scan once more to preserve the exact file name rather than assuming the + // file base name matches the directory name. + dir = Storage.open(path); + if (!dir || !dir.isDirectory()) return false; + while (true) { + HalFile entry = dir.openNextFile(); + if (!entry) break; + const bool isDirectory = entry.isDirectory(); + if (!isDirectory) entry.getName(filename, sizeof(filename)); + entry.close(); + if (isDirectory) continue; + + uint8_t pointSize = 0; + uint8_t style = 0; + if (!SdCardFontRegistry::parseFilename(filename, pointSize, style) || style != 0 || + pointSize != selectedPointSize) { + continue; + } + const int pathLength = std::snprintf(path, pathSize, "%s/%s/%s", root, familyName, filename); + dir.close(); + return pathLength > 0 && static_cast(pathLength) < pathSize; + } + dir.close(); + return false; +} + +} // namespace +void SdCardFontSystem::begin(GfxRenderer& renderer) { // Register this system as the SD font ID resolver in settings. // Uses a static trampoline since CrossPointSettings stores a plain function pointer. - SETTINGS.sdFontIdResolver = [](void* ctx, const char* familyName, uint8_t fontSizeEnum) -> int { - return static_cast(ctx)->resolveFontId(familyName, fontSizeEnum); + SETTINGS.sdFontIdResolver = [](void* ctx, const char* familyName, uint8_t pointSize) -> int { + return static_cast(ctx)->resolveFontId(familyName, pointSize); }; SETTINGS.sdFontResolverCtx = this; - // If user has a saved SD font selection, load it - if (SETTINGS.sdFontFamilyName[0] != '\0') { - const auto* family = registry_.findFamily(SETTINGS.sdFontFamilyName); - if (family) { - if (manager_.loadFamily(*family, renderer, SETTINGS.getSdFontTargetPointSize(), SETTINGS.fontSize)) { - LOG_DBG("SDFS", "Loaded SD card font family: %s", SETTINGS.sdFontFamilyName); - } else { - LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName); - SETTINGS.sdFontFamilyName[0] = '\0'; - SETTINGS.saveToFile(); - } - } else { - LOG_DBG("SDFS", "SD font family not found on card: %s (clearing)", SETTINGS.sdFontFamilyName); - SETTINGS.sdFontFamilyName[0] = '\0'; - SETTINGS.saveToFile(); - } + if (SETTINGS.sdFontFamilyName[0] == '\0') { + LOG_DBG("SDFS", "SD font resolver ready; discovery deferred until requested"); + return; } - LOG_DBG("SDFS", "SD font system ready (%d families discovered)", registry_.getFamilyCount()); + ensureLoaded(renderer); + releaseRegistry(); } void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) { @@ -41,30 +123,45 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) { // Track whether we just re-discovered so we can force a reload below even // when the wanted family/size still maps to the same point size — the file // contents on disk may have changed (e.g. user re-uploaded a new build). - const bool registryWasDirty = registryDirty_.exchange(false, std::memory_order_acquire); - if (registryWasDirty) { - LOG_DBG("SDFS", "Registry dirty — re-discovering fonts"); - registry_.discover(); - } + const bool registryWasDirty = registryDirty_.load(std::memory_order_acquire); const char* wantedFamily = SETTINGS.sdFontFamilyName; const std::string& currentFamily = manager_.currentFamilyName(); - const uint8_t targetPointSize = SETTINGS.getSdFontTargetPointSize(); - const uint8_t sizeStep = SETTINGS.fontSize; + uint8_t targetPointSize = SETTINGS.getSdFontTargetPointSize(); if (wantedFamily[0] == '\0') { if (!currentFamily.empty()) { manager_.unloadAll(renderer); + loadedFontPointSize_ = 0; } return; } + if (!registryWasDirty && currentFamily == wantedFamily && loadedFontPointSize_ == targetPointSize && + SETTINGS.legacySdFontSizeStep == UINT8_MAX) { + return; + } + + ensureRegistry(); + + const auto* family = registry_.findFamily(wantedFamily); + if (family && SETTINGS.legacySdFontSizeStep != UINT8_MAX) { + const auto sizes = family->availableSizes(); + if (!sizes.empty()) { + const uint8_t step = std::min(SETTINGS.legacySdFontSizeStep, sizes.size() - 1); + targetPointSize = sizes[step]; + SETTINGS.readerFontPointSize = targetPointSize; + SETTINGS.legacySdFontSizeStep = UINT8_MAX; + SETTINGS.saveToFile(); + LOG_INF("SDFS", "Migrated SD font size to %u pt", targetPointSize); + } + } + // Reload if family changed OR if the user-selected size maps to a // different file than what's currently loaded OR if the registry was // just rediscovered (file may have been replaced on disk). bool familyMatches = (currentFamily == wantedFamily); if (familyMatches) { - const auto* family = registry_.findFamily(wantedFamily); if (!family) { LOG_DBG("SDFS", "SD font family disappeared: %s (clearing)", wantedFamily); manager_.unloadAll(renderer); @@ -72,20 +169,21 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) { SETTINGS.saveToFile(); return; } - const auto* wantedFile = family->selectFile(targetPointSize, sizeStep); + const auto* wantedFile = family->findClosestFile(targetPointSize); uint8_t wantedPt = wantedFile ? wantedFile->pointSize : 0; if (!registryWasDirty && wantedPt == manager_.currentPointSize()) return; - LOG_DBG("SDFS", "Reloading %s: size %u -> %u (target %u step %u)%s", wantedFamily, manager_.currentPointSize(), - wantedPt, targetPointSize, sizeStep, registryWasDirty ? " [registry dirty]" : ""); + LOG_DBG("SDFS", "Reloading %s: size %u -> %u (target %u)%s", wantedFamily, manager_.currentPointSize(), wantedPt, + targetPointSize, registryWasDirty ? " [registry dirty]" : ""); } if (!currentFamily.empty()) { manager_.unloadAll(renderer); } - const auto* family = registry_.findFamily(wantedFamily); if (family) { - if (manager_.loadFamily(*family, renderer, targetPointSize, sizeStep)) { + if (manager_.loadFamilyClosest(*family, renderer, targetPointSize)) { + loadedFontPointSize_ = targetPointSize; + setupUiFallbacks(renderer); LOG_DBG("SDFS", "Loaded SD font family: %s", wantedFamily); } else { LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily); @@ -105,22 +203,100 @@ void SdCardFontSystem::releaseLoadedFont(GfxRenderer& renderer) { const std::string familyName = manager_.currentFamilyName(); (void)familyName; manager_.unloadAll(renderer); + loadedFontPointSize_ = 0; LOG_DBG("SDFS", "Released SD card font before low-memory operation: %s", familyName.c_str()); } +void SdCardFontSystem::ensureRegistry() { + const bool dirty = registryDirty_.exchange(false, std::memory_order_acq_rel); + if (registryLoaded_ && !dirty) return; + if (dirty) LOG_DBG("SDFS", "Registry dirty — re-discovering fonts"); + registry_.discover(); + if (registry_.lastDiscoveryFailed()) { + LOG_ERR("SDFS", "SD font registry scan ran out of memory (free=%u maxAlloc=%u)", ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + registryDirty_.store(true, std::memory_order_release); + return; + } + registryLoaded_ = true; +} + +void SdCardFontSystem::releaseRegistry() { + if (!registryLoaded_) return; + LOG_DBG("SDFS", "Releasing SD font catalog (%d families)", registry_.getFamilyCount()); + registry_.clear(); + registryLoaded_ = false; +} + void SdCardFontSystem::releaseForNetwork(GfxRenderer& renderer) { releaseLoadedFont(renderer); - const int familyCount = registry_.getFamilyCount(); - if (familyCount == 0) return; - - registry_.clear(); + releaseRegistry(); registryDirty_.store(true, std::memory_order_release); - LOG_DBG("SDFS", "Released SD font registry before network operation (%d families)", familyCount); } -int SdCardFontSystem::resolveFontId(const char* familyName, uint8_t /*fontSizeEnum*/) const { - // The manager loads exactly one size (closest to SETTINGS.fontSize), so the +void SdCardFontSystem::setupUiFallbacks(GfxRenderer& renderer) { + const std::string& familyName = manager_.currentFamilyName(); + if (familyName.empty()) return; + + const auto* family = registry_.findFamily(familyName); + if (!family) return; + + const auto readerIt = renderer.getFontMap().find(manager_.getFontId(familyName)); + if (readerIt == renderer.getFontMap().end()) return; + + static constexpr uint32_t kCjkProbes[] = {0x4E00, 0x3042, 0x30A2, 0xAC00}; + bool hasCjk = false; + for (const uint32_t cp : kCjkProbes) { + if (readerIt->second.hasCodepoint(cp)) { + hasCjk = true; + break; + } + } + if (!hasCjk) { + LOG_DBG("SDFS", "%s has no CJK coverage - skipping UI fallback sizes", familyName.c_str()); + return; + } + + for (const auto& ui : kUiFontSizes) { + const int sdFontId = manager_.loadFamilyExtraSize(*family, renderer, ui.pointSize); + if (sdFontId != 0) { + renderer.setFallbackFont(ui.fontId, sdFontId); + } else { + LOG_DBG("SDFS", "No %u pt SD glyphs for UI fallback in %s", ui.pointSize, familyName.c_str()); + } + } +} + +void SdCardFontSystem::setupUiFallbacksDirect(GfxRenderer& renderer, const char* familyName) { + if (!familyName || familyName[0] == '\0') return; + + const auto readerIt = renderer.getFontMap().find(manager_.getFontId(manager_.currentFamilyName())); + if (readerIt == renderer.getFontMap().end()) return; + + static constexpr uint32_t kCjkProbes[] = {0x4E00, 0x3042, 0x30A2, 0xAC00}; + bool hasCjk = false; + for (const uint32_t cp : kCjkProbes) { + if (readerIt->second.hasCodepoint(cp)) { + hasCjk = true; + break; + } + } + if (!hasCjk) return; + + for (const auto& ui : kUiFontSizes) { + char path[160] = {}; + uint8_t pointSize = 0; + if (!findInstalledFontFile(familyName, ui.pointSize, FontFileSelection::Exact, path, sizeof(path), pointSize)) { + continue; + } + const int sdFontId = manager_.loadFamilyExtraFile(path, familyName, pointSize, renderer); + if (sdFontId != 0) renderer.setFallbackFont(ui.fontId, sdFontId); + } +} + +int SdCardFontSystem::resolveFontId(const char* familyName, uint8_t /*pointSize*/) const { + // The manager loads exactly one size (closest to the selected point size), so the // enum is implicit — always return the single loaded font ID for this family. // ensureLoaded() must have been called with the current settings before this. return manager_.getFontId(familyName); @@ -134,13 +310,18 @@ bool SdCardFontSystem::changeReaderFontSize(const bool larger) { if (family) { const auto sizes = family->availableSizes(); if (sizes.size() > 1) { - uint8_t current = SETTINGS.fontSize < sizes.size() ? SETTINGS.fontSize : static_cast(sizes.size() - 1); + uint8_t current = 0; + while (current + 1 < sizes.size() && sizes[current] < SETTINGS.readerFontPointSize) ++current; + if (sizes[current] != SETTINGS.readerFontPointSize && current > 0 && + SETTINGS.readerFontPointSize - sizes[current - 1] <= sizes[current] - SETTINGS.readerFontPointSize) { + --current; + } if (larger) { current = static_cast((current + 1) % sizes.size()); } else { current = current == 0 ? static_cast(sizes.size() - 1) : static_cast(current - 1); } - SETTINGS.fontSize = current; + SETTINGS.readerFontPointSize = sizes[current]; return true; } } @@ -148,3 +329,134 @@ bool SdCardFontSystem::changeReaderFontSize(const bool larger) { return SETTINGS.changeReaderFontSize(larger); } + +uint8_t SdCardFontSystem::resolveLegacySizeStep(const char* familyName, const uint8_t sizeStep) { + ensureRegistry(); + const auto* family = familyName ? registry_.findFamily(familyName) : nullptr; + if (family) { + const auto sizes = family->availableSizes(); + if (!sizes.empty()) return sizes[std::min(sizeStep, sizes.size() - 1)]; + } + return CrossPointSettings::getSdFontRangePointSize(SETTINGS.sdFontSizeRange, sizeStep); +} + +DictionaryFontActivation SdCardFontSystem::activateDictionaryFont(GfxRenderer& renderer, const char* familyName, + uint8_t targetPointSize) { + // A non-zero size with no dedicated family means "use the reader's installed + // family at this size". This keeps the setting useful when the same custom + // family is wanted for reading and definitions without keeping two families + // resident. + if ((!familyName || familyName[0] == '\0') && targetPointSize != 0 && SETTINGS.sdFontFamilyName[0] != '\0') { + familyName = SETTINGS.sdFontFamilyName; + } + if (!familyName || familyName[0] == '\0') { + return {restoreReaderFont(renderer), false}; + } + + MemoryBudget::logHeapShape("dict.font_before_activate"); + char path[160] = {}; + uint8_t selectedPointSize = 0; + // Prefer the actual loaded reader-file size. Built-in readers have no SD + // file, so use their effective physical point size instead. + if (targetPointSize == 0) { + targetPointSize = manager_.currentPointSize() != 0 + ? manager_.currentPointSize() + : CrossPointSettings::getReaderFontPointSize(SETTINGS.getEffectiveReaderFontSize()); + } + if (!findInstalledFontFile(familyName, targetPointSize, FontFileSelection::Closest, path, sizeof(path), + selectedPointSize)) { + LOG_DBG("SDFS", "Dictionary font not found on card: %s", familyName); + const char* globalFamilyName = SETTINGS.dictionarySdFontFamilyName; + if (globalFamilyName[0] != '\0' && std::strcmp(familyName, globalFamilyName) != 0) { + LOG_DBG("SDFS", "Using global dictionary font while per-book font is unavailable: %s", globalFamilyName); + return activateDictionaryFont(renderer, globalFamilyName, SETTINGS.dictionaryFontPointSize); + } + const int readerFontId = restoreReaderFont(renderer); + MemoryBudget::logHeapShape("dict.font_reader_fallback"); + return {readerFontId, false}; + } + + if (manager_.currentFamilyName() == familyName && manager_.currentPointSize() == selectedPointSize) { + const int fontId = manager_.getFontId(manager_.currentFamilyName()); + MemoryBudget::logHeapShape("dict.font_reused_reader"); + return {fontId, true}; + } + + // A reader SD font can retain page glyphs, kerning, and advance tables after + // a long reading session. They are disposable at this handoff: keeping them + // through the headroom check makes a dictionary font appear unavailable until + // its book cache is deleted or the heap happens to be less fragmented. + const int activeReaderFontId = SETTINGS.getReaderFontId(); + const auto beforeCacheRelease = MemoryBudget::snapshot(); + if (renderer.releaseSdCardFontForLowMemory(activeReaderFontId)) { + const auto afterCacheRelease = MemoryBudget::snapshot(); + LOG_DBG("SDFS", "Released reader SD-font caches before dictionary swap: free=%u->%u maxAlloc=%u->%u", + beforeCacheRelease.freeHeap, afterCacheRelease.freeHeap, beforeCacheRelease.maxAllocHeap, + afterCacheRelease.maxAllocHeap); + } + + const auto heap = MemoryBudget::snapshot(); + if (!MemoryBudget::hasHeapForDictionarySdFont(heap)) { + LOG_ERR("SDFS", "Low heap for dictionary font swap (%u free, %u max alloc, need %u/%u); using reader font", + heap.freeHeap, heap.maxAllocHeap, MemoryBudget::DICTIONARY_SD_FONT_MIN_FREE, + MemoryBudget::DICTIONARY_SD_FONT_MIN_MAX_ALLOC); + const int readerFontId = restoreReaderFont(renderer); + MemoryBudget::logHeapShape("dict.font_heap_fallback"); + return {readerFontId, false}; + } + + // unloadAll() also drops optional CJK UI sizes before the dictionary file is + // allocated, so both families are never resident at once. + if (!manager_.currentFamilyName().empty()) { + manager_.unloadAll(renderer); + } + loadedFontPointSize_ = 0; + + if (manager_.loadFamilyFile(path, familyName, selectedPointSize, renderer)) { + const int fontId = manager_.getFontId(manager_.currentFamilyName()); + LOG_DBG("SDFS", "Activated dictionary font %s at %u pt", familyName, manager_.currentPointSize()); + MemoryBudget::logHeapShape("dict.font_after_activate"); + return {fontId, true}; + } + + LOG_ERR("SDFS", "Failed to load dictionary font %s; restoring reader font", familyName); + const int readerFontId = restoreReaderFont(renderer); + MemoryBudget::logHeapShape("dict.font_reader_fallback"); + return {readerFontId, false}; +} + +int SdCardFontSystem::restoreReaderFont(GfxRenderer& renderer) { + const char* familyName = SETTINGS.sdFontFamilyName; + if (!familyName || familyName[0] == '\0') { + if (!manager_.currentFamilyName().empty()) manager_.unloadAll(renderer); + loadedFontPointSize_ = 0; + MemoryBudget::logHeapShape("dict.font_after_restore"); + return SETTINGS.getBuiltInReaderFontId(); + } + + char path[160] = {}; + uint8_t selectedPointSize = 0; + if (!findInstalledFontFile(familyName, SETTINGS.getSdFontTargetPointSize(), FontFileSelection::Closest, path, + sizeof(path), selectedPointSize)) { + LOG_ERR("SDFS", "Reader font unavailable while restoring: %s", familyName); + if (!manager_.currentFamilyName().empty()) manager_.unloadAll(renderer); + loadedFontPointSize_ = 0; + MemoryBudget::logHeapShape("dict.font_after_restore"); + return SETTINGS.getBuiltInReaderFontId(); + } + + if (manager_.currentFamilyName() != familyName || manager_.currentPointSize() != selectedPointSize) { + if (!manager_.currentFamilyName().empty()) manager_.unloadAll(renderer); + if (!manager_.loadFamilyFile(path, familyName, selectedPointSize, renderer)) { + LOG_ERR("SDFS", "Failed to restore reader font: %s", familyName); + MemoryBudget::logHeapShape("dict.font_after_restore"); + return SETTINGS.getBuiltInReaderFontId(); + } + loadedFontPointSize_ = SETTINGS.getSdFontTargetPointSize(); + setupUiFallbacksDirect(renderer, familyName); + } + + const int fontId = manager_.getFontId(manager_.currentFamilyName()); + MemoryBudget::logHeapShape("dict.font_after_restore"); + return fontId != 0 ? fontId : SETTINGS.getBuiltInReaderFontId(); +} diff --git a/src/SdCardFontSystem.h b/src/SdCardFontSystem.h index 0d6d7435cc..d4c2050b83 100644 --- a/src/SdCardFontSystem.h +++ b/src/SdCardFontSystem.h @@ -7,6 +7,11 @@ class GfxRenderer; +struct DictionaryFontActivation { + int fontId = 0; + bool usingDictionaryFont = false; +}; + /// Facade that owns the SD card font registry, manager, and resolver logic. /// Hides implementation details behind a single begin() + ensureLoaded() API. class SdCardFontSystem { @@ -14,7 +19,9 @@ class SdCardFontSystem { SdCardFontSystem() = default; SdCardFontSystem(const SdCardFontSystem&) = delete; SdCardFontSystem& operator=(const SdCardFontSystem&) = delete; - /// Discover SD card fonts and load user's saved selection. Call once during setup. + /// Register the font resolver and load a saved SD font selection. When the + /// built-in font is selected, discovery stays deferred until font metadata + /// is explicitly requested. void begin(GfxRenderer& renderer); /// Ensure the correct SD font family is loaded for the current settings. @@ -29,13 +36,36 @@ class SdCardFontSystem { /// Release all SD-font RAM that network/TLS work does not need. void releaseForNetwork(GfxRenderer& renderer); - /// Resolve an SD card font ID from family name + fontSize enum. + /// Ensure the font catalog is available for settings/web enumeration, including + /// newly uploaded or deleted fonts visible in the web UI. + void ensureRegistry(); + + /// Release catalog names and paths without unloading the active reader font. + void releaseRegistry(); + + /// Resolve an SD card font ID from family name + selected point size. /// Returns 0 if not found. Used by CrossPointSettings::getReaderFontId(). - int resolveFontId(const char* familyName, uint8_t fontSizeEnum) const; + int resolveFontId(const char* familyName, uint8_t pointSize) const; /// Change the reader font size using the active SD family when one is selected. bool changeReaderFontSize(bool larger); + /// Convert a pre-point-size SD font slot into the exact installed size. + /// Used while reading legacy per-book reader settings. + uint8_t resolveLegacySizeStep(const char* familyName, uint8_t sizeStep); + + // Temporarily replace the reader SD font with a per-book dictionary font. + // At most one SD font family remains resident. Missing or failed dictionary + // fonts leave the reader font active and retain the saved selection. + // targetPointSize of zero follows the active reader size. The selected file + // still uses the closest installed size, so a removed font file degrades + // safely without retaining another family or size cache in RAM. + DictionaryFontActivation activateDictionaryFont(GfxRenderer& renderer, const char* familyName, + uint8_t targetPointSize = 0); + + // Restore the saved reader font after a dictionary lookup and return its ID. + int restoreReaderFont(GfxRenderer& renderer); + /// Access the registry (e.g. for settings UI to enumerate available fonts). const SdCardFontRegistry& registry() const { return registry_; } @@ -46,19 +76,25 @@ class SdCardFontSystem { /// Thread-safe: can be called from the web server task. void markRegistryDirty() { registryDirty_.store(true, std::memory_order_release); } - /// If the registry is dirty, re-scan the SD card now and clear the flag. + /// Ensure the registry is available and re-scan it after SD changes. /// Used by the web UI so uploaded/deleted fonts appear in the list /// without waiting for the reader activity to run ensureLoaded(). - void refreshIfDirty() { - if (registryDirty_.exchange(false, std::memory_order_acquire)) { - registry_.discover(); - } - } + void refreshIfDirty() { ensureRegistry(); } private: + // Load the active SD family at the built-in UI point sizes and register each + // as a size-matched CJK fallback for the corresponding UI font, so CJK book + // titles/list rows render at the same size as the surrounding Latin UI text. + // No-op when no SD family is loaded. Safe to call repeatedly (sizes already + // loaded are reused). + void setupUiFallbacks(GfxRenderer& renderer); + void setupUiFallbacksDirect(GfxRenderer& renderer, const char* familyName); + SdCardFontRegistry registry_; SdCardFontManager manager_; std::atomic registryDirty_{false}; + bool registryLoaded_ = false; + uint8_t loadedFontPointSize_ = 0; }; // Global SD card font system instance (defined in main.cpp). diff --git a/src/SettingsList.h b/src/SettingsList.h index 36a790c4dc..4c7480108d 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -16,35 +16,31 @@ #include "CrossPointSettings.h" #include "KOReaderCredentialStore.h" #include "activities/settings/SettingsActivity.h" +#include "util/Dictionary.h" +#include "util/DictionaryRegistry.h" inline std::string fontSizePointLabel(const uint8_t pointSize) { return std::to_string(pointSize) + " pt"; } inline void appendBuiltinFontSizeOption(SettingInfo& setting, const CrossPointSettings::FONT_SIZE size) { - const uint8_t stored = CrossPointSettings::getStoredReaderFontSize(size); - if (stored == UINT8_MAX) return; - - setting.enumStringValues.push_back(fontSizePointLabel(CrossPointSettings::getReaderFontPointSize(size))); - setting.enumRawValues.push_back(stored); + const uint8_t pointSize = CrossPointSettings::getReaderFontPointSize(size); + setting.enumStringValues.push_back(fontSizePointLabel(pointSize)); + setting.enumRawValues.push_back(pointSize); } inline SettingInfo buildBuiltinFontSizeSetting() { SettingInfo s; s.nameId = StrId::STR_FONT_SIZE; s.type = SettingType::ENUM; - s.valuePtr = &CrossPointSettings::fontSize; + s.valuePtr = &CrossPointSettings::readerFontPointSize; s.key = "fontSize"; s.category = StrId::STR_CAT_READER; s.enumStringValues.reserve(CrossPointSettings::FONT_SIZE_COUNT); s.enumRawValues.reserve(CrossPointSettings::FONT_SIZE_COUNT); - appendBuiltinFontSizeOption(s, CrossPointSettings::TEENSY); - appendBuiltinFontSizeOption(s, CrossPointSettings::ITTY_BITTY); appendBuiltinFontSizeOption(s, CrossPointSettings::TINY); appendBuiltinFontSizeOption(s, CrossPointSettings::SMALL); appendBuiltinFontSizeOption(s, CrossPointSettings::MEDIUM); appendBuiltinFontSizeOption(s, CrossPointSettings::LARGE); - appendBuiltinFontSizeOption(s, CrossPointSettings::EXTRA_LARGE); - appendBuiltinFontSizeOption(s, CrossPointSettings::HUGE_SIZE); return s; } @@ -53,7 +49,7 @@ inline SettingInfo buildSdFontSizeSetting(const SdCardFontFamilyInfo& family) { SettingInfo s; s.nameId = StrId::STR_FONT_SIZE; s.type = SettingType::ENUM; - s.valuePtr = &CrossPointSettings::fontSize; + s.valuePtr = &CrossPointSettings::readerFontPointSize; s.key = "fontSize"; s.category = StrId::STR_CAT_READER; @@ -62,7 +58,7 @@ inline SettingInfo buildSdFontSizeSetting(const SdCardFontFamilyInfo& family) { s.enumRawValues.reserve(sizes.size()); for (size_t i = 0; i < sizes.size(); i++) { s.enumStringValues.push_back(fontSizePointLabel(sizes[i])); - s.enumRawValues.push_back(static_cast(i)); + s.enumRawValues.push_back(sizes[i]); } return s; } @@ -207,24 +203,18 @@ inline SettingInfo buildFontFamilySetting(const SdCardFontRegistry* registry) { }; s.valueSetter = [sdFamilyNames, sdFamilySizes](uint8_t v) { - uint8_t targetPointSize = CrossPointSettings::getReaderFontPointSize(SETTINGS.getEffectiveReaderFontSize()); - if (SETTINGS.sdFontFamilyName[0] != '\0') { - for (size_t i = 0; i < sdFamilyNames.size(); i++) { - if (sdFamilyNames[i] == SETTINGS.sdFontFamilyName && SETTINGS.fontSize < sdFamilySizes[i].size()) { - targetPointSize = sdFamilySizes[i][SETTINGS.fontSize]; - break; - } - } - } + const uint8_t targetPointSize = SETTINGS.readerFontPointSize; if (v < CrossPointSettings::BUILTIN_FONT_COUNT) { SETTINGS.fontFamily = v; SETTINGS.sdFontFamilyName[0] = '\0'; - SETTINGS.fontSize = closestBuiltinFontSizeIndex(targetPointSize); + SETTINGS.readerFontPointSize = CrossPointSettings::getReaderFontPointSize( + static_cast(closestBuiltinFontSizeIndex(targetPointSize))); } else { int sdIdx = v - CrossPointSettings::BUILTIN_FONT_COUNT; if (sdIdx < static_cast(sdFamilyNames.size())) { - SETTINGS.fontSize = closestPointSizeIndex(sdFamilySizes[sdIdx], targetPointSize); + SETTINGS.readerFontPointSize = + sdFamilySizes[sdIdx][closestPointSizeIndex(sdFamilySizes[sdIdx], targetPointSize)]; strncpy(SETTINGS.sdFontFamilyName, sdFamilyNames[sdIdx].c_str(), sizeof(SETTINGS.sdFontFamilyName) - 1); SETTINGS.sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName) - 1] = '\0'; } @@ -234,6 +224,118 @@ inline SettingInfo buildFontFamilySetting(const SdCardFontRegistry* registry) { return s; } +inline SettingInfo buildDictionaryFontFamilySetting(const SdCardFontRegistry* registry) { + SettingInfo s; + s.nameId = StrId::STR_DICTIONARY_FONT; + s.type = SettingType::ENUM; + s.key = "dictionaryFont"; + s.category = StrId::STR_CAT_READER; + s.enumStringValues.push_back(I18N.get(StrId::STR_USE_READER_FONT)); + + std::vector familyNames; + if (registry) { + const auto& families = registry->getFamilies(); + familyNames.reserve(families.size()); + s.enumStringValues.reserve(families.size() + 1); + for (const auto& family : families) { + familyNames.push_back(family.name); + s.enumStringValues.push_back(family.name); + } + } + + s.valueGetter = [familyNames]() -> uint8_t { + for (size_t i = 0; i < familyNames.size(); ++i) { + if (familyNames[i] == SETTINGS.dictionarySdFontFamilyName) return static_cast(i + 1); + } + return 0; + }; + s.valueSetter = [familyNames](const uint8_t value) { + if (value == 0 || value > familyNames.size()) { + SETTINGS.dictionarySdFontFamilyName[0] = '\0'; + SETTINGS.dictionaryFontPointSize = 0; + return; + } + strncpy(SETTINGS.dictionarySdFontFamilyName, familyNames[value - 1].c_str(), + sizeof(SETTINGS.dictionarySdFontFamilyName) - 1); + SETTINGS.dictionarySdFontFamilyName[sizeof(SETTINGS.dictionarySdFontFamilyName) - 1] = '\0'; + }; + return s; +} + +inline SettingInfo buildDictionaryFontSizeSetting(const SdCardFontRegistry* registry) { + SettingInfo s; + s.nameId = StrId::STR_DICTIONARY_FONT_SIZE; + s.type = SettingType::ENUM; + s.valuePtr = &CrossPointSettings::dictionaryFontPointSize; + s.key = "dictionaryFontSize"; + s.category = StrId::STR_CAT_READER; + s.enumStringValues.push_back(I18N.get(StrId::STR_USE_READER_FONT_SIZE)); + s.enumRawValues.push_back(0); + + if (!registry) return s; + // With no dedicated dictionary family, a non-zero dictionary size applies + // to the reader's SD-card family. Built-in reader fonts have no selectable + // files, so they deliberately retain just the "use reader size" entry. + const char* familyName = + SETTINGS.dictionarySdFontFamilyName[0] != '\0' ? SETTINGS.dictionarySdFontFamilyName : SETTINGS.sdFontFamilyName; + if (familyName[0] == '\0') return s; + const auto* family = registry->findFamily(familyName); + if (!family) return s; + + const auto sizes = family->availableSizes(); + s.enumStringValues.reserve(sizes.size() + 1); + s.enumRawValues.reserve(sizes.size() + 1); + for (const uint8_t pointSize : sizes) { + s.enumStringValues.push_back(fontSizePointLabel(pointSize)); + s.enumRawValues.push_back(pointSize); + } + return s; +} + +inline SettingInfo buildDictionarySetting(const DictionaryRegistry* dictRegistry) { + SettingInfo s; + s.nameId = StrId::STR_DICTIONARY; + s.type = SettingType::ENUM; + s.key = "dictionary"; + s.category = StrId::STR_CAT_READER; + s.enumStringValues.push_back(I18N.get(StrId::STR_DICT_NONE)); + + std::vector entries; + if (dictRegistry) { + entries = dictRegistry->getEntries(); + s.enumStringValues.reserve(entries.size() + 1); + for (const auto& entry : entries) { + s.enumStringValues.push_back(entry.name); + } + } + + s.valueGetter = [entries]() -> uint8_t { + const std::string activePath = Dictionary::readDictPath(); + if (activePath.empty()) { + return 0; + } + for (size_t i = 0; i < entries.size(); i++) { + if (entries[i].basePath == activePath) { + return static_cast(i + 1); + } + } + return 0; + }; + + s.valueSetter = [entries](uint8_t v) { + if (v == 0) { + Dictionary::saveGlobalDictPath(""); + return; + } + const size_t entryIndex = static_cast(v - 1); + if (entryIndex < entries.size()) { + Dictionary::saveGlobalDictPath(entries[entryIndex].basePath.c_str()); + } + }; + + return s; +} + inline SettingInfo buildSleepScreenSetting() { SettingInfo s = SettingInfo::Enum( StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, @@ -263,14 +365,13 @@ inline SettingInfo buildSleepScreenSetting() { // ACTION-type entries and entries without a key are device-only. // // The static list is constructed exactly once (master's optimization, #1086 + -// #1636) so the per-entry SettingInfo cost is paid once. When an -// SdCardFontRegistry is supplied AND has SD card fonts installed, the -// font-family entry is replaced in a per-call copy with a registry-aware -// version. Callers without SD fonts pay only a vector copy. -inline std::vector getSettingsList(const SdCardFontRegistry* registry = nullptr) { +// #1636) so the per-entry SettingInfo cost is paid once. Read-only consumers +// can use it directly; mutable device UI lists use getSettingsList(), which +// returns an owned copy and can add SD-card font and dictionary options. +inline const std::vector& getBaseSettingsList() { static const std::vector baseList = [] { std::vector v; - v.reserve(66); + v.reserve(72); auto add = [&v](SettingInfo setting) { v.push_back(std::move(setting)); }; // --- Display --- @@ -305,10 +406,17 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist CrossPointSettings::UI_THEME::LYRA_3_COVERS, CrossPointSettings::UI_THEME::LYRA_CAROUSEL, CrossPointSettings::UI_THEME::ROUNDEDRAFF})); + add(SettingInfo::Enum(StrId::STR_UI_SCALE, &CrossPointSettings::uiScale, {StrId::STR_SMALL, StrId::STR_LARGE}, + "uiScale", StrId::STR_CAT_DISPLAY) + .withEnumRawValues({CrossPointSettings::UI_SCALE_SMALL, CrossPointSettings::UI_SCALE_LARGE})); add(SettingInfo::Enum(StrId::STR_RECENT_BOOKS_VIEW, &CrossPointSettings::recentBooksView, {StrId::STR_LIST_VIEW, StrId::STR_GRID_VIEW}, "recentBooksView", StrId::STR_CAT_DISPLAY)); add(SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix", StrId::STR_CAT_DISPLAY)); +#if FREEINK_CAP_FRONTLIGHT + add(SettingInfo::Toggle(StrId::STR_RESTORE_LIGHT_ON_WAKE, &CrossPointSettings::frontlightRestoreOnWake, + "frontlightRestoreOnWake", StrId::STR_CAT_DISPLAY)); +#endif // --- Reader --- // Built-in font-family entry. Replaced per-call with a registry-aware @@ -318,12 +426,18 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist add(buildBuiltinFontSizeSetting()); add(SettingInfo::Enum(StrId::STR_SD_FONT_SIZE_RANGE, &CrossPointSettings::sdFontSizeRange, {StrId::STR_FONT_RANGE_TEENSY, StrId::STR_FONT_RANGE_TINY, StrId::STR_FONT_RANGE_XLARGE, - StrId::STR_FONT_RANGE_NO_EMOJI, StrId::STR_FONT_RANGE_ALL}, - "sdFontSizeRange", StrId::STR_CAT_READER)); + StrId::STR_FONT_RANGE_ALL}, + "sdFontSizeRange", StrId::STR_CAT_READER) + .withEnumRawValues({CrossPointSettings::SD_FONT_RANGE_TEENSY, CrossPointSettings::SD_FONT_RANGE_TINY, + CrossPointSettings::SD_FONT_RANGE_XLARGE, CrossPointSettings::SD_FONT_RANGE_ALL})); add(SettingInfo::Value(StrId::STR_LINE_SPACING, &CrossPointSettings::lineHeightPercent, {CrossPointSettings::MIN_LINE_HEIGHT_PERCENT, CrossPointSettings::MAX_LINE_HEIGHT_PERCENT, CrossPointSettings::LINE_HEIGHT_PERCENT_STEP}, "lineHeightPercent", StrId::STR_CAT_READER)); + add(SettingInfo::Enum( + StrId::STR_WORD_SPACING, &CrossPointSettings::wordSpacing, + {StrId::STR_NORMAL, StrId::STR_LEVEL_1, StrId::STR_LEVEL_2, StrId::STR_LEVEL_3, StrId::STR_LEVEL_4}, + "wordSpacing", StrId::STR_CAT_READER)); add(SettingInfo::Enum( StrId::STR_ORIENTATION, &CrossPointSettings::orientation, {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_LANDSCAPE_CCW, StrId::STR_ORIENTATION_INVERTED}, @@ -349,6 +463,10 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist add(SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering, {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, "imageRendering", StrId::STR_CAT_READER)); + add(SettingInfo::Toggle(StrId::STR_TOUCH_READER_CONTROLS, &CrossPointSettings::touchReaderControls, + "touchReaderControls", StrId::STR_CAT_READER)); + add(SettingInfo::Toggle(StrId::STR_DISABLE_TOUCHSCREEN, &CrossPointSettings::disableReaderTouchscreen, + "disableReaderTouchscreen", StrId::STR_CAT_READER)); add(SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, "extraParagraphSpacing", StrId::STR_CAT_READER)); add(SettingInfo::Toggle(StrId::STR_FORCE_PARAGRAPH_INDENTS, &CrossPointSettings::forceParagraphIndents, @@ -357,6 +475,9 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist "bionicReadingEnabled", StrId::STR_CAT_READER)); add(SettingInfo::Toggle(StrId::STR_GUIDE_READING, &CrossPointSettings::guideReadingEnabled, "guideReadingEnabled", StrId::STR_CAT_READER)); + add(SettingInfo::Enum(StrId::STR_INDEXING_METHOD, &CrossPointSettings::indexingMethod, + {StrId::STR_INDEXING_INCREMENTAL, StrId::STR_INDEXING_FULL_SECTION}, "indexingMethod", + StrId::STR_CAT_READER)); // --- Controls --- add(SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout, @@ -403,7 +524,8 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist StrId::STR_READER_DARK_MODE, StrId::STR_FOOTNOTES, StrId::STR_BROWSE_FILES, - StrId::STR_SAVE_CLIPPING}, + StrId::STR_SAVE_CLIPPING, + StrId::STR_LOOKUP}, "shortPwrBtn", StrId::STR_CAT_CONTROLS) .withEnumRawValues({CrossPointSettings::IGNORE, CrossPointSettings::SLEEP, @@ -425,7 +547,8 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist CrossPointSettings::TOGGLE_DARK_MODE, CrossPointSettings::FOOTNOTES, CrossPointSettings::FILE_BROWSER, - CrossPointSettings::CREATE_CLIPPING})); + CrossPointSettings::CREATE_CLIPPING, + CrossPointSettings::LOOKUP_WORD})); add(SettingInfo::Enum(StrId::STR_LONG_PRESS_ACTION, &CrossPointSettings::longPwrBtn, {StrId::STR_IGNORE, StrId::STR_SLEEP, @@ -447,7 +570,8 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist StrId::STR_READER_DARK_MODE, StrId::STR_FOOTNOTES, StrId::STR_BROWSE_FILES, - StrId::STR_SAVE_CLIPPING}, + StrId::STR_SAVE_CLIPPING, + StrId::STR_LOOKUP}, "longPwrBtn", StrId::STR_CAT_CONTROLS) .withEnumRawValues({CrossPointSettings::IGNORE, CrossPointSettings::SLEEP, @@ -469,7 +593,8 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist CrossPointSettings::TOGGLE_DARK_MODE, CrossPointSettings::FOOTNOTES, CrossPointSettings::FILE_BROWSER, - CrossPointSettings::CREATE_CLIPPING})); + CrossPointSettings::CREATE_CLIPPING, + CrossPointSettings::LOOKUP_WORD})); add(SettingInfo::Enum(StrId::STR_LONG_PRESS_MENU_ACTION, &CrossPointSettings::longPressMenuAction, {StrId::STR_IGNORE, StrId::STR_SLEEP, @@ -490,7 +615,8 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist StrId::STR_READER_DARK_MODE, StrId::STR_FOOTNOTES, StrId::STR_BROWSE_FILES, - StrId::STR_SAVE_CLIPPING}, + StrId::STR_SAVE_CLIPPING, + StrId::STR_LOOKUP}, "longPressMenuAction", StrId::STR_CAT_CONTROLS) .withEnumRawValues({CrossPointSettings::LONG_MENU_OFF, CrossPointSettings::LONG_MENU_SLEEP, @@ -511,7 +637,8 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist CrossPointSettings::LONG_MENU_TOGGLE_DARK_MODE, CrossPointSettings::LONG_MENU_FOOTNOTES, CrossPointSettings::LONG_MENU_FILE_BROWSER, - CrossPointSettings::LONG_MENU_CREATE_CLIPPING})); + CrossPointSettings::LONG_MENU_CREATE_CLIPPING, + CrossPointSettings::LONG_MENU_LOOKUP_WORD})); add(SettingInfo::Enum(StrId::STR_LONG_PRESS_BACK_ACTION, &CrossPointSettings::longPressBackAction, {StrId::STR_IGNORE, StrId::STR_SLEEP, @@ -532,7 +659,8 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist StrId::STR_READER_DARK_MODE, StrId::STR_FOOTNOTES, StrId::STR_BROWSE_FILES, - StrId::STR_SAVE_CLIPPING}, + StrId::STR_SAVE_CLIPPING, + StrId::STR_LOOKUP}, "longPressBackAction", StrId::STR_CAT_CONTROLS) .withEnumRawValues({CrossPointSettings::LONG_MENU_OFF, CrossPointSettings::LONG_MENU_SLEEP, @@ -553,7 +681,8 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist CrossPointSettings::LONG_MENU_TOGGLE_DARK_MODE, CrossPointSettings::LONG_MENU_FOOTNOTES, CrossPointSettings::LONG_MENU_FILE_BROWSER, - CrossPointSettings::LONG_MENU_CREATE_CLIPPING})); + CrossPointSettings::LONG_MENU_CREATE_CLIPPING, + CrossPointSettings::LONG_MENU_LOOKUP_WORD})); add(SettingInfo::Toggle(StrId::STR_PWR_BTN_FOOTNOTE_BACK, &CrossPointSettings::pwrBtnFootnoteBack, "pwrBtnFootnoteBack", StrId::STR_CAT_CONTROLS)); @@ -577,6 +706,13 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist "moveFinishedToReadFolder", StrId::STR_CAT_SYSTEM)); add(SettingInfo::Toggle(StrId::STR_AUTO_BACKUP_STATS, &CrossPointSettings::autoBackupStats, "autoBackupStats", StrId::STR_CAT_SYSTEM)); + // Persisted and available to the web settings API, but category-less because + // the on-device editor lives under System > OPDS Servers. + add(SettingInfo::String(StrId::STR_OPDS_DOWNLOAD_FOLDER, SETTINGS.opdsDownloadFolder, + sizeof(SETTINGS.opdsDownloadFolder), "opdsDownloadFolder")); + // Persisted here, but edited from the nearby receive screen's folder picker. + add(SettingInfo::String(StrId::STR_NEARBY_RECEIVE_FOLDER, SETTINGS.nearbyReceiveFolder, + sizeof(SETTINGS.nearbyReceiveFolder), "nearbyReceiveFolder")); add(SettingInfo::Value(StrId::STR_IDLE_TIME_THRESHOLD, &CrossPointSettings::readingIdleTimeThresholdUnits, {CrossPointSettings::MIN_READING_IDLE_TIME_THRESHOLD_UNITS, CrossPointSettings::MAX_READING_IDLE_TIME_THRESHOLD_UNITS, 1}, @@ -586,6 +722,13 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist StrId::STR_CAT_SYSTEM)); #endif + // Frontlight quick-panel state: persisted + web-exposed, category-less so + // it stays off the Settings screen (edited from the swipe-down panel). + add(SettingInfo::Value(StrId::STR_BRIGHTNESS, &CrossPointSettings::frontlightBrightness, {0, 100, 5}, + "frontlightBrightness")); + add(SettingInfo::Value(StrId::STR_WARMTH, &CrossPointSettings::frontlightWarmth, {0, 100, 5}, "frontlightWarmth")); + add(SettingInfo::Toggle(StrId::STR_FRONTLIGHT, &CrossPointSettings::frontlightOn, "frontlightOn")); + // --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- add(SettingInfo::DynamicString( StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); }, @@ -616,6 +759,23 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist KOREADER_STORE.saveToFile(); }, "koMatchMethod", StrId::STR_KOREADER_SYNC)); + add(SettingInfo::DynamicEnum( + StrId::STR_SEND_METADATA, {StrId::STR_STATE_OFF, StrId::STR_STATE_ON}, + [] { return static_cast(KOREADER_STORE.getSendMetadata()); }, + [](uint8_t v) { + KOREADER_STORE.setSendMetadata(v != 0); + KOREADER_STORE.saveToFile(); + }, + "koSendMetadata", StrId::STR_KOREADER_SYNC)); + + add(SettingInfo::DynamicEnum( + StrId::STR_SYNC_BEHAVIOR, {StrId::STR_ASK_EVERY_TIME, StrId::STR_SMART_SYNC}, + [] { return static_cast(KOREADER_STORE.getSyncBehavior()); }, + [](uint8_t v) { + KOREADER_STORE.setSyncBehavior(static_cast(v)); + KOREADER_STORE.saveToFile(); + }, + "koSyncBehavior", StrId::STR_KOREADER_SYNC)); // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- add(SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, @@ -652,11 +812,22 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist add(SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat, {StrId::STR_CLOCK_FORMAT_24H, StrId::STR_CLOCK_FORMAT_12H}, "clockFormat", StrId::STR_CAT_SYSTEM)); + add(SettingInfo::Enum(StrId::STR_DATE_FORMAT, &CrossPointSettings::dateFormat, + {StrId::STR_DATE_FORMAT_MONTH_DAY_YEAR_LONG, StrId::STR_DATE_FORMAT_DAY_MONTH_YEAR_LONG, + StrId::STR_DATE_FORMAT_MONTH_DAY_YEAR_NUMERIC, StrId::STR_DATE_FORMAT_DAY_MONTH_YEAR_NUMERIC, + StrId::STR_DATE_FORMAT_YEAR_MONTH_DAY_NUMERIC, StrId::STR_DATE_FORMAT_MONTH_DAY_NUMERIC, + StrId::STR_DATE_FORMAT_DAY_MONTH_NUMERIC, StrId::STR_DATE_FORMAT_MONTH_DAY_LONG, + StrId::STR_DATE_FORMAT_DAY_MONTH_LONG}, + "dateFormat", StrId::STR_CAT_SYSTEM)); + add(SettingInfo::Enum( + StrId::STR_DATE_SEPARATOR, &CrossPointSettings::dateSeparator, + {StrId::STR_DATE_SEPARATOR_PERIOD, StrId::STR_DATE_SEPARATOR_HYPHEN, StrId::STR_DATE_SEPARATOR_SLASH}, + "dateSeparator", StrId::STR_CAT_SYSTEM)); // Persistence flag for NTP debounce. Resetting from the web UI forces a re-sync // on next WiFi connect, which is useful when crossing time zones. add(SettingInfo::Toggle(StrId::STR_CLOCK_SYNCED, &CrossPointSettings::clockHasBeenSynced, "clockHasBeenSynced", StrId::STR_CAT_SYSTEM)); - // Only show tilt page turn setting when the QMI8658 IMU is present (X3). + // Only show tilt page turn settings when the active device has a supported IMU. if (halTiltSensor.isAvailable()) { for (auto& setting : v) { if (setting.nameId == StrId::STR_SHORT_PWR_BTN || setting.nameId == StrId::STR_LONG_PRESS_ACTION || @@ -683,10 +854,47 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist "tiltPageTurnDirection", StrId::STR_CAT_CONTROLS)); } } + + if (!gpio.deviceIsX3()) { + auto sleepScreenIt = + std::find_if(v.begin(), v.end(), [](const SettingInfo& s) { return s.nameId == StrId::STR_SLEEP_SCREEN; }); + if (sleepScreenIt != v.end()) { + removeEnumRawValue(*sleepScreenIt, static_cast(CrossPointSettings::MINIMAL_STATS_SLEEP)); + } + } return v; }(); - std::vector v = baseList; + return baseList; +} + +inline std::vector getSettingsList(const SdCardFontRegistry* registry = nullptr, + const DictionaryRegistry* dictRegistry = nullptr) { + std::vector v = getBaseSettingsList(); + const bool hasTouch = gpio.hasTouch(); + if (!hasTouch) { + v.erase(std::remove_if(v.begin(), v.end(), + [](const SettingInfo& s) { + return s.nameId == StrId::STR_TOUCH_READER_CONTROLS || + s.nameId == StrId::STR_DISABLE_TOUCHSCREEN; + }), + v.end()); + } + if (hasTouch) { + v.erase(std::remove_if(v.begin(), v.end(), + [](const SettingInfo& s) { + return s.nameId == StrId::STR_FRONT_BTN_FOLLOW_ORIENTATION || + s.nameId == StrId::STR_SUNLIGHT_FADING_FIX; + }), + v.end()); + + const auto themeIt = + std::find_if(v.begin(), v.end(), [](const SettingInfo& s) { return s.nameId == StrId::STR_UI_THEME; }); + if (themeIt != v.end()) { + removeEnumRawValue(*themeIt, static_cast(CrossPointSettings::UI_THEME::CLASSIC)); + removeEnumRawValue(*themeIt, static_cast(CrossPointSettings::UI_THEME::ROUNDEDRAFF)); + } + } if (registry && registry->getFamilyCount() > 0) { auto it = std::find_if(v.begin(), v.end(), [](const SettingInfo& s) { return s.nameId == StrId::STR_FONT_FAMILY; }); if (it != v.end()) { @@ -698,19 +906,26 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist *fontSizeIt = buildFontSizeSetting(registry); } } - if (!gpio.deviceIsX3()) { - auto sleepScreenIt = - std::find_if(v.begin(), v.end(), [](const SettingInfo& s) { return s.nameId == StrId::STR_SLEEP_SCREEN; }); - if (sleepScreenIt != v.end()) { - removeEnumRawValue(*sleepScreenIt, static_cast(CrossPointSettings::MINIMAL_STATS_SLEEP)); + if (dictRegistry) { + if (dictRegistry->count() > 0) { + auto fontSizeIt = + std::find_if(v.begin(), v.end(), [](const SettingInfo& s) { return s.nameId == StrId::STR_FONT_SIZE; }); + const size_t insertIndex = + fontSizeIt == v.end() ? v.size() : static_cast(std::distance(v.begin(), fontSizeIt) + 1); + v.insert(v.begin() + insertIndex, buildDictionaryFontFamilySetting(registry)); + v.insert(v.begin() + insertIndex + 1, buildDictionaryFontSizeSetting(registry)); } + auto guideIt = + std::find_if(v.begin(), v.end(), [](const SettingInfo& s) { return s.nameId == StrId::STR_GUIDE_READING; }); + const auto insertPos = guideIt == v.end() ? v.end() : guideIt + 1; + v.insert(insertPos, buildDictionarySetting(dictRegistry)); } return v; } inline std::vector buildGroupedReaderSettingsList(const std::vector& allSettings) { std::vector readerSettings; - readerSettings.reserve(22); + readerSettings.reserve(23); auto addReaderSetting = [&](StrId nameId) { const auto it = std::find_if(allSettings.begin(), allSettings.end(), @@ -723,11 +938,14 @@ inline std::vector buildGroupedReaderSettingsList(const std::vector readerSettings.push_back(SettingInfo::SectionHeader(StrId::STR_READER_FONT_OPTIONS)); addReaderSetting(StrId::STR_FONT_FAMILY); addReaderSetting(StrId::STR_FONT_SIZE); + addReaderSetting(StrId::STR_DICTIONARY_FONT); + addReaderSetting(StrId::STR_DICTIONARY_FONT_SIZE); readerSettings.push_back(SettingInfo::Action(StrId::STR_MANAGE_FONTS, SettingAction::DownloadFonts)); addReaderSetting(StrId::STR_SD_FONT_SIZE_RANGE); readerSettings.push_back(SettingInfo::SectionHeader(StrId::STR_READER_PAGE_LAYOUT)); addReaderSetting(StrId::STR_LINE_SPACING); + addReaderSetting(StrId::STR_WORD_SPACING); addReaderSetting(StrId::STR_SCREEN_MARGIN); addReaderSetting(StrId::STR_PARA_ALIGNMENT); addReaderSetting(StrId::STR_EXTRA_SPACING); @@ -746,6 +964,7 @@ inline std::vector buildGroupedReaderSettingsList(const std::vector readerSettings.push_back(SettingInfo::SectionHeader(StrId::STR_READER_UI)); addReaderSetting(StrId::STR_ORIENTATION); addReaderSetting(StrId::STR_PUBLISHER_PAGE_NUMBERS); + addReaderSetting(StrId::STR_DISABLE_TOUCHSCREEN); readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar)); return readerSettings; @@ -762,28 +981,43 @@ inline void addSettingByName(std::vector& target, const std::vector inline std::vector buildReaderSettingsParentList(const std::vector& allSettings) { std::vector readerSettings; - readerSettings.reserve(8); + readerSettings.reserve(12); readerSettings.push_back(SettingInfo::Submenu(StrId::STR_READER_FONT_OPTIONS, SettingAction::ReaderFontOptions)); readerSettings.push_back(SettingInfo::Submenu(StrId::STR_READER_PAGE_LAYOUT, SettingAction::ReaderPageLayout)); readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar)); addSettingByName(readerSettings, allSettings, StrId::STR_PUBLISHER_PAGE_NUMBERS); addSettingByName(readerSettings, allSettings, StrId::STR_READER_DARK_MODE); + addSettingByName(readerSettings, allSettings, StrId::STR_DISABLE_TOUCHSCREEN); addSettingByName(readerSettings, allSettings, StrId::STR_EMBEDDED_STYLE); addSettingByName(readerSettings, allSettings, StrId::STR_IMAGES); addSettingByName(readerSettings, allSettings, StrId::STR_BIONIC_READING); addSettingByName(readerSettings, allSettings, StrId::STR_GUIDE_READING); + addSettingByName(readerSettings, allSettings, StrId::STR_DICTIONARY); + addSettingByName(readerSettings, allSettings, StrId::STR_INDEXING_METHOD); return readerSettings; } +inline std::vector buildBookReaderSettingsParentList(const std::vector& allSettings) { + auto settings = buildReaderSettingsParentList(allSettings); + settings.erase( + std::remove_if(settings.begin(), settings.end(), + [](const SettingInfo& setting) { return setting.nameId == StrId::STR_DISABLE_TOUCHSCREEN; }), + settings.end()); + return settings; +} + inline std::vector buildReaderFontSettingsList(const std::vector& allSettings) { std::vector settings; - settings.reserve(5); + settings.reserve(9); addSettingByName(settings, allSettings, StrId::STR_FONT_FAMILY); addSettingByName(settings, allSettings, StrId::STR_FONT_SIZE); + addSettingByName(settings, allSettings, StrId::STR_DICTIONARY_FONT); + addSettingByName(settings, allSettings, StrId::STR_DICTIONARY_FONT_SIZE); addSettingByName(settings, allSettings, StrId::STR_LINE_SPACING); + addSettingByName(settings, allSettings, StrId::STR_WORD_SPACING); + addSettingByName(settings, allSettings, StrId::STR_TEXT_AA); settings.push_back(SettingInfo::Action(StrId::STR_MANAGE_FONTS, SettingAction::DownloadFonts)); addSettingByName(settings, allSettings, StrId::STR_SD_FONT_SIZE_RANGE); - addSettingByName(settings, allSettings, StrId::STR_TEXT_AA); return settings; } @@ -817,11 +1051,15 @@ inline bool hasSettingByName(const std::vector& allSettings, StrId inline std::vector buildControlsSettingsParentList(const std::vector& allSettings) { const bool hasTiltPageTurnSetting = hasSettingByName(allSettings, StrId::STR_TILT_PAGE_TURN); const bool hasTiltPageTurnDirectionSetting = hasSettingByName(allSettings, StrId::STR_TILT_PAGE_TURN_DIRECTION); + const bool hasFrontButtons = !gpio.hasTouch(); std::vector settings; - settings.reserve(3 + (hasTiltPageTurnSetting ? 1u : 0u) + (hasTiltPageTurnDirectionSetting ? 1u : 0u)); + settings.reserve(2 + (hasFrontButtons ? 1u : 0u) + (hasTiltPageTurnSetting ? 1u : 0u) + + (hasTiltPageTurnDirectionSetting ? 1u : 0u)); settings.push_back(SettingInfo::Submenu(StrId::STR_POWER_BUTTON, SettingAction::ControlsPowerButton)); - settings.push_back(SettingInfo::Submenu(StrId::STR_FRONT_BUTTONS, SettingAction::ControlsFrontButtons)); + if (hasFrontButtons) { + settings.push_back(SettingInfo::Submenu(StrId::STR_FRONT_BUTTONS, SettingAction::ControlsFrontButtons)); + } settings.push_back(SettingInfo::Submenu(StrId::STR_SIDE_BUTTONS, SettingAction::ControlsSideButtons)); if (hasTiltPageTurnSetting) addSettingByName(settings, allSettings, StrId::STR_TILT_PAGE_TURN); if (hasTiltPageTurnDirectionSetting) addSettingByName(settings, allSettings, StrId::STR_TILT_PAGE_TURN_DIRECTION); @@ -866,7 +1104,7 @@ inline std::vector buildControlsSideButtonSettingsList(const std::v inline std::vector buildGroupedDisplaySettingsList(const std::vector& allSettings) { std::vector displaySettings; - displaySettings.reserve(7); + displaySettings.reserve(8); auto addDisplaySetting = [&](StrId nameId) { const auto it = std::find_if(allSettings.begin(), allSettings.end(), @@ -883,6 +1121,7 @@ inline std::vector buildGroupedDisplaySettingsList(const std::vecto } addDisplaySetting(StrId::STR_REFRESH_FREQ); addDisplaySetting(StrId::STR_UI_THEME); + addDisplaySetting(StrId::STR_UI_SCALE); addDisplaySetting(StrId::STR_RECENT_BOOKS_VIEW); addDisplaySetting(StrId::STR_SUNLIGHT_FADING_FIX); @@ -926,13 +1165,15 @@ inline std::vector buildSystemSettingsParentList(const std::vector< inline std::vector buildSystemDeviceSettingsList(const std::vector& allSettings) { std::vector settings; - settings.reserve(7); + settings.reserve(9); addSettingByName(settings, allSettings, StrId::STR_DEVICE_NAME); addSettingByName(settings, allSettings, StrId::STR_TIME_TO_SLEEP); settings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language)); if (halClock.isAvailable()) { addSettingByName(settings, allSettings, StrId::STR_CLOCK_FORMAT); addSettingByName(settings, allSettings, StrId::STR_CLOCK_UTC_OFFSET); + addSettingByName(settings, allSettings, StrId::STR_DATE_FORMAT); + addSettingByName(settings, allSettings, StrId::STR_DATE_SEPARATOR); settings.push_back(SettingInfo::Action(StrId::STR_CLOCK_SYNC_NOW, SettingAction::ClockSync)); } return settings; diff --git a/src/SilentRestart.h b/src/SilentRestart.h index f94345c5ff..0736539541 100644 --- a/src/SilentRestart.h +++ b/src/SilentRestart.h @@ -1,8 +1,48 @@ #pragma once +#include +#include + // ESP.restart() with an RTC_NOINIT flag that survives the reboot, so setup() // skips the boot splash and routes straight to a destination. Used to clear // heap fragmentation accumulated during a wifi session. +enum class NetworkBootTarget : uint32_t { + OTA = 2, + OPDS = 3, + KOREADER_SYNC = 4, + KOREADER_AUTH = 5, + FILE_TRANSFER = 6, + MANAGE_FONTS = 7, +}; + +constexpr bool isNetworkBootTargetValue(const uint32_t value) { + switch (static_cast(value)) { + case NetworkBootTarget::OTA: + case NetworkBootTarget::OPDS: + case NetworkBootTarget::KOREADER_SYNC: + case NetworkBootTarget::KOREADER_AUTH: + case NetworkBootTarget::FILE_TRANSFER: + case NetworkBootTarget::MANAGE_FONTS: + return true; + } + return false; +} + +static_assert(isNetworkBootTargetValue(static_cast(NetworkBootTarget::OTA)) && + isNetworkBootTargetValue(static_cast(NetworkBootTarget::OPDS)) && + isNetworkBootTargetValue(static_cast(NetworkBootTarget::KOREADER_SYNC)) && + isNetworkBootTargetValue(static_cast(NetworkBootTarget::KOREADER_AUTH)) && + isNetworkBootTargetValue(static_cast(NetworkBootTarget::FILE_TRANSFER)) && + isNetworkBootTargetValue(static_cast(NetworkBootTarget::MANAGE_FONTS)), + "Every network boot target must pass RTC target validation"); + void silentRestart(); // home screen void silentRestartToReader(); // currently-open EPUB (APP_STATE.openEpubPath) +void silentRestartToNetwork(NetworkBootTarget target, uint32_t payload = 0); +void silentRestartToManageFonts(); + +void armSilentRestartReaderPageBuild(const std::string& bookPath, uint16_t spineIndex, uint16_t targetPage, + bool autoPageTurnActive); +bool consumeSilentRestartReaderPageBuild(const std::string& bookPath, uint16_t& spineIndex, uint16_t& targetPage, + bool& autoPageTurnActive); diff --git a/src/WifiCredentialStore.cpp b/src/WifiCredentialStore.cpp index 98aec23ec9..5007a8838a 100644 --- a/src/WifiCredentialStore.cpp +++ b/src/WifiCredentialStore.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace { @@ -23,6 +24,7 @@ void legacyDeobfuscate(std::string& data) { } // namespace void WifiCredentialStore::toJson(JsonDocument& doc) const { + std::lock_guard lock(credentialMutex); doc["lastConnectedSsid"] = lastConnectedSsid; JsonArray arr = doc["credentials"].to(); @@ -34,6 +36,7 @@ void WifiCredentialStore::toJson(JsonDocument& doc) const { } bool WifiCredentialStore::fromJson(JsonVariantConst doc) { + std::lock_guard lock(credentialMutex); lastConnectedSsid = doc["lastConnectedSsid"] | ""; // Tolerate a missing/invalid 'credentials' key (treat as empty list); only @@ -53,13 +56,24 @@ bool WifiCredentialStore::fromJson(JsonVariantConst doc) { } obfuscation::DecodeStatus status = obfuscation::DecodeStatus::INVALID; - cred.password = obfuscation::deobfuscateFromBase64(obj["password_obf"] | "", &status); + cred.password = obfuscation::deobfuscateFromBase64(obj["password_obf"] | "", MAX_PASSWORD_LENGTH, &status); + if (status == obfuscation::DecodeStatus::TOO_LONG) { + LOG_ERR("WCS", "Skipping WiFi credential with oversized password: %s", cred.ssid.c_str()); + needsResave = true; + continue; + } if (status == obfuscation::DecodeStatus::LEGACY && !cred.password.empty()) { needsResave = true; } if (status == obfuscation::DecodeStatus::INVALID || status == obfuscation::DecodeStatus::EMPTY || cred.password.empty()) { - cred.password = obj["password"] | ""; + const char* legacyPassword = obj["password"] | ""; + if (strlen(legacyPassword) > MAX_PASSWORD_LENGTH) { + LOG_ERR("WCS", "Skipping WiFi credential with oversized plaintext password: %s", cred.ssid.c_str()); + needsResave = true; + continue; + } + cred.password = legacyPassword; if (!cred.password.empty()) needsResave = true; } if (status == obfuscation::DecodeStatus::INVALID && cred.password.empty()) { @@ -69,17 +83,21 @@ bool WifiCredentialStore::fromJson(JsonVariantConst doc) { credentials.push_back(std::move(cred)); } - LOG_DBG("WCS", "Loaded %zu WiFi credentials from file", credentials.size()); - if (needsResave) { LOG_DBG("WCS", "Resaving JSON with obfuscated passwords"); - saveToFile(); + requestResave(); } return true; } bool WifiCredentialStore::loadFromFile() { + { + std::lock_guard lock(credentialMutex); + credentials.clear(); + lastConnectedSsid.clear(); + } + const bool hasStoreFile = Storage.exists(getFilePath()); if (PersistableStore::loadFromFile()) { return true; @@ -100,6 +118,10 @@ bool WifiCredentialStore::loadFromFile() { return false; } +void WifiCredentialStore::ensureLoaded() const { + std::call_once(loadOnce, [this] { const_cast(this)->loadFromFile(); }); +} + bool WifiCredentialStore::loadFromBinaryFile() { HalFile file; if (!Storage.openFileForRead("WCS", WIFI_FILE_BIN, file)) { @@ -136,73 +158,124 @@ bool WifiCredentialStore::loadFromBinaryFile() { } bool WifiCredentialStore::addCredential(const std::string& ssid, const std::string& password) { - // Check if this SSID already exists and update it - const auto cred = find_if(credentials.begin(), credentials.end(), - [&ssid](const WifiCredential& cred) { return cred.ssid == ssid; }); - if (cred != credentials.end()) { - cred->password = password; - LOG_DBG("WCS", "Updated credentials for: %s", ssid.c_str()); - return saveToFile(); - } - - // Check if we've reached the limit - if (credentials.size() >= MAX_NETWORKS) { - LOG_DBG("WCS", "Cannot add more networks, limit of %zu reached", MAX_NETWORKS); + ensureLoaded(); + if (password.size() > MAX_PASSWORD_LENGTH) { + LOG_ERR("WCS", "Refusing oversized WiFi password for %s", ssid.c_str()); return false; } - // Add new credential - credentials.push_back({ssid, password}); - LOG_DBG("WCS", "Added credentials for: %s", ssid.c_str()); + { + std::lock_guard lock(credentialMutex); + const auto cred = find_if(credentials.begin(), credentials.end(), + [&ssid](const WifiCredential& cred) { return cred.ssid == ssid; }); + if (cred != credentials.end()) { + cred->password = password; + } else { + if (credentials.size() >= MAX_NETWORKS) { + LOG_DBG("WCS", "Cannot add more networks, limit of %zu reached", MAX_NETWORKS); + return false; + } + + credentials.push_back({ssid, password}); + } + } return saveToFile(); } bool WifiCredentialStore::removeCredential(const std::string& ssid) { - const auto cred = find_if(credentials.begin(), credentials.end(), - [&ssid](const WifiCredential& cred) { return cred.ssid == ssid; }); - if (cred != credentials.end()) { + ensureLoaded(); + + { + std::lock_guard lock(credentialMutex); + const auto cred = find_if(credentials.begin(), credentials.end(), + [&ssid](const WifiCredential& cred) { return cred.ssid == ssid; }); + if (cred == credentials.end()) return false; credentials.erase(cred); - LOG_DBG("WCS", "Removed credentials for: %s", ssid.c_str()); - if (ssid == lastConnectedSsid) { - clearLastConnectedSsid(); - } - return saveToFile(); + if (ssid == lastConnectedSsid) lastConnectedSsid.clear(); } - return false; // Not found + return saveToFile(); } -const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssid) const { +std::optional WifiCredentialStore::findCredential(const std::string& ssid) const { + ensureLoaded(); + std::lock_guard lock(credentialMutex); + const auto cred = find_if(credentials.begin(), credentials.end(), [&ssid](const WifiCredential& cred) { return cred.ssid == ssid; }); + return cred != credentials.end() ? std::optional(*cred) : std::nullopt; +} - if (cred != credentials.end()) { - return &*cred; - } +std::optional WifiCredentialStore::getCredentialAt(const size_t index) const { + ensureLoaded(); + std::lock_guard lock(credentialMutex); + return index < credentials.size() ? std::optional(credentials[index]) : std::nullopt; +} + +std::optional WifiCredentialStore::getSsidAt(const size_t index) const { + ensureLoaded(); + std::lock_guard lock(credentialMutex); + return index < credentials.size() ? std::optional(credentials[index].ssid) : std::nullopt; +} - return nullptr; +size_t WifiCredentialStore::getCredentialCount() const { + ensureLoaded(); + std::lock_guard lock(credentialMutex); + return credentials.size(); } -bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { return findCredential(ssid) != nullptr; } +std::vector WifiCredentialStore::getCredentialSummaries() const { + ensureLoaded(); + std::lock_guard lock(credentialMutex); + std::vector summaries; + summaries.reserve(credentials.size()); + for (const auto& credential : credentials) { + summaries.push_back({credential.ssid, !credential.password.empty(), credential.ssid == lastConnectedSsid}); + } + return summaries; +} + +bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { + ensureLoaded(); + std::lock_guard lock(credentialMutex); + return find_if(credentials.begin(), credentials.end(), + [&ssid](const WifiCredential& cred) { return cred.ssid == ssid; }) != credentials.end(); +} void WifiCredentialStore::setLastConnectedSsid(const std::string& ssid) { - if (lastConnectedSsid != ssid) { + ensureLoaded(); + + { + std::lock_guard lock(credentialMutex); + if (lastConnectedSsid == ssid) return; lastConnectedSsid = ssid; - saveToFile(); } + saveToFile(); } -const std::string& WifiCredentialStore::getLastConnectedSsid() const { return lastConnectedSsid; } +std::string WifiCredentialStore::getLastConnectedSsid() const { + ensureLoaded(); + std::lock_guard lock(credentialMutex); + return lastConnectedSsid; +} void WifiCredentialStore::clearLastConnectedSsid() { - if (!lastConnectedSsid.empty()) { + ensureLoaded(); + + { + std::lock_guard lock(credentialMutex); + if (lastConnectedSsid.empty()) return; lastConnectedSsid.clear(); - saveToFile(); } + saveToFile(); } void WifiCredentialStore::clearAll() { - credentials.clear(); - lastConnectedSsid.clear(); + ensureLoaded(); + + { + std::lock_guard lock(credentialMutex); + credentials.clear(); + lastConnectedSsid.clear(); + } saveToFile(); - LOG_DBG("WCS", "Cleared all WiFi credentials"); } diff --git a/src/WifiCredentialStore.h b/src/WifiCredentialStore.h index e4e197500b..5c1c397359 100644 --- a/src/WifiCredentialStore.h +++ b/src/WifiCredentialStore.h @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -10,6 +12,12 @@ struct WifiCredential { std::string password; // Plaintext in memory; obfuscated with hardware key on disk }; +struct WifiCredentialSummary { + std::string ssid; + bool hasPassword = false; + bool isLastConnected = false; +}; + /** * Singleton class for storing WiFi credentials on the SD card. * Passwords are XOR-obfuscated with the device's unique hardware MAC address @@ -20,12 +28,17 @@ class WifiCredentialStore : public PersistableStore { private: std::vector credentials; std::string lastConnectedSsid; + mutable std::mutex credentialMutex; + mutable std::once_flag loadOnce; static constexpr size_t MAX_NETWORKS = 8; + static constexpr size_t MAX_PASSWORD_LENGTH = 64; // Private constructor for singleton WifiCredentialStore() = default; + bool loadFromFile(); bool loadFromBinaryFile(); + void ensureLoaded() const; friend class PersistableStore; @@ -33,22 +46,24 @@ class WifiCredentialStore : public PersistableStore { static const char* getFilePath() { return "/.crosspoint/wifi.json"; } void toJson(JsonDocument& doc) const; bool fromJson(JsonVariantConst doc); - bool loadFromFile(); // Credential management bool addCredential(const std::string& ssid, const std::string& password); bool removeCredential(const std::string& ssid); - const WifiCredential* findCredential(const std::string& ssid) const; - - // Get all stored credentials (for UI display) - const std::vector& getCredentials() const { return credentials; } + std::optional findCredential(const std::string& ssid) const; + std::optional getCredentialAt(size_t index) const; + std::optional getSsidAt(size_t index) const; + size_t getCredentialCount() const; + // Bounded to MAX_NETWORKS (8): copy SSIDs so the web task never holds the + // credential mutex across blocking network writes. + std::vector getCredentialSummaries() const; // Check if a network is saved bool hasSavedCredential(const std::string& ssid) const; // Last connected network void setLastConnectedSsid(const std::string& ssid); - const std::string& getLastConnectedSsid() const; + std::string getLastConnectedSsid() const; void clearLastConnectedSsid(); // Clear all credentials diff --git a/src/activities/Activity.h b/src/activities/Activity.h index 31d7f343c8..7e16af8c7b 100644 --- a/src/activities/Activity.h +++ b/src/activities/Activity.h @@ -38,6 +38,9 @@ class Activity { virtual void render(RenderLock&&) {} + // Let activities that need more than a framebuffer refresh prepare a full redraw. + virtual bool prepareManualRefresh() { return false; } + // If immediate is true, the update will be triggered immediately. // Otherwise, it will be deferred until the end of the current loop iteration. virtual void requestUpdate(bool immediate = false); @@ -48,8 +51,19 @@ class Activity { virtual bool skipLoopDelay() { return false; } virtual bool preventAutoSleep() { return false; } virtual bool isReaderActivity() const { return false; } + virtual bool isHomeActivity() const { return false; } + // The open book uses its vertical swipe actions across the entire page; + // dialogs and lists retain their normal edge or scroll gestures. + virtual bool usesFullScreenReaderVerticalSwipes() const { return false; } + // Overlays can reserve the frontlight gesture for their own dismissal. + virtual bool allowFrontlightPanelGesture() const { return true; } virtual bool allowPowerAsConfirmInReaderMode() const { return false; } + virtual bool allowGlobalHomeGesture() const { return true; } + // Let overlays consume the global Home gesture as a dismiss action. + virtual bool handleHomeGesture() { return false; } virtual bool canSnapshotForSleepOverlay() const { return false; } + virtual bool handlesReaderPowerSettingsOverride() const { return false; } + virtual bool openReaderSettingsMenu() { return false; } virtual std::string getCurrentBookPath() const { return {}; } virtual ScreenshotInfo getScreenshotInfo() const { return {}; } diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index 2e978466c1..f1643446cc 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -1,15 +1,22 @@ #include "ActivityManager.h" +#include #include #include +#include +#include +#include #include +#include "CrossPointSettings.h" #include "CrossPointState.h" #include "OpdsServerStore.h" +#include "SilentRestart.h" #include "boot_sleep/BootActivity.h" #include "boot_sleep/SleepActivity.h" #include "browser/OpdsBookBrowserActivity.h" +#include "components/TouchRegistry.h" #include "home/AlertActivity.h" #include "home/CrashActivity.h" #include "home/FileBrowserActivity.h" @@ -17,22 +24,41 @@ #include "home/RecentBooksActivity.h" #include "home/RecentBooksGridActivity.h" #include "network/CrossPointWebServerActivity.h" +#include "network/NearbyBookTransferActivity.h" #include "network/NearbyStatsSyncActivity.h" #include "reader/ReaderActivity.h" #include "settings/OpdsServerListActivity.h" #include "settings/SettingsActivity.h" +#include "util/FrontlightPanelActivity.h" #include "util/FullScreenMessageActivity.h" -void ActivityManager::begin() { - xTaskCreatePinnedToCore(&renderTaskTrampoline, "ActivityManagerRender", - 16384, // Stack size - createSectionFile() puts ChapterHtmlSlimParser on stack during - // silentIndexNextChapterIfNeeded - this, // Parameters - 1, // Priority +namespace { +constexpr uint32_t FILE_TRANSFER_MODE_MASK = 0xFF; +constexpr uint32_t FILE_TRANSFER_RETURN_TO_READER = 1U << 8; + +uint32_t fileTransferBootPayload(const NetworkMode mode, const bool returnToReader) { + return static_cast(mode) | (returnToReader ? FILE_TRANSFER_RETURN_TO_READER : 0); +} + +void restartToFileTransfer(const NetworkMode mode, const std::string& returnBookPath) { + silentRestartToNetwork(NetworkBootTarget::FILE_TRANSFER, fileTransferBootPayload(mode, !returnBookPath.empty())); +} +} // namespace + +void ActivityManager::begin(const uint32_t renderTaskStackBytes) { +#if defined(configNUM_CORES) && configNUM_CORES > 1 + constexpr BaseType_t renderTaskCore = 1; +#else + constexpr BaseType_t renderTaskCore = 0; +#endif + xTaskCreatePinnedToCore(&renderTaskTrampoline, "ActivityManagerRender", renderTaskStackBytes, + this, // Parameters + 1, // Priority &renderTaskHandle, // Task handle - 0 // Pin to core 0 (PRO_CPU) + renderTaskCore // Keep long renders/cover decodes off CPU 0's idle watchdog when available ); assert(renderTaskHandle != nullptr && "Failed to create render task"); + LOG_DBG("ACT", "Render task started with %lu-byte stack", static_cast(renderTaskStackBytes)); } void ActivityManager::renderTaskTrampoline(void* param) { @@ -46,10 +72,13 @@ void ActivityManager::renderTaskLoop() { // Acquire the lock before reading currentActivity to avoid a TOCTOU race // where the main task deletes the activity between the null-check and render(). RenderLock lock; + TouchRegistry::getInstance().setEnabled(mappedInput.hasTouch()); + TouchRegistry::getInstance().beginFrame(); if (currentActivity) { HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering currentActivity->render(std::move(lock)); } + TouchRegistry::getInstance().publish(); // Notify any task blocked in requestUpdateAndWait() that the render is done. TaskHandle_t waiter = nullptr; taskENTER_CRITICAL(&renderStateMux); @@ -65,8 +94,22 @@ void ActivityManager::renderTaskLoop() { void ActivityManager::loop() { if (currentActivity) { mappedInput.setPowerAsConfirmInReaderMode(currentActivity->allowPowerAsConfirmInReaderMode()); + + // Frontlight quick panel: top-edge down-swipe on home-key boards, except + // that the open EPUB reader exposes the same action across the whole page. + // Pushed, so it returns to whatever was underneath — including mid-book. + const bool lightPanelGesture = currentActivity->usesFullScreenReaderVerticalSwipes() + ? mappedInput.wasReaderLightPanelGesture() + : mappedInput.wasLightPanelGesture(); + if (Frontlight.present() && currentActivity->name != "FrontlightPanel" && + currentActivity->allowFrontlightPanelGesture() && lightPanelGesture) { + pushActivity(std::make_unique(renderer, mappedInput)); + return; + } // Note: do not hold a lock here, the loop() method must be responsible for acquire one if needed - currentActivity->loop(); + if (!handleReaderPowerButtonSettingsOverride() && !handleGlobalHomeGesture()) { + currentActivity->loop(); + } } else { mappedInput.setPowerAsConfirmInReaderMode(false); } @@ -89,7 +132,6 @@ void ActivityManager::loop() { pendingAction = PendingAction::None; if (stackActivities.empty()) { - LOG_DBG("ACT", "No more activities on stack, going home"); lock.unlock(); // goHome may acquire its own lock goHome(); continue; // Will launch goHome immediately @@ -97,11 +139,8 @@ void ActivityManager::loop() { } else { currentActivity = std::move(stackActivities.back()); stackActivities.pop_back(); - LOG_DBG("ACT", "Popped from activity stack, new size = %zu", stackActivities.size()); // Handle result if necessary if (currentActivity->resultHandler) { - LOG_DBG("ACT", "Handling result for popped activity"); - // Move it here to avoid the case where handler calling another startActivityForResult() auto handler = std::move(currentActivity->resultHandler); currentActivity->resultHandler = nullptr; @@ -137,7 +176,6 @@ void ActivityManager::loop() { } else if (pendingAction == PendingAction::Push) { // Move current activity to stack stackActivities.push_back(std::move(currentActivity)); - LOG_DBG("ACT", "Pushed to activity stack, new size = %zu", stackActivities.size()); } pendingAction = PendingAction::None; currentActivity = std::move(pendingActivity); @@ -164,9 +202,47 @@ void ActivityManager::loop() { } } +bool ActivityManager::handleGlobalHomeGesture() { + if (!currentActivity || pendingAction != PendingAction::None || currentActivity->isHomeActivity() || + !currentActivity->allowGlobalHomeGesture() || (!mappedInput.hasTouch() && !mappedInput.hasHomeKey())) { + return false; + } + + const bool homeGesture = currentActivity->usesFullScreenReaderVerticalSwipes() ? mappedInput.wasReaderHomeGesture() + : mappedInput.wasHomeGesture(); + if (!homeGesture) { + return false; + } + + if (currentActivity->handleHomeGesture()) { + return true; + } + + goHome(); + return true; +} + +bool ActivityManager::handleReaderPowerButtonSettingsOverride() { + if (!readerPowerButtonOpensSettings()) { + return false; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Power)) { + if (!currentActivity->openReaderSettingsMenu()) { + goToSettings(); + } + return true; + } + + // Do not let reader activities run configured short/long Power actions while + // the button is held. Its release is reserved for restoring Settings access. + return mappedInput.isPressed(MappedInputManager::Button::Power); +} + void ActivityManager::exitActivity(const RenderLock& lock) { // Note: lock must be held by the caller if (currentActivity) { + TouchRegistry::getInstance().clear(); currentActivity->onExit(); currentActivity.reset(); } @@ -175,12 +251,14 @@ void ActivityManager::exitActivity(const RenderLock& lock) { void ActivityManager::replaceActivity(std::unique_ptr&& newActivity) { // Note: no lock here, this is usually called by loop() and we may run into deadlock if (currentActivity) { + TouchRegistry::getInstance().clear(); // Defer launch if we're currently in an activity, to avoid deleting the current activity // leading to the "delete this" problem pendingActivity = std::move(newActivity); pendingAction = PendingAction::Replace; } else { // No current activity, safe to launch immediately + TouchRegistry::getInstance().clear(); currentActivity = std::move(newActivity); currentActivity->onEnter(); } @@ -190,26 +268,75 @@ void ActivityManager::goToFileTransfer(std::string returnBookPath) { replaceActivity(std::make_unique(renderer, mappedInput, std::move(returnBookPath))); } -void ActivityManager::goToCalibreWireless(std::string returnBookPath) { - replaceActivity(std::make_unique(renderer, mappedInput, NetworkMode::CONNECT_CALIBRE, - std::move(returnBookPath))); +void ActivityManager::goToNearbyBookSend(std::string path, const bool returnToReader) { + auto activity = makeUniqueNoThrow( + renderer, mappedInput, NearbyBookTransferActivity::Mode::Send, std::move(path), returnToReader); + if (!activity) { + LOG_ERR("ACT", "OOM: nearby file sender"); + return; + } + replaceActivity(std::move(activity)); } -void ActivityManager::goToJoinNetworkFileTransfer(std::string returnBookPath) { - replaceActivity(std::make_unique(renderer, mappedInput, NetworkMode::JOIN_NETWORK, - std::move(returnBookPath))); +void ActivityManager::goToNearbyBookReceive() { + auto activity = + makeUniqueNoThrow(renderer, mappedInput, NearbyBookTransferActivity::Mode::Receive); + if (!activity) { + LOG_ERR("ACT", "OOM: nearby file receiver"); + return; + } + replaceActivity(std::move(activity)); } -void ActivityManager::goToHotspotFileTransfer(std::string returnBookPath) { - replaceActivity(std::make_unique(renderer, mappedInput, NetworkMode::CREATE_HOTSPOT, - std::move(returnBookPath))); +void ActivityManager::goToCalibreWireless(const std::string& returnBookPath) { + restartToFileTransfer(NetworkMode::CONNECT_CALIBRE, returnBookPath); +} + +void ActivityManager::goToJoinNetworkFileTransfer(const std::string& returnBookPath) { + restartToFileTransfer(NetworkMode::JOIN_NETWORK, returnBookPath); +} + +void ActivityManager::goToHotspotFileTransfer(const std::string& returnBookPath) { + restartToFileTransfer(NetworkMode::CREATE_HOTSPOT, returnBookPath); +} + +bool ActivityManager::resumeFileTransferFromNetworkBoot(const uint32_t payload) { + const uint32_t rawMode = payload & FILE_TRANSFER_MODE_MASK; + if (rawMode > static_cast(NetworkMode::CREATE_HOTSPOT)) { + LOG_ERR("ACT", "Invalid file transfer network boot mode: %lu", static_cast(rawMode)); + return false; + } + + std::string returnBookPath; + if ((payload & FILE_TRANSFER_RETURN_TO_READER) != 0) { + if (!APP_STATE.openEpubPath.empty() && Storage.exists(APP_STATE.openEpubPath.c_str())) { + returnBookPath = APP_STATE.openEpubPath; + } else { + LOG_ERR("ACT", "File transfer cannot return to missing reader path: %s", APP_STATE.openEpubPath.c_str()); + } + } + + // The activity must outlive this boot function, so allocate its small control object on the heap; web buffers + // remain owned and released by the activity lifecycle. + auto activity = makeUniqueNoThrow( + renderer, mappedInput, static_cast(rawMode), std::move(returnBookPath), true); + if (!activity) { + LOG_ERR("ACT", "OOM: file transfer after minimal boot (free=%u maxAlloc=%u)", ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + return false; + } + + replaceActivity(std::move(activity)); + return true; } void ActivityManager::goToNearbyStatsSync() { replaceActivity(std::make_unique(renderer, mappedInput)); } -void ActivityManager::goToSettings() { replaceActivity(std::make_unique(renderer, mappedInput)); } +void ActivityManager::goToSettings(const bool dismissOnUpSwipe) { + replaceActivity(std::make_unique(renderer, mappedInput, dismissOnUpSwipe)); +} void ActivityManager::goToFileBrowser(std::string path) { replaceActivity(std::make_unique(renderer, mappedInput, std::move(path))); @@ -227,20 +354,56 @@ void ActivityManager::goToBrowser() { const auto& servers = OPDS_STORE.getServers(); // Skip the server picker when there's only one server configured if (servers.size() == 1) { - replaceActivity(std::make_unique(renderer, mappedInput, servers[0])); + goToOpdsServer(0); } else { replaceActivity(std::make_unique(renderer, mappedInput, true)); } } -void ActivityManager::goToReader(std::string path, const bool suppressBackRelease) { - replaceActivity(std::make_unique(renderer, mappedInput, std::move(path), suppressBackRelease)); +bool ActivityManager::goToOpdsServer(const uint32_t serverIndex, const bool networkBootReady) { +#ifndef SIMULATOR + if (!networkBootReady) { + silentRestartToNetwork(NetworkBootTarget::OPDS, serverIndex); + return true; + } +#else + // The desktop build has no fragmented WiFi heap to clear; keep the preview in-process. + (void)networkBootReady; +#endif + + OPDS_STORE.loadFromFile(); + const auto* storedServer = OPDS_STORE.getServer(serverIndex); + if (!storedServer) { + LOG_ERR("ACT", "OPDS network boot server index out of range: %lu", static_cast(serverIndex)); + return false; + } + + OpdsServer server = *storedServer; + OPDS_STORE.release(); + auto browser = makeUniqueNoThrow(renderer, mappedInput, std::move(server)); + if (!browser) { + LOG_ERR("ACT", "OOM: OPDS browser after minimal boot (free=%u maxAlloc=%u)", ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + return false; + } + replaceActivity(std::move(browser)); + return true; +} + +void ActivityManager::goToReader(std::string path, const bool suppressBackRelease, const bool allowFastInitialRefresh, + const bool cleanImageBaseOnEntry) { + // OPDS credentials are unrelated to local reading and may contain several + // heap-backed strings. Home reloads them lazily when it becomes active. + OPDS_STORE.release(); + replaceActivity(std::make_unique(renderer, mappedInput, std::move(path), suppressBackRelease, + allowFastInitialRefresh, cleanImageBaseOnEntry)); } void ActivityManager::goToSleep(bool fromTimeout) { const bool canSnapshotOverlay = currentActivity && currentActivity->canSnapshotForSleepOverlay(); - replaceActivity( - std::make_unique(renderer, mappedInput, canSnapshotOverlay, getCurrentBookPath(), fromTimeout)); + const GfxRenderer::Orientation sleepPopupOrientation = renderer.getOrientation(); + replaceActivity(std::make_unique(renderer, mappedInput, canSnapshotOverlay, getCurrentBookPath(), + fromTimeout, sleepPopupOrientation)); loop(); // Important: sleep screen must be rendered immediately, the caller will go to sleep right after this returns } @@ -250,7 +413,7 @@ void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily:: replaceActivity(std::make_unique(renderer, mappedInput, std::move(message), style)); } -void ActivityManager::goHome(HomeMenuItem initialMenuItem) { +void ActivityManager::goHome(HomeMenuItem initialMenuItem, const bool initialFullRefresh) { if (initialMenuItem == HomeMenuItem::NONE && currentActivity) { const auto& activityName = currentActivity->name; if (activityName == "FileBrowser") { @@ -267,7 +430,7 @@ void ActivityManager::goHome(HomeMenuItem initialMenuItem) { initialMenuItem = HomeMenuItem::SETTINGS_MENU; } } - replaceActivity(std::make_unique(renderer, mappedInput, initialMenuItem)); + replaceActivity(std::make_unique(renderer, mappedInput, initialMenuItem, initialFullRefresh)); } void ActivityManager::goToCrashReport() { replaceActivity(std::make_unique(renderer, mappedInput)); } @@ -277,6 +440,7 @@ void ActivityManager::pushActivity(std::unique_ptr&& activity) { LOG_ERR("ACT", "pendingActivity while pushActivity is not expected"); pendingActivity.reset(); } + TouchRegistry::getInstance().clear(); pendingActivity = std::move(activity); pendingAction = PendingAction::Push; } @@ -287,6 +451,7 @@ void ActivityManager::popActivity() { LOG_ERR("ACT", "pendingActivity while popActivity is not expected"); pendingActivity.reset(); } + TouchRegistry::getInstance().clear(); pendingAction = PendingAction::Pop; } @@ -303,10 +468,41 @@ bool ActivityManager::isReaderActivity() const { [](const auto& activity) { return activity && activity->isReaderActivity(); }); } +bool ActivityManager::readerPowerButtonOpensSettings() const { + return mappedInput.hasTouchHardware() && SETTINGS.disableReaderTouchscreen && currentActivity && + currentActivity->handlesReaderPowerSettingsOverride(); +} + +bool ActivityManager::hasActivityNamed(const char* activityName) const { + const auto matches = [activityName](const auto& activity) { return activity && activity->name == activityName; }; + if (matches(currentActivity) || matches(pendingActivity)) { + return true; + } + + return std::any_of(stackActivities.begin(), stackActivities.end(), matches); +} + +#ifdef SIMULATOR +bool ActivityManager::isCurrentActivityNamed(const char* activityName) const { + return currentActivity && currentActivity->name == activityName; +} +#endif + bool ActivityManager::canSnapshotForSleepOverlay() const { return currentActivity && currentActivity->canSnapshotForSleepOverlay(); } +bool ActivityManager::requestManualReaderRefresh() { + RenderLock lock; + if (!currentActivity || !currentActivity->isReaderActivity() || !currentActivity->prepareManualRefresh()) { + return false; + } + + lock.unlock(); + requestUpdate(true); + return true; +} + bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); } std::string ActivityManager::getCurrentBookPath() const { @@ -331,8 +527,23 @@ std::string ActivityManager::getCurrentBookPath() const { ScreenshotInfo ActivityManager::getScreenshotInfo() const { if (currentActivity) { - return currentActivity->getScreenshotInfo(); + const ScreenshotInfo info = currentActivity->getScreenshotInfo(); + if (info.readerType != ScreenshotInfo::ReaderType::None) { + return info; + } + } + + // Reader overlays such as the dictionary are pushed above the book activity. + // Keep their visible framebuffer, but inherit the book's screenshot filename. + for (auto it = stackActivities.rbegin(); it != stackActivities.rend(); ++it) { + if (*it) { + const ScreenshotInfo info = (*it)->getScreenshotInfo(); + if (info.readerType != ScreenshotInfo::ReaderType::None) { + return info; + } + } } + return {}; } diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h index 717c1379f5..8e73672dfb 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -75,6 +75,9 @@ class ActivityManager { // This variable must only be set by the main loop, to avoid race conditions std::atomic requestedUpdate{false}; + bool handleGlobalHomeGesture(); + bool handleReaderPowerButtonSettingsOverride(); + public: explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput) : renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) { @@ -83,7 +86,7 @@ class ActivityManager { } ~ActivityManager() { assert(false); /* should never be called */ }; - void begin(); + void begin(uint32_t renderTaskStackBytes = 16384); void loop(); // Will replace currentActivity and drop all activities on stack @@ -91,20 +94,25 @@ class ActivityManager { // goTo... functions are convenient wrapper for replaceActivity() void goToFileTransfer(std::string returnBookPath = {}); - void goToCalibreWireless(std::string returnBookPath = {}); - void goToJoinNetworkFileTransfer(std::string returnBookPath = {}); - void goToHotspotFileTransfer(std::string returnBookPath = {}); + void goToCalibreWireless(const std::string& returnBookPath = {}); + void goToJoinNetworkFileTransfer(const std::string& returnBookPath = {}); + void goToHotspotFileTransfer(const std::string& returnBookPath = {}); + bool resumeFileTransferFromNetworkBoot(uint32_t payload); void goToNearbyStatsSync(); - void goToSettings(); + void goToNearbyBookSend(std::string path, bool returnToReader); + void goToNearbyBookReceive(); + void goToSettings(bool dismissOnUpSwipe = false); void goToFileBrowser(std::string path = {}); void goToRecentBooks(); void goToBrowser(); - void goToReader(std::string path, bool suppressBackRelease = false); + bool goToOpdsServer(uint32_t serverIndex, bool networkBootReady = false); + void goToReader(std::string path, bool suppressBackRelease = false, bool allowFastInitialRefresh = false, + bool cleanImageBaseOnEntry = false); void goToSleep(bool fromTimeout = false); void goToBoot(); void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR); void goToCrashReport(); - void goHome(HomeMenuItem initialMenuItem = HomeMenuItem::NONE); + void goHome(HomeMenuItem initialMenuItem = HomeMenuItem::NONE, bool initialFullRefresh = false); // This will move current activity to stack instead of deleting it void pushActivity(std::unique_ptr&& activity); @@ -116,7 +124,13 @@ class ActivityManager { bool preventAutoSleep() const; bool isHomeActivity() const; bool isReaderActivity() const; + bool readerPowerButtonOpensSettings() const; + bool hasActivityNamed(const char* activityName) const; +#ifdef SIMULATOR + bool isCurrentActivityNamed(const char* activityName) const; +#endif bool canSnapshotForSleepOverlay() const; + bool requestManualReaderRefresh(); bool skipLoopDelay() const; std::string getCurrentBookPath() const; ScreenshotInfo getScreenshotInfo() const; diff --git a/src/activities/ActivityResult.h b/src/activities/ActivityResult.h index 060e880f95..d5c95996bd 100644 --- a/src/activities/ActivityResult.h +++ b/src/activities/ActivityResult.h @@ -48,6 +48,10 @@ struct PageResult { struct ProgressChangeResult { int spineIndex = 0; int page = 0; + int totalPages = 0; + std::string xpath; + float percentage = 0.0f; + bool hasSavedProgress = false; }; struct SyncResult { @@ -79,6 +83,10 @@ struct FilePathResult { std::string path; }; +struct WordResult { + std::string word; +}; + struct ReadingStatsResult { bool changed = false; }; @@ -114,7 +122,7 @@ struct ClippingJumpResult { using ResultVariant = std::variant; + FilePathResult, WordResult, ReadingStatsResult, ClippingResult, ClippingJumpResult>; struct ActivityResult { bool isCancelled = false; diff --git a/src/activities/ScreenTransitionRefresh.h b/src/activities/ScreenTransitionRefresh.h new file mode 100644 index 0000000000..fb23c68441 --- /dev/null +++ b/src/activities/ScreenTransitionRefresh.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +#include + +// Network workflows replace large parts of the screen as their state changes. +// X4-class panels need a clean waveform for those transitions; repeated renders +// within the same state (progress, signal strength, selection) remain fast. +class ScreenTransitionRefresh { + public: + HalDisplay::RefreshMode modeFor(const uint8_t screenState) { + const bool screenChanged = !hasRendered_ || screenState != lastScreenState_; + lastScreenState_ = screenState; + hasRendered_ = true; + return screenChanged && !gpio.deviceIsX3() ? HalDisplay::FULL_REFRESH : HalDisplay::FAST_REFRESH; + } + + private: + uint8_t lastScreenState_ = 0; + bool hasRendered_ = false; +}; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 834245b192..2b82650dac 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include "../home/RecentBookProgress.h" #include "../reader/BookStatsView.h" @@ -259,7 +261,7 @@ std::string loadChapterTitleForPath(const std::string& path) { } Epub epub(path, "/.crosspoint"); - if (!epub.load(false, true)) { + if (!epub.load(false, true, Epub::XLocationLoadMode::Skip)) { return {}; } @@ -357,16 +359,29 @@ bool selectPinnedSleepImage(SleepImageMode mode, SleepImageSelection& selection) return false; } -bool selectRandomSleepImage(SleepImageMode mode, SleepImageSelection& selection) { +bool selectRandomSleepImage(SleepImageMode mode, SleepImageSelection& selection, bool validateBmpHeaders = false, + bool bmpOnly = false) { FsFile dir; std::string sleepDir; if (!openPreferredSleepDirectory(dir, sleepDir)) { return false; } - const bool allowPng = mode == SleepImageMode::Overlay; - std::vector files; - files.reserve(16); + const bool allowPng = mode == SleepImageMode::Overlay && !bmpOnly; + // Keep one reservoir for every candidate and one that excludes recent images. + // This avoids holding the whole directory in RAM or opening every BMP just to + // parse its header before picking one. + std::string nonRecentPath; + uint16_t candidateCount = 0; + uint16_t selectedIndex = 0; + uint16_t nonRecentCount = 0; + uint16_t nonRecentIndex = 0; + const uint8_t recentWindow = std::min(APP_STATE.recentSleepFill, CrossPointState::SLEEP_RECENT_COUNT); + const auto setSleepImagePath = [&](std::string& path, std::string_view filename) { + path = sleepDir; + path += '/'; + path.append(filename.data(), filename.size()); + }; char name[500]; for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { if (file.isDirectory()) { @@ -375,8 +390,8 @@ bool selectRandomSleepImage(SleepImageMode mode, SleepImageSelection& selection) } file.getName(name, sizeof(name)); - std::string filename(name); - if (filename.empty() || filename[0] == '.') { + const std::string_view filename(name); + if (filename.empty() || filename.front() == '.') { file.close(); continue; } @@ -388,37 +403,57 @@ bool selectRandomSleepImage(SleepImageMode mode, SleepImageSelection& selection) continue; } - if (isBmp) { + // The normal path defers this SD read until after selection so folders with + // many wallpapers stay fast. Only use the slower validation pass after a + // selected BMP failed to render. + if (isBmp && validateBmpHeaders) { Bitmap bitmap(file); const BmpReaderError parseResult = bitmap.parseHeaders(); if (parseResult != BmpReaderError::Ok) { - LOG_ERR("SLP", "Skipping invalid BMP sleep image %s/%s: %s", sleepDir.c_str(), filename.c_str(), - Bitmap::errorToString(parseResult)); + LOG_ERR("SLP", "Skipping invalid BMP sleep image %s/%.*s: %s", sleepDir.c_str(), + static_cast(filename.size()), filename.data(), Bitmap::errorToString(parseResult)); file.close(); continue; } } - files.emplace_back(std::move(filename)); + if (candidateCount == UINT16_MAX) { + file.close(); + continue; + } + + candidateCount++; + const uint16_t candidateIndex = candidateCount - 1; + if (random(candidateCount) == 0) { + setSleepImagePath(selection.path, filename); + selectedIndex = candidateIndex; + } + + if (!APP_STATE.isRecentSleep(candidateIndex, recentWindow)) { + nonRecentCount++; + if (random(nonRecentCount) == 0) { + setSleepImagePath(nonRecentPath, filename); + nonRecentIndex = candidateIndex; + } + } file.close(); } dir.close(); - if (files.empty()) { + if (candidateCount == 0) { return false; } - const uint16_t fileCount = static_cast(std::min(files.size(), static_cast(UINT16_MAX))); - const uint8_t window = - static_cast(std::min(static_cast(APP_STATE.recentSleepFill), files.size() - 1)); - auto randomFileIndex = static_cast(random(fileCount)); - for (uint8_t attempt = 0; attempt < 20 && APP_STATE.isRecentSleep(randomFileIndex, window); attempt++) { - randomFileIndex = static_cast(random(fileCount)); + if (nonRecentCount > 0) { + selection.path = std::move(nonRecentPath); + selectedIndex = nonRecentIndex; } - APP_STATE.pushRecentSleep(randomFileIndex); + // With fewer images than the recent-history window, every candidate can be + // recent. Fall back to the all-candidates reservoir so a custom sleep screen + // still renders. + APP_STATE.pushRecentSleep(selectedIndex); APP_STATE.saveToFile(); - selection.path = sleepDir + "/" + files[randomFileIndex]; selection.isPng = FsHelpers::hasPngExtension(selection.path); return true; } @@ -440,10 +475,10 @@ void SleepActivity::onEnter() { overlayBackgroundBufferStored = SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::OVERLAY && renderer.storeBwBuffer(); - // Show the popup in the reader's orientation when sleep starts from an open book. - // Reset to portrait afterwards so the sleep screen renderer keeps its existing layout. + // Show the popup in the orientation that was visible before reader exit restores + // global settings. Reset to portrait afterwards so sleep screen layout stays unchanged. if (APP_STATE.lastSleepFromReader) { - ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); + renderer.setOrientation(sleepPopupOrientation); GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP)); renderer.setOrientation(GfxRenderer::Orientation::Portrait); } else { @@ -479,22 +514,42 @@ void SleepActivity::onEnter() { } void SleepActivity::renderCustomSleepScreen() const { - SleepImageSelection selection; - if (selectPinnedSleepImage(SleepImageMode::Custom, selection) || - selectRandomSleepImage(SleepImageMode::Custom, selection)) { + const auto tryRenderSelection = [this](const SleepImageSelection& selection) { FsFile file; - if (Storage.openFileForRead("SLP", selection.path, file)) { - LOG_INF("SLP", "Loading custom sleep image: %s", selection.path.c_str()); - delay(100); - Bitmap bitmap(file, true); - if (bitmap.parseHeaders() == BmpReaderError::Ok) { - renderBitmapSleepScreen(bitmap); - return; - } - LOG_ERR("SLP", "Failed to parse custom sleep BMP: %s", selection.path.c_str()); - } else { + if (!Storage.openFileForRead("SLP", selection.path, file)) { LOG_ERR("SLP", "Failed to open custom sleep image: %s", selection.path.c_str()); + return false; + } + + LOG_INF("SLP", "Loading custom sleep image: %s", selection.path.c_str()); + delay(100); + Bitmap bitmap(file, true); + const BmpReaderError parseResult = bitmap.parseHeaders(); + if (parseResult != BmpReaderError::Ok) { + LOG_ERR("SLP", "Failed to parse custom sleep BMP %s: %s", selection.path.c_str(), + Bitmap::errorToString(parseResult)); + return false; } + + renderBitmapSleepScreen(bitmap); + return true; + }; + + SleepImageSelection selection; + if (selectPinnedSleepImage(SleepImageMode::Custom, selection) && tryRenderSelection(selection)) { + return; + } + + if (selectRandomSleepImage(SleepImageMode::Custom, selection) && tryRenderSelection(selection)) { + return; + } + + // A corrupt BMP should not make an otherwise valid custom folder fall back + // to the dark default screen. Re-scan only on this error path and choose + // from the files whose headers are valid. + if (!selection.path.empty() && selectRandomSleepImage(SleepImageMode::Custom, selection, true) && + tryRenderSelection(selection)) { + return; } // Look for sleep.bmp on the root of the sd card to determine if we should @@ -503,7 +558,6 @@ void SleepActivity::renderCustomSleepScreen() const { if (Storage.openFileForRead("SLP", "/sleep.bmp", file)) { Bitmap bitmap(file, true); if (bitmap.parseHeaders() == BmpReaderError::Ok) { - LOG_DBG("SLP", "Loading: /sleep.bmp"); renderBitmapSleepScreen(bitmap); return; } @@ -512,6 +566,10 @@ void SleepActivity::renderCustomSleepScreen() const { renderDefaultSleepScreen(); } +// Sleep screens paint with a single HALF refresh (stock parity): the OEM X4 +// firmware's only clean refresh in normal operation is the single-pass 0xD7 +// sequence, used once for the sleep image. It never runs the multi-flash GC +// waveform (0xF7) that FULL_REFRESH selects (#2471's blinking complaint). void SleepActivity::renderDefaultSleepScreen() const { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); @@ -534,7 +592,7 @@ void SleepActivity::renderDefaultSleepScreen() const { renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 118, visibleBuildInfo.c_str(), lightSleepScreen); #endif - renderer.displayBuffer(HalDisplay::FULL_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); } void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { @@ -543,33 +601,27 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { const auto pageHeight = renderer.getScreenHeight(); float cropX = 0, cropY = 0; - LOG_DBG("SLP", "bitmap %d x %d, screen %d x %d", bitmap.getWidth(), bitmap.getHeight(), pageWidth, pageHeight); if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) { // image will scale, make sure placement is right float ratio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); const float screenRatio = static_cast(pageWidth) / static_cast(pageHeight); - LOG_DBG("SLP", "bitmap ratio: %f, screen ratio: %f", ratio, screenRatio); if (ratio > screenRatio) { // image wider than viewport ratio, scaled down image needs to be centered vertically if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) { cropX = 1.0f - (screenRatio / ratio); - LOG_DBG("SLP", "Cropping bitmap x: %f", cropX); ratio = (1.0f - cropX) * static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); } x = 0; y = std::round((static_cast(pageHeight) - static_cast(pageWidth) / ratio) / 2); - LOG_DBG("SLP", "Centering with ratio %f to y=%d", ratio, y); } else { // image taller than viewport ratio, scaled down image needs to be centered horizontally if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) { cropY = 1.0f - (ratio / screenRatio); - LOG_DBG("SLP", "Cropping bitmap y: %f", cropY); ratio = static_cast(bitmap.getWidth()) / ((1.0f - cropY) * static_cast(bitmap.getHeight())); } x = std::round((static_cast(pageWidth) - static_cast(pageHeight) * ratio) / 2); y = 0; - LOG_DBG("SLP", "Centering with ratio %f to x=%d", ratio, x); } } else { // center the image @@ -577,7 +629,6 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { y = (pageHeight - bitmap.getHeight()) / 2; } - LOG_DBG("SLP", "drawing to %d x %d", x, y); renderer.clearScreen(); const bool hasGreyscale = bitmap.hasGreyscale() && @@ -590,11 +641,13 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { } if (hasGreyscale) { - // OEM grayscale pipeline base: use a full sleep-screen paint so the panel - // enters deep sleep from a clean B/W baseline before the gray nudge refresh. - renderer.displayGrayscaleBase(HalDisplay::FULL_REFRESH); + // OEM grayscale pipeline base. Must stay HALF: the gray nudge LUT is + // calibrated against the pixel state the single-pass HALF waveform leaves + // behind. A FULL (GC) base parks pixels in a different charge state and + // the differential nudge then lands unevenly (blotchy noise in gray areas). + renderer.displayGrayscaleBase(HalDisplay::HALF_REFRESH); } else { - renderer.displayBuffer(HalDisplay::FULL_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); } if (hasGreyscale) { @@ -680,7 +733,7 @@ void SleepActivity::renderReadingStatsSleepScreen() const { if (!sleepCoverFilterInvertsGeneratedScreen()) { renderer.invertScreen(); } - renderer.displayBuffer(HalDisplay::FULL_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); } void SleepActivity::renderMinimalSleepScreen() const { @@ -699,7 +752,7 @@ void SleepActivity::renderMinimalSleepScreen() const { const float progressPercent = RecentBookProgress::loadPercent(book); MinimalTheme theme; theme.drawSleepScreen(renderer, book, &bookStats, progressPercent, sleepCoverFilterInvertsGeneratedScreen()); - renderer.displayBuffer(HalDisplay::FULL_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); } void SleepActivity::renderMinimalStatsSleepScreen() const { @@ -720,7 +773,7 @@ void SleepActivity::renderMinimalStatsSleepScreen() const { MinimalTheme theme; theme.drawStatsSleepScreen(renderer, book, &bookStats, &globalStats, progressPercent, sleepCoverFilterInvertsGeneratedScreen()); - renderer.displayBuffer(HalDisplay::FULL_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); } void SleepActivity::renderDashboardSleepScreen() const { @@ -746,7 +799,7 @@ void SleepActivity::renderDashboardSleepScreen() const { DashboardTheme theme; theme.drawSleepScreen(renderer, book, &bookStats, &globalStats, progressPercent, chapterTitle.c_str(), sleepCoverFilterInvertsGeneratedScreen()); - renderer.displayBuffer(HalDisplay::FULL_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); } void SleepActivity::renderLastScreenSleepScreen() const { @@ -756,12 +809,18 @@ void SleepActivity::renderLastScreenSleepScreen() const { } else { renderer.drawImage(MoonIcon, 0, pageHeight - MOONICON_HEIGHT, MOONICON_WIDTH, MOONICON_HEIGHT); } - renderer.displayBuffer(HalDisplay::FULL_REFRESH); + if (gpio.deviceIsX3()) { + // The controller still holds the displayed page, so its differential base + // waveform can add the moon without a full-screen flash. + renderer.displayGrayscaleBase(HalDisplay::FAST_REFRESH); + } else { + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + } } void SleepActivity::renderBlankSleepScreen() const { renderer.clearScreen(); - renderer.displayBuffer(HalDisplay::FULL_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH, TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); } void SleepActivity::renderOverlaySleepScreen() const { @@ -872,6 +931,17 @@ void SleepActivity::renderOverlaySleepScreen() const { return OverlayDrawResult::NotFound; } + // The reader activity has already released its document/layout state, and + // Page Overlay has captured the page framebuffer. Its active SD-font glyph + // cache is therefore regenerable and not needed for the final sleep frame. + // Free it before PNGdec requests its contiguous decode buffer. + const uint32_t freeBeforeRelease = ESP.getFreeHeap(); + const uint32_t maxAllocBeforeRelease = ESP.getMaxAllocHeap(); + if (renderer.releaseSdCardFontForLowMemory(SETTINGS.getReaderFontId())) { + LOG_DBG("SLP", "Released reader font cache for PNG overlay: free=%u->%u maxAlloc=%u->%u", freeBeforeRelease, + ESP.getFreeHeap(), maxAllocBeforeRelease, ESP.getMaxAllocHeap()); + } + constexpr size_t MIN_FREE_HEAP = 60 * 1024; // PNG decoder ~42 KB + overhead if (ESP.getFreeHeap() < MIN_FREE_HEAP) { LOG_ERR("SLP", "Not enough heap for PNG overlay decoder: %u free, need %u for %s", ESP.getFreeHeap(), @@ -943,6 +1013,18 @@ void SleepActivity::renderOverlaySleepScreen() const { trySelectedOverlay(selection); } + // Page Overlay can mix PNGs and BMPs. PNG decoding needs a sizeable + // temporary buffer while the reader page is still resident, so an otherwise + // valid PNG can fail on C3 devices. Try another folder image before using + // the root/default fallback: use BMP-only after a PNG failure, or validate + // BMP headers after a failed BMP selection. + if (!overlayDrawn && overlayCandidateFailed && !selection.path.empty() && + selectRandomSleepImage(SleepImageMode::Overlay, selection, + /*validateBmpHeaders=*/!selection.isPng, + /*bmpOnly=*/selection.isPng)) { + trySelectedOverlay(selection); + } + if (!overlayDrawn) { const OverlayDrawResult result = tryDrawOverlay("/sleep.bmp"); overlayDrawn = result == OverlayDrawResult::Drawn; @@ -974,7 +1056,7 @@ void SleepActivity::renderOverlaySleepScreen() const { // over the sleep image. const bool shouldRunGrayscalePass = shouldUseReaderPageBackground && backgroundSupportsGrayscale && !overlayDrawn && (backgroundWasRebuilt || (overlayBackgroundBufferStored && !path.empty())); - renderer.displayBuffer(HalDisplay::FULL_REFRESH, !shouldRunGrayscalePass && TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH, !shouldRunGrayscalePass && TURN_OFF_SCREEN_AFTER_SLEEP_REFRESH); if (!shouldRunGrayscalePass) { return; diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 3be8cf2e6a..832a565bb6 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -9,11 +9,13 @@ class Bitmap; class SleepActivity final : public Activity { public: explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool canSnapshotOverlayBackground, - std::string currentBookPath = {}, bool fromTimeout = false) + std::string currentBookPath = {}, bool fromTimeout = false, + GfxRenderer::Orientation sleepPopupOrientation = GfxRenderer::Orientation::Portrait) : Activity("Sleep", renderer, mappedInput), canSnapshotOverlayBackground(canSnapshotOverlayBackground), currentBookPath(std::move(currentBookPath)), - fromTimeout(fromTimeout) {} + fromTimeout(fromTimeout), + sleepPopupOrientation(sleepPopupOrientation) {} void onEnter() override; private: @@ -32,4 +34,5 @@ class SleepActivity final : public Activity { bool overlayBackgroundBufferStored = false; std::string currentBookPath; bool fromTimeout = false; + GfxRenderer::Orientation sleepPopupOrientation = GfxRenderer::Orientation::Portrait; }; diff --git a/src/activities/boot_sleep/SleepCoverAssets.cpp b/src/activities/boot_sleep/SleepCoverAssets.cpp index d8cf5899c5..69949562fe 100644 --- a/src/activities/boot_sleep/SleepCoverAssets.cpp +++ b/src/activities/boot_sleep/SleepCoverAssets.cpp @@ -74,7 +74,7 @@ bool prepareFullCoverForPath(const std::string& bookPath, const bool cropped, co if (FsHelpers::hasEpubExtension(bookPath)) { Epub epub(bookPath, "/.crosspoint"); - if (!epub.load(/*buildIfMissing=*/false, /*skipLoadingCss=*/true)) { + if (!epub.load(/*buildIfMissing=*/false, /*skipLoadingCss=*/true, Epub::XLocationLoadMode::Skip)) { return false; } return epub.generateCoverBmp(cropped, renderer, readerFontIdForRenderer(renderer)); @@ -100,7 +100,7 @@ bool prepareMinimalCoverForPath(const std::string& bookPath, const GfxRenderer* if (FsHelpers::hasEpubExtension(bookPath)) { Epub epub(bookPath, "/.crosspoint"); - if (!epub.load(/*buildIfMissing=*/true, /*skipLoadingCss=*/true)) { + if (!epub.load(/*buildIfMissing=*/true, /*skipLoadingCss=*/true, Epub::XLocationLoadMode::Skip)) { return false; } return epub.generateAdaptiveThumbBmp(kMinimalSleepCoverWidth, kMinimalSleepCoverHeight, renderer, @@ -128,7 +128,7 @@ bool prepareDashboardCoverForPath(const std::string& bookPath, const GfxRenderer if (FsHelpers::hasEpubExtension(bookPath)) { Epub epub(bookPath, "/.crosspoint"); - if (!epub.load(/*buildIfMissing=*/true, /*skipLoadingCss=*/true)) { + if (!epub.load(/*buildIfMissing=*/true, /*skipLoadingCss=*/true, Epub::XLocationLoadMode::Skip)) { return false; } return epub.generateAdaptiveThumbBmp(kDashboardSleepCoverWidth, kDashboardSleepCoverHeight, renderer, diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index acb0413e1f..ba2c54626d 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -1,28 +1,44 @@ #include "OpdsBookBrowserActivity.h" +#include +#include #include +#include #include #include #include #include #include +#include + +#include "CrossPointSettings.h" #include "MappedInputManager.h" #include "SdCardFontSystem.h" #include "SilentRestart.h" #include "activities/network/WifiSelectionActivity.h" #include "activities/util/KeyboardEntryActivity.h" +#include "components/UIScale.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" +#include "components/icons/listIcons.h" #include "fontIds.h" #include "network/HttpDownloader.h" #include "util/BookCacheUtils.h" #include "util/StringUtils.h" #include "util/UrlUtils.h" +namespace fui = freeink::ui; + namespace { -constexpr int PAGE_ITEMS = 23; constexpr size_t OPDS_BROWSER_ENTRY_CAPACITY = MAX_OPDS_FEED_ENTRIES + 2; constexpr size_t OPDS_DOWNLOAD_BUFFER_SIZE = 2048; +constexpr fui::ActionId ACTION_ROW = 1; +constexpr fui::ActionId ACTION_SEARCH = 2; +constexpr fui::ActionId ACTION_CANCEL = 3; +constexpr int DOWNLOAD_PROGRESS_STEP_PERCENT = 5; +constexpr unsigned long DOWNLOAD_PROGRESS_MIN_UPDATE_MS = 5000; std::string buildBookFilenameBase(const OpdsEntry& book, const OpdsFilenameFormat format) { if (book.author.empty()) return book.title; @@ -30,8 +46,17 @@ std::string buildBookFilenameBase(const OpdsEntry& book, const OpdsFilenameForma if (format == OpdsFilenameFormat::TITLE_AUTHOR) return book.title + " - " + book.author; return book.author + " - " + book.title; } + } // namespace +OpdsBookBrowserActivity::OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + OpdsServer server) + : Activity("OpdsBookBrowser", renderer, mappedInput), + buttonNavigator(), + server(std::move(server)), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + void OpdsBookBrowserActivity::onEnter() { Activity::onEnter(); @@ -47,6 +72,14 @@ void OpdsBookBrowserActivity::onEnter() { consumeBack = false; errorMessage.clear(); statusMessage = tr(STR_CHECKING_WIFI); + + uiReady = false; + visibleRows = 1; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &OpdsBookBrowserActivity::onRowEvent, this); + app.on(ACTION_SEARCH, &OpdsBookBrowserActivity::onSearchEvent, this); + app.on(ACTION_CANCEL, &OpdsBookBrowserActivity::onCancelEvent, this); + app.setScreen(&OpdsBookBrowserActivity::rootScreen, this); requestUpdate(); if (!ensureEntryBuffer()) { @@ -56,7 +89,12 @@ void OpdsBookBrowserActivity::onEnter() { return; } +#ifdef SIMULATOR + // Use deterministic catalog data so the UI can be exercised without WiFi or an OPDS server. + fetchFeed(currentPath); +#else checkAndConnectWifi(); +#endif } void OpdsBookBrowserActivity::onExit() { @@ -65,11 +103,46 @@ void OpdsBookBrowserActivity::onExit() { entries.reset(); navigationHistory.clear(); +#ifndef SIMULATOR if (WiFi.getMode() != WIFI_MODE_NULL) { WiFi.disconnect(false); delay(30); - silentRestart(); } + // OPDS launches from minimal network boot, so restore the full app state + // even if setup failed before WiFi was started. + silentRestart(); +#endif +} + +void OpdsBookBrowserActivity::activateSelected() { + if (!entries || entryCount == 0 || selectorIndex < 0 || selectorIndex >= static_cast(entryCount)) return; + const auto& entry = entries[selectorIndex]; + entry.type == OpdsEntryType::BOOK ? downloadBook(entry) : navigateToEntry(entry); +} + +void OpdsBookBrowserActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (self->state != BrowserState::BROWSING) return; + if (event.value < 0 || event.value >= static_cast(self->entryCount)) return; + self->selectorIndex = event.value; + // The tapped row leaves the screen either way (new feed or download view); + // a lingering tap flash would gray an unrelated row on the next list. + self->app.clearTapFlash(); + self->activateSelected(); +} + +void OpdsBookBrowserActivity::onSearchEvent(const fui::ActionEvent&, void* user) { + auto* self = static_cast(user); + if (self->state != BrowserState::BROWSING) return; + self->app.clearTapFlash(); + self->launchSearch(); +} + +void OpdsBookBrowserActivity::onCancelEvent(const fui::ActionEvent&, void* user) { + auto* self = static_cast(user); + if (self->state != BrowserState::DOWNLOADING) return; + self->app.clearTapFlash(); + self->cancelDownload = true; } void OpdsBookBrowserActivity::loop() { @@ -87,7 +160,9 @@ void OpdsBookBrowserActivity::loop() { } if (state == BrowserState::ERROR) { - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + int tx = 0; + int ty = 0; + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || mappedInput.wasScreenTapped(tx, ty)) { if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { showLoadingBeforeFetch(); fetchFeed(currentPath); @@ -107,36 +182,74 @@ void OpdsBookBrowserActivity::loop() { return; } - if (state == BrowserState::DOWNLOADING) return; + if (state == BrowserState::DOWNLOADING) { +#ifdef SIMULATOR + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) app.route(snap); + } + if (cancelDownload || mappedInput.wasReleased(MappedInputManager::Button::Back)) { + cancelDownload = false; + state = BrowserState::BROWSING; + requestUpdate(); + } +#endif + return; + } if (state == BrowserState::BROWSING) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (entryCount > 0) { - const auto& entry = entries[selectorIndex]; - entry.type == OpdsEntryType::BOOK ? downloadBook(entry) : navigateToEntry(entry); - } + activateSelected(); } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { navigateBack(); } else if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { if (!searchTemplate.empty() && selectorIndex == 0) launchSearch(); } + // Touch goes through the FreeInkApp: render() registered every tap target + // (rows, header search button); route the snapshot and let the registered + // handlers dispatch. + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + // No pressed-state repaint: the render it triggers would drop a slow + // tap's release inside the uiReady window (tap-to-activate needed two + // taps), and it costs a second e-ink refresh per tap. + if (app.invalidated()) requestUpdate(); + if (event) return; // dispatched to onRowEvent/onSearchEvent + if (state != BrowserState::BROWSING) return; + } + } + if (entryCount > 0) { - buttonNavigator.onNextRelease([this] { - selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entryCount); - requestUpdate(); - }); - buttonNavigator.onPreviousRelease([this] { - selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entryCount); - requestUpdate(); - }); - buttonNavigator.onNextContinuous([this] { - selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entryCount, PAGE_ITEMS); + // Swipes scroll the viewport; the selection stays put (it may scroll + // off-screen) and button navigation pulls the view back to it. + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, static_cast(entryCount)); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + + const auto moveSelection = [this](const int index) { + selectorIndex = index; + topIndex = followListSelection(selectorIndex, topIndex, visibleRows, static_cast(entryCount)); requestUpdate(); + }; + buttonNavigator.onNextRelease( + [this, &moveSelection] { moveSelection(ButtonNavigator::nextIndex(selectorIndex, entryCount)); }); + buttonNavigator.onPreviousRelease( + [this, &moveSelection] { moveSelection(ButtonNavigator::previousIndex(selectorIndex, entryCount)); }); + buttonNavigator.onNextContinuous([this, &moveSelection] { + moveSelection(ButtonNavigator::nextPageIndex(selectorIndex, entryCount, visibleRows)); }); - buttonNavigator.onPreviousContinuous([this] { - selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entryCount, PAGE_ITEMS); - requestUpdate(); + buttonNavigator.onPreviousContinuous([this, &moveSelection] { + moveSelection(ButtonNavigator::previousPageIndex(selectorIndex, entryCount, visibleRows)); }); } } @@ -157,68 +270,163 @@ bool OpdsBookBrowserActivity::preventAutoSleep() { return false; } -void OpdsBookBrowserActivity::render(RenderLock&&) { - renderer.clearScreen(); - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); +void OpdsBookBrowserActivity::rootScreen(UiApp::ScreenType& screen, void* user) { + auto* self = static_cast(user); + switch (self->state) { + case BrowserState::BROWSING: + self->buildBrowsingScreen(screen); + break; + case BrowserState::DOWNLOADING: + self->buildDownloadScreen(screen); + break; + default: + self->buildStatusScreen(screen); + break; + } +} - // Show server name in header if available, otherwise generic title - const char* headerTitle = server.name.empty() ? tr(STR_OPDS_BROWSER) : server.name.c_str(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, headerTitle, true, EpdFontFamily::BOLD); +// Shared chrome for every state: reserve the firmware's button-hint band and +// draw the themed header (padding, centering, and rule come from the theme). +void OpdsBookBrowserActivity::screenHeader(UiApp::ScreenType& screen, const bool withSearch) { + screen.takeBottom(static_cast(UITheme::getInstance().getMetrics().buttonHintsHeight)); + fui::HeaderProps header; + header.title = server.name.empty() ? tr(STR_OPDS_BROWSER) : server.name.c_str(); + header.borderEdges = fui::EdgeBottom; + if (withSearch && !searchTemplate.empty()) { + header.trailingIcon = fui::bitmapFromIcon(icon_search_32); + header.trailingAction = ACTION_SEARCH; + // Optically align the icon with the title glyphs: text hangs low in its + // line cell by the font's internal leading; drop the button to match. + const int titleFontId = uiScaleSpec().titleFontId; + header.actionOffsetY = + static_cast((renderer.getLineHeight(titleFontId) - renderer.getTextHeight(titleFontId)) / 2); + } + screen.header(header); + // Same breathing room between header and content as the legacy screens. + screen.spacer(static_cast(UITheme::getInstance().getMetrics().verticalSpacing)); +} - if (state == BrowserState::CHECK_WIFI || state == BrowserState::LOADING) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str()); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); +void OpdsBookBrowserActivity::buildBrowsingScreen(UiApp::ScreenType& screen) { + screenHeader(screen, true); + + if (entryCount == 0) { + screen.centeredText(tr(STR_NO_ENTRIES), screen.theme().bodyText); return; } - if (state == BrowserState::ERROR) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_ERROR_MSG)); - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, errorMessage.c_str()); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_RETRY), "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); - return; + // Transient per-render: sized once via reserve, points into `entries` + // strings, freed on scope exit. + std::vector items; + items.reserve(entryCount); + for (size_t i = 0; i < entryCount; ++i) { + const auto& entry = entries[i]; + fui::ListItem item; + item.label = entry.title.c_str(); + if (entry.type == OpdsEntryType::BOOK && !entry.author.empty()) item.subtitle = entry.author.c_str(); + if (entry.type == OpdsEntryType::NAVIGATION) item.value = ">"; + item.actionValue = static_cast(items.size()); + items.push_back(item); } - if (state == BrowserState::DOWNLOADING) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, tr(STR_DOWNLOADING)); - auto title = renderer.truncatedText(UI_10_FONT_ID, statusMessage.c_str(), pageWidth - 40); - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, title.c_str()); - if (downloadTotal > 0) { - GUI.drawProgressBar(renderer, Rect{50, pageHeight / 2 + 20, pageWidth - 100, 20}, downloadProgress, - downloadTotal); - } - const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectorIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; // physical buttons stay in loop() + props.valueInset = 8; // air between the nav chevron and the row edge + const auto rows = configureUiList(props, screen.theme(), screen.body(), UiListRowType::WithSubtitle); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(entryCount)); // clamp to range + props.topIndex = static_cast(topIndex); + screen.list(props); +} + +void OpdsBookBrowserActivity::buildDownloadScreen(UiApp::ScreenType& screen) { + screenHeader(screen, false); + + // Centered block: status line, book title, progress bar, cancel button. + const auto& theme = screen.theme(); + fui::TextStyle centered = theme.bodyText; + centered.align = fui::TextAlign::Center; + const int16_t lh = screen.target().lineHeight(centered.font); + const int16_t gap = theme.spaceMd; + const int16_t barH = 16; + const int16_t btnH = theme.rowHeight; + const int16_t blockH = static_cast(lh * 2 + barH + btnH + gap * 3); + const fui::Rect body = screen.body(); + if (body.height > blockH) screen.spacer(static_cast((body.height - blockH) / 2)); + + screen.target().text(screen.takeTop(lh, gap), tr(STR_DOWNLOADING), centered); + screen.target().text(screen.takeTop(lh, gap), statusMessage.c_str(), centered); + + const fui::Rect bar = screen.takeTop(barH, gap).inset(fui::Insets{0, 50, 0, 50}); + if (downloadTotal > 0) { + fui::ProgressBarProps progress; + progress.value = static_cast(downloadProgress); + progress.max = static_cast(downloadTotal); + progress.border = fui::Paint::solid(fui::Color::Black); + progress.borderWidth = 1; + fui::progressBar(screen.frame(), bar, progress); + } + + const fui::Rect btnArea = screen.takeTop(btnH); + const int16_t btnW = static_cast(btnArea.width / 3); + fui::ButtonProps cancel; + cancel.label = tr(STR_CANCEL); + cancel.action = ACTION_CANCEL; + screen.button(cancel, fui::Rect{static_cast(btnArea.x + (btnArea.width - btnW) / 2), btnArea.y, btnW, btnH}); +} + +void OpdsBookBrowserActivity::buildStatusScreen(UiApp::ScreenType& screen) { + screenHeader(screen, false); + + fui::TextStyle centered = screen.theme().bodyText; + centered.align = fui::TextAlign::Center; + if (state == BrowserState::ERROR) { + const int16_t lh = screen.target().lineHeight(centered.font); + const int16_t gap = screen.theme().spaceMd; + const bool showTapHint = mappedInput.hasTouch(); + const int16_t blockH = static_cast(lh * (showTapHint ? 3 : 2) + gap * (showTapHint ? 2 : 1)); + const fui::Rect body = screen.body(); + if (body.height > blockH) screen.spacer(static_cast((body.height - blockH) / 2)); + screen.target().text(screen.takeTop(lh, gap), tr(STR_ERROR_MSG), centered); + screen.target().text(screen.takeTop(lh, gap), errorMessage.c_str(), centered); + if (showTapHint) screen.target().text(screen.takeTop(lh), tr(STR_TAP_TO_RETRY), centered); return; } + // CHECK_WIFI / LOADING (and the brief child-activity handoff states). + screen.centeredText(statusMessage.c_str(), centered); +} - const char* confirmLabel = - (entryCount > 0 && entries[selectorIndex].type == OpdsEntryType::BOOK) ? tr(STR_DOWNLOAD) : tr(STR_OPEN); - const char* searchLabel = (!searchTemplate.empty() && selectorIndex == 0) ? tr(STR_SEARCH) : tr(STR_DIR_UP); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, searchLabel, tr(STR_DIR_DOWN)); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); +void OpdsBookBrowserActivity::render(RenderLock&&) { + renderer.clearScreen(); - if (entryCount == 0) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_NO_ENTRIES)); - } else { - const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; - renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); - - for (size_t i = pageStartIndex; i < entryCount && i < static_cast(pageStartIndex + PAGE_ITEMS); i++) { - const auto& entry = entries[i]; - std::string displayText = (entry.type == OpdsEntryType::NAVIGATION) ? "> " + entry.title : entry.title; - if (entry.type == OpdsEntryType::BOOK && !entry.author.empty()) displayText += " - " + entry.author; - auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), pageWidth - 40); - renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), - i != static_cast(selectorIndex)); + MappedInputManager::Labels labels; + switch (state) { + case BrowserState::BROWSING: { + const char* confirmLabel = + (entryCount > 0 && entries[selectorIndex].type == OpdsEntryType::BOOK) ? tr(STR_DOWNLOAD) : tr(STR_OPEN); + const char* searchLabel = (!searchTemplate.empty() && selectorIndex == 0) ? tr(STR_SEARCH) : tr(STR_DIR_UP); + labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, searchLabel, tr(STR_DIR_DOWN)); + break; } + case BrowserState::DOWNLOADING: + labels = mappedInput.mapLabels(tr(STR_CANCEL), "", "", ""); + break; + case BrowserState::ERROR: + labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_RETRY), "", ""); + break; + default: + labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + break; } - renderer.displayBuffer(); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + uiReady = false; + app.render(); + uiReady = true; + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); } void OpdsBookBrowserActivity::showLoadingBeforeFetch() { @@ -238,6 +446,31 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { return; } +#ifdef SIMULATOR + clearEntries(); + searchTemplate = "simulator://search?query={searchTerms}"; + + if (path.empty()) { + appendEntry(OpdsEntry{OpdsEntryType::NAVIGATION, "Browse fiction", "", "/fiction", ""}); + appendEntry(OpdsEntry{OpdsEntryType::BOOK, "The Left Hand of Darkness", "Ursula K. Le Guin", + "/books/the-left-hand-of-darkness.epub", ""}); + appendEntry( + OpdsEntry{OpdsEntryType::BOOK, "A Room of One's Own", "Virginia Woolf", "/books/a-room-of-ones-own.epub", ""}); + appendEntry(OpdsEntry{OpdsEntryType::BOOK, "Frankenstein", "Mary Shelley", "/books/frankenstein.epub", ""}); + } else { + appendEntry( + OpdsEntry{OpdsEntryType::BOOK, "The Dispossessed", "Ursula K. Le Guin", "/books/the-dispossessed.epub", ""}); + appendEntry(OpdsEntry{OpdsEntryType::BOOK, "Kindred", "Octavia E. Butler", "/books/kindred.epub", ""}); + appendEntry(OpdsEntry{OpdsEntryType::BOOK, "The Time Machine", "H. G. Wells", "/books/the-time-machine.epub", ""}); + } + + selectorIndex = 0; + topIndex = 0; + state = BrowserState::BROWSING; + requestUpdate(); + return; +#endif + if (server.url.empty()) { state = BrowserState::ERROR; errorMessage = tr(STR_NO_SERVER_URL); @@ -246,12 +479,17 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { } clearEntries(); - std::string url = (path.find("http") == 0) ? path : UrlUtils::buildUrl(server.url, path); + const std::string url = UrlUtils::buildUrl(server.url, path); LOG_DBG("OPDS", "Fetching: %s", url.c_str()); OpdsParser parser(entries.get(), MAX_OPDS_FEED_ENTRIES); { OpdsParserStream stream{parser}; - if (!HttpDownloader::fetchUrl(url, stream, server.username, server.password)) { + HttpDownloader::DownloadOptions downloadOptions; + downloadOptions.transport = HttpDownloader::Transport::WOLFSSL; + const auto result = HttpDownloader::streamUrl( + url, [&stream](const uint8_t* data, const size_t len) { return stream.write(data, len) == len; }, nullptr, + server.username, server.password, std::move(downloadOptions)); + if (result != HttpDownloader::OK) { state = BrowserState::ERROR; errorMessage = tr(STR_FETCH_FEED_FAILED); requestUpdate(); @@ -287,6 +525,7 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { } selectorIndex = 0; + topIndex = 0; state = entryCount == 0 ? BrowserState::ERROR : BrowserState::BROWSING; if (entryCount == 0) errorMessage = tr(STR_NO_ENTRIES); requestUpdate(); @@ -299,7 +538,12 @@ bool OpdsBookBrowserActivity::ensureEntryBuffer() { } void OpdsBookBrowserActivity::clearEntries() { - // Slots past entryCount are ignored and overwritten by the next feed parse. + // The app's interaction table still references the old row indices until + // the next render, so stop routing touches while clearing the backing data. + uiReady = false; + for (size_t i = 0; entries && i < entryCount; ++i) { + entries[i] = OpdsEntry{}; + } entryCount = 0; } @@ -338,21 +582,49 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) { state = BrowserState::DOWNLOADING; statusMessage = book.title; downloadProgress = downloadTotal = 0; + cancelDownload = false; + goHomeAfterCancel = false; requestUpdate(true); +#ifdef SIMULATOR + downloadProgress = 1; + downloadTotal = 2; + requestUpdate(true); + return; +#endif + // Build full download URL relative to the current feed, not the root server URL const std::string feedUrl = UrlUtils::buildUrl(server.url, currentPath); std::string downloadUrl = UrlUtils::buildUrl(feedUrl, book.href); - std::string filename = - "/" + StringUtils::sanitizeFilename(buildBookFilenameBase(book, server.filenameFormat)) + ".epub"; + const char* downloadFolder = SETTINGS.opdsDownloadFolder; + bool useDownloadFolder = downloadFolder[0] != '\0'; + if (useDownloadFolder && !Storage.exists(downloadFolder) && !Storage.mkdir(downloadFolder)) { + LOG_ERR("OPDS", "Could not create download folder %s", downloadFolder); + state = BrowserState::ERROR; + errorMessage = tr(STR_DOWNLOAD_FAILED); + requestUpdate(); + return; + } + + std::string filename; + filename.reserve(96); + if (useDownloadFolder) filename += downloadFolder; + filename += '/'; + filename += StringUtils::sanitizeFilename(buildBookFilenameBase(book, server.filenameFormat)); + filename += ".epub"; LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str()); bool cancelRequested = false; auto pollCancel = [this, &cancelRequested] { - if (cancelRequested) { + if (cancelRequested || cancelDownload) { + cancelRequested = true; return true; } mappedInput.update(); + if (mappedInput.wasHomeGesture()) { + goHomeAfterCancel = true; + cancelRequested = true; + } if (mappedInput.isPressed(MappedInputManager::Button::Back) || mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasReleased(MappedInputManager::Button::Back)) { @@ -363,13 +635,37 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) { HttpDownloader::DownloadOptions downloadOptions; downloadOptions.shouldCancel = pollCancel; downloadOptions.bufferSize = OPDS_DOWNLOAD_BUFFER_SIZE; + downloadOptions.transport = HttpDownloader::Transport::WOLFSSL; + int lastRenderedPercent = -1; + unsigned long lastProgressUpdateMs = 0; const auto result = HttpDownloader::downloadToFile( downloadUrl, filename, - [this](const size_t downloaded, const size_t total) { + [this, &cancelRequested, &lastRenderedPercent, &lastProgressUpdateMs](const size_t downloaded, + const size_t total) { downloadProgress = downloaded; downloadTotal = total; - requestUpdate(true); + // The activity loop is blocked for the whole download; pump input here + // so the Cancel button or a Back press can abort mid-transfer. + mappedInput.update(); + if (mappedInput.wasHomeGesture()) { + goHomeAfterCancel = true; + cancelRequested = true; + } + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) cancelRequested = true; + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) app.route(snap); + } + const int percent = total > 0 ? static_cast(static_cast(downloaded) * 100 / total) : 0; + const unsigned long now = millis(); + if (percent >= 100 || lastRenderedPercent < 0 || + percent >= lastRenderedPercent + DOWNLOAD_PROGRESS_STEP_PERCENT || + now - lastProgressUpdateMs >= DOWNLOAD_PROGRESS_MIN_UPDATE_MS) { + lastRenderedPercent = percent; + lastProgressUpdateMs = now; + requestUpdate(true); + } }, &cancelRequested, server.username, server.password, downloadOptions); @@ -377,7 +673,11 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) { clearBookCache(filename); state = BrowserState::BROWSING; } else if (result == HttpDownloader::ABORTED) { - LOG_DBG("OPDS", "Download cancelled"); + LOG_INF("OPDS", "Download cancelled"); + if (goHomeAfterCancel) { + onGoHome(); + return; + } mappedInput.suppressNextBackRelease(); state = BrowserState::BROWSING; } else { @@ -430,9 +730,11 @@ void OpdsBookBrowserActivity::performSearch(const std::string& query) { const size_t pos = url.find(placeholder); if (pos != std::string::npos) url.replace(pos, placeholder.length(), urlEncode(query)); - navigationHistory.push_back(currentPath); // <-- add this - currentPath = url; // <-- add this + navigationHistory.push_back(currentPath); + currentPath = url; + clearEntries(); + selectorIndex = 0; showLoadingBeforeFetch(); fetchFeed(url); } diff --git a/src/activities/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index a8e661f686..0aea477ba2 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -1,6 +1,9 @@ #pragma once +#include +#include #include +#include #include #include #include @@ -8,6 +11,7 @@ #include "OpdsServerStore.h" #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" #include "util/ButtonNavigator.h" /** @@ -18,8 +22,7 @@ class OpdsBookBrowserActivity final : public Activity { public: enum class BrowserState { CHECK_WIFI, WIFI_SELECTION, LOADING, BROWSING, DOWNLOADING, ERROR, SEARCH_INPUT }; - explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, OpdsServer server) - : Activity("OpdsBookBrowser", renderer, mappedInput), buttonNavigator(), server(std::move(server)) {} + explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, OpdsServer server); void onEnter() override; void onExit() override; @@ -27,15 +30,22 @@ class OpdsBookBrowserActivity final : public Activity { void render(RenderLock&&) override; private: + // FreeInkUI app runtime for the browsing screen: owns the interaction table, + // routes touch snapshots, and dispatches row/search actions to the static + // handlers below. 24 interaction slots cover the densest page (Small scale, + // ~12 rows) plus the header's search button, with headroom. + using UiApp = freeink::ui::FreeInkApp<24, 4>; + ButtonNavigator buttonNavigator; BrowserState state = BrowserState::LOADING; + ScreenTransitionRefresh screenTransitionRefresh; std::unique_ptr entries; size_t entryCount = 0; std::vector navigationHistory; std::string currentPath; std::string searchTemplate; bool consumeConfirm = false; - bool consumeBack = false; // Added missing member + bool consumeBack = false; int selectorIndex = 0; std::string errorMessage; std::string statusMessage; @@ -44,6 +54,32 @@ class OpdsBookBrowserActivity final : public Activity { OpdsServer server; // Copied at construction — safe even if the store changes during browsing + freeink::ui::GfxRendererTarget uiTarget; // must precede `app`: the app holds a reference to it + UiApp app; + // render() rebuilds the app's interaction table; loop() only routes touch + // snapshots against it while this is true (the two run on different tasks). + std::atomic uiReady{false}; + int visibleRows = 1; // rows per page at the current scale; set by the screen builder + int topIndex = 0; // viewport scroll position, decoupled from the selection + // Read by HttpDownloader between chunks; set by the Cancel button handler or + // a Back press, both pumped from the download's progress callback. + bool cancelDownload = false; + // A blocking downloader consumes the one-shot Home event itself. Defer the + // activity exit until HttpDownloader has unwound and closed the partial file. + bool goHomeAfterCancel = false; + + // Single screen fn dispatching on `state`: every state shares the themed + // header and gets built through FreeInkUI. + static void rootScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + static void onSearchEvent(const freeink::ui::ActionEvent& event, void* user); + static void onCancelEvent(const freeink::ui::ActionEvent& event, void* user); + void screenHeader(UiApp::ScreenType& screen, bool withSearch); + void buildBrowsingScreen(UiApp::ScreenType& screen); + void buildDownloadScreen(UiApp::ScreenType& screen); + void buildStatusScreen(UiApp::ScreenType& screen); + void activateSelected(); + void checkAndConnectWifi(); void launchWifiSelection(); void onWifiSelectionComplete(bool connected); diff --git a/src/activities/home/BookActions.cpp b/src/activities/home/BookActions.cpp index fa9d7a424b..1f10fd1b95 100644 --- a/src/activities/home/BookActions.cpp +++ b/src/activities/home/BookActions.cpp @@ -70,6 +70,11 @@ bool hasClearableBookCache(const std::string& path) { return FsHelpers::hasEpubExtension(path) || FsHelpers::hasXtcExtension(path); } +bool canSendNearby(const std::string& path) { + return FsHelpers::hasEpubExtension(path) || FsHelpers::hasTxtExtension(path) || FsHelpers::hasXtcExtension(path) || + FsHelpers::hasPngExtension(path) || FsHelpers::hasBmpExtension(path); +} + void clearFileMetadata(const std::string& fullPath) { if (FsHelpers::hasEpubExtension(fullPath)) { Epub(fullPath, "/.crosspoint").clearCache(); @@ -80,7 +85,6 @@ void clearFileMetadata(const std::string& fullPath) { } else if (FsHelpers::hasTxtExtension(fullPath) || FsHelpers::hasMarkdownExtension(fullPath)) { BookmarkStore::deleteForFilePath(fullPath, "txt"); } - LOG_DBG("BookActions", "Cleared metadata for: %s", fullPath.c_str()); } bool clearBookCache(const std::string& fullPath) { diff --git a/src/activities/home/BookActions.h b/src/activities/home/BookActions.h index deaebcf05d..08bc1a5f78 100644 --- a/src/activities/home/BookActions.h +++ b/src/activities/home/BookActions.h @@ -12,6 +12,7 @@ namespace BookActions { std::vector buildBookActionItems(const std::string& fullPath, bool includeRemoveFromRecents); bool hasClearableBookCache(const std::string& path); +bool canSendNearby(const std::string& path); void clearFileMetadata(const std::string& fullPath); bool clearBookCache(const std::string& fullPath); bool deleteBookStats(const std::string& fullPath); diff --git a/src/activities/home/BookmarksHomeActivity.cpp b/src/activities/home/BookmarksHomeActivity.cpp index 80a8c59c52..688881505a 100644 --- a/src/activities/home/BookmarksHomeActivity.cpp +++ b/src/activities/home/BookmarksHomeActivity.cpp @@ -8,12 +8,22 @@ #include "CrossPointState.h" #include "FileBrowserActionActivity.h" #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" -#include "fontIds.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" + +namespace fui = freeink::ui; namespace { constexpr unsigned long BOOKMARK_DELETE_HOLD_MS = 1000; -} +constexpr fui::ActionId ACTION_ROW = 1; +} // namespace + +BookmarksHomeActivity::BookmarksHomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("BookmarksHome", renderer, mappedInput), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} void BookmarksHomeActivity::reloadBookmarks() { books.clear(); @@ -30,6 +40,12 @@ void BookmarksHomeActivity::onEnter() { reloadBookmarks(); selectedIndex = 0; + topIndex = 0; + visibleRows = 1; + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &BookmarksHomeActivity::onRowEvent, this); + app.setScreen(&BookmarksHomeActivity::listScreen, this); requestUpdate(); } @@ -39,6 +55,10 @@ void BookmarksHomeActivity::onExit() { } void BookmarksHomeActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + onGoHome(); + return; + } if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoHome(); return; @@ -62,51 +82,123 @@ void BookmarksHomeActivity::loop() { return; } - const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true); const int listSize = static_cast(books.size()); + if (listSize == 0) return; - buttonNavigator.onNextRelease([this, listSize] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, listSize); - requestUpdate(); - }); + int tx = 0; + int ty = 0; + if (mappedInput.isScreenTouchLongPress(tx, ty, BOOKMARK_DELETE_HOLD_MS) && listRowStep > 0 && ty >= listTop && + ty < listBottom) { + const int offset = ty - listTop; + const int row = offset / listRowStep; + const int touchedIndex = topIndex + row; + if (row >= visibleRows || offset % listRowStep >= listRowHeight || touchedIndex >= listSize) return; + selectedIndex = touchedIndex; + mappedInput.suppressNextTouchTap(); + longPressOpenHandled = true; + showBookmarkBookActionMenu(selectedIndex, true); + return; + } + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } - buttonNavigator.onPreviousRelease([this, listSize] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, listSize); - requestUpdate(); - }); + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int next = scrollListBy(topIndex, swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows, + visibleRows, listSize); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } - buttonNavigator.onNextContinuous([this, listSize, pageItems] { - selectedIndex = ButtonNavigator::nextPageIndex(selectedIndex, listSize, pageItems); + const auto moveSelection = [this, listSize](const int next) { + selectedIndex = next; + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, listSize); requestUpdate(); + }; + buttonNavigator.onNextRelease( + [this, listSize, &moveSelection] { moveSelection(ButtonNavigator::nextIndex(selectedIndex, listSize)); }); + buttonNavigator.onPreviousRelease( + [this, listSize, &moveSelection] { moveSelection(ButtonNavigator::previousIndex(selectedIndex, listSize)); }); + buttonNavigator.onNextContinuous([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::nextPageIndex(selectedIndex, listSize, visibleRows)); }); - - buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { - selectedIndex = ButtonNavigator::previousPageIndex(selectedIndex, listSize, pageItems); - requestUpdate(); + buttonNavigator.onPreviousContinuous([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::previousPageIndex(selectedIndex, listSize, visibleRows)); }); } -void BookmarksHomeActivity::render(RenderLock&&) { - renderer.clearScreen(); +void BookmarksHomeActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(self->books.size())) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->openBookmarkList(self->selectedIndex); +} - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - const auto& metrics = UITheme::getInstance().getMetrics(); +void BookmarksHomeActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_BOOKMARKS)); +void BookmarksHomeActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + screen.spacer(static_cast(metrics.verticalSpacing)); + if (books.empty()) { + screen.centeredText(tr(STR_NO_BOOKMARKS), screen.theme().bodyText); + return; + } + std::vector items; + items.reserve(books.size()); + for (size_t i = 0; i < books.size(); ++i) { + fui::ListItem item; + item.label = books[i].bookTitle.c_str(); + if (!books[i].bookAuthor.empty()) item.subtitle = books[i].bookAuthor.c_str(); + item.actionValue = static_cast(i); + items.push_back(item); + } + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + const fui::Rect bounds = screen.body(); + listTop = bounds.y; + listBottom = bounds.bottom(); + const auto rows = configureUiList(props, screen.theme(), bounds, UiListRowType::WithSubtitle); + listRowHeight = props.rowHeight; + listRowStep = props.rowHeight + props.rowGap; + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(books.size())); + props.topIndex = static_cast(topIndex); + screen.list(props); +} - const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; +void BookmarksHomeActivity::render(RenderLock&&) { + renderer.clearScreen(); - if (books.empty()) { - renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_BOOKMARKS)); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_BOOKMARKS), false); } else { - GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, books.size(), selectedIndex, - [this](int index) { return books[index].bookTitle; }, [this](int index) { return books[index].bookAuthor; }, - nullptr); + GUI.drawHeader(renderer, header, tr(STR_BOOKMARKS)); } + uiReady = false; + app.render(); + uiReady = true; + const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/home/BookmarksHomeActivity.h b/src/activities/home/BookmarksHomeActivity.h index 594c99a70f..936ce4cf02 100644 --- a/src/activities/home/BookmarksHomeActivity.h +++ b/src/activities/home/BookmarksHomeActivity.h @@ -1,4 +1,8 @@ #pragma once +#include +#include + +#include #include #include @@ -8,8 +12,7 @@ class BookmarksHomeActivity final : public Activity { public: - explicit BookmarksHomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("BookmarksHome", renderer, mappedInput) {} + BookmarksHomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput); void onEnter() override; void onExit() override; @@ -21,6 +24,20 @@ class BookmarksHomeActivity final : public Activity { int selectedIndex = 0; bool longPressOpenHandled = false; ButtonNavigator buttonNavigator; + using UiApp = freeink::ui::FreeInkApp<20, 4>; + freeink::ui::GfxRendererTarget uiTarget; + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + int listTop = 0; + int listBottom = 0; + int listRowHeight = 0; + int listRowStep = 0; + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); void reloadBookmarks(); void openBookmarkList(int bookIndex); diff --git a/src/activities/home/CrashActivity.cpp b/src/activities/home/CrashActivity.cpp index 8a7707d512..27302e46e1 100644 --- a/src/activities/home/CrashActivity.cpp +++ b/src/activities/home/CrashActivity.cpp @@ -23,7 +23,9 @@ void CrashActivity::onEnter() { } void CrashActivity::loop() { - if (mappedInput.isPressed(MappedInputManager::Button::Back)) { + int x = 0; + int y = 0; + if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasScreenTapped(x, y)) { finish(); } } diff --git a/src/activities/home/FileBrowserActionActivity.cpp b/src/activities/home/FileBrowserActionActivity.cpp index b5960d785c..f857761383 100644 --- a/src/activities/home/FileBrowserActionActivity.cpp +++ b/src/activities/home/FileBrowserActionActivity.cpp @@ -1,30 +1,51 @@ #include "FileBrowserActionActivity.h" -#include #include #include -#include "components/UITheme.h" -#include "fontIds.h" - -namespace { -constexpr int kTitleFontId = UI_10_FONT_ID; -constexpr int kTitleMaxLines = 2; -constexpr int kCompactTitleY = 14; -constexpr int kTallHeaderTitleBottomPadding = 8; -constexpr int kCompactHeaderTitleBottomPadding = 4; -constexpr int kTitleLineGap = 1; -constexpr int kBatteryTextReserveWidth = 90; -} // namespace +FileBrowserActionActivity::FileBrowserActionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + std::string title, std::vector items, + const bool ignoreInitialConfirmRelease) + : Activity("FileBrowserAction", renderer, mappedInput), + title(std::move(title)), + items(std::move(items)), + ignoreConfirmRelease(ignoreInitialConfirmRelease) {} void FileBrowserActionActivity::onEnter() { Activity::onEnter(); - selectedIndex = 0; + // A touch long-press opens this activity while the finger is still down. + // Wait for that contact to end so its release cannot activate a menu row. + int touchX = 0; + int touchY = 0; + ignoreTouchRelease = mappedInput.isScreenTouchHeld(touchX, touchY); + optionLabels.resize(items.size()); + std::transform(items.begin(), items.end(), optionLabels.begin(), + [](const MenuItem& item) { return std::string(I18N.get(item.labelId)); }); + optionPopup.show(title.c_str(), optionLabels, 0, [this](const int index) { + if (index < 0 || index >= static_cast(items.size())) return; + selectionMade = true; + setResult(FileBrowserActionResult{static_cast(items[index].action)}); + finish(); + }); requestUpdate(); } +void FileBrowserActionActivity::finishCancelled() { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); +} + void FileBrowserActionActivity::loop() { + if (ignoreTouchRelease) { + if (mappedInput.wasScreenTouchReleased()) { + ignoreTouchRelease = false; + } + return; + } + if (ignoreConfirmRelease) { const bool confirmReleased = mappedInput.wasReleased(MappedInputManager::Button::Confirm); if (confirmReleased || !mappedInput.isPressed(MappedInputManager::Button::Confirm)) { @@ -33,63 +54,8 @@ void FileBrowserActionActivity::loop() { } } - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); - return; - } - - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - setResult(FileBrowserActionResult{static_cast(items[selectedIndex].action)}); - finish(); - return; - } - - buttonNavigator.onNext([this] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast(items.size())); - requestUpdate(); - }); - - buttonNavigator.onPrevious([this] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast(items.size())); - requestUpdate(); - }); + optionPopup.handleInput(mappedInput, [this] { requestUpdate(); }); + if (!optionPopup.isActive() && !selectionMade) finishCancelled(); } -void FileBrowserActionActivity::render(RenderLock&&) { - renderer.clearScreen(); - - const auto& metrics = UITheme::getInstance().getMetrics(); - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - const int titleX = metrics.contentSidePadding; - const int titleMaxWidth = std::max(0, pageWidth - titleX - metrics.contentSidePadding - kBatteryTextReserveWidth); - const auto titleLines = - renderer.wrappedText(kTitleFontId, title.c_str(), titleMaxWidth, kTitleMaxLines, EpdFontFamily::BOLD); - const int titleLineHeight = renderer.getLineHeight(kTitleFontId); - const int titleBlockHeight = static_cast(titleLines.size()) * titleLineHeight + - std::max(0, static_cast(titleLines.size()) - 1) * kTitleLineGap; - const bool tallHeader = metrics.headerHeight > 60; - const int titleY = metrics.topPadding + (tallHeader ? metrics.batteryBarHeight + 3 : kCompactTitleY); - const int titleBottomPadding = tallHeader ? kTallHeaderTitleBottomPadding : kCompactHeaderTitleBottomPadding; - const int actionHeaderHeight = - std::max(metrics.headerHeight, titleY - metrics.topPadding + titleBlockHeight + titleBottomPadding); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, actionHeaderHeight}, ""); - - for (int i = 0; i < static_cast(titleLines.size()); ++i) { - renderer.drawText(kTitleFontId, titleX, titleY + i * (titleLineHeight + kTitleLineGap), titleLines[i].c_str(), true, - EpdFontFamily::BOLD); - } - - const int contentTop = metrics.topPadding + actionHeaderHeight + metrics.verticalSpacing; - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; - GUI.drawList(renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(items.size()), selectedIndex, - [this](int index) { return std::string(I18N.get(items[index].labelId)); }); - - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - - renderer.displayBuffer(); -} +void FileBrowserActionActivity::render(RenderLock&&) { optionPopup.processRender(renderer, mappedInput); } diff --git a/src/activities/home/FileBrowserActionActivity.h b/src/activities/home/FileBrowserActionActivity.h index df11bb2b52..891e02023c 100644 --- a/src/activities/home/FileBrowserActionActivity.h +++ b/src/activities/home/FileBrowserActionActivity.h @@ -6,7 +6,7 @@ #include #include "../Activity.h" -#include "util/ButtonNavigator.h" +#include "components/OptionPopup.h" enum class FileBrowserAction : int { Delete = 0, @@ -24,6 +24,7 @@ enum class FileBrowserAction : int { DeleteClippings = 12, EpubRenderMode = 13, ResetReaderSettings = 14, + SendNearby = 15, }; class FileBrowserActionActivity final : public Activity { @@ -34,20 +35,20 @@ class FileBrowserActionActivity final : public Activity { }; FileBrowserActionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string title, - std::vector items, bool ignoreInitialConfirmRelease = false) - : Activity("FileBrowserAction", renderer, mappedInput), - title(std::move(title)), - items(std::move(items)), - ignoreConfirmRelease(ignoreInitialConfirmRelease) {} + std::vector items, bool ignoreInitialConfirmRelease = false); void onEnter() override; void loop() override; void render(RenderLock&&) override; private: - ButtonNavigator buttonNavigator; + void finishCancelled(); + std::string title; std::vector items; - int selectedIndex = 0; + std::vector optionLabels; + OptionPopup optionPopup; bool ignoreConfirmRelease = false; + bool ignoreTouchRelease = false; + bool selectionMade = false; }; diff --git a/src/activities/home/FileBrowserActivity.cpp b/src/activities/home/FileBrowserActivity.cpp index 81809dabeb..f0980bc594 100644 --- a/src/activities/home/FileBrowserActivity.cpp +++ b/src/activities/home/FileBrowserActivity.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include "BookActions.h" @@ -19,19 +20,29 @@ #include "activities/util/ConfirmationActivity.h" #include "activities/util/OptionSelectionActivity.h" #include "components/CompactHeader.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "components/themes/minimal/MinimalTheme.h" #include "fontIds.h" +namespace fui = freeink::ui; + namespace { constexpr unsigned long GO_HOME_MS = 1000; constexpr unsigned long COMPLETED_FEEDBACK_MS = 1000; constexpr int ROOT_HINT_GAP = 20; constexpr size_t NAME_BUFFER_SIZE = 500; +constexpr fui::ActionId ACTION_ROW = 1; constexpr size_t INDEX_THRESHOLD = 200; constexpr uint32_t FILE_BROWSER_APPEND_MIN_FREE_AFTER_ALLOC = 48U * 1024U; constexpr uint32_t FILE_BROWSER_APPEND_MIN_MAX_ALLOC_AFTER_ALLOC = 16U * 1024U; +bool usesTwoLineFileBrowserRows() { + return SETTINGS.fileBrowserDisplay == CrossPointSettings::FILE_BROWSER_DISPLAY_2_LINES; +} + bool isDefaultSleepFolderPath(const std::string& path) { return path == "/sleep" || path == "/.sleep"; } bool isSleepImageFile(const std::string& path) { @@ -105,6 +116,11 @@ bool acceptFirmware(const char* name, bool isDir) { return isDir || FsHelpers::checkFileExtension(std::string_view{name}, ".bin"); } +bool acceptDirectory(const char* name, const bool isDir) { + return isDir && !isMacOSMetadataEntry(name) && !isWindowsMetadataEntry(name) && + (SETTINGS.showHiddenFiles || name[0] != '.'); +} + std::string buildFullPath(std::string basepath, const std::string& entry) { if (basepath.back() != '/') basepath += "/"; return basepath + entry; @@ -157,8 +173,17 @@ void collectMetadataPathsRecursively(const std::string& dirPath, std::vector(cap, INDEX_THRESHOLD)); for (auto file = root.openNextFile(); file; file = root.openNextFile()) { @@ -251,7 +277,8 @@ void FileBrowserActivity::loadFiles() { GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); } - const auto accept = (mode == Mode::PickFirmware) ? acceptFirmware : acceptCommon; + const auto accept = + mode == Mode::PickFirmware ? acceptFirmware : (mode == Mode::PickDirectory ? acceptDirectory : acceptCommon); if (fileIndex->open(basepath.c_str(), accept)) { usingIndex = true; requestUpdate(true); @@ -313,6 +340,7 @@ void FileBrowserActivity::onEnter() { } selectorIndex = 0; + showFileSelection = true; // If Confirm was held while this activity opened (typical when launched from a menu), ignore // its release — otherwise we'd immediately auto-open whatever is at index 0. @@ -343,6 +371,12 @@ void FileBrowserActivity::onEnter() { loadFiles(); } + uiReady = false; + visibleRows = 1; + topIndex = followListSelection(static_cast(selectorIndex), 0, visibleRows, static_cast(entryCount())); + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &FileBrowserActivity::onRowEvent, this); + app.setScreen(&FileBrowserActivity::listScreen, this); requestUpdate(); } @@ -359,18 +393,15 @@ void FileBrowserActivity::onExit() { void FileBrowserActivity::promptDeleteFile(const std::string& fullPath, const std::string& entry) { auto handler = [this, fullPath](const ActivityResult& res) { if (res.isCancelled) { - LOG_DBG("FileBrowser", "Delete cancelled by user"); return; } - LOG_DBG("FileBrowser", "Attempting to delete: %s", fullPath.c_str()); BookActions::clearFileMetadata(fullPath); if (!Storage.remove(fullPath.c_str())) { LOG_ERR("FileBrowser", "Failed to delete file: %s", fullPath.c_str()); return; } - LOG_DBG("FileBrowser", "Deleted successfully"); if (isPinnedSleepFavorite(fullPath)) { unpinSleepFavorite(); } @@ -394,20 +425,17 @@ void FileBrowserActivity::promptDeleteDirectory(const std::string& fullPath, con auto handler = [this, dirPath](const ActivityResult& res) { longPressConfirmHandled = false; if (res.isCancelled) { - LOG_DBG("FileBrowser", "Delete cancelled by user"); return; } std::vector metadataPaths; collectMetadataPathsRecursively(dirPath, metadataPaths); - LOG_DBG("FileBrowser", "Attempting to delete directory: %s", dirPath.c_str()); if (!Storage.removeDir(dirPath.c_str())) { LOG_ERR("FileBrowser", "Failed to delete directory: %s", dirPath.c_str()); return; } - LOG_DBG("FileBrowser", "Deleted successfully"); for (const auto& metadataPath : metadataPaths) { BookActions::clearFileMetadata(metadataPath); } @@ -475,6 +503,7 @@ void FileBrowserActivity::showDirectoryActionMenu(const std::string& entry, bool case FileBrowserAction::DeleteClippings: case FileBrowserAction::EpubRenderMode: case FileBrowserAction::ResetReaderSettings: + case FileBrowserAction::SendNearby: return; } }); @@ -555,6 +584,10 @@ void FileBrowserActivity::showFileActionMenu(const std::string& entry, bool igno const std::string fullPath = buildFullPath(basepath, entry); std::vector items = BookActions::buildBookActionItems(fullPath, false); + if (BookActions::canSendNearby(fullPath)) { + items.push_back({FileBrowserAction::SendNearby, StrId::STR_SEND_NEARBY_BOOK}); + } + const bool canPinFavorite = isSleepFavoriteFolder(basepath) && isSleepImageFile(entry); if (canPinFavorite) { items.push_back( @@ -573,6 +606,9 @@ void FileBrowserActivity::showFileActionMenu(const std::string& entry, bool igno const auto action = static_cast(std::get(result.data).action); switch (action) { + case FileBrowserAction::SendNearby: + activityManager.goToNearbyBookSend(fullPath, false); + return; case FileBrowserAction::Delete: promptDeleteFile(fullPath, entry); return; @@ -703,7 +739,55 @@ void FileBrowserActivity::toggleHiddenFiles() { requestUpdate(); } +void FileBrowserActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || static_cast(event.value) >= self->entryCount()) return; + self->selectorIndex = static_cast(event.value); + // Activation navigates or opens; a lingering flash would gray an unrelated + // row on the next list. + self->app.clearTapFlash(); + self->activateSelected(); +} + +void FileBrowserActivity::activateSelected() { + if (lockNextConfirmRelease) { + lockNextConfirmRelease = false; + return; + } + if (entryCount() == 0) return; + + const std::string entry = entryNameAt(selectorIndex); + const bool isDirectory = (entry.back() == '/'); + + // Firmware picker: select file -> return path; navigate into directories normally. + if (mode == Mode::PickFirmware && !isDirectory) { + std::string cleanBasePath = basepath; + if (cleanBasePath.back() != '/') cleanBasePath += "/"; + ActivityResult res{FilePathResult{cleanBasePath + entry}}; + res.isCancelled = false; + setResult(std::move(res)); + finish(); + return; + } + + if (basepath.back() != '/') basepath += "/"; + if (isDirectory) { + basepath += entry.substr(0, entry.length() - 1); + loadFiles(); + selectorIndex = 0; + topIndex = 0; + showFileSelection = true; + requestUpdate(); + } else { + onSelectBook(basepath + entry); + } +} + void FileBrowserActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + navigateBack(); + return; + } if (pendingCompletedFeedback) { const bool timedOut = (millis() - completedFeedbackShowTime) >= COMPLETED_FEEDBACK_MS; const bool navPressed = mappedInput.wasReleased(MappedInputManager::Button::Left) || @@ -717,7 +801,17 @@ void FileBrowserActivity::loop() { } } - // Long press BACK/HOME (1s+) toggles hidden files (Books mode only). + // In directory-picker mode the fourth front button selects the folder shown + // in the path band; Confirm continues to descend into the highlighted child. + if (mode == Mode::PickDirectory && mappedInput.wasReleased(MappedInputManager::Button::Right)) { + ActivityResult result{FilePathResult{normalizeDirectoryPath(basepath)}}; + result.isCancelled = false; + setResult(std::move(result)); + finish(); + return; + } + + // Long press BACK (1s+) toggles hidden files (Books mode only). // In firmware-pick mode we keep navigation simple: short Back = up dir / cancel. if (mode == Mode::Books && !longPressBackHandled && mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS && !lockLongPressBack) { @@ -732,15 +826,32 @@ void FileBrowserActivity::loop() { } const auto& metrics = UITheme::getInstance().getMetrics(); - const int pathReserved = renderer.getLineHeight(SMALL_FONT_ID) + metrics.verticalSpacing; - const int contentTop = CompactHeader::contentTop(metrics); - const int contentHeight = - renderer.getScreenHeight() - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing - pathReserved; - int pageItems = std::max(1, contentHeight / metrics.listRowHeight); - const bool compactFileRows = - !usingIndex && SETTINGS.fileBrowserDisplay == CrossPointSettings::FILE_BROWSER_DISPLAY_2_LINES; - if (compactFileRows) { - pageItems = std::max(1, contentHeight / MinimalTheme::compactFileBrowserRowHeightFor(renderer)); + const int contentTop = + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; + const int contentBottom = renderer.getScreenHeight() - metrics.buttonHintsHeight - + renderer.getLineHeight(SMALL_FONT_ID) - metrics.verticalSpacing; + const auto tokens = uiThemeTokens(uiTarget); + const auto rowType = usesTwoLineFileBrowserRows() ? UiListRowType::WithSubtitle : UiListRowType::SingleLine; + const int rowStep = uiListRowHeight(tokens, rowType) + tokens.listRowGap; + const int listSize = static_cast(entryCount()); + int touchX = 0; + int touchY = 0; + if (mode == Mode::Books && !longPressConfirmHandled && + mappedInput.isScreenTouchLongPress(touchX, touchY, GO_HOME_MS) && touchY >= contentTop && + touchY < contentBottom && rowStep > 0) { + const int touchedEntry = topIndex + (touchY - contentTop) / rowStep; + if (touchedEntry < 0 || touchedEntry >= listSize || touchedEntry >= topIndex + visibleRows) return; + selectorIndex = static_cast(touchedEntry); + showFileSelection = true; + mappedInput.suppressNextTouchTap(); + longPressConfirmHandled = true; + const std::string entry = entryNameAt(selectorIndex); + if (entry.back() == '/') { + showDirectoryActionMenu(entry, true); + } else { + showFileActionMenu(entry, true); + } + return; } if (entryCount() > 0) { @@ -758,50 +869,26 @@ void FileBrowserActivity::loop() { } } + // Touch goes through the FreeInkApp: render() registered the row hit rects; + // route the snapshot and let onRowEvent dispatch. + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + // No pressed-state repaint: the render it triggers would drop a slow + // tap's release inside the uiReady window (tap-to-activate needed two + // taps), and it costs a second e-ink refresh per tap. + if (app.invalidated()) requestUpdate(); + if (event) return; // dispatched to onRowEvent + } + } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (longPressConfirmHandled) { longPressConfirmHandled = false; - return; - } - if (lockNextConfirmRelease) { - lockNextConfirmRelease = false; - return; - } - if (entryCount() == 0) return; - - const std::string entry = entryNameAt(selectorIndex); - bool isDirectory = (entry.back() == '/'); - - // Firmware picker: select file -> return path; navigate into directories normally. - if (mode == Mode::PickFirmware && !isDirectory) { - std::string cleanBasePath = basepath; - if (cleanBasePath.back() != '/') cleanBasePath += "/"; - ActivityResult res{FilePathResult{cleanBasePath + entry}}; - res.isCancelled = false; - setResult(std::move(res)); - finish(); - return; - } - - if (mode == Mode::Books && mappedInput.getHeldTime() >= GO_HOME_MS) { - if (isDirectory) { - showDirectoryActionMenu(entry); - } else { - showFileActionMenu(entry); - } - return; } else { - // --- SHORT PRESS ACTION: OPEN/NAVIGATE --- - if (basepath.back() != '/') basepath += "/"; - - if (isDirectory) { - basepath += entry.substr(0, entry.length() - 1); - loadFiles(); - selectorIndex = 0; - requestUpdate(); - } else { - onSelectBook(basepath + entry); - } + showFileSelection = true; + activateSelected(); } return; } @@ -811,52 +898,79 @@ void FileBrowserActivity::loop() { longPressBackHandled = false; return; } - // Short press: go up one directory, or go home if at root if (mappedInput.getHeldTime() < GO_HOME_MS) { - if (basepath != "/") { - const std::string oldPath = basepath; - - basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); - if (basepath.empty()) basepath = "/"; - loadFiles(); - - const auto pos = oldPath.find_last_of('/'); - const std::string dirName = oldPath.substr(pos + 1) + "/"; - selectorIndex = findEntry(dirName); - - requestUpdate(); - } else if (mode == Mode::PickFirmware) { - // Firmware picker at root: cancel back to caller instead of going home. - ActivityResult res; - res.isCancelled = true; - setResult(std::move(res)); - finish(); - } else { - onGoHome(); - } + navigateBack(); } } - int listSize = static_cast(entryCount()); - buttonNavigator.onNextRelease([this, listSize] { - selectorIndex = ButtonNavigator::nextIndex(static_cast(selectorIndex), listSize); - requestUpdate(); - }); + if (listSize <= 0) return; - buttonNavigator.onPreviousRelease([this, listSize] { - selectorIndex = ButtonNavigator::previousIndex(static_cast(selectorIndex), listSize); - requestUpdate(); - }); + // Swipes scroll the viewport; the selection stays put and button navigation + // pulls the view back to it. + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, listSize); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } - buttonNavigator.onNextContinuous([this, listSize, pageItems] { - selectorIndex = ButtonNavigator::nextPageIndex(static_cast(selectorIndex), listSize, pageItems); + const auto moveSelection = [this, listSize](const int index) { + selectorIndex = static_cast(index); + showFileSelection = true; + topIndex = followListSelection(static_cast(selectorIndex), topIndex, visibleRows, listSize); requestUpdate(); - }); + }; + const auto moveNext = [this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::nextIndex(static_cast(selectorIndex), listSize)); + }; + const auto movePrevious = [this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::previousIndex(static_cast(selectorIndex), listSize)); + }; + const auto pageNext = [this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::nextPageIndex(static_cast(selectorIndex), listSize, visibleRows)); + }; + const auto pagePrevious = [this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::previousPageIndex(static_cast(selectorIndex), listSize, visibleRows)); + }; + if (mode == Mode::PickDirectory) { + // The front-right button selects this folder. Keep navigation on the side + // buttons so the remaining front-left button is not a misleading one-way control. + buttonNavigator.onRelease({MappedInputManager::Button::Down}, moveNext); + buttonNavigator.onRelease({MappedInputManager::Button::Up}, movePrevious); + buttonNavigator.onContinuous({MappedInputManager::Button::Down}, pageNext); + buttonNavigator.onContinuous({MappedInputManager::Button::Up}, pagePrevious); + } else { + buttonNavigator.onNextRelease(moveNext); + buttonNavigator.onPreviousRelease(movePrevious); + buttonNavigator.onNextContinuous(pageNext); + buttonNavigator.onPreviousContinuous(pagePrevious); + } +} - buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { - selectorIndex = ButtonNavigator::previousPageIndex(static_cast(selectorIndex), listSize, pageItems); +void FileBrowserActivity::navigateBack() { + if (basepath != "/") { + const std::string oldPath = basepath; + basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); + if (basepath.empty()) basepath = "/"; + loadFiles(); + + const std::string dirName = oldPath.substr(oldPath.find_last_of('/') + 1) + "/"; + selectorIndex = findEntry(dirName); + showFileSelection = true; + topIndex = followListSelection(static_cast(selectorIndex), 0, visibleRows, static_cast(entryCount())); requestUpdate(); - }); + } else if (mode != Mode::Books) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + } else { + onGoHome(); + } } namespace { @@ -886,77 +1000,27 @@ std::string getFileExtension(const std::string& filename) { } // namespace -void FileBrowserActivity::render(RenderLock&&) { - renderer.clearScreen(); +void FileBrowserActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); +void FileBrowserActivity::buildListScreen(UiApp::ScreenType& screen) { const auto& metrics = UITheme::getInstance().getMetrics(); - - std::string folderName = - (mode == Mode::PickFirmware) - ? std::string(tr(STR_SELECT_FIRMWARE_FILE)) - : ((basepath == "/") ? std::string(tr(STR_SD_CARD)) : basepath.substr(basepath.rfind('/') + 1)); - CompactHeader::drawTitle(renderer, folderName.c_str()); - - const int pathLineHeight = renderer.getLineHeight(SMALL_FONT_ID); - const int pathReserved = pathLineHeight + metrics.verticalSpacing; - const int pathY = pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - pathLineHeight; - const int pathMaxWidth = pageWidth - metrics.contentSidePadding * 2; - const int contentTop = CompactHeader::contentTop(metrics); - const int contentHeight = - pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing - pathReserved; - const size_t visibleEntries = entryCount(); - if (visibleEntries == 0) { - const char* emptyMsg = fileListMemoryLimited - ? tr(STR_MEMORY_ERROR) - : ((mode == Mode::PickFirmware) ? tr(STR_NO_BIN_FILES) : tr(STR_NO_FILES_FOUND)); - renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, emptyMsg); - } else { - const bool compactFileRows = - !usingIndex && SETTINGS.fileBrowserDisplay == CrossPointSettings::FILE_BROWSER_DISPLAY_2_LINES; - std::function compactRowMarker; - if (compactFileRows) { - compactRowMarker = [this](int index) { - const std::string entry = entryNameAt(index); - return !entry.empty() && entry.back() == '/' ? "folder" : ""; - }; - } - const auto rowTitle = [this](int index) { - const std::string entry = entryNameAt(index); - return getFileName(entry); - }; - const auto rowIcon = [this](int index) { - const std::string entry = entryNameAt(index); - return UITheme::getFileIcon(entry); - }; - const auto rowValue = [this](int index) { - const std::string entry = entryNameAt(index); - const std::string extension = SETTINGS.hideFileExtension != 0 ? std::string() : getFileExtension(entry); - const std::string fullPath = buildFullPath(basepath, entry); - if (entry.back() == '/' && isPreferredSleepFolder(fullPath)) { - return std::string("*"); - } - if (isPinnedSleepFavorite(fullPath)) { - return extension.empty() ? std::string("*") : "* " + extension; - } - return extension; - }; - const Rect listRect{0, contentTop, pageWidth, contentHeight}; - if (compactFileRows) { - MinimalTheme::drawCompactFileBrowserList(renderer, listRect, static_cast(visibleEntries), selectorIndex, - rowTitle, compactRowMarker, rowIcon, rowValue); - } else { - GUI.drawList(renderer, listRect, static_cast(visibleEntries), selectorIndex, rowTitle, compactRowMarker, - rowIcon, rowValue, false); - } - } - - // Full path display + // Content below the GUI.drawHeader band, above the button hints. + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + screen.spacer(static_cast(metrics.verticalSpacing)); + + // Full path band at the bottom: separator on top, left-truncated so the + // deepest directory stays visible. { - const int separatorY = pathY - metrics.verticalSpacing / 2; - renderer.drawLine(0, separatorY, pageWidth - 1, separatorY, 3, true); - // Left-truncate so the deepest directory is always visible + const int pathLineHeight = renderer.getLineHeight(SMALL_FONT_ID); + const fui::Rect band = screen.takeBottom(static_cast(pathLineHeight + metrics.verticalSpacing)); + screen.target().fill(fui::Rect{band.x, band.y, band.width, 3}, fui::Paint::solid(fui::Color::Black)); + const int pathY = + band.y + metrics.verticalSpacing / 2 + (band.height - metrics.verticalSpacing / 2 - pathLineHeight) / 2; + const int pathMaxWidth = band.width - metrics.contentSidePadding * 2; const char* pathStr = basepath.c_str(); const char* pathDisplay = pathStr; char leftTruncBuf[256]; @@ -974,21 +1038,114 @@ void FileBrowserActivity::render(RenderLock&&) { snprintf(leftTruncBuf, sizeof(leftTruncBuf), "%s%s", ellipsis, p); pathDisplay = leftTruncBuf; } - renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, pathY, pathDisplay); + renderer.drawText(SMALL_FONT_ID, band.x + metrics.contentSidePadding, pathY, pathDisplay); + } + + const size_t totalEntries = entryCount(); + if (totalEntries == 0) { + const char* emptyMessage = fileListMemoryLimited + ? tr(STR_MEMORY_ERROR) + : (mode == Mode::PickFirmware ? tr(STR_NO_BIN_FILES) : tr(STR_NO_FILES_FOUND)); + screen.centeredText(emptyMessage, screen.theme().bodyText); + return; + } + + fui::ListProps props; + const bool twoLineRows = usesTwoLineFileBrowserRows(); + const auto rowType = twoLineRows ? UiListRowType::WithSubtitle : UiListRowType::SingleLine; + props.labelText = screen.theme().bodyText; + props.labelText.maxLines = twoLineRows ? 2 : 1; + const fui::Rect listRect = screen.body(); + const auto rows = configureUiList(props, screen.theme(), listRect, rowType); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(totalEntries)); + const size_t drawCount = std::min(visibleRows, totalEntries - static_cast(topIndex)); + + // Only materialize the visible window. Large folders continue to use + // FileIndex instead of duplicating every filename on the heap for UI rows. + std::vector names(drawCount); + std::vector values(drawCount); + std::vector items; + items.reserve(drawCount); + for (size_t i = 0; i < drawCount; i++) { + const size_t entryIndex = static_cast(topIndex) + i; + const std::string entry = entryNameAt(entryIndex); + names[i] = getFileName(entry); + if (SETTINGS.hideFileExtension == 0) values[i] = getFileExtension(entry); + const std::string fullPath = buildFullPath(basepath, entry); + if ((entry.back() == '/' && isPreferredSleepFolder(fullPath)) || isPinnedSleepFavorite(fullPath)) { + values[i] = values[i].empty() ? "*" : "* " + values[i]; + } + fui::ListItem item; + item.label = names[i].c_str(); + if (!values[i].empty()) item.value = values[i].c_str(); + item.icon = listIconFor(UITheme::getFileIcon(entry), twoLineRows ? 32 : 24); + item.actionValue = static_cast(entryIndex); + items.push_back(item); } - // Help text - const char* backLabel = (basepath == "/") ? (mode == Mode::PickFirmware ? tr(STR_BACK) : tr(STR_HOME)) : tr(STR_BACK); + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = + selectorIndex >= static_cast(topIndex) && selectorIndex < static_cast(topIndex) + drawCount + ? static_cast(selectorIndex - static_cast(topIndex)) + : -1; + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; // physical buttons stay in loop() + props.valueInset = 8; // air between the extension and the row edge + // A file extension is short, so do not sacrifice most of a two-line title + // to visually balance it with the value column. + props.balanceWrappedLabelWithValue = false; + props.topIndex = 0; + screen.list(props); + fui::drawListScrollIndicator(screen.target(), listRect, totalEntries, visibleRows, topIndex, + screen.theme().listScrollWidth, screen.theme().listScrollSide, + screen.theme().listScrollInset); +} + +void FileBrowserActivity::render(RenderLock&&) { + renderer.clearScreen(); + + const auto pageWidth = renderer.getScreenWidth(); + const auto& metrics = UITheme::getInstance().getMetrics(); + + std::string folderName = + mode == Mode::PickFirmware + ? std::string(tr(STR_SELECT_FIRMWARE_FILE)) + : (mode == Mode::PickDirectory + ? std::string(tr(STR_SELECT_RECEIVE_FOLDER)) + : ((basepath == "/") ? std::string(tr(STR_SD_CARD)) : basepath.substr(basepath.rfind('/') + 1))); + // Header via GUI.drawHeader (already FreeInkUI-themed) for the battery + // indicator; the rest of the screen renders through the app. + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, folderName.c_str(), false); + } else { + GUI.drawHeader(renderer, header, folderName.c_str()); + } + + uiReady = false; + app.render(); + uiReady = true; + + const size_t visibleEntries = entryCount(); + const char* backLabel = (basepath == "/") ? (mode == Mode::Books ? tr(STR_HOME) : tr(STR_BACK)) : tr(STR_BACK); // In PickFirmware mode, Confirm on a .bin returns the path to the caller (not "open"); show // STR_SELECT instead. Directories in the same picker still descend, so keep STR_OPEN there. const bool selectingFirmwareFile = mode == Mode::PickFirmware && visibleEntries > 0 && std::string(entryNameAt(selectorIndex)).back() != '/'; const char* confirmLabel = visibleEntries == 0 ? "" : (selectingFirmwareFile ? tr(STR_SELECT) : tr(STR_OPEN)); - const auto labels = mappedInput.mapLabels(backLabel, confirmLabel, visibleEntries == 0 ? "" : tr(STR_DIR_UP), - visibleEntries == 0 ? "" : tr(STR_DIR_DOWN)); + const auto labels = mappedInput.mapLabels( + backLabel, confirmLabel, visibleEntries == 0 || mode == Mode::PickDirectory ? "" : tr(STR_DIR_UP), + mode == Mode::PickDirectory ? tr(STR_SELECT) : (visibleEntries == 0 ? "" : tr(STR_DIR_DOWN))); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - if (mode == Mode::Books && basepath == "/") { + if (!mappedInput.hasTouch() && mode == Mode::Books && basepath == "/") { + const int pathLineHeight = renderer.getLineHeight(SMALL_FONT_ID); + const int bandY = renderer.getScreenHeight() - metrics.buttonHintsHeight - pathLineHeight - metrics.verticalSpacing; + const int pathY = bandY + metrics.verticalSpacing / 2 + + (pathLineHeight + metrics.verticalSpacing - metrics.verticalSpacing / 2 - pathLineHeight) / 2; + const int pathMaxWidth = pageWidth - metrics.contentSidePadding * 2; const int usedPathWidth = renderer.getTextWidth(SMALL_FONT_ID, basepath.c_str()); const int hintMaxWidth = pathMaxWidth - usedPathWidth - ROOT_HINT_GAP; const auto hint = renderer.truncatedText(SMALL_FONT_ID, tr(STR_TOGGLE_HIDDEN_FILES_HINT), hintMaxWidth); diff --git a/src/activities/home/FileBrowserActivity.h b/src/activities/home/FileBrowserActivity.h index 0e599501dd..c9834b9764 100644 --- a/src/activities/home/FileBrowserActivity.h +++ b/src/activities/home/FileBrowserActivity.h @@ -1,8 +1,11 @@ #pragma once #include +#include +#include #include +#include #include #include #include @@ -14,10 +17,14 @@ class FileBrowserActivity final : public Activity { public: - // Books = standard reader browser; PickFirmware = filter to .bin only and return path via ActivityResult. - enum class Mode { Books, PickFirmware }; + // Picker modes return their selected path via ActivityResult. + enum class Mode { Books, PickFirmware, PickDirectory }; private: + // FreeInkApp hosts the file list (themed rows, icons, touch routing); the + // header stays on GUI.drawHeader for the battery indicator. + using UiApp = freeink::ui::FreeInkApp<20, 4>; + // Deletion void promptDeleteFile(const std::string& fullPath, const std::string& entry); void promptDeleteDirectory(const std::string& fullPath, const std::string& entry, @@ -35,6 +42,7 @@ class FileBrowserActivity final : public Activity { ButtonNavigator buttonNavigator; size_t selectorIndex = 0; + bool showFileSelection = true; bool lockLongPressBack = false; bool longPressBackHandled = false; @@ -60,6 +68,20 @@ class FileBrowserActivity final : public Activity { bool usingIndex = false; bool fileListMemoryLimited = false; + freeink::ui::GfxRendererTarget uiTarget; // must precede `app`: the app holds a reference to it + UiApp app; + // render() rebuilds the app's interaction table; loop() only routes touch + // snapshots against it while this is true (the two run on different tasks). + std::atomic uiReady{false}; + int visibleRows = 1; // rows per page at the current scale; set by the screen builder + int topIndex = 0; // viewport scroll position, decoupled from the selection + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); + void activateSelected(); + void navigateBack(); + // Data loading void clearIndexNameCache(); void loadFiles(); @@ -71,10 +93,7 @@ class FileBrowserActivity final : public Activity { public: explicit FileBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialPath = "/", - Mode mode = Mode::Books) - : Activity("FileBrowser", renderer, mappedInput), - mode(mode), - basepath(initialPath.empty() ? "/" : std::move(initialPath)) {} + Mode mode = Mode::Books); void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index dccaf991ad..4e1432dc74 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -4,10 +4,9 @@ #include #include #include +#include #include #include -#include -#include #include #include #include @@ -41,7 +40,9 @@ namespace { constexpr uint32_t CAROUSEL_CACHE_MAGIC = 0x43434152; // "CCAR" -constexpr uint16_t CAROUSEL_CACHE_VERSION = 4; +// Cached frames include all Home visuals, including the menu icons. Bump this +// whenever their rendering changes so stale snapshots are rebuilt after OTA. +constexpr uint16_t CAROUSEL_CACHE_VERSION = 5; constexpr char CAROUSEL_CACHE_PATH[] = "/.crosspoint/home_carousel_cache.bin"; constexpr char CAROUSEL_CACHE_TMP_PATH[] = "/.crosspoint/home_carousel_cache.tmp"; constexpr uint32_t CAROUSEL_FRAME_MIN_FREE_AFTER_ALLOC = 64U * 1024U; @@ -81,6 +82,10 @@ struct HomeMenuEntries { const HomeMenuEntry& operator[](int index) const { return entries[index]; } }; +bool containsPoint(const Rect& rect, const int x, const int y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} + struct CarouselCacheHeader { uint32_t magic; uint16_t version; @@ -207,12 +212,18 @@ bool loadEpubHighlightedContext(const RecentBook& book, const bool loadProgress, return true; } -void updateRecentBookCoverPath(const RecentBook& book, const std::string& coverBmpPath) { - if (!RECENT_BOOKS.updateBook(book.path, book.title, book.author, coverBmpPath)) { +void updateRecentBookCover(const RecentBook& book) { + if (!RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.coverBmpPath, book.coverState)) { LOG_ERR("HOME", "failed to update recent book metadata: %s", book.path.c_str()); } } +void markCoverMissing(RecentBook& book) { + book.coverBmpPath.clear(); + book.coverState = RecentBook::CoverState::Missing; + updateRecentBookCover(book); +} + bool hasThumbnailPlaceholder(const std::string& coverBmpPath) { return coverBmpPath.find("[WIDTH]") != std::string::npos || coverBmpPath.find("[HEIGHT]") != std::string::npos; } @@ -228,7 +239,7 @@ std::string getReusableCoverPath(const RecentBook& book) { } bool ensureReusableCoverPath(RecentBook& book) { - if (hasThumbnailPlaceholder(book.coverBmpPath)) { + if (book.coverState == RecentBook::CoverState::Missing || hasThumbnailPlaceholder(book.coverBmpPath)) { return false; } @@ -238,7 +249,7 @@ bool ensureReusableCoverPath(RecentBook& book) { } book.coverBmpPath = reusablePath; - updateRecentBookCoverPath(book, reusablePath); + updateRecentBookCover(book); return true; } @@ -338,6 +349,8 @@ bool isDashboardTheme() { bool usesMinimalHomeInteraction() { return isMinimalTheme() || isDashboardTheme(); } +bool showMinimalHomeButtonHints(const MappedInputManager& mappedInput) { return !mappedInput.hasTouch(); } + bool isAnyFrontButtonPressed(const MappedInputManager& mappedInput) { return mappedInput.isFrontButtonPressed(HalGPIO::BTN_BACK) || mappedInput.isFrontButtonPressed(HalGPIO::BTN_CONFIRM) || @@ -532,6 +545,7 @@ int getHomeMenuSelectionOffset(const std::vector& recentBooks) { const auto& metrics = UITheme::getInstance().getMetrics(); return metrics.homeContinueReadingInMenu ? 0 : getVisibleRecentBookCount(recentBooks); } + } // namespace // --------------------------------------------------------------------------- @@ -638,13 +652,16 @@ void HomeActivity::loadRecentCovers(int coverHeight) { } recentsLoading = true; - // EPUB cover extraction needs the ZIP inflater's 32KB history buffer. Drop - // the saved cover tile while generating thumbnails so Home has a larger - // contiguous heap block available. - freeCoverBuffer(); - auto zipInflateScratch = ScratchWorkspace::acquire(InflateReader::STREAMING_DICT_SIZE, "Home EPUB thumbnails"); bool showingLoading = false; Rect popupRect; + auto showLoadingProgress = [&](const int value) { + if (!showingLoading) { + showingLoading = true; + popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); + } + GUI.fillPopupProgress(renderer, popupRect, std::clamp(value, 0, 100)); + renderer.displayBuffer(); + }; const bool isCarouselTheme = static_cast(SETTINGS.uiTheme) == CrossPointSettings::UI_THEME::LYRA_CAROUSEL; @@ -677,15 +694,9 @@ void HomeActivity::loadRecentCovers(int coverHeight) { if (centerMissing || sideMissing) { if (FsHelpers::hasEpubExtension(book.path)) { Epub epub(book.path, "/.crosspoint"); - if (!showingLoading) { - showingLoading = true; - popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - } - GUI.fillPopupProgress(renderer, popupRect, 10 + progress * progressIncrement); - if (!epub.load(true, true)) { + showLoadingProgress(10 + progress * progressIncrement); + if (!epub.load(true, true, Epub::XLocationLoadMode::Skip)) { LOG_ERR("HOME", "carousel: failed to load EPUB cache for thumb generation: %s", book.path.c_str()); - updateRecentBookCoverPath(book, ""); - book.coverBmpPath = ""; coverRendered = false; requestUpdate(); progress++; @@ -701,21 +712,16 @@ void HomeActivity::loadRecentCovers(int coverHeight) { SETTINGS.getReaderFontId()) && success; if (!success) { - updateRecentBookCoverPath(book, ""); - book.coverBmpPath = ""; - } else { - if (bookIdx < bookUpdated.size()) bookUpdated[bookIdx] = true; + if (!epub.hasCoverImage()) markCoverMissing(book); + } else if (bookIdx < bookUpdated.size()) { + bookUpdated[bookIdx] = true; } coverRendered = false; requestUpdate(); } else if (FsHelpers::hasXtcExtension(book.path)) { Xtc xtc(book.path, "/.crosspoint"); if (xtc.load()) { - if (!showingLoading) { - showingLoading = true; - popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - } - GUI.fillPopupProgress(renderer, popupRect, 10 + progress * progressIncrement); + showLoadingProgress(10 + progress * progressIncrement); bool success = true; if (centerMissing) success = @@ -723,10 +729,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) { if (sideMissing) success = xtc.generateThumbBmp(LyraCarouselTheme::kSideCoverW, LyraCarouselTheme::kSideCoverH) && success; - if (!success) { - updateRecentBookCoverPath(book, ""); - book.coverBmpPath = ""; - } else { + if (success) { if (bookIdx < bookUpdated.size()) bookUpdated[bookIdx] = true; } coverRendered = false; @@ -748,15 +751,9 @@ void HomeActivity::loadRecentCovers(int coverHeight) { if (coverPath.empty() || !Storage.exists(coverPath.c_str())) { if (FsHelpers::hasEpubExtension(book.path)) { Epub epub(book.path, "/.crosspoint"); - if (!showingLoading) { - showingLoading = true; - popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - } - GUI.fillPopupProgress(renderer, popupRect, 10 + progress * progressIncrement); - if (!epub.load(true, true)) { + showLoadingProgress(10 + progress * progressIncrement); + if (!epub.load(true, true, Epub::XLocationLoadMode::Skip)) { LOG_ERR("HOME", "failed to load EPUB cache for thumb generation: %s", book.path.c_str()); - updateRecentBookCoverPath(book, ""); - book.coverBmpPath = ""; coverRendered = false; requestUpdate(); progress++; @@ -773,21 +770,16 @@ void HomeActivity::loadRecentCovers(int coverHeight) { SETTINGS.getReaderFontId()) : epub.generateThumbBmp(0, coverHeight, &renderer, SETTINGS.getReaderFontId())); if (!success) { - updateRecentBookCoverPath(book, ""); - book.coverBmpPath = ""; - } else { - if (bookIdx < bookUpdated.size()) bookUpdated[bookIdx] = true; // non-carousel path reuses same tracking + if (!epub.hasCoverImage()) markCoverMissing(book); + } else if (bookIdx < bookUpdated.size()) { + bookUpdated[bookIdx] = true; // non-carousel path reuses same tracking } coverRendered = false; requestUpdate(); } else if (FsHelpers::hasXtcExtension(book.path)) { Xtc xtc(book.path, "/.crosspoint"); if (xtc.load()) { - if (!showingLoading) { - showingLoading = true; - popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - } - GUI.fillPopupProgress(renderer, popupRect, 10 + progress * progressIncrement); + showLoadingProgress(10 + progress * progressIncrement); const bool success = useDashboardThumb ? xtc.generateThumbBmp(static_cast(dashboardHomeCoverWidth(coverHeight)), @@ -796,10 +788,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) { ? xtc.generateThumbBmp(static_cast(minimalHomeCoverWidth(coverHeight)), static_cast(minimalHomeCoverHeight(coverHeight))) : xtc.generateThumbBmp(coverHeight)); - if (!success) { - updateRecentBookCoverPath(book, ""); - book.coverBmpPath = ""; - } else { + if (success) { if (bookIdx < bookUpdated.size()) bookUpdated[bookIdx] = true; } coverRendered = false; @@ -870,8 +859,11 @@ void HomeActivity::onEnter() { selectorIndex = 0; lastCarouselBookIndex = 0; + carouselCoverTouchDownIndex = -1; + carouselCoverTouchDownWasSelected = false; minimalMenuOpen = false; minimalSuppressInitialFrontRelease = usesMinimalHomeInteraction(); + backPressSeen = false; minimalMenuIndex = 0; minimalHomeNavIndex = -1; carouselFramesReady = false; @@ -904,7 +896,7 @@ void HomeActivity::onEnter() { if (isCarouselTheme) { loadAllBookStats(); } - updateHighlightedBookContext(); + updateHighlightedBookContext(false); if (initialMenuItem != HomeMenuItem::NONE) { const bool includeContinueReading = metrics.homeContinueReadingInMenu && !recentBooks.empty(); @@ -913,7 +905,6 @@ void HomeActivity::onEnter() { const int menuIndex = findMenuActionIndex(menuItems, homeActionForInitialMenuItem(initialMenuItem)); if (menuIndex >= 0) { selectorIndex = getHomeMenuSelectionOffset(recentBooks) + menuIndex; - updateHighlightedBookContext(); } } @@ -960,8 +951,7 @@ std::string HomeActivity::getCurrentBookPath() const { return idx >= 0 ? recentBooks[idx].path : std::string{}; } -void HomeActivity::updateHighlightedBookContext() { - const auto start = millis(); +void HomeActivity::updateHighlightedBookContext(const bool allowEpubLoad) { currentBookStats = BookReadingStats{}; currentBookProgressPercent = -1.0f; currentBookChapterTitle.clear(); @@ -975,15 +965,17 @@ void HomeActivity::updateHighlightedBookContext() { if (useCachedStats) { currentBookStats = cachedBookStats[idx]; currentBookProgressPercent = cachedBookProgress[idx]; - if (loadChapterTitle && isEpub) { + if (allowEpubLoad && loadChapterTitle && isEpub) { loadEpubHighlightedContext(book, false, true, nullptr, ¤tBookChapterTitle); } } else { currentBookStats = loadRecentBookStats(book); - if (isEpub) { + if (isEpub && allowEpubLoad) { loadEpubHighlightedContext(book, true, loadChapterTitle, ¤tBookProgressPercent, ¤tBookChapterTitle); - } else { + } else if (!isEpub) { currentBookProgressPercent = RecentBookProgress::loadPercent(book); + } else { + currentBookProgressPercent = RecentBookProgress::loadCachedEpubPercent(book); } if (loadChapterTitle && !isEpub) { currentBookChapterTitle.clear(); @@ -993,8 +985,6 @@ void HomeActivity::updateHighlightedBookContext() { hasReadingStats = hasAnyBookStats(currentBookStats) || hasAnyGlobalStats(globalStats) || (showAllDevicesStats && hasAnyGlobalStats(allDevicesGlobalStats)); - LOG_DBG("HOME", "updateHighlightedBookContext idx=%d cached=%s took %lums", idx, useCachedStats ? "yes" : "no", - millis() - start); } void HomeActivity::onExit() { @@ -1013,6 +1003,11 @@ bool HomeActivity::storeCoverBuffer() { freeCoverBuffer(); const size_t needed = renderer.getRegionByteSize(coverRectX, coverRectY, coverRectW, coverRectH); if (needed == 0) return false; + if (ESP.getFreeHeap() < needed || ESP.getMaxAllocHeap() < needed) { + LOG_DBG("HOME", "Skipping cover buffer cache (%zu bytes, free=%u, maxAlloc=%u)", needed, ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + return false; + } coverBuffer = static_cast(malloc(needed)); if (!coverBuffer) { LOG_ERR("HOME", "OOM: cover buffer (%u bytes)", (unsigned)needed); @@ -1143,7 +1138,7 @@ void HomeActivity::renderCarouselFrameToCurrentBuffer(int bookIdx, BookReadingSt static_cast(menuItems.size()), -1, [&menuItems](int index) { return menuItems[index].label; }, [&menuItems](int index) { return menuItems[index].icon; }); - const auto labels = mappedInput.mapLabels("", tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); + const auto labels = mappedInput.mapLabels(tr(STR_READ), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); if (outStats) *outStats = frameStats; @@ -1433,21 +1428,7 @@ void HomeActivity::loop() { minimalMenuIndex = menuCount - 1; } - buttonNavigator.onPreviousPress([this, menuCount] { - minimalMenuIndex = ButtonNavigator::previousIndex(minimalMenuIndex, menuCount); - requestUpdate(); - }); - buttonNavigator.onNextPress([this, menuCount] { - minimalMenuIndex = ButtonNavigator::nextIndex(minimalMenuIndex, menuCount); - requestUpdate(); - }); - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - minimalMenuOpen = false; - minimalHomeNavIndex = -1; - requestUpdate(); - return; - } - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + auto activateMinimalMenuAction = [this, &menuItems]() { switch (menuItems[minimalMenuIndex].action) { case HomeMenuAction::BrowseFiles: onFileBrowserOpen(); @@ -1471,10 +1452,72 @@ void HomeActivity::loop() { case HomeMenuAction::Settings: break; } + }; + + int touchedMenuIndex = -1; + if (mappedInput.wasItemTouchedDown(touchedMenuIndex) && touchedMenuIndex >= 0 && touchedMenuIndex < menuCount) { + if (minimalMenuIndex != touchedMenuIndex) { + minimalMenuIndex = touchedMenuIndex; + requestUpdate(); + } + return; + } + if (mappedInput.wasItemTapped(touchedMenuIndex) && touchedMenuIndex >= 0 && touchedMenuIndex < menuCount) { + minimalMenuIndex = touchedMenuIndex; + activateMinimalMenuAction(); + return; + } + + int touchX = 0; + int touchY = 0; + if (mappedInput.wasScreenTouchDown(touchX, touchY) && + !containsPoint(MinimalTheme::buttonMenuPanelRect(renderer, menuCount), touchX, touchY)) { + minimalMenuOpen = false; + minimalHomeNavIndex = -1; + requestUpdate(); + return; + } + + buttonNavigator.onPreviousPress([this, menuCount] { + minimalMenuIndex = ButtonNavigator::previousIndex(minimalMenuIndex, menuCount); + requestUpdate(); + }); + buttonNavigator.onNextPress([this, menuCount] { + minimalMenuIndex = ButtonNavigator::nextIndex(minimalMenuIndex, menuCount); + requestUpdate(); + }); + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + minimalMenuOpen = false; + minimalHomeNavIndex = -1; + requestUpdate(); + return; + } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + activateMinimalMenuAction(); } return; } + switch (mappedInput.wasSwipe()) { + case MappedInputManager::SwipeDir::Down: + minimalHomeNavIndex = 2; + onSettingsOpen(); + return; + case MappedInputManager::SwipeDir::Right: + minimalHomeNavIndex = 1; + onFileBrowserOpen(); + return; + case MappedInputManager::SwipeDir::Up: + minimalHomeNavIndex = 0; + minimalMenuOpen = true; + minimalMenuIndex = 0; + requestUpdate(); + return; + case MappedInputManager::SwipeDir::Left: + case MappedInputManager::SwipeDir::None: + break; + } + if (canSwapHomeBook() && mappedInput.isFrontButtonPressed(HalGPIO::BTN_BACK) && mappedInput.getHeldTime() >= HOME_BOOK_SWAP_LONG_PRESS_MS) { homeBookSwapLongPressHandled = true; @@ -1518,6 +1561,33 @@ void HomeActivity::loop() { } }; + int touchedHomeNav = -1; + if (mappedInput.wasItemTouchedDown(touchedHomeNav) && touchedHomeNav >= 0 && touchedHomeNav < homeNavCount) { + if (minimalHomeNavIndex != touchedHomeNav) { + minimalHomeNavIndex = touchedHomeNav; + requestUpdate(); + } + return; + } + int touchedBookIndex = -1; + if (mappedInput.wasCoverTouchedDown(touchedBookIndex) && touchedBookIndex >= 0 && !recentBooks.empty()) { + if (minimalHomeNavIndex != 3) { + minimalHomeNavIndex = 3; + requestUpdate(); + } + return; + } + if (mappedInput.wasItemTapped(touchedHomeNav) && touchedHomeNav >= 0 && touchedHomeNav < homeNavCount) { + minimalHomeNavIndex = touchedHomeNav; + activateMinimalHomeNav(minimalHomeNavIndex); + return; + } + if (mappedInput.wasCoverTapped(touchedBookIndex) && touchedBookIndex >= 0 && !recentBooks.empty()) { + minimalHomeNavIndex = 3; + onContinueReading(); + return; + } + if (releasedFrontButton == HalGPIO::BTN_BACK) { minimalHomeNavIndex = 0; activateMinimalHomeNav(minimalHomeNavIndex); @@ -1541,19 +1611,76 @@ void HomeActivity::loop() { return; } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (minimalHomeNavIndex >= 0) { - activateMinimalHomeNav(minimalHomeNavIndex); - } + onContinueReading(); return; } return; } + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + backPressSeen = true; + } + + // Minimal and Dashboard already returned through their dedicated home + // interaction path above. On other themes, Back opens the most recent book. + // Requiring a press observed on Home ignores the stale release that can + // arrive after Back closed the previous activity. + if (mappedInput.wasReleased(MappedInputManager::Button::Back) && backPressSeen && !recentBooks.empty()) { + onContinueReading(); + return; + } + const bool isCarousel = static_cast(SETTINGS.uiTheme) == CrossPointSettings::UI_THEME::LYRA_CAROUSEL; const int previousHighlightedBookIdx = getHighlightedBookIndex(); const int visibleBookCount = getVisibleRecentBookCount(); + auto activateHomeMenuAction = [this](const HomeMenuAction action) { + switch (action) { + case HomeMenuAction::BrowseFiles: + onFileBrowserOpen(); + break; + case HomeMenuAction::ContinueReading: + onContinueReading(); + break; + case HomeMenuAction::RecentBooks: + onRecentsOpen(); + break; + case HomeMenuAction::OpdsBrowser: + onOpdsBrowserOpen(); + break; + case HomeMenuAction::ReadingStats: + onReadingStatsOpen(); + break; + case HomeMenuAction::Bookmarks: + onSavedItemsOpen(); + break; + case HomeMenuAction::FileTransfer: + onFileTransferOpen(); + break; + case HomeMenuAction::Settings: + onSettingsOpen(); + break; + } + }; + + auto activateSelectedHomeItem = [this, visibleBookCount, &activateHomeMenuAction]() { + const auto& metrics = UITheme::getInstance().getMetrics(); + if (!metrics.homeContinueReadingInMenu && selectorIndex < visibleBookCount) { + onSelectBook(recentBooks[selectorIndex].path); + return; + } + + auto menuItems = buildSelectableHomeMenuItems(hasOpdsServers, hasReadingStats, hasBookmarks, hasClippings, + metrics.homeContinueReadingInMenu && !recentBooks.empty()); + const int menuSelectedIndex = selectorIndex - getHomeMenuSelectionOffset(recentBooks); + if (menuSelectedIndex < 0 || menuSelectedIndex >= static_cast(menuItems.size())) { + return; + } + + activateHomeMenuAction(menuItems[menuSelectedIndex].action); + }; + if (homeBookSwapLongPressHandled) { if (!mappedInput.isPressed(MappedInputManager::Button::Confirm)) { homeBookSwapLongPressHandled = false; @@ -1575,19 +1702,95 @@ void HomeActivity::loop() { const bool inCarouselRow = (selectorIndex < bookCount); const int menuIdx = inCarouselRow ? 0 : (selectorIndex - bookCount); - if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { - if (inCarouselRow && bookCount > 0) + auto handleTouch = [&](const bool activate) { + int touchedMenuIndex = -1; + if (activate ? mappedInput.wasItemTapped(touchedMenuIndex) : mappedInput.wasItemTouchedDown(touchedMenuIndex)) { + if (touchedMenuIndex < 0 || touchedMenuIndex >= menuItemCount) return false; + const int previousSelectorIndex = selectorIndex; + selectorIndex = bookCount + touchedMenuIndex; + if (selectorIndex != previousSelectorIndex) { + invalidateCoverCache(); + } + if (activate) { + activateSelectedHomeItem(); + } else if (selectorIndex != previousSelectorIndex) { + requestUpdate(); + } + return true; + } + + int bookIndex = -1; + if (bookCount > 0 && + (activate ? mappedInput.wasCoverTapped(bookIndex) : mappedInput.wasCoverTouchedDown(bookIndex))) { + bookIndex = std::clamp(bookIndex, 0, bookCount - 1); + const int previousSelectorIndex = selectorIndex; + const bool wasSelectedAtTouchStart = !activate && inCarouselRow && previousSelectorIndex == bookIndex; + const bool shouldActivateBook = + activate && ((carouselCoverTouchDownIndex == bookIndex && carouselCoverTouchDownWasSelected) || + (carouselCoverTouchDownIndex < 0 && inCarouselRow && previousSelectorIndex == bookIndex)); + selectorIndex = bookIndex; + lastCarouselBookIndex = bookIndex; + if (!activate) { + carouselCoverTouchDownIndex = bookIndex; + carouselCoverTouchDownWasSelected = wasSelectedAtTouchStart; + } else { + carouselCoverTouchDownIndex = -1; + carouselCoverTouchDownWasSelected = false; + } + if (selectorIndex != previousSelectorIndex) { + invalidateCoverCache(); + } + if (shouldActivateBook) { + activateSelectedHomeItem(); + } else if (selectorIndex != previousSelectorIndex) { + requestUpdate(); + } + return true; + } + return false; + }; + + if (handleTouch(/*activate=*/false)) { + return; + } + if (handleTouch(/*activate=*/true)) { + return; + } + + auto moveRight = [&]() { + if (inCarouselRow && bookCount > 0) { selectorIndex = (selectorIndex + 1) % bookCount; - else if (!inCarouselRow) + lastCarouselBookIndex = selectorIndex; + } else if (!inCarouselRow) { selectorIndex = bookCount + (menuIdx + 1) % menuItemCount; + } requestUpdate(); - } - if (mappedInput.wasPressed(MappedInputManager::Button::Left)) { - if (inCarouselRow && bookCount > 0) + }; + auto moveLeft = [&]() { + if (inCarouselRow && bookCount > 0) { selectorIndex = (selectorIndex + bookCount - 1) % bookCount; - else if (!inCarouselRow) + lastCarouselBookIndex = selectorIndex; + } else if (!inCarouselRow) { selectorIndex = bookCount + (menuIdx + menuItemCount - 1) % menuItemCount; + } requestUpdate(); + }; + + bool handledHorizontalNav = false; + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Left) { + moveRight(); + handledHorizontalNav = true; + } else if (swipe == MappedInputManager::SwipeDir::Right) { + moveLeft(); + handledHorizontalNav = true; + } + + if (!handledHorizontalNav && mappedInput.wasPressed(MappedInputManager::Button::Right)) { + moveRight(); + } + if (!handledHorizontalNav && mappedInput.wasPressed(MappedInputManager::Button::Left)) { + moveLeft(); } if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { if (inCarouselRow) { @@ -1612,6 +1815,45 @@ void HomeActivity::loop() { requestUpdate(); } } else { + const auto& metrics = UITheme::getInstance().getMetrics(); + const auto menuItems = buildSelectableHomeMenuItems(hasOpdsServers, hasReadingStats, hasBookmarks, hasClippings, + metrics.homeContinueReadingInMenu && !recentBooks.empty()); + auto handleTouch = [&](const bool activate) { + int touchedBookIndex = -1; + if (activate ? mappedInput.wasCoverTapped(touchedBookIndex) : mappedInput.wasCoverTouchedDown(touchedBookIndex)) { + if (touchedBookIndex < 0 || touchedBookIndex >= visibleBookCount) return false; + const int previousSelectorIndex = selectorIndex; + selectorIndex = metrics.homeContinueReadingInMenu ? 0 : touchedBookIndex; + if (activate) { + activateSelectedHomeItem(); + } else if (selectorIndex != previousSelectorIndex) { + requestUpdate(); + } + return true; + } + + int touchedMenuIndex = -1; + if (activate ? mappedInput.wasItemTapped(touchedMenuIndex) : mappedInput.wasItemTouchedDown(touchedMenuIndex)) { + if (touchedMenuIndex < 0 || touchedMenuIndex >= static_cast(menuItems.size())) return false; + const int previousSelectorIndex = selectorIndex; + selectorIndex = getHomeMenuSelectionOffset(recentBooks) + touchedMenuIndex; + if (activate) { + activateSelectedHomeItem(); + } else if (selectorIndex != previousSelectorIndex) { + requestUpdate(); + } + return true; + } + return false; + }; + + if (handleTouch(/*activate=*/false)) { + return; + } + if (handleTouch(/*activate=*/true)) { + return; + } + const int menuCount = getMenuItemCount(); buttonNavigator.onNext([this, menuCount] { selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount); @@ -1628,45 +1870,7 @@ void HomeActivity::loop() { } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - const auto& metrics = UITheme::getInstance().getMetrics(); - if (!metrics.homeContinueReadingInMenu && selectorIndex < visibleBookCount) { - onSelectBook(recentBooks[selectorIndex].path); - return; - } - - auto menuItems = buildSelectableHomeMenuItems(hasOpdsServers, hasReadingStats, hasBookmarks, hasClippings, - metrics.homeContinueReadingInMenu && !recentBooks.empty()); - const int menuSelectedIndex = selectorIndex - getHomeMenuSelectionOffset(recentBooks); - if (menuSelectedIndex < 0 || menuSelectedIndex >= static_cast(menuItems.size())) { - return; - } - - switch (menuItems[menuSelectedIndex].action) { - case HomeMenuAction::BrowseFiles: - onFileBrowserOpen(); - break; - case HomeMenuAction::ContinueReading: - onContinueReading(); - break; - case HomeMenuAction::RecentBooks: - onRecentsOpen(); - break; - case HomeMenuAction::OpdsBrowser: - onOpdsBrowserOpen(); - break; - case HomeMenuAction::ReadingStats: - onReadingStatsOpen(); - break; - case HomeMenuAction::Bookmarks: - onSavedItemsOpen(); - break; - case HomeMenuAction::FileTransfer: - onFileTransferOpen(); - break; - case HomeMenuAction::Settings: - onSettingsOpen(); - break; - } + activateSelectedHomeItem(); } } @@ -1674,6 +1878,11 @@ void HomeActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); + const auto displayHomeBuffer = [this] { + const auto refreshMode = initialFullRefresh ? HalDisplay::FULL_REFRESH : HalDisplay::FAST_REFRESH; + initialFullRefresh = false; + renderer.displayBuffer(refreshMode); + }; if (usesMinimalHomeInteraction()) { renderer.clearScreen(); @@ -1686,9 +1895,11 @@ void HomeActivity::render(RenderLock&&) { static_cast(menuItems.size()), minimalMenuIndex, [&menuItems](int index) { return menuItems[index].label; }, [&menuItems](int index) { return menuItems[index].icon; }); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); + if (showMinimalHomeButtonHints(mappedInput)) { + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + } + displayHomeBuffer(); return; } @@ -1710,11 +1921,13 @@ void HomeActivity::render(RenderLock&&) { if (minimalHomeNavIndex >= homeNavCount) { minimalHomeNavIndex = homeNavCount - 1; } - MinimalTheme::setHomeButtonHintSelection(minimalHomeNavIndex); - GUI.drawButtonHints(renderer, tr(STR_MENU), tr(STR_BROWSE), tr(STR_SETTINGS_SHORT), - recentBooks.empty() ? "" : tr(STR_READ)); + if (showMinimalHomeButtonHints(mappedInput)) { + MinimalTheme::setHomeButtonHintSelection(minimalHomeNavIndex); + GUI.drawButtonHints(renderer, tr(STR_MENU), tr(STR_BROWSE), tr(STR_SETTINGS_SHORT), + recentBooks.empty() ? "" : tr(STR_READ)); + } - renderer.displayBuffer(); + displayHomeBuffer(); if (!firstRenderDone) { firstRenderDone = true; @@ -1753,8 +1966,12 @@ void HomeActivity::render(RenderLock&&) { GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, nullptr); GUI.drawCarouselBorder(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight}, recentBooks, centerIdx, inCarouselRow); + const auto menuItems = buildHomeMenuItems(hasOpdsServers, hasReadingStats, hasBookmarks, hasClippings); + if (static_cast(SETTINGS.uiTheme) == CrossPointSettings::UI_THEME::LYRA_CAROUSEL) { + static_cast(GUI).registerButtonMenuTouchTargets(renderer, + static_cast(menuItems.size())); + } if (!inCarouselRow) { - const auto menuItems = buildHomeMenuItems(hasOpdsServers, hasReadingStats, hasBookmarks, hasClippings); if (static_cast(SETTINGS.uiTheme) == CrossPointSettings::UI_THEME::LYRA_CAROUSEL) { static_cast(GUI).drawButtonMenuSelectionOverlay( @@ -1764,7 +1981,7 @@ void HomeActivity::render(RenderLock&&) { } } - renderer.displayBuffer(); + displayHomeBuffer(); // E-ink refresh complete — pre-render the missing adjacent frame while idle. updateSlidingWindowCache(centerIdx, bookCount); // Mirror the slow-path trigger: generate missing thumbnails on the second @@ -1783,6 +2000,20 @@ void HomeActivity::render(RenderLock&&) { renderer.clearScreen(); bool bufferRestored = coverBufferStored && restoreCoverBuffer(); + auto menuItems = buildSelectableHomeMenuItems(hasOpdsServers, hasReadingStats, hasBookmarks, hasClippings, + metrics.homeContinueReadingInMenu && !recentBooks.empty()); + int homeCoverTileHeight = metrics.homeCoverTileHeight; + if (SETTINGS.uiTheme == CrossPointSettings::UI_THEME::CLASSIC) { + // Keep the four always-present actions clear of the button-hint strip on + // shorter displays; any optional actions paginate below them. + const int menuRows = std::min(4, static_cast(menuItems.size())); + const int requiredMenuHeight = + metrics.verticalSpacing + menuRows * metrics.menuRowHeight + std::max(0, menuRows - 1) * metrics.menuSpacing; + const int maxCoverHeight = pageHeight - metrics.buttonHintsHeight - metrics.homeTopPadding - + metrics.homeMenuTopOffset - requiredMenuHeight; + homeCoverTileHeight = std::clamp(maxCoverHeight, 0, metrics.homeCoverTileHeight); + } + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, metrics.homeContinueReadingInMenu && !recentBooks.empty() ? recentBooks[0].title.c_str() : nullptr); @@ -1792,17 +2023,14 @@ void HomeActivity::render(RenderLock&&) { coverRectX = 0; coverRectY = metrics.homeTopPadding; coverRectW = pageWidth; - coverRectH = metrics.homeCoverTileHeight; + coverRectH = homeCoverTileHeight; - GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight}, - recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored, + GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, homeCoverTileHeight}, recentBooks, + selectorIndex, coverRendered, coverBufferStored, bufferRestored, std::bind(&HomeActivity::storeCoverBuffer, this), hasAnyBookStats(currentBookStats) ? ¤tBookStats : nullptr, currentBookProgressPercent); - auto menuItems = buildSelectableHomeMenuItems(hasOpdsServers, hasReadingStats, hasBookmarks, hasClippings, - metrics.homeContinueReadingInMenu && !recentBooks.empty()); - - const int menuStartY = metrics.homeTopPadding + metrics.homeCoverTileHeight + metrics.homeMenuTopOffset; + const int menuStartY = metrics.homeTopPadding + homeCoverTileHeight + metrics.homeMenuTopOffset; const int menuEndY = pageHeight - metrics.buttonHintsHeight; const int menuHeight = std::max(0, menuEndY - menuStartY); @@ -1814,11 +2042,13 @@ void HomeActivity::render(RenderLock&&) { const bool isCarouselTheme = static_cast(SETTINGS.uiTheme) == CrossPointSettings::UI_THEME::LYRA_CAROUSEL; - const auto labels = isCarouselTheme ? mappedInput.mapLabels("", tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)) - : mappedInput.mapLabels("", tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + const char* readLabel = recentBooks.empty() ? "" : tr(STR_READ); + const auto labels = isCarouselTheme + ? mappedInput.mapLabels(readLabel, tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)) + : mappedInput.mapLabels(readLabel, tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); + displayHomeBuffer(); if (!firstRenderDone) { firstRenderDone = true; @@ -1843,19 +2073,13 @@ void HomeActivity::render(RenderLock&&) { } void HomeActivity::renderCarouselFrame(int bookIdx, int slotIdx) { - const auto start = millis(); uint8_t* frameBuffer = renderer.getFrameBuffer(); if (!frameBuffer || !gCarouselCache.frames[slotIdx]) return; - BookReadingStats frameStats; - float frameProgressPercent = -1.0f; - bool usedCachedStats = false; - renderCarouselFrameToCurrentBuffer(bookIdx, &frameStats, &frameProgressPercent, &usedCachedStats); + renderCarouselFrameToCurrentBuffer(bookIdx, nullptr, nullptr, nullptr); memcpy(gCarouselCache.frames[slotIdx], frameBuffer, renderer.getBufferSize()); gCarouselCache.frameBookIdx[slotIdx] = bookIdx; carouselFrames[slotIdx] = gCarouselCache.frames[slotIdx]; - LOG_DBG("HOME", "carousel: renderCarouselFrame book=%d slot=%d cached=%s took %lums", bookIdx, slotIdx, - usedCachedStats ? "yes" : "no", millis() - start); } void HomeActivity::updateSlidingWindowCache(int centerIdx, int bookCount) { @@ -1877,8 +2101,13 @@ void HomeActivity::onSelectBook(const std::string& path) { void HomeActivity::onFileBrowserOpen() { activityManager.goToFileBrowser(); } void HomeActivity::onContinueReading() { - if (!recentBooks.empty()) { - onSelectBook(recentBooks[0].path); + if (recentBooks.empty()) return; + + const bool isCarousel = + static_cast(SETTINGS.uiTheme) == CrossPointSettings::UI_THEME::LYRA_CAROUSEL; + const int bookIndex = isCarousel ? getHighlightedBookIndex() : 0; + if (bookIndex >= 0 && bookIndex < static_cast(recentBooks.size())) { + onSelectBook(recentBooks[bookIndex].path); } } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index e2bb189c91..fb79d20c9e 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -25,9 +25,14 @@ class HomeActivity final : public Activity { ButtonNavigator buttonNavigator; int selectorIndex = 0; int lastCarouselBookIndex = 0; // remembered position when leaving carousel row + int carouselCoverTouchDownIndex = -1; + bool carouselCoverTouchDownWasSelected = false; bool recentsLoading = false; bool recentsLoaded = false; bool firstRenderDone = false; + // Silent restarts keep the panel's previous frame. The first Home paint must + // use a clean waveform so X4 panels do not diff against a WiFi screen. + bool initialFullRefresh = false; bool hasReadingStats = false; bool hasBookmarks = false; bool hasClippings = false; @@ -39,6 +44,9 @@ class HomeActivity final : public Activity { int minimalHomeNavIndex = -1; bool coverRendered = false; // Track if cover has been rendered once bool coverBufferStored = false; // Track if cover buffer is stored + // Home can be entered while Back is still held (e.g. leaving Settings with + // Back): ignore that stale release until a fresh press is seen here. + bool backPressSeen = false; uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image size_t coverBufferSize = 0; // Bytes allocated to coverBuffer // Logical rect last passed to drawRecentBookCover. The cover snapshot only @@ -98,18 +106,21 @@ class HomeActivity final : public Activity { int getVisibleRecentBookCount() const; bool canSwapHomeBook() const; void showNextRecentBookOnHome(); - void updateHighlightedBookContext(); + void updateHighlightedBookContext(bool allowEpubLoad = true); void loadRecentBooks(int maxBooks); void loadAllBookStats(); void loadRecentCovers(int coverHeight); public: explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - HomeMenuItem initialMenuItemValue = HomeMenuItem::NONE) - : Activity("Home", renderer, mappedInput), initialMenuItem(initialMenuItemValue) {} + HomeMenuItem initialMenuItemValue = HomeMenuItem::NONE, bool initialFullRefreshValue = false) + : Activity("Home", renderer, mappedInput), + initialFullRefresh(initialFullRefreshValue), + initialMenuItem(initialMenuItemValue) {} void onEnter() override; void onExit() override; void loop() override; void render(RenderLock&&) override; + bool isHomeActivity() const override { return true; } std::string getCurrentBookPath() const override; }; diff --git a/src/activities/home/RecentBookProgress.cpp b/src/activities/home/RecentBookProgress.cpp index 0de1238114..459b1ce374 100644 --- a/src/activities/home/RecentBookProgress.cpp +++ b/src/activities/home/RecentBookProgress.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -16,11 +17,100 @@ #include "activities/reader/EpubReaderUtils.h" namespace { +constexpr uint32_t EPUB_PERCENT_CACHE_MAGIC = 0x45505250; // "EPRP" +constexpr uint8_t EPUB_PERCENT_CACHE_VERSION = 1; +constexpr char EPUB_PERCENT_CACHE_FILE[] = "/progress_percent.bin"; constexpr uint32_t TXT_CACHE_MAGIC = 0x54585449; // "TXTI" constexpr uint8_t TXT_CACHE_VERSION = 3; float clampProgressPercent(const float progress) { return std::clamp(progress, 0.0f, 100.0f); } +std::string epubPercentCachePath(const std::string& cachePath) { return cachePath + EPUB_PERCENT_CACHE_FILE; } + +float loadCachedEpubPercentFromCachePath(const std::string& cachePath) { + if (cachePath.empty()) { + return -1.0f; + } + + FsFile file; + if (!Storage.openFileForRead("RBPR", epubPercentCachePath(cachePath), file)) { + return -1.0f; + } + + uint32_t magic = 0; + uint8_t version = 0; + uint16_t basisPoints = 0; + const bool readOk = serialization::tryReadPod(file, magic) && serialization::tryReadPod(file, version) && + serialization::tryReadPod(file, basisPoints); + file.close(); + if (!readOk || magic != EPUB_PERCENT_CACHE_MAGIC || version != EPUB_PERCENT_CACHE_VERSION || basisPoints > 10000) { + return -1.0f; + } + + return static_cast(basisPoints) / 100.0f; +} + +void saveCachedEpubPercentToCachePath(const std::string& cachePath, const float progress) { + if (cachePath.empty() || progress < 0.0f) { + return; + } + + const float clamped = clampProgressPercent(progress); + const uint16_t basisPoints = static_cast((clamped * 100.0f) + 0.5f); + + FsFile file; + if (!Storage.openFileForWrite("RBPR", epubPercentCachePath(cachePath), file)) { + LOG_ERR("RBPR", "failed to open EPUB percent cache for write: %s", cachePath.c_str()); + return; + } + + const bool writeOk = serialization::tryWritePod(file, EPUB_PERCENT_CACHE_MAGIC) && + serialization::tryWritePod(file, EPUB_PERCENT_CACHE_VERSION) && + serialization::tryWritePod(file, basisPoints) && file.sync(); + file.close(); + if (!writeOk) { + LOG_ERR("RBPR", "failed to write EPUB percent cache: %s", cachePath.c_str()); + } +} + +float loadEpubSizeProgressPercentFromCachePath(const std::string& cachePath) { + EpubReaderUtils::Progress progress; + if (!EpubReaderUtils::readProgressFile("RBPR", cachePath + "/progress.bin", progress) && + !EpubReaderUtils::readProgressFile("RBPR", cachePath + "/progress.bin.bak", progress)) { + return -1.0f; + } + if (!progress.hasPageCount || progress.pageCount <= 0) { + return -1.0f; + } + + float progressPercent = -1.0f; + { + BookMetadataCache metadata(cachePath); + if (!metadata.load()) { + return -1.0f; + } + if (progress.spineIndex < 0 || progress.spineIndex >= metadata.getSpineCount()) { + return -1.0f; + } + + const size_t bookSize = metadata.getSpineCumulativeSize(metadata.getSpineCount() - 1); + if (bookSize == 0) { + return -1.0f; + } + + const size_t prevChapterSize = + progress.spineIndex >= 1 ? metadata.getSpineCumulativeSize(progress.spineIndex - 1) : 0; + const size_t currentChapterSize = metadata.getSpineCumulativeSize(progress.spineIndex) - prevChapterSize; + const float chapterProgress = + std::clamp(static_cast(progress.pageNumber + 1) / static_cast(progress.pageCount), 0.0f, 1.0f); + const float totalProgress = + static_cast(prevChapterSize) + (static_cast(currentChapterSize) * chapterProgress); + progressPercent = clampProgressPercent((totalProgress / static_cast(bookSize)) * 100.0f); + } + saveCachedEpubPercentToCachePath(cachePath, progressPercent); + return progressPercent; +} + float loadEpubProgressPercent(const RecentBook& book) { Epub epub(book.path, "/.crosspoint"); if (!epub.load(false, true)) { @@ -37,7 +127,10 @@ float loadEpubProgressPercent(const RecentBook& book) { } const float chapterProgress = static_cast(progress.pageNumber + 1) / static_cast(progress.pageCount); - return clampProgressPercent(epub.calculateProgress(progress.spineIndex, chapterProgress) * 100.0f); + const float progressPercent = + clampProgressPercent(epub.calculateProgress(progress.spineIndex, chapterProgress) * 100.0f); + saveCachedEpubPercentToCachePath(epub.getCachePath(), progressPercent); + return progressPercent; } float loadXtcProgressPercent(const RecentBook& book) { @@ -136,6 +229,33 @@ float RecentBookProgress::loadPercent(const RecentBook& book) { return -1.0f; } +float RecentBookProgress::loadCachedEpubPercent(const RecentBook& book) { + if (!FsHelpers::hasEpubExtension(book.path)) { + return -1.0f; + } + const std::string cachePath = Epub::cachePathForFilePath(book.path, "/.crosspoint"); + const float cachedProgress = loadCachedEpubPercentFromCachePath(cachePath); + if (cachedProgress >= 0.0f) { + return cachedProgress; + } + return loadEpubSizeProgressPercentFromCachePath(cachePath); +} + +void RecentBookProgress::saveCachedEpubPercent(const std::string& cachePath, const float progress) { + saveCachedEpubPercentToCachePath(cachePath, progress); +} + +void RecentBookProgress::saveCachedEpubPercent(const Epub& epub, const int spineIndex, const int currentPage, + const int pageCount) { + if (pageCount <= 0) { + return; + } + + const float chapterProgress = static_cast(currentPage + 1) / static_cast(pageCount); + const float progressPercent = clampProgressPercent(epub.calculateProgress(spineIndex, chapterProgress) * 100.0f); + saveCachedEpubPercentToCachePath(epub.getCachePath(), progressPercent); +} + bool RecentBookProgress::hasPercent(const float progress) { return progress >= 0.0f; } std::string RecentBookProgress::formatPercent(const float progress) { diff --git a/src/activities/home/RecentBookProgress.h b/src/activities/home/RecentBookProgress.h index c87c7fe671..d133773bc9 100644 --- a/src/activities/home/RecentBookProgress.h +++ b/src/activities/home/RecentBookProgress.h @@ -2,6 +2,7 @@ #include +class Epub; struct RecentBook; // Helpers for loading and displaying recent-book reading progress. @@ -14,6 +15,13 @@ namespace RecentBookProgress { // Loads the saved reading percentage for a recent EPUB, XTC, TXT, or Markdown book. // Returns -1.0f when progress is unavailable or the cache cannot be read. float loadPercent(const RecentBook& book); +// Loads the cached EPUB reading percentage without opening the EPUB. +// Returns -1.0f when progress is unavailable or the cache cannot be read. +float loadCachedEpubPercent(const RecentBook& book); +// Saves a cached EPUB reading percentage for fast Home screen rendering. +void saveCachedEpubPercent(const std::string& cachePath, float progress); +// Calculates and saves cached EPUB progress from a reader position. +void saveCachedEpubPercent(const Epub& epub, int spineIndex, int currentPage, int pageCount); // Returns true when progress contains a known 0-100 percentage. bool hasPercent(float progress); // Formats progress as a rounded percentage string such as "42%". diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index c8d53f9427..f9fc207d79 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -16,15 +16,27 @@ #include "activities/util/ConfirmationActivity.h" #include "activities/util/OptionSelectionActivity.h" #include "components/CompactHeader.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" +namespace fui = freeink::ui; + namespace { constexpr size_t MAX_LIST_RECENT_BOOKS = 10; // Hold threshold for the long-press action menu (firmware convention). constexpr unsigned long LONG_PRESS_MS = 1000; +constexpr unsigned long ACTION_FEEDBACK_MS = 1000; +constexpr fui::ActionId ACTION_ROW = 1; } // namespace +RecentBooksActivity::RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("RecentBooks", renderer, mappedInput), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + void RecentBooksActivity::loadRecentBooks() { recentBooks.clear(); const auto& books = RECENT_BOOKS.getBooks(); @@ -41,6 +53,16 @@ void RecentBooksActivity::loadRecentBooks() { } } +void RecentBooksActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(self->recentBooks.size())) return; + self->selectorIndex = static_cast(event.value); + // Opening the book leaves this screen; a lingering flash would gray an + // unrelated row when the list next appears. + self->app.clearTapFlash(); + self->onSelectBook(self->recentBooks[self->selectorIndex].path); +} + void RecentBooksActivity::onEnter() { Activity::onEnter(); @@ -54,6 +76,12 @@ void RecentBooksActivity::onEnter() { loadRecentBooks(); selectorIndex = 0; + uiReady = false; + visibleRows = 1; + topIndex = 0; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &RecentBooksActivity::onRowEvent, this); + app.setScreen(&RecentBooksActivity::listScreen, this); requestUpdate(); } @@ -63,12 +91,17 @@ void RecentBooksActivity::onExit() { } void RecentBooksActivity::loop() { - const auto& metrics = UITheme::getInstance().getMetrics(); - const int contentTop = CompactHeader::contentTop(metrics); - const int contentHeight = - renderer.getScreenHeight() - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; - const int pageItems = std::max(1, contentHeight / metrics.listWithSubtitleRowHeight); + if (pendingCacheDeletedFeedback && millis() - cacheDeletedFeedbackShowTime >= ACTION_FEEDBACK_MS) { + pendingCacheDeletedFeedback = false; + requestUpdate(); + return; + } + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + onGoHome(); + return; + } + const int listSize = static_cast(recentBooks.size()); // After a long-press has fired, swallow input until Confirm is physically released // (so the release doesn't also open the book; re-arm only once the button is up). if (longPressFired) { @@ -88,9 +121,39 @@ void RecentBooksActivity::loop() { return; } + int touchX = 0; + int touchY = 0; + if (mappedInput.isScreenTouchLongPress(touchX, touchY, LONG_PRESS_MS) && listRowStep > 0 && touchY >= listTop && + touchY < listBottom) { + const int rowOffset = touchY - listTop; + const int row = rowOffset / listRowStep; + const int heldIndex = topIndex + row; + if (rowOffset % listRowStep >= listRowHeight || row >= visibleRows || heldIndex < 0 || heldIndex >= listSize) { + return; + } + selectorIndex = static_cast(heldIndex); + mappedInput.suppressNextTouchTap(); + longPressFired = true; + showBookActionMenu(selectorIndex, true); + return; + } + + // Touch goes through the FreeInkApp: render() registered the row hit rects; + // route the snapshot and let onRowEvent dispatch. + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + // No pressed-state repaint: the render it triggers would drop a slow + // tap's release inside the uiReady window (tap-to-activate needed two + // taps), and it costs a second e-ink refresh per tap. + if (app.invalidated()) requestUpdate(); + if (event) return; // dispatched to onRowEvent + } + } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (!recentBooks.empty() && selectorIndex < static_cast(recentBooks.size())) { - LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str()); + if (!recentBooks.empty() && selectorIndex < recentBooks.size()) { onSelectBook(recentBooks[selectorIndex].path); return; } @@ -98,28 +161,38 @@ void RecentBooksActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoHome(); + return; } - int listSize = static_cast(recentBooks.size()); + // Swipes scroll the viewport; the selection stays put and button navigation + // pulls the view back to it. + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, listSize); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } - buttonNavigator.onNextRelease([this, listSize] { - selectorIndex = ButtonNavigator::nextIndex(static_cast(selectorIndex), listSize); + const auto moveSelection = [this, listSize](const int index) { + selectorIndex = static_cast(index); + topIndex = followListSelection(static_cast(selectorIndex), topIndex, visibleRows, listSize); requestUpdate(); + }; + buttonNavigator.onNextRelease([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::nextIndex(static_cast(selectorIndex), listSize)); }); - - buttonNavigator.onPreviousRelease([this, listSize] { - selectorIndex = ButtonNavigator::previousIndex(static_cast(selectorIndex), listSize); - requestUpdate(); + buttonNavigator.onPreviousRelease([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::previousIndex(static_cast(selectorIndex), listSize)); }); - - buttonNavigator.onNextContinuous([this, listSize, pageItems] { - selectorIndex = ButtonNavigator::nextPageIndex(static_cast(selectorIndex), listSize, pageItems); - requestUpdate(); + buttonNavigator.onNextContinuous([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::nextPageIndex(static_cast(selectorIndex), listSize, visibleRows)); }); - - buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { - selectorIndex = ButtonNavigator::previousPageIndex(static_cast(selectorIndex), listSize, pageItems); - requestUpdate(); + buttonNavigator.onPreviousContinuous([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::previousPageIndex(static_cast(selectorIndex), listSize, visibleRows)); }); } @@ -130,6 +203,8 @@ void RecentBooksActivity::reloadAfterBookAction() { } else if (selectorIndex >= recentBooks.size()) { selectorIndex = recentBooks.size() - 1; } + topIndex = + followListSelection(static_cast(selectorIndex), topIndex, visibleRows, static_cast(recentBooks.size())); requestUpdate(true); } @@ -137,11 +212,9 @@ void RecentBooksActivity::promptDeleteBook(const RecentBook& book) { const std::string path = book.path; auto handler = [this, path](const ActivityResult& res) { if (res.isCancelled) { - LOG_DBG("RBA", "Delete cancelled"); return; } - LOG_DBG("RBA", "Attempting to delete: %s", path.c_str()); BookActions::clearFileMetadata(path); if (!Storage.remove(path.c_str())) { LOG_ERR("RBA", "Failed to delete file: %s", path.c_str()); @@ -160,11 +233,9 @@ void RecentBooksActivity::promptDeleteBook(const RecentBook& book) { void RecentBooksActivity::promptRemoveBook(const std::string& path, const std::string& title) { auto handler = [this, path](const ActivityResult& res) { if (res.isCancelled) { - LOG_DBG("RBA", "Remove from recents cancelled"); return; } if (RECENT_BOOKS.removeByPath(path)) { - LOG_DBG("RBA", "Removed from recents: %s", path.c_str()); reloadAfterBookAction(); } }; @@ -181,11 +252,15 @@ void RecentBooksActivity::showBookActionMenu(const size_t bookIndex, const bool const RecentBook book = recentBooks[bookIndex]; std::vector items = BookActions::buildBookActionItems(book.path, /*includeRemoveFromRecents=*/true); + if (BookActions::canSendNearby(book.path)) { + items.push_back({FileBrowserAction::SendNearby, StrId::STR_SEND_NEARBY_BOOK}); + } startActivityForResult( std::make_unique(renderer, mappedInput, book.title, std::move(items), ignoreInitialConfirmRelease), [this, book](const ActivityResult& result) { + longPressFired = false; if (result.isCancelled) { return; } @@ -209,8 +284,8 @@ void RecentBooksActivity::showBookActionMenu(const size_t bookIndex, const bool if (!BookActions::clearBookCache(book.path)) { LOG_ERR("RBA", "Failed to clear book cache for: %s", book.path.c_str()); } else { - BookActions::drawToast(renderer, tr(STR_BOOK_CACHE_DELETED)); - delay(1000); + pendingCacheDeletedFeedback = true; + cacheDeletedFeedbackShowTime = millis(); } } reloadAfterBookAction(); @@ -281,6 +356,9 @@ void RecentBooksActivity::showBookActionMenu(const size_t bookIndex, const bool case FileBrowserAction::RemoveFromRecents: promptRemoveBook(book.path, book.title); return; + case FileBrowserAction::SendNearby: + activityManager.goToNearbyBookSend(book.path, false); + return; case FileBrowserAction::PinFavorite: case FileBrowserAction::UnpinFavorite: case FileBrowserAction::SetSleepFolder: @@ -294,31 +372,81 @@ void RecentBooksActivity::showBookActionMenu(const size_t bookIndex, const bool }); } +void RecentBooksActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} + +void RecentBooksActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + // Content below the GUI.drawHeader band, above the button hints. + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + screen.spacer(static_cast(metrics.verticalSpacing)); + + if (recentBooks.empty()) { + screen.centeredText(tr(STR_NO_RECENT_BOOKS), screen.theme().bodyText); + return; + } + + // Transient per-render: points into the recentBooks strings. + std::vector items; + items.reserve(recentBooks.size()); + for (const auto& book : recentBooks) { + fui::ListItem item; + item.label = book.title.c_str(); + if (!book.author.empty()) item.subtitle = book.author.c_str(); + item.icon = listIconFor(UITheme::getFileIcon(book.path), 32); // subtitle rows carry the larger icon + item.actionValue = static_cast(items.size()); + items.push_back(item); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectorIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; // physical buttons stay in loop() + props.iconSize = 28; + props.labelText = screen.theme().bodyText; + props.labelText.bold = true; + const fui::Rect listBounds = screen.body(); + listTop = listBounds.y; + listBottom = listBounds.bottom(); + const auto rows = configureUiList(props, screen.theme(), listBounds, UiListRowType::WithSubtitle); + listRowHeight = props.rowHeight; + listRowStep = listRowHeight + props.rowGap; + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(recentBooks.size())); // clamp to range + props.topIndex = static_cast(topIndex); + screen.list(props); +} + void RecentBooksActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); const auto& metrics = UITheme::getInstance().getMetrics(); - CompactHeader::drawTitle(renderer, tr(STR_MENU_RECENT_BOOKS)); - - const int contentTop = CompactHeader::contentTop(metrics); - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; - - // Recent tab - if (recentBooks.empty()) { - renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_RECENT_BOOKS)); + // Header via GUI.drawHeader (already FreeInkUI-themed) for the battery + // indicator; the rest of the screen renders through the app. + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_MENU_RECENT_BOOKS), false); } else { - GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, recentBooks.size(), selectorIndex, - [this](int index) { return recentBooks[index].title; }, [this](int index) { return recentBooks[index].author; }, - [this](int index) { return UITheme::getFileIcon(recentBooks[index].path); }); + GUI.drawHeader(renderer, header, tr(STR_MENU_RECENT_BOOKS)); } - // Help text + uiReady = false; + app.render(); + uiReady = true; + const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + if (pendingCacheDeletedFeedback) { + GUI.drawPopup(renderer, tr(STR_BOOK_CACHE_DELETED)); + } + renderer.displayBuffer(); } diff --git a/src/activities/home/RecentBooksActivity.h b/src/activities/home/RecentBooksActivity.h index 36ff306c80..cf1e769cd1 100644 --- a/src/activities/home/RecentBooksActivity.h +++ b/src/activities/home/RecentBooksActivity.h @@ -1,6 +1,9 @@ #pragma once +#include +#include #include +#include #include #include #include @@ -11,6 +14,10 @@ class RecentBooksActivity final : public Activity { private: + // FreeInkApp hosts the book list (themed rows, icons, touch routing); the + // header stays on GUI.drawHeader for the battery indicator. + using UiApp = freeink::ui::FreeInkApp<20, 4>; + ButtonNavigator buttonNavigator; size_t selectorIndex = 0; @@ -18,10 +25,28 @@ class RecentBooksActivity final : public Activity { // Set when a long-press has fired; input is swallowed until Confirm is released // again so the release doesn't also open the book. bool longPressFired = false; + bool pendingCacheDeletedFeedback = false; + unsigned long cacheDeletedFeedbackShowTime = 0UL; // Recent tab state std::vector recentBooks; + freeink::ui::GfxRendererTarget uiTarget; // must precede `app`: the app holds a reference to it + UiApp app; + // render() rebuilds the app's interaction table; loop() only routes touch + // snapshots against it while this is true (the two run on different tasks). + std::atomic uiReady{false}; + int visibleRows = 1; // rows per page at the current scale; set by the screen builder + int topIndex = 0; // viewport scroll position, decoupled from the selection + int listTop = 0; + int listBottom = 0; + int listRowHeight = 0; + int listRowStep = 0; + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); + // Data loading void loadRecentBooks(); void reloadAfterBookAction(); @@ -32,8 +57,7 @@ class RecentBooksActivity final : public Activity { void showBookActionMenu(size_t bookIndex, bool ignoreInitialConfirmRelease = false); public: - explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("RecentBooks", renderer, mappedInput) {} + explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput); void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/home/RecentBooksGridActivity.cpp b/src/activities/home/RecentBooksGridActivity.cpp index cb9d987414..29059c6e90 100644 --- a/src/activities/home/RecentBooksGridActivity.cpp +++ b/src/activities/home/RecentBooksGridActivity.cpp @@ -24,14 +24,21 @@ #include "activities/util/ConfirmationActivity.h" #include "activities/util/OptionSelectionActivity.h" #include "components/CompactHeader.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" -#include "components/icons/book.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" namespace { constexpr int kCoverCornerRadius = 2; constexpr int kGridColumns = 3; +constexpr int kTitleStripHeight = 32; +constexpr int kTitleGridGap = 8; +constexpr int kSelectionPadding = 4; +constexpr int kSelectionOutlineGap = 2; +constexpr int kSelectionOuterInset = kSelectionPadding + kSelectionOutlineGap; constexpr unsigned long kLongPressMs = 1000; +constexpr unsigned long kActionFeedbackMs = 1000; constexpr float kCircleRadians = 6.2831853f; constexpr float kCircleRadiansPerPercent = kCircleRadians / 100.0f; @@ -129,12 +136,18 @@ int moveVerticalInGrid(const int currentIndex, const int totalItems, const int c return std::max(previousPageStart, previousPageCandidate); } -void updateRecentBookCoverPath(const RecentBook& book, const std::string& coverBmpPath) { - if (!RECENT_BOOKS.updateBook(book.path, book.title, book.author, coverBmpPath)) { +void updateRecentBookCover(const RecentBook& book) { + if (!RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.coverBmpPath, book.coverState)) { LOG_ERR("RBGA", "failed to update recent book metadata: %s", book.path.c_str()); } } +void markCoverMissing(RecentBook& book) { + book.coverBmpPath.clear(); + book.coverState = RecentBook::CoverState::Missing; + updateRecentBookCover(book); +} + bool hasThumbnailPlaceholder(const std::string& coverBmpPath) { return coverBmpPath.find("[WIDTH]") != std::string::npos || coverBmpPath.find("[HEIGHT]") != std::string::npos; } @@ -187,7 +200,7 @@ std::string getReusableCoverPath(const RecentBook& book) { } void ensureReusableCoverPath(RecentBook& book) { - if (hasThumbnailPlaceholder(book.coverBmpPath)) { + if (book.coverState == RecentBook::CoverState::Missing || hasThumbnailPlaceholder(book.coverBmpPath)) { return; } @@ -197,7 +210,7 @@ void ensureReusableCoverPath(RecentBook& book) { } book.coverBmpPath = reusablePath; - updateRecentBookCoverPath(book, reusablePath); + updateRecentBookCover(book); } } // namespace @@ -231,8 +244,7 @@ void RecentBooksGridActivity::loadPageCovers(int pageStart) { RecentBook& book = recentBooks[i].book; ensureReusableCoverPath(book); if (book.coverBmpPath.empty()) { - needsGeneration = true; - break; + continue; } const std::string thumbPath = UITheme::getCoverThumbPath(book.coverBmpPath, COVER_WIDTH, COVER_HEIGHT); if (needsCoverThumbGeneration(book, thumbPath)) { @@ -252,12 +264,15 @@ void RecentBooksGridActivity::loadPageCovers(int pageStart) { for (int i = pageStart; i < pageEnd; ++i) { RecentBook& book = recentBooks[i].book; - const std::string coverPath = - book.coverBmpPath.empty() ? "" : UITheme::getCoverThumbPath(book.coverBmpPath, COVER_WIDTH, COVER_HEIGHT); + if (book.coverBmpPath.empty()) { + processedCount++; + continue; + } + const std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, COVER_WIDTH, COVER_HEIGHT); if (needsCoverThumbGeneration(book, coverPath)) { if (FsHelpers::hasEpubExtension(book.path)) { Epub epub(book.path, "/.crosspoint"); - if (epub.load(true, true)) { + if (epub.load(true, true, Epub::XLocationLoadMode::Skip)) { if (!showingLoading) { showingLoading = true; popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); @@ -266,10 +281,9 @@ void RecentBooksGridActivity::loadPageCovers(int pageStart) { if (epub.generateThumbBmp(COVER_WIDTH, COVER_HEIGHT, &renderer, SETTINGS.getReaderFontId())) { const std::string reusablePath = epub.getThumbBmpPath(); book.coverBmpPath = reusablePath; - updateRecentBookCoverPath(book, reusablePath); - } else { - updateRecentBookCoverPath(book, ""); - book.coverBmpPath = ""; + updateRecentBookCover(book); + } else if (!epub.hasCoverImage()) { + markCoverMissing(book); } } } else if (FsHelpers::hasXtcExtension(book.path)) { @@ -283,10 +297,7 @@ void RecentBooksGridActivity::loadPageCovers(int pageStart) { if (xtc.generateThumbBmp(COVER_WIDTH, COVER_HEIGHT)) { const std::string reusablePath = xtc.getThumbBmpPath(); book.coverBmpPath = reusablePath; - updateRecentBookCoverPath(book, reusablePath); - } else { - updateRecentBookCoverPath(book, ""); - book.coverBmpPath = ""; + updateRecentBookCover(book); } } } @@ -314,7 +325,50 @@ void RecentBooksGridActivity::onExit() { recentBooks.clear(); } +int RecentBooksGridActivity::bookIndexFromPoint(const int x, const int y) { + if (recentBooks.empty()) return -1; + + const auto pageWidth = renderer.getScreenWidth(); + const auto& metrics = UITheme::getInstance().getMetrics(); + const int contentTop = CompactHeader::contentTop(metrics); + const int gridSpacing = metrics.verticalSpacing; + const int rowSpacing = gridSpacing + 4; + const int totalGridWidth = kGridColumns * COVER_WIDTH + (kGridColumns - 1) * gridSpacing; + const int startXOffset = (pageWidth - totalGridWidth) / 2; + const int totalBooks = static_cast(recentBooks.size()); + const int safeSelector = std::clamp(selectorIndex, 0, totalBooks - 1); + const int currentPage = (safeSelector / BOOKS_PER_PAGE); + const int pageStart = currentPage * BOOKS_PER_PAGE; + const int pageCount = std::min(BOOKS_PER_PAGE, totalBooks - pageStart); + + for (int i = 0; i < pageCount; ++i) { + const int col = i % kGridColumns; + const int row = i / kGridColumns; + const int coverX = startXOffset + col * (COVER_WIDTH + gridSpacing); + const int coverY = contentTop + kTitleStripHeight + kTitleGridGap + row * (COVER_HEIGHT + rowSpacing); + const int hitX = coverX - kSelectionOuterInset; + const int hitY = coverY - kSelectionOuterInset; + const int hitWidth = COVER_WIDTH + kSelectionOuterInset * 2; + const int hitHeight = COVER_HEIGHT + kSelectionOuterInset * 2; + if (x >= hitX && x < hitX + hitWidth && y >= hitY && y < hitY + hitHeight) { + return pageStart + i; + } + } + + return -1; +} + void RecentBooksGridActivity::loop() { + if (pendingCacheDeletedFeedback && millis() - cacheDeletedFeedbackShowTime >= kActionFeedbackMs) { + pendingCacheDeletedFeedback = false; + requestUpdate(); + return; + } + + if (TouchHeaderBackButton::wasTapped(mappedInput, TouchHeaderBackButton::compactHeaderRect(renderer))) { + onGoHome(); + return; + } if (longPressFired) { if (!mappedInput.isPressed(MappedInputManager::Button::Confirm)) { longPressFired = false; @@ -329,9 +383,22 @@ void RecentBooksGridActivity::loop() { return; } + int touchX = 0; + int touchY = 0; + if (mappedInput.isScreenTouchLongPress(touchX, touchY, kLongPressMs)) { + const int touchedIndex = bookIndexFromPoint(touchX, touchY); + if (touchedIndex >= 0) { + selectorIndex = touchedIndex; + ensureProgressLoaded(selectorIndex); + mappedInput.suppressNextTouchTap(); + longPressFired = true; + showBookActionMenu(selectorIndex, true); + return; + } + } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (!recentBooks.empty() && selectorIndex >= 0 && selectorIndex < static_cast(recentBooks.size())) { - LOG_DBG("RBGA", "Selected recent book: %s", recentBooks[selectorIndex].book.path.c_str()); onSelectBook(recentBooks[selectorIndex].book.path); return; } @@ -362,6 +429,31 @@ void RecentBooksGridActivity::loop() { ensureProgressLoaded(selectorIndex); requestUpdate(); }; + auto handlePageSwipe = [this, listSize](const bool nextPage) { + selectorIndex = nextPage ? ButtonNavigator::nextPageIndex(selectorIndex, listSize, BOOKS_PER_PAGE) + : ButtonNavigator::previousPageIndex(selectorIndex, listSize, BOOKS_PER_PAGE); + ensureProgressLoaded(selectorIndex); + requestUpdate(); + }; + + const auto swipe = mappedInput.wasSwipe(); + if (swipe != MappedInputManager::SwipeDir::None) { + if (listSize > BOOKS_PER_PAGE) { + const bool nextPage = swipe == MappedInputManager::SwipeDir::Left || swipe == MappedInputManager::SwipeDir::Up; + handlePageSwipe(nextPage); + } + return; + } + + if (mappedInput.wasScreenTapped(touchX, touchY)) { + const int touchedIndex = bookIndexFromPoint(touchX, touchY); + if (touchedIndex >= 0) { + selectorIndex = touchedIndex; + ensureProgressLoaded(selectorIndex); + onSelectBook(recentBooks[selectorIndex].book.path); + return; + } + } buttonNavigator.onRelease({MappedInputManager::Button::Right}, [&] { handleNav(NavDirection::Right); }); buttonNavigator.onRelease({MappedInputManager::Button::Left}, [&] { handleNav(NavDirection::Left); }); @@ -390,11 +482,9 @@ void RecentBooksGridActivity::promptDeleteBook(const RecentBook& book) { const std::string path = book.path; auto handler = [this, path](const ActivityResult& res) { if (res.isCancelled) { - LOG_DBG("RBGA", "Delete cancelled"); return; } - LOG_DBG("RBGA", "Attempting to delete: %s", path.c_str()); BookActions::clearFileMetadata(path); if (!Storage.remove(path.c_str())) { LOG_ERR("RBGA", "Failed to delete file: %s", path.c_str()); @@ -413,11 +503,9 @@ void RecentBooksGridActivity::promptDeleteBook(const RecentBook& book) { void RecentBooksGridActivity::promptRemoveBook(const std::string& path, const std::string& title) { auto handler = [this, path](const ActivityResult& res) { if (res.isCancelled) { - LOG_DBG("RBGA", "Remove from recents cancelled"); return; } if (RECENT_BOOKS.removeByPath(path)) { - LOG_DBG("RBGA", "Removed from recents: %s", path.c_str()); reloadAfterBookAction(); } }; @@ -433,11 +521,15 @@ void RecentBooksGridActivity::showBookActionMenu(const int bookIndex, const bool const RecentBook book = recentBooks[bookIndex].book; std::vector items = BookActions::buildBookActionItems(book.path, /*includeRemoveFromRecents=*/true); + if (BookActions::canSendNearby(book.path)) { + items.push_back({FileBrowserAction::SendNearby, StrId::STR_SEND_NEARBY_BOOK}); + } startActivityForResult( std::make_unique(renderer, mappedInput, book.title, std::move(items), ignoreInitialConfirmRelease), [this, book](const ActivityResult& result) { + longPressFired = false; if (result.isCancelled) { return; } @@ -461,8 +553,8 @@ void RecentBooksGridActivity::showBookActionMenu(const int bookIndex, const bool if (!BookActions::clearBookCache(book.path)) { LOG_ERR("RBGA", "Failed to clear book cache for: %s", book.path.c_str()); } else { - BookActions::drawToast(renderer, tr(STR_BOOK_CACHE_DELETED)); - delay(1000); + pendingCacheDeletedFeedback = true; + cacheDeletedFeedbackShowTime = millis(); } } reloadAfterBookAction(); @@ -533,6 +625,9 @@ void RecentBooksGridActivity::showBookActionMenu(const int bookIndex, const bool case FileBrowserAction::RemoveFromRecents: promptRemoveBook(book.path, book.title); return; + case FileBrowserAction::SendNearby: + activityManager.goToNearbyBookSend(book.path, false); + return; case FileBrowserAction::PinFavorite: case FileBrowserAction::UnpinFavorite: case FileBrowserAction::SetSleepFolder: @@ -553,13 +648,12 @@ void RecentBooksGridActivity::render(RenderLock&&) { const auto pageHeight = renderer.getScreenHeight(); const auto& metrics = UITheme::getInstance().getMetrics(); - CompactHeader::drawTitle(renderer, tr(STR_MENU_RECENT_BOOKS)); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::drawCompact(renderer, tr(STR_MENU_RECENT_BOOKS)); + } else { + CompactHeader::drawTitle(renderer, tr(STR_MENU_RECENT_BOOKS)); + } const int contentTop = CompactHeader::contentTop(metrics); - constexpr int titleStripHeight = 32; - constexpr int titleGridGap = 8; - constexpr int selectionPadding = 4; - constexpr int selectionOutlineGap = 2; - constexpr int selectionOuterInset = selectionPadding + selectionOutlineGap; const int gridSpacing = metrics.verticalSpacing; const int rowSpacing = gridSpacing + 4; const int totalGridWidth = kGridColumns * COVER_WIDTH + (kGridColumns - 1) * gridSpacing; @@ -580,7 +674,7 @@ void RecentBooksGridActivity::render(RenderLock&&) { // grid top, rather than only within titleStripHeight, so it stays vertically // centered after the grid was nudged up. const int headerBottomY = CompactHeader::headerBottomY(metrics); - const int gridTopY = contentTop + titleStripHeight + titleGridGap; + const int gridTopY = contentTop + kTitleStripHeight + kTitleGridGap; const int titleY = headerBottomY + (gridTopY - headerBottomY - titleLh) / 2; const auto& selectedBook = recentBooks[selectorIndex]; const bool hasProgress = selectedBook.progressLoaded && RecentBookProgress::hasPercent(selectedBook.progress); @@ -617,7 +711,7 @@ void RecentBooksGridActivity::render(RenderLock&&) { const int col = i % kGridColumns; const int row = i / kGridColumns; const int x = startXOffset + col * (COVER_WIDTH + gridSpacing); - const int y = contentTop + titleStripHeight + titleGridGap + row * (COVER_HEIGHT + rowSpacing); + const int y = contentTop + kTitleStripHeight + kTitleGridGap + row * (COVER_HEIGHT + rowSpacing); const int bx = x; const int by = y; @@ -648,13 +742,13 @@ void RecentBooksGridActivity::render(RenderLock&&) { if (!drawn) { renderer.fillRoundedRect(bx, by, bw, bh, kCoverCornerRadius, Color::White); renderer.drawRoundedRect(bx, by, bw, bh, 2, kCoverCornerRadius, true); - renderer.drawIcon(BookIcon, bx + (bw - 32) / 2, by + (bh - 32) / 2, 32, 32); + drawLucideIcon(renderer, icon_book_marked_32, bx + (bw - 32) / 2, by + (bh - 32) / 2); } if (bookIdx == static_cast(selectorIndex)) { - renderer.drawRoundedRect(bx - selectionPadding, by - selectionPadding, bw + selectionPadding * 2, - bh + selectionPadding * 2, 3, kCoverCornerRadius + selectionPadding, true); - renderer.drawRoundedRect(bx - selectionOuterInset, by - selectionOuterInset, bw + selectionOuterInset * 2, - bh + selectionOuterInset * 2, 1, kCoverCornerRadius + selectionOuterInset, true); + renderer.drawRoundedRect(bx - kSelectionPadding, by - kSelectionPadding, bw + kSelectionPadding * 2, + bh + kSelectionPadding * 2, 3, kCoverCornerRadius + kSelectionPadding, true); + renderer.drawRoundedRect(bx - kSelectionOuterInset, by - kSelectionOuterInset, bw + kSelectionOuterInset * 2, + bh + kSelectionOuterInset * 2, 1, kCoverCornerRadius + kSelectionOuterInset, true); } } @@ -680,6 +774,10 @@ void RecentBooksGridActivity::render(RenderLock&&) { const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + if (pendingCacheDeletedFeedback) { + GUI.drawPopup(renderer, tr(STR_BOOK_CACHE_DELETED)); + } + renderer.displayBuffer(); if (!recentBooks.empty() && loadedPageStart != pageStart) { diff --git a/src/activities/home/RecentBooksGridActivity.h b/src/activities/home/RecentBooksGridActivity.h index 75dbd0a731..72a3cdd65b 100644 --- a/src/activities/home/RecentBooksGridActivity.h +++ b/src/activities/home/RecentBooksGridActivity.h @@ -31,6 +31,8 @@ class RecentBooksGridActivity final : public Activity { ButtonNavigator buttonNavigator; int selectorIndex = 0; bool longPressFired = false; + bool pendingCacheDeletedFeedback = false; + unsigned long cacheDeletedFeedbackShowTime = 0UL; std::vector recentBooks; int loadedPageStart = NO_PAGE_LOADED; @@ -41,4 +43,5 @@ class RecentBooksGridActivity final : public Activity { void promptDeleteBook(const RecentBook& book); void promptRemoveBook(const std::string& path, const std::string& title); void showBookActionMenu(int bookIndex, bool ignoreInitialConfirmRelease = false); + int bookIndexFromPoint(int x, int y); }; diff --git a/src/activities/home/SavedItemsHomeActivity.cpp b/src/activities/home/SavedItemsHomeActivity.cpp index 84dc5f027f..725a39a17f 100644 --- a/src/activities/home/SavedItemsHomeActivity.cpp +++ b/src/activities/home/SavedItemsHomeActivity.cpp @@ -3,22 +3,30 @@ #include #include #include +#include #include #include "../reader/EpubReaderBookmarkListActivity.h" #include "../reader/EpubReaderClippingListActivity.h" +#include "BookActions.h" #include "BookmarkStore.h" #include "ClippingStore.h" #include "CrossPointState.h" #include "FileBrowserActionActivity.h" #include "MappedInputManager.h" +#include "activities/util/ConfirmationActivity.h" #include "components/CompactHeader.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" -#include "fontIds.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" + +namespace fui = freeink::ui; namespace { constexpr unsigned long SAVED_ITEM_DELETE_HOLD_MS = 1000; +constexpr fui::ActionId ACTION_ROW = 1; void mergeBookmarkEntry(std::vector& out, const BookmarkedBookEntry& entry) { auto it = std::find_if(out.begin(), out.end(), [&](const SavedBookEntry& existing) { @@ -49,6 +57,11 @@ void mergeClippingEntry(std::vector& out, const ClippedBookEntry } } // namespace +SavedItemsHomeActivity::SavedItemsHomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("SavedItemsHome", renderer, mappedInput), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + void SavedItemsHomeActivity::reloadSavedBooks() { books.clear(); @@ -76,6 +89,12 @@ void SavedItemsHomeActivity::onEnter() { Activity::onEnter(); reloadSavedBooks(); selectedIndex = 0; + topIndex = 0; + visibleRows = 1; + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &SavedItemsHomeActivity::onRowEvent, this); + app.setScreen(&SavedItemsHomeActivity::listScreen, this); requestUpdate(); } @@ -85,6 +104,10 @@ void SavedItemsHomeActivity::onExit() { } void SavedItemsHomeActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, TouchHeaderBackButton::compactHeaderRect(renderer))) { + onGoHome(); + return; + } if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoHome(); return; @@ -108,51 +131,121 @@ void SavedItemsHomeActivity::loop() { return; } - const auto& metrics = UITheme::getInstance().getMetrics(); - const int contentTop = CompactHeader::contentTop(metrics); - const int contentHeight = - renderer.getScreenHeight() - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; - const int pageItems = std::max(1, contentHeight / metrics.listWithSubtitleRowHeight); const int listSize = static_cast(books.size()); + if (listSize == 0) return; + + int tx = 0; + int ty = 0; + if (mappedInput.isScreenTouchLongPress(tx, ty, SAVED_ITEM_DELETE_HOLD_MS) && listRowStep > 0 && ty >= listTop && + ty < listBottom) { + const int offset = ty - listTop; + const int row = offset / listRowStep; + const int touchedIndex = topIndex + row; + if (row >= visibleRows || offset % listRowStep >= listRowHeight || touchedIndex >= listSize) return; + selectedIndex = touchedIndex; + mappedInput.suppressNextTouchTap(); + longPressOpenHandled = true; + showSavedBookActionMenu(selectedIndex, true); + return; + } + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } - buttonNavigator.onNextRelease([this, listSize] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, listSize); - requestUpdate(); - }); - buttonNavigator.onPreviousRelease([this, listSize] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, listSize); - requestUpdate(); - }); - buttonNavigator.onNextContinuous([this, listSize, pageItems] { - selectedIndex = ButtonNavigator::nextPageIndex(selectedIndex, listSize, pageItems); + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int next = scrollListBy(topIndex, swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows, + visibleRows, listSize); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + + const auto moveSelection = [this, listSize](const int next) { + selectedIndex = next; + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, listSize); requestUpdate(); + }; + buttonNavigator.onNextRelease( + [this, listSize, &moveSelection] { moveSelection(ButtonNavigator::nextIndex(selectedIndex, listSize)); }); + buttonNavigator.onPreviousRelease( + [this, listSize, &moveSelection] { moveSelection(ButtonNavigator::previousIndex(selectedIndex, listSize)); }); + buttonNavigator.onNextContinuous([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::nextPageIndex(selectedIndex, listSize, visibleRows)); }); - buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { - selectedIndex = ButtonNavigator::previousPageIndex(selectedIndex, listSize, pageItems); - requestUpdate(); + buttonNavigator.onPreviousContinuous([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::previousPageIndex(selectedIndex, listSize, visibleRows)); }); } -void SavedItemsHomeActivity::render(RenderLock&&) { - renderer.clearScreen(); +void SavedItemsHomeActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(self->books.size())) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->openSavedItems(self->selectedIndex); +} - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - const auto& metrics = UITheme::getInstance().getMetrics(); +void SavedItemsHomeActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} - CompactHeader::drawTitle(renderer, tr(STR_BOOKMARKS_AND_CLIPPINGS)); +void SavedItemsHomeActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + screen.setContentMargin(fui::Insets{static_cast(CompactHeader::contentTop(metrics)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + screen.spacer(static_cast(metrics.verticalSpacing)); + if (books.empty()) { + screen.centeredText(tr(STR_NO_BOOKMARKS), screen.theme().bodyText); + return; + } + std::vector items; + items.reserve(books.size()); + for (size_t i = 0; i < books.size(); ++i) { + fui::ListItem item; + item.label = books[i].bookTitle.c_str(); + if (!books[i].bookAuthor.empty()) item.subtitle = books[i].bookAuthor.c_str(); + item.actionValue = static_cast(i); + items.push_back(item); + } + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.labelText = screen.theme().bodyText; + props.labelText.bold = true; + const fui::Rect bounds = screen.body(); + listTop = bounds.y; + listBottom = bounds.bottom(); + const auto rows = configureUiList(props, screen.theme(), bounds, UiListRowType::WithSubtitle); + listRowHeight = props.rowHeight; + listRowStep = props.rowHeight + props.rowGap; + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(books.size())); + props.topIndex = static_cast(topIndex); + screen.list(props); +} - const int contentTop = CompactHeader::contentTop(metrics); - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; +void SavedItemsHomeActivity::render(RenderLock&&) { + renderer.clearScreen(); - if (books.empty()) { - renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_BOOKMARKS)); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::drawCompact(renderer, tr(STR_BOOKMARKS_AND_CLIPPINGS)); } else { - GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, books.size(), selectedIndex, - [this](int index) { return books[index].bookTitle; }, [this](int index) { return books[index].bookAuthor; }, - nullptr); + CompactHeader::drawTitle(renderer, tr(STR_BOOKMARKS_AND_CLIPPINGS)); } + uiReady = false; + app.render(); + uiReady = true; const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); @@ -227,10 +320,26 @@ void SavedItemsHomeActivity::showSavedBookActionMenu(const int bookIndex, const const auto* actionResult = std::get_if(&result.data); if (!result.isCancelled && actionResult) { switch (static_cast(actionResult->action)) { - case FileBrowserAction::DeleteBookmarks: - BOOKMARKS.loadForBook(entry.bookPath, entry.bookTitle, entry.bookAuthor, entry.bookType); - BOOKMARKS.clearAll(); - break; + case FileBrowserAction::DeleteBookmarks: { + auto confirmation = makeUniqueNoThrow( + renderer, mappedInput, BookActions::confirmationHeading(StrId::STR_DELETE_BOOKMARKS), + entry.bookTitle); + if (!confirmation) { + LOG_ERR("SVA", "OOM: bookmark clear ConfirmationActivity"); + reloadSavedBooks(); + requestUpdate(); + return; + } + startActivityForResult(std::move(confirmation), [this, entry](const ActivityResult& confirmation) { + if (!confirmation.isCancelled) { + BOOKMARKS.loadForBook(entry.bookPath, entry.bookTitle, entry.bookAuthor, entry.bookType); + BOOKMARKS.clearAll(); + } + reloadSavedBooks(); + requestUpdate(); + }); + return; + } case FileBrowserAction::DeleteClippings: CLIPPINGS.loadForBook(entry.bookPath, entry.bookTitle, entry.bookAuthor, entry.bookType); CLIPPINGS.clearAll(); @@ -273,26 +382,26 @@ void SavedItemsHomeActivity::openBookmarkList(const SavedBookEntry& entry) { void SavedItemsHomeActivity::openClippingList(const SavedBookEntry& entry) { CLIPPINGS.loadForBook(entry.bookPath, entry.bookTitle, entry.bookAuthor, entry.bookType); - startActivityForResult( - std::make_unique(renderer, mappedInput, CLIPPINGS.getClippings()), - [this, entry](const ActivityResult& result) { - if (!result.isCancelled) { - const auto* clipping = std::get_if(&result.data); - if (clipping) { - APP_STATE.pendingBookmarkSpine = clipping->spineIndex; - APP_STATE.pendingBookmarkProgress = - clipping->pageCount > 0 ? static_cast(clipping->page) / clipping->pageCount : 0.0f; - APP_STATE.pendingBookmarkParagraphIndex = clipping->paragraphIndex; - APP_STATE.pendingClippingIndex = clipping->clippingIndex; - APP_STATE.saveToFile(); - onSelectBook(entry.bookPath); - } else { - LOG_ERR("SVA", "openClippingList: unexpected result variant"); - requestUpdate(); - } - } else { - reloadSavedBooks(); - requestUpdate(); - } - }); + startActivityForResult(std::make_unique(renderer, mappedInput), + [this, entry](const ActivityResult& result) { + if (!result.isCancelled) { + const auto* clipping = std::get_if(&result.data); + if (clipping) { + APP_STATE.pendingBookmarkSpine = clipping->spineIndex; + APP_STATE.pendingBookmarkProgress = + clipping->pageCount > 0 ? static_cast(clipping->page) / clipping->pageCount + : 0.0f; + APP_STATE.pendingBookmarkParagraphIndex = clipping->paragraphIndex; + APP_STATE.pendingClippingIndex = clipping->clippingIndex; + APP_STATE.saveToFile(); + onSelectBook(entry.bookPath); + } else { + LOG_ERR("SVA", "openClippingList: unexpected result variant"); + requestUpdate(); + } + } else { + reloadSavedBooks(); + requestUpdate(); + } + }); } diff --git a/src/activities/home/SavedItemsHomeActivity.h b/src/activities/home/SavedItemsHomeActivity.h index e1364ee44f..a317735e26 100644 --- a/src/activities/home/SavedItemsHomeActivity.h +++ b/src/activities/home/SavedItemsHomeActivity.h @@ -1,5 +1,9 @@ #pragma once +#include +#include + +#include #include #include @@ -17,8 +21,7 @@ struct SavedBookEntry { class SavedItemsHomeActivity final : public Activity { public: - explicit SavedItemsHomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("SavedItemsHome", renderer, mappedInput) {} + SavedItemsHomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput); void onEnter() override; void onExit() override; @@ -30,6 +33,20 @@ class SavedItemsHomeActivity final : public Activity { ButtonNavigator buttonNavigator; int selectedIndex = 0; bool longPressOpenHandled = false; + using UiApp = freeink::ui::FreeInkApp<20, 4>; + freeink::ui::GfxRendererTarget uiTarget; + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + int listTop = 0; + int listBottom = 0; + int listRowHeight = 0; + int listRowStep = 0; + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); void reloadSavedBooks(); void openSavedItems(int bookIndex); diff --git a/src/activities/network/CalibreConnectActivity.cpp b/src/activities/network/CalibreConnectActivity.cpp index b29bf6abf6..baab5894e8 100644 --- a/src/activities/network/CalibreConnectActivity.cpp +++ b/src/activities/network/CalibreConnectActivity.cpp @@ -60,7 +60,11 @@ void CalibreConnectActivity::onExit() { if (WiFi.getMode() != WIFI_MODE_NULL) { WiFi.disconnect(false); delay(30); - silentRestart(); + if (returnToReader) { + silentRestartToReader(); + } else { + silentRestart(); + } } } @@ -231,5 +235,5 @@ void CalibreConnectActivity::render(RenderLock&&) { const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); } diff --git a/src/activities/network/CalibreConnectActivity.h b/src/activities/network/CalibreConnectActivity.h index a8afd4af3e..ac9b19d4f7 100644 --- a/src/activities/network/CalibreConnectActivity.h +++ b/src/activities/network/CalibreConnectActivity.h @@ -5,6 +5,7 @@ #include #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" #include "network/CrossPointWebServer.h" enum class CalibreConnectState { WIFI_SELECTION, SERVER_STARTING, SERVER_RUNNING, ERROR }; @@ -15,6 +16,7 @@ enum class CalibreConnectState { WIFI_SELECTION, SERVER_STARTING, SERVER_RUNNING */ class CalibreConnectActivity final : public Activity { CalibreConnectState state = CalibreConnectState::WIFI_SELECTION; + ScreenTransitionRefresh screenTransitionRefresh; std::unique_ptr webServer; std::string connectedIP; @@ -27,6 +29,7 @@ class CalibreConnectActivity final : public Activity { unsigned long lastCompleteAt = 0; unsigned long lastProcessedCompleteAt = 0; // Track which server value we've already processed bool exitRequested = false; + bool returnToReader = false; void renderServerRunning() const; @@ -35,8 +38,8 @@ class CalibreConnectActivity final : public Activity { void stopWebServer(); public: - explicit CalibreConnectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("CalibreConnect", renderer, mappedInput) {} + explicit CalibreConnectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool returnToReader = false) + : Activity("CalibreConnect", renderer, mappedInput), returnToReader(returnToReader) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index e4f1c742b7..60d6dbd235 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -4,8 +4,8 @@ #include #include #include +#include #include -#include #include @@ -17,6 +17,7 @@ #include "activities/ActivityManager.h" #include "activities/network/CalibreConnectActivity.h" #include "components/CompactHeader.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" #include "util/QrUtils.h" @@ -65,7 +66,7 @@ int barsForRssi(int rssi, int currentBars) { void CrossPointWebServerActivity::onEnter() { Activity::onEnter(); - sdFontSystem.releaseLoadedFont(renderer); + sdFontSystem.releaseForNetwork(renderer); LOG_DBG("WEBACT", "Free heap at onEnter: %d bytes", ESP.getFreeHeap()); @@ -84,7 +85,6 @@ void CrossPointWebServerActivity::onEnter() { } // Launch network mode selection subactivity - LOG_DBG("WEBACT", "Launching NetworkModeSelectionActivity..."); startActivityForResult(std::make_unique(renderer, mappedInput), [this](const ActivityResult& result) { if (result.isCancelled) { @@ -98,9 +98,21 @@ void CrossPointWebServerActivity::onEnter() { void CrossPointWebServerActivity::onExit() { Activity::onExit(); - LOG_DBG("WEBACT", "Free heap at onExit start: %d bytes", ESP.getFreeHeap()); - state = WebServerActivityState::SHUTTING_DOWN; + + // Every active WiFi exit already reboots to clear network heap + // fragmentation. Restart before graceful socket teardown: a stalled browser + // can otherwise keep WebSocketsServer::close() retrying writes for seconds. + // silentRestart() returns only when deep sleep is already in progress; that + // path still needs the explicit cleanup below. + if (WiFi.getMode() != WIFI_MODE_NULL) { + if (returnBookPath.empty()) { + silentRestart(); + } else { + silentRestartToReader(); + } + } + stopDnsServer(); MDNS.end(); @@ -108,14 +120,14 @@ void CrossPointWebServerActivity::onExit() { stopWebServer(); MDNS.end(); if (dnsServer) { - LOG_DBG("WEBACT", "Stopping DNS server..."); dnsServer->stop(); delete dnsServer; dnsServer = nullptr; } delay(50); - // Skip reboot if WiFi was never activated (e.g. user backed out of mode selection). + // On the deep-sleep path silentRestart() returns without rebooting, so shut + // WiFi down after local services have released their sockets. if (WiFi.getMode() != WIFI_MODE_NULL) { if (isApMode) { WiFi.softAPdisconnect(true); @@ -123,7 +135,6 @@ void CrossPointWebServerActivity::onExit() { WiFi.disconnect(false); } delay(30); - silentRestart(); } LOG_DBG("WEBACT", "Free heap at onExit end: %d bytes", ESP.getFreeHeap()); @@ -136,7 +147,9 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) } else if (mode == NetworkMode::CREATE_HOTSPOT) { modeName = "Create Hotspot"; } else if (mode == NetworkMode::NEARBY_STATS_SYNC) { - modeName = "Nearby Stats Sync"; + modeName = "Sync Stats"; + } else if (mode == NetworkMode::NEARBY_BOOK_RECEIVE) { + modeName = "Receive File"; } LOG_DBG("WEBACT", "Network mode selected: %s", modeName); @@ -147,31 +160,64 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) activityManager.goToNearbyStatsSync(); return; } + if (mode == NetworkMode::NEARBY_BOOK_RECEIVE) { + activityManager.goToNearbyBookReceive(); + return; + } + + if (!networkBootReady) { + switch (mode) { + case NetworkMode::JOIN_NETWORK: + activityManager.goToJoinNetworkFileTransfer(returnBookPath); + break; + case NetworkMode::CONNECT_CALIBRE: + activityManager.goToCalibreWireless(returnBookPath); + break; + case NetworkMode::CREATE_HOTSPOT: + activityManager.goToHotspotFileTransfer(returnBookPath); + break; + case NetworkMode::NEARBY_STATS_SYNC: + case NetworkMode::NEARBY_BOOK_RECEIVE: + break; + } + return; + } if (mode == NetworkMode::CONNECT_CALIBRE) { - startActivityForResult( - std::make_unique(renderer, mappedInput), [this](const ActivityResult& result) { - state = WebServerActivityState::MODE_SELECTION; - - startActivityForResult(std::make_unique(renderer, mappedInput), - [this](const ActivityResult& result) { - if (result.isCancelled) { - exitToOrigin(); - } else { - onNetworkModeSelected(std::get(result.data).mode); - } - }); - }); + // The child activity must survive this callback; allocate only its small control object on the heap. + auto calibreActivity = makeUniqueNoThrow(renderer, mappedInput, !returnBookPath.empty()); + if (!calibreActivity) { + LOG_ERR("WEBACT", "OOM: Calibre activity (size=%u free=%u maxAlloc=%u)", + static_cast(sizeof(CalibreConnectActivity)), ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + exitToOrigin(); + return; + } + + startActivityForResult(std::move(calibreActivity), [this](const ActivityResult& result) { + state = WebServerActivityState::MODE_SELECTION; + + if (networkBootReady) { + exitToOrigin(); + return; + } + + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { + if (result.isCancelled) { + exitToOrigin(); + } else { + onNetworkModeSelected(std::get(result.data).mode); + } + }); + }); return; } if (mode == NetworkMode::JOIN_NETWORK) { // STA mode - launch WiFi selection - LOG_DBG("WEBACT", "Turning on WiFi (STA mode)..."); WiFi.mode(WIFI_STA); state = WebServerActivityState::WIFI_SELECTION; - LOG_DBG("WEBACT", "Launching WifiSelectionActivity..."); startActivityForResult(std::make_unique(renderer, mappedInput), [this](const ActivityResult& result) { if (!result.isCancelled) { @@ -190,8 +236,6 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) } void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) { - LOG_DBG("WEBACT", "WifiSelectionActivity completed, connected=%d", connected); - if (connected) { // Get connection info before exiting subactivity isApMode = false; @@ -217,7 +261,6 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) } void CrossPointWebServerActivity::startAccessPoint() { - LOG_DBG("WEBACT", "Starting Access Point mode..."); LOG_DBG("WEBACT", "Free heap before AP start: %d bytes", ESP.getFreeHeap()); // Configure and start the AP @@ -248,10 +291,6 @@ void CrossPointWebServerActivity::startAccessPoint() { connectedIP = ipStr; connectedSSID = AP_SSID; - LOG_DBG("WEBACT", "Access Point started!"); - LOG_DBG("WEBACT", "SSID: %s", AP_SSID); - LOG_DBG("WEBACT", "IP: %s", connectedIP.c_str()); - // Start mDNS for hostname resolution restartMdns(AP_HOSTNAME, "WEBACT"); @@ -261,7 +300,6 @@ void CrossPointWebServerActivity::startAccessPoint() { dnsServer = new DNSServer(); dnsServer->setErrorReplyCode(DNSReplyCode::NoError); dnsServer->start(DNS_PORT, "*", apIP); - LOG_DBG("WEBACT", "DNS server started for captive portal"); LOG_DBG("WEBACT", "Free heap after AP start: %d bytes", ESP.getFreeHeap()); @@ -270,15 +308,12 @@ void CrossPointWebServerActivity::startAccessPoint() { } void CrossPointWebServerActivity::startWebServer() { - LOG_DBG("WEBACT", "Starting web server..."); - // Create the web server instance webServer.reset(new CrossPointWebServer()); webServer->begin(); if (webServer->isRunning()) { state = WebServerActivityState::SERVER_RUNNING; - LOG_DBG("WEBACT", "Web server started successfully"); lastWifiBars = isApMode ? 0 : barsForRssi(WiFi.RSSI(), 0); // Force an immediate render since we're transitioning from a subactivity @@ -293,6 +328,15 @@ void CrossPointWebServerActivity::startWebServer() { } void CrossPointWebServerActivity::exitToOrigin() { + if (networkBootReady) { + if (returnBookPath.empty()) { + silentRestart(); + } else { + silentRestartToReader(); + } + return; + } + if (returnBookPath.empty()) { onGoHome(); return; @@ -303,14 +347,18 @@ void CrossPointWebServerActivity::exitToOrigin() { void CrossPointWebServerActivity::stopWebServer() { if (webServer && webServer->isRunning()) { - LOG_DBG("WEBACT", "Stopping web server..."); webServer->stop(); - LOG_DBG("WEBACT", "Web server stopped"); } webServer.reset(); } void CrossPointWebServerActivity::loop() { + if ((state == WebServerActivityState::SERVER_RUNNING || state == WebServerActivityState::AP_STARTING) && + exitRequested()) { + exitToOrigin(); + return; + } + // Handle different states if (state == WebServerActivityState::SERVER_RUNNING) { // Handle DNS requests for captive portal (AP mode only) @@ -363,7 +411,7 @@ void CrossPointWebServerActivity::loop() { } } - // Handle web server requests - maximize throughput with watchdog safety + // Handle web server requests while keeping input responsive. if (webServer && webServer->isRunning()) { const unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime; @@ -372,26 +420,20 @@ void CrossPointWebServerActivity::loop() { LOG_DBG("WEBACT", "WARNING: %lu ms gap since last handleClient", timeSinceLastHandleClient); } - // Reset watchdog BEFORE processing - HTTP header parsing can be slow - esp_task_wdt_reset(); - // Process HTTP requests in tight loop for maximum throughput // More iterations = more data processed per main loop cycle constexpr int MAX_ITERATIONS = 500; for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) { webServer->handleClient(); - // Reset watchdog every 32 iterations - if ((i & 0x1F) == 0x1F) { - esp_task_wdt_reset(); - } // Yield and check for exit button every 64 iterations if ((i & 0x3F) == 0x3F) { yield(); // Force trigger an update of which buttons are being pressed so be have accurate state // for back button checking mappedInput.update(); - // Check for exit button inside loop for responsiveness - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + // This local update can consume one-shot exit events before the + // ActivityManager sees them, so honor every exit route here. + if (exitRequested()) { exitToOrigin(); return; } @@ -399,12 +441,6 @@ void CrossPointWebServerActivity::loop() { } lastHandleClientTime = millis(); } - - // Handle exit on Back button (also check outside loop) - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - exitToOrigin(); - return; - } } } @@ -413,30 +449,39 @@ void CrossPointWebServerActivity::render(RenderLock&&) { // Subactivities handle their own rendering if (state == WebServerActivityState::SERVER_RUNNING || state == WebServerActivityState::AP_STARTING) { renderer.clearScreen(); - const auto& metrics = UITheme::getInstance().getMetrics(); - const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); - CompactHeader::drawTitle(renderer, isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER)); - if (state == WebServerActivityState::SERVER_RUNNING) { - GUI.drawSubHeader(renderer, Rect{0, CompactHeader::contentTop(metrics), pageWidth, metrics.tabBarHeight}, - connectedSSID.c_str()); renderServerRunning(); } else { + renderHeader(); const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height) / 2; renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_STARTING_HOTSPOT)); } - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); } } +void CrossPointWebServerActivity::renderHeader() const { + const char* title = isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::drawCompact(renderer, title); + } else { + CompactHeader::drawTitle(renderer, title); + } +} + +bool CrossPointWebServerActivity::exitRequested() const { + return TouchHeaderBackButton::wasTapped(mappedInput, renderer) || + mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasHomeGesture(); +} + void CrossPointWebServerActivity::renderServerRunning() const { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); - CompactHeader::drawTitle(renderer, isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER)); + renderHeader(); const int subHeaderTop = CompactHeader::contentTop(metrics); GUI.drawSubHeader(renderer, Rect{0, subHeaderTop, pageWidth, metrics.tabBarHeight}, connectedSSID.c_str()); diff --git a/src/activities/network/CrossPointWebServerActivity.h b/src/activities/network/CrossPointWebServerActivity.h index 253f7d92e4..e0c46fb0c0 100644 --- a/src/activities/network/CrossPointWebServerActivity.h +++ b/src/activities/network/CrossPointWebServerActivity.h @@ -7,6 +7,7 @@ #include "NetworkModeSelectionActivity.h" #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" #include "network/CrossPointWebServer.h" // Web server activity states @@ -33,6 +34,7 @@ class CrossPointWebServerActivity final : public Activity { std::string returnBookPath; bool hasInitialNetworkMode = false; NetworkMode initialNetworkMode = NetworkMode::JOIN_NETWORK; + bool networkBootReady = false; // Network mode NetworkMode networkMode = NetworkMode::JOIN_NETWORK; @@ -55,9 +57,12 @@ class CrossPointWebServerActivity final : public Activity { // Cached signal-strength bracket (0..4) for the WiFi indicator. int lastWifiBars = 0; + ScreenTransitionRefresh screenTransitionRefresh; void renderServerRunning() const; + void renderHeader() const; void renderWifiIndicator(int subHeaderTop) const; + bool exitRequested() const; void onNetworkModeSelected(NetworkMode mode); void onWifiSelectionComplete(bool connected); @@ -71,11 +76,12 @@ class CrossPointWebServerActivity final : public Activity { std::string returnBookPath = {}) : Activity("CrossPointWebServer", renderer, mappedInput), returnBookPath(std::move(returnBookPath)) {} CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, NetworkMode initialNetworkMode, - std::string returnBookPath = {}) + std::string returnBookPath = {}, bool networkBootReady = false) : Activity("CrossPointWebServer", renderer, mappedInput), returnBookPath(std::move(returnBookPath)), hasInitialNetworkMode(true), - initialNetworkMode(initialNetworkMode) {} + initialNetworkMode(initialNetworkMode), + networkBootReady(networkBootReady) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/network/NearbyBookTransferActivity.cpp b/src/activities/network/NearbyBookTransferActivity.cpp new file mode 100644 index 0000000000..5796994b6b --- /dev/null +++ b/src/activities/network/NearbyBookTransferActivity.cpp @@ -0,0 +1,939 @@ +#include "NearbyBookTransferActivity.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "CrossPointSettings.h" +#include "CrossPointState.h" +#include "MappedInputManager.h" +#include "SilentRestart.h" +#include "activities/home/FileBrowserActivity.h" +#include "components/TouchHeaderBackButton.h" +#include "components/UITheme.h" +#include "fontIds.h" + +#if defined(ARDUINO_ARCH_ESP32) && !defined(SIMULATOR) +#include +#endif + +namespace nearby = freeink::nearby; +namespace fui = freeink::ui; + +namespace { +constexpr const char* LOG_TAG = "NBOOK"; +constexpr uint8_t BROADCAST_MAC[nearby::MAC_BYTES] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; +constexpr uint8_t RESULT_OK = 0; +constexpr uint8_t RESULT_FAILED = 1; +constexpr uint8_t REJECT_USER = 1; +constexpr uint8_t REJECT_STORAGE = 2; +constexpr int TOUCH_ACTION_BUTTON_WIDTH = 120; +constexpr int TOUCH_ACTION_BUTTON_HEIGHT = 48; +constexpr int TOUCH_COLLISION_BUTTON_WIDTH = 280; +constexpr int TOUCH_COLLISION_BUTTON_HEIGHT = 52; +constexpr int TOUCH_COLLISION_BUTTON_GAP = 16; + +Rect touchActionButtonRect(const Rect& screen, const bool accept) { + constexpr int SIDE_MARGIN = 46; + return Rect{accept ? screen.x + screen.width - SIDE_MARGIN - TOUCH_ACTION_BUTTON_WIDTH : screen.x + SIDE_MARGIN, + screen.y + screen.height - TOUCH_ACTION_BUTTON_HEIGHT - 28, TOUCH_ACTION_BUTTON_WIDTH, + TOUCH_ACTION_BUTTON_HEIGHT}; +} + +Rect touchCollisionButtonRect(const Rect& screen, const int index) { + constexpr int SIDE_MARGIN = 32; + constexpr int BUTTON_COUNT = 3; + const int width = std::min(TOUCH_COLLISION_BUTTON_WIDTH, screen.width - SIDE_MARGIN * 2); + const int totalHeight = + TOUCH_COLLISION_BUTTON_HEIGHT * BUTTON_COUNT + TOUCH_COLLISION_BUTTON_GAP * (BUTTON_COUNT - 1); + return Rect{screen.x + (screen.width - width) / 2, + screen.y + (screen.height - totalHeight) / 2 + TOUCH_ACTION_BUTTON_HEIGHT + + index * (TOUCH_COLLISION_BUTTON_HEIGHT + TOUCH_COLLISION_BUTTON_GAP), + width, TOUCH_COLLISION_BUTTON_HEIGHT}; +} + +bool contains(const Rect& rect, const int x, const int y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} + +bool sameMac(const std::array& lhs, const uint8_t* rhs) { + return rhs && memcmp(lhs.data(), rhs, nearby::MAC_BYTES) == 0; +} +} // namespace + +NearbyBookTransferActivity::NearbyBookTransferActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const Mode mode, std::string sourcePath, + const bool returnToReader) + : Activity("NearbyBookTransfer", renderer, mappedInput), + mode_(mode), + sourcePath_(std::move(sourcePath)), + returnToReader_(returnToReader), + uiTarget_(makeUiTarget(renderer)), + app_(uiTarget_, uiTarget_.deviceContext()) {} + +NearbyBookTransferActivity::~NearbyBookTransferActivity() { stopRadio(); } + +bool NearbyBookTransferActivity::supportedFile(const std::string& path) { + return FsHelpers::hasEpubExtension(path) || FsHelpers::hasTxtExtension(path) || FsHelpers::hasXtcExtension(path) || + FsHelpers::hasPngExtension(path) || FsHelpers::hasBmpExtension(path); +} + +bool NearbyBookTransferActivity::safeFileName(const std::string& name) { + if (name.empty() || name.size() > 180 || name.front() == '.' || name.back() == ' ' || name.back() == '.') { + return false; + } + return name.find_first_of("/\\:*?\"<>|") == std::string::npos && supportedFile(name); +} + +std::string NearbyBookTransferActivity::fileNameFromPath(const std::string& path) { + const size_t slash = path.find_last_of('/'); + return slash == std::string::npos ? path : path.substr(slash + 1); +} + +std::string NearbyBookTransferActivity::joinPath(const std::string& folder, const std::string& name) { + if (folder.empty() || folder == "/") return "/" + name; + return folder.back() == '/' ? folder + name : folder + "/" + name; +} + +std::string NearbyBookTransferActivity::keepBothPath(const std::string& path) { + const size_t slash = path.find_last_of('/'); + const size_t dot = path.find_last_of('.'); + const bool hasExtension = dot != std::string::npos && (slash == std::string::npos || dot > slash); + const std::string base = hasExtension ? path.substr(0, dot) : path; + const std::string extension = hasExtension ? path.substr(dot) : ""; + for (uint16_t suffix = 2; suffix < 1000; ++suffix) { + char number[12]; + snprintf(number, sizeof(number), " (%u)", static_cast(suffix)); + const std::string candidate = base + number + extension; + if (!Storage.exists(candidate.c_str())) return candidate; + } + return {}; +} + +void NearbyBookTransferActivity::onEnter() { + Activity::onEnter(); + renderer.setOrientation(GfxRenderer::Orientation::Portrait); + destinationFolder_ = SETTINGS.nearbyReceiveFolder[0] ? SETTINGS.nearbyReceiveFolder : "/"; + uiReady_ = false; + app_.setTheme(uiThemeTokens(uiTarget_)); + app_.on(ACTION_ROW, &NearbyBookTransferActivity::onRowEvent, this); + app_.setScreen(&NearbyBookTransferActivity::menuScreen, this); + + if (mode_ == Mode::Receive) { + setState(State::ChooseReceiveAction); + return; + } + + if (!supportedFile(sourcePath_)) { + setError(tr(STR_NEARBY_TRANSFER_UNSUPPORTED)); + return; + } + HalFile probe; + if (!Storage.openFileForRead(LOG_TAG, sourcePath_, probe)) { + setError(tr(STR_NEARBY_TRANSFER_SOURCE_FAILED)); + return; + } + offeredFileSize_ = probe.fileSize64(); + probe.close(); + offeredFileName_ = fileNameFromPath(sourcePath_); + if (!safeFileName(offeredFileName_)) { + setError(tr(STR_NEARBY_TRANSFER_SOURCE_FAILED)); + return; + } + startDiscovery(); +} + +void NearbyBookTransferActivity::onExit() { + sourceFile_.close(); + receiveFile_.close(); + stopRadio(); + Activity::onExit(); +} + +bool NearbyBookTransferActivity::skipLoopDelay() { + return state_ == State::Discovering || state_ == State::WaitingForApproval || state_ == State::Sending || + state_ == State::Receiving; +} + +bool NearbyBookTransferActivity::startRadio() { + if (transport_.started()) return true; + radioUsed_ = true; + if (!transport_.begin(ESPNOW_CHANNEL)) { + setError(tr(STR_NEARBY_TRANSFER_RADIO_FAILED)); + return false; + } +#if defined(ARDUINO_ARCH_ESP32) && !defined(SIMULATOR) + LOG_INF(LOG_TAG, "radio ready: free=%u maxAlloc=%u stack=%u", ESP.getFreeHeap(), ESP.getMaxAllocHeap(), + static_cast(uxTaskGetStackHighWaterMark(nullptr))); +#endif + return true; +} + +void NearbyBookTransferActivity::stopRadio() { transport_.end(); } + +void NearbyBookTransferActivity::startListening() { + if (!startRadio()) return; + session_.reset(); + peerMac_ = {}; + offeredFileName_.clear(); + senderName_.clear(); + finalPath_.clear(); + tempPath_.clear(); + retryCount_ = 0; + setState(State::Listening); +} + +void NearbyBookTransferActivity::startDiscovery() { + if (!startRadio()) return; +#if defined(ARDUINO_ARCH_ESP32) && !defined(SIMULATOR) + sessionId_ = esp_random(); +#else + sessionId_ = static_cast(millis()) ^ 0x43494654u; +#endif + if (sessionId_ == 0) sessionId_ = 1; + peerCount_ = 0; + selectedIndex_ = 0; + lastActionMs_ = 0; + setState(State::Discovering); + sendDiscovery(); +} + +bool NearbyBookTransferActivity::sendPacket(const nearby::PacketType type, const uint8_t* destination, + const uint32_t sequence, const void* payload, + const uint16_t payloadLength) { + size_t length = 0; + if (!nearby::encodePacket(packetBuffer_.data(), packetBuffer_.size(), type, sessionId_, sequence, payload, + payloadLength, length)) { + return false; + } + return transport_.send(destination, packetBuffer_.data(), length); +} + +bool NearbyBookTransferActivity::sendDiscovery() { + lastActionMs_ = millis(); + return sendPacket(nearby::PacketType::Discover, BROADCAST_MAC); +} + +bool NearbyBookTransferActivity::sendAdvertisement(const uint8_t* destination) { + const char* name = SETTINGS.getEffectiveDeviceName(); + const uint16_t length = static_cast(std::min(strlen(name), CrossPointSettings::MAX_DEVICE_NAME_LENGTH)); + return sendPacket(nearby::PacketType::Advertise, destination, 0, name, length); +} + +void NearbyBookTransferActivity::selectPeer() { + if (peerCount_ == 0 || selectedIndex_ < 0 || selectedIndex_ >= peerCount_) return; + peerMac_ = peers_[selectedIndex_].mac; + retryCount_ = 0; + setState(State::WaitingForApproval); + sendOffer(); +} + +bool NearbyBookTransferActivity::sendOffer() { + const char* deviceName = SETTINGS.getEffectiveDeviceName(); + const size_t senderLength = std::min(strlen(deviceName), CrossPointSettings::MAX_DEVICE_NAME_LENGTH); + if (offeredFileName_.size() > 180 || senderLength == 0) return false; + std::array payload{}; + nearby::writeU64(payload.data(), offeredFileSize_); + nearby::writeU16(payload.data() + 8, nearby::V2_CHUNK_BYTES); + payload[10] = static_cast(senderLength); + payload[11] = static_cast(offeredFileName_.size()); + memcpy(payload.data() + 12, deviceName, senderLength); + memcpy(payload.data() + 12 + senderLength, offeredFileName_.data(), offeredFileName_.size()); + lastActionMs_ = millis(); + return sendPacket(nearby::PacketType::Offer, peerMac_.data(), 0, payload.data(), + static_cast(12 + senderLength + offeredFileName_.size())); +} + +void NearbyBookTransferActivity::processPackets() { + while (transport_.poll(eventBuffer_)) { + nearby::PacketView packet; + if (nearby::decodePacket(eventBuffer_.data.data(), eventBuffer_.length, packet)) handlePacket(eventBuffer_, packet); + } +} + +void NearbyBookTransferActivity::handlePacket(const nearby::EspNowTransport::Event& event, + const nearby::PacketView& packet) { + if (packet.type == nearby::PacketType::Discover && state_ == State::Listening) { + sessionId_ = packet.sessionId; + sendAdvertisement(event.sourceMac.data()); + return; + } + + if (mode_ == Mode::Send && packet.sessionId == sessionId_) { + if (packet.type == nearby::PacketType::Advertise && (state_ == State::Discovering || state_ == State::DeviceList) && + packet.payloadLength >= 2 && packet.payloadLength <= CrossPointSettings::MAX_DEVICE_NAME_LENGTH) { + for (uint8_t i = 0; i < peerCount_; ++i) { + if (sameMac(peers_[i].mac, event.sourceMac.data())) return; + } + if (peerCount_ >= MAX_PEERS) return; + Peer& peer = peers_[peerCount_++]; + peer.mac = event.sourceMac; + memcpy(peer.name.data(), packet.payload, packet.payloadLength); + peer.name[packet.payloadLength] = '\0'; + if (state_ == State::Discovering) + setState(State::DeviceList); + else + requestUpdate(); + return; + } + if (!sameMac(peerMac_, event.sourceMac.data())) return; + if (packet.type == nearby::PacketType::Accept && state_ == State::WaitingForApproval && packet.payloadLength == 2) { + negotiatedChunkBytes_ = + std::clamp(nearby::readU16(packet.payload), nearby::COMPAT_CHUNK_BYTES, nearby::V2_CHUNK_BYTES); + if (!Storage.openFileForRead(LOG_TAG, sourcePath_, sourceFile_)) { + setError(tr(STR_NEARBY_TRANSFER_SOURCE_FAILED)); + return; + } + session_.begin(nearby::ReliableTransferSession::Role::Sender, sessionId_, offeredFileSize_, + negotiatedChunkBytes_); + retryCount_ = 0; + setState(State::Sending); + if (offeredFileSize_ == 0) { + sourceFile_.close(); + sendComplete(); + } else { + sendNextChunk(); + } + return; + } + if (packet.type == nearby::PacketType::Reject && state_ == State::WaitingForApproval) { + setError(tr(STR_NEARBY_TRANSFER_REJECTED)); + return; + } + if (packet.type == nearby::PacketType::Ack && state_ == State::Sending && packet.payloadLength == 4) { + const uint32_t nextSequence = nearby::readU32(packet.payload); + if (!session_.acceptAcknowledgement(nextSequence)) return; + session_.advanceSentBytes(pendingChunkLength_); + pendingChunkLength_ = 0; + retryCount_ = 0; + maybeRefreshProgress(); + if (session_.transferredBytes() == session_.totalBytes()) { + sourceFile_.close(); + sendComplete(); + } else { + sendNextChunk(); + } + return; + } + if (packet.type == nearby::PacketType::Result && state_ == State::Sending && packet.payloadLength == 1) { + if (packet.payload[0] == RESULT_OK) { + setState(State::Success); + } else { + setError(tr(STR_NEARBY_TRANSFER_VERIFY_FAILED)); + } + return; + } + if (packet.type == nearby::PacketType::Cancel) setError(tr(STR_NEARBY_TRANSFER_CANCELLED)); + return; + } + + if (mode_ != Mode::Receive) return; + if (packet.type == nearby::PacketType::Offer && state_ == State::Listening && packet.payloadLength >= 14) { + const uint8_t senderLength = packet.payload[10]; + const uint8_t nameLength = packet.payload[11]; + if (senderLength == 0 || senderLength > CrossPointSettings::MAX_DEVICE_NAME_LENGTH || nameLength == 0 || + nameLength > 180 || packet.payloadLength != 12 + senderLength + nameLength) { + return; + } + std::string name(reinterpret_cast(packet.payload + 12 + senderLength), nameLength); + if (!safeFileName(name)) return; + sessionId_ = packet.sessionId; + peerMac_ = event.sourceMac; + offeredFileSize_ = nearby::readU64(packet.payload); + negotiatedChunkBytes_ = + std::clamp(nearby::readU16(packet.payload + 8), nearby::COMPAT_CHUNK_BYTES, nearby::V2_CHUNK_BYTES); + offeredFileName_ = std::move(name); + senderName_.assign(reinterpret_cast(packet.payload + 12), senderLength); + finalPath_ = joinPath(destinationFolder_, offeredFileName_); + setState(State::OfferPrompt); + return; + } + if (packet.sessionId != sessionId_ || !sameMac(peerMac_, event.sourceMac.data())) return; + if (packet.type == nearby::PacketType::Offer && state_ == State::Receiving) { + if (acceptPending_) return; + uint8_t payload[2]; + nearby::writeU16(payload, negotiatedChunkBytes_); + sendPacket(nearby::PacketType::Accept, peerMac_.data(), 0, payload, sizeof(payload)); + return; + } + if (packet.type == nearby::PacketType::Data && state_ == State::Receiving) { + if (packet.sequence < session_.nextSequence()) { + sendAck(); + return; + } + if (packet.sequence != session_.nextSequence() || packet.payloadLength == 0 || + packet.payloadLength > negotiatedChunkBytes_) { + sendAck(); + return; + } + const size_t written = receiveFile_.write(packet.payload, packet.payloadLength); + if (written != packet.payloadLength || + !session_.acceptReceivedChunk(packet.sequence, static_cast(packet.payloadLength))) { + const uint8_t failed = RESULT_FAILED; + sendPacket(nearby::PacketType::Result, peerMac_.data(), 0, &failed, 1); + setError(tr(STR_NEARBY_TRANSFER_WRITE_FAILED)); + return; + } + session_.includeBytes(packet.payload, packet.payloadLength); + sendAck(); + maybeRefreshProgress(); + return; + } + if (packet.type == nearby::PacketType::Complete && (state_ == State::Receiving || state_ == State::Success) && + packet.payloadLength == 12) { + const uint64_t expectedBytes = nearby::readU64(packet.payload); + const uint32_t expectedCrc = nearby::readU32(packet.payload + 8); + const bool success = state_ == State::Success || finishReceivedFile(expectedBytes, expectedCrc); + const uint8_t result = success ? RESULT_OK : RESULT_FAILED; + sendPacket(nearby::PacketType::Result, peerMac_.data(), 0, &result, 1); + if (success) setState(State::Success); + return; + } + if (packet.type == nearby::PacketType::Cancel) { + cancelTransfer(); + setError(tr(STR_NEARBY_TRANSFER_CANCELLED)); + } +} + +bool NearbyBookTransferActivity::acceptOffer(const bool keepBoth) { + receivingScreenDrawn_.store(false, std::memory_order_release); + acceptPending_ = false; + setState(State::Validating); + if (requestUpdateAndWait() != RequestUpdateResult::Rendered) { + LOG_ERR(LOG_TAG, "Validation screen could not be rendered synchronously"); + requestUpdate(true); + } + + if (keepBoth) { + finalPath_ = keepBothPath(finalPath_); + if (finalPath_.empty()) { + setError(tr(STR_NEARBY_TRANSFER_NAME_FAILED)); + return false; + } + } + uint64_t total = 0; + uint64_t used = 0; +#ifndef SIMULATOR + total = Storage.totalBytes(); + used = Storage.usedBytes(); +#endif + if (total > 0 && used <= total && offeredFileSize_ > total - used) { + const uint8_t reason = REJECT_STORAGE; + sendPacket(nearby::PacketType::Reject, peerMac_.data(), 0, &reason, 1); + setError(tr(STR_NEARBY_TRANSFER_NO_SPACE)); + return false; + } + const std::string finalName = fileNameFromPath(finalPath_); + tempPath_ = joinPath(destinationFolder_, "." + finalName + ".crossink-part"); + backupPath_ = joinPath(destinationFolder_, "." + finalName + ".crossink-backup"); + if (Storage.exists(tempPath_.c_str())) Storage.remove(tempPath_.c_str()); + if (!Storage.openFileForWrite(LOG_TAG, tempPath_, receiveFile_)) { + setError(tr(STR_NEARBY_TRANSFER_WRITE_FAILED)); + return false; + } + session_.begin(nearby::ReliableTransferSession::Role::Receiver, sessionId_, offeredFileSize_, negotiatedChunkBytes_); + retryCount_ = 0; + lastActionMs_ = millis(); + acceptPending_ = true; + setState(State::Receiving); + if (requestUpdateAndWait() != RequestUpdateResult::Rendered) { + LOG_ERR(LOG_TAG, "Receiving screen could not be rendered synchronously"); + requestUpdate(true); + } + + return true; +} + +void NearbyBookTransferActivity::sendPendingAccept() { + if (!acceptPending_ || !receivingScreenDrawn_.load(std::memory_order_acquire)) return; + acceptPending_ = false; + uint8_t payload[2]; + nearby::writeU16(payload, negotiatedChunkBytes_); + if (!sendPacket(nearby::PacketType::Accept, peerMac_.data(), 0, payload, sizeof(payload))) { + setError(tr(STR_NEARBY_TRANSFER_RADIO_FAILED)); + return; + } + lastActionMs_ = millis(); +} + +bool NearbyBookTransferActivity::sendNextChunk() { + pendingChunkLength_ = 0; + const int bytesRead = sourceFile_.read(chunkBuffer_.data(), negotiatedChunkBytes_); + if (bytesRead <= 0) { + setError(tr(STR_NEARBY_TRANSFER_SOURCE_FAILED)); + return false; + } + pendingChunkLength_ = static_cast(bytesRead); + session_.includeBytes(chunkBuffer_.data(), pendingChunkLength_); + retryCount_ = 0; + return resendPending(); +} + +bool NearbyBookTransferActivity::resendPending() { + lastActionMs_ = millis(); + return sendPacket(nearby::PacketType::Data, peerMac_.data(), session_.nextSequence(), chunkBuffer_.data(), + static_cast(pendingChunkLength_)); +} + +bool NearbyBookTransferActivity::sendAck() { + uint8_t payload[4]; + nearby::writeU32(payload, session_.nextSequence()); + lastActionMs_ = millis(); + return sendPacket(nearby::PacketType::Ack, peerMac_.data(), 0, payload, sizeof(payload)); +} + +bool NearbyBookTransferActivity::sendComplete() { + uint8_t payload[12]; + nearby::writeU64(payload, session_.totalBytes()); + nearby::writeU32(payload + 8, session_.crc32()); + lastActionMs_ = millis(); + retryCount_ = 0; + return sendPacket(nearby::PacketType::Complete, peerMac_.data(), 0, payload, sizeof(payload)); +} + +bool NearbyBookTransferActivity::finishReceivedFile(const uint64_t expectedBytes, const uint32_t expectedCrc) { + if (expectedBytes != session_.totalBytes() || expectedBytes != session_.transferredBytes() || + expectedCrc != session_.crc32()) { + receiveFile_.close(); + Storage.remove(tempPath_.c_str()); + setError(tr(STR_NEARBY_TRANSFER_VERIFY_FAILED)); + return false; + } + receiveFile_.flush(); + const bool synced = receiveFile_.sync(); + const bool closed = receiveFile_.close(); + if (!synced || !closed) { + Storage.remove(tempPath_.c_str()); + setError(tr(STR_NEARBY_TRANSFER_WRITE_FAILED)); + return false; + } + const bool replacing = Storage.exists(finalPath_.c_str()); + if (replacing) { + if (Storage.exists(backupPath_.c_str())) Storage.remove(backupPath_.c_str()); + if (!Storage.rename(finalPath_.c_str(), backupPath_.c_str())) { + Storage.remove(tempPath_.c_str()); + setError(tr(STR_NEARBY_TRANSFER_REPLACE_FAILED)); + return false; + } + } + if (!Storage.rename(tempPath_.c_str(), finalPath_.c_str())) { + if (replacing) Storage.rename(backupPath_.c_str(), finalPath_.c_str()); + Storage.remove(tempPath_.c_str()); + setError(tr(STR_NEARBY_TRANSFER_REPLACE_FAILED)); + return false; + } + if (replacing) Storage.remove(backupPath_.c_str()); + return true; +} + +void NearbyBookTransferActivity::cancelTransfer() { + acceptPending_ = false; + receivingScreenDrawn_.store(false, std::memory_order_release); + if (transport_.started() && peerMac_ != std::array{}) { + sendPacket(nearby::PacketType::Cancel, peerMac_.data()); + } + sourceFile_.close(); + receiveFile_.close(); + if (!tempPath_.empty() && Storage.exists(tempPath_.c_str())) Storage.remove(tempPath_.c_str()); +} + +void NearbyBookTransferActivity::rejectOffer() { + const uint8_t reason = REJECT_USER; + sendPacket(nearby::PacketType::Reject, peerMac_.data(), 0, &reason, 1); + startListening(); +} + +void NearbyBookTransferActivity::setState(const State state) { + state_ = state; + selectedIndex_ = 0; + uiReady_ = false; + app_.clearTapFlash(); + lastUiMs_ = millis(); + requestUpdate(); +} + +void NearbyBookTransferActivity::setError(const char* message) { + errorMessage_ = message ? message : tr(STR_NEARBY_TRANSFER_FAILED); + LOG_ERR(LOG_TAG, "%s", errorMessage_.c_str()); + cancelTransfer(); + setState(State::Error); +} + +void NearbyBookTransferActivity::exitAfterRadio() { + if (state_ == State::Sending || state_ == State::Receiving || state_ == State::WaitingForApproval || + state_ == State::OfferPrompt || state_ == State::CollisionPrompt) { + cancelTransfer(); + } else { + sourceFile_.close(); + receiveFile_.close(); + } + stopRadio(); + if (radioUsed_) { + if (returnToReader_) { + silentRestartToReader(); + } else { + silentRestart(); + } + } else { + onGoHome(); + } +} + +void NearbyBookTransferActivity::openReceivedFile() { + if (finalPath_.empty()) return; + APP_STATE.openEpubPath = finalPath_; + APP_STATE.saveToFile(); + stopRadio(); + silentRestartToReader(); +} + +void NearbyBookTransferActivity::chooseDestinationFolder() { + auto picker = makeUniqueNoThrow(renderer, mappedInput, destinationFolder_, + FileBrowserActivity::Mode::PickDirectory); + if (!picker) { + setError(tr(STR_NEARBY_TRANSFER_FAILED)); + return; + } + startActivityForResult(std::move(picker), [this](const ActivityResult& result) { + const auto* path = std::get_if(&result.data); + if (result.isCancelled || !path) return; + destinationFolder_ = path->path.empty() ? "/" : path->path; + const std::string stored = destinationFolder_ == "/" ? "" : destinationFolder_; + strncpy(SETTINGS.nearbyReceiveFolder, stored.c_str(), sizeof(SETTINGS.nearbyReceiveFolder) - 1); + SETTINGS.nearbyReceiveFolder[sizeof(SETTINGS.nearbyReceiveFolder) - 1] = '\0'; + if (!SETTINGS.saveToFile()) LOG_ERR(LOG_TAG, "Failed to save receive folder"); + requestUpdate(); + }); +} + +void NearbyBookTransferActivity::updateTimers() { + const uint32_t now = millis(); + if (state_ == State::Discovering && now - lastActionMs_ >= DISCOVERY_INTERVAL_MS) { + sendDiscovery(); + return; + } + if (state_ == State::WaitingForApproval && now - lastActionMs_ >= RETRY_INTERVAL_MS * 2) { + if (++retryCount_ > MAX_APPROVAL_RETRIES) { + setError(tr(STR_NEARBY_TRANSFER_TIMEOUT)); + } else { + sendOffer(); + } + return; + } + if (state_ == State::Receiving && now - lastActionMs_ >= RECEIVE_TIMEOUT_MS) { + setError(tr(STR_NEARBY_TRANSFER_TIMEOUT)); + return; + } + if (state_ != State::Sending || now - lastActionMs_ < RETRY_INTERVAL_MS) return; + if (++retryCount_ > MAX_RETRIES) { + setError(tr(STR_NEARBY_TRANSFER_TIMEOUT)); + return; + } + if (pendingChunkLength_ > 0) { + resendPending(); + } else { + sendComplete(); + } +} + +void NearbyBookTransferActivity::maybeRefreshProgress() { + if (millis() - lastUiMs_ < UI_REFRESH_MS) return; + lastUiMs_ = millis(); + requestUpdate(); +} + +bool NearbyBookTransferActivity::isMenuState() const { + return state_ == State::ChooseReceiveAction || state_ == State::DeviceList || + (state_ == State::CollisionPrompt && !mappedInput.hasTouch()); +} + +int NearbyBookTransferActivity::menuItemCount() const { + if (state_ == State::ChooseReceiveAction) return 2; + if (state_ == State::DeviceList) return peerCount_; + if (state_ == State::CollisionPrompt) return 3; + return 0; +} + +void NearbyBookTransferActivity::activateSelected() { + switch (state_) { + case State::ChooseReceiveAction: + if (selectedIndex_ == 0) + startListening(); + else + chooseDestinationFolder(); + break; + case State::DeviceList: + selectPeer(); + break; + case State::OfferPrompt: + if (Storage.exists(finalPath_.c_str())) + setState(State::CollisionPrompt); + else + acceptOffer(false); + break; + case State::CollisionPrompt: + if (selectedIndex_ == 0) { + acceptOffer(false); + } else if (selectedIndex_ == 1) { + acceptOffer(true); + } else { + rejectOffer(); + } + break; + case State::Success: + if (mode_ == Mode::Receive) + openReceivedFile(); + else + exitAfterRadio(); + break; + case State::Error: + exitAfterRadio(); + break; + default: + break; + } +} + +void NearbyBookTransferActivity::menuScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildMenuScreen(screen); +} + +void NearbyBookTransferActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (!self->isMenuState() || event.value < 0 || event.value >= self->menuItemCount()) return; + self->selectedIndex_ = event.value; + self->app_.clearTapFlash(); + self->activateSelected(); +} + +void NearbyBookTransferActivity::buildMenuScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + + metrics.verticalSpacing), + 0, static_cast(metrics.buttonHintsHeight + metrics.verticalSpacing), 0}); + + std::array items{}; + const int count = menuItemCount(); + for (int i = 0; i < count; ++i) { + if (state_ == State::ChooseReceiveAction) { + items[i].label = i == 0 ? tr(STR_START_RECEIVING) : tr(STR_CHANGE_FOLDER); + if (i == 1) items[i].value = destinationFolder_.c_str(); + } else if (state_ == State::DeviceList) { + items[i].label = peers_[i].name.data(); + } else if (state_ == State::CollisionPrompt) { + items[i].label = i == 0 ? tr(STR_REPLACE) : (i == 1 ? tr(STR_KEEP_BOTH) : tr(STR_CANCEL)); + } + items[i].actionValue = static_cast(i); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(count); + props.selectedIndex = static_cast(selectedIndex_); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.valueInset = 8; + props.labelText = screen.theme().bodyText; + props.labelText.maxLines = 2; + configureUiList(props, screen.theme(), screen.body()); + screen.list(props); +} + +void NearbyBookTransferActivity::updateNavigation() { + const int count = menuItemCount(); + if (count <= 1) return; + buttonNavigator_.onNextRelease([this, count] { + selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, count); + requestUpdate(); + }); + buttonNavigator_.onPreviousRelease([this, count] { + selectedIndex_ = ButtonNavigator::previousIndex(selectedIndex_, count); + requestUpdate(); + }); +} + +void NearbyBookTransferActivity::loop() { + sendPendingAccept(); + processPackets(); + updateTimers(); + + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer) || + mappedInput.wasReleased(MappedInputManager::Button::Back)) { + if (state_ == State::OfferPrompt || state_ == State::CollisionPrompt) { + const uint8_t reason = REJECT_USER; + sendPacket(nearby::PacketType::Reject, peerMac_.data(), 0, &reason, 1); + startListening(); + } else { + exitAfterRadio(); + } + return; + } + + int tx = 0; + int ty = 0; + if (mappedInput.hasTouch() && + (state_ == State::OfferPrompt || state_ == State::CollisionPrompt || state_ == State::Success) && + mappedInput.wasScreenTouchDown(tx, ty)) { + const Rect safeArea = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + if (state_ == State::CollisionPrompt) { + for (int index = 0; index < 3; ++index) { + if (!contains(touchCollisionButtonRect(safeArea, index), tx, ty)) continue; + selectedIndex_ = index; + activateSelected(); + return; + } + } + if ((state_ == State::OfferPrompt || mode_ == Mode::Receive) && + contains(touchActionButtonRect(safeArea, false), tx, ty)) { + if (state_ == State::Success) { + exitAfterRadio(); + return; + } + rejectOffer(); + return; + } + if (contains(touchActionButtonRect(safeArea, true), tx, ty)) { + activateSelected(); + return; + } + } + + if (isMenuState() && uiReady_) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app_.route(snap); + if (app_.invalidated()) requestUpdate(); + if (event) return; + } + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + activateSelected(); + return; + } + updateNavigation(); +} + +void NearbyBookTransferActivity::render(RenderLock&&) { + renderer.clearScreen(); + bool drewReceivingScreen = false; + const auto& metrics = UITheme::getInstance().getMetrics(); + const int width = renderer.getScreenWidth(); + const int height = renderer.getScreenHeight(); + const Rect header{0, metrics.topPadding, width, TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget_, header, tr(STR_NEARBY_BOOK_TRANSFER), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_NEARBY_BOOK_TRANSFER)); + } + + const int textWidth = width - metrics.contentSidePadding * 2; + auto centered = [this, height](const char* text, const int offset = 0, const int font = UI_10_FONT_ID) { + renderer.drawCenteredText(font, height / 2 + offset, text, true, + offset == 0 ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); + }; + auto centeredWrapped = [this, height, textWidth](const char* text, const int offset, const int maxLines, + const int font = UI_10_FONT_ID) { + const auto lines = renderer.wrappedText(font, text, textWidth, maxLines); + const int lineHeight = renderer.getLineHeight(font); + const int extraLines = std::max(0, static_cast(lines.size()) - 1); + int y = height / 2 + offset - extraLines * lineHeight / 2; + for (const auto& line : lines) { + renderer.drawCenteredText(font, y, line.c_str()); + y += lineHeight; + } + }; + auto drawTouchButton = [this](const Rect& rect, const char* label) { + renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::White); + renderer.drawRect(rect.x, rect.y, rect.width, rect.height, true); + const int x = rect.x + (rect.width - renderer.getTextWidth(UI_10_FONT_ID, label)) / 2; + const int y = rect.y + (rect.height - renderer.getLineHeight(UI_10_FONT_ID)) / 2; + renderer.drawText(UI_10_FONT_ID, x, y, label); + }; + + uiReady_ = false; + if (isMenuState()) { + app_.render(); + uiReady_ = true; + } else if (state_ == State::Listening) { + centered(tr(STR_NEARBY_TRANSFER_LISTENING)); + centeredWrapped(destinationFolder_.c_str(), renderer.getLineHeight(UI_10_FONT_ID) + 10, 2, SMALL_FONT_ID); + } else if (state_ == State::Discovering) { + centered(tr(STR_NEARBY_TRANSFER_DISCOVERING)); + } else if (state_ == State::WaitingForApproval) { + centeredWrapped(tr(STR_NEARBY_TRANSFER_WAITING_APPROVAL), -10, 2); + centeredWrapped(peers_[selectedIndex_].name.data(), renderer.getLineHeight(UI_10_FONT_ID) + 18, 2, SMALL_FONT_ID); + } else if (state_ == State::Validating) { + centered(tr(STR_NEARBY_TRANSFER_VALIDATING)); + } else if (state_ == State::OfferPrompt) { + centeredWrapped(tr(STR_NEARBY_TRANSFER_INCOMING), -65, 2); + centeredWrapped(senderName_.c_str(), -25, 2, SMALL_FONT_ID); + centeredWrapped(offeredFileName_.c_str(), 25, 2, SMALL_FONT_ID); + char sizeText[48]; + snprintf(sizeText, sizeof(sizeText), tr(STR_NEARBY_TRANSFER_SIZE), static_cast(offeredFileSize_)); + centered(sizeText, 70, SMALL_FONT_ID); + } else if (state_ == State::CollisionPrompt) { + centeredWrapped(offeredFileName_.c_str(), -150, 2, SMALL_FONT_ID); + } else if (state_ == State::Sending || state_ == State::Receiving) { + centeredWrapped(state_ == State::Sending ? tr(STR_NEARBY_TRANSFER_SENDING) : tr(STR_NEARBY_TRANSFER_RECEIVING), -45, + 2); + drewReceivingScreen = state_ == State::Receiving; + const uint64_t scale = std::max(1, session_.totalBytes() / 10000 + 1); + GUI.drawProgressBar(renderer, + Rect{metrics.contentSidePadding, height / 2 + 10, width - metrics.contentSidePadding * 2, + metrics.progressBarHeight}, + static_cast(session_.transferredBytes() / scale), + static_cast(session_.totalBytes() / scale)); + } else if (state_ == State::Success) { + centered(mode_ == Mode::Receive ? tr(STR_NEARBY_TRANSFER_RECEIVED) : tr(STR_NEARBY_TRANSFER_SENT)); + centeredWrapped(mode_ == Mode::Receive ? finalPath_.c_str() : offeredFileName_.c_str(), + renderer.getLineHeight(UI_10_FONT_ID) + 10, 3, SMALL_FONT_ID); + } else if (state_ == State::Error) { + centered(tr(STR_NEARBY_TRANSFER_FAILED)); + centeredWrapped(errorMessage_.c_str(), renderer.getLineHeight(UI_10_FONT_ID) + 10, 3, SMALL_FONT_ID); + } + + const char* back = tr(STR_CANCEL); + const char* confirm = ""; + if (state_ == State::ChooseReceiveAction || state_ == State::DeviceList || state_ == State::CollisionPrompt) + confirm = tr(STR_SELECT); + else if (state_ == State::OfferPrompt) + confirm = tr(STR_ACCEPT); + else if (state_ == State::Success && mode_ == Mode::Receive) { + back = tr(STR_BACK); + confirm = + FsHelpers::hasPngExtension(finalPath_) || FsHelpers::hasBmpExtension(finalPath_) ? tr(STR_OPEN) : tr(STR_READ); + } else if (state_ == State::Success) { + back = ""; + confirm = tr(STR_OK); + } else if (state_ == State::Error) + confirm = tr(STR_OK); + const bool showNavigation = !mappedInput.hasTouch() && menuItemCount() > 1; + const bool showTouchCollisionActions = mappedInput.hasTouch() && state_ == State::CollisionPrompt; + const bool showTouchActions = mappedInput.hasTouch() && (state_ == State::OfferPrompt || state_ == State::Success); + if (showTouchCollisionActions) { + const Rect safeArea = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + drawTouchButton(touchCollisionButtonRect(safeArea, 0), tr(STR_REPLACE)); + drawTouchButton(touchCollisionButtonRect(safeArea, 1), tr(STR_KEEP_BOTH)); + drawTouchButton(touchCollisionButtonRect(safeArea, 2), tr(STR_CANCEL)); + } else if (showTouchActions) { + const Rect safeArea = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + if (state_ == State::OfferPrompt || mode_ == Mode::Receive) + drawTouchButton(touchActionButtonRect(safeArea, false), back); + drawTouchButton(touchActionButtonRect(safeArea, true), confirm); + } else { + const auto labels = mappedInput.mapLabels(back, confirm, showNavigation ? tr(STR_DIR_UP) : "", + showNavigation ? tr(STR_DIR_DOWN) : ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + } + renderer.displayBuffer(screenTransitionRefresh_.modeFor(static_cast(state_))); + if (drewReceivingScreen) receivingScreenDrawn_.store(true, std::memory_order_release); +} diff --git a/src/activities/network/NearbyBookTransferActivity.h b/src/activities/network/NearbyBookTransferActivity.h new file mode 100644 index 0000000000..3f7d245597 --- /dev/null +++ b/src/activities/network/NearbyBookTransferActivity.h @@ -0,0 +1,149 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" +#include "util/ButtonNavigator.h" + +class NearbyBookTransferActivity final : public Activity { + public: + enum class Mode : uint8_t { Send, Receive }; + + NearbyBookTransferActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, Mode mode, + std::string sourcePath = {}, bool returnToReader = false); + ~NearbyBookTransferActivity() override; + + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; + bool preventAutoSleep() override { return true; } + bool skipLoopDelay() override; + + private: + enum class State : uint8_t { + ChooseReceiveAction, + Listening, + Discovering, + DeviceList, + WaitingForApproval, + OfferPrompt, + CollisionPrompt, + Validating, + Sending, + Receiving, + Success, + Error, + }; + + struct Peer { + std::array mac{}; + std::array name{}; + }; + + static constexpr size_t MAX_PEERS = 4; + static constexpr uint8_t ESPNOW_CHANNEL = 1; + static constexpr uint32_t DISCOVERY_INTERVAL_MS = 900; + static constexpr uint32_t RETRY_INTERVAL_MS = 450; + static constexpr uint8_t MAX_RETRIES = 12; + static constexpr uint8_t MAX_APPROVAL_RETRIES = 60; + static constexpr uint32_t RECEIVE_TIMEOUT_MS = 15000; + static constexpr uint32_t UI_REFRESH_MS = 1800; + + Mode mode_; + State state_ = State::ChooseReceiveAction; + ScreenTransitionRefresh screenTransitionRefresh_; + std::string sourcePath_; + bool returnToReader_ = false; + bool radioUsed_ = false; + + freeink::nearby::EspNowTransport transport_; + freeink::nearby::EspNowTransport::Event eventBuffer_; + freeink::nearby::ReliableTransferSession session_; + std::array peers_{}; + uint8_t peerCount_ = 0; + int selectedIndex_ = 0; + std::array peerMac_{}; + + HalFile sourceFile_; + HalFile receiveFile_; + std::array chunkBuffer_{}; + std::array packetBuffer_{}; + size_t pendingChunkLength_ = 0; + uint64_t offeredFileSize_ = 0; + uint16_t negotiatedChunkBytes_ = freeink::nearby::COMPAT_CHUNK_BYTES; + uint32_t sessionId_ = 0; + uint32_t lastActionMs_ = 0; + uint32_t lastUiMs_ = 0; + uint8_t retryCount_ = 0; + + std::string offeredFileName_; + std::string senderName_; + std::string destinationFolder_; + std::string finalPath_; + std::string tempPath_; + std::string backupPath_; + std::string errorMessage_; + + ButtonNavigator buttonNavigator_; + + using UiApp = freeink::ui::FreeInkApp<8, 4>; + static constexpr freeink::ui::ActionId ACTION_ROW = 1; + freeink::ui::GfxRendererTarget uiTarget_; // Must precede app_: app_ holds a reference to it. + UiApp app_; + std::atomic uiReady_{false}; + std::atomic receivingScreenDrawn_{false}; + bool acceptPending_ = false; + + bool startRadio(); + void stopRadio(); + void startListening(); + void startDiscovery(); + void selectPeer(); + void processPackets(); + void handlePacket(const freeink::nearby::EspNowTransport::Event& event, const freeink::nearby::PacketView& packet); + bool sendPacket(freeink::nearby::PacketType type, const uint8_t* destination, uint32_t sequence = 0, + const void* payload = nullptr, uint16_t payloadLength = 0); + bool sendDiscovery(); + bool sendAdvertisement(const uint8_t* destination); + bool sendOffer(); + bool acceptOffer(bool keepBoth); + void sendPendingAccept(); + bool sendNextChunk(); + bool resendPending(); + bool sendAck(); + bool sendComplete(); + bool finishReceivedFile(uint64_t expectedBytes, uint32_t expectedCrc); + void cancelTransfer(); + void rejectOffer(); + void setState(State state); + void setError(const char* message); + void exitAfterRadio(); + void openReceivedFile(); + void chooseDestinationFolder(); + void updateTimers(); + void updateNavigation(); + void maybeRefreshProgress(); + bool isMenuState() const; + int menuItemCount() const; + void activateSelected(); + static void menuScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildMenuScreen(UiApp::ScreenType& screen); + + static bool supportedFile(const std::string& path); + static bool safeFileName(const std::string& name); + static std::string fileNameFromPath(const std::string& path); + static std::string joinPath(const std::string& folder, const std::string& name); + static std::string keepBothPath(const std::string& path); +}; diff --git a/src/activities/network/NearbyStatsSyncActivity.cpp b/src/activities/network/NearbyStatsSyncActivity.cpp index 8ea5836105..b272d30ac0 100644 --- a/src/activities/network/NearbyStatsSyncActivity.cpp +++ b/src/activities/network/NearbyStatsSyncActivity.cpp @@ -1,5 +1,7 @@ #include "NearbyStatsSyncActivity.h" +#include "components/TouchHeaderBackButton.h" + #ifdef SIMULATOR #include @@ -22,7 +24,9 @@ void NearbyStatsSyncActivity::onEnter() { void NearbyStatsSyncActivity::onExit() { Activity::onExit(); } void NearbyStatsSyncActivity::loop() { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) exitViaBack(); + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer) || + mappedInput.wasPressed(MappedInputManager::Button::Back)) + exitViaBack(); } void NearbyStatsSyncActivity::render(RenderLock&&) { @@ -31,12 +35,17 @@ void NearbyStatsSyncActivity::render(RenderLock&&) { const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_NEARBY_STATS_SYNC)); + const Rect header{0, metrics.topPadding, pageWidth, TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_NEARBY_STATS_SYNC), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_NEARBY_STATS_SYNC)); + } renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_NEARBY_STATS_SIMULATOR_UNAVAILABLE), true, EpdFontFamily::BOLD); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh_.modeFor(static_cast(state_))); } void NearbyStatsSyncActivity::enqueueEspNowPacket(const uint8_t*, const uint8_t*, int) {} @@ -223,7 +232,8 @@ void NearbyStatsSyncActivity::onExit() { void NearbyStatsSyncActivity::loop() { processEvents(); - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer) || + mappedInput.wasPressed(MappedInputManager::Button::Back)) { exitViaBack(); return; } @@ -567,7 +577,12 @@ void NearbyStatsSyncActivity::render(RenderLock&&) { const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_NEARBY_STATS_SYNC)); + const Rect header{0, metrics.topPadding, pageWidth, TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_NEARBY_STATS_SYNC), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_NEARBY_STATS_SYNC)); + } const int centerY = pageHeight / 2 - 20; std::string primary; @@ -611,7 +626,7 @@ void NearbyStatsSyncActivity::render(RenderLock&&) { renderReady(primary, detailPrimary, detailSecondary); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_NEARBY_STATS_SYNC_BUTTON), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh_.modeFor(static_cast(state_))); return; } @@ -627,21 +642,27 @@ void NearbyStatsSyncActivity::render(RenderLock&&) { } const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh_.modeFor(static_cast(state_))); } void NearbyStatsSyncActivity::renderReady(const std::string& primary, const std::string& detailPrimary, const std::string& detailSecondary) const { const auto& metrics = UITheme::getInstance().getMetrics(); - const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentTop = + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); int y = contentTop + 70; renderer.drawCenteredText(UI_10_FONT_ID, y, primary.c_str(), true, EpdFontFamily::BOLD); y += lineHeight + metrics.verticalSpacing; if (!detailPrimary.empty()) { - renderer.drawCenteredText(SMALL_FONT_ID, y, detailPrimary.c_str(), true); - y += renderer.getLineHeight(SMALL_FONT_ID) + metrics.verticalSpacing; + const auto detailLines = renderer.wrappedText(SMALL_FONT_ID, detailPrimary.c_str(), + renderer.getScreenWidth() - metrics.contentSidePadding * 2, 3); + for (const auto& line : detailLines) { + renderer.drawCenteredText(SMALL_FONT_ID, y, line.c_str(), true); + y += renderer.getLineHeight(SMALL_FONT_ID); + } + y += metrics.verticalSpacing; } if (!detailSecondary.empty()) { renderer.drawCenteredText(SMALL_FONT_ID, y, detailSecondary.c_str(), true); diff --git a/src/activities/network/NearbyStatsSyncActivity.h b/src/activities/network/NearbyStatsSyncActivity.h index c4436ecb48..76090e7983 100644 --- a/src/activities/network/NearbyStatsSyncActivity.h +++ b/src/activities/network/NearbyStatsSyncActivity.h @@ -9,6 +9,7 @@ #include #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" #include "activities/reader/GlobalReadingStats.h" class NearbyStatsSyncActivity final : public Activity { @@ -41,6 +42,7 @@ class NearbyStatsSyncActivity final : public Activity { static constexpr size_t MAX_SYNC_EVENTS = 8; State state_ = State::STARTING; + ScreenTransitionRefresh screenTransitionRefresh_; SemaphoreHandle_t eventMutex_ = nullptr; std::array events_ = {}; uint8_t eventHead_ = 0; diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index 66f22eb2e6..9e16505ae1 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -4,27 +4,80 @@ #include #include "MappedInputManager.h" -#include "components/CompactHeader.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" +#include "components/icons/chart.h" #include "fontIds.h" +namespace fui = freeink::ui; + namespace { -constexpr int MENU_ITEM_COUNT = 4; +constexpr int MENU_ITEM_COUNT = 5; +constexpr int LIST_ITEM_COUNT = MENU_ITEM_COUNT + 1; +constexpr int NEARBY_SECTION_INDEX = 3; +constexpr fui::ActionId ACTION_ROW = 1; +constexpr StrId menuItems[MENU_ITEM_COUNT] = {StrId::STR_JOIN_NETWORK, StrId::STR_CALIBRE_WIRELESS, + StrId::STR_CREATE_HOTSPOT, StrId::STR_RECEIVE_NEARBY_BOOK, + StrId::STR_NEARBY_STATS_SYNC}; +constexpr StrId menuDescs[MENU_ITEM_COUNT] = {StrId::STR_JOIN_DESC, StrId::STR_CALIBRE_DESC, StrId::STR_HOTSPOT_DESC, + StrId::STR_RECEIVE_NEARBY_BOOK_DESC, StrId::STR_NEARBY_STATS_SYNC_DESC}; +constexpr UIIcon menuIcons[MENU_ITEM_COUNT] = {UIIcon::Wifi, UIIcon::Library, UIIcon::Hotspot, UIIcon::Transfer, + UIIcon::Transfer}; + +int listIndexForMenuIndex(const int menuIndex) { return menuIndex < NEARBY_SECTION_INDEX ? menuIndex : menuIndex + 1; } } // namespace +NetworkModeSelectionActivity::NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("NetworkModeSelection", renderer, mappedInput), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + +void NetworkModeSelectionActivity::selectCurrent() { + NetworkMode mode = NetworkMode::JOIN_NETWORK; + if (selectedIndex == 1) { + mode = NetworkMode::CONNECT_CALIBRE; + } else if (selectedIndex == 2) { + mode = NetworkMode::CREATE_HOTSPOT; + } else if (selectedIndex == 3) { + mode = NetworkMode::NEARBY_BOOK_RECEIVE; + } else if (selectedIndex == 4) { + mode = NetworkMode::NEARBY_STATS_SYNC; + } + onModeSelected(mode); +} + +void NetworkModeSelectionActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= MENU_ITEM_COUNT) return; + self->selectedIndex = event.value; + // Selection leaves this screen; a lingering flash would gray an unrelated + // element on the next render. + self->app.clearTapFlash(); + self->selectCurrent(); +} + void NetworkModeSelectionActivity::onEnter() { Activity::onEnter(); - // Reset selection selectedIndex = 0; - - // Trigger first update + uiReady = false; + visibleRows = 1; + topIndex = 0; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &NetworkModeSelectionActivity::onRowEvent, this); + app.setScreen(&NetworkModeSelectionActivity::listScreen, this); requestUpdate(); } void NetworkModeSelectionActivity::onExit() { Activity::onExit(); } void NetworkModeSelectionActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + onCancel(); + return; + } // Handle back button - cancel if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { onCancel(); @@ -32,60 +85,119 @@ void NetworkModeSelectionActivity::loop() { } // Handle confirm button - select current option - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - NetworkMode mode = NetworkMode::JOIN_NETWORK; - if (selectedIndex == 1) { - mode = NetworkMode::CONNECT_CALIBRE; - } else if (selectedIndex == 2) { - mode = NetworkMode::CREATE_HOTSPOT; - } else if (selectedIndex == 3) { - mode = NetworkMode::NEARBY_STATS_SYNC; - } - onModeSelected(mode); + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + mappedInput.suppressNextConfirmRelease(); + selectCurrent(); return; } + // Touch goes through the FreeInkApp: render() registered the row hit rects; + // route the snapshot and let onRowEvent dispatch. + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; // dispatched to onRowEvent + } + } + // Handle navigation buttonNavigator.onNext([this] { selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT); + topIndex = followListSelection(listIndexForMenuIndex(selectedIndex), topIndex, visibleRows, LIST_ITEM_COUNT); requestUpdate(); }); buttonNavigator.onPrevious([this] { selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEM_COUNT); + topIndex = followListSelection(listIndexForMenuIndex(selectedIndex), topIndex, visibleRows, LIST_ITEM_COUNT); requestUpdate(); }); } +void NetworkModeSelectionActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} + +void NetworkModeSelectionActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + // Content below the GUI.drawHeader band, above the button hints. + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + screen.spacer(static_cast(metrics.verticalSpacing)); + + std::vector items; + items.reserve(LIST_ITEM_COUNT); + for (int i = 0; i < MENU_ITEM_COUNT; i++) { + if (i == NEARBY_SECTION_INDEX) { + fui::ListItem header; + header.label = I18N.get(StrId::STR_NEARBY_DEVICE); + header.isHeader = true; + items.push_back(header); + } + + fui::ListItem item; + item.label = I18N.get(menuItems[i]); + item.subtitle = I18N.get(menuDescs[i]); + if (i == 3) { + item.icon = fui::bitmapFromIcon(icon_chevrons_left_right_ellipsis_32); + } else if (i == 4) { + item.icon = fui::BitmapRef{ChartListIcon, 32, 32, fui::BitmapFormat::Mask1}; + } else { + item.icon = listIconFor(menuIcons[i], 32); // subtitle rows carry the larger icon + } + item.actionValue = static_cast(i); + items.push_back(item); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(listIndexForMenuIndex(selectedIndex)); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; // physical buttons stay in loop() + props.labelText = screen.theme().bodyText; + props.labelText.bold = true; + props.subtitleText = screen.theme().smallText; + props.subtitleText.bold = false; + props.headerText = screen.theme().bodyText; + props.headerText.bold = true; + props.rowGap = 10; + // The normal 10px row gap plus this extra 10px separates Nearby Device + // from the hotspot group by about 20px. + props.sectionGap = 10; + const auto rows = configureUiList(props, screen.theme(), screen.body(), UiListRowType::WithSubtitle); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, LIST_ITEM_COUNT); // clamp to range + props.topIndex = static_cast(topIndex); + screen.list(props); +} + void NetworkModeSelectionActivity::render(RenderLock&&) { renderer.clearScreen(); const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - - CompactHeader::drawTitle(renderer, tr(STR_FILE_TRANSFER)); - - const int contentTop = CompactHeader::contentTop(metrics); - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; - // Menu items and descriptions - static constexpr StrId menuItems[MENU_ITEM_COUNT] = {StrId::STR_JOIN_NETWORK, StrId::STR_CALIBRE_WIRELESS, - StrId::STR_CREATE_HOTSPOT, StrId::STR_NEARBY_STATS_SYNC}; - static constexpr StrId menuDescs[MENU_ITEM_COUNT] = {StrId::STR_JOIN_DESC, StrId::STR_CALIBRE_DESC, - StrId::STR_HOTSPOT_DESC, StrId::STR_NEARBY_STATS_SYNC_DESC}; - static constexpr UIIcon menuIcons[MENU_ITEM_COUNT] = {UIIcon::Wifi, UIIcon::Library, UIIcon::Hotspot, - UIIcon::Transfer}; - - GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(MENU_ITEM_COUNT), selectedIndex, - [](int index) { return std::string(I18N.get(menuItems[index])); }, - [](int index) { return std::string(I18N.get(menuDescs[index])); }, [](int index) { return menuIcons[index]; }); - - // Draw help text at bottom + + // Header via GUI.drawHeader (already FreeInkUI-themed) for the battery + // indicator; the rest of the screen renders through the app. + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_FILE_TRANSFER), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_FILE_TRANSFER)); + } + + uiReady = false; + app.render(); + uiReady = true; + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(0)); } void NetworkModeSelectionActivity::onModeSelected(NetworkMode mode) { diff --git a/src/activities/network/NetworkModeSelectionActivity.h b/src/activities/network/NetworkModeSelectionActivity.h index 447da71f56..25b7a746d6 100644 --- a/src/activities/network/NetworkModeSelectionActivity.h +++ b/src/activities/network/NetworkModeSelectionActivity.h @@ -1,30 +1,53 @@ #pragma once +#include +#include + +#include #include #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" #include "util/ButtonNavigator.h" -enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT, NEARBY_STATS_SYNC }; +enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT, NEARBY_BOOK_RECEIVE, NEARBY_STATS_SYNC }; /** * NetworkModeSelectionActivity presents the user with a choice: * - "Join a Network" - Connect to an existing WiFi network (STA mode) * - "Connect to Calibre" - Use Calibre wireless device transfers * - "Create Hotspot" - Create an Access Point that others can connect to (AP mode) - * - "Nearby Stats Sync" - Sync reading stats directly with a nearby reader + * - "Sync Stats" - Sync reading stats directly with a nearby reader + * - "Receive File" - Receive a file directly from another reader * * The onModeSelected callback is called with the user's choice. * The onCancel callback is called if the user presses back. */ class NetworkModeSelectionActivity final : public Activity { + // FreeInkApp hosts the mode list (themed rows, icons, touch routing); the + // header stays on GUI.drawHeader for the battery indicator. + using UiApp = freeink::ui::FreeInkApp<12, 4>; + ButtonNavigator buttonNavigator; int selectedIndex = 0; + freeink::ui::GfxRendererTarget uiTarget; // must precede `app`: the app holds a reference to it + UiApp app; + // render() rebuilds the app's interaction table; loop() only routes touch + // snapshots against it while this is true (the two run on different tasks). + std::atomic uiReady{false}; + int visibleRows = 1; // rows per page at the current scale; set by the screen builder + int topIndex = 0; // viewport scroll position, decoupled from the selection + ScreenTransitionRefresh screenTransitionRefresh; + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); + void selectCurrent(); + public: - explicit NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("NetworkModeSelection", renderer, mappedInput) {} + explicit NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput); void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 9a372fd810..0c69e6f558 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -16,11 +16,18 @@ #include "SdCardFontSystem.h" #include "WifiCredentialStore.h" #include "activities/util/KeyboardEntryActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" +namespace fui = freeink::ui; + namespace { +constexpr fui::ActionId ACTION_ROW = 1; + #ifndef SIMULATOR uint8_t sLastStaDisconnectReason = 0; bool sConnectionAttemptLoggingActive = false; @@ -166,21 +173,45 @@ const char* wifiAuthName(const int authMode) { } // namespace +WifiSelectionActivity::WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const bool autoConnect, const bool useReaderButtonHints) + : Activity("WifiSelection", renderer, mappedInput), + allowAutoConnect(autoConnect), + useReaderButtonHints(useReaderButtonHints), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + +void WifiSelectionActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (self->state != WifiSelectionState::NETWORK_LIST) return; + if (event.value < 0 || event.value >= static_cast(self->networks.size())) return; + self->selectedNetworkIndex = static_cast(event.value); + // Long-press a saved network to forget it (mirrors the Left-button hold in loop()). + if (event.longPress) { + if (self->networks[self->selectedNetworkIndex].hasSavedPassword) { + self->selectedSSID = self->networks[self->selectedNetworkIndex].ssid; + self->state = WifiSelectionState::FORGET_PROMPT; + self->forgetPromptSelection = 0; // Default to "Cancel" + self->app.clearTapFlash(); + self->requestUpdate(); + } + return; + } + // Selection leaves this screen (password entry / connecting); a lingering + // flash would gray an unrelated row. + self->app.clearTapFlash(); + self->selectNetwork(static_cast(self->selectedNetworkIndex)); +} + void WifiSelectionActivity::onEnter() { Activity::onEnter(); sdFontSystem.releaseLoadedFont(renderer); ensureWifiEventLoggingRegistered(); - // Load saved WiFi credentials - SD card operations need lock as we use SPI - // for both - { - RenderLock lock(*this); - WIFI_STORE.loadFromFile(); - } - // Reset state selectedNetworkIndex = 0; networks.clear(); + realNetworkCount = 0; state = WifiSelectionState::SCANNING; selectedSSID.clear(); connectedIP.clear(); @@ -193,30 +224,38 @@ void WifiSelectionActivity::onEnter() { autoConnecting = false; lastConnectionStatusLogTime = 0; lastLoggedWifiStatus = -1; + manualNetworkListRequested = false; + autoAttemptedSsids.clear(); + const size_t savedCredentialCount = WIFI_STORE.getCredentialCount(); + autoAttemptedSsids.reserve(savedCredentialCount); // Cache MAC address for display cachedMacAddress = getDisplayMacAddress(); + uiReady = false; + visibleRows = 1; + topIndex = 0; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &WifiSelectionActivity::onRowEvent, this); + app.setScreen(&WifiSelectionActivity::listScreen, this); + // Trigger first update to show scanning message requestUpdate(); - // Attempt to auto-connect to the last network - if (allowAutoConnect) { + // Attempt to auto-connect to known networks. Try the last successful + // network first for speed, then scan and try any visible saved networks by + // signal strength. The user can interrupt this and show the scan result. + if (allowAutoConnect && savedCredentialCount != 0) { const std::string lastSsid = WIFI_STORE.getLastConnectedSsid(); if (!lastSsid.empty()) { - const auto* cred = WIFI_STORE.findCredential(lastSsid); - if (cred) { - LOG_INF("WIFI", "Auto-connect candidate: ssid=%s saved=1", lastSsid.c_str()); - selectedSSID = cred->ssid; - enteredPassword = cred->password; - selectedRequiresPassword = !cred->password.empty(); - usedSavedPassword = true; - autoConnecting = true; - attemptConnection(); - requestUpdate(); + const auto cred = WIFI_STORE.findCredential(lastSsid); + if (cred && tryAutoConnectCredential(*cred)) { return; } } + + startWifiScan(true); + return; } // Fallback to scanning @@ -226,31 +265,27 @@ void WifiSelectionActivity::onEnter() { void WifiSelectionActivity::onExit() { Activity::onExit(); - LOG_DBG("WIFI", "Free heap at onExit start: %d bytes", ESP.getFreeHeap()); - // Stop any ongoing WiFi scan - LOG_DBG("WIFI", "Deleting WiFi scan..."); WiFi.scanDelete(); - LOG_DBG("WIFI", "Free heap after scanDelete: %d bytes", ESP.getFreeHeap()); // Successful connections leave WiFi up for the parent activity. Canceled // flows own their cleanup because no parent may be present to tear WiFi down. if (tearDownWifiOnExit) { - LOG_DBG("WIFI", "Tearing down WiFi after cancelled selection..."); #ifndef SIMULATOR sConnectionAttemptLoggingActive = false; #endif WiFi.disconnect(false); delay(30); WiFi.mode(WIFI_OFF); - LOG_DBG("WIFI", "Free heap after WiFi off: %d bytes", ESP.getFreeHeap()); } LOG_DBG("WIFI", "Free heap at onExit end: %d bytes", ESP.getFreeHeap()); } -void WifiSelectionActivity::startWifiScan() { - autoConnecting = false; +void WifiSelectionActivity::startWifiScan(const bool autoScan) { + autoConnecting = autoScan; + manualNetworkListRequested = false; + topIndex = 0; state = WifiSelectionState::SCANNING; networks.clear(); requestUpdate(); @@ -277,7 +312,13 @@ void WifiSelectionActivity::processWifiScanResults() { if (scanResult == WIFI_SCAN_FAILED) { LOG_INF("WIFI", "WiFi scan failed"); + networks.clear(); + realNetworkCount = 0; + appendHiddenNetworkEntry(); + autoConnecting = false; + manualNetworkListRequested = false; state = WifiSelectionState::NETWORK_LIST; + selectedNetworkIndex = 0; requestUpdate(); return; } @@ -318,9 +359,6 @@ void WifiSelectionActivity::processWifiScanResults() { it->rssi = rssi; it->isEncrypted = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN); } - - LOG_DBG("WIFI", "Scan result: ssid=%s rssi=%d auth=%s saved=%d", ssid, rssi, wifiAuthName(authMode), - WIFI_STORE.hasSavedCredential(ssid)); } // Sort: saved-password networks first, then by signal strength (strongest first) @@ -331,20 +369,48 @@ void WifiSelectionActivity::processWifiScanResults() { return a.rssi > b.rssi; }); + realNetworkCount = networks.size(); + appendHiddenNetworkEntry(); + WiFi.scanDelete(); - LOG_INF("WIFI", "WiFi scan usable networks=%zu hidden=%d duplicates=%d", networks.size(), hiddenNetworks, + LOG_INF("WIFI", "WiFi scan usable networks=%zu hidden=%d duplicates=%d", realNetworkCount, hiddenNetworks, duplicateNetworks); + + if (autoConnecting && !manualNetworkListRequested && tryNextSavedNetworkFromScan()) { + return; + } + + autoConnecting = false; + manualNetworkListRequested = false; state = WifiSelectionState::NETWORK_LIST; selectedNetworkIndex = 0; requestUpdate(); } +void WifiSelectionActivity::appendHiddenNetworkEntry() { + // Synthetic list entry that lets the user type an SSID that is not broadcast. + // ESP32 can join hidden APs as long as the SSID is supplied to WiFi.begin(). + WifiNetworkInfo placeholder; + placeholder.rssi = 0; + placeholder.isEncrypted = true; // Treated as encrypted; an empty password still connects open APs + placeholder.hasSavedPassword = false; + placeholder.isHiddenPlaceholder = true; + networks.push_back(std::move(placeholder)); +} + void WifiSelectionActivity::selectNetwork(const int index) { if (index < 0 || index >= static_cast(networks.size())) { return; } const auto& network = networks[index]; + + // Synthetic "Add hidden network..." entry: prompt the user to type the SSID first + if (network.isHiddenPlaceholder) { + promptHiddenSsid(); + return; + } + selectedSSID = network.ssid; selectedRequiresPassword = network.isEncrypted; usedSavedPassword = false; @@ -352,34 +418,19 @@ void WifiSelectionActivity::selectNetwork(const int index) { autoConnecting = false; // Check if we have saved credentials for this network - const auto* savedCred = WIFI_STORE.findCredential(selectedSSID); + const auto savedCred = WIFI_STORE.findCredential(selectedSSID); if (savedCred && !savedCred->password.empty()) { // Use saved password - connect directly enteredPassword = savedCred->password; usedSavedPassword = true; LOG_INF("WIFI", "Selected network: ssid=%s encrypted=%d saved=1 rssi=%d", selectedSSID.c_str(), selectedRequiresPassword, network.rssi); - LOG_DBG("WiFi", "Using saved password for %s, length: %zu", selectedSSID.c_str(), enteredPassword.size()); attemptConnection(); return; } if (selectedRequiresPassword) { - // Show password entry - state = WifiSelectionState::PASSWORD_ENTRY; - // Don't allow screen updates while changing activity - startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), - "", // No initial text - 64, // Max password length - InputType::Password), - [this](const ActivityResult& result) { - if (result.isCancelled) { - state = WifiSelectionState::NETWORK_LIST; - } else { - enteredPassword = std::get(result.data).text; - // state will be updated in next loop iteration - } - }); + promptPasswordEntry(); } else { // Connect directly for open networks LOG_INF("WIFI", "Selected open network: ssid=%s rssi=%d", selectedSSID.c_str(), network.rssi); @@ -387,6 +438,119 @@ void WifiSelectionActivity::selectNetwork(const int index) { } } +void WifiSelectionActivity::promptPasswordEntry() { + // Show password entry + state = WifiSelectionState::PASSWORD_ENTRY; + // Don't allow screen updates while changing activity + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), + "", // No initial text + 64, // Max password length + InputType::Password), + [this](const ActivityResult& result) { + if (result.isCancelled) { + state = WifiSelectionState::NETWORK_LIST; + } else { + enteredPassword = std::get(result.data).text; + // state will be updated in next loop iteration + } + }); +} + +void WifiSelectionActivity::promptHiddenSsid() { + selectedSSID.clear(); + selectedRequiresPassword = true; // Hidden networks are usually encrypted; empty password still joins open APs + usedSavedPassword = false; + enteredPassword.clear(); + autoConnecting = false; + + // Suppress rendering during the activity transition (see render()). + state = WifiSelectionState::HIDDEN_SSID_ENTRY; + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_ENTER_WIFI_SSID), + "", // No initial text + 32, // Max SSID length (IEEE 802.11: 32 bytes) + InputType::Text), + [this](const ActivityResult& result) { + if (result.isCancelled) { + state = WifiSelectionState::NETWORK_LIST; + return; + } + selectedSSID = std::get(result.data).text; + if (selectedSSID.empty()) { + state = WifiSelectionState::NETWORK_LIST; + } + // Otherwise stay in HIDDEN_SSID_ENTRY; loop() continues the flow. + }); +} + +bool WifiSelectionActivity::hasAttemptedAutoSsid(const std::string& ssid) const { + return std::find(autoAttemptedSsids.begin(), autoAttemptedSsids.end(), ssid) != autoAttemptedSsids.end(); +} + +bool WifiSelectionActivity::tryAutoConnectCredential(const WifiCredential& cred) { + if (hasAttemptedAutoSsid(cred.ssid)) { + return false; + } + + LOG_DBG("WIFI", "Attempting saved network: %s", cred.ssid.c_str()); + autoAttemptedSsids.push_back(cred.ssid); + selectedSSID = cred.ssid; + enteredPassword = cred.password; + selectedRequiresPassword = !cred.password.empty(); + usedSavedPassword = true; + autoConnecting = true; + manualNetworkListRequested = false; + attemptConnection(); + requestUpdate(); + return true; +} + +bool WifiSelectionActivity::tryNextSavedNetworkFromScan() { + for (const auto& network : networks) { + if (!network.hasSavedPassword || hasAttemptedAutoSsid(network.ssid)) { + continue; + } + + const auto cred = WIFI_STORE.findCredential(network.ssid); + if (cred && tryAutoConnectCredential(*cred)) { + return true; + } + } + return false; +} + +void WifiSelectionActivity::handleAutoConnectFailure() { + LOG_DBG("WIFI", "Saved network failed: %s", selectedSSID.c_str()); + WiFi.disconnect(); + + if (!networks.empty()) { + if (tryNextSavedNetworkFromScan()) { + return; + } + autoConnecting = false; + state = WifiSelectionState::NETWORK_LIST; + selectedNetworkIndex = 0; + requestUpdate(); + return; + } + + startWifiScan(true); +} + +void WifiSelectionActivity::showNetworkListFromAutoConnect() { + WiFi.disconnect(); + autoConnecting = false; + manualNetworkListRequested = true; + + if (networks.empty()) { + startWifiScan(false); + return; + } + + state = WifiSelectionState::NETWORK_LIST; + selectedNetworkIndex = 0; + requestUpdate(); +} + void WifiSelectionActivity::attemptConnection() { state = autoConnecting ? WifiSelectionState::AUTO_CONNECTING : WifiSelectionState::CONNECTING; connectionStartTime = millis(); @@ -418,6 +582,11 @@ void WifiSelectionActivity::attemptConnection() { sConnectionAttemptLoggingActive = true; #endif + // Scan all channels so networks with multiple APs use the strongest matching + // BSSID instead of the first match found by the framework's default fast scan. + WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); + WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); + // Set hostname so routers show "CrossPoint-Reader-AABBCCDDEEFF" instead of "esp32-XXXXXXXXXXXX" String mac = WiFi.macAddress(); mac.replace(":", ""); @@ -461,8 +630,17 @@ void WifiSelectionActivity::checkConnectionStatus() { #endif LOG_INF("WIFI", "Connected to ssid=%s ip=%s rssi=%d", selectedSSID.c_str(), connectedIP.c_str(), WiFi.RSSI()); - // Sync RTC from NTP on the first successful WiFi connection only. The DS3231 - // drifts ~2 ppm so one sync is enough; users can force a re-sync from +#if defined(ENABLE_SERIAL_LOG) && LOG_LEVEL >= 2 + uint8_t connectedBssid[6] = {}; + WiFi.BSSID(connectedBssid); + LOG_DBG("WIFI", "Connected BSSID: %02x:%02x:%02x:%02x:%02x:%02x, channel: %d, RSSI: %d dBm", + static_cast(connectedBssid[0]), static_cast(connectedBssid[1]), + static_cast(connectedBssid[2]), static_cast(connectedBssid[3]), + static_cast(connectedBssid[4]), static_cast(connectedBssid[5]), WiFi.channel(), + WiFi.RSSI()); +#endif + + // Sync RTC from NTP on the first successful WiFi connection only. Users can force a re-sync from // Settings > System > Device > Sync Date/Time Now. if (halClock.isAvailable() && (!SETTINGS.clockHasBeenSynced || !SETTINGS.clockDateHasBeenSynced)) { if (halClock.syncFromNTP()) { @@ -493,7 +671,6 @@ void WifiSelectionActivity::checkConnectionStatus() { "completing immediately"); onComplete(true); } else { - LOG_DBG("WIFI", "Connected from manual network settings, showing connected status"); state = WifiSelectionState::CONNECTED; requestUpdate(); } @@ -515,13 +692,18 @@ void WifiSelectionActivity::checkConnectionStatus() { } sConnectionAttemptLoggingActive = false; #endif + if (autoConnecting) { + handleAutoConnectFailure(); + return; + } state = WifiSelectionState::CONNECTION_FAILED; requestUpdate(); return; } // Check for timeout - if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) { + const unsigned long timeoutMs = autoConnecting ? AUTO_CONNECTION_TIMEOUT_MS : CONNECTION_TIMEOUT_MS; + if (millis() - connectionStartTime > timeoutMs) { WiFi.disconnect(); connectionError = tr(STR_ERROR_CONNECTION_TIMEOUT); LOG_INF("WIFI", "Connection timed out: ssid=%s elapsed=%lums lastStatus=%d/%s", selectedSSID.c_str(), @@ -533,6 +715,10 @@ void WifiSelectionActivity::checkConnectionStatus() { } sConnectionAttemptLoggingActive = false; #endif + if (autoConnecting) { + handleAutoConnectFailure(); + return; + } state = WifiSelectionState::CONNECTION_FAILED; requestUpdate(); return; @@ -540,13 +726,59 @@ void WifiSelectionActivity::checkConnectionStatus() { } void WifiSelectionActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + switch (state) { + case WifiSelectionState::SCANNING: +#ifndef SIMULATOR + sConnectionAttemptLoggingActive = false; +#endif + WiFi.scanDelete(); + onComplete(false); + return; + case WifiSelectionState::CONNECTING: + case WifiSelectionState::AUTO_CONNECTING: +#ifndef SIMULATOR + sConnectionAttemptLoggingActive = false; +#endif + WiFi.disconnect(); + onComplete(false); + return; + case WifiSelectionState::SAVE_PROMPT: + case WifiSelectionState::CONNECTED: + onComplete(true); + return; + case WifiSelectionState::FORGET_PROMPT: + startWifiScan(); + return; + case WifiSelectionState::CONNECTION_FAILED: + if (autoConnecting || usedSavedPassword) { + autoConnecting = false; + state = WifiSelectionState::FORGET_PROMPT; + forgetPromptSelection = 0; + } else { + state = WifiSelectionState::NETWORK_LIST; + } + requestUpdate(); + return; + case WifiSelectionState::NETWORK_LIST: + onComplete(false); + return; + default: + break; + } + } + if ((state == WifiSelectionState::SCANNING || state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) && mappedInput.wasPressed(MappedInputManager::Button::Back)) { #ifndef SIMULATOR sConnectionAttemptLoggingActive = false; #endif - WiFi.disconnect(); + if (state == WifiSelectionState::SCANNING) { + WiFi.scanDelete(); + } else { + WiFi.disconnect(); + } mappedInput.suppressNextBackRelease(); onComplete(false); return; @@ -554,16 +786,42 @@ void WifiSelectionActivity::loop() { // Check scan progress if (state == WifiSelectionState::SCANNING) { + if (autoConnecting && mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + autoConnecting = false; + manualNetworkListRequested = true; + requestUpdate(); + } processWifiScanResults(); return; } // Check connection progress if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) { + if (state == WifiSelectionState::AUTO_CONNECTING) { + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + showNetworkListFromAutoConnect(); + return; + } + } checkConnectionStatus(); return; } + // Reached once the hidden-network SSID has been entered (and was non-empty). + if (state == WifiSelectionState::HIDDEN_SSID_ENTRY) { + const auto savedCred = WIFI_STORE.findCredential(selectedSSID); + if (savedCred && !savedCred->password.empty()) { + // We already know this hidden network - connect with the saved password + enteredPassword = savedCred->password; + usedSavedPassword = true; + attemptConnection(); + } else { + // Prompt for the password (empty password connects to open hidden APs) + promptPasswordEntry(); + } + return; + } + if (state == WifiSelectionState::PASSWORD_ENTRY) { // Reach here once password entry finished in subactivity attemptConnection(); @@ -572,6 +830,34 @@ void WifiSelectionActivity::loop() { // Handle save prompt state if (state == WifiSelectionState::SAVE_PROMPT) { + { + const Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const auto height = renderer.getLineHeight(UI_10_FONT_ID); + const int buttonY = screen.y + (screen.height - height * 3) / 2 + 80; + constexpr int buttonWidth = 60; + constexpr int buttonSpacing = 30; + const int startX = screen.x + (screen.width - (buttonWidth * 2 + buttonSpacing)) / 2; + int touchedOption = -1; + const auto touch = mappedInput.colTouch(touchedOption, startX - 8, buttonWidth + buttonSpacing, 2, buttonY - 8, + buttonY + height + 8, buttonWidth + 16); + if (touch == MappedInputManager::RowTouch::Down) { + if (savePromptSelection != touchedOption) { + savePromptSelection = touchedOption; + requestUpdate(); + } + return; + } + if (touch == MappedInputManager::RowTouch::Tap) { + savePromptSelection = touchedOption; + if (savePromptSelection == 0) { + RenderLock lock(*this); + WIFI_STORE.addCredential(selectedSSID, enteredPassword); + } + onComplete(true); + return; + } + } + if (mappedInput.wasPressed(MappedInputManager::Button::Up) || mappedInput.wasPressed(MappedInputManager::Button::Left)) { if (savePromptSelection > 0) { @@ -602,6 +888,39 @@ void WifiSelectionActivity::loop() { // Handle forget prompt state (connection failed with saved credentials) if (state == WifiSelectionState::FORGET_PROMPT) { + { + const Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const auto height = renderer.getLineHeight(UI_10_FONT_ID); + const int buttonY = screen.y + (screen.height - height * 3) / 2 + 80; + constexpr int buttonWidth = 120; + constexpr int buttonSpacing = 30; + const int startX = screen.x + (screen.width - (buttonWidth * 2 + buttonSpacing)) / 2; + int touchedOption = -1; + const auto touch = mappedInput.colTouch(touchedOption, startX - 8, buttonWidth + buttonSpacing, 2, buttonY - 8, + buttonY + height + 8, buttonWidth + 16); + if (touch == MappedInputManager::RowTouch::Down) { + if (forgetPromptSelection != touchedOption) { + forgetPromptSelection = touchedOption; + requestUpdate(); + } + return; + } + if (touch == MappedInputManager::RowTouch::Tap) { + forgetPromptSelection = touchedOption; + if (forgetPromptSelection == 1) { + RenderLock lock(*this); + WIFI_STORE.removeCredential(selectedSSID); + const auto network = find_if(networks.begin(), networks.end(), + [this](const WifiNetworkInfo& net) { return net.ssid == selectedSSID; }); + if (network != networks.end()) { + network->hasSavedPassword = false; + } + } + startWifiScan(); + return; + } + } + if (mappedInput.wasPressed(MappedInputManager::Button::Up) || mappedInput.wasPressed(MappedInputManager::Button::Left)) { if (forgetPromptSelection > 0) { @@ -703,15 +1022,42 @@ void WifiSelectionActivity::loop() { } } - // Handle navigation - buttonNavigator.onNext([this] { - selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size()); - requestUpdate(); - }); + // Touch goes through the FreeInkApp: render() registered the row hit + // rects; route the snapshot and let onRowEvent dispatch. + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; // dispatched to onRowEvent + } + } + + if (!networks.empty()) { + // Swipes scroll the viewport; the selection stays put and button + // navigation pulls the view back to it. + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, static_cast(networks.size())); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + } - buttonNavigator.onPrevious([this] { - selectedNetworkIndex = ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size()); + const auto moveSelection = [this](const int index) { + selectedNetworkIndex = static_cast(index); + topIndex = followListSelection(static_cast(selectedNetworkIndex), topIndex, visibleRows, + static_cast(networks.size())); requestUpdate(); + }; + buttonNavigator.onNext( + [this, &moveSelection] { moveSelection(ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size())); }); + buttonNavigator.onPrevious([this, &moveSelection] { + moveSelection(ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size())); }); } } @@ -731,9 +1077,9 @@ std::string WifiSelectionActivity::getSignalStrengthIndicator(const int32_t rssi } void WifiSelectionActivity::render(RenderLock&&) { - // Don't render if we're in PASSWORD_ENTRY state - we're just transitioning + // Don't render if we're in a keyboard-entry state - we're just transitioning // from the keyboard subactivity back to the main activity - if (state == WifiSelectionState::PASSWORD_ENTRY) { + if (state == WifiSelectionState::PASSWORD_ENTRY || state == WifiSelectionState::HIDDEN_SSID_ENTRY) { return; } @@ -744,14 +1090,21 @@ void WifiSelectionActivity::render(RenderLock&&) { Rect screen = theme.getScreenSafeArea(renderer, true, false); // Draw header - char countStr[32]; - snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size()); - GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, - tr(STR_WIFI_NETWORKS), countStr); - GUI.drawSubHeader( - renderer, - Rect{screen.x, screen.y + metrics.topPadding + metrics.headerHeight, screen.width, metrics.tabBarHeight}, - cachedMacAddress.c_str()); + // Translated labels can exceed the English byte count in UTF-8 (Arabic is + // currently about 37 bytes), so leave room without allocating on the heap. + char countStr[64]; + snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), realNetworkCount); + const Rect header{screen.x, screen.y + metrics.topPadding, screen.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_WIFI_NETWORKS), false, 150, countStr); + } else { + GUI.drawHeader(renderer, header, tr(STR_WIFI_NETWORKS), countStr); + } + GUI.drawSubHeader(renderer, + Rect{screen.x, screen.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput), + screen.width, metrics.tabBarHeight}, + cachedMacAddress.c_str()); switch (state) { case WifiSelectionState::AUTO_CONNECTING: @@ -763,6 +1116,9 @@ void WifiSelectionActivity::render(RenderLock&&) { case WifiSelectionState::NETWORK_LIST: renderNetworkList(&screen, &metrics); break; + case WifiSelectionState::HIDDEN_SSID_ENTRY: + // Transitioning to/from the SSID keyboard subactivity - nothing to draw + break; case WifiSelectionState::CONNECTING: renderConnecting(&screen, &metrics); break; @@ -782,28 +1138,70 @@ void WifiSelectionActivity::render(RenderLock&&) { break; // Handled by early return above } - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); +} + +void WifiSelectionActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} + +void WifiSelectionActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + // Content below the header + MAC sub-band, above the legend line. + screen.setContentMargin(fui::Insets{ + static_cast(safe.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + + metrics.tabBarHeight + metrics.verticalSpacing), + static_cast(renderer.getScreenWidth() - (safe.x + safe.width)), + static_cast(renderer.getScreenHeight() - (safe.y + safe.height) + metrics.verticalSpacing * 2), + static_cast(safe.x)}); + + if (networks.empty()) { + screen.centeredText(tr(STR_NO_NETWORKS), screen.theme().bodyText); + return; + } + + // Per-render owned status strings ("+ * ||||"); items point into them for + // the draw only. + std::vector statuses(networks.size()); + std::vector items; + items.reserve(networks.size()); + for (size_t i = 0; i < networks.size(); i++) { + const auto& network = networks[i]; + if (!network.isHiddenPlaceholder) { + statuses[i] = std::string(network.hasSavedPassword ? "+ " : "") + (network.isEncrypted ? "* " : "") + + getSignalStrengthIndicator(network.rssi); + } + fui::ListItem item; + item.label = network.isHiddenPlaceholder ? tr(STR_ADD_HIDDEN_NETWORK) : network.ssid.c_str(); + if (!statuses[i].empty()) item.value = statuses[i].c_str(); + item.actionValue = static_cast(i); + items.push_back(item); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedNetworkIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch | fui::InputLongPress; + props.valueInset = 8; // air between the signal bars and the row edge + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(networks.size())); // clamp to range + props.topIndex = static_cast(topIndex); + screen.list(props); } -void WifiSelectionActivity::renderNetworkList(const Rect* screen, const ThemeMetrics* metrics) const { +void WifiSelectionActivity::renderNetworkList(const Rect* screen, const ThemeMetrics* metrics) { + uiReady = false; + app.render(); + uiReady = true; if (networks.empty()) { - // No networks found or scan failed + // Below the centered "no networks" line the app drew. const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = screen->y + (screen->height - height) / 2; - UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, tr(STR_NO_NETWORKS)); UITheme::drawCenteredText(renderer, *screen, SMALL_FONT_ID, top + height + 10, tr(STR_PRESS_OK_SCAN)); - } else { - int contentTop = - screen->y + metrics->topPadding + metrics->headerHeight + metrics->tabBarHeight + metrics->verticalSpacing; - int contentHeight = screen->height - contentTop - metrics->verticalSpacing * 2; - GUI.drawList( - renderer, Rect{screen->x, contentTop, screen->width, contentHeight}, static_cast(networks.size()), - selectedNetworkIndex, [this](int index) { return networks[index].ssid; }, nullptr, nullptr, - [this](int index) { - auto network = networks[index]; - return std::string(network.hasSavedPassword ? "+ " : "") + (network.isEncrypted ? "* " : "") + - getSignalStrengthIndicator(network.rssi); - }); } GUI.drawHelpText(renderer, @@ -814,7 +1212,7 @@ void WifiSelectionActivity::renderNetworkList(const Rect* screen, const ThemeMet const char* forgetLabel = hasSavedPassword ? tr(STR_FORGET_BUTTON) : ""; const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_CONNECT), forgetLabel, tr(STR_RETRY)); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, useReaderButtonHints); } void WifiSelectionActivity::renderConnecting(const Rect* screen, const ThemeMetrics* metrics) const { @@ -822,20 +1220,36 @@ void WifiSelectionActivity::renderConnecting(const Rect* screen, const ThemeMetr const auto top = screen->y + (screen->height - height) / 2; if (state == WifiSelectionState::SCANNING) { - UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, tr(STR_SCANNING)); + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, + autoConnecting ? tr(STR_FINDING_SAVED_WIFI) : tr(STR_SCANNING)); + if (autoConnecting) { + const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_SHOW_NETWORKS), "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, useReaderButtonHints); + } } else { - UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 40, tr(STR_CONNECTING), true, + UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 40, + autoConnecting ? tr(STR_CONNECTING_SAVED_WIFI) : tr(STR_CONNECTING), true, EpdFontFamily::BOLD); - std::string ssidInfo = std::string(tr(STR_TO_PREFIX)) + selectedSSID; - if (ssidInfo.length() > 25) { - ssidInfo.replace(22, ssidInfo.length() - 22, "..."); + const std::string ssidInfo = std::string(tr(STR_TO_PREFIX)) + selectedSSID; + const int textWidth = std::max(1, screen->width - metrics->contentSidePadding * 2); + const auto ssidLines = renderer.wrappedText(UI_10_FONT_ID, ssidInfo.c_str(), textWidth, 3); + const int ssidBlockHeight = static_cast(ssidLines.size()) * height; + int ssidY = top - ssidBlockHeight / 2 + height / 2; + for (const auto& line : ssidLines) { + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, ssidY, line.c_str()); + ssidY += height; + } + if (autoConnecting) { + const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_SHOW_NETWORKS), "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, useReaderButtonHints); } - UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, ssidInfo.c_str()); } - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + if (!autoConnecting) { + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, useReaderButtonHints); + } } void WifiSelectionActivity::renderConnected(const Rect* screen, const ThemeMetrics* metrics) const { @@ -855,7 +1269,7 @@ void WifiSelectionActivity::renderConnected(const Rect* screen, const ThemeMetri // Use centralized button hints const auto labels = mappedInput.mapLabels("", tr(STR_DONE), "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, useReaderButtonHints); } void WifiSelectionActivity::renderSavePrompt(const Rect* screen, const ThemeMetrics* metrics) const { @@ -897,20 +1311,26 @@ void WifiSelectionActivity::renderSavePrompt(const Rect* screen, const ThemeMetr // Use centralized button hints const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, useReaderButtonHints); } void WifiSelectionActivity::renderConnectionFailed(const Rect* screen, const ThemeMetrics* metrics) const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); - const auto top = screen->y + (screen->height - height * 2) / 2; + const int messageWidth = screen->width - metrics->contentSidePadding * 2; + const auto errorLines = renderer.wrappedText(UI_10_FONT_ID, connectionError.c_str(), messageWidth, 3); + const auto top = screen->y + (screen->height - height * (1 + errorLines.size())) / 2; UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 20, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD); - UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top + 20, connectionError.c_str()); + int errorY = top + height + 10; + for (const auto& line : errorLines) { + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, errorY, line.c_str()); + errorY += height; + } // Use centralized button hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_DONE), "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, useReaderButtonHints); } void WifiSelectionActivity::renderForgetPrompt(const Rect* screen, const ThemeMetrics* metrics) const { @@ -953,7 +1373,7 @@ void WifiSelectionActivity::renderForgetPrompt(const Rect* screen, const ThemeMe // Use centralized button hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, useReaderButtonHints); } void WifiSelectionActivity::onComplete(const bool connected) { diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index 211b4f3540..4554eaeff1 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -1,5 +1,9 @@ #pragma once +#include +#include + +#include #include #include #include @@ -7,17 +11,20 @@ #include #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" #include "util/ButtonNavigator.h" struct Rect; struct ThemeMetrics; +struct WifiCredential; // Structure to hold WiFi network information struct WifiNetworkInfo { std::string ssid; int32_t rssi; bool isEncrypted; - bool hasSavedPassword; // Whether we have saved credentials for this network + bool hasSavedPassword; // Whether we have saved credentials for this network + bool isHiddenPlaceholder = false; // Synthetic "Add hidden network..." list entry }; // WiFi selection states @@ -25,6 +32,7 @@ enum class WifiSelectionState { AUTO_CONNECTING, // Trying to connect to the last known network SCANNING, // Scanning for networks NETWORK_LIST, // Displaying available networks + HIDDEN_SSID_ENTRY, // Entering SSID for a hidden network PASSWORD_ENTRY, // Entering password for selected network CONNECTING, // Attempting to connect CONNECTED, // Successfully connected @@ -50,6 +58,8 @@ class WifiSelectionActivity final : public Activity { WifiSelectionState state = WifiSelectionState::SCANNING; size_t selectedNetworkIndex = 0; std::vector networks; + // Number of real (scanned) networks, excluding the synthetic hidden-network entry + size_t realNetworkCount = 0; // Selected network for connection std::string selectedSSID; @@ -71,10 +81,20 @@ class WifiSelectionActivity final : public Activity { // Whether to attempt auto-connect on entry const bool allowAutoConnect; - // Whether we are attempting to auto-connect + // Reader flows keep the reader orientation while WiFi is selected. Button + // hint text needs the same inverted portrait treatment in that context. + const bool useReaderButtonHints; + + // Whether we are attempting to auto-connect or auto-scan saved networks. bool autoConnecting = false; bool tearDownWifiOnExit = false; + // True when the user stopped auto-connect and asked to see the scan result. + bool manualNetworkListRequested = false; + + // Saved SSIDs already attempted during the current auto-connect session. + std::vector autoAttemptedSsids; + // Save/forget prompt selection (0 = Yes, 1 = No) int savePromptSelection = 0; int forgetPromptSelection = 0; @@ -82,11 +102,28 @@ class WifiSelectionActivity final : public Activity { // Connection timeout static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000; static constexpr unsigned long CONNECTION_STATUS_LOG_INTERVAL_MS = 2000; + static constexpr unsigned long AUTO_CONNECTION_TIMEOUT_MS = 7000; unsigned long connectionStartTime = 0; unsigned long lastConnectionStatusLogTime = 0; int lastLoggedWifiStatus = -1; - void renderNetworkList(const Rect* screen, const ThemeMetrics* metrics) const; + // FreeInkApp hosts the network list (themed rows, touch routing); every + // other state keeps its legacy centered-text rendering. + using UiApp = freeink::ui::FreeInkApp<20, 4>; + freeink::ui::GfxRendererTarget uiTarget; // must precede `app`: the app holds a reference to it + UiApp app; + // render() rebuilds the app's interaction table; loop() only routes touch + // snapshots against it while this is true (the two run on different tasks). + std::atomic uiReady{false}; + int visibleRows = 1; // rows per page at the current scale; set by the screen builder + int topIndex = 0; // viewport scroll position, decoupled from the selection + ScreenTransitionRefresh screenTransitionRefresh; + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); + + void renderNetworkList(const Rect* screen, const ThemeMetrics* metrics); void renderPasswordEntry(const Rect* screen, const ThemeMetrics* metrics) const; void renderConnecting(const Rect* screen, const ThemeMetrics* metrics) const; void renderConnected(const Rect* screen, const ThemeMetrics* metrics) const; @@ -94,18 +131,26 @@ class WifiSelectionActivity final : public Activity { void renderConnectionFailed(const Rect* screen, const ThemeMetrics* metrics) const; void renderForgetPrompt(const Rect* screen, const ThemeMetrics* metrics) const; - void startWifiScan(); + void startWifiScan(bool autoScan = false); void processWifiScanResults(); + void appendHiddenNetworkEntry(); void selectNetwork(int index); + void promptHiddenSsid(); + void promptPasswordEntry(); void attemptConnection(); void checkConnectionStatus(); + bool tryAutoConnectCredential(const WifiCredential& cred); + bool tryNextSavedNetworkFromScan(); + void handleAutoConnectFailure(); + void showNetworkListFromAutoConnect(); + bool hasAttemptedAutoSsid(const std::string& ssid) const; std::string getSignalStrengthIndicator(int32_t rssi) const; void onComplete(bool connected); public: - explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool autoConnect = true) - : Activity("WifiSelection", renderer, mappedInput), allowAutoConnect(autoConnect) {} + explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool autoConnect = true, + bool useReaderButtonHints = false); void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/reader/BookReadingStats.h b/src/activities/reader/BookReadingStats.h index f495c0e4ed..d762f8b43d 100644 --- a/src/activities/reader/BookReadingStats.h +++ b/src/activities/reader/BookReadingStats.h @@ -36,7 +36,7 @@ struct BookReadingStats { // Updates the running reading pace with one forward page dwell sample. void recordForwardPageRead(uint32_t seconds); - // Attributes reading time to the X3 local date/time buckets when RTC data exists. + // Attributes reading time to local date/time buckets when RTC data exists. void recordReadingSpan(const ReadingStatsDateTime& localStart, uint32_t seconds); // Formats a duration in seconds into a human-readable string. diff --git a/src/activities/reader/BookStatsActivity.cpp b/src/activities/reader/BookStatsActivity.cpp index f495ea68de..6810c04a01 100644 --- a/src/activities/reader/BookStatsActivity.cpp +++ b/src/activities/reader/BookStatsActivity.cpp @@ -4,6 +4,7 @@ #include "BookStatsView.h" #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" BookStatsActivity::BookStatsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const std::string& bookCachePath, const BookReadingStats& stats, @@ -217,21 +218,18 @@ void BookStatsActivity::adjustSelectedDateField(const int delta) { void BookStatsActivity::onEnter() { Activity::onEnter(); + previousOrientation = renderer.getOrientation(); + renderer.setOrientation(GfxRenderer::Orientation::Portrait); requestUpdate(); } void BookStatsActivity::onExit() { saveStats(); + renderer.setOrientation(previousOrientation); Activity::onExit(); } -void BookStatsActivity::exitStatsActivity(const bool viaBack) { - if (viaBack) { - mappedInput.suppressNextBackRelease(); - } else { - mappedInput.suppressNextConfirmRelease(); - } - +void BookStatsActivity::exitStatsActivity() { if (returnToHomeOnExit) { onGoHome(); return; @@ -240,25 +238,101 @@ void BookStatsActivity::exitStatsActivity(const bool viaBack) { finish(); } +bool BookStatsActivity::showNextStatsPage() { + if (page == Page::PerBook) { + page = Page::ThisDevice; + requestUpdate(); + return true; + } + + if (page == Page::ThisDevice && showAllDevicesStats) { + page = Page::AllDevices; + requestUpdate(); + return true; + } + + return false; +} + +bool BookStatsActivity::showPreviousStatsPage() { + if (page == Page::AllDevices) { + page = Page::ThisDevice; + requestUpdate(); + return true; + } + + if (page == Page::ThisDevice) { + page = Page::PerBook; + requestUpdate(); + return true; + } + + return false; +} + +bool BookStatsActivity::selectEditFieldFromTouchTarget(const int touchTarget) { + if (touchTarget < BookStatsTouchTarget::DateFieldBase || + touchTarget >= BookStatsTouchTarget::DateFieldBase + BookStatsTouchTarget::DateFieldCount) { + return false; + } + + const int newEditField = touchTarget - BookStatsTouchTarget::DateFieldBase; + if (selectedEditField != newEditField) { + selectedEditField = newEditField; + requestUpdate(); + } + return true; +} + void BookStatsActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, TouchHeaderBackButton::compactHeaderRect(renderer))) { + if (page == Page::EditDates) { + saveStats(); + page = Page::PerBook; + requestUpdate(); + } else { + exitStatsActivity(); + } + return; + } if (usesNoRtcSingleScreenLayout()) { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - exitStatsActivity(true); + mappedInput.suppressNextBackRelease(); + exitStatsActivity(); return; } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - exitStatsActivity(false); + mappedInput.suppressNextConfirmRelease(); + exitStatsActivity(); return; } return; } - const bool editShortcutPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left); - const bool moreShortcutPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right); + const bool upOrLeftPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || + mappedInput.wasPressed(MappedInputManager::Button::Left); + const bool downOrRightPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || + mappedInput.wasPressed(MappedInputManager::Button::Right); if (page == Page::EditDates) { + int touchedTarget = -1; + if (mappedInput.wasItemTouchedDown(touchedTarget) && selectEditFieldFromTouchTarget(touchedTarget)) { + return; + } + int tappedTarget = -1; + if (mappedInput.wasItemTapped(tappedTarget)) { + if (selectEditFieldFromTouchTarget(tappedTarget)) { + return; + } + if (tappedTarget == BookStatsTouchTarget::DateAdjustUp) { + adjustSelectedDateField(1); + return; + } + if (tappedTarget == BookStatsTouchTarget::DateAdjustDown) { + adjustSelectedDateField(-1); + return; + } + } if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { saveStats(); page = Page::PerBook; @@ -284,40 +358,60 @@ void BookStatsActivity::loop() { } if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - if (page == Page::PerBook) { - exitStatsActivity(true); - } else if (page == Page::ThisDevice) { - page = Page::PerBook; - requestUpdate(); - } else if (page == Page::AllDevices) { - page = Page::ThisDevice; + mappedInput.suppressNextBackRelease(); + exitStatsActivity(); + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + mappedInput.suppressNextConfirmRelease(); + if (page == Page::PerBook && hasEditableBook()) { + page = Page::EditDates; requestUpdate(); + return; } + exitStatsActivity(); return; } - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - exitStatsActivity(false); + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Left && showNextStatsPage()) { + return; + } + if (swipe == MappedInputManager::SwipeDir::Right) { + if (!showPreviousStatsPage()) { + exitStatsActivity(); + } return; } if (page == Page::PerBook) { - if (hasEditableBook() && editShortcutPressed) { + int touchedTarget = -1; + if (hasEditableBook() && mappedInput.wasItemTapped(touchedTarget) && + touchedTarget == BookStatsTouchTarget::StartedDaysStat) { page = Page::EditDates; requestUpdate(); return; } - if (moreShortcutPressed) { - page = Page::ThisDevice; + if (hasEditableBook() && upOrLeftPressed) { + page = Page::EditDates; requestUpdate(); return; } + if (downOrRightPressed) { + showNextStatsPage(); + return; + } return; } - if (page == Page::ThisDevice && showAllDevicesStats && moreShortcutPressed) { - page = Page::AllDevices; - requestUpdate(); + if (upOrLeftPressed) { + showPreviousStatsPage(); + return; + } + + if (page == Page::ThisDevice && showAllDevicesStats && downOrRightPressed) { + showNextStatsPage(); } } diff --git a/src/activities/reader/BookStatsActivity.h b/src/activities/reader/BookStatsActivity.h index fda8d74ffb..941f82691a 100644 --- a/src/activities/reader/BookStatsActivity.h +++ b/src/activities/reader/BookStatsActivity.h @@ -21,6 +21,7 @@ class BookStatsActivity final : public Activity { float progressPercent = -1.0f; bool hasEstimatedTimeLeft = false; uint32_t estimatedTimeLeftSeconds = 0; + GfxRenderer::Orientation previousOrientation = GfxRenderer::Orientation::Portrait; Page page = Page::PerBook; int selectedEditField = 0; bool didChangeStats = false; @@ -36,7 +37,10 @@ class BookStatsActivity final : public Activity { void clearEditedDate(bool finishedField); bool shouldClearDateOnAdjust(const ReadingStatsDate& date, bool finishedField, int fieldIndex, int delta) const; void normalizeEditedDates(const bool editedFinishedField); - void exitStatsActivity(bool viaBack); + void exitStatsActivity(); + bool showPreviousStatsPage(); + bool showNextStatsPage(); + bool selectEditFieldFromTouchTarget(int touchTarget); public: BookStatsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, diff --git a/src/activities/reader/BookStatsView.cpp b/src/activities/reader/BookStatsView.cpp index d424c6ef6e..d97abc5aec 100644 --- a/src/activities/reader/BookStatsView.cpp +++ b/src/activities/reader/BookStatsView.cpp @@ -1,5 +1,6 @@ #include "BookStatsView.h" +#include #include #include #include @@ -10,7 +11,10 @@ #include "MappedInputManager.h" #include "components/CompactHeader.h" +#include "components/TouchHeaderBackButton.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" +#include "components/icons/listIcons.h" #include "fontIds.h" namespace { @@ -131,7 +135,10 @@ const StatsLayout& getStatsLayout(const GfxRenderer& renderer, const bool global const auto& metrics = UITheme::getInstance().getMetrics(); const int availableHeight = renderer.getScreenHeight() - metrics.topPadding - statsBottomInset(metrics, showButtonHints); - if (statsContentHeight(kDefaultLayout, globalPage, showRtcStats) <= availableHeight) { + const bool defaultFitsCurrentPage = statsContentHeight(kDefaultLayout, globalPage, showRtcStats) <= availableHeight; + const bool defaultMatchesPerBookCharts = + !globalPage || !showRtcStats || statsContentHeight(kDefaultLayout, false, showRtcStats) <= availableHeight; + if (defaultFitsCurrentPage && defaultMatchesPerBookCharts) { return kDefaultLayout; } return kCompactLayout; @@ -148,25 +155,6 @@ const StatsLayout& getNoRtcCombinedLayout(const GfxRenderer& renderer, const boo return kCompactLayout; } -void formatCompactEstimate(const uint32_t seconds, char* buf, const size_t len) { - if (seconds < 60) { - snprintf(buf, len, "<1m"); - return; - } - const uint32_t minutes = (seconds + 30u) / 60u; - if (minutes < 60) { - snprintf(buf, len, "%lum", static_cast(minutes)); - return; - } - const uint32_t hours = minutes / 60u; - const uint32_t remainder = minutes % 60u; - if (remainder == 0) { - snprintf(buf, len, "%luh", static_cast(hours)); - } else { - snprintf(buf, len, "%luh %lum", static_cast(hours), static_cast(remainder)); - } -} - bool fallbackEstimatedTimeLeft(const BookReadingStats& stats, const float progressPercent, uint32_t& seconds) { seconds = 0; if (progressPercent <= 0.0f || progressPercent >= 100.0f || stats.totalReadingSeconds < 120) { @@ -326,10 +314,10 @@ void drawPerBookStatsCard(GfxRenderer& renderer, const int x, const int y, const const bool hasCachedEstimate = cachedEstimatedTimeLeft(stats, cachedEstimateSeconds); const bool hasFallbackEstimate = fallbackEstimatedTimeLeft(stats, progressPercent, fallbackEstimateSeconds); if (!stats.isCompleted && (hasEstimatedTimeLeft || hasCachedEstimate || hasFallbackEstimate)) { - formatCompactEstimate(hasEstimatedTimeLeft ? estimatedTimeLeftSeconds - : hasCachedEstimate ? cachedEstimateSeconds - : fallbackEstimateSeconds, - buf, sizeof(buf)); + formatCompactReadingDuration(hasEstimatedTimeLeft ? estimatedTimeLeftSeconds + : hasCachedEstimate ? cachedEstimateSeconds + : fallbackEstimateSeconds, + buf, sizeof(buf)); } else { snprintf(buf, sizeof(buf), "-"); } @@ -359,7 +347,10 @@ void drawPerBookStatsCard(GfxRenderer& renderer, const int x, const int y, const char dateBuf[24]; formatReadingStatsShortDate(stats.startDate, dateBuf, sizeof(dateBuf)); snprintf(startedLabel, sizeof(startedLabel), "%s %s", tr(STR_STATS_STARTED), dateBuf); - drawStatCell(renderer, x, halfW, y + layout.topCardTitleH + rowH * 2, rowH, buf, startedLabel); + const int startedY = y + layout.topCardTitleH + rowH * 2; + TouchRegistry::getInstance().add(Rect(x, startedY, halfW, rowH), BookStatsTouchTarget::StartedDaysStat, + TouchRegistry::Item); + drawStatCell(renderer, x, halfW, startedY, rowH, buf, startedLabel); ReadingStatsDate finishDisplayDate; bool finished = stats.isCompleted; @@ -433,8 +424,11 @@ void drawGlobalStatsCard(GfxRenderer& renderer, const int x, const int y, const } void drawDateField(const GfxRenderer& renderer, const int x, const int y, const int w, const char* text, - const bool selected) { + const bool selected, const int touchTarget = -1) { const int h = renderer.getLineHeight(UI_12_FONT_ID) + 10; + if (touchTarget >= 0) { + TouchRegistry::getInstance().add(Rect(x, y, w, h), touchTarget, TouchRegistry::Item); + } renderer.fillRectDither(x, y, w, h, selected ? Color::LightGray : Color::White); renderer.drawRect(x, y, w, h, true); if (selected) { @@ -442,6 +436,18 @@ void drawDateField(const GfxRenderer& renderer, const int x, const int y, const } drawCenteredLabel(renderer, UI_12_FONT_ID, x, w, y + 5, text); } + +void drawDateAdjustButton(const GfxRenderer& renderer, const int x, const int y, const int size, + const freeink::Icon& icon, const int touchTarget) { + TouchRegistry::getInstance().add(Rect(x, y, size, size), touchTarget, TouchRegistry::Item); + renderer.drawRect(x, y, size, size, true); + const freeink::ui::BitmapRef bitmap{icon.bits, icon.w, icon.h, freeink::ui::BitmapFormat::Mask1, true}; + freeink::ui::forEachBitmapPixel( + freeink::ui::Rect{static_cast(x), static_cast(y), static_cast(size), + static_cast(size)}, + bitmap, freeink::ui::BitmapMode::Center, + [&renderer](const int16_t px, const int16_t py) { renderer.drawPixel(px, py, true); }); +} } // namespace void renderPerBookStatsPage(GfxRenderer& renderer, const MappedInputManager* mappedInput, const std::string& bookTitle, @@ -452,7 +458,11 @@ void renderPerBookStatsPage(GfxRenderer& renderer, const MappedInputManager* map const bool showRtcStats = shouldShowRtcBasedStats(); const auto& metrics = UITheme::getInstance().getMetrics(); const auto& layout = getStatsLayout(renderer, false, showButtonHints, showRtcStats); - CompactHeader::drawTitle(renderer, tr(STR_READING_STATS), true); + if (mappedInput && mappedInput->hasTouchHardware()) { + TouchHeaderBackButton::drawCompact(renderer, tr(STR_READING_STATS), true, true); + } else { + CompactHeader::drawTitle(renderer, tr(STR_READING_STATS), true); + } const int screenW = renderer.getScreenWidth(); const int cardX = metrics.contentSidePadding; const int cardW = screenW - metrics.contentSidePadding * 2; @@ -506,7 +516,7 @@ void renderPerBookStatsPage(GfxRenderer& renderer, const MappedInputManager* map } if (showButtonHints && mappedInput) { - const auto labels = mappedInput->mapLabels(tr(STR_BACK), "", showEditButton ? tr(STR_EDIT) : "", + const auto labels = mappedInput->mapLabels(tr(STR_BACK), showEditButton ? tr(STR_EDIT) : "", "", showMoreButton ? tr(STR_MORE) : ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); } @@ -518,7 +528,11 @@ void renderGlobalStatsPage(GfxRenderer& renderer, const MappedInputManager* mapp const bool showRtcStats = shouldShowRtcBasedStats(); const auto& metrics = UITheme::getInstance().getMetrics(); const auto& layout = getStatsLayout(renderer, true, showButtonHints, showRtcStats); - CompactHeader::drawTitle(renderer, screenTitle); + if (mappedInput && mappedInput->hasTouchHardware()) { + TouchHeaderBackButton::drawCompact(renderer, screenTitle, true); + } else { + CompactHeader::drawTitle(renderer, screenTitle); + } const int screenW = renderer.getScreenWidth(); const int cardX = metrics.contentSidePadding; const int cardW = screenW - metrics.contentSidePadding * 2; @@ -562,7 +576,7 @@ void renderGlobalStatsPage(GfxRenderer& renderer, const MappedInputManager* mapp } if (showButtonHints && mappedInput) { - const auto labels = mappedInput->mapLabels(tr(STR_BACK), tr(STR_HOME), "", showMoreButton ? tr(STR_MORE) : ""); + const auto labels = mappedInput->mapLabels(tr(STR_EXIT), "", tr(STR_BACK), showMoreButton ? tr(STR_MORE) : ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); } } @@ -575,7 +589,11 @@ void renderNoRtcCombinedStatsPage(GfxRenderer& renderer, const MappedInputManage renderer.clearScreen(); const auto& metrics = UITheme::getInstance().getMetrics(); const auto& layout = getNoRtcCombinedLayout(renderer, showButtonHints, allDevicesStats != nullptr); - CompactHeader::drawTitle(renderer, tr(STR_READING_STATS)); + if (mappedInput && mappedInput->hasTouchHardware()) { + TouchHeaderBackButton::drawCompact(renderer, tr(STR_READING_STATS), true); + } else { + CompactHeader::drawTitle(renderer, tr(STR_READING_STATS)); + } const int screenW = renderer.getScreenWidth(); const int cardX = metrics.contentSidePadding; const int cardW = screenW - metrics.contentSidePadding * 2; @@ -616,7 +634,11 @@ void renderNoRtcCombinedStatsPage(GfxRenderer& renderer, const MappedInputManage void renderEditBookDatesPage(GfxRenderer& renderer, const MappedInputManager* mappedInput, const std::string& bookTitle, const BookReadingStats& stats, const int selectedField, const bool showButtonHints) { renderer.clearScreen(); - CompactHeader::drawTitle(renderer, tr(STR_READING_STATS)); + if (mappedInput && mappedInput->hasTouchHardware()) { + TouchHeaderBackButton::drawCompact(renderer, tr(STR_READING_STATS), true); + } else { + CompactHeader::drawTitle(renderer, tr(STR_READING_STATS)); + } const auto& metrics = UITheme::getInstance().getMetrics(); const int pageWidth = renderer.getScreenWidth(); @@ -638,7 +660,17 @@ void renderEditBookDatesPage(GfxRenderer& renderer, const MappedInputManager* ma const int yearW = 68; const int gap = 14; const int totalFieldW = monthW + gap + dayW + gap + yearW; - const int fieldStartX = cardX + (cardW - totalFieldW) / 2; +#if CROSSINK_APP_CAP_TOUCH + const bool showTouchControls = mappedInput && mappedInput->hasTouch(); + constexpr int adjustButtonSize = 60; + constexpr int adjustButtonRightPadding = 34; + constexpr int adjustButtonGap = 24; + const int adjustButtonX = cardX + cardW - adjustButtonRightPadding - adjustButtonSize; + const int fieldAreaW = showTouchControls ? adjustButtonX - cardX - adjustButtonGap : cardW; +#else + const int fieldAreaW = cardW; +#endif + const int fieldStartX = cardX + (std::max(totalFieldW, fieldAreaW) - totalFieldW) / 2; char monthBuf[8]; char dayBuf[8]; @@ -654,9 +686,11 @@ void renderEditBookDatesPage(GfxRenderer& renderer, const MappedInputManager* ma snprintf(dayBuf, sizeof(dayBuf), "-"); snprintf(yearBuf, sizeof(yearBuf), "-"); } - drawDateField(renderer, fieldStartX, row1Y, monthW, monthBuf, selectedField == 0); - drawDateField(renderer, fieldStartX + monthW + gap, row1Y, dayW, dayBuf, selectedField == 1); - drawDateField(renderer, fieldStartX + monthW + gap + dayW + gap, row1Y, yearW, yearBuf, selectedField == 2); + drawDateField(renderer, fieldStartX, row1Y, monthW, monthBuf, selectedField == 0, BookStatsTouchTarget::dateField(0)); + drawDateField(renderer, fieldStartX + monthW + gap, row1Y, dayW, dayBuf, selectedField == 1, + BookStatsTouchTarget::dateField(1)); + drawDateField(renderer, fieldStartX + monthW + gap + dayW + gap, row1Y, yearW, yearBuf, selectedField == 2, + BookStatsTouchTarget::dateField(2)); drawCenteredLabel(renderer, UI_10_FONT_ID, cardX, cardW, cardY + 24 + sectionGap, tr(STR_STATS_FINISHED_DATE), true); const bool showFinishedFields = stats.isCompleted && stats.finishedDate.isValid(); @@ -669,9 +703,21 @@ void renderEditBookDatesPage(GfxRenderer& renderer, const MappedInputManager* ma snprintf(dayBuf, sizeof(dayBuf), "-"); snprintf(yearBuf, sizeof(yearBuf), "-"); } - drawDateField(renderer, fieldStartX, row2Y, monthW, monthBuf, selectedField == 3); - drawDateField(renderer, fieldStartX + monthW + gap, row2Y, dayW, dayBuf, selectedField == 4); - drawDateField(renderer, fieldStartX + monthW + gap + dayW + gap, row2Y, yearW, yearBuf, selectedField == 5); + drawDateField(renderer, fieldStartX, row2Y, monthW, monthBuf, selectedField == 3, BookStatsTouchTarget::dateField(3)); + drawDateField(renderer, fieldStartX + monthW + gap, row2Y, dayW, dayBuf, selectedField == 4, + BookStatsTouchTarget::dateField(4)); + drawDateField(renderer, fieldStartX + monthW + gap + dayW + gap, row2Y, yearW, yearBuf, selectedField == 5, + BookStatsTouchTarget::dateField(5)); + +#if CROSSINK_APP_CAP_TOUCH + if (showTouchControls) { + const int fieldH = renderer.getLineHeight(UI_12_FONT_ID) + 10; + drawDateAdjustButton(renderer, adjustButtonX, row1Y + (fieldH - adjustButtonSize) / 2, adjustButtonSize, + icon_chevron_up_32, BookStatsTouchTarget::DateAdjustUp); + drawDateAdjustButton(renderer, adjustButtonX, row2Y + (fieldH - adjustButtonSize) / 2, adjustButtonSize, + icon_chevron_down_32, BookStatsTouchTarget::DateAdjustDown); + } +#endif if (showButtonHints && mappedInput) { const auto labels = mappedInput->mapLabels(tr(STR_BACK), tr(STR_NEXT_FIELD), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); diff --git a/src/activities/reader/BookStatsView.h b/src/activities/reader/BookStatsView.h index 8c1f511a9f..3fa20c47e1 100644 --- a/src/activities/reader/BookStatsView.h +++ b/src/activities/reader/BookStatsView.h @@ -8,6 +8,16 @@ class GfxRenderer; class MappedInputManager; +namespace BookStatsTouchTarget { +constexpr int StartedDaysStat = 1; +constexpr int DateFieldBase = 10; +constexpr int DateFieldCount = 6; +constexpr int DateAdjustUp = 20; +constexpr int DateAdjustDown = 21; + +constexpr int dateField(const int index) { return DateFieldBase + index; } +} // namespace BookStatsTouchTarget + void renderPerBookStatsPage(GfxRenderer& renderer, const MappedInputManager* mappedInput, const std::string& bookTitle, const BookReadingStats& stats, float progressPercent, bool hasEstimatedTimeLeft, uint32_t estimatedTimeLeftSeconds, bool showButtonHints, bool showEditButton, diff --git a/src/activities/reader/ClipSelectionActivity.cpp b/src/activities/reader/ClipSelectionActivity.cpp index 224055cff7..1edc06738a 100644 --- a/src/activities/reader/ClipSelectionActivity.cpp +++ b/src/activities/reader/ClipSelectionActivity.cpp @@ -18,19 +18,17 @@ namespace { constexpr int CLIP_SELECTION_FALLBACK_FONT_ID = UI_12_FONT_ID; +constexpr unsigned long TOUCH_CLIP_HOLD_MS = 500; -bool hasEmSpace(const std::string& text) { - return text.size() >= 3 && static_cast(text[0]) == 0xE2 && - static_cast(text[1]) == 0x80 && static_cast(text[2]) == 0x83; -} +bool hasEmSpace(const char* text) { return text[0] == '\xe2' && text[1] == '\x80' && text[2] == '\x83'; } } // namespace ClipSelectionActivity::ClipSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - std::vector words, const int fontId, Section& section, + ClipWordStore wordStore, const int fontId, Section& section, const int startPageInSection, const int marginTop, const int marginLeft) : Activity("ClipSelection", renderer, mappedInput), - words(std::move(words)), + wordStore(std::move(wordStore)), renderFontId(fontId), section(section), startPageInSection(startPageInSection), @@ -40,7 +38,7 @@ ClipSelectionActivity::ClipSelectionActivity(GfxRenderer& renderer, MappedInputM void ClipSelectionActivity::onEnter() { Activity::onEnter(); - if (words.empty()) { + if (wordStore.words.empty()) { LOG_ERR("CLIP", "No words available for selection"); ActivityResult result; result.isCancelled = true; @@ -49,7 +47,7 @@ void ClipSelectionActivity::onEnter() { return; } buildReadingOrder(); - if (readingOrder.empty()) { + if (readingOrderSize == 0) { LOG_ERR("CLIP", "No readable word order available"); ActivityResult result; result.isCancelled = true; @@ -60,13 +58,6 @@ void ClipSelectionActivity::onEnter() { cursorIdx = 0; savedSectionPage = section.currentPage; - if (!allocateSavedBuffer()) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); - return; - } if (!switchToPage(0)) { ActivityResult result; @@ -80,7 +71,7 @@ void ClipSelectionActivity::onEnter() { void ClipSelectionActivity::onExit() { section.currentPage = savedSectionPage; - savedBufferChunks.clear(); + resetSavedBufferChunks(); hasSavedBuffer = false; if (usingFallbackFont) { if (auto* fcm = renderer.getFontCacheManager()) { @@ -90,29 +81,48 @@ void ClipSelectionActivity::onExit() { Activity::onExit(); } -bool ClipSelectionActivity::allocateSavedBuffer() { +void ClipSelectionActivity::allocateSavedBuffer() { savedBufferSize = renderer.getBufferSize(); const size_t chunkCount = (savedBufferSize + BUFFER_CHUNK_SIZE - 1) / BUFFER_CHUNK_SIZE; - savedBufferChunks.reserve(chunkCount); + savedBufferChunkCount = 0; + + if (chunkCount > savedBufferChunks.size()) { + LOG_ERR("CLIP", "Framebuffer snapshot needs %u chunks; using rerender fallback", static_cast(chunkCount)); + savedBufferSize = 0; + return; + } for (size_t i = 0; i < chunkCount; i++) { const size_t offset = i * BUFFER_CHUNK_SIZE; const size_t chunkSize = std::min(BUFFER_CHUNK_SIZE, savedBufferSize - offset); auto chunk = makeUniqueNoThrow(chunkSize); if (!chunk) { - LOG_ERR("CLIP", "OOM: clipping page snapshot chunk %u (%u bytes)", static_cast(i), - static_cast(chunkSize)); - savedBufferChunks.clear(); - return false; + LOG_ERR("CLIP", "OOM: clipping page snapshot chunk %u (%u bytes); using rerender fallback", + static_cast(i), static_cast(chunkSize)); + resetSavedBufferChunks(); + savedBufferSize = 0; + return; } - savedBufferChunks.push_back(std::move(chunk)); + savedBufferChunks[i] = std::move(chunk); } - return true; + savedBufferChunkCount = chunkCount; +} + +void ClipSelectionActivity::resetSavedBufferChunks() { + for (auto& chunk : savedBufferChunks) { + chunk.reset(); + } + savedBufferChunkCount = 0; } void ClipSelectionActivity::storeCurrentBuffer() { + if (savedBufferChunkCount == 0) { + hasSavedBuffer = false; + return; + } + const uint8_t* frameBuffer = renderer.getFrameBuffer(); - for (size_t i = 0; i < savedBufferChunks.size(); i++) { + for (size_t i = 0; i < savedBufferChunkCount; i++) { const size_t offset = i * BUFFER_CHUNK_SIZE; const size_t chunkSize = std::min(BUFFER_CHUNK_SIZE, savedBufferSize - offset); memcpy(savedBufferChunks[i].get(), frameBuffer + offset, chunkSize); @@ -121,8 +131,10 @@ void ClipSelectionActivity::storeCurrentBuffer() { } void ClipSelectionActivity::restoreSavedBuffer() const { + if (!hasSavedBuffer) return; + uint8_t* frameBuffer = renderer.getFrameBuffer(); - for (size_t i = 0; i < savedBufferChunks.size(); i++) { + for (size_t i = 0; i < savedBufferChunkCount; i++) { const size_t offset = i * BUFFER_CHUNK_SIZE; const size_t chunkSize = std::min(BUFFER_CHUNK_SIZE, savedBufferSize - offset); memcpy(frameBuffer + offset, savedBufferChunks[i].get(), chunkSize); @@ -130,10 +142,10 @@ void ClipSelectionActivity::restoreSavedBuffer() const { } void ClipSelectionActivity::buildReadingOrder() { - readingOrder.clear(); - readingOrder.reserve(words.size()); + readingOrderSize = 0; int lineStart = 0; + const auto& words = wordStore.words; const int total = static_cast(words.size()); while (lineStart < total) { int lineEnd = lineStart + 1; @@ -144,11 +156,21 @@ void ClipSelectionActivity::buildReadingOrder() { if (words[lineStart].lineIsRtl) { for (int i = lineEnd - 1; i >= lineStart; --i) { - readingOrder.push_back(i); + if (readingOrderSize >= readingOrder.size()) { + LOG_ERR("CLIP", "Reading order cap hit (%u words); clipping range truncated", + static_cast(readingOrder.size())); + return; + } + readingOrder[readingOrderSize++] = static_cast(i); } } else { for (int i = lineStart; i < lineEnd; ++i) { - readingOrder.push_back(i); + if (readingOrderSize >= readingOrder.size()) { + LOG_ERR("CLIP", "Reading order cap hit (%u words); clipping range truncated", + static_cast(readingOrder.size())); + return; + } + readingOrder[readingOrderSize++] = static_cast(i); } } lineStart = lineEnd; @@ -156,19 +178,44 @@ void ClipSelectionActivity::buildReadingOrder() { } void ClipSelectionActivity::loop() { - const int total = static_cast(readingOrder.size()); + const int total = static_cast(readingOrderSize); using Button = MappedInputManager::Button; auto moveCursor = [this](const int nextOrderIdx) { - if (nextOrderIdx == cursorIdx || nextOrderIdx < 0 || nextOrderIdx >= static_cast(readingOrder.size())) return; - const int previousPage = words[readingOrder[cursorIdx]].pageIdx; + if (nextOrderIdx == cursorIdx || nextOrderIdx < 0 || nextOrderIdx >= static_cast(readingOrderSize)) return; + const int previousPage = wordStore.words[readingOrder[cursorIdx]].pageIdx; cursorIdx = nextOrderIdx; - if (words[readingOrder[cursorIdx]].pageIdx != previousPage) { + if (wordStore.words[readingOrder[cursorIdx]].pageIdx != previousPage) { needsPageSwitch = true; } requestUpdate(); }; + int touchX = 0; + int touchY = 0; + if (touchDragSelecting) { + if (mappedInput.isScreenTouchHeld(touchX, touchY)) { + if (selectWordAtPoint(touchX, touchY)) requestUpdate(); + return; + } + if (mappedInput.wasScreenTouchReleased()) { + touchDragSelecting = false; + confirmSelection(); + return; + } + } else if (mappedInput.isScreenTouchLongPress(touchX, touchY, TOUCH_CLIP_HOLD_MS) && + selectWordAtPoint(touchX, touchY)) { + if (startMarkIdx == -1) startMarkIdx = cursorIdx; + touchDragSelecting = true; + requestUpdate(); + return; + } + + if (mappedInput.wasScreenTapped(touchX, touchY) && selectWordAtPoint(touchX, touchY)) { + confirmSelection(); + return; + } + buttonNavigator.onRelease({Button::Left}, [this, &moveCursor] { if (cursorIdx > 0) moveCursor(cursorIdx - 1); }); @@ -187,19 +234,7 @@ void ClipSelectionActivity::loop() { buttonNavigator.onContinuous({Button::Up}, [this, &moveCursor] { moveCursor(lineEndBackward(cursorIdx)); }); if (mappedInput.wasReleased(Button::Confirm)) { - if (startMarkIdx == -1) { - startMarkIdx = cursorIdx; - requestUpdate(); - } else { - const int from = std::min(startMarkIdx, cursorIdx); - const int to = std::max(startMarkIdx, cursorIdx); - auto result = ClipTextBuilder::build(words, readingOrder, from, to, total, startPageInSection, section.pageCount); - if (const auto paragraphIndex = section.getParagraphIndexForPage(result.sectionPage)) { - result.paragraphIndex = *paragraphIndex; - } - setResult(std::move(result)); - finish(); - } + confirmSelection(); return; } @@ -217,20 +252,51 @@ void ClipSelectionActivity::loop() { } } -void ClipSelectionActivity::render(RenderLock&&) { - if (!hasSavedBuffer) return; +bool ClipSelectionActivity::selectWordAtPoint(const int x, const int y) { + for (size_t orderIdx = 0; orderIdx < readingOrderSize; orderIdx++) { + const WordRef& word = wordStore.words[readingOrder[orderIdx]]; + if (word.pageIdx != currentDisplayPage || x < word.x || x >= word.x + word.w || y < word.y || + y >= word.y + word.h) { + continue; + } + if (cursorIdx != static_cast(orderIdx)) { + cursorIdx = static_cast(orderIdx); + requestUpdate(); + } + return true; + } + return false; +} + +void ClipSelectionActivity::confirmSelection() { + if (startMarkIdx == -1) { + startMarkIdx = cursorIdx; + requestUpdate(); + return; + } + const int total = static_cast(readingOrderSize); + const int from = std::min(startMarkIdx, cursorIdx); + const int to = std::max(startMarkIdx, cursorIdx); + auto result = + ClipTextBuilder::build(wordStore, readingOrder.data(), from, to, total, startPageInSection, section.pageCount); + if (const auto paragraphIndex = section.getParagraphIndexForPage(result.sectionPage)) { + result.paragraphIndex = *paragraphIndex; + } + setResult(std::move(result)); + finish(); +} + +void ClipSelectionActivity::render(RenderLock&&) { if (needsPageSwitch) { - switchToPage(words[readingOrder[cursorIdx]].pageIdx); + switchToPage(wordStore.words[readingOrder[cursorIdx]].pageIdx); needsPageSwitch = false; - } else { + } else if (hasSavedBuffer) { restoreSavedBuffer(); + } else if (!switchToPage(currentDisplayPage)) { + return; } - if (!prewarmHighlightedWords() && renderer.isSdCardFont(renderFontId)) { - useFallbackFont("highlight prewarm"); - if (!switchToPage(currentDisplayPage)) return; - } drawHighlights(); const auto confirmLabel = startMarkIdx == -1 ? tr(STR_SELECT) : tr(STR_DONE); @@ -250,6 +316,11 @@ bool ClipSelectionActivity::switchToPage(const int pageIdx) { return false; } + // The snapshot is almost a full framebuffer. Release it before SD-font + // prewarming so its chunks do not crowd out the contiguous glyph bitmap. + resetSavedBufferChunks(); + hasSavedBuffer = false; + if (auto* fcm = renderer.getFontCacheManager()) { bool renderWithFallback = false; { @@ -275,6 +346,10 @@ bool ClipSelectionActivity::switchToPage(const int pageIdx) { page->render(renderer, renderFontId, marginLeft, marginTop, ReaderUtils::readerForegroundBlack()); } + // The rendered page is now in the framebuffer, so its deserialized objects + // no longer need to overlap with the snapshot allocation. + page.reset(); + allocateSavedBuffer(); storeCurrentBuffer(); currentDisplayPage = pageIdx; return true; @@ -282,72 +357,32 @@ bool ClipSelectionActivity::switchToPage(const int pageIdx) { void ClipSelectionActivity::applyWordStyle(const WordRef& word, const ClipWordStyle& style) const { const auto textStyle = static_cast(word.style & ~EpdFontFamily::UNDERLINE); - const int skipX = hasEmSpace(word.text) ? renderer.getTextAdvanceX(renderFontId, "\xe2\x80\x83", textStyle) : 0; + const int skipX = + hasEmSpace(wordStore.text(word)) ? renderer.getTextAdvanceX(renderFontId, "\xe2\x80\x83", textStyle) : 0; const int drawX = word.x + skipX; const int drawW = word.w - skipX; if (drawW <= 0) return; - - const bool invert = (style.flags & ClipWordStyle::INVERT) != 0; - const bool fill = !invert && (style.flags & ClipWordStyle::FILL) != 0; - if (invert) { - renderer.fillRect(drawX, word.y, drawW, word.h, true); - } else if (fill) { - renderer.fillRectDither(drawX, word.y, drawW, word.h, style.fillColor); - } - - if ((style.flags & ClipWordStyle::BORDER) != 0) { - renderer.drawRect(drawX, word.y, drawW, word.h, !invert); - } - - if (word.text.find_first_not_of(" \t") != std::string::npos) { - const bool textBlack = !invert; - renderer.drawText(renderFontId, drawX, word.y, hasEmSpace(word.text) ? word.text.c_str() + 3 : word.text.c_str(), - textBlack, textStyle); - } - - if ((style.flags & ClipWordStyle::UNDERLINE) != 0) { - const int underlineY = word.y + renderer.getFontAscenderSize(renderFontId) + 2; - renderer.drawLine(drawX, underlineY, drawX + drawW, underlineY, true); - } -} - -bool ClipSelectionActivity::prewarmHighlightedWords() const { - if (!renderer.isSdCardFont(renderFontId)) return true; - - auto* fcm = renderer.getFontCacheManager(); - if (!fcm) return true; - - for (auto& text : prewarmTextByStyle) { - text.clear(); - } - - const auto appendWord = [this](const WordRef& word) { - if (word.pageIdx != currentDisplayPage) return; - const uint8_t styleIdx = static_cast(word.style) & 0x03; - if (styleIdx >= prewarmTextByStyle.size()) return; - prewarmTextByStyle[styleIdx] += word.text; - prewarmTextByStyle[styleIdx].push_back(' '); - }; - - if (startMarkIdx != -1) { - const int from = std::min(startMarkIdx, cursorIdx); - const int to = std::max(startMarkIdx, cursorIdx); - for (int i = from; i <= to; i++) { - appendWord(words[readingOrder[i]]); + const bool foregroundBlack = ReaderUtils::readerForegroundBlack(); + + const bool fill = (style.flags & ClipWordStyle::FILL) != 0; + if (fill) { + // Build the highlight from the existing framebuffer instead of redrawing + // the word, which avoids font-cache work on every selection step. + for (int y = word.y; y < word.y + word.h; y += 2) { + for (int x = drawX; x < drawX + drawW; x += 2) { + renderer.drawPixel(x, y, foregroundBlack); + } + } + if (!foregroundBlack) { + // Dark mode starts with white text on black. Inverting after adding the + // dither produces black text on a light-gray highlight. + renderer.invertRect(drawX, word.y, drawW, word.h); } } - appendWord(words[readingOrder[cursorIdx]]); - - bool ok = true; - for (uint8_t styleIdx = 0; styleIdx < prewarmTextByStyle.size(); styleIdx++) { - if (!prewarmTextByStyle[styleIdx].empty()) { - ok = - fcm->prewarmCache(renderFontId, prewarmTextByStyle[styleIdx].c_str(), static_cast(1u << styleIdx)) && - ok; - } + if ((style.flags & ClipWordStyle::BORDER) != 0) { + renderer.drawRect(drawX, word.y, drawW, word.h, foregroundBlack); } - return ok; } void ClipSelectionActivity::useFallbackFont(const char* reason) { @@ -359,28 +394,35 @@ void ClipSelectionActivity::useFallbackFont(const char* reason) { } void ClipSelectionActivity::drawHighlights() { - static constexpr ClipWordStyle selectionStyle{ClipWordStyle::FILL | ClipWordStyle::UNDERLINE, Color::LightGray}; - static constexpr ClipWordStyle cursorStyle{ClipWordStyle::INVERT, Color::LightGray}; + // An underline sits below the ascender and collides with descenders when + // the reader's line height has no spare leading. The fill and cursor border + // already distinguish the selected range and its active endpoint. + static constexpr ClipWordStyle selectionStyle{ClipWordStyle::FILL}; + static constexpr ClipWordStyle initialCursorStyle{ClipWordStyle::FILL | ClipWordStyle::BORDER}; + static constexpr ClipWordStyle selectedCursorStyle{ClipWordStyle::BORDER}; if (startMarkIdx != -1) { const int from = std::min(startMarkIdx, cursorIdx); const int to = std::max(startMarkIdx, cursorIdx); for (int i = from; i <= to; i++) { - const WordRef& word = words[readingOrder[i]]; + const WordRef& word = wordStore.words[readingOrder[i]]; if (word.pageIdx == currentDisplayPage) { applyWordStyle(word, selectionStyle); } } } - const WordRef& cursorWord = words[readingOrder[cursorIdx]]; + const WordRef& cursorWord = wordStore.words[readingOrder[cursorIdx]]; if (cursorWord.pageIdx == currentDisplayPage) { - applyWordStyle(cursorWord, cursorStyle); + // Before the first endpoint, the cursor supplies its own fill. Afterwards + // selectionStyle has already filled it, so only add the active border. + applyWordStyle(cursorWord, startMarkIdx == -1 ? initialCursorStyle : selectedCursorStyle); } } int ClipSelectionActivity::lineEndForward(const int orderIdx) const { const int total = static_cast(readingOrder.size()); + const auto& words = wordStore.words; const WordRef& current = words[readingOrder[orderIdx]]; for (int i = orderIdx + 1; i < total; ++i) { const WordRef& word = words[readingOrder[i]]; @@ -390,6 +432,7 @@ int ClipSelectionActivity::lineEndForward(const int orderIdx) const { } int ClipSelectionActivity::lineEndBackward(const int orderIdx) const { + const auto& words = wordStore.words; const WordRef& current = words[readingOrder[orderIdx]]; int i = orderIdx - 1; for (; i >= 0; --i) { diff --git a/src/activities/reader/ClipSelectionActivity.h b/src/activities/reader/ClipSelectionActivity.h index 050cdf92f4..841bb0fdb0 100644 --- a/src/activities/reader/ClipSelectionActivity.h +++ b/src/activities/reader/ClipSelectionActivity.h @@ -17,18 +17,15 @@ struct ClipWordStyle { enum Flags : uint8_t { NONE = 0, FILL = 1 << 0, - INVERT = 1 << 1, - UNDERLINE = 1 << 2, - BORDER = 1 << 3, + BORDER = 1 << 1, }; uint8_t flags = FILL; - Color fillColor = Color::LightGray; }; class ClipSelectionActivity final : public Activity { public: - ClipSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::vector words, int fontId, + ClipSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, ClipWordStore wordStore, int fontId, Section& section, int startPageInSection, int marginTop, int marginLeft); void onEnter() override; @@ -40,15 +37,18 @@ class ClipSelectionActivity final : public Activity { private: static constexpr size_t BUFFER_CHUNK_SIZE = 4096; + static constexpr size_t MAX_SAVED_BUFFER_CHUNKS = 16; + static constexpr size_t MAX_READING_ORDER_WORDS = 240; - std::vector words; + ClipWordStore wordStore; int renderFontId = 0; Section& section; int startPageInSection = 0; int marginTop = 0; int marginLeft = 0; - std::vector> savedBufferChunks; + std::array, MAX_SAVED_BUFFER_CHUNKS> savedBufferChunks; + size_t savedBufferChunkCount = 0; size_t savedBufferSize = 0; int currentDisplayPage = 0; int savedSectionPage = 0; @@ -58,20 +58,23 @@ class ClipSelectionActivity final : public Activity { bool needsPageSwitch = false; bool hasSavedBuffer = false; bool usingFallbackFont = false; - mutable std::array prewarmTextByStyle; - std::vector readingOrder; + bool touchDragSelecting = false; + std::array readingOrder{}; + size_t readingOrderSize = 0; ButtonNavigator buttonNavigator; void buildReadingOrder(); - bool allocateSavedBuffer(); + void resetSavedBufferChunks(); + void allocateSavedBuffer(); void storeCurrentBuffer(); void restoreSavedBuffer() const; bool switchToPage(int pageIdx); - bool prewarmHighlightedWords() const; void drawHighlights(); void applyWordStyle(const WordRef& word, const ClipWordStyle& style) const; void useFallbackFont(const char* reason); + bool selectWordAtPoint(int x, int y); + void confirmSelection(); int lineEndForward(int orderIdx) const; int lineEndBackward(int orderIdx) const; }; diff --git a/src/activities/reader/ControlsOptionsActivity.cpp b/src/activities/reader/ControlsOptionsActivity.cpp index 77fcf62393..e940d197d3 100644 --- a/src/activities/reader/ControlsOptionsActivity.cpp +++ b/src/activities/reader/ControlsOptionsActivity.cpp @@ -1,6 +1,7 @@ #include "ControlsOptionsActivity.h" #include +#include #include #include @@ -10,8 +11,10 @@ #include "MappedInputManager.h" #include "SettingsList.h" #include "activities/settings/ButtonRemapActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" -#include "fontIds.h" + +namespace fui = freeink::ui; namespace { uint8_t enumDisplayIndexForRawValue(const SettingInfo& setting, uint8_t rawValue) { @@ -42,6 +45,12 @@ void ControlsOptionsActivity::onEnter() { activeSubmenu = SettingAction::None; rebuildSettingsList(); + uiReady = false; + visibleRows = 1; + topIndex = 0; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &ControlsOptionsActivity::onRowEvent, this); + app.setScreen(&ControlsOptionsActivity::optionsScreen, this); requestUpdate(); } @@ -56,7 +65,13 @@ void ControlsOptionsActivity::rebuildSettingsList() { const auto allSettings = getSettingsList(); settings = buildControlsSettingsParentList(allSettings); powerSettings = buildControlsPowerSettingsList(allSettings); +#if CROSSINK_APP_CAP_TOUCH + if (!gpio.hasTouch()) { + frontButtonSettings = buildControlsFrontButtonSettingsList(allSettings); + } +#else frontButtonSettings = buildControlsFrontButtonSettingsList(allSettings); +#endif sideButtonSettings = buildControlsSideButtonSettingsList(allSettings); setCurrentSettings(); @@ -98,12 +113,14 @@ void ControlsOptionsActivity::openSubmenu(SettingAction action) { activeSubmenu = action; setCurrentSettings(); selectedIndex = 0; + topIndex = 0; } void ControlsOptionsActivity::closeSubmenu() { activeSubmenu = SettingAction::None; setCurrentSettings(); selectedIndex = 0; + topIndex = 0; } void ControlsOptionsActivity::moveSelection(bool forward) { @@ -113,6 +130,7 @@ void ControlsOptionsActivity::moveSelection(bool forward) { selectedIndex = forward ? ButtonNavigator::nextIndex(selectedIndex, settingsCount) : ButtonNavigator::previousIndex(selectedIndex, settingsCount); if ((*currentSettings)[selectedIndex].type != SettingType::SECTION_HEADER) { + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, settingsCount); break; } } @@ -196,6 +214,41 @@ void ControlsOptionsActivity::toggleCurrentSetting() { void ControlsOptionsActivity::loop() { if (optionPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return; + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + if (activeSubmenu != SettingAction::None) { + closeSubmenu(); + requestUpdate(); + return; + } + SETTINGS.saveToFile(); + finish(); + return; + } + if (mappedInput.wasHomeGesture()) { + finish(); + return; + } + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } + + if (mappedInput.hasTouch()) { + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, settingsCount); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + } buttonNavigator.onNextRelease([this] { moveSelection(true); @@ -225,65 +278,104 @@ void ControlsOptionsActivity::loop() { } } -void ControlsOptionsActivity::render(RenderLock&&) { - if (optionPopup.processRender(renderer, mappedInput)) return; +void ControlsOptionsActivity::optionsScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildOptionsScreen(screen); +} - renderer.clearScreen(); +void ControlsOptionsActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (self->optionPopup.isActive() || event.value < 0 || event.value >= self->settingsCount) return; + if ((*self->currentSettings)[event.value].type == SettingType::SECTION_HEADER) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->toggleCurrentSetting(); +} - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); +void ControlsOptionsActivity::buildOptionsScreen(UiApp::ScreenType& screen) { const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, !mappedInput.hasTouchHardware(), false); + screen.setContentMargin(fui::Insets{ + static_cast(safe.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), + static_cast(renderer.getScreenWidth() - safe.x - safe.width), + static_cast(renderer.getScreenHeight() - safe.y - safe.height), static_cast(safe.x)}); + screen.spacer(static_cast(metrics.verticalSpacing)); - const auto orientation = renderer.getOrientation(); - const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; - const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; - const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? metrics.buttonHintsHeight : 0; - const int contentX = isLandscapeCw ? hintGutterWidth : 0; - const int contentWidth = pageWidth - hintGutterWidth; - - GUI.drawHeader(renderer, Rect{contentX, metrics.topPadding, contentWidth, metrics.headerHeight}, tr(STR_CAT_CONTROLS), - nullptr, true); - - const auto& visibleSettings = *currentSettings; - Rect listRect{contentX, metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing, contentWidth, - pageHeight - (metrics.topPadding + metrics.headerHeight + metrics.buttonHintsHeight + - metrics.verticalSpacing * 2)}; const StrId submenuTitleId = activeSubmenuTitleId(); if (submenuTitleId != StrId::STR_NONE_OPT) { - constexpr int submenuHeaderFontId = UI_10_FONT_ID; - const int headerLineHeight = renderer.getLineHeight(submenuHeaderFontId); - const int headerOffset = headerLineHeight + metrics.verticalSpacing; - const int headerMaxWidth = listRect.width - metrics.contentSidePadding * 2; - const auto headerLabel = - renderer.truncatedText(submenuHeaderFontId, I18N.get(submenuTitleId), headerMaxWidth, EpdFontFamily::BOLD); - renderer.drawText(submenuHeaderFontId, listRect.x + metrics.contentSidePadding, listRect.y, headerLabel.c_str(), - true, EpdFontFamily::BOLD); - listRect.y += headerOffset; - listRect.height = std::max(0, listRect.height - headerOffset); + fui::TextStyle titleStyle = screen.theme().smallText; + titleStyle.bold = true; + titleStyle.maxLines = 1; + const int16_t titleHeight = screen.target().lineHeight(titleStyle.font); + fui::Rect titleRect = screen.takeTop(titleHeight, static_cast(metrics.verticalSpacing)); + const int16_t sidePadding = static_cast(metrics.contentSidePadding); + titleRect.x = static_cast(titleRect.x + sidePadding); + titleRect.width = static_cast(titleRect.width > sidePadding * 2 ? titleRect.width - sidePadding * 2 : 0); + screen.target().text(titleRect, I18N.get(submenuTitleId), titleStyle); + } + + const auto& currentSettingsList = *currentSettings; + std::vector values(currentSettingsList.size()); + std::vector items; + items.reserve(currentSettingsList.size()); + for (size_t i = 0; i < currentSettingsList.size(); ++i) { + const auto& setting = currentSettingsList[i]; + if (settingShowsNavigationCaret(setting)) { + values[i] = ">"; + } else if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { + values[i] = SETTINGS.*(setting.valuePtr) ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); + } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { + const uint8_t displayValue = enumDisplayIndexForRawValue(setting, SETTINGS.*(setting.valuePtr)); + values[i] = settingEnumOptionLabel(setting, displayValue < settingEnumOptionCount(setting) ? displayValue : 0); + } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { + values[i] = std::to_string(SETTINGS.*(setting.valuePtr)); + } + + const bool isSectionHeader = setting.type == SettingType::SECTION_HEADER; + fui::ListItem item; + item.label = + isSectionHeader ? uiListSectionHeaderLabel(values[i], I18N.get(setting.nameId)) : I18N.get(setting.nameId); + if (!isSectionHeader && !values[i].empty()) item.value = values[i].c_str(); + item.isHeader = isSectionHeader; + item.actionValue = static_cast(i); + items.push_back(item); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.valueInset = 8; + props.labelText = screen.theme().bodyText; + props.labelText.maxLines = 2; + configureUiListSectionHeaders(props, screen.theme()); + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, settingsCount); + props.topIndex = static_cast(topIndex); + screen.list(props); +} + +void ControlsOptionsActivity::render(RenderLock&&) { + if (optionPopup.processRender(renderer, mappedInput)) return; + + renderer.clearScreen(); + + const auto& metrics = UITheme::getInstance().getMetrics(); + Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, !mappedInput.hasTouchHardware(), false); + header.x = safe.x; + header.width = safe.width; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_CAT_CONTROLS), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_CAT_CONTROLS), nullptr, true); } - GUI.drawList( - renderer, listRect, settingsCount, selectedIndex, - [&visibleSettings](int i) { return std::string(I18N.get(visibleSettings[i].nameId)); }, nullptr, nullptr, - [&visibleSettings](int i) { - const auto& setting = visibleSettings[i]; - std::string valueText; - if (setting.type == SettingType::SUBMENU) { - valueText = ">"; - } else if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - valueText = SETTINGS.*(setting.valuePtr) ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); - } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { - const uint8_t value = SETTINGS.*(setting.valuePtr); - const uint8_t displayValue = enumDisplayIndexForRawValue(setting, value); - const size_t optionCount = settingEnumOptionCount(setting); - const uint8_t safeValue = displayValue < optionCount ? displayValue : 0; - valueText = settingEnumOptionLabel(setting, safeValue); - } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - valueText = std::to_string(SETTINGS.*(setting.valuePtr)); - } - return valueText; - }, - true, nullptr, [&visibleSettings](int i) { return visibleSettings[i].type == SettingType::SECTION_HEADER; }); + uiReady = false; + app.render(); + uiReady = true; const bool currentIsAction = selectedIndex >= 0 && selectedIndex < settingsCount && ((*currentSettings)[selectedIndex].type == SettingType::ACTION || diff --git a/src/activities/reader/ControlsOptionsActivity.h b/src/activities/reader/ControlsOptionsActivity.h index e22188fd1d..718a645b20 100644 --- a/src/activities/reader/ControlsOptionsActivity.h +++ b/src/activities/reader/ControlsOptionsActivity.h @@ -1,11 +1,16 @@ #pragma once +#include +#include #include +#include #include #include "../Activity.h" #include "../settings/SettingsActivity.h" #include "components/OptionPopup.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "util/ButtonNavigator.h" class ControlsOptionsActivity final : public Activity { @@ -20,6 +25,14 @@ class ControlsOptionsActivity final : public Activity { SettingAction activeSubmenu = SettingAction::None; OptionPopup optionPopup; + using UiApp = freeink::ui::FreeInkApp<20, 4>; + static constexpr freeink::ui::ActionId ACTION_ROW = 1; + freeink::ui::GfxRendererTarget uiTarget; // Must precede app: the app holds a reference to it. + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + void rebuildSettingsList(); void setCurrentSettings(); StrId activeSubmenuTitleId() const; @@ -29,13 +42,19 @@ class ControlsOptionsActivity final : public Activity { bool currentSettingUsesOptionMenu(const SettingInfo& setting) const; void openEnumOptionPicker(const SettingInfo& setting); void toggleCurrentSetting(); + static void optionsScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildOptionsScreen(UiApp::ScreenType& screen); public: explicit ControlsOptionsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("ControlsOptions", renderer, mappedInput) {} + : Activity("ControlsOptions", renderer, mappedInput), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} void onEnter() override; void onExit() override; void loop() override; void render(RenderLock&&) override; bool allowPowerAsConfirmInReaderMode() const override { return true; } + bool allowGlobalHomeGesture() const override { return false; } }; diff --git a/src/activities/reader/DictionaryDefinitionActivity.cpp b/src/activities/reader/DictionaryDefinitionActivity.cpp new file mode 100644 index 0000000000..ad768ace7f --- /dev/null +++ b/src/activities/reader/DictionaryDefinitionActivity.cpp @@ -0,0 +1,1607 @@ +#include "DictionaryDefinitionActivity.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../settings/DictionarySelectActivity.h" +#include "CrossPointSettings.h" +#include "MappedInputManager.h" +#include "Memory.h" +#include "SdCardFontSystem.h" +#include "components/UITheme.h" +#include "fontIds.h" +#include "util/Dictionary.h" +#include "util/DictionaryActivityUtils.h" +#include "util/IpaUtils.h" +#include "util/LookupHistory.h" +#include "util/TextPool.h" + +static constexpr char kBullet[] = "- "; +static constexpr const char kEtymologyTreeMarker[] = "Etymology tree"; +static constexpr int kDictionarySwitchTouchHeight = 56; + +class DictionaryDefinitionActivity; + +static std::string dictionaryNameFromPath(const std::string& path) { + const size_t stemSlash = path.rfind('/'); + if (stemSlash == std::string::npos) return path; + + const size_t nameSlash = stemSlash == 0 ? std::string::npos : path.rfind('/', stemSlash - 1); + const size_t nameStart = nameSlash == std::string::npos ? 0 : nameSlash + 1; + return path.substr(nameStart, stemSlash - nameStart); +} + +struct DefinitionSpanFeedContext { + DictionaryDefinitionActivity* activity = nullptr; + DictLayout::Wrapper* wrapper = nullptr; + char lastVisibleChar = '\0'; + bool sawEtymologyTree = false; +}; + +static bool isAsciiWordChar(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); +} + +static bool isGreekCodepoint(uint32_t cp) { return (cp >= 0x0370 && cp <= 0x03FF) || (cp >= 0x1F00 && cp <= 0x1FFF); } + +static bool startsWithAt(const std::string& text, size_t pos, const char* needle) { + const size_t len = std::char_traits::length(needle); + return pos + len <= text.size() && text.compare(pos, len, needle) == 0; +} + +static bool startsWithAt(const char* text, const char* needle) { + if (!text) return false; + const size_t len = std::char_traits::length(needle); + return strncmp(text, needle, len) == 0; +} + +static bool isAsciiUpper(char c) { return c >= 'A' && c <= 'Z'; } + +static bool startsWithEtymologyTreeMarker(const char* text) { return startsWithAt(text, kEtymologyTreeMarker); } + +static bool definitionTextHasEtymologySpacingArtifact(const char* text, bool inEtymologyTree) { + if (!text || !text[0]) return false; + for (const char* p = text; *p; ++p) { + if (startsWithEtymologyTreeMarker(p)) { + const char* afterMarker = p + sizeof(kEtymologyTreeMarker) - 1; + if (*afterMarker != '\0' && *afterMarker != ' ') return true; + } + if (inEtymologyTree && *p == '.' && isAsciiUpper(*(p + 1))) return true; + } + return false; +} + +static void fixEtymologySpacingArtifacts(std::string& text, bool inEtymologyTree) { + if (text.size() < 2) return; + + std::string out; + out.reserve(text.size() + 8); + for (size_t i = 0; i < text.size();) { + if (startsWithAt(text, i, kEtymologyTreeMarker)) { + const size_t markerLen = sizeof(kEtymologyTreeMarker) - 1; + out.append(text, i, markerLen); + i += markerLen; + if (i < text.size() && text[i] != ' ') out += ' '; + inEtymologyTree = true; + continue; + } + + if (inEtymologyTree && text[i] == '.' && i + 1 < text.size() && isAsciiUpper(text[i + 1])) { + out += ". "; + ++i; + continue; + } + + out += text[i++]; + } + text = std::move(out); +} + +static char lastVisibleAsciiChar(const char* text) { + if (!text) return '\0'; + char last = '\0'; + while (*text) { + if (*text != ' ' && *text != '\t' && *text != '\r' && *text != '\n') last = *text; + ++text; + } + return last; +} + +static bool shouldSanitizeDefinitionCodepoint(uint32_t cp) { + switch (cp) { + case 0x00A0: + case 0x2013: + case 0x2014: + case 0x2212: + case 0x2018: + case 0x2019: + case 0x201C: + case 0x201D: + case 0x2026: + return true; + default: + return isIpaCodepoint(cp) || isGreekCodepoint(cp) || utf8IsCombiningMark(cp) || cp == REPLACEMENT_GLYPH; + } +} + +// Every Lexend Deca/Bitter reader font header (all sizes and styles) has the +// same direct coverage among the codepoints above. Keep that coverage beside +// the approximation policy instead of probing the font map for every definition. +// The Greek entries below are the renderer's fixed synthetic fallbacks; U+02BB +// is its fixed alias for the left single quotation mark. +static constexpr bool builtinDefinitionFontSupportsCandidate(const uint32_t cp) { + switch (cp) { + case 0x00A0: + case 0x00E6: + case 0x00F0: + case 0x00F8: + case 0x0127: + case 0x014B: + case 0x0153: + case 0x02BB: + case 0x0393: + case 0x03B5: + case 0x03C9: + case 0x2013: + case 0x2014: + case 0x2018: + case 0x2019: + case 0x201C: + case 0x201D: + case 0x2026: + case 0x2212: + return true; + default: + return (cp >= 0x0300 && cp <= 0x0304) || (cp >= 0x0306 && cp <= 0x030C) || cp == 0x030F || + (cp >= 0x0311 && cp <= 0x0312) || cp == 0x031B || (cp >= 0x0323 && cp <= 0x0324) || + (cp >= 0x0326 && cp <= 0x0328) || cp == 0x032E || cp == 0x0331 || cp == 0x0335 || cp == 0x034F || + cp == 0x03BB; + } +} + +static void appendDictionaryApproximation(uint32_t cp, std::string& out) { + switch (cp) { + case 0x00A0: + out += ' '; + break; // nbsp + case 0x00E6: + out += "ae"; + break; // ae + case 0x00F0: + out += "th"; + break; // eth + case 0x00F8: + out += 'o'; + break; // o slash + case 0x0127: + out += 'h'; + break; // h stroke + case 0x014B: + out += "ng"; + break; // eng + case 0x0153: + out += "oe"; + break; // oe + case 0x0251: // script a + case 0x0252: + out += 'a'; + break; // turned alpha + case 0x0254: + out += 'o'; + break; // open o + case 0x0259: + out += 'e'; + break; // schwa + case 0x025A: // rhotacized schwa + case 0x025D: + out += "er"; + break; // reversed epsilon hook + case 0x025B: + out += 'e'; + break; // open e + case 0x0261: + out += 'g'; + break; // script g + case 0x026A: + out += 'i'; + break; // small capital i + case 0x026B: + out += 'l'; + break; // velarized l + case 0x0272: + out += "ny"; + break; // n hook + case 0x0273: + out += 'n'; + break; // retroflex n + case 0x0279: // turned r + case 0x027B: // turned r hook + case 0x027D: // r tail + case 0x027E: + out += 'r'; + break; // tap r + case 0x0280: + out += 'R'; + break; // small capital r + case 0x0283: + out += "sh"; + break; // esh + case 0x028A: + out += 'u'; + break; // upsilon + case 0x028C: + out += 'u'; + break; // turned v + case 0x0292: + out += "zh"; + break; // ezh + case 0x0294: + out += '\''; + break; // glottal stop + case 0x02A4: + out += 'j'; + break; // dezh digraph + case 0x02A7: + out += "ch"; + break; // tesh digraph + case 0x02B0: + out += 'h'; + break; // modifier h + case 0x02B2: + out += 'y'; + break; // modifier j + case 0x02B7: + out += 'w'; + break; // modifier w + case 0x02BC: + out += '\''; + break; // modifier apostrophe + case 0x02C8: // primary stress + case 0x02CC: + break; // secondary stress + case 0x02D0: + out += ':'; + break; // length mark + case 0x03B2: + out += 'b'; + break; // beta + case 0x03B8: + out += "th"; + break; // theta + case 0x03C7: + out += "kh"; + break; // chi + case 0x2013: // en dash + case 0x2014: // em dash + case 0x2212: + out += '-'; + break; // minus + case 0x2018: // left single quote + case 0x2019: + out += '\''; + break; // right single quote + case 0x201C: // left double quote + case 0x201D: + out += '"'; + break; // right double quote + case 0x2026: + out += "..."; + break; // ellipsis + default: + if (!isIpaCodepoint(cp) && !isGreekCodepoint(cp) && !utf8IsCombiningMark(cp) && cp != REPLACEMENT_GLYPH) { + utf8AppendCodepoint(cp, out); + } + break; + } +} + +static bool startsWithCapitalizedToken(const char* text) { return text && isAsciiUpper(text[0]); } + +static bool definitionTextMayNeedBidi(const char* text) { + if (!text) return false; + while (*text) { + const auto b = static_cast(*text++); + if (b >= 0xD6 && b <= 0xDB) return true; // Hebrew and Arabic UTF-8 lead bytes + } + return false; +} + +static EpdFontFamily::Style styleForSpan(const StyledSpan& span) { + if (span.bold && span.italic) return EpdFontFamily::BOLD_ITALIC; + if (span.bold) return EpdFontFamily::BOLD; + if (span.italic) return EpdFontFamily::ITALIC; + return EpdFontFamily::REGULAR; +} + +void DictionaryDefinitionActivity::onEnter() { + Activity::onEnter(); + const DictionaryFontActivation activation = + sdFontSystem.activateDictionaryFont(renderer, dictionaryFontFamilyName_, dictionaryFontPointSize_); + definitionFontId_ = activation.fontId; + definitionFontSource_ = + activation.usingDictionaryFont ? DefinitionFontSource::Dictionary : DefinitionFontSource::Reader; + if (renderer.isSdCardFont(definitionFontId_)) { + // Dictionary preparation retains advances but deliberately starts without + // reader kerning/page caches; the lean policy below reloads ligatures and + // exact visible glyph/style data only. + renderer.releaseSdCardFontForLowMemory(definitionFontId_, /*preserveAdvanceTable=*/true); + } + wrapText(); + requestUpdate(); + // SD write overlaps the e-ink refresh kicked by requestUpdate() on the render task. + LookupHistory::addWordIf(cachePath, historyWord, historyStatus, recordHistory); + + // Seed the back-nav chain. The initial word is the newest history entry iff it + // was just logged (same condition addWordIf applies internally). + chain_.reset(LookupHistory::MAX_VISIBLE_ENTRIES); + const bool initialLogged = recordHistory && !historyWord.empty() && !cachePath.empty(); + chain_.setCurrentHistIndex(initialLogged ? 0 : -1); +} + +void DictionaryDefinitionActivity::onExit() { + controller.onExit(); + Dictionary::clearLookupDictPathOverride(); + sdFontSystem.restoreReaderFont(renderer); + Activity::onExit(); +} + +int DictionaryDefinitionActivity::getDefinitionFontId(bool) const { + return definitionFontId_ != 0 ? definitionFontId_ : SETTINGS.getReaderFontId(); +} + +void DictionaryDefinitionActivity::useBuiltInDefinitionFontFallback() { + const int failedFontId = getDefinitionFontId(); + if (definitionFontSource_ == DefinitionFontSource::Dictionary) { + definitionFontId_ = sdFontSystem.restoreReaderFont(renderer); + definitionFontSource_ = DefinitionFontSource::Reader; + LOG_ERR("DICT", "Dictionary SD font %d failed to prepare; retrying with reader font %d", failedFontId, + definitionFontId_); + } else if (definitionFontSource_ == DefinitionFontSource::Reader) { + definitionFontId_ = SETTINGS.getBuiltInReaderFontId(); + definitionFontSource_ = DefinitionFontSource::BuiltIn; + LOG_ERR("DICT", "Reader SD font %d failed to prepare dictionary glyphs; using built-in font %d", failedFontId, + definitionFontId_); + } else { + return; + } + reflowForDefinitionFontChange(); + // The failed font was the active owner of the definition bitmap cache. Its + // family may also have replaced the reader family while the modal background + // was prepared, so redraw the page with the restored reader font before the + // fallback definition is rendered. This reuses the existing framebuffer; + // retaining the old pixels produces replacement diamonds behind a blank + // modal after a large dictionary-font prewarm fails. + if (hasModalBackground()) { + modalBackgroundNeedsRedraw_ = true; + modalCleanRefreshNeeded_ = true; + } +} + +void DictionaryDefinitionActivity::reflowForDefinitionFontChange() { + const int requestedPage = currentPage; + wrapText(); + currentPage = std::min(requestedPage, std::max(0, totalPages - 1)); + if (currentPage > 0) loadPage(currentPage); + if (hasModalBackground()) sizeModalForCurrentPage(); +} + +void DictionaryDefinitionActivity::redrawModalBackground() { + if (!hasModalBackground() || !modalBackgroundNeedsRedraw_) return; + + if (definitionFontSource_ == DefinitionFontSource::Dictionary) { + sdFontSystem.restoreReaderFont(renderer); + backgroundRender_(backgroundContext_); + const DictionaryFontActivation activation = + sdFontSystem.activateDictionaryFont(renderer, dictionaryFontFamilyName_, dictionaryFontPointSize_); + definitionFontId_ = activation.fontId; + definitionFontSource_ = + activation.usingDictionaryFont ? DefinitionFontSource::Dictionary : DefinitionFontSource::Reader; + if (renderer.isSdCardFont(definitionFontId_)) { + renderer.releaseSdCardFontForLowMemory(definitionFontId_, /*preserveAdvanceTable=*/true); + } + // SD font IDs are deterministic, so reloading the same family returns the + // same ID even though its advance table was discarded with the old font. + // Reflow every restored dictionary font to rebuild those metrics before + // drawing; otherwise lines wrapped with stale/narrow widths can overflow. + reflowForDefinitionFontChange(); + } else { + backgroundRender_(backgroundContext_); + } + modalBackgroundNeedsRedraw_ = false; +} + +void DictionaryDefinitionActivity::displayModalBuffer() { + renderer.displayBuffer(modalCleanRefreshNeeded_ ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH); + modalCleanRefreshNeeded_ = false; +} + +bool DictionaryDefinitionActivity::shouldApproximateDefinitionCodepoint(const uint32_t cp) const { + if (!shouldSanitizeDefinitionCodepoint(cp)) return false; + + const int fontId = getDefinitionFontId(); + // SD fonts own their coverage. Do not replace pronunciation, Greek, or combining + // characters before the active .cpfont gets a chance to draw them. + if (renderer.isSdCardFont(fontId)) return false; + + return !builtinDefinitionFontSupportsCandidate(cp); +} + +bool DictionaryDefinitionActivity::definitionTextNeedsApproximation(const char* text) const { + if (!text) return false; + const auto* p = reinterpret_cast(text); + uint32_t cp = 0; + while ((cp = utf8NextCodepoint(&p))) { + if (shouldApproximateDefinitionCodepoint(cp)) return true; + } + return false; +} + +std::string DictionaryDefinitionActivity::approximateDefinitionText(const char* text, + const bool inEtymologyTree) const { + std::string out; + if (!text) return out; + out.reserve(std::char_traits::length(text)); + + const auto* p = reinterpret_cast(text); + uint32_t cp = 0; + while ((cp = utf8NextCodepoint(&p))) { + if (shouldApproximateDefinitionCodepoint(cp)) { + appendDictionaryApproximation(cp, out); + } else { + utf8AppendCodepoint(cp, out); + } + } + fixEtymologySpacingArtifacts(out, inEtymologyTree); + return out; +} + +int DictionaryDefinitionActivity::getLineHeight() const { + return static_cast(renderer.getLineHeight(getDefinitionFontId()) * SETTINGS.getReaderLineCompression()); +} + +#if CROSSINK_APP_CAP_TOUCH +bool DictionaryDefinitionActivity::showTouchDictionarySwitch() const { + return hasModalBackground() && showLookupButton && mappedInput.hasTouch(); +} +#endif + +int DictionaryDefinitionActivity::dictionaryFooterHeight() const { + if (!hasModalBackground()) return 0; + + const auto metrics = UITheme::getInstance().getMetrics(); + const int dictionaryNameHeight = renderer.getLineHeight(UI_10_FONT_ID) + metrics.optionPopupTitleGap; +#if CROSSINK_APP_CAP_TOUCH + return dictionaryNameHeight + (showTouchDictionarySwitch() ? kDictionarySwitchTouchHeight : 0); +#else + return dictionaryNameHeight; +#endif +} + +#if CROSSINK_APP_CAP_TOUCH +bool DictionaryDefinitionActivity::dictionarySwitchButtonContains(const int x, const int y) const { + const int buttonY = modalY_ + modalHeight_ - kDictionarySwitchTouchHeight; + return x >= modalX_ && x < modalX_ + modalWidth_ && y >= buttonY && y < buttonY + kDictionarySwitchTouchHeight; +} +#endif + +// --------------------------------------------------------------------------- +// Layout helpers — shared setup +// --------------------------------------------------------------------------- + +void DictionaryDefinitionActivity::wrapText() { + isWordSelectMode = false; + navigator.reset(); + currentPage = 0; // new definition always starts at page 0 + // The initial reader page is already present when opened from word-select. + // Chained definitions keep the existing modal covered by growing its frame, + // so they do not need to swap back to the reader font for a background draw. + if (hasModalBackground()) { + if (skipInitialModalBackgroundRedraw_) { + modalBackgroundNeedsRedraw_ = false; + skipInitialModalBackgroundRedraw_ = false; + } + } + + // Match the folder-name label shown by DictionarySelectActivity. + dictionaryName_ = dictionaryNameFromPath(foundLocation.folderPath); + + const auto orient = renderer.getOrientation(); + const auto metrics = UITheme::getInstance().getMetrics(); + const bool isLandscapeCw = orient == GfxRenderer::Orientation::LandscapeClockwise; + const bool isLandscapeCcw = orient == GfxRenderer::Orientation::LandscapeCounterClockwise; + const bool isInverted = orient == GfxRenderer::Orientation::PortraitInverted; + hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? metrics.buttonHintsHeight : 0; + hintGutterHeight = isInverted ? (metrics.buttonHintsHeight + metrics.verticalSpacing) : 0; + contentX = isLandscapeCw ? hintGutterWidth : 0; + const int bottomArea = metrics.buttonHintsHeight + metrics.verticalSpacing; + + if (hasModalBackground()) { + const int dialogMargin = metrics.optionPopupDialogSideMargin; + // Reserve the orientation hint gutter on both sides. The controls only + // occupy one side, but symmetrical gutters keep the modal centered. + modalX_ = hintGutterWidth + dialogMargin; + modalWidth_ = renderer.getScreenWidth() - modalX_ * 2; + modalHeight_ = renderer.getScreenHeight() - bottomArea - dialogMargin * 2; + modalY_ = (renderer.getScreenHeight() - modalHeight_) / 2; + + const int innerPadding = metrics.optionPopupInnerPadding; + leftPadding = modalX_ + innerPadding; + rightPadding = renderer.getScreenWidth() - (modalX_ + modalWidth_ - innerPadding); + bodyStartY = modalY_ + innerPadding + renderer.getLineHeight(getDefinitionFontId()) + metrics.optionPopupTitleGap; + } else { + const int sidePadding = metrics.contentSidePadding + SETTINGS.screenMargin; + leftPadding = contentX + sidePadding; + rightPadding = (isLandscapeCcw ? hintGutterWidth : 0) + sidePadding; + bodyStartY = hintGutterHeight + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + } + + const int footerHeight = dictionaryFooterHeight(); + const int bodyBottom = hasModalBackground() ? modalY_ + modalHeight_ - metrics.optionPopupInnerPadding - footerHeight + : renderer.getScreenHeight() - bottomArea; + linesPerPage = (bodyBottom - bodyStartY) / getLineHeight(); + if (linesPerPage < 1) linesPerPage = 1; + + // Resolve the immutable definition slice once. Page turns reuse it instead + // of reopening the dictionary metadata files before every layout pass. + const DictInfo info = Dictionary::readInfo(foundLocation.folderPath.c_str()); + const DictDefinitionSlice slice = Dictionary::resolveDefinitionSlice(foundLocation, info); + definitionReadFailed_ = !slice.found; + if (definitionReadFailed_) { + LOG_ERR("DICT", "Failed to resolve definition slice for %s", foundLocation.folderPath.c_str()); + } + definitionOffset_ = slice.offset; + definitionSize_ = slice.size; + definitionIsHtml_ = slice.isHtml; + definitionHtmlNeedsPlainFallback_ = false; + + prepareDefinitionFontAdvances(); + loadPage(currentPage); + // Pick the modal height once per definition. Page turns re-fill the content + // without moving the frame, so the last (usually shorter) page feels like + // part of the same lookup rather than a different modal. + if (hasModalBackground()) sizeModalForCurrentPage(); +} + +void DictionaryDefinitionActivity::drawModalFrame() const { + const auto metrics = UITheme::getInstance().getMetrics(); + const int frameThickness = metrics.popupFrameThickness; + const int frameRadius = metrics.popupCornerRadius; + + if (frameRadius > 0) { + renderer.fillRoundedRect(modalX_ - frameThickness, modalY_ - frameThickness, modalWidth_ + frameThickness * 2, + modalHeight_ + frameThickness * 2, frameRadius + frameThickness, Color::White); + renderer.fillRoundedRect(modalX_, modalY_, modalWidth_, modalHeight_, frameRadius, Color::Black); + renderer.fillRoundedRect(modalX_ + frameThickness, modalY_ + frameThickness, modalWidth_ - frameThickness * 2, + modalHeight_ - frameThickness * 2, std::max(0, frameRadius - frameThickness), + Color::White); + } else { + renderer.fillRect(modalX_ - frameThickness, modalY_ - frameThickness, modalWidth_ + frameThickness * 2, + modalHeight_ + frameThickness * 2, true); + renderer.fillRect(modalX_, modalY_, modalWidth_, modalHeight_, false); + } +} + +// Re-parse the definition and lay out ONLY `page` into layoutLines. The wrap +// produces every line, but collectLineSink keeps only this page's lines (the +// rest are produced then dropped, so peak RAM is one page, not the whole +// definition) and counts all lines to recompute totalPages. Called on entry and +// on every page turn (Stage 2a: re-parse every turn, both directions). +void DictionaryDefinitionActivity::loadPage(int page) { + MemoryBudget::logHeapShape("dict.page_before"); + layoutLines.clear(); + layoutLines.reserve(static_cast(linesPerPage) + 1); + layoutSegments.clear(); + layoutSegments.reserve(static_cast(linesPerPage) * 4); + pagePool_.clear(); + collectTargetPage_ = page; + collectLineCount_ = 0; + + if (definitionReadFailed_) { + totalPages = 1; + return; + } + + if (definitionIsHtml_) { + wrapHtml(); + } else { + wrapPlain(); + } + + totalPages = DictLayout::paginate(collectLineCount_, linesPerPage); + MemoryBudget::logHeapShape("dict.page_after"); +} + +void DictionaryDefinitionActivity::collectDefinitionAdvanceText(const char* text, const EpdFontFamily::Style style) { + if (!text || *text == '\0') return; + + const uint8_t baseStyle = static_cast(style) & 0x03; + definitionAdvanceStyleMask_ |= static_cast(1u << baseStyle); + + const auto collect = [this](const char* utf8) { + const auto* p = reinterpret_cast(utf8); + uint32_t cp = 0; + while ((cp = utf8NextCodepoint(&p))) { + const auto begin = definitionAdvanceCodepoints_.begin(); + const auto end = begin + definitionAdvanceCodepointCount_; + if (std::find(begin, end, cp) != end) continue; + if (definitionAdvanceCodepointCount_ >= definitionAdvanceCodepoints_.size()) { + definitionAdvanceCodepointsTruncated_ = true; + continue; + } + definitionAdvanceCodepoints_[definitionAdvanceCodepointCount_++] = cp; + } + }; + + collect(text); + if (definitionTextMayNeedBidi(text) && + BidiUtils::applyBidiVisual(text, definitionAdvanceVisualText_, static_cast(BidiUtils::BidiBaseDir::AUTO))) { + collect(definitionAdvanceVisualText_.c_str()); + } +} + +void DictionaryDefinitionActivity::collectSpanForAdvances(void* ctx, const StyledSpan& span) { + auto* self = static_cast(ctx); + self->collectDefinitionAdvanceText(span.text, styleForSpan(span)); +} + +void DictionaryDefinitionActivity::prepareDefinitionFontAdvances() { + const int fontId = getDefinitionFontId(); + if (!renderer.isSdCardFont(fontId) || definitionSize_ == 0) return; + + definitionAdvanceCodepointCount_ = 0; + definitionAdvanceStyleMask_ = 0; + definitionAdvanceCodepointsTruncated_ = false; + definitionAdvanceVisualText_.clear(); + + // The title is measured before the bitmap prewarm scan, so include it in the + // same batched advance read as the definition body. + collectDefinitionAdvanceText(headword.c_str(), EpdFontFamily::BOLD); + collectDefinitionAdvanceText(kBullet, EpdFontFamily::REGULAR); + + const std::string dictPath = foundLocation.folderPath + ".dict"; + if (definitionIsHtml_) { + const DictHtmlRenderer::SpanSink sink{this, &DictionaryDefinitionActivity::collectSpanForAdvances}; + if (!htmlRenderer_.renderFromFileStreaming(dictPath.c_str(), definitionOffset_, definitionSize_, sink)) { + definitionHtmlNeedsPlainFallback_ = true; + LOG_ERR("DICT", "Malformed HTML definition; collecting SD-font advances from plain text"); + if (!htmlRenderer_.renderPlainTextFromFileStreaming(dictPath.c_str(), definitionOffset_, definitionSize_, sink)) { + LOG_ERR("DICT", "Failed to collect SD-font advances from definition fallback"); + return; + } + } + } else { + HalFile dictFile; + if (!Storage.openFileForRead("DICT", dictPath.c_str(), dictFile)) { + LOG_ERR("DICT", "Failed to open definition for SD-font advance preparation"); + return; + } + if (!dictFile.seekSet(definitionOffset_)) { + LOG_ERR("DICT", "Failed to seek definition for SD-font advance preparation"); + dictFile.close(); + return; + } + + // Keep only one word while streaming so UTF-8 sequences split across file + // chunks remain intact without retaining the full definition. + std::string word; + word.reserve(64); + uint32_t remaining = definitionSize_; + char chunk[512]; + while (remaining > 0) { + const uint32_t toRead = std::min(remaining, sizeof(chunk)); + const int n = dictFile.read(reinterpret_cast(chunk), static_cast(toRead)); + if (n <= 0) break; + remaining -= static_cast(n); + for (int i = 0; i < n; i++) { + const char c = chunk[i]; + if (c == '\0' || c == '\n' || c == ' ') { + collectDefinitionAdvanceText(word.c_str(), EpdFontFamily::REGULAR); + word.clear(); + } else { + word += c; + } + } + } + collectDefinitionAdvanceText(word.c_str(), EpdFontFamily::REGULAR); + dictFile.close(); + } + + if (definitionAdvanceCodepointCount_ == 0) return; + if (definitionAdvanceCodepointsTruncated_) { + LOG_DBG("DICT", "SD-font advance preparation capped at %u unique codepoints", kDefinitionAdvanceCodepointCapacity); + } + + // The .dict reader is closed above. Batch the sorted font reads now so the + // wrapping pass below performs RAM lookups instead of opening .cpfont once + // per character while streaming the dictionary. + LOG_DBG("DICT", "Preparing %u SD-font advance codepoints (styles=0x%02X)", definitionAdvanceCodepointCount_, + definitionAdvanceStyleMask_); + renderer.ensureSdCardFontReady(fontId, definitionAdvanceCodepoints_.data(), definitionAdvanceCodepointCount_, + /*includeSpace=*/true, /*includeHyphen=*/false, definitionAdvanceStyleMask_); +} + +void DictionaryDefinitionActivity::sizeModalForCurrentPage() { + const auto metrics = UITheme::getInstance().getMetrics(); + const int dialogMargin = metrics.optionPopupDialogSideMargin; + const int maxHeight = + renderer.getScreenHeight() - metrics.buttonHintsHeight - metrics.verticalSpacing - dialogMargin * 2; + const int visibleLines = std::max(1, static_cast(layoutLines.size())); + const int titleLineHeight = renderer.getLineHeight(getDefinitionFontId()); + const int footerHeight = dictionaryFooterHeight(); + const int contentHeight = metrics.optionPopupInnerPadding * 2 + titleLineHeight + metrics.optionPopupTitleGap + + visibleLines * getLineHeight() + footerHeight; + + const int desiredHeight = std::min(maxHeight, contentHeight); + const int retainedHeight = modalSessionHeight_; + if (retainedHeight > 0 && desiredHeight > retainedHeight) { + // The new outer frame erases the old black border. A clean waveform keeps + // that erased border from appearing as a second modal on the physical panel. + modalCleanRefreshNeeded_ = true; + } + modalSessionHeight_ = std::max(retainedHeight, desiredHeight); + modalHeight_ = modalSessionHeight_; + modalY_ = (renderer.getScreenHeight() - modalHeight_) / 2; + bodyStartY = modalY_ + metrics.optionPopupInnerPadding + titleLineHeight + metrics.optionPopupTitleGap; + LOG_DBG("DICT", "Modal height desired=%d retained=%d final=%d", desiredHeight, retainedHeight, modalHeight_); +} + +void DictionaryDefinitionActivity::collectLineSink(void* ctx, const DictLayout::LayoutLineView& line) { + auto* self = static_cast(ctx); + const int idx = self->collectLineCount_++; + const int start = self->collectTargetPage_ * self->linesPerPage; + if (idx < start || idx >= start + self->linesPerPage) return; // not on this page — discard + + // Pool the kept line's text: each (already same-style-merged) segment becomes + // one null-terminated pool entry referenced by {offset, len}. + PooledLine pooled; + pooled.indentLevel = line.indentLevel; + pooled.isListItem = line.isListItem; + pooled.firstSegment = static_cast(self->layoutSegments.size()); + pooled.segmentCount = line.segmentCount; + for (uint16_t i = 0; i < line.segmentCount; ++i) { + const auto& seg = line.segments[i]; + PooledSegment ps; + ps.offset = TextPool::append(self->pagePool_, line.textPool + seg.offset, seg.length); + ps.len = seg.length; + ps.style = seg.style; + ps.isIpa = seg.isIpa; + self->layoutSegments.push_back(ps); + } + self->layoutLines.push_back(std::move(pooled)); +} + +// --------------------------------------------------------------------------- +// Shared helper: measure text width accounting for mixed IPA/non-IPA runs +// --------------------------------------------------------------------------- + +int DictionaryDefinitionActivity::getMixedWidth(std::vector& ipaRuns, const char* text, + EpdFontFamily::Style style) { + ipaRuns.clear(); + splitIpaRuns(text, ipaRuns); + return std::accumulate(ipaRuns.begin(), ipaRuns.end(), 0, [&](int sum, const IpaTextSpan& run) { + return sum + renderer.getTextWidth(getDefinitionFontId(run.isIpa), run.text.c_str(), style); + }); +} + +// --------------------------------------------------------------------------- +// HTML path: run DictHtmlRenderer, lay out spans into LayoutLines +// --------------------------------------------------------------------------- + +int DictionaryDefinitionActivity::measureWidthAdapter(void* ctx, const char* text, EpdFontFamily::Style style, + bool isIpa) { + auto* self = static_cast(ctx); + const int fontId = self->getDefinitionFontId(isIpa); + if (!isIpa && text[0] == ' ' && text[1] == '\0') return self->renderer.getSpaceWidth(fontId, style); + return self->renderer.getTextWidth(fontId, text, style); +} + +void DictionaryDefinitionActivity::wrapHtml() { + const int maxWidth = renderer.getScreenWidth() - leftPadding - rightPadding; + // Indent step: 3 spaces worth of pixels at regular weight. + const int bodyFontId = getDefinitionFontId(); + const int indentStep = renderer.getTextWidth(bodyFontId, " "); + const int bulletWidth = renderer.getTextWidth(bodyFontId, kBullet); + + // Fully streamed: the renderer delivers spans one at a time to the Wrapper, the + // Wrapper emits completed lines to the page collector, and the collector keeps + // only the current page. Neither the whole-definition span/textBuf (renderer) + // nor all pages of lines (here) is ever materialized. + const std::string dictPath = foundLocation.folderPath + ".dict"; + const auto streamIntoLayout = [&](const bool plainTextFallback) { + DictLayout::Measurer measure{this, &DictionaryDefinitionActivity::measureWidthAdapter}; + DictLayout::LineSink lineSink{this, &DictionaryDefinitionActivity::collectLineSink}; + DictLayout::Wrapper wrapper(DictLayout::WrapMetrics{maxWidth, indentStep, bulletWidth}, measure, lineSink); + DefinitionSpanFeedContext feedCtx{this, &wrapper}; + const DictHtmlRenderer::SpanSink spanSink{&feedCtx, &DictionaryDefinitionActivity::feedSpanToWrapper}; + const bool ok = plainTextFallback ? htmlRenderer_.renderPlainTextFromFileStreaming( + dictPath.c_str(), definitionOffset_, definitionSize_, spanSink) + : htmlRenderer_.renderFromFileStreaming(dictPath.c_str(), definitionOffset_, + definitionSize_, spanSink); + wrapper.finish(); + return ok; + }; + + // Both paths reset and stream one span at a time, so recovery never + // materializes the full definition. Once strict parsing fails, page turns + // reuse the known-good fallback without repeating the failed parse. + if (!definitionHtmlNeedsPlainFallback_ && !streamIntoLayout(false)) { + definitionHtmlNeedsPlainFallback_ = true; + LOG_ERR("DICT", "Malformed HTML definition; displaying plain text"); + } + if (definitionHtmlNeedsPlainFallback_) { + // The strict pass may already have emitted the valid prefix. Reset the page + // collector before replaying the complete definition as plain text. + layoutLines.clear(); + layoutSegments.clear(); + pagePool_.clear(); + collectLineCount_ = 0; + if (!streamIntoLayout(true)) { + LOG_ERR("DICT", "Failed to read definition plain-text fallback"); + definitionReadFailed_ = true; + } + } + // Only the kept page's span text was ever copied into layoutLines. +} + +void DictionaryDefinitionActivity::feedSpanToWrapper(void* ctx, const StyledSpan& span) { + auto* feedCtx = static_cast(ctx); + const bool hasMarker = span.text && strstr(span.text, kEtymologyTreeMarker) != nullptr; + const bool needsBoundarySpace = feedCtx->sawEtymologyTree && feedCtx->lastVisibleChar != '\0' && + (isAsciiWordChar(feedCtx->lastVisibleChar) || feedCtx->lastVisibleChar == '.') && + startsWithCapitalizedToken(span.text); + const bool needsCleanup = needsBoundarySpace || feedCtx->activity->definitionTextNeedsApproximation(span.text) || + definitionTextHasEtymologySpacingArtifact(span.text, feedCtx->sawEtymologyTree); + if (!needsCleanup) { + feedCtx->wrapper->onSpan(span); + feedCtx->lastVisibleChar = lastVisibleAsciiChar(span.text); + feedCtx->sawEtymologyTree = feedCtx->sawEtymologyTree || hasMarker; + return; + } + + std::string cleaned = feedCtx->activity->approximateDefinitionText(span.text, feedCtx->sawEtymologyTree); + if (needsBoundarySpace && !cleaned.empty() && cleaned[0] != ' ') { + cleaned.insert(cleaned.begin(), ' '); + } + if (cleaned.empty()) return; + + StyledSpan cleanedSpan = span; + cleanedSpan.text = cleaned.c_str(); + feedCtx->wrapper->onSpan(cleanedSpan); + feedCtx->lastVisibleChar = lastVisibleAsciiChar(cleaned.c_str()); + feedCtx->sawEtymologyTree = feedCtx->sawEtymologyTree || hasMarker; +} + +// --------------------------------------------------------------------------- +// Plain text path: word-wrap into single-segment REGULAR lines +// --------------------------------------------------------------------------- + +void DictionaryDefinitionActivity::wrapPlain() { + const int screenWidth = renderer.getScreenWidth(); + const int maxWidth = screenWidth - leftPadding - rightPadding; + std::string currentWord; + currentWord.reserve(64); + + DictLayout::Measurer measure{this, &DictionaryDefinitionActivity::measureWidthAdapter}; + DictLayout::LineSink sink{this, &DictionaryDefinitionActivity::collectLineSink}; + DictLayout::Wrapper wrapper(DictLayout::WrapMetrics{maxWidth, 0, 0}, measure, sink); + + auto feedWord = [&]() { + if (currentWord.empty()) return; + std::string word = (definitionTextNeedsApproximation(currentWord.c_str()) || + definitionTextHasEtymologySpacingArtifact(currentWord.c_str(), false)) + ? approximateDefinitionText(currentWord.c_str(), false) + : std::move(currentWord); + currentWord.clear(); + if (word.empty()) return; + + const StyledSpan span{.text = word.c_str()}; + wrapper.onSpan(span); + }; + + const StyledSpan space{.text = " "}; + + // Stream from .dict file — the full definition is never held in RAM. + const std::string dictPath = foundLocation.folderPath + ".dict"; + HalFile dictFile; + if (!Storage.openFileForRead("DICT", dictPath.c_str(), dictFile)) { + LOG_ERR("DICT", "Failed to open definition %s", dictPath.c_str()); + definitionReadFailed_ = true; + return; + } + if (!dictFile.seekSet(definitionOffset_)) { + LOG_ERR("DICT", "Failed to seek definition %s", dictPath.c_str()); + definitionReadFailed_ = true; + dictFile.close(); + return; + } + + uint32_t remaining = definitionSize_; + char chunk[512]; + + while (remaining > 0) { + uint32_t toRead = remaining < sizeof(chunk) ? remaining : static_cast(sizeof(chunk)); + int n = dictFile.read(reinterpret_cast(chunk), static_cast(toRead)); + if (n <= 0) { + LOG_ERR("DICT", "Failed reading definition %s", dictPath.c_str()); + definitionReadFailed_ = true; + break; + } + remaining -= static_cast(n); + + for (int ci = 0; ci < n; ci++) { + char c = chunk[ci]; + if (c == '\0') { + feedWord(); + wrapper.onSpan(space); + } else if (c == '\n') { + feedWord(); + wrapper.lineBreak(); + } else if (c == ' ') { + feedWord(); + wrapper.onSpan(space); + } else { + currentWord += c; + } + } + } + + feedWord(); + wrapper.finish(); + dictFile.close(); +} + +// --------------------------------------------------------------------------- +// Word-select: extract words from the currently visible page +// --------------------------------------------------------------------------- + +void DictionaryDefinitionActivity::extractWordsFromLayout() { + const int bodyFontId = getDefinitionFontId(); + const int indentStep = renderer.getTextWidth(bodyFontId, " "); + + std::vector words; + words.reserve(64); + std::vector rows; + rows.reserve(16); + std::string textPool; + textPool.reserve(512); + + const int lineHeight = getLineHeight(); // cached for loop + for (int i = 0; i < linesPerPage && i < static_cast(layoutLines.size()); i++) { + const PooledLine& line = layoutLines[i]; + const int16_t lineY = static_cast(bodyStartY + i * lineHeight); + int x = leftPadding + line.indentLevel * indentStep; + + if (line.isListItem) { + x += renderer.getTextWidth(bodyFontId, kBullet); + } + + for (uint16_t segmentIdx = 0; segmentIdx < line.segmentCount; ++segmentIdx) { + const PooledSegment& seg = layoutSegments[line.firstSegment + segmentIdx]; + const int segFontId = getDefinitionFontId(seg.isIpa); + const int spaceWidth = renderer.getSpaceWidth(segFontId, seg.style); + const char* p = pagePool_.data() + seg.offset; + while (*p) { + while (*p == ' ') { + x += spaceWidth; + ++p; + } + if (!*p) break; + + const char* tokStart = p; + while (*p && *p != ' ') ++p; + const size_t tokLen = static_cast(p - tokStart); + std::string tok(tokStart, tokLen); + + const int tokAdvanceX = renderer.getTextAdvanceX(segFontId, tok.c_str(), seg.style); + std::string cleaned = Dictionary::cleanWord(tok); + if (!cleaned.empty()) { + uint16_t tokOff = WordSelectNavigator::poolAppend(textPool, tok.c_str(), tok.size()); + uint16_t cleanedOff = WordSelectNavigator::poolAppend(textPool, cleaned.c_str(), cleaned.size()); + WordSelectNavigator::WordInfo wi; + wi.textOffset = tokOff; + wi.textLen = static_cast(tok.size()); + wi.lookupOffset = cleanedOff; + wi.lookupLen = static_cast(cleaned.size()); + wi.screenX = static_cast(x); + wi.screenY = lineY; + wi.width = static_cast(tokAdvanceX); + wi.style = seg.style; + wi.isIpa = seg.isIpa; + wi.fontId = segFontId; + words.push_back(wi); + } + x += tokAdvanceX; + } + } + } + + WordSelectNavigator::organizeIntoRows(words, rows); + navigator.load(std::move(words), std::move(rows), std::move(textPool)); +} + +void DictionaryDefinitionActivity::openDictionarySwitch() { + if (hasModalBackground()) { + // Capture the exact frame currently on the panel before the picker covers + // it. This is the hard lower bound for every replacement definition, even + // if its content or active font would naturally produce a shorter dialog. + modalSessionHeight_ = std::max(modalSessionHeight_, modalHeight_); + LOG_DBG("DICT", "Dictionary switch retaining modal height=%d", modalSessionHeight_); + } + auto picker = makeUniqueNoThrow(renderer, mappedInput, cachePath, true, true); + if (!picker) { + LOG_ERR("DICT", "OOM: DictionarySelectActivity"); + return; + } + + startActivityForResult(std::move(picker), [this](const ActivityResult& result) { + // The picker replaced the full framebuffer. Its result handler runs before + // this activity is queued for repaint, so restore the reader background on + // that next frame whether the selection changed or was cancelled. + modalBackgroundNeedsRedraw_ = true; + modalCleanRefreshNeeded_ = true; + if (result.isCancelled) { + requestUpdate(); + return; + } + const auto* selection = std::get_if(&result.data); + if (!selection) { + LOG_ERR("DICT", "Dictionary switch returned no path"); + requestUpdate(); + return; + } + Dictionary::setLookupDictPathOverride(selection->path.c_str()); + dictionaryName_ = dictionaryNameFromPath(selection->path); + dictionarySwitchLookupInProgress = true; + controller.startLookup(headword, false); + }); +} + +// --------------------------------------------------------------------------- +// Input loop +// --------------------------------------------------------------------------- + +bool DictionaryDefinitionActivity::handleLongPressExitAll(bool enabled) { + if (exitAllOnBackRelease_) { + if (mappedInput.wasReleased(MappedInputManager::Button::Back) || + !mappedInput.isPressed(MappedInputManager::Button::Back)) { + setResult(ActivityResult{}); + finish(); + } + return true; + } + + if (enabled && mappedInput.isPressed(MappedInputManager::Button::Back) && + mappedInput.getHeldTime() >= Dictionary::LONG_PRESS_MS) { + exitAllOnBackRelease_ = true; + return true; + } + return false; +} + +void DictionaryDefinitionActivity::loop() { + // Own the complete long-press gesture. Returning to the reader while Back is + // still held would let the reader fire its configured long-press shortcut. + if (handleLongPressExitAll(showLookupButton)) return; + + // --- Controller active (LookingUp / AltFormPrompt / NotFound) --- + if (controller.isActive()) { +#if CROSSINK_APP_CAP_TOUCH + int failureTouchX = 0; + int failureTouchY = 0; + if (hasModalBackground() && controller.hasFailureFeedback() && showTouchDictionarySwitch() && + mappedInput.wasScreenTapped(failureTouchX, failureTouchY) && + dictionarySwitchButtonContains(failureTouchX, failureTouchY) && + controller.dismissFailureForDictionarySwitch()) { + openDictionarySwitch(); + return; + } +#endif + switch (controller.handleInput()) { + case DictionaryLookupController::LookupEvent::FoundDefinition: { + const bool wasBackNav = chainBackNavInProgress; + const bool wasDictionarySwitch = dictionarySwitchLookupInProgress; + const bool willLog = !wasBackNav && controller.getRecordHistory(); + { + // A dictionary-picker return queues a background repaint while the + // lookup worker can finish immediately. Serialize reflow with that + // repaint: it temporarily unloads the dictionary font, and measuring + // against the missing font otherwise collapses pagination to one line. + RenderLock lock(*this); + if (!wasBackNav && !wasDictionarySwitch) { + // Forward: push a back-entry for the word being left (current headword, + // on currentPage), referencing its history position. + chain_.onForward(static_cast(currentPage), willLog); + } + chainBackNavInProgress = false; + dictionarySwitchLookupInProgress = false; + headword = controller.getFoundWord(); + foundLocation = controller.getFoundLocation(); + wrapText(); // resets currentPage to 0 and loads page 0 + if (wasBackNav) { + // Re-derive the now-current word's history position and restore its page. + chain_.setCurrentHistIndex(pendingBack_.histIndex); + currentPage = (pendingBack_.page < totalPages) ? pendingBack_.page : (totalPages - 1); + if (currentPage < 0) currentPage = 0; + if (currentPage > 0) loadPage(currentPage); + } + isWordSelectMode = false; + } + requestUpdate(); + // Chain-forward records; chain-back-nav does not. + LookupHistory::addWordIf(cachePath, controller.getLookupWord(), + DictionaryLookupController::toHistStatus(controller.getFoundStatus()), willLog); + break; + } + case DictionaryLookupController::LookupEvent::NotFoundDismissedBack: + dictionarySwitchLookupInProgress = false; + requestUpdate(); + break; + case DictionaryLookupController::LookupEvent::NotFoundDismissedDone: + dictionarySwitchLookupInProgress = false; + setResult(ActivityResult{}); + finish(); + break; + case DictionaryLookupController::LookupEvent::SwitchDictionary: + openDictionarySwitch(); + break; + case DictionaryLookupController::LookupEvent::Cancelled: + dictionarySwitchLookupInProgress = false; + isWordSelectMode = false; + navigator.reset(); + requestUpdate(); + break; + default: + break; + } + return; + } + + // --- Word-select mode --- + if (isWordSelectMode) { + if (navigator.handleNavigation(mappedInput, renderer)) { + requestUpdate(); + } + +#if CROSSINK_APP_CAP_TOUCH + if (touchDragLookup_) { + int dragX = 0; + int dragY = 0; + if (mappedInput.isScreenTouchHeld(dragX, dragY)) { + if (navigator.selectWordAtPoint(dragX, dragY, getLineHeight())) { + requestUpdate(); + } + return; + } + + touchDragLookup_ = false; + controller.lookupOrPopup(navigator.finishTouchMultiSelect()); + return; + } + + int touchX = 0; + int touchY = 0; + if (mappedInput.wasScreenTouchDown(touchX, touchY)) { + bool touchedWord = false; + navigator.selectWordAtPoint(touchX, touchY, getLineHeight(), &touchedWord); + if (touchedWord && navigator.beginTouchMultiSelect()) { + touchDragLookup_ = true; + // Finish this fast refresh before lookup can replace the definition, + // so the touched word always provides visible press feedback on e-ink. + requestUpdateAndWait(); + } + return; + } +#endif + + if (controller.handleMultiSelect(navigator)) return; + + if (!navigator.isMultiSelecting()) { + if (controller.handleConfirmLookup(navigator)) return; + + // Short press Back: exit word-select mode. + if (mappedInput.wasReleased(MappedInputManager::Button::Back) && + mappedInput.getHeldTime() < Dictionary::LONG_PRESS_MS) { + isWordSelectMode = false; + navigator.reset(); + requestUpdate(); + } + } + return; + } + + // --- View mode --- +#if CROSSINK_APP_CAP_TOUCH + int touchX = 0; + int touchY = 0; + if (showTouchDictionarySwitch() && mappedInput.wasScreenTapped(touchX, touchY) && + dictionarySwitchButtonContains(touchX, touchY)) { + openDictionarySwitch(); + return; + } +#endif + + if (showLookupButton && mappedInput.wasReleased(MappedInputManager::Button::Left)) { + openDictionarySwitch(); + return; + } + +#if CROSSINK_APP_CAP_TOUCH + if (showLookupButton && mappedInput.hasTouch() && mappedInput.wasLeftEdgeGesture()) { + setResult(ActivityResult{}); + finish(); + return; + } +#endif + + const auto swipe = mappedInput.wasSwipe(); + const bool prevPage = + DictUtils::dictionaryPageButtonTriggered(mappedInput, true) || swipe == MappedInputManager::SwipeDir::Down; + const bool nextPage = + DictUtils::dictionaryPageButtonTriggered(mappedInput, false) || swipe == MappedInputManager::SwipeDir::Up; + + if (prevPage && currentPage > 0) { + currentPage--; + loadPage(currentPage); + requestUpdate(); + } + + if (nextPage && currentPage < totalPages - 1) { + currentPage++; + loadPage(currentPage); + requestUpdate(); + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (showLookupButton) { + if (!hasSharedHighlightSnapshotStorage_ && !ownedHighlightSnapshotStorage_) { + // Fixed 4 KB activity-lifetime buffer: too large for the task stack and + // only needed when history-launched definitions enter word selection. + ownedHighlightSnapshotStorage_ = makeUniqueNoThrow(); + if (!ownedHighlightSnapshotStorage_) { + LOG_ERR("DICT", "OOM allocating definition highlight snapshot (%u bytes)", + static_cast(sizeof(WordSelectNavigator::HighlightSnapshotStorage))); + } else { + navigator.setHighlightSnapshotStorage(ownedHighlightSnapshotStorage_.get()); + } + } + extractWordsFromLayout(); + if (!navigator.isEmpty()) { + isWordSelectMode = true; + requestUpdate(); + } + } else { + DictUtils::cancelAndFinish(*this); + } + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Back) && + (!showLookupButton || mappedInput.getHeldTime() < Dictionary::LONG_PRESS_MS)) { + if (!cachePath.empty() && !chain_.empty()) { + pendingBack_ = chain_.pop(); + // Resolve the prior headword from the persisted history by distance-from-newest. + const auto hist = LookupHistory::load(cachePath); // newest-first + if (pendingBack_.histIndex < hist.size()) { + chainBackNavInProgress = true; + controller.startLookup(hist[pendingBack_.histIndex].word, false); + return; + } + // Unresolvable (should not happen under the depth cap) — fall through to exit. + } + DictUtils::cancelAndFinish(*this); + return; + } +} + +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +void DictionaryDefinitionActivity::render(RenderLock&&) { + if (hasModalBackground() && controller.consumeFullScreenChildDisturbance()) { + modalBackgroundNeedsRedraw_ = true; + modalCleanRefreshNeeded_ = true; + } + // Differential fast path: only when we're already in word-select mode AND + // we set it up on the previous frame AND the controller has nothing pending. + if (isWordSelectMode && nextRenderMode_ == RenderMode::Differential && !controller.isActive()) { + const int currIdx = navigator.getCurrentFlatIndex(); + if (currIdx >= 0) { + if (const auto* word = navigator.getWordAt(currIdx); word && renderer.isSdCardFont(word->fontId)) { + if (auto* fcm = renderer.getFontCacheManager()) { + const uint8_t styleMask = static_cast(1u << (static_cast(word->style) & 0x03)); + fcm->prewarmCache(word->fontId, navigator.getDisplay(*word), styleMask, + FontCacheManager::PreparationPolicy::DictionaryLean); + } + } + const int lineHeight = getLineHeight(); + auto dirty = navigator.renderHighlightDifferential(renderer, lineHeight, prevHighlightIdx_, currIdx); + if (dirty.has_value()) { + // Full panel push — matches DictionaryWordSelectActivity. Windowed refresh is not + // wired up because the SDK's experimental path produces alternating black→white + // failures on consecutive partial refreshes. Savings come from skipping page->render. + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + prevHighlightIdx_ = currIdx; + return; + } + // fall through to full repaint path + } + } + + // Alt-form confirmation intentionally owns the full screen. Leave the reader + // font unloaded while it is visible, then rebuild the background once after + // the controller returns to the modal flow. + if (hasModalBackground() && controller.requiresBackgroundRedrawAfterOverlay()) { + controller.render(); + modalBackgroundNeedsRedraw_ = true; + modalCleanRefreshNeeded_ = true; + nextRenderMode_ = RenderMode::FullPage; + prevHighlightIdx_ = -1; + return; + } + + // Full repaint path. The framebuffer retains the book pixels outside the + // opaque, stable modal, so ordinary definition page turns only repaint the + // modal. Rebuild the book after another screen or overlay disturbed it. + if (hasModalBackground()) { + redrawModalBackground(); + } else { + renderer.clearScreen(); + } + + // After the picker closes, rebuild the reader once but do not redraw stale + // definition glyphs. Keep lookup feedback inside the modal; the successful + // replacement can then reuse this background without another font swap. + if (dictionarySwitchLookupInProgress && controller.isLookingUp()) { + if (hasModalBackground()) { + drawModalFrame(); + const int messageY = modalY_ + (modalHeight_ - renderer.getLineHeight(UI_10_FONT_ID)) / 2; + renderer.drawCenteredText(UI_10_FONT_ID, messageY, tr(STR_DICT_LOOKING_UP)); + displayModalBuffer(); + } else { + GUI.drawPopup(renderer, tr(STR_DICT_LOOKING_UP)); + } + nextRenderMode_ = RenderMode::FullPage; + prevHighlightIdx_ = -1; + return; + } + const bool inlineFailureFeedback = hasModalBackground() && controller.hasFailureFeedback(); + if (!inlineFailureFeedback && controller.render()) { + // Controller drew an overlay; framebuffer state is unknown. + if (hasModalBackground() && controller.requiresBackgroundRedrawAfterOverlay()) { + modalBackgroundNeedsRedraw_ = true; + } + nextRenderMode_ = RenderMode::FullPage; + prevHighlightIdx_ = -1; + return; + } + + const auto metrics = UITheme::getInstance().getMetrics(); + const int bodyFontId = getDefinitionFontId(); + const int titleFontId = bodyFontId; + const int titleLineHeight = renderer.getLineHeight(titleFontId); + const int indentStep = renderer.getTextWidth(bodyFontId, " "); + + // Header chrome stays in the built-in UI font; the looked-up word uses the + // active reader font and is drawn with the definition body after prewarming. + Rect headerRect{contentX, hintGutterHeight + metrics.topPadding, renderer.getScreenWidth() - hintGutterWidth, + metrics.headerHeight}; + if (hasModalBackground()) { + drawModalFrame(); + const int innerPadding = metrics.optionPopupInnerPadding; + headerRect = Rect{modalX_ + innerPadding, modalY_ + innerPadding, modalWidth_ - innerPadding * 2, titleLineHeight}; + if (metrics.optionPopupTitleSeparator) { + const int separatorY = headerRect.y + headerRect.height + metrics.optionPopupTitleGap / 2; + renderer.drawLine(headerRect.x, separatorY, headerRect.x + headerRect.width, separatorY, true); + } + } else { + GUI.drawHeader(renderer, headerRect, ""); + } + + int titleRight = headerRect.x + headerRect.width; + if (!isWordSelectMode && totalPages > 1) { + char pageInfo[16]; + snprintf(pageInfo, sizeof(pageInfo), "%d/%d", currentPage + 1, totalPages); + const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, pageInfo); + const int pageInfoY = + hasModalBackground() + ? headerRect.y + (headerRect.height - renderer.getLineHeight(SMALL_FONT_ID)) / 2 + : headerRect.y + headerRect.height - renderer.getLineHeight(SMALL_FONT_ID) - metrics.verticalSpacing / 2; + const int pageInfoX = hasModalBackground() ? headerRect.x + headerRect.width - textWidth + : renderer.getScreenWidth() - rightPadding - textWidth; + renderer.drawText(SMALL_FONT_ID, pageInfoX, pageInfoY, pageInfo); + titleRight = pageInfoX - metrics.optionPopupInnerPadding; + } + + const int titleX = hasModalBackground() ? headerRect.x : headerRect.x + metrics.contentSidePadding; + const int titleY = hasModalBackground() ? headerRect.y : headerRect.y + (headerRect.height - titleLineHeight) / 2; + const int titleMaxWidth = std::max(0, titleRight - titleX); + const std::string visibleHeadword = + renderer.truncatedText(titleFontId, headword.c_str(), titleMaxWidth, EpdFontFamily::BOLD); + auto renderTitle = [&]() { + renderer.drawText(titleFontId, titleX, titleY, visibleHeadword.c_str(), true, EpdFontFamily::BOLD); + }; + + // Body: draw layout lines for the current page (BW pass). layoutLines holds + // only the current page (Stage 2a streaming), so it is indexed from 0. + const int lineHeight = getLineHeight(); // cached for loop + renderHighlight + auto renderBody = [&]() { + for (int i = 0; i < linesPerPage && i < static_cast(layoutLines.size()); i++) { + const PooledLine& line = layoutLines[i]; + const int y = bodyStartY + i * lineHeight; + int x = leftPadding + line.indentLevel * indentStep; + + if (line.isListItem) { + renderer.drawText(bodyFontId, x, y, kBullet); + x += renderer.getTextWidth(bodyFontId, kBullet); + } + + for (uint16_t segmentIdx = 0; segmentIdx < line.segmentCount; ++segmentIdx) { + const PooledSegment& seg = layoutSegments[line.firstSegment + segmentIdx]; + const int segFontId = getDefinitionFontId(seg.isIpa); + const char* segText = pagePool_.data() + seg.offset; + renderer.drawText(segFontId, x, y, segText, true, seg.style); + if ((seg.style & EpdFontFamily::UNDERLINE) != 0) { + const int segWidth = renderer.getTextWidth(segFontId, segText, seg.style); + const int underlineY = y + renderer.getFontAscenderSize(segFontId) + 2; + renderer.drawLine(x, underlineY, x + segWidth, underlineY, true); + } + x += renderer.getTextAdvanceX(segFontId, segText, seg.style); + } + } + }; + + // SD fonts retain bitmaps for only the most recently prepared page. The + // reader background above may have replaced that cache, so scan the visible + // title and definition page before drawing them. The .dict reader was closed + // by loadPage(), keeping font and dictionary SD access serialized. + bool definitionTextRendered = false; + if (renderer.isSdCardFont(bodyFontId)) { + if (auto* fcm = renderer.getFontCacheManager()) { + // Keep this alive through the word-select overlay below. Its destructor + // clears SD glyph bitmaps, and that overlay redraws the selected word. + std::optional definitionRenderScope; + const auto prewarmAndRender = [&]() { + definitionRenderScope.emplace(*fcm, FontCacheManager::PreparationPolicy::DictionaryLean); + renderTitle(); + renderBody(); // scan pass + if (!definitionRenderScope->endScanAndPrewarm()) { + definitionRenderScope.reset(); + return false; + } + renderTitle(); + renderBody(); // prepared render + return true; + }; + + definitionTextRendered = prewarmAndRender(); + if (!definitionTextRendered) { + // A switched definition can require a different glyph/style set. + // Release optional bitmap/kern caches, retain the advance table used by + // layout, and retry once with the exact visible page glyphs. + LOG_ERR("DICT", "SD-font definition prewarm failed; releasing caches and retrying"); + renderer.releaseSdCardFontForLowMemory(bodyFontId, /*preserveAdvanceTable=*/true); + definitionTextRendered = prewarmAndRender(); + if (!definitionTextRendered) { + useBuiltInDefinitionFontFallback(); + requestUpdate(); + return; + } + } + } + } + if (!definitionTextRendered) { + renderTitle(); + renderBody(); + } + + const char* inlineStatusMessage = nullptr; + if (hasModalBackground() && controller.isLookingUp()) { + inlineStatusMessage = tr(STR_DICT_LOOKING_UP); + } else if (inlineFailureFeedback) { + inlineStatusMessage = controller.getFailureMessage(); + } else if (hasModalBackground() && definitionReadFailed_) { + inlineStatusMessage = tr(STR_DICT_READ_FAILED); + } + if (inlineStatusMessage) { + const int messageHeight = renderer.getLineHeight(UI_10_FONT_ID) + metrics.optionPopupInnerPadding * 2; + const int messageY = modalY_ + (modalHeight_ - messageHeight) / 2; + renderer.fillRect(modalX_ + metrics.optionPopupInnerPadding, messageY, + modalWidth_ - metrics.optionPopupInnerPadding * 2, messageHeight, false); + renderer.drawCenteredText(UI_10_FONT_ID, messageY + metrics.optionPopupInnerPadding, inlineStatusMessage); + } + + if (!hasModalBackground() && definitionReadFailed_) GUI.drawPopup(renderer, tr(STR_DICT_READ_FAILED)); + + if (hasModalBackground() && !dictionaryName_.empty()) { + const int innerPadding = metrics.optionPopupInnerPadding; + const int footerLineHeight = renderer.getLineHeight(UI_10_FONT_ID); +#if CROSSINK_APP_CAP_TOUCH + const int switchButtonHeight = showTouchDictionarySwitch() ? kDictionarySwitchTouchHeight : 0; +#else + constexpr int switchButtonHeight = 0; +#endif + const int footerY = modalY_ + modalHeight_ - switchButtonHeight - innerPadding - footerLineHeight; + const int separatorY = footerY - metrics.optionPopupTitleGap / 2; + const int footerBottom = modalY_ + modalHeight_ - switchButtonHeight; + const int dictionaryNameY = separatorY + (footerBottom - separatorY - footerLineHeight) / 2; + renderer.drawLine(modalX_, separatorY, modalX_ + modalWidth_, separatorY, true); + const auto visibleName = + renderer.truncatedText(UI_10_FONT_ID, dictionaryName_.c_str(), modalWidth_ - innerPadding * 2); + renderer.drawText(UI_10_FONT_ID, modalX_ + innerPadding, dictionaryNameY, visibleName.c_str()); + } + +#if CROSSINK_APP_CAP_TOUCH + if (!isWordSelectMode && showTouchDictionarySwitch()) { + const Rect buttonRect{modalX_, modalY_ + modalHeight_ - kDictionarySwitchTouchHeight, modalWidth_, + kDictionarySwitchTouchHeight}; + renderer.drawLine(buttonRect.x, buttonRect.y, buttonRect.x + buttonRect.width, buttonRect.y, true); + const char* label = tr(STR_SWITCH_DICTIONARY); + const int labelX = + buttonRect.x + (buttonRect.width - renderer.getTextWidth(UI_10_FONT_ID, label, EpdFontFamily::BOLD)) / 2; + const int labelY = buttonRect.y + (buttonRect.height - renderer.getLineHeight(UI_10_FONT_ID)) / 2; + renderer.drawText(UI_10_FONT_ID, labelX, labelY, label, true, EpdFontFamily::BOLD); + } +#endif + + // Word-select mode: overlay highlighted word(s) and prime snapshot for next frame. + // The -1 prevWordIdx literal is load-bearing: renderHighlightDifferential uses + // prevWordIdx < 0 as the signal "framebuffer was just redrawn from scratch, + // discard any stale snapshot rather than restoring it on top of fresh pixels." + // This is the only path that disturbs the framebuffer outside the differential + // cycle, so it's also the only call site that must pass -1. + if (isWordSelectMode) { + const int currIdx = navigator.getCurrentFlatIndex(); + bool snapshotPrimed = false; + if (currIdx >= 0) { + auto setup = navigator.renderHighlightDifferential(renderer, lineHeight, /*prevWordIdx=*/-1, currIdx); + snapshotPrimed = setup.has_value(); + } + if (!snapshotPrimed) { + navigator.renderHighlight(renderer, lineHeight); + } + + DictUtils::drawWordSelectButtonHints(renderer, mappedInput, navigator); + displayModalBuffer(); + + prevHighlightIdx_ = currIdx; + nextRenderMode_ = snapshotPrimed ? RenderMode::Differential : RenderMode::FullPage; + return; + } + + // View mode: differential state is irrelevant — reset so that the next entry + // into word-select starts cleanly with a full repaint. + nextRenderMode_ = RenderMode::FullPage; + prevHighlightIdx_ = -1; + + // Button hints + const char* btn2 = inlineFailureFeedback ? tr(STR_DONE) : (showLookupButton ? tr(STR_LOOKUP_SHORT) : ""); + const char* btn3 = showLookupButton ? tr(STR_DICT_SWITCH) : ""; + const char* btn4 = ""; + const auto labels = mappedInput.mapLabels(tr(STR_BACK), btn2, btn3, btn4); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + if (hasModalBackground()) { + displayModalBuffer(); + } else { + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + } + + // Skip the full-screen grayscale anti-aliasing overlay here. This modal is + // short-lived, and the 48KB BW backup it needs can fragment the heap enough + // that the reader's largest free block does not recover after returning. +} diff --git a/src/activities/reader/DictionaryDefinitionActivity.h b/src/activities/reader/DictionaryDefinitionActivity.h new file mode 100644 index 0000000000..61a3e56ea1 --- /dev/null +++ b/src/activities/reader/DictionaryDefinitionActivity.h @@ -0,0 +1,232 @@ +#pragma once +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../Activity.h" +#include "util/DictLayout.h" +#include "util/DictionaryLookupController.h" +#include "util/IpaUtils.h" +#include "util/LookupChain.h" +#include "util/LookupHistory.h" +#include "util/WordSelectNavigator.h" + +class DictionaryDefinitionActivity final : public Activity { + public: + using BackgroundRenderFn = void (*)(void* context); + + // showLookupButton=true: + // Confirm = enter word-select mode on the definition text (Look Up Word). + // Back (short press) = return to caller (isCancelled=true). + // Touch Back gesture = exit the lookup flow and return to the reader. + // Back (long press, >= LONG_PRESS_MS) = Done — exit to reader (isCancelled=false). + // showLookupButton=false: + // Back/Confirm both return to caller (isCancelled=true). Unchanged from old behaviour. + explicit DictionaryDefinitionActivity( + GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& headword, const DictLocation& location, + bool showLookupButton = false, std::string bookCachePath = "", bool recordHistory = false, + std::string historyWord = "", LookupHistory::Status historyStatus = LookupHistory::Status::NotFound, + void* backgroundContext = nullptr, BackgroundRenderFn backgroundRender = nullptr, + const char* dictionaryFontFamilyName = nullptr, uint8_t dictionaryFontPointSize = 0, + bool modalBackgroundAlreadyPrepared = false, + WordSelectNavigator::HighlightSnapshotStorage* sharedHighlightSnapshotStorage = nullptr) + : Activity("DictionaryDefinition", renderer, mappedInput), + headword(headword), + foundLocation(location), + showLookupButton(showLookupButton), + cachePath(std::move(bookCachePath)), + recordHistory(recordHistory), + historyWord(std::move(historyWord)), + historyStatus(historyStatus), + backgroundContext_(backgroundContext), + backgroundRender_(backgroundRender), + dictionaryFontFamilyName_(dictionaryFontFamilyName), + dictionaryFontPointSize_(dictionaryFontPointSize), + skipInitialModalBackgroundRedraw_(modalBackgroundAlreadyPrepared), + hasSharedHighlightSnapshotStorage_(sharedHighlightSnapshotStorage != nullptr), + controller(renderer, mappedInput, *this, cachePath) { + navigator.setHighlightSnapshotStorage(sharedHighlightSnapshotStorage); + controller.setLookupToastEnabled(!backgroundRender); + } + + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; + + private: + std::string headword; + DictLocation foundLocation; + bool showLookupButton; + std::string cachePath; + bool recordHistory; + std::string historyWord; + LookupHistory::Status historyStatus; + // Cross-definition back-navigation stack (compact: history-index + page per + // entry, not owned strings). pendingBack_ carries the popped entry from the + // Back keypress to the async FoundDefinition that completes the re-lookup. + LookupChain chain_; + LookupChain::Entry pendingBack_{}; + bool chainBackNavInProgress = false; + bool dictionarySwitchLookupInProgress = false; + bool exitAllOnBackRelease_ = false; + // The reader activity beneath this modal owns the page and outlives us. A + // small callback lets it redraw that page without retaining a second 48 KB + // framebuffer while dictionary parsing is active. + void* backgroundContext_ = nullptr; + BackgroundRenderFn backgroundRender_ = nullptr; + // Non-owning pointer to EpubReaderActivity's fixed per-book settings. + const char* dictionaryFontFamilyName_ = nullptr; + // Zero keeps the dictionary at the reader's active physical point size. + uint8_t dictionaryFontPointSize_ = 0; + bool skipInitialModalBackgroundRedraw_ = false; + bool hasSharedHighlightSnapshotStorage_ = false; + // The framebuffer retains the book pixels outside the opaque modal. Normal + // page turns keep this false and redraw only the modal; screens and overlays + // that replace unrelated pixels set it true to rebuild the book. + bool modalBackgroundNeedsRedraw_ = true; + int modalX_ = 0; + int modalY_ = 0; + int modalWidth_ = 0; + int modalHeight_ = 0; + // Chained definitions may grow this frame but never shrink it. Keeping old + // modal pixels covered avoids a reader-background redraw and the associated + // reader/dictionary SD-font swap for ordinary lookup chaining. + int modalSessionHeight_ = 0; + // Erasing a prior black modal border with FAST_REFRESH can leave that border + // visible on e-ink. Consume this on the next complete modal paint; it costs + // one byte of activity state and does not allocate another framebuffer. + bool modalCleanRefreshNeeded_ = false; + std::string dictionaryName_; + + // Resident page representation (Stage 2b-pool). Segments reference text by + // {offset, len} into pagePool_ instead of owning a std::string each — the + // Wrapper already merged same-style runs, so each segment is one pooled, + // null-terminated entry (kerning preserved, valid for C-API drawText). + struct PooledSegment { + uint16_t offset = 0; // into pagePool_ + uint16_t len = 0; + EpdFontFamily::Style style = EpdFontFamily::REGULAR; + bool isIpa = false; + }; + struct PooledLine { + uint16_t firstSegment = 0; + uint16_t segmentCount = 0; + uint8_t indentLevel = 0; + bool isListItem = false; + }; + + // layoutLines holds ONLY the current page's lines (Stage 2a streaming). render + // and extractWordsFromLayout index it from 0; loadPage() refills it per turn. + // pagePool_ backs all segment text for the resident page. + std::vector layoutLines; + std::vector layoutSegments; + std::string pagePool_; + int currentPage = 0; + int linesPerPage = 0; + int totalPages = 0; + uint32_t definitionOffset_ = 0; + uint32_t definitionSize_ = 0; + bool definitionIsHtml_ = false; + bool definitionHtmlNeedsPlainFallback_ = false; + bool definitionReadFailed_ = false; + // Kept per lookup so a failed SD-font prewarm can use the matching built-in + // reader font without changing the user's selected font setting. + int definitionFontId_ = 0; + enum class DefinitionFontSource { Dictionary, Reader, BuiltIn }; + DefinitionFontSource definitionFontSource_ = DefinitionFontSource::Reader; + + // SD-font layout needs advance widths before the .dict stream is opened for + // wrapping. This fixed 1 KB buffer lives inside the heap-owned activity (not + // on the small task stack) and matches the SD font's persistent-cache budget. + static constexpr uint16_t kDefinitionAdvanceCodepointCapacity = 256; + std::array definitionAdvanceCodepoints_{}; + uint16_t definitionAdvanceCodepointCount_ = 0; + uint8_t definitionAdvanceStyleMask_ = 0; + bool definitionAdvanceCodepointsTruncated_ = false; + // Reused RTL shaping scratch; BidiUtils caps input at 512 codepoints, so the + // retained high-water capacity is about 2 KB in the worst UTF-8 case. + std::string definitionAdvanceVisualText_; + + // Reused across page turns (3.1-A): avoids re-allocating the renderer object + + // its parser/buffers on every loadPage. A value member is fine — the activity + // is heap-allocated, so this lives on the heap. reset()+re-feed each turn (NOT + // kept alive mid-parse; that is the won't-fixed 2c). + DictHtmlRenderer htmlRenderer_; + + // Page-collector state (used by collectLineSink during a wrap pass): keep only + // collectTargetPage_'s lines into layoutLines, counting all lines produced. + int collectTargetPage_ = 0; + int collectLineCount_ = 0; + + // Orientation-aware layout gutters (computed in wrapText, used in render and extractWordsFromLayout) + int leftPadding = 20; + int rightPadding = 20; + int hintGutterHeight = 0; + int contentX = 0; + int hintGutterWidth = 0; + int bodyStartY = 0; // top of the text body (set in wrapText) + + // Word-select mode (activated by pressing Look Up Word in view mode) + bool isWordSelectMode = false; + WordSelectNavigator navigator; + // History-launched definitions have no parent snapshot to borrow. Allocate + // the same bounded storage only if the user enters definition word-select. + std::unique_ptr ownedHighlightSnapshotStorage_; + DictionaryLookupController controller; +#if CROSSINK_APP_CAP_TOUCH + bool touchDragLookup_ = false; +#endif + + // Differential repaint state for in-definition word-select mode. Only consulted + // when isWordSelectMode is true; reset on every view-mode render. + enum class RenderMode { FullPage, Differential }; + RenderMode nextRenderMode_ = RenderMode::FullPage; + int prevHighlightIdx_ = -1; + + bool skipLoopDelay() override { return controller.skipLoopDelay(); } + + void wrapText(); + // Re-parse the definition and lay out ONLY page `page` into layoutLines, + // discarding other pages as they are produced; also recomputes totalPages. + void loadPage(int page); + void wrapHtml(); + void wrapPlain(); + void prepareDefinitionFontAdvances(); + void collectDefinitionAdvanceText(const char* text, EpdFontFamily::Style style); + static void collectSpanForAdvances(void* ctx, const StyledSpan& span); + void extractWordsFromLayout(); + void openDictionarySwitch(); +#if CROSSINK_APP_CAP_TOUCH + bool showTouchDictionarySwitch() const; + bool dictionarySwitchButtonContains(int x, int y) const; +#endif + int dictionaryFooterHeight() const; + bool hasModalBackground() const { return backgroundContext_ && backgroundRender_; } + void sizeModalForCurrentPage(); + void drawModalFrame() const; + int getMixedWidth(std::vector& ipaRuns, const char* text, EpdFontFamily::Style style); + bool definitionTextNeedsApproximation(const char* text) const; + std::string approximateDefinitionText(const char* text, bool inEtymologyTree) const; + bool shouldApproximateDefinitionCodepoint(uint32_t cp) const; + // Width measurement adapter injected into DictLayout::wrapSpans. ctx is `this`. + static int measureWidthAdapter(void* ctx, const char* text, EpdFontFamily::Style style, bool isIpa); + // Line sink injected into DictLayout::wrapSpans: keeps collectTargetPage_'s + // lines, counts the rest. ctx is `this`. + static void collectLineSink(void* ctx, const DictLayout::LayoutLineView& line); + // Span sink bridge: sanitizes and forwards each streamed span into the DictLayout::Wrapper. + static void feedSpanToWrapper(void* ctx, const StyledSpan& span); + bool handleLongPressExitAll(bool enabled); + int getDefinitionFontId(bool isIpa = false) const; + void useBuiltInDefinitionFontFallback(); + void reflowForDefinitionFontChange(); + void redrawModalBackground(); + void displayModalBuffer(); + int getLineHeight() const; +}; diff --git a/src/activities/reader/DictionarySuggestionsActivity.cpp b/src/activities/reader/DictionarySuggestionsActivity.cpp new file mode 100644 index 0000000000..e3ba40ad25 --- /dev/null +++ b/src/activities/reader/DictionarySuggestionsActivity.cpp @@ -0,0 +1,143 @@ +#include "DictionarySuggestionsActivity.h" + +#include +#include + +#include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" +#include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" +#include "util/DictionaryActivityUtils.h" + +namespace fui = freeink::ui; + +DictionarySuggestionsActivity::DictionarySuggestionsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + std::vector suggestions) + : Activity("DictionarySuggestions", renderer, mappedInput), + suggestions(std::move(suggestions)), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + +void DictionarySuggestionsActivity::onEnter() { + Activity::onEnter(); + selectedIndex = 0; + topIndex = 0; + visibleRows = 1; + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &DictionarySuggestionsActivity::onRowEvent, this); + app.setScreen(&DictionarySuggestionsActivity::suggestionsScreen, this); + uiItems.clear(); + uiItems.reserve(suggestions.size()); + for (size_t i = 0; i < suggestions.size(); ++i) { + fui::ListItem item; + item.label = suggestions[i].c_str(); + item.actionValue = static_cast(i); + uiItems.push_back(item); + } + requestUpdate(); +} + +void DictionarySuggestionsActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(self->suggestions.size())) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->setResult(WordResult{self->suggestions[self->selectedIndex]}); + self->finish(); +} + +void DictionarySuggestionsActivity::suggestionsScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildSuggestionsScreen(screen); +} + +void DictionarySuggestionsActivity::buildSuggestionsScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + + metrics.verticalSpacing), + 0, static_cast(metrics.buttonHintsHeight + metrics.verticalSpacing), 0}); + + fui::ListProps props; + props.items = uiItems.data(); + props.count = static_cast(uiItems.size()); + props.selectedIndex = static_cast(selectedIndex); + props.topIndex = static_cast(topIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.labelText = screen.theme().bodyText; + props.labelText.maxLines = 2; + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(suggestions.size())); + props.topIndex = static_cast(topIndex); + screen.list(props); +} + +void DictionarySuggestionsActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + DictUtils::cancelAndFinish(*this); + return; + } + + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } + + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, static_cast(suggestions.size())); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + setResult(WordResult{suggestions[selectedIndex]}); + finish(); + return; + } + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + DictUtils::cancelAndFinish(*this); + return; + } + const bool prevItem = DictUtils::dictionaryPageButtonTriggered(mappedInput, true) || + mappedInput.wasReleased(MappedInputManager::Button::Left); + const bool nextItem = DictUtils::dictionaryPageButtonTriggered(mappedInput, false) || + mappedInput.wasReleased(MappedInputManager::Button::Right); + if (prevItem && selectedIndex > 0) { + selectedIndex--; + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, static_cast(suggestions.size())); + requestUpdate(); + } + if (nextItem && selectedIndex < static_cast(suggestions.size()) - 1) { + selectedIndex++; + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, static_cast(suggestions.size())); + requestUpdate(); + } +} + +void DictionarySuggestionsActivity::render(RenderLock&&) { + renderer.clearScreen(); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_DICT_DID_YOU_MEAN), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_DICT_DID_YOU_MEAN)); + } + uiReady = false; + app.render(); + uiReady = true; + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); +} diff --git a/src/activities/reader/DictionarySuggestionsActivity.h b/src/activities/reader/DictionarySuggestionsActivity.h new file mode 100644 index 0000000000..75851b9779 --- /dev/null +++ b/src/activities/reader/DictionarySuggestionsActivity.h @@ -0,0 +1,35 @@ +#pragma once +#include +#include + +#include +#include + +#include "../Activity.h" + +class DictionarySuggestionsActivity final : public Activity { + public: + explicit DictionarySuggestionsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + std::vector suggestions); + + void onEnter() override; + void loop() override; + void render(RenderLock&&) override; + + private: + using UiApp = freeink::ui::FreeInkApp<20, 4>; + static constexpr freeink::ui::ActionId ACTION_ROW = 1; + + static void suggestionsScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildSuggestionsScreen(UiApp::ScreenType& screen); + + std::vector suggestions; + std::vector uiItems; + int selectedIndex = 0; + freeink::ui::GfxRendererTarget uiTarget; + UiApp app; + bool uiReady = false; + int visibleRows = 1; + int topIndex = 0; +}; diff --git a/src/activities/reader/DictionaryWordSelectActivity.cpp b/src/activities/reader/DictionaryWordSelectActivity.cpp new file mode 100644 index 0000000000..90f68e43df --- /dev/null +++ b/src/activities/reader/DictionaryWordSelectActivity.cpp @@ -0,0 +1,1050 @@ +#include "DictionaryWordSelectActivity.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "../settings/DictionarySelectActivity.h" +#include "CrossPointSettings.h" +#include "DictionaryDefinitionActivity.h" +#include "MappedInputManager.h" +#include "Memory.h" +#include "components/UITheme.h" +#include "fontIds.h" +#include "util/Dictionary.h" +#include "util/DictionaryActivityUtils.h" + +namespace { + +// Soft-hyphen U+00AD encoded as 2 UTF-8 bytes. Layout (ParsedText.cpp:19) +// strips these before measurement, so we mirror that here — otherwise +// derived word widths include the soft-hyphen glyph's advance and the +// highlight rectangle overruns into the inter-word gap. +constexpr char SOFT_HYPHEN_UTF8[] = "\xC2\xAD"; +constexpr size_t SOFT_HYPHEN_BYTES = 2; +constexpr size_t WORD_SELECT_ALLOCATION_HEADROOM = 8U * 1024U; +constexpr size_t ADVANCE_CODEPOINT_CAPACITY = SdCardFont::MAX_PAGE_GLYPHS; + +struct WorkingSetBudget { + size_t wordCount = 0; + size_t rowCount = 0; + size_t textBytes = 0; + size_t maxSourceWordBytes = 0; +}; + +struct WordPartRef { + const char* text = nullptr; + size_t length = 0; + size_t sourceOffset = 0; +}; + +bool isDashSeparator(const char* text, const size_t length, const size_t offset) { + return offset + 2 < length && static_cast(text[offset]) == 0xE2 && + static_cast(text[offset + 1]) == 0x80 && + (static_cast(text[offset + 2]) == 0x93 || static_cast(text[offset + 2]) == 0x94); +} + +bool containsDashSeparator(const char* text, const size_t length) { + for (size_t i = 0; i < length; ++i) { + if (isDashSeparator(text, length, i)) return true; + } + return false; +} + +template +void forEachWordPart(const char* text, const size_t length, Sink&& sink) { + size_t partStart = 0; + for (size_t i = 0; i < length;) { + if (!isDashSeparator(text, length, i)) { + i++; + continue; + } + if (i > partStart) sink(WordPartRef{text + partStart, i - partStart, partStart}); + i += 3; + partStart = i; + } + if (partStart < length) sink(WordPartRef{text + partStart, length - partStart, partStart}); +} + +bool addBudgetBytes(size_t& total, const size_t bytes) { + if (bytes > std::numeric_limits::max() - total) return false; + total += bytes; + return true; +} + +const char* withoutSoftHyphens(const char* word, const size_t length, char* scratch, const size_t scratchCapacity) { + if (!word || !scratch || scratchCapacity == 0) return word; + bool hasSoftHyphen = false; + for (size_t i = 0; i + 1 < length; ++i) { + if (word[i] == SOFT_HYPHEN_UTF8[0] && word[i + 1] == SOFT_HYPHEN_UTF8[1]) { + hasSoftHyphen = true; + break; + } + } + if (!hasSoftHyphen) return word; + + size_t out = 0; + for (size_t i = 0; i < length && out + 1 < scratchCapacity;) { + if (i + 1 < length && word[i] == SOFT_HYPHEN_UTF8[0] && word[i + 1] == SOFT_HYPHEN_UTF8[1]) { + i += SOFT_HYPHEN_BYTES; + continue; + } + scratch[out++] = word[i++]; + } + scratch[out] = '\0'; + return scratch; +} + +int16_t measureWordAdvanceX(const GfxRenderer& renderer, const int fontId, const char* word, const size_t length, + const EpdFontFamily::Style style, char* scratch, const size_t scratchCapacity) { + const char* measured = withoutSoftHyphens(word, length, scratch, scratchCapacity); + return static_cast(renderer.getTextAdvanceX(fontId, measured, style)); +} + +int16_t measureWordAdvanceX(const GfxRenderer& renderer, const int fontId, const char* word, const size_t length, + const EpdFontFamily::Style style, const uint8_t bionicBoundary, + const uint16_t bionicSuffixX, char* scratch, const size_t scratchCapacity) { + if (bionicBoundary == 0 || bionicSuffixX == 0) { + return measureWordAdvanceX(renderer, fontId, word, length, style, scratch, scratchCapacity); + } + const size_t suffixStart = std::min(bionicBoundary, length); + return static_cast(bionicSuffixX + renderer.getTextAdvanceX(fontId, word + suffixStart, style)); +} + +int16_t measureWordAdvanceX(const GfxRenderer& renderer, const int fontId, const char* word, const size_t length, + const EpdFontFamily::Style style, const uint8_t bionicBoundary, + const uint16_t bionicRunOffset, const bool wordIsRtl, char* scratch, + const size_t scratchCapacity) { + if (!wordIsRtl || bionicBoundary == 0 || bionicRunOffset == 0) { + return measureWordAdvanceX(renderer, fontId, word, length, style, bionicBoundary, bionicRunOffset, scratch, + scratchCapacity); + } + + const auto boldStyle = static_cast(style | EpdFontFamily::BOLD); + char boldBuf[40]; + const size_t boldLen = std::min({static_cast(bionicBoundary), length, sizeof(boldBuf) - 1}); + memcpy(boldBuf, word, boldLen); + boldBuf[boldLen] = '\0'; + return static_cast(bionicRunOffset + renderer.getTextAdvanceX(fontId, boldBuf, boldStyle)); +} + +bool isRtlWord(const char* word, const bool fallbackRtl) { + return BidiUtils::detectParagraphLevel(word, fallbackRtl ? 1 : 0) == 1; +} + +// Single-style prewarm/advance-table bitmask: bit 0 = REGULAR, 1 = BOLD, +// 2 = ITALIC, 3 = BOLD_ITALIC. The `& 0x03` is defensive — Style enum +// is two bits, but UNDERLINE etc. live in higher bits if ever OR'd in. +constexpr uint8_t styleToBitMask(EpdFontFamily::Style style) { + return static_cast(1u << (static_cast(style) & 0x03)); +} + +} // namespace + +void DictionaryWordSelectActivity::onEnter() { + Activity::onEnter(); + Dictionary::clearLookupDictPathOverride(); + mappedInput.setReaderTouchscreenOverride(true); + ignoreInitialBackRelease_ = mappedInput.isPressed(MappedInputManager::Button::Back); + const bool consumeInitialConfirm = mappedInput.isPressed(MappedInputManager::Button::Confirm); + if (!buildWorkingSet(consumeInitialConfirm)) { + if (workingSetMemoryError_) { + GUI.drawPopup(renderer, tr(STR_MEMORY_ERROR)); + renderer.displayBuffer(); + delay(1000); + } + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return; + } + autoLookupInitialWord_ = false; + requestUpdate(); +} + +bool DictionaryWordSelectActivity::buildWorkingSet(const bool consumeInitialConfirm) { + workingSetMemoryError_ = false; + if (!page) { + LOG_ERR("DICT", "Cannot build word selection without a reader page"); + return false; + } + navigator.releaseWorkingSet(); + workingSet_.clear(); + if (!allocateWorkingSet()) { + workingSetMemoryError_ = true; + return false; + } + prebuildAdvanceTable(); + if (!extractWords() || !mergeHyphenatedWords()) { + workingSetMemoryError_ = true; + navigator.releaseWorkingSet(); + workingSet_.clear(); + return false; + } + navigator.loadView(workingSet_.words.get(), workingSet_.wordCount, workingSet_.rows.get(), workingSet_.rowCount, + workingSet_.textPool.get(), consumeInitialConfirm); +#if CROSSINK_APP_CAP_TOUCH + navigator.setTouchDragCursorVisible(mappedInput.hasTouch()); + bool initialTouchHit = false; + if (initialTouchX_ >= 0 && initialTouchY_ >= 0) { + navigator.selectWordAtPoint(initialTouchX_, initialTouchY_, renderer.getLineHeight(SETTINGS.getReaderFontId()), + &initialTouchHit); + } + if (autoLookupInitialWord_) { + const auto* selected = initialTouchHit ? navigator.getSelected() : nullptr; + if (!selected) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return false; + } + touchDragLookup_ = navigator.beginTouchMultiSelect(); + } +#else + navigator.setTouchDragCursorVisible(false); +#endif + return true; +} + +void DictionaryWordSelectActivity::suspendWorkingSet() { + if (workingSetSuspended_ || !readerPageReload_) return; + if (const auto* selected = navigator.getSelected()) { + suspendedSelectionX_ = selected->screenX + selected->width / 2; + suspendedSelectionY_ = selected->screenY + renderer.getLineHeight(SETTINGS.getReaderFontId()) / 2; + } + navigator.releaseWorkingSet(); + workingSet_.clear(); + page.reset(); + workingSetSuspended_ = true; + MemoryBudget::logHeapShape("dict.parent_suspended"); +} + +bool DictionaryWordSelectActivity::restoreWorkingSet() { + if (!workingSetSuspended_) return true; + page = readerPageReload_(readerContext_); + if (!page) { + LOG_ERR("DICT", "Failed to reload reader page after dictionary definition"); + return false; + } + if (!buildWorkingSet(/*consumeInitialConfirm=*/false)) return false; + if (suspendedSelectionX_ >= 0 && suspendedSelectionY_ >= 0) { + navigator.selectWordAtPoint(suspendedSelectionX_, suspendedSelectionY_, + renderer.getLineHeight(SETTINGS.getReaderFontId())); + } + workingSetSuspended_ = false; + MemoryBudget::logHeapShape("dict.parent_restored"); + return true; +} + +void DictionaryWordSelectActivity::onExit() { + controller.onExit(); + navigator.releaseWorkingSet(); + workingSet_.clear(); + Dictionary::clearLookupDictPathOverride(); + mappedInput.setReaderTouchscreenOverride(false); + const auto& sdFonts = renderer.getSdCardFonts(); + auto it = sdFonts.find(SETTINGS.getReaderFontId()); + if (it != sdFonts.end()) it->second->clearPersistentCache(); + Activity::onExit(); +} + +void DictionaryWordSelectActivity::prewarmHighlightGlyphs(int currIdx) { + const auto* w = navigator.getWordAt(currIdx); + if (!w) return; + auto* fcm = renderer.getFontCacheManager(); + if (!fcm) return; + uint8_t styleMask = styleToBitMask(w->style); + if (w->bionicBoundary > 0) { + styleMask |= styleToBitMask(static_cast(w->style | EpdFontFamily::BOLD)); + } + fcm->prewarmCache(SETTINGS.getReaderFontId(), navigator.getDisplay(*w), styleMask); +} + +void DictionaryWordSelectActivity::prebuildAdvanceTable() { + if (!renderer.isSdCardFont(SETTINGS.getReaderFontId())) return; + + // A page can contain hundreds of distinct glyphs, so this 2 KB collector is + // too large for the render-task stack. Allocate it fallibly and release it + // before extraction; a failure only loses the SD-font speed-up. + auto codepoints = makeUniqueNoThrow(ADVANCE_CODEPOINT_CAPACITY); + if (!codepoints) { + const auto heap = MemoryBudget::snapshot(); + LOG_ERR("DICT", "OOM allocating advance collector (%u bytes, free=%u maxAlloc=%u)", + static_cast(ADVANCE_CODEPOINT_CAPACITY * sizeof(uint32_t)), heap.freeHeap, heap.maxAllocHeap); + return; + } + const uint32_t* const codepointData = codepoints.get(); + + uint16_t codepointCount = 0; + uint8_t pageStyleMask = 0; + bool truncated = false; + for (const auto& element : page->elements) { + if (element->getTag() != TAG_PageLine) continue; + const auto* line = static_cast(element.get()); + const auto& block = line->getBlock(); + if (!block) continue; + for (uint16_t i = 0; i < block->wordCount(); i++) { + const auto* cursor = reinterpret_cast(block->wordText(i)); + uint32_t codepoint = 0; + while ((codepoint = utf8NextCodepoint(&cursor))) { + if (std::find(codepointData, codepointData + codepointCount, codepoint) != codepointData + codepointCount) { + continue; + } + if (codepointCount >= ADVANCE_CODEPOINT_CAPACITY) { + truncated = true; + break; + } + codepoints[codepointCount++] = codepoint; + } + pageStyleMask |= styleToBitMask(block->wordStyle(i)); + } + } + if (pageStyleMask == 0) pageStyleMask = styleToBitMask(EpdFontFamily::REGULAR); + if (truncated) { + LOG_ERR("DICT", "SD-font advance collector cap hit (%u); remaining glyphs will load on demand", + static_cast(ADVANCE_CODEPOINT_CAPACITY)); + } + if (codepointCount > 0) { + // Use the codepoint overload so prewarm itself has no growing std::string. + // RTL presentation forms that are absent here still use the normal + // fallible on-demand font path during measurement. + renderer.ensureSdCardFontReady(SETTINGS.getReaderFontId(), codepointData, codepointCount, + /*includeSpace=*/true, /*includeHyphen=*/true, pageStyleMask); + } +} + +void DictionaryWordSelectActivity::clearFrontButtonHintArea() { + const auto& metrics = UITheme::getInstance().getMetrics(); + const int hintSize = metrics.buttonHintsHeight; + const int screenWidth = renderer.getScreenWidth(); + const int screenHeight = renderer.getScreenHeight(); + + switch (renderer.getOrientation()) { + case GfxRenderer::Orientation::Portrait: + renderer.fillRect(0, screenHeight - hintSize, screenWidth, hintSize, false); + break; + case GfxRenderer::Orientation::LandscapeClockwise: + renderer.fillRect(0, 0, hintSize, screenHeight, false); + break; + case GfxRenderer::Orientation::PortraitInverted: + renderer.fillRect(0, 0, screenWidth, hintSize, false); + break; + case GfxRenderer::Orientation::LandscapeCounterClockwise: + renderer.fillRect(screenWidth - hintSize, 0, hintSize, screenHeight, false); + break; + } +} + +void DictionaryWordSelectActivity::renderDefinitionBackground() { + if (!page) { + LOG_ERR("DICT", "Cannot redraw dictionary background without a reader page"); + return; + } + renderer.clearScreen(); + + // Dictionary layout can evict the reader font's bitmap glyph cache. Rebuild + // it before redrawing the page behind the modal; the persistent advance + // table only preserves glyph widths, not the bitmaps themselves. + auto* fcm = renderer.getFontCacheManager(); + auto scope = fcm->createPrewarmScope(); + page->render(renderer, SETTINGS.getReaderFontId(), marginLeft, marginTop); // scan pass + scope.endScanAndPrewarm(); + page->render(renderer, SETTINGS.getReaderFontId(), marginLeft, marginTop); +} + +void DictionaryWordSelectActivity::renderDefinitionBackgroundCallback(void* context) { + static_cast(context)->renderDefinitionBackground(); +} + +bool DictionaryWordSelectActivity::allocateWorkingSet() { + WorkingSetBudget budget; + bool valid = true; + bool haveRow = false; + int16_t currentRowY = 0; + WordPartRef previousRowLast{}; + + const auto addMergedBudget = [&](const WordPartRef& first, const WordPartRef& second, const bool stripLeadingSecond) { + if (first.length == 0 || first.text[first.length - 1] != '-' || first.text[0] == '-') return; + const size_t secondSkip = stripLeadingSecond && second.length > 0 && second.text[0] == '-' ? 1 : 0; + const size_t mergedLength = first.length - 1 + second.length - secondSkip; + if (mergedLength > UINT16_MAX || !addBudgetBytes(budget.textBytes, mergedLength + 1)) valid = false; + }; + + for (const auto& element : page->elements) { + if (element->getTag() != TAG_PageLine) continue; + const auto* line = static_cast(element.get()); + const auto& block = line->getBlock(); + if (!block) continue; + const int16_t screenY = static_cast( + line->yPos + marginTop + block->getRubyShift(renderer.getFontAscenderSize(SETTINGS.getReaderFontId()))); + for (uint16_t wordIndex = 0; wordIndex < block->wordCount(); ++wordIndex) { + const char* word = block->wordText(wordIndex); + const size_t wordLength = block->wordTextLen(wordIndex); + budget.maxSourceWordBytes = std::max(budget.maxSourceWordBytes, wordLength + 1); + if (wordLength > UINT16_MAX || !utf8ContainsLookupCharacter(word)) continue; + + forEachWordPart(word, wordLength, [&](const WordPartRef& part) { + if (!valid || part.length == 0 || part.length > UINT16_MAX || budget.wordCount >= UINT16_MAX) { + valid = false; + return; + } + budget.wordCount++; + if (!addBudgetBytes(budget.textBytes, part.length + 1)) { + valid = false; + return; + } + + if (!haveRow) { + haveRow = true; + currentRowY = screenY; + } else if (std::abs(static_cast(screenY) - static_cast(currentRowY)) > 2) { + budget.rowCount++; + addMergedBudget(previousRowLast, part, /*stripLeadingSecond=*/true); + currentRowY = screenY; + } + previousRowLast = part; + }); + } + } + if (haveRow) budget.rowCount++; + if (budget.rowCount > static_cast(INT16_MAX)) valid = false; + if (haveRow && !nextPageFirstWord.empty()) { + const WordPartRef next{nextPageFirstWord.c_str(), nextPageFirstWord.size(), 0}; + addMergedBudget(previousRowLast, next, /*stripLeadingSecond=*/false); + } + + constexpr size_t MAX_TEXT_POOL_BYTES = static_cast(UINT16_MAX) + 1U; + if (!valid || budget.textBytes > MAX_TEXT_POOL_BYTES || + budget.maxSourceWordBytes > std::numeric_limits::max() / 2U) { + LOG_ERR("DICT", "Word selection page exceeds bounded working-set limits"); + return false; + } + + const size_t wordBytes = budget.wordCount * sizeof(WordSelectNavigator::WordInfo); + const size_t rowBytes = budget.rowCount * sizeof(WordSelectNavigator::Row); + const size_t scratchBytes = budget.maxSourceWordBytes * 2U; + size_t totalBytes = 0; + valid = addBudgetBytes(totalBytes, wordBytes) && addBudgetBytes(totalBytes, rowBytes) && + addBudgetBytes(totalBytes, budget.textBytes) && addBudgetBytes(totalBytes, scratchBytes); + if (!valid) { + LOG_ERR("DICT", "Word selection working-set size overflow"); + return false; + } + + const size_t largestBlock = std::max({wordBytes, rowBytes, budget.textBytes, scratchBytes}); + auto heap = MemoryBudget::snapshot(); + if (heap.maxAllocHeap < largestBlock || + heap.freeHeap < + totalBytes + std::min(WORD_SELECT_ALLOCATION_HEADROOM, std::numeric_limits::max() - totalBytes)) { + const auto before = heap; + if (renderer.releaseSdCardFontForLowMemory(SETTINGS.getReaderFontId())) { + heap = MemoryBudget::snapshot(); + LOG_DBG("DICT", "Released reader SD-font caches for %u-byte working set: free=%u->%u maxAlloc=%u->%u", + static_cast(totalBytes), before.freeHeap, heap.freeHeap, before.maxAllocHeap, + heap.maxAllocHeap); + } + } + + workingSet_.wordCapacity = budget.wordCount; + workingSet_.rowCapacity = budget.rowCount; + workingSet_.textCapacity = budget.textBytes; + workingSet_.measurementScratchCapacity = scratchBytes; + + if (budget.wordCount > 0) { + workingSet_.words = makeUniqueNoThrow(budget.wordCount); + if (!workingSet_.words) { + heap = MemoryBudget::snapshot(); + LOG_ERR("DICT", "OOM allocating word metadata (%u bytes, free=%u maxAlloc=%u)", static_cast(wordBytes), + heap.freeHeap, heap.maxAllocHeap); + workingSet_.clear(); + return false; + } + } + if (budget.textBytes > 0) { + workingSet_.textPool = makeUniqueNoThrow(budget.textBytes); + if (!workingSet_.textPool) { + heap = MemoryBudget::snapshot(); + LOG_ERR("DICT", "OOM allocating word text arena (%u bytes, free=%u maxAlloc=%u)", + static_cast(budget.textBytes), heap.freeHeap, heap.maxAllocHeap); + workingSet_.clear(); + return false; + } + } + if (budget.rowCount > 0) { + workingSet_.rows = makeUniqueNoThrow(budget.rowCount); + if (!workingSet_.rows) { + heap = MemoryBudget::snapshot(); + LOG_ERR("DICT", "OOM allocating row metadata (%u bytes, free=%u maxAlloc=%u)", static_cast(rowBytes), + heap.freeHeap, heap.maxAllocHeap); + workingSet_.clear(); + return false; + } + } + if (scratchBytes > 0) { + workingSet_.measurementScratch = makeUniqueNoThrow(scratchBytes); + if (!workingSet_.measurementScratch) { + heap = MemoryBudget::snapshot(); + LOG_ERR("DICT", "OOM allocating word measurement scratch (%u bytes, free=%u maxAlloc=%u)", + static_cast(scratchBytes), heap.freeHeap, heap.maxAllocHeap); + workingSet_.clear(); + return false; + } + } + return true; +} + +bool DictionaryWordSelectActivity::appendText(const char* text, const size_t length, uint16_t& offset) { + if (!text || length > UINT16_MAX || workingSet_.textUsed > UINT16_MAX || + length + 1 > workingSet_.textCapacity - workingSet_.textUsed) { + LOG_ERR("DICT", "Word text arena overflow (used=%u capacity=%u append=%u)", + static_cast(workingSet_.textUsed), static_cast(workingSet_.textCapacity), + static_cast(length + 1)); + return false; + } + offset = static_cast(workingSet_.textUsed); + char* const textPool = workingSet_.textPool.get(); + memcpy(textPool + workingSet_.textUsed, text, length); + workingSet_.textPool[workingSet_.textUsed + length] = '\0'; + workingSet_.textUsed += length + 1; + return true; +} + +bool DictionaryWordSelectActivity::appendMergedText(const char* first, const size_t firstLength, const char* second, + const size_t secondLength, uint16_t& offset) { + const size_t mergedLength = firstLength + secondLength; + if (!first || !second || mergedLength > UINT16_MAX || workingSet_.textUsed > UINT16_MAX || + mergedLength + 1 > workingSet_.textCapacity - workingSet_.textUsed) { + LOG_ERR("DICT", "Merged word text exceeds arena (used=%u capacity=%u append=%u)", + static_cast(workingSet_.textUsed), static_cast(workingSet_.textCapacity), + static_cast(mergedLength + 1)); + return false; + } + offset = static_cast(workingSet_.textUsed); + char* const textPool = workingSet_.textPool.get(); + char* destination = textPool + workingSet_.textUsed; + memcpy(destination, first, firstLength); + memcpy(destination + firstLength, second, secondLength); + destination[mergedLength] = '\0'; + workingSet_.textUsed += mergedLength + 1; + return true; +} + +bool DictionaryWordSelectActivity::appendWord(WordSelectNavigator::WordInfo word) { + if (workingSet_.wordCount >= workingSet_.wordCapacity) { + LOG_ERR("DICT", "Word metadata capacity exceeded"); + return false; + } + if (workingSet_.rowCount == 0 || std::abs(static_cast(word.screenY) - + static_cast(workingSet_.rows[workingSet_.rowCount - 1].yPos)) > 2) { + if (workingSet_.rowCount >= workingSet_.rowCapacity || workingSet_.wordCount > UINT16_MAX) { + LOG_ERR("DICT", "Row metadata capacity exceeded"); + return false; + } + workingSet_.rows[workingSet_.rowCount] = {word.screenY, static_cast(workingSet_.wordCount), 0}; + workingSet_.rowCount++; + } + auto& row = workingSet_.rows[workingSet_.rowCount - 1]; + if (row.wordCount == UINT16_MAX) { + LOG_ERR("DICT", "Row word count exceeded"); + return false; + } + word.row = static_cast(workingSet_.rowCount - 1); + workingSet_.words[workingSet_.wordCount++] = word; + row.wordCount++; + return true; +} + +bool DictionaryWordSelectActivity::extractWords() { + const size_t scratchHalf = workingSet_.measurementScratchCapacity / 2U; + char* prefixScratch = workingSet_.measurementScratch.get(); + char* sanitizeScratch = prefixScratch ? prefixScratch + scratchHalf : nullptr; + const char* const textPool = workingSet_.textPool.get(); + + // Fallback used by blocks where we can't derive a per-line gap + // (single-word blocks, degenerate first-word measurements). + const int16_t naturalSpaceWidth = + static_cast(renderer.getTextAdvanceX(SETTINGS.getReaderFontId(), " ", EpdFontFamily::REGULAR)); + + for (const auto& element : page->elements) { + if (element->getTag() != TAG_PageLine) continue; + const auto* line = static_cast(element.get()); + const auto& block = line->getBlock(); + if (!block) continue; + + const uint16_t sourceWordCount = block->wordCount(); + const int rubyShift = block->getRubyShift(renderer.getFontAscenderSize(SETTINGS.getReaderFontId())); + int16_t lineGapWidth = naturalSpaceWidth; + if (sourceWordCount >= 2 && block->wordTextLen(0) > 0) { + const char* firstWord = block->wordText(0); + const size_t firstLength = block->wordTextLen(0); + const auto firstStyle = block->wordStyle(0); + const uint8_t firstBionicBoundary = block->bionicBoundary(0); + const uint16_t firstBionicSuffixX = block->bionicRunOffset(0); + const bool firstWordIsRtl = isRtlWord(firstWord, block->getBlockStyle().isRtl); + const int16_t firstWidth = + measureWordAdvanceX(renderer, SETTINGS.getReaderFontId(), firstWord, firstLength, firstStyle, + firstBionicBoundary, firstBionicSuffixX, firstWordIsRtl, sanitizeScratch, scratchHalf); + const int16_t derivedGap = static_cast(block->wordXpos(1) - block->wordXpos(0) - firstWidth); + if (derivedGap > naturalSpaceWidth / 2) lineGapWidth = derivedGap; + } + + int lastSelectableWordIndex = -2; + for (uint16_t wordIndex = 0; wordIndex < sourceWordCount; ++wordIndex) { + const int16_t screenX = line->xPos + block->wordXpos(wordIndex) + marginLeft; + const int16_t screenY = line->yPos + marginTop + rubyShift; + const char* wordText = block->wordText(wordIndex); + const size_t wordLength = block->wordTextLen(wordIndex); + const auto wordStyle = block->wordStyle(wordIndex); + const uint8_t bionicBoundary = block->bionicBoundary(wordIndex); + const uint16_t bionicSuffixX = block->bionicRunOffset(wordIndex); + const bool wordIsRtl = isRtlWord(wordText, block->getBlockStyle().isRtl); + + if (!utf8ContainsLookupCharacter(wordText)) { + lastSelectableWordIndex = -2; + continue; + } + + if (!containsDashSeparator(wordText, wordLength)) { + int16_t wordWidth; + if (bionicBoundary > 0 && bionicSuffixX > 0) { + wordWidth = measureWordAdvanceX(renderer, SETTINGS.getReaderFontId(), wordText, wordLength, wordStyle, + bionicBoundary, bionicSuffixX, wordIsRtl, sanitizeScratch, scratchHalf); + } else if (wordIndex + 1 < sourceWordCount) { + const int16_t raw = static_cast(block->wordXpos(wordIndex + 1) - block->wordXpos(wordIndex)); + wordWidth = std::max(static_cast(1), static_cast(raw - lineGapWidth)); + } else { + wordWidth = measureWordAdvanceX(renderer, SETTINGS.getReaderFontId(), wordText, wordLength, wordStyle, + bionicBoundary, bionicSuffixX, sanitizeScratch, scratchHalf); + } + + bool joinWithoutSpaceBefore = false; + if (lastSelectableWordIndex == static_cast(wordIndex) - 1 && workingSet_.wordCount > 0) { + const uint16_t previousIndex = wordIndex - 1; + const auto previousStyle = block->wordStyle(previousIndex); + const int16_t previousMeasuredWidth = static_cast( + renderer.getTextAdvanceX(SETTINGS.getReaderFontId(), block->wordText(previousIndex), previousStyle)); + const int16_t currentMeasuredWidth = + measureWordAdvanceX(renderer, SETTINGS.getReaderFontId(), wordText, wordLength, wordStyle, bionicBoundary, + bionicSuffixX, wordIsRtl, sanitizeScratch, scratchHalf); + const int currentLeft = screenX; + const int currentRight = screenX + currentMeasuredWidth; + auto& previousWord = workingSet_.words[workingSet_.wordCount - 1]; + const int previousLeft = previousWord.screenX; + const int previousRight = previousLeft + previousMeasuredWidth; + const int gap = currentLeft >= previousLeft ? currentLeft - previousRight : previousLeft - currentRight; + joinWithoutSpaceBefore = gap < naturalSpaceWidth / 2; + if (joinWithoutSpaceBefore) { + previousWord.width = previousMeasuredWidth; + wordWidth = currentMeasuredWidth; + } + } + + uint16_t offset = 0; + if (!appendText(wordText, wordLength, offset)) return false; + WordSelectNavigator::WordInfo word; + word.textOffset = offset; + word.textLen = static_cast(wordLength); + word.lookupOffset = offset; + word.lookupLen = word.textLen; + word.screenX = screenX; + word.screenY = screenY; + word.width = wordWidth; + word.style = wordStyle; + word.fontId = SETTINGS.getReaderFontId(); + word.isRtl = wordIsRtl; + word.joinWithoutSpaceBefore = joinWithoutSpaceBefore; + word.bionicBoundary = bionicBoundary; + word.bionicSuffixX = bionicSuffixX; + if (!appendWord(word)) return false; + lastSelectableWordIndex = wordIndex; + continue; + } + + bool partSucceeded = true; + forEachWordPart(wordText, wordLength, [&](const WordPartRef& part) { + if (!partSucceeded || part.length == 0) return; + int16_t offsetX = 0; + if (part.sourceOffset > 0) { + if (!prefixScratch || part.sourceOffset + 1 > scratchHalf) { + partSucceeded = false; + return; + } + memcpy(prefixScratch, wordText, part.sourceOffset); + prefixScratch[part.sourceOffset] = '\0'; + offsetX = measureWordAdvanceX(renderer, SETTINGS.getReaderFontId(), prefixScratch, part.sourceOffset, + wordStyle, sanitizeScratch, scratchHalf); + } + + uint16_t offset = 0; + if (!appendText(part.text, part.length, offset)) { + partSucceeded = false; + return; + } + const char* storedPart = textPool + offset; + const int16_t partWidth = measureWordAdvanceX(renderer, SETTINGS.getReaderFontId(), storedPart, part.length, + wordStyle, sanitizeScratch, scratchHalf); + WordSelectNavigator::WordInfo word; + word.textOffset = offset; + word.textLen = static_cast(part.length); + word.lookupOffset = offset; + word.lookupLen = word.textLen; + word.screenX = static_cast(screenX + offsetX); + word.screenY = screenY; + word.width = partWidth; + word.style = wordStyle; + word.fontId = SETTINGS.getReaderFontId(); + word.isRtl = wordIsRtl; + if (!appendWord(word)) partSucceeded = false; + }); + if (!partSucceeded) return false; + lastSelectableWordIndex = -2; + } + } + return true; +} + +bool DictionaryWordSelectActivity::mergeHyphenatedWords() { + const char* const textPool = workingSet_.textPool.get(); + for (size_t rowIndex = 0; rowIndex + 1 < workingSet_.rowCount; ++rowIndex) { + const auto& row = workingSet_.rows[rowIndex]; + const auto& nextRow = workingSet_.rows[rowIndex + 1]; + if (row.wordCount == 0 || nextRow.wordCount == 0) continue; + const size_t lastWordIndex = row.firstWord + row.wordCount - 1; + const size_t nextWordIndex = nextRow.firstWord; + auto& last = workingSet_.words[lastWordIndex]; + auto& next = workingSet_.words[nextWordIndex]; + const char* lastText = textPool + last.textOffset; + if (!utf8EndsWithHyphen(lastText, last.textLen) || lastText[0] == '-') continue; + + const char* nextText = textPool + next.textOffset; + const size_t nextSkip = next.textLen > 0 && nextText[0] == '-' ? 1 : 0; + uint16_t mergedOffset = 0; + if (!appendMergedText(lastText, last.textLen - 1, nextText + nextSkip, next.textLen - nextSkip, mergedOffset)) { + return false; + } + const size_t mergedLength = last.textLen - 1 + next.textLen - nextSkip; + last.continuationIndex = static_cast(nextWordIndex); + next.continuationOf = static_cast(lastWordIndex); + last.lookupOffset = mergedOffset; + last.lookupLen = static_cast(mergedLength); + next.lookupOffset = mergedOffset; + next.lookupLen = static_cast(mergedLength); + } + + if (!nextPageFirstWord.empty() && workingSet_.rowCount > 0) { + const auto& lastRow = workingSet_.rows[workingSet_.rowCount - 1]; + if (lastRow.wordCount > 0) { + auto& last = workingSet_.words[lastRow.firstWord + lastRow.wordCount - 1]; + const char* lastText = textPool + last.textOffset; + if (utf8EndsWithHyphen(lastText, last.textLen) && lastText[0] != '-') { + uint16_t mergedOffset = 0; + if (!appendMergedText(lastText, last.textLen - 1, nextPageFirstWord.c_str(), nextPageFirstWord.size(), + mergedOffset)) { + return false; + } + last.lookupOffset = mergedOffset; + last.lookupLen = static_cast(last.textLen - 1 + nextPageFirstWord.size()); + } + } + } + return true; +} + +void DictionaryWordSelectActivity::openDictionarySwitch() { + auto picker = makeUniqueNoThrow(renderer, mappedInput, cachePath, true, true); + if (!picker) { + LOG_ERR("DICT", "OOM: DictionarySelectActivity"); + return; + } + + startActivityForResult(std::move(picker), [this](const ActivityResult& result) { + forceFullRepaintOnNextRender(); + if (result.isCancelled) { + requestUpdate(); + return; + } + const auto* selection = std::get_if(&result.data); + if (!selection) { + LOG_ERR("DICT", "Dictionary switch returned no path"); + requestUpdate(); + return; + } + Dictionary::setLookupDictPathOverride(selection->path.c_str()); + controller.startLookup(controller.getLookupWord(), false); + }); +} + +void DictionaryWordSelectActivity::loop() { + if (ignoreInitialBackRelease_) { + if (mappedInput.wasReleased(MappedInputManager::Button::Back) || + !mappedInput.isPressed(MappedInputManager::Button::Back)) { + ignoreInitialBackRelease_ = false; + } + return; + } + + if (controller.isActive()) { + switch (controller.handleInput()) { + case DictionaryLookupController::LookupEvent::FoundDefinition: { + // Rebuild the reader page while its font is still resident. The child + // then overlays the modal after swapping to the dictionary font, so we + // never need a second framebuffer or two live SD-font families. + { + RenderLock lock(*this); + renderDefinitionBackground(); + } + auto definition = makeUniqueNoThrow( + renderer, mappedInput, controller.getFoundWord(), controller.getFoundLocation(), true, cachePath, + controller.getRecordHistory(), controller.getLookupWord(), + DictionaryLookupController::toHistStatus(controller.getFoundStatus()), + readerBackgroundRender_ ? readerContext_ : this, + readerBackgroundRender_ ? readerBackgroundRender_ + : &DictionaryWordSelectActivity::renderDefinitionBackgroundCallback, + dictionaryFontFamilyName_, dictionaryFontPointSize_, true, &highlightSnapshotStorage_); + if (!definition) { + LOG_ERR("DICT", "OOM allocating DictionaryDefinitionActivity (%u bytes)", + static_cast(sizeof(DictionaryDefinitionActivity))); + forceFullRepaintOnNextRender(); + requestUpdate(); + break; + } + suspendWorkingSet(); + startActivityForResult(std::move(definition), [this](const ActivityResult& result) { + if (!result.isCancelled) { + setResult(ActivityResult{}); + finish(); + } else { + { + RenderLock lock(*this); + if (!restoreWorkingSet()) { + GUI.drawPopup(renderer, tr(STR_MEMORY_ERROR)); + renderer.displayBuffer(); + delay(1000); + ActivityResult parentResult; + parentResult.isCancelled = true; + setResult(std::move(parentResult)); + finish(); + return; + } + } + forceFullRepaintOnNextRender(); + requestUpdate(); + } + }); + break; + } + case DictionaryLookupController::LookupEvent::NotFoundDismissedBack: + forceFullRepaintOnNextRender(); + requestUpdate(); + break; + case DictionaryLookupController::LookupEvent::NotFoundDismissedDone: + setResult(ActivityResult{}); + finish(); + break; + case DictionaryLookupController::LookupEvent::SwitchDictionary: + openDictionarySwitch(); + break; + case DictionaryLookupController::LookupEvent::Cancelled: + forceFullRepaintOnNextRender(); + requestUpdate(); + break; + default: + break; + } + return; + } + + if (navigator.isEmpty()) { + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + DictUtils::cancelAndFinish(*this); + } + return; + } + + if (navigator.handleNavigation(mappedInput, renderer)) { + requestUpdate(); + } + +#if CROSSINK_APP_CAP_TOUCH + if (touchDragLookup_) { + int dragX = 0; + int dragY = 0; + if (mappedInput.isScreenTouchHeld(dragX, dragY)) { + if (navigator.selectWordAtPoint(dragX, dragY, renderer.getLineHeight(SETTINGS.getReaderFontId()))) { + requestUpdate(); + } + return; + } + + touchDragLookup_ = false; + controller.lookupOrPopup(navigator.finishTouchMultiSelect()); + return; + } + + int touchX = 0; + int touchY = 0; + if (mappedInput.wasScreenTouchDown(touchX, touchY)) { + bool touchedWord = false; + navigator.selectWordAtPoint(touchX, touchY, renderer.getLineHeight(SETTINGS.getReaderFontId()), &touchedWord); + if (touchedWord && navigator.beginTouchMultiSelect()) { + touchDragLookup_ = true; + // Finish this fast refresh before lookup can replace the screen, so the + // touched word always provides visible press feedback on e-ink. + requestUpdateAndWait(); + } + return; + } +#endif + + // Check Back early when not in multi-select mode. This allows exit even when + // confirmReleaseConsumed is stuck true (menu-triggered entry has no Confirm release). + if (!navigator.isMultiSelecting() && mappedInput.wasReleased(MappedInputManager::Button::Back)) { + DictUtils::cancelAndFinish(*this); + return; + } + + if (controller.handleMultiSelect(navigator)) return; + + if (navigator.isMultiSelecting()) return; + + if (controller.handleConfirmLookup(navigator)) return; + + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + DictUtils::cancelAndFinish(*this); + return; + } +} + +void DictionaryWordSelectActivity::render(RenderLock&&) { + const int lineHeight = renderer.getLineHeight(SETTINGS.getReaderFontId()); + const int currIdx = navigator.getCurrentFlatIndex(); + + // Differential fast path. Only valid when: + // - we set it up on the previous frame (RenderMode::Differential), + // - the controller has nothing pending to draw, + // - we have a current selection. + if (nextRenderMode_ == RenderMode::Differential && !controller.isActive() && currIdx >= 0) { + prewarmHighlightGlyphs(currIdx); + auto dirty = navigator.renderHighlightDifferential(renderer, lineHeight, prevHighlightIdx_, currIdx); + if (dirty.has_value()) { + // Push full panel — the SDK's windowed-refresh path produces alternating black→white + // transition failures on consecutive fast partial refreshes, so it's intentionally not + // wired up here. The savings come from skipping page->render, which dominates the + // pre-optimization cost; the full push at the end is a hardware floor (~444ms). + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + prevHighlightIdx_ = currIdx; + return; + } + // Fall through to full repaint. + } + + // Skip-initial-render fast path. Fires at most once per activity instance, + // when the caller signalled the framebuffer already contains the page at + // our margins (currently only EpubReaderActivity's hold-to-lookup path). + // Conditions: + // - flag still set (one-shot), + // - controller has nothing to draw (an active controller would mean we + // re-entered render() after a sub-activity returned without the + // framebuffer being reset by forceFullRepaintOnNextRender()), + // - we have a current selection (currIdx >= 0); otherwise there is + // nothing to overlay and we fall through to a normal repaint. + // We consume the flag unconditionally on first entry so any later + // full-repaint goes through the normal clearScreen + page->render path. + if (framebufferContainsPage_) { + framebufferContainsPage_ = false; + if (!controller.isActive() && currIdx >= 0) { + // Clear the bottom strip the caller reserved (status bar OR auto-turn + // label). Match the menu→lookup path, which wipes via clearScreen() + + // page->render(); we skipped both, so clear that one region instead. + if (reservedBottomHeight_ > 0) { + int bezelTop, bezelRight, bezelBottom, bezelLeft; + renderer.getOrientedViewableTRBL(&bezelTop, &bezelRight, &bezelBottom, &bezelLeft); + const int clearY = renderer.getScreenHeight() - bezelBottom - reservedBottomHeight_; + const int clearW = renderer.getScreenWidth() - bezelLeft - bezelRight; + renderer.fillRect(bezelLeft, clearY, clearW, reservedBottomHeight_, false); + } + + prewarmHighlightGlyphs(currIdx); + + auto setup = navigator.renderHighlightDifferential(renderer, lineHeight, /*prevWordIdx=*/-1, currIdx); + bool snapshotPrimed = setup.has_value(); + if (!snapshotPrimed) { + // Hyphenated wrap or oversize capture. The framebuffer still holds + // the page, but we cannot prime the snapshot for the differential + // path. Draw the multi-word highlight (which overwrites pixels under + // each highlight rect) and force the next render to do a full + // repaint so the renderer state is consistent. The user just pays + // for one regular page render on the next cursor move instead of + // on entry. + navigator.renderHighlight(renderer, lineHeight); + } + clearFrontButtonHintArea(); + DictUtils::drawWordSelectButtonHints(renderer, mappedInput, navigator); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + prevHighlightIdx_ = currIdx; + nextRenderMode_ = snapshotPrimed ? RenderMode::Differential : RenderMode::FullPage; + return; + } + // Flag was set but conditions weren't met (controller active or no + // current selection). Fall through to the normal full-repaint path. + } + + // Full repaint path. + renderer.clearScreen(); + if (controller.render()) { + // Controller drew an overlay; framebuffer state is unknown. + nextRenderMode_ = RenderMode::FullPage; + prevHighlightIdx_ = -1; + return; + } + + // Font prewarm: scan pass accumulates text, then prewarm, then real render. + // Without this, every cold codepoint cold-misses the 8-slot SD glyph + // overflow ring and the page render serializes ~100+ individual SD reads. + // Same pattern as EpubReaderActivity::renderContents(). + auto* fcm = renderer.getFontCacheManager(); + auto scope = fcm->createPrewarmScope(); + page->render(renderer, SETTINGS.getReaderFontId(), marginLeft, marginTop); // scan pass + scope.endScanAndPrewarm(); + page->render(renderer, SETTINGS.getReaderFontId(), marginLeft, marginTop); + + // Set up snapshot AND draw the highlight via the differential entry point with + // prevWordIdx = -1 (no previous highlight to wipe). This both draws the highlight + // for this frame and primes snapshot_ so the next frame can run the fast path. + // If the navigator declines (multi-select, hyphenated, oversize), fall back to + // the multi-word renderHighlight and stay on the full path next frame. + // + // The -1 literal is load-bearing: renderHighlightDifferential uses prevWordIdx + // < 0 as the signal "framebuffer was just redrawn from scratch, discard any + // stale snapshot rather than restoring it on top of fresh pixels." This is the + // only path that disturbs the framebuffer outside the differential cycle, so + // it's also the only call site that must pass -1. + bool snapshotPrimed = false; + if (currIdx >= 0) { + auto setup = navigator.renderHighlightDifferential(renderer, lineHeight, /*prevWordIdx=*/-1, currIdx); + snapshotPrimed = setup.has_value(); + } + if (!snapshotPrimed) { + navigator.renderHighlight(renderer, lineHeight); + } + + clearFrontButtonHintArea(); + DictUtils::drawWordSelectButtonHints(renderer, mappedInput, navigator); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + + prevHighlightIdx_ = currIdx; + nextRenderMode_ = snapshotPrimed ? RenderMode::Differential : RenderMode::FullPage; +} diff --git a/src/activities/reader/DictionaryWordSelectActivity.h b/src/activities/reader/DictionaryWordSelectActivity.h new file mode 100644 index 0000000000..70fc5be754 --- /dev/null +++ b/src/activities/reader/DictionaryWordSelectActivity.h @@ -0,0 +1,184 @@ +#pragma once +#include +#include +#include + +#include +#include +#include +#include + +#include "../Activity.h" +#include "util/DictionaryLookupController.h" +#include "util/WordSelectNavigator.h" + +class DictionaryWordSelectActivity final : public Activity { + public: + using ReaderBackgroundRenderFn = void (*)(void* context); + using ReaderPageReloadFn = std::unique_ptr (*)(void* context); + + // reservedBottomHeight is the post-bezel reserved space the caller (EpubReader) + // left below the page text — status-bar height OR auto-page-turn indicator + // height, per the caller's own layout formula. The skip-initial-render fast + // path clears exactly that strip so the framebuffer matches the menu→lookup + // path (no status bar, no auto-turn label visible during word-select). + explicit DictionaryWordSelectActivity( + GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr page, int marginLeft, int marginTop, + std::string cachePath, std::string nextPageFirstWord = "", bool framebufferContainsPage = false, + int reservedBottomHeight = 0, int initialTouchX = -1, int initialTouchY = -1, bool autoLookupInitialWord = false, + const char* dictionaryFontFamilyName = nullptr, uint8_t dictionaryFontPointSize = 0, + void* readerContext = nullptr, ReaderBackgroundRenderFn readerBackgroundRender = nullptr, + ReaderPageReloadFn readerPageReload = nullptr) + : Activity("DictionaryWordSelect", renderer, mappedInput), + page(std::move(page)), + marginLeft(marginLeft), + marginTop(marginTop), + cachePath(std::move(cachePath)), + nextPageFirstWord(std::move(nextPageFirstWord)), + // DictionaryLookupController borrows this activity-owned path. Qualify + // the member so it cannot instead bind to the constructor parameter, + // which is destroyed as soon as construction completes. + controller(renderer, mappedInput, *this, this->cachePath), + framebufferContainsPage_(framebufferContainsPage), + reservedBottomHeight_(reservedBottomHeight), + initialTouchX_(initialTouchX), + initialTouchY_(initialTouchY), + autoLookupInitialWord_(autoLookupInitialWord), + dictionaryFontPointSize_(dictionaryFontPointSize), + readerContext_(readerContext), + readerBackgroundRender_(readerBackgroundRender), + readerPageReload_(readerPageReload) { + if (dictionaryFontFamilyName) { + std::strncpy(dictionaryFontFamilyName_, dictionaryFontFamilyName, sizeof(dictionaryFontFamilyName_) - 1); + } + navigator.setHighlightSnapshotStorage(&highlightSnapshotStorage_); + } + + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; + + private: + std::unique_ptr page; + int marginLeft; + int marginTop; + std::string cachePath; + std::string nextPageFirstWord; + + struct WorkingSet { + std::unique_ptr words; + std::unique_ptr rows; + std::unique_ptr textPool; + std::unique_ptr measurementScratch; + size_t wordCapacity = 0; + size_t wordCount = 0; + size_t rowCapacity = 0; + size_t rowCount = 0; + size_t textCapacity = 0; + size_t textUsed = 0; + size_t measurementScratchCapacity = 0; + + void clear() { + words.reset(); + rows.reset(); + textPool.reset(); + measurementScratch.reset(); + wordCapacity = 0; + wordCount = 0; + rowCapacity = 0; + rowCount = 0; + textCapacity = 0; + textUsed = 0; + measurementScratchCapacity = 0; + } + }; + + WorkingSet workingSet_; + WordSelectNavigator navigator; + // Shared with the stacked definition activity. It is fixed storage inside + // this activity, so it adds no allocator churn and replaces two simultaneous + // 4 KB snapshots with one. + WordSelectNavigator::HighlightSnapshotStorage highlightSnapshotStorage_; + DictionaryLookupController controller; + + // Differential repaint state. The first render in a session always goes through + // the full path (page->render + snapshot setup). After that, cursor moves use the + // differential path: restore previous highlight pixels, snapshot new region, draw + // new highlight, push only the dirty rect to the panel. State is reset to FullPage + // whenever the framebuffer is disturbed by something other than the highlight + // (controller overlay, multi-select, hyphenated wrap). + enum class RenderMode { FullPage, Differential }; + RenderMode nextRenderMode_ = RenderMode::FullPage; + int prevHighlightIdx_ = -1; + + // One-shot opt-in from the caller: "the framebuffer already contains the + // page at marginLeft/marginTop (e.g. EpubReader just rendered it before the + // hold-to-lookup gesture)". When true, the first render() skips clearScreen + // + page->render and overlays only the highlight + button hints. Consumed + // (cleared) on the first render() call regardless of which branch is taken, + // so any future full-repaint goes through the normal re-render path. + bool framebufferContainsPage_ = false; + + // Bottom-of-screen reserved area the caller left below the page text + // (status bar OR auto-page-turn indicator). Cleared in the skip-initial + // fast path so the entry frame matches the menu→lookup visual state. + int reservedBottomHeight_ = 0; + bool ignoreInitialBackRelease_ = false; + int initialTouchX_ = -1; + int initialTouchY_ = -1; + bool autoLookupInitialWord_ = false; + // This child owns its fixed-size copy so the reader does not keep the + // dictionary selection resident while laying out EPUB sections. + char dictionaryFontFamilyName_[64] = ""; + uint8_t dictionaryFontPointSize_ = 0; + bool touchDragLookup_ = false; + void* readerContext_ = nullptr; + ReaderBackgroundRenderFn readerBackgroundRender_ = nullptr; + ReaderPageReloadFn readerPageReload_ = nullptr; + bool workingSetSuspended_ = false; + bool workingSetMemoryError_ = false; + int suspendedSelectionX_ = -1; + int suspendedSelectionY_ = -1; + + bool skipLoopDelay() override { return controller.skipLoopDelay(); } + + // Force the next render() to take the full-repaint path. Used after the lookup controller + // or a sub-activity (e.g. DictionaryDefinitionActivity) has drawn directly to the + // framebuffer outside our render(), making the snapshot/dirty-rect state stale. Without + // this reset, the differential path would restore a small region of "page bg + word text" + // onto a framebuffer full of unrelated content, and the next push would show that + // unrelated content with one word's worth of correct page state overlaid. + void forceFullRepaintOnNextRender() { + nextRenderMode_ = RenderMode::FullPage; + prevHighlightIdx_ = -1; + } + + // Batched bitmap-glyph prewarm for the word at currIdx, so the upcoming + // drawText (inside renderHighlightDifferential / renderHighlight) doesn't + // serialize cold-miss SD reads one character at a time. No-op for invalid + // indices or missing FontCacheManager. + void prewarmHighlightGlyphs(int currIdx); + + // Build SdCardFont's persistent advance table for every codepoint on the + // current page, in one batched SD operation. After this, all subsequent + // getTextAdvanceX calls take the in-RAM binary-search fast path instead + // of the slow per-glyph fontMap fallback (~50ms each). + void prebuildAdvanceTable(); + + void clearFrontButtonHintArea(); + void openDictionarySwitch(); + void renderDefinitionBackground(); + static void renderDefinitionBackgroundCallback(void* context); + bool buildWorkingSet(bool consumeInitialConfirm); + void suspendWorkingSet(); + bool restoreWorkingSet(); + + bool allocateWorkingSet(); + bool extractWords(); + bool mergeHyphenatedWords(); + bool appendText(const char* text, size_t length, uint16_t& offset); + bool appendMergedText(const char* first, size_t firstLength, const char* second, size_t secondLength, + uint16_t& offset); + bool appendWord(WordSelectNavigator::WordInfo word); +}; diff --git a/src/activities/reader/EndOfBookOptions.cpp b/src/activities/reader/EndOfBookOptions.cpp index be0dbade3d..253dde1f59 100644 --- a/src/activities/reader/EndOfBookOptions.cpp +++ b/src/activities/reader/EndOfBookOptions.cpp @@ -31,7 +31,9 @@ void EndOfBookOptions::loadOnce(const std::string& currentBookPath) { isLoaded.store(true, std::memory_order_release); } -bool EndOfBookOptions::menuActive() const { return isLoaded.load(std::memory_order_acquire) && !names.empty(); } +bool EndOfBookOptions::loaded() const { return isLoaded.load(std::memory_order_acquire); } + +bool EndOfBookOptions::menuActive() const { return loaded() && !names.empty(); } std::string EndOfBookOptions::fullPath(const size_t index) const { if (index >= names.size()) { diff --git a/src/activities/reader/EndOfBookOptions.h b/src/activities/reader/EndOfBookOptions.h index ed0ff940ea..c89efebf4b 100644 --- a/src/activities/reader/EndOfBookOptions.h +++ b/src/activities/reader/EndOfBookOptions.h @@ -17,12 +17,14 @@ class EndOfBookOptions { static constexpr size_t MAX_SUGGESTIONS = 3; - // Scans the book's folder for suggestions; no-op when already loaded. Call ONLY from - // the reader's render() (the render task, serialized by RenderLock) — the loaded flag - // is the release/acquire publication point that lets the main task read the finished - // list safely. + // Scans the book's folder for suggestions; no-op when already loaded. Call while + // serialized by RenderLock. The loaded flag is the release/acquire publication point + // that lets the other task read the finished list safely. void loadOnce(const std::string& currentBookPath); + // True once the original folder scan has completed, including when it found no books. + bool loaded() const; + // True when the suggestion menu is showing and should own the reader's input. bool menuActive() const; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index ce68b5ed11..a0960b0f1e 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -21,12 +22,14 @@ #include #include +#include "../settings/DictionarySelectActivity.h" #include "../settings/KOReaderSettingsActivity.h" #include "BookStatsActivity.h" #include "ClipSelectionActivity.h" #include "ClippingStore.h" #include "CrossPointSettings.h" #include "CrossPointState.h" +#include "DictionaryWordSelectActivity.h" #include "EpubReaderBookmarkListActivity.h" #include "EpubReaderChapterSelectionActivity.h" #include "EpubReaderClippingListActivity.h" @@ -36,6 +39,7 @@ #include "GlobalActions.h" #include "KOReaderCredentialStore.h" #include "KOReaderSyncActivity.h" +#include "LookedUpWordsActivity.h" #include "MappedInputManager.h" #include "NearbyBookPositionSyncActivity.h" #include "ProgressMapper.h" @@ -43,17 +47,24 @@ #include "ReaderUtils.h" #include "RecentBooksStore.h" #include "SdCardFontSystem.h" +#include "SilentRestart.h" #include "WordRef.h" +#include "activities/home/RecentBookProgress.h" #include "activities/util/ConfirmationActivity.h" #include "activities/util/IntervalSelectionActivity.h" #include "clippings/ClippingsManager.h" #include "components/UITheme.h" +#if CROSSINK_APP_CAP_TOUCH +#include "components/TouchHeaderBackButton.h" +#endif #include "fontIds.h" #include "util/BookCacheUtils.h" #include "util/BookMoveUtils.h" +#include "util/Dictionary.h" #include "util/ScreenshotUtil.h" namespace { +constexpr unsigned long TOUCH_DICTIONARY_LOOKUP_HOLD_MS = 1000; // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() constexpr unsigned long longPressMenuMs = 600; constexpr uint16_t DEFAULT_AUTO_PAGE_TURN_INTERVAL_S = 30; @@ -61,14 +72,23 @@ constexpr uint16_t MIN_AUTO_PAGE_TURN_INTERVAL_S = 5; constexpr uint16_t MAX_AUTO_PAGE_TURN_INTERVAL_S = 120; constexpr int MAX_PAGE_LOAD_RETRIES = 3; constexpr uint8_t LEGACY_READER_SETTINGS_FILE_VERSION = 1; -constexpr uint8_t READER_SETTINGS_FILE_VERSION = 2; +constexpr uint8_t PRE_WORD_SPACING_READER_SETTINGS_FILE_VERSION = 2; +constexpr uint8_t PRE_INDEXING_METHOD_READER_SETTINGS_FILE_VERSION = 3; +constexpr uint8_t PRE_DICTIONARY_FONT_READER_SETTINGS_FILE_VERSION = 4; +constexpr uint8_t PRE_POINT_SIZE_READER_SETTINGS_FILE_VERSION = 5; +constexpr uint8_t PRE_DICTIONARY_FONT_SIZE_READER_SETTINGS_FILE_VERSION = 6; +constexpr uint8_t READER_SETTINGS_FILE_VERSION = 7; constexpr uint8_t READER_SETTINGS_FLAG_CUSTOM = 1 << 0; constexpr uint8_t READER_SETTINGS_FLAG_AUTO_PAGE_TURN = 1 << 1; constexpr uint8_t READER_SETTINGS_FLAG_RENDER_MODE = 1 << 2; +constexpr uint8_t READER_SETTINGS_FLAG_DICTIONARY_FONT = 1 << 3; constexpr char READER_SETTINGS_FILE_NAME[] = "/reader_settings.bin"; constexpr char BALANCED_SECTION_CACHE_SUFFIX[] = "_balanced"; constexpr char LIGHT_SECTION_CACHE_SUFFIX[] = "_light"; constexpr unsigned long RENDER_MODE_TOAST_MS = 1500UL; +constexpr unsigned long IDLE_SD_FONT_PREWARM_DELAY_MS = 400UL; +constexpr uint32_t IDLE_SD_FONT_PREWARM_MIN_FREE = 64U * 1024U; +constexpr uint32_t IDLE_SD_FONT_PREWARM_MIN_MAX_ALLOC = 40U * 1024U; constexpr unsigned long MIN_READING_STATS_PAGE_MS = 2000UL; constexpr uint32_t MIN_READING_PACE_SAMPLE_SECONDS = 2; constexpr uint16_t MIN_STORED_TIME_LEFT_PACE_SAMPLE_COUNT = 3; @@ -79,8 +99,49 @@ constexpr uint16_t MIN_STORED_PACE_FASTER_RECOVERY_SESSION_SAMPLES = 15; constexpr uint8_t STORED_PACE_FASTER_RECOVERY_PERCENT = 90; constexpr uint8_t BOOK_PROGRESS_ESTIMATE_FLOOR_PERCENT = 90; constexpr uint16_t FOOTNOTE_PREVIEW_MAX_PAGES = 3; +#if CROSSINK_APP_CAP_TOUCH +constexpr int TOUCH_FOOTNOTE_TARGET_SIZE = 48; +#endif constexpr uint8_t PUBLISHER_PAGE_NUMBER_LEFT_MARGIN_MIN = 15; constexpr int PUBLISHER_PAGE_NUMBER_X = 5; +constexpr uint16_t CLIP_ADVANCE_CODEPOINT_CAPACITY = 256; + +struct ClipAdvanceCollector { + static constexpr uint8_t STYLE_COUNT = 4; + uint32_t codepoints[STYLE_COUNT][CLIP_ADVANCE_CODEPOINT_CAPACITY]{}; + uint16_t counts[STYLE_COUNT]{}; + uint8_t truncatedStyles = 0; + std::string rtlTokenScratch; + std::string rtlVisualScratch; + + void reset() { + std::fill(std::begin(counts), std::end(counts), 0); + truncatedStyles = 0; + rtlTokenScratch.clear(); + rtlVisualScratch.clear(); + } + + bool addCodepoint(const uint8_t style, const uint32_t codepoint) { + auto& count = counts[style]; + auto* bucket = codepoints[style]; + if (std::find(bucket, bucket + count, codepoint) != bucket + count) return false; + if (count >= CLIP_ADVANCE_CODEPOINT_CAPACITY) { + truncatedStyles |= static_cast(1U << style); + return true; + } + bucket[count++] = codepoint; + return false; + } + + bool addLogicalText(const uint8_t style, const char* text) { + const auto* p = reinterpret_cast(text); + uint32_t codepoint = 0; + while ((codepoint = utf8NextCodepoint(&p))) { + if (addCodepoint(style, codepoint)) return true; + } + return false; + } +}; uint32_t pagesCentipages(const float pages) { if (pages <= 0.0f) { @@ -207,6 +268,69 @@ SectionBuildProfile safeModeBuildProfile() { return SectionBuildProfile{EpubRenderMode::Light, false, false, false, "safe", true}; } +struct SectionBuildAttempt { + bool succeeded; + bool lowMemory; +}; + +struct SectionFallbackResult { + bool succeeded = false; + bool lastAttemptLowMemory = false; + bool usedSafeMode = false; +}; + +template +SectionFallbackResult runSectionBuildFallbacks(const EpubRenderMode selectedMode, const bool allowSafeMode, + BuildFn& build, BeforeRetryFn& beforeRetry, + const SectionBuildAttempt* initialAttempt = nullptr) { + SectionFallbackResult result; + uint8_t fallbackCount = 0; + const auto fallbackModes = fallbackModesForSelection(selectedMode, fallbackCount); + uint8_t firstModeIndex = 0; + if (initialAttempt) { + result.succeeded = initialAttempt->succeeded; + result.lastAttemptLowMemory = initialAttempt->lowMemory; + firstModeIndex = 1; + } + for (uint8_t i = firstModeIndex; i < fallbackCount && !result.succeeded; ++i) { + const SectionBuildProfile profile = buildProfileForRenderMode(fallbackModes[i]); + if (i > 0) { + if (!result.lastAttemptLowMemory) break; + beforeRetry(profile); + } + const SectionBuildAttempt attempt = build(profile); + result.succeeded = attempt.succeeded; + result.lastAttemptLowMemory = attempt.lowMemory; + } + + if (!result.succeeded && result.lastAttemptLowMemory && allowSafeMode) { + const SectionBuildProfile profile = safeModeBuildProfile(); + beforeRetry(profile); + const SectionBuildAttempt attempt = build(profile); + result.succeeded = attempt.succeeded; + result.lastAttemptLowMemory = attempt.lowMemory; + result.usedSafeMode = attempt.succeeded; + } + return result; +} + +ReaderRenderSpec readerRenderSpecForProfile(const int fontId, const uint16_t viewportWidth, + const uint16_t viewportHeight, const SectionBuildProfile& profile) { + ReaderRenderSpec spec = SETTINGS.readerRenderSpec(viewportWidth, viewportHeight, profile.renderMode); + spec.fontId = fontId; + spec.embeddedStyle = profile.embeddedStyle; + spec.bionicReadingEnabled = profile.bionicReadingEnabled; + spec.guideReadingEnabled = profile.guideReadingEnabled; + return spec; +} + +void ensureReaderSdFontLoaded(GfxRenderer& renderer) { + sdFontSystem.ensureLoaded(renderer); + // Layout only needs the active font. Release the settings-only family metadata + // before building a section so it does not consume reader heap headroom. + sdFontSystem.releaseRegistry(); +} + void applySafeModeReaderSettings() { SETTINGS.epubRenderMode = static_cast(EpubRenderMode::Light); SETTINGS.embeddedStyle = 0; @@ -232,20 +356,6 @@ bool hasVisibleWordText(const char* text) { bool hasVisibleWordText(const std::string& text) { return hasVisibleWordText(text.c_str()); } -uint8_t largestBlockPercent(const MemoryBudget::HeapSnapshot& heap) { - if (heap.freeHeap == 0) { - return 0; - } - return static_cast(std::min(100, (heap.maxAllocHeap * 100U) / heap.freeHeap)); -} - -struct TiledGrayscaleTimings { - unsigned long grayLsb = 0; - unsigned long grayMsb = 0; - unsigned long grayDisplay = 0; - unsigned long cleanup = 0; -}; - struct ClippingPageMatch { uint16_t startWord = 0; uint16_t endWord = 0; @@ -361,12 +471,12 @@ bool forEachVisiblePageWord(const Page& page, Callback&& callback) { return true; } -bool matchClipRunFromPageWord(const Page& page, const Clipping& clipping, const uint16_t startPageWord, +bool matchClipRunFromPageWord(const Page& page, const std::string& clippingText, const uint16_t startPageWord, const uint16_t startClipToken, const uint16_t minPartialMatch, ClippingPageMatch& match) { const char* cursor = nullptr; const char* token = nullptr; size_t tokenLen = 0; - if (!advanceClipCursorToToken(clipping.text, startClipToken, cursor, token, tokenLen)) { + if (!advanceClipCursorToToken(clippingText, startClipToken, cursor, token, tokenLen)) { return false; } @@ -404,8 +514,13 @@ bool matchClipRunFromPageWord(const Page& page, const Clipping& clipping, const } // A relayout can split a saved clipping so this page starts mid-clipping. - // Accept complete runs or page-boundary partial runs. - if (!reachedClipEnd && matchedTokens < minPartialMatch) { + // Accept complete runs (the whole clipping matched, start to end) or + // page-boundary partial runs of a meaningful length. Reaching the end of the + // clip text is not enough on its own: starting the search at a late token + // (e.g. the clipping's last word) trivially "reaches the end" after matching + // a single coincidental word, which must not be treated as a full match. + const bool completeClipMatch = startClipToken == 0 && reachedClipEnd; + if (!completeClipMatch && matchedTokens < minPartialMatch) { const bool startsAtClipBoundary = startClipToken == 0; const bool startsAtPageBoundary = startPageWord == 0; if (!startsAtClipBoundary && !startsAtPageBoundary) { @@ -418,10 +533,10 @@ bool matchClipRunFromPageWord(const Page& page, const Clipping& clipping, const return true; } -bool findClippingTextOnPage(const Page& page, const Clipping& clipping, ClippingPageMatch& match) { - if (clipping.text.empty()) return false; +bool findClippingTextOnPage(const Page& page, const std::string& clippingText, ClippingPageMatch& match) { + if (clippingText.empty()) return false; - const uint16_t tokenCount = countClipTokens(clipping.text); + const uint16_t tokenCount = countClipTokens(clippingText); if (tokenCount == 0) return false; const uint16_t minPartialMatch = std::min(tokenCount, 3); @@ -429,7 +544,7 @@ bool findClippingTextOnPage(const Page& page, const Clipping& clipping, Clipping forEachVisiblePageWord(page, [&](const uint16_t wordIndex, const PageLine&, const TextBlock& block, const size_t i) { const char* word = block.wordText(static_cast(i)); - const char* cursor = clipping.text.c_str(); + const char* cursor = clippingText.c_str(); const char* token = nullptr; size_t tokenLen = 0; uint16_t tokenIndex = 0; @@ -438,7 +553,7 @@ bool findClippingTextOnPage(const Page& page, const Clipping& clipping, Clipping break; } if (wordMatchesToken(word, token, tokenLen) && - matchClipRunFromPageWord(page, clipping, wordIndex, tokenIndex, minPartialMatch, match)) { + matchClipRunFromPageWord(page, clippingText, wordIndex, tokenIndex, minPartialMatch, match)) { found = true; return false; } @@ -494,6 +609,12 @@ uint16_t clampSectionPage(const uint32_t page, const uint16_t pageCount) { return static_cast(std::min(page, pageCount - 1)); } +uint16_t pageFromStoredProgress(const float progress, const uint16_t pageCount) { + if (pageCount == 0 || progress <= 0.0f) return 0; + if (progress >= 1.0f) return static_cast(pageCount - 1); + return clampSectionPage(static_cast(progress * static_cast(pageCount) + 0.001f), pageCount); +} + uint16_t approximateRelayoutPage(const Clipping& clipping, const uint16_t currentPageCount) { if (currentPageCount == 0) return 0; if (clipping.pageCount <= 1) return 0; @@ -505,22 +626,43 @@ uint16_t approximateRelayoutPage(const Clipping& clipping, const uint16_t curren return clampSectionPage(scaledPage, currentPageCount); } -bool pageContainsClippingText(Section& section, const Clipping& clipping, const uint16_t page) { +uint16_t resolveParagraphJumpPage(const Section& section, const uint16_t paragraphIndex, const uint16_t fallbackPage) { + if (section.pageCount == 0 || paragraphIndex == UINT16_MAX) return fallbackPage; + + const uint16_t pageCount = static_cast(section.pageCount); + const uint16_t clampedFallback = clampSectionPage(fallbackPage, pageCount); + const auto paragraphPage = section.getPageForParagraphIndex(paragraphIndex); + if (!paragraphPage.has_value()) return clampedFallback; + + const uint16_t startPage = clampSectionPage(*paragraphPage, pageCount); + if (clampedFallback < startPage) return startPage; + + if (paragraphIndex < UINT16_MAX - 1) { + const auto nextParagraphPage = section.getPageForParagraphIndex(static_cast(paragraphIndex + 1)); + if (nextParagraphPage.has_value() && *nextParagraphPage > startPage && clampedFallback >= *nextParagraphPage) { + return static_cast(*nextParagraphPage - 1); + } + } + + return clampedFallback; +} + +bool pageContainsClippingText(Section& section, const std::string& clippingText, const uint16_t page) { section.currentPage = page; - auto loadedPage = section.loadPageFromSectionFile(); + auto loadedPage = section.loadPage(page); if (!loadedPage) return false; ClippingPageMatch match; - return findClippingTextOnPage(*loadedPage, clipping, match); + return findClippingTextOnPage(*loadedPage, clippingText, match); } -bool findClippingPageNear(Section& section, const Clipping& clipping, const uint16_t center, const uint16_t radius, - uint16_t& outPage) { +bool findClippingPageNear(Section& section, const std::string& clippingText, const uint16_t center, + const uint16_t radius, uint16_t& outPage) { if (section.pageCount == 0) return false; const uint16_t pageCount = static_cast(section.pageCount); const uint16_t clampedCenter = clampSectionPage(center, pageCount); - if (pageContainsClippingText(section, clipping, clampedCenter)) { + if (pageContainsClippingText(section, clippingText, clampedCenter)) { outPage = clampedCenter; return true; } @@ -528,13 +670,13 @@ bool findClippingPageNear(Section& section, const Clipping& clipping, const uint for (uint16_t distance = 1; distance <= radius; ++distance) { if (clampedCenter >= distance) { const uint16_t before = static_cast(clampedCenter - distance); - if (pageContainsClippingText(section, clipping, before)) { + if (pageContainsClippingText(section, clippingText, before)) { outPage = before; return true; } } const uint32_t after = static_cast(clampedCenter) + distance; - if (after < pageCount && pageContainsClippingText(section, clipping, static_cast(after))) { + if (after < pageCount && pageContainsClippingText(section, clippingText, static_cast(after))) { outPage = static_cast(after); return true; } @@ -542,45 +684,109 @@ bool findClippingPageNear(Section& section, const Clipping& clipping, const uint return false; } -uint16_t resolveClippingJumpPage(Section& section, const Clipping& clipping, const uint16_t fallbackPage) { +uint16_t resolveClippingJumpPage(Section& section, const Clipping& clipping, const std::string& clippingText, + const uint16_t fallbackPage) { constexpr uint16_t SEARCH_RADIUS = 8; if (section.pageCount == 0) return fallbackPage; const uint16_t pageCount = static_cast(section.pageCount); uint16_t resolvedPage = clampSectionPage(fallbackPage, pageCount); const uint16_t approximatePage = approximateRelayoutPage(clipping, pageCount); - if (findClippingPageNear(section, clipping, approximatePage, SEARCH_RADIUS, resolvedPage)) { + if (!clippingText.empty() && + findClippingPageNear(section, clippingText, approximatePage, SEARCH_RADIUS, resolvedPage)) { return resolvedPage; } if (clipping.paragraphIndex != UINT16_MAX) { const auto paragraphPage = section.getPageForParagraphIndex(clipping.paragraphIndex); - if (paragraphPage.has_value() && - findClippingPageNear(section, clipping, clampSectionPage(*paragraphPage, pageCount), SEARCH_RADIUS, + if (paragraphPage.has_value() && !clippingText.empty() && + findClippingPageNear(section, clippingText, clampSectionPage(*paragraphPage, pageCount), SEARCH_RADIUS, resolvedPage)) { return resolvedPage; } } - findClippingPageNear(section, clipping, resolvedPage, SEARCH_RADIUS, resolvedPage); + if (!clippingText.empty()) { + findClippingPageNear(section, clippingText, resolvedPage, SEARCH_RADIUS, resolvedPage); + } return resolvedPage; } +constexpr int GRAYSCALE_STRIP_ROWS = 80; + bool runTiledGrayscalePass(GfxRenderer& renderer, const Page& page, const int fontId, const int marginLeft, const int marginTop, const bool foregroundBlack, const bool needsTextGrayscale, - const bool needsImageGrayscale, TiledGrayscaleTimings& timings) { + const bool needsImageGrayscale, uint8_t* scratch, const size_t scratchSize, + const bool asyncRefreshPending) { if ((!needsTextGrayscale && !needsImageGrayscale) || !renderer.supportsStripGrayscale()) { return false; } - constexpr int STRIP_ROWS = 80; const int displayHeight = renderer.getDisplayHeight(); const int displayWidthBytes = renderer.getDisplayWidthBytes(); - auto scratch = - std::unique_ptr(new (std::nothrow) uint8_t[static_cast(displayWidthBytes) * STRIP_ROWS]); - if (!scratch) { - LOG_ERR("ERS", "OOM: grayscale strip scratch (%d bytes); falling back to BW snapshot", - displayWidthBytes * STRIP_ROWS); + const size_t planeBytes = static_cast(displayWidthBytes) * displayHeight; + + const auto renderPlaneToBuffer = [&](const GfxRenderer::RenderMode mode, uint8_t* buffer) { + renderer.setRenderMode(mode); + for (int y = 0; y < displayHeight; y += GRAYSCALE_STRIP_ROWS) { + const int rows = std::min(GRAYSCALE_STRIP_ROWS, displayHeight - y); + renderer.beginStripTarget(buffer + static_cast(y) * displayWidthBytes, y, rows); + renderer.clearScreen(0x00); + if (needsTextGrayscale) { + page.render(renderer, fontId, marginLeft, marginTop, foregroundBlack); + } else { + page.renderImages(renderer, fontId, marginLeft, marginTop); + } + renderer.endStripTarget(); + } + }; + + // Whole-plane buffers are about 48 KB each, so they are unsuitable for the + // task stack or permanent activity storage. Each transient allocation must + // leave enough total and contiguous heap for the next render allocations. + constexpr size_t PLANE_BUFFER_FREE_HEAP_RESERVE = 60000; + constexpr size_t PLANE_BUFFER_MAX_ALLOC_RESERVE = 16 * 1024; + const auto planeBufferFits = [planeBytes] { + return ESP.getFreeHeap() >= planeBytes + PLANE_BUFFER_FREE_HEAP_RESERVE && + ESP.getMaxAllocHeap() >= planeBytes + PLANE_BUFFER_MAX_ALLOC_RESERVE; + }; + auto lsbPlaneBuf = (asyncRefreshPending && planeBufferFits()) ? makeUniqueNoThrow(planeBytes) : nullptr; + auto msbPlaneBuf = (lsbPlaneBuf && planeBufferFits()) ? makeUniqueNoThrow(planeBytes) : nullptr; + + if (lsbPlaneBuf) { + renderPlaneToBuffer(GfxRenderer::GRAYSCALE_LSB, lsbPlaneBuf.get()); + if (msbPlaneBuf) { + renderPlaneToBuffer(GfxRenderer::GRAYSCALE_MSB, msbPlaneBuf.get()); + } + + renderer.waitRefreshComplete(); + renderer.writeGrayscalePlaneStrip(true, lsbPlaneBuf.get(), 0, displayHeight); + if (msbPlaneBuf) { + renderer.writeGrayscalePlaneStrip(false, msbPlaneBuf.get(), 0, displayHeight); + } else { + renderPlaneToBuffer(GfxRenderer::GRAYSCALE_MSB, lsbPlaneBuf.get()); + renderer.writeGrayscalePlaneStrip(false, lsbPlaneBuf.get(), 0, displayHeight); + } + + renderer.setRenderMode(GfxRenderer::BW); + renderer.displayGrayBuffer(); + renderer.cleanupGrayscaleWithFrameBuffer(); + return true; + } + + if (asyncRefreshPending) { + // Controller writes and the BW snapshot fallback both need the refresh to + // be complete when the whole-plane allocation cannot be satisfied. + renderer.waitRefreshComplete(); + } + + const size_t requiredScratchSize = static_cast(displayWidthBytes) * GRAYSCALE_STRIP_ROWS; + if (!scratch || scratchSize < requiredScratchSize) { + if (asyncRefreshPending) { + // The shadow-free async update does not rebuild the controller's + // differential baseline. Re-sync it even when grayscale is skipped. + renderer.cleanupGrayscaleWithFrameBuffer(); + } return false; } @@ -588,9 +794,9 @@ bool runTiledGrayscalePass(GfxRenderer& renderer, const Page& page, const int fo // then re-sync the controller BW state from the framebuffer. const auto renderPlane = [&](const GfxRenderer::RenderMode mode, const bool lsbPlane) { renderer.setRenderMode(mode); - for (int y = 0; y < displayHeight; y += STRIP_ROWS) { - const int rows = std::min(STRIP_ROWS, displayHeight - y); - renderer.beginStripTarget(scratch.get(), y, rows); + for (int y = 0; y < displayHeight; y += GRAYSCALE_STRIP_ROWS) { + const int rows = std::min(GRAYSCALE_STRIP_ROWS, displayHeight - y); + renderer.beginStripTarget(scratch, y, rows); renderer.clearScreen(0x00); if (needsTextGrayscale) { page.render(renderer, fontId, marginLeft, marginTop, foregroundBlack); @@ -598,21 +804,17 @@ bool runTiledGrayscalePass(GfxRenderer& renderer, const Page& page, const int fo page.renderImages(renderer, fontId, marginLeft, marginTop); } renderer.endStripTarget(); - renderer.writeGrayscalePlaneStrip(lsbPlane, scratch.get(), y, rows); + renderer.writeGrayscalePlaneStrip(lsbPlane, scratch, y, rows); } }; renderPlane(GfxRenderer::GRAYSCALE_LSB, true); - timings.grayLsb = millis(); renderPlane(GfxRenderer::GRAYSCALE_MSB, false); - timings.grayMsb = millis(); renderer.setRenderMode(GfxRenderer::BW); renderer.displayGrayBuffer(); - timings.grayDisplay = millis(); renderer.cleanupGrayscaleWithFrameBuffer(); - timings.cleanup = millis(); return true; } @@ -723,7 +925,8 @@ struct ReaderViewportLayout { uint16_t viewportHeight; }; -ReaderViewportLayout computeReaderViewportLayout(GfxRenderer& renderer, const bool automaticPageTurnActive) { +ReaderViewportLayout computeReaderViewportLayout(GfxRenderer& renderer, const bool automaticPageTurnActive, + const bool showFootnoteHeader = false) { ReaderViewportLayout layout{}; renderer.getOrientedViewableTRBL(&layout.marginTop, &layout.marginRight, &layout.marginBottom, &layout.marginLeft); layout.marginLeft += effectiveReaderLeftMargin(); @@ -738,6 +941,15 @@ ReaderViewportLayout computeReaderViewportLayout(GfxRenderer& renderer, const bo layout.marginTop += SETTINGS.screenMargin; } +#if CROSSINK_APP_CAP_TOUCH + if (showFootnoteHeader) { + const Rect header = TouchHeaderBackButton::compactHeaderRect(renderer); + layout.marginTop = std::max(layout.marginTop, header.y + header.height + static_cast(SETTINGS.screenMargin)); + } +#else + (void)showFootnoteHeader; +#endif + if (automaticPageTurnActive && (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight())) { layout.marginBottom += @@ -844,8 +1056,9 @@ bool writeU16(FsFile& file, const uint16_t value) { void captureReaderSettings(EpubReaderActivity::ReaderSettingsSnapshot& out) { out.fontFamily = SETTINGS.fontFamily; - out.fontSize = SETTINGS.fontSize; + out.readerFontPointSize = SETTINGS.readerFontPointSize; out.lineHeightPercent = SETTINGS.lineHeightPercent; + out.wordSpacing = SETTINGS.wordSpacing; out.orientation = SETTINGS.orientation; out.screenMargin = SETTINGS.screenMargin; out.publisherPageNumbers = SETTINGS.publisherPageNumbers; @@ -860,24 +1073,27 @@ void captureReaderSettings(EpubReaderActivity::ReaderSettingsSnapshot& out) { out.bionicReadingEnabled = SETTINGS.bionicReadingEnabled; out.guideReadingEnabled = SETTINGS.guideReadingEnabled; out.epubRenderMode = normalizeRenderModeRaw(SETTINGS.epubRenderMode); + out.indexingMethod = SETTINGS.indexingMethod; std::strncpy(out.sdFontFamilyName, SETTINGS.sdFontFamilyName, sizeof(out.sdFontFamilyName) - 1); out.sdFontFamilyName[sizeof(out.sdFontFamilyName) - 1] = '\0'; } -uint8_t clampedStoredReaderFontSize(const EpubReaderActivity::ReaderSettingsSnapshot& in) { - if (in.sdFontFamilyName[0] != '\0') { - return std::min(in.fontSize, CrossPointSettings::SD_FONT_MAX_SIZE_STEPS - 1); - } - const uint8_t builtinSizeCount = CrossPointSettings::getActiveReaderFontSizeCount(); - return in.fontSize < builtinSizeCount ? in.fontSize : SETTINGS.fontSize; -} - void applyReaderSettings(const EpubReaderActivity::ReaderSettingsSnapshot& in) { SETTINGS.fontFamily = in.fontFamily < CrossPointSettings::BUILTIN_FONT_COUNT ? in.fontFamily : SETTINGS.fontFamily; std::strncpy(SETTINGS.sdFontFamilyName, in.sdFontFamilyName, sizeof(SETTINGS.sdFontFamilyName) - 1); SETTINGS.sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName) - 1] = '\0'; - SETTINGS.fontSize = clampedStoredReaderFontSize(in); + if (in.readerFontPointSize < CrossPointSettings::MIN_READER_FONT_POINT_SIZE) { + if (in.sdFontFamilyName[0] != '\0') { + SETTINGS.readerFontPointSize = sdFontSystem.resolveLegacySizeStep(in.sdFontFamilyName, in.readerFontPointSize); + } else { + SETTINGS.readerFontPointSize = CrossPointSettings::getReaderFontPointSize( + static_cast(in.readerFontPointSize)); + } + } else { + SETTINGS.readerFontPointSize = in.readerFontPointSize; + } SETTINGS.lineHeightPercent = CrossPointSettings::clampedLineHeightPercent(in.lineHeightPercent); + SETTINGS.wordSpacing = std::min(in.wordSpacing, CrossPointSettings::MAX_WORD_SPACING); SETTINGS.orientation = in.orientation < CrossPointSettings::ORIENTATION_COUNT ? in.orientation : SETTINGS.orientation; SETTINGS.screenMargin = std::clamp(in.screenMargin, 5, 40); SETTINGS.publisherPageNumbers = in.publisherPageNumbers ? 1 : 0; @@ -895,20 +1111,18 @@ void applyReaderSettings(const EpubReaderActivity::ReaderSettingsSnapshot& in) { SETTINGS.bionicReadingEnabled = in.bionicReadingEnabled ? 1 : 0; SETTINGS.guideReadingEnabled = in.guideReadingEnabled ? 1 : 0; SETTINGS.epubRenderMode = normalizeRenderModeRaw(in.epubRenderMode); + SETTINGS.indexingMethod = in.indexingMethod < CrossPointSettings::INDEXING_METHOD_COUNT + ? in.indexingMethod + : CrossPointSettings::INDEXING_FULL_SECTION; } -struct BookReaderSettingsData { - bool hasAutoPageTurnInterval = false; - uint16_t autoPageTurnSeconds = DEFAULT_AUTO_PAGE_TURN_INTERVAL_S; - bool hasCustomReaderSettings = false; - bool hasRenderModeOverride = false; - uint8_t renderMode = static_cast(EpubRenderMode::CrossInkDefault); - EpubReaderActivity::ReaderSettingsSnapshot readerSettings; -}; +using BookReaderSettingsData = EpubReaderActivity::BookReaderSettingsData; -bool readReaderSettingsSnapshot(FsFile& file, EpubReaderActivity::ReaderSettingsSnapshot& out) { - if (!(readU8(file, out.fontFamily) && readU8(file, out.fontSize) && readU8(file, out.lineHeightPercent) && - readU8(file, out.orientation) && readU8(file, out.screenMargin) && readU8(file, out.publisherPageNumbers) && +bool readReaderSettingsSnapshot(FsFile& file, EpubReaderActivity::ReaderSettingsSnapshot& out, + const bool includesWordSpacing, const bool includesIndexingMethod) { + if (!(readU8(file, out.fontFamily) && readU8(file, out.readerFontPointSize) && readU8(file, out.lineHeightPercent) && + (!includesWordSpacing || readU8(file, out.wordSpacing)) && readU8(file, out.orientation) && + readU8(file, out.screenMargin) && readU8(file, out.publisherPageNumbers) && readU8(file, out.paragraphAlignment) && readU8(file, out.embeddedStyle) && readU8(file, out.hyphenationEnabled) && readU8(file, out.textAntiAliasing) && readU8(file, out.readerDarkMode) && readU8(file, out.imageRendering) && @@ -920,11 +1134,15 @@ bool readReaderSettingsSnapshot(FsFile& file, EpubReaderActivity::ReaderSettings return false; } out.epubRenderMode = normalizeRenderModeRaw(out.epubRenderMode); + if (includesIndexingMethod && !readU8(file, out.indexingMethod)) { + return false; + } return readExact(file, out.sdFontFamilyName, sizeof(out.sdFontFamilyName)); } bool writeReaderSettingsSnapshot(FsFile& file, const EpubReaderActivity::ReaderSettingsSnapshot& in) { - return writeU8(file, in.fontFamily) && writeU8(file, in.fontSize) && writeU8(file, in.lineHeightPercent) && + return writeU8(file, in.fontFamily) && writeU8(file, in.readerFontPointSize) && writeU8(file, in.lineHeightPercent) && + writeU8(file, std::min(in.wordSpacing, CrossPointSettings::MAX_WORD_SPACING)) && writeU8(file, in.orientation) && writeU8(file, in.screenMargin) && writeU8(file, in.publisherPageNumbers) && writeU8(file, in.paragraphAlignment) && writeU8(file, in.embeddedStyle) && writeU8(file, in.hyphenationEnabled) && writeU8(file, in.textAntiAliasing) && @@ -932,12 +1150,19 @@ bool writeReaderSettingsSnapshot(FsFile& file, const EpubReaderActivity::ReaderS writeU8(file, in.extraParagraphSpacing) && writeU8(file, in.forceParagraphIndents) && writeU8(file, in.bionicReadingEnabled) && writeU8(file, in.guideReadingEnabled) && writeU8(file, normalizeRenderModeRaw(in.epubRenderMode)) && + writeU8(file, in.indexingMethod < CrossPointSettings::INDEXING_METHOD_COUNT + ? in.indexingMethod + : CrossPointSettings::INDEXING_FULL_SECTION) && writeExact(file, in.sdFontFamilyName, sizeof(in.sdFontFamilyName)); } BookReaderSettingsData loadBookReaderSettingsFile(const std::string& cachePath) { BookReaderSettingsData data; captureReaderSettings(data.readerSettings); + std::strncpy(data.dictionarySdFontFamilyName, SETTINGS.dictionarySdFontFamilyName, + sizeof(data.dictionarySdFontFamilyName) - 1); + data.dictionarySdFontFamilyName[sizeof(data.dictionarySdFontFamilyName) - 1] = '\0'; + data.dictionaryFontPointSize = SETTINGS.dictionaryFontPointSize; FsFile file; if (!Storage.openFileForRead("ERS", cachePath + READER_SETTINGS_FILE_NAME, file)) { @@ -961,7 +1186,11 @@ BookReaderSettingsData loadBookReaderSettingsFile(const std::string& cachePath) return data; } - if (version != READER_SETTINGS_FILE_VERSION) { + if (version != PRE_WORD_SPACING_READER_SETTINGS_FILE_VERSION && + version != PRE_INDEXING_METHOD_READER_SETTINGS_FILE_VERSION && + version != PRE_DICTIONARY_FONT_READER_SETTINGS_FILE_VERSION && + version != PRE_POINT_SIZE_READER_SETTINGS_FILE_VERSION && + version != PRE_DICTIONARY_FONT_SIZE_READER_SETTINGS_FILE_VERSION && version != READER_SETTINGS_FILE_VERSION) { file.close(); LOG_DBG("ERS", "Reader settings version mismatch, using defaults"); return data; @@ -971,12 +1200,22 @@ BookReaderSettingsData loadBookReaderSettingsFile(const std::string& cachePath) uint16_t seconds = 0; uint8_t renderMode = static_cast(EpubRenderMode::CrossInkDefault); EpubReaderActivity::ReaderSettingsSnapshot snapshot; + // Version 2 books inherit the current global indexing method instead of + // silently changing modes when their older custom settings are loaded. + snapshot.indexingMethod = data.readerSettings.indexingMethod; bool ok = readU8(file, flags) && readU16(file, seconds); if (ok) { ok = readU8(file, renderMode); } if (ok) { - ok = readReaderSettingsSnapshot(file, snapshot); + ok = readReaderSettingsSnapshot(file, snapshot, version >= PRE_INDEXING_METHOD_READER_SETTINGS_FILE_VERSION, + version >= PRE_DICTIONARY_FONT_READER_SETTINGS_FILE_VERSION); + } + if (ok && version >= PRE_POINT_SIZE_READER_SETTINGS_FILE_VERSION) { + ok = readExact(file, data.dictionarySdFontFamilyName, sizeof(data.dictionarySdFontFamilyName)); + } + if (ok && version >= READER_SETTINGS_FILE_VERSION) { + ok = readU8(file, data.dictionaryFontPointSize); } file.close(); if (!ok) { @@ -996,13 +1235,20 @@ BookReaderSettingsData loadBookReaderSettingsFile(const std::string& cachePath) data.hasRenderModeOverride = true; data.renderMode = normalizeRenderModeRaw(renderMode); } + if (flags & READER_SETTINGS_FLAG_DICTIONARY_FONT) { + data.dictionarySdFontFamilyName[sizeof(data.dictionarySdFontFamilyName) - 1] = '\0'; + data.hasDictionaryFontOverride = data.dictionarySdFontFamilyName[0] != '\0'; + } + if (!data.hasDictionaryFontOverride) { + std::strncpy(data.dictionarySdFontFamilyName, SETTINGS.dictionarySdFontFamilyName, + sizeof(data.dictionarySdFontFamilyName) - 1); + data.dictionarySdFontFamilyName[sizeof(data.dictionarySdFontFamilyName) - 1] = '\0'; + data.dictionaryFontPointSize = SETTINGS.dictionaryFontPointSize; + } return data; } -bool saveBookReaderSettingsFile(const std::string& cachePath, const bool hasAutoPageTurnInterval, - const uint16_t autoPageTurnSeconds, const bool hasCustomReaderSettings, - const bool hasRenderModeOverride, const uint8_t renderMode, - const EpubReaderActivity::ReaderSettingsSnapshot& readerSettings) { +bool saveBookReaderSettingsFile(const std::string& cachePath, const BookReaderSettingsData& data) { FsFile file; if (!Storage.openFileForWrite("ERS", cachePath + READER_SETTINGS_FILE_NAME, file)) { LOG_ERR("ERS", "Could not open reader settings file for write"); @@ -1010,15 +1256,20 @@ bool saveBookReaderSettingsFile(const std::string& cachePath, const bool hasAuto } uint8_t flags = 0; - if (hasCustomReaderSettings) flags |= READER_SETTINGS_FLAG_CUSTOM; - if (hasAutoPageTurnInterval) flags |= READER_SETTINGS_FLAG_AUTO_PAGE_TURN; - if (hasRenderModeOverride) flags |= READER_SETTINGS_FLAG_RENDER_MODE; - const uint16_t clampedSeconds = clampAutoPageTurnIntervalSeconds(autoPageTurnSeconds); - EpubReaderActivity::ReaderSettingsSnapshot normalizedReaderSettings = readerSettings; - normalizedReaderSettings.epubRenderMode = normalizeRenderModeRaw(renderMode); + if (data.hasCustomReaderSettings) flags |= READER_SETTINGS_FLAG_CUSTOM; + if (data.hasAutoPageTurnInterval) flags |= READER_SETTINGS_FLAG_AUTO_PAGE_TURN; + if (data.hasRenderModeOverride) flags |= READER_SETTINGS_FLAG_RENDER_MODE; + if (data.hasDictionaryFontOverride && data.dictionarySdFontFamilyName[0] != '\0') { + flags |= READER_SETTINGS_FLAG_DICTIONARY_FONT; + } + const uint16_t clampedSeconds = clampAutoPageTurnIntervalSeconds(data.autoPageTurnSeconds); + EpubReaderActivity::ReaderSettingsSnapshot normalizedReaderSettings = data.readerSettings; + normalizedReaderSettings.epubRenderMode = normalizeRenderModeRaw(data.renderMode); const bool ok = writeU8(file, READER_SETTINGS_FILE_VERSION) && writeU8(file, flags) && - writeU16(file, clampedSeconds) && writeU8(file, normalizeRenderModeRaw(renderMode)) && - writeReaderSettingsSnapshot(file, normalizedReaderSettings); + writeU16(file, clampedSeconds) && writeU8(file, normalizeRenderModeRaw(data.renderMode)) && + writeReaderSettingsSnapshot(file, normalizedReaderSettings) && + writeExact(file, data.dictionarySdFontFamilyName, sizeof(data.dictionarySdFontFamilyName)) && + writeU8(file, data.dictionaryFontPointSize); file.close(); if (!ok) { LOG_ERR("ERS", "Short write saving reader settings"); @@ -1031,9 +1282,7 @@ bool saveBookRenderModeForCache(const std::string& cachePath, const uint8_t rend data.hasRenderModeOverride = true; data.renderMode = normalizeRenderModeRaw(renderMode); data.readerSettings.epubRenderMode = data.renderMode; - return saveBookReaderSettingsFile(cachePath, data.hasAutoPageTurnInterval, data.autoPageTurnSeconds, - data.hasCustomReaderSettings, data.hasRenderModeOverride, data.renderMode, - data.readerSettings); + return saveBookReaderSettingsFile(cachePath, data); } bool saveRuntimeReaderSettingsForCache(const std::string& cachePath) { @@ -1043,9 +1292,8 @@ bool saveRuntimeReaderSettingsForCache(const std::string& cachePath) { data.hasCustomReaderSettings = true; data.hasRenderModeOverride = true; data.renderMode = normalizeRenderModeRaw(SETTINGS.epubRenderMode); - return saveBookReaderSettingsFile(cachePath, data.hasAutoPageTurnInterval, data.autoPageTurnSeconds, - data.hasCustomReaderSettings, data.hasRenderModeOverride, data.renderMode, - snapshot); + data.readerSettings = snapshot; + return saveBookReaderSettingsFile(cachePath, data); } class ScopedReaderSettingsRestore { @@ -1057,29 +1305,6 @@ class ScopedReaderSettingsRestore { EpubReaderActivity::ReaderSettingsSnapshot snapshot; }; -void formatCompactTimeLeft(const uint32_t seconds, char* out, const size_t outSize) { - if (!out || outSize == 0) return; - if (seconds < 60) { - snprintf(out, outSize, "<1m"); - return; - } - - const uint32_t minutes = (seconds + 30U) / 60U; - if (minutes < 60) { - snprintf(out, outSize, "%lum", static_cast(minutes)); - return; - } - - const uint32_t hours = minutes / 60U; - const uint32_t remainingMinutes = minutes % 60U; - if (remainingMinutes == 0) { - snprintf(out, outSize, "%luh", static_cast(hours)); - } else { - snprintf(out, outSize, "%luh %lum", static_cast(hours), - static_cast(remainingMinutes)); - } -} - // SD card folder finished books are moved into. Single source of truth for the path. constexpr char READ_FOLDER[] = "/Read"; @@ -1112,6 +1337,10 @@ void moveFinishedBookToReadFolder(const std::string& srcPath, const std::string& } // namespace +EpubReaderActivity::BookReaderSettingsData EpubReaderActivity::readBookReaderSettings(const Epub& epub) { + return loadBookReaderSettingsFile(epub.getCachePath()); +} + uint8_t EpubReaderActivity::loadBookRenderMode(const std::string& filePath) { Epub epub(filePath, "/.crosspoint"); epub.setupCacheDir(); @@ -1141,7 +1370,7 @@ bool EpubReaderActivity::resetBookReaderSettings(const std::string& filePath) { } float EpubReaderActivity::getCurrentBookProgressPercent() const { - const int totalPages = section ? section->pageCount : 0; + const int totalPages = section ? section->estimatedTotalPages() : 0; if (activeFootnotePreview || !epub || !section || totalPages == 0 || epub->getBookSize() == 0) { return 0.0f; } @@ -1201,9 +1430,6 @@ bool EpubReaderActivity::currentPageReadingSecondsForStats(uint32_t& seconds, co const uint32_t thresholdSeconds = SETTINGS.getReadingIdleTimeThresholdSeconds(); if (elapsedSeconds > thresholdSeconds) { - LOG_DBG("ERS", "Reading time interval rejected as idle: source=%s seconds=%lu threshold=%lu", - source ? source : "unknown", static_cast(elapsedSeconds), - static_cast(thresholdSeconds)); return false; } @@ -1230,15 +1456,11 @@ void EpubReaderActivity::recordForwardPagePaceSample(uint32_t seconds, const cha } if (seconds < MIN_READING_PACE_SAMPLE_SECONDS) { - LOG_DBG("ERS", "Time-left pace sample rejected: source=%s seconds=%lu minSeconds=%lu", source ? source : "unknown", - static_cast(seconds), static_cast(MIN_READING_PACE_SAMPLE_SECONDS)); return; } const uint32_t maxReadingPaceSampleSeconds = SETTINGS.getReadingIdleTimeThresholdSeconds(); if (seconds > maxReadingPaceSampleSeconds) { - LOG_DBG("ERS", "Time-left pace sample rejected: source=%s seconds=%lu maxSeconds=%lu", source ? source : "unknown", - static_cast(seconds), static_cast(maxReadingPaceSampleSeconds)); return; } @@ -1319,13 +1541,18 @@ bool EpubReaderActivity::getTimeLeftPaceSeconds(uint16_t& avgSeconds, const char bool EpubReaderActivity::estimateRemainingTimeLeftPages(const bool bookEstimate, float& remainingPages) const { remainingPages = 0.0f; - const int totalPages = section ? section->pageCount : 0; + const int totalPages = section ? section->estimatedTotalPages() : 0; if (!epub || !section || totalPages == 0) { return false; } if (!bookEstimate) { - const int remainingChapterPages = totalPages - section->currentPage - 1; + int groupedCurrentPage = section->currentPage + 1; + int groupedPageCount = totalPages; + float groupedProgress = 0.0f; + bool groupedEstimated = false; + resolveChapterGroupPageProgress(groupedCurrentPage, groupedPageCount, groupedProgress, groupedEstimated); + const int remainingChapterPages = groupedPageCount - groupedCurrentPage; if (remainingChapterPages <= 0) { return false; } @@ -1362,7 +1589,7 @@ bool EpubReaderActivity::estimateRemainingTimeLeftPages(const bool bookEstimate, bool EpubReaderActivity::estimateProgressTimeLeftSeconds(uint32_t& seconds) const { seconds = 0; - const int totalPages = section ? section->pageCount : 0; + const int totalPages = section ? section->estimatedTotalPages() : 0; if (!epub || !section || totalPages == 0 || epub->getBookSize() == 0) { return false; } @@ -1444,7 +1671,7 @@ bool EpubReaderActivity::formatTimeLeftLabel(char* buf, const size_t len) const const bool bookEstimate = SETTINGS.statusBarTimeLeft == CrossPointSettings::STATUS_BAR_TIME_LEFT::TIME_LEFT_BOOK; uint32_t seconds = 0; if (estimateTimeLeftSeconds(bookEstimate, seconds)) { - formatCompactTimeLeft(seconds, buf, len); + formatCompactReadingDuration(seconds, buf, len); return true; } @@ -1555,7 +1782,7 @@ void EpubReaderActivity::initializeCompletionPromptTrigger() { } bool EpubReaderActivity::isAtOrPastCompletionTrigger() const { - const int totalPages = section ? section->pageCount : 0; + const int totalPages = section ? section->estimatedTotalPages() : 0; if (!epub || !section || totalPages == 0 || completionTriggerSpineIndex < 0) { return false; } @@ -1574,7 +1801,7 @@ bool EpubReaderActivity::isAtOrPastCompletionTrigger() const { bool EpubReaderActivity::shouldQueueCompletionPromptOnChapterExit() const { if (completionPromptShown || completionPromptQueued || stats.isCompleted || footnoteDepth > 0 || !completionTriggerCrossed || !epub || !section || section->pageCount == 0 || completionTriggerSpineIndex < 0 || - section->currentPage < 0) { + section->isBuilding() || section->isPartial()) { return false; } @@ -1625,7 +1852,7 @@ void EpubReaderActivity::loadBookReaderSettings() { return; } - const auto data = loadBookReaderSettingsFile(epub->getCachePath()); + const auto& data = initialBookReaderSettings; bookHasCustomReaderSettings = data.hasCustomReaderSettings; bookHasAutoPageTurnInterval = data.hasAutoPageTurnInterval; bookHasRenderModeOverride = data.hasRenderModeOverride; @@ -1636,7 +1863,6 @@ void EpubReaderActivity::loadBookReaderSettings() { } SETTINGS.epubRenderMode = data.hasRenderModeOverride ? normalizeRenderModeRaw(data.renderMode) : static_cast(EpubRenderMode::CrossInkDefault); - sdFontSystem.ensureLoaded(renderer); } void EpubReaderActivity::saveCurrentBookReaderSettings() { @@ -1644,12 +1870,49 @@ void EpubReaderActivity::saveCurrentBookReaderSettings() { return; } - ReaderSettingsSnapshot snapshot; - captureReaderSettings(snapshot); + if (section && section->isBuilding()) { + // The incremental parser and section writer keep SD handles open. Release + // them before reader_settings.bin is read/written; the build reopens both + // lazily on its next chunk. + section->releaseBuildFile(); + } + + BookReaderSettingsData data = loadBookReaderSettingsFile(epub->getCachePath()); + captureReaderSettings(data.readerSettings); bookHasCustomReaderSettings = true; bookHasRenderModeOverride = true; - saveBookReaderSettingsFile(epub->getCachePath(), bookHasAutoPageTurnInterval, lastAutoPageTurnIntervalSeconds, - bookHasCustomReaderSettings, bookHasRenderModeOverride, SETTINGS.epubRenderMode, snapshot); + initialBookReaderSettings.hasCustomReaderSettings = true; + initialBookReaderSettings.hasAutoPageTurnInterval = bookHasAutoPageTurnInterval; + initialBookReaderSettings.autoPageTurnSeconds = lastAutoPageTurnIntervalSeconds; + initialBookReaderSettings.hasRenderModeOverride = true; + initialBookReaderSettings.renderMode = SETTINGS.epubRenderMode; + data.hasCustomReaderSettings = true; + data.hasAutoPageTurnInterval = bookHasAutoPageTurnInterval; + data.autoPageTurnSeconds = lastAutoPageTurnIntervalSeconds; + data.hasRenderModeOverride = true; + data.renderMode = SETTINGS.epubRenderMode; + saveBookReaderSettingsFile(epub->getCachePath(), data); +} + +void EpubReaderActivity::saveDictionaryFontForBook(const char* familyName, const uint8_t pointSize) { + if (!epub) return; + + if (section && section->isBuilding()) { + section->releaseBuildFile(); + } + + BookReaderSettingsData data = loadBookReaderSettingsFile(epub->getCachePath()); + if (familyName && familyName[0] != '\0') { + std::strncpy(data.dictionarySdFontFamilyName, familyName, sizeof(data.dictionarySdFontFamilyName) - 1); + data.dictionarySdFontFamilyName[sizeof(data.dictionarySdFontFamilyName) - 1] = '\0'; + data.hasDictionaryFontOverride = true; + } else { + data.dictionarySdFontFamilyName[0] = '\0'; + data.hasDictionaryFontOverride = false; + data.dictionaryFontPointSize = 0; + } + if (data.hasDictionaryFontOverride) data.dictionaryFontPointSize = pointSize; + saveBookReaderSettingsFile(epub->getCachePath(), data); } void EpubReaderActivity::saveGlobalSettingsPreservingBookOverrides() { @@ -1689,6 +1952,11 @@ void EpubReaderActivity::saveReaderOptionsForBook(void* ctx) { static_cast(ctx)->saveCurrentBookReaderSettings(); } +void EpubReaderActivity::saveDictionaryFontForBookReader(void* ctx, const char* familyName, const uint8_t pointSize) { + if (!ctx) return; + static_cast(ctx)->saveDictionaryFontForBook(familyName, pointSize); +} + void EpubReaderActivity::saveGlobalSettingsForBookReader(void* ctx) { if (!ctx) { return; @@ -1721,7 +1989,11 @@ void EpubReaderActivity::onEnter() { captureGlobalReaderSettings(); epub->setupCacheDir(); loadBookReaderSettings(); - sdFontSystem.ensureLoaded(renderer); + ensureReaderSdFontLoaded(renderer); + ImageBlock::clearSessionRenderFailures(); + ImageBlock::setExtractor(epub.get(), [](void* context, const char* source, const char* destination) { + return static_cast(context)->extractItemToFile(source, destination); + }); // Configure screen orientation based on settings // NOTE: This affects layout math and must be applied before any render calls. @@ -1733,6 +2005,12 @@ void EpubReaderActivity::onEnter() { BOOKMARKS.loadForBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), "epub"); CLIPPINGS.loadForBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), "epub"); + uint16_t restartPageBuildSpine = UINT16_MAX; + uint16_t restartPageBuildTarget = 0; + bool resumeAutoPageTurnAfterRestart = false; + const bool resumePageBuildAfterRestart = consumeSilentRestartReaderPageBuild( + epub->getPath(), restartPageBuildSpine, restartPageBuildTarget, resumeAutoPageTurnAfterRestart); + if (APP_STATE.pendingBookmarkSpine != UINT16_MAX && APP_STATE.pendingBookmarkProgress >= 0.0f) { // Resume from a bookmark selected on the Home screen currentSpineIndex = APP_STATE.pendingBookmarkSpine; @@ -1758,7 +2036,12 @@ void EpubReaderActivity::onEnter() { cachedChapterPageNumber = progress.pageNumber; cachedChapterTotalPageCount = progress.pageCount; } - LOG_DBG("ERS", "Loaded cache: %d, %d", currentSpineIndex, nextPageNumber); + if (progress.hasVisibleTextOffset) { + cachedVisibleTextOffset = progress.visibleTextOffset; + // A content position may come from another device whose page count does not + // match this layout. Let the section builder resolve it before showing a page. + pendingRelayoutReposition = true; + } } } // We may want a better condition to detect if we are opening for the first time. @@ -1767,7 +2050,31 @@ void EpubReaderActivity::onEnter() { int textSpineIndex = epub->getSpineIndexForTextReference(); if (textSpineIndex != 0) { currentSpineIndex = textSpineIndex; - LOG_DBG("ERS", "Opened for first time, navigating to text reference at index %d", textSpineIndex); + } + } + + if (resumePageBuildAfterRestart) { + if (static_cast(restartPageBuildSpine) < epub->getSpineItemsCount()) { + currentSpineIndex = restartPageBuildSpine; + nextPageNumber = restartPageBuildTarget; + cachedSpineIndex = currentSpineIndex; + pendingPageJump = restartPageBuildTarget; + pendingPercentJump = false; + pendingParagraphIndex = UINT16_MAX; + pendingClippingIndex = UINT16_MAX; + lowMemoryPartialRestartAttempted = true; + LOG_INF("ERS", "Resuming low-memory partial build after silent restart: spine=%u target=%u", + restartPageBuildSpine, restartPageBuildTarget); + if (resumeAutoPageTurnAfterRestart) { + const uint16_t seconds = getAutoPageTurnIntervalSeconds(); + lastPageTurnTime = millis(); + pageTurnDuration = static_cast(seconds) * 1000UL; + automaticPageTurnActive = true; + LOG_INF("ERS", "Restored Auto Page Turn after silent restart: interval=%u", seconds); + } + } else { + LOG_ERR("ERS", "Ignoring invalid low-memory partial restart target: spine=%u target=%u", restartPageBuildSpine, + restartPageBuildTarget); } } @@ -1793,18 +2100,33 @@ void EpubReaderActivity::onEnter() { // Save current epub as last opened epub and add to recent books APP_STATE.openEpubPath = epub->getPath(); APP_STATE.saveToFile(); - RECENT_BOOKS.addOrUpdateBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath()); + const RecentBook::CoverState coverState = + epub->hasCoverImage() ? RecentBook::CoverState::Unknown : RecentBook::CoverState::Missing; + RECENT_BOOKS.addOrUpdateBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), + coverState == RecentBook::CoverState::Missing ? "" : epub->getThumbBmpPath(), + coverState); // Trigger first update requestUpdate(); } void EpubReaderActivity::onExit() { + // The extraction callback holds the Epub as a raw context pointer. + ImageBlock::setExtractor(nullptr, nullptr); + releaseGrayscaleStripScratch(); + + // SD-font caches live in the renderer singleton, so leaving them resident after + // the reader exits can fragment the contiguous heap needed for Home cover images. + releaseReaderSdFontCachesForLowMemory(renderer, "ERS", "reader exit"); Activity::onExit(); // Deactivate reader-specific front button mapping. mappedInput.setReaderMode(false); + if (footnoteDepth == 0 && !flushQueuedProgress()) { + LOG_ERR("ERS", "Failed to flush debounced reader progress on exit"); + } + // Reset orientation back to portrait for the rest of the UI renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -1882,7 +2204,7 @@ void EpubReaderActivity::openReaderMenu() { RenderLock lock(*this); previewActive = activeFootnotePreview; currentPage = section ? section->currentPage + 1 : 0; - totalPages = section ? section->pageCount : 0; + totalPages = section ? section->estimatedTotalPages() : 0; bmSpine = static_cast(currentSpineIndex); bmProgress = (section && totalPages > 0) ? static_cast(section->currentPage) / totalPages : 0.0f; bookmarkPageCount = totalPages > 0 ? totalPages : 1; @@ -1892,24 +2214,27 @@ void EpubReaderActivity::openReaderMenu() { const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); pauseReadingPaceTimer("reader_menu"); + const BookReaderSettingsData bookSettings = loadBookReaderSettingsFile(epub->getCachePath()); startActivityForResult( std::make_unique( renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation, - !previewActive && !currentPageFootnotes.empty(), !BOOKMARKS.getBookmarks().empty(), CLIPPINGS.hasClippings(), + !previewActive && !currentPageFootnotes.empty(), + !previewActive && epub && Dictionary::exists(epub->getCachePath().c_str()), !BOOKMARKS.getBookmarks().empty(), + CLIPPINGS.hasClippings(), !previewActive && BOOKMARKS.hasBookmarkForPage(bmSpine, bmProgress, bookmarkPageCount), isBookCompleted, automaticPageTurnActive, getAutoPageTurnIntervalSeconds(), SETTINGS.statusBarTimeLeft != CrossPointSettings::STATUS_BAR_TIME_LEFT::TIME_LEFT_HIDE, saveReaderOptionsForBook, this, saveGlobalSettingsForBookReader, this, beginGlobalSettingsEditForBookReader, - this, !previewActive && epub && epub->hasStablePageNumbers(), endGlobalSettingsEditForBookReader, this), + this, !previewActive && epub && epub->hasStablePageNumbers(), endGlobalSettingsEditForBookReader, this, + bookSettings.dictionarySdFontFamilyName, bookSettings.dictionaryFontPointSize, + bookSettings.hasDictionaryFontOverride, saveDictionaryFontForBookReader, this), [this](const ActivityResult& result) { if (const auto* clipping = std::get_if(&result.data)) { applyOrientation(clipping->orientation); if (clipping->settingsChanged) { - sdFontSystem.ensureLoaded(renderer); + ensureReaderSdFontLoaded(renderer); RenderLock lock(*this); - if (section) { - cacheCurrentSectionPosition(); - } + prepareCurrentSectionForRelayout(); section.reset(); // Force re-layout with changed reader settings } handleClippingJump(*clipping); @@ -1926,11 +2251,9 @@ void EpubReaderActivity::openReaderMenu() { } applyOrientation(menu->orientation); if (menu->settingsChanged) { - sdFontSystem.ensureLoaded(renderer); + ensureReaderSdFontLoaded(renderer); RenderLock lock(*this); - if (section) { - cacheCurrentSectionPosition(); - } + prepareCurrentSectionForRelayout(); section.reset(); // Force re-layout with changed reader settings } resumeReadingPaceTimer("reader_menu_return"); @@ -1940,6 +2263,74 @@ void EpubReaderActivity::openReaderMenu() { }); } +void EpubReaderActivity::showBuildPopup() { + if (!buildPopupPending || !renderer.hasFrameBuffer()) return; + GUI.drawPopup(renderer, tr(STR_INDEXING)); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + pagesUntilFullRefresh = 1; + buildPopupPending = false; +} + +bool EpubReaderActivity::backgroundSectionBuildHasHeap() { + const auto heap = MemoryBudget::snapshot(); + if (MemoryBudget::hasHeap(heap, MemoryBudget::EPUB_TEXT_LAYOUT_MIN_FREE, + MemoryBudget::EPUB_TEXT_LAYOUT_MIN_MAX_ALLOC)) { + backgroundBuildPausedForLowMemory = false; + return true; + } + + if (!backgroundBuildPausedForLowMemory) { + LOG_DBG("ERS", "Pausing background section build: low heap (free=%u, maxAlloc=%u, need %u/%u)", heap.freeHeap, + heap.maxAllocHeap, MemoryBudget::EPUB_TEXT_LAYOUT_MIN_FREE, MemoryBudget::EPUB_TEXT_LAYOUT_MIN_MAX_ALLOC); + } + backgroundBuildPausedForLowMemory = true; + return false; +} + +void EpubReaderActivity::idlePrewarmNextPage() { + if (!section || section->isBuilding() || activeFootnotePreview || automaticPageTurnActive || + !renderer.hasFrameBuffer() || RenderLock::peek() || lastRenderCompleteMs == 0 || + (millis() - lastRenderCompleteMs) < IDLE_SD_FONT_PREWARM_DELAY_MS) { + return; + } + + const int renderFontId = activeSectionFontId != 0 ? activeSectionFontId : SETTINGS.getReaderFontId(); + if (!renderer.isSdCardFont(renderFontId) || + (idlePrewarmSpine == currentSpineIndex && idlePrewarmPage == section->currentPage && + idlePrewarmFontId == renderFontId)) { + return; + } + + const int nextPage = section->currentPage + 1; + if (nextPage >= section->pageCount) { + return; + } + + const auto heap = MemoryBudget::snapshot(); + if (!MemoryBudget::hasHeap(heap, IDLE_SD_FONT_PREWARM_MIN_FREE, IDLE_SD_FONT_PREWARM_MIN_MAX_ALLOC)) { + return; + } + + // The scan loads one serialized page and may grow persistent SD-font mini buffers. Keep it + // behind both free-heap and contiguous-block gates, and run it once per visible page/font. + idlePrewarmSpine = currentSpineIndex; + idlePrewarmPage = section->currentPage; + idlePrewarmFontId = renderFontId; + + RenderLock lock(*this); + auto page = section->loadPage(nextPage); + if (!page) { + LOG_DBG("ERS", "Idle SD font prewarm skipped: failed to load spine=%d page=%d", currentSpineIndex, nextPage); + return; + } + + const unsigned long startedAt = millis(); + auto scope = renderer.getFontCacheManager()->createPrewarmScope(); + page->renderText(renderer, renderFontId, 0, 0); + scope.endScanAndPrewarm(); + LOG_DBG("ERS", "Idle SD font prewarm: spine=%d page=%d in %lums", currentSpineIndex, nextPage, millis() - startedAt); +} + void EpubReaderActivity::loop() { if (!epub) { // Should never happen @@ -1947,6 +2338,102 @@ void EpubReaderActivity::loop() { return; } + const auto touch = ReaderUtils::detectTouchPageTurn(renderer, mappedInput); + const bool userInputPending = mappedInput.wasAnyPressed() || mappedInput.wasAnyReleased() || touch.tapped || + touch.prev || touch.next || mappedInput.wasScreenTouchReleased(); + if (userInputPending) { + // Do not tear down the parser: suspending here would write a partial cache and + // make the next build replay from page 0. Yield until the requested render starts. + backgroundBuildYieldForInput.store(true, std::memory_order_relaxed); + } + + if (goHomeAfterBuildCancel.load(std::memory_order_relaxed) && !RenderLock::peek()) { + goHomeAfterBuildCancel.store(false, std::memory_order_relaxed); + sectionBuildCancelRequested.store(false, std::memory_order_relaxed); + onGoHome(); + return; + } + + if (RenderLock::peek() && !touch.prev && !touch.next && mappedInput.wasReleased(MappedInputManager::Button::Back) && + mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { + sectionBuildCancelRequested.store(true, std::memory_order_relaxed); + goHomeAfterBuildCancel.store(true, std::memory_order_relaxed); + automaticPageTurnActive = false; + LOG_DBG("ERS", "Back requested while EPUB indexing is busy; cancelling build"); + return; + } + +#if CROSSINK_APP_CAP_TOUCH + if (activeFootnotePreview && touch.tapped && !RenderLock::peek() && + TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + restoreSavedPosition(); + return; + } +#endif + + // Lazily resume a partial's extension build once the reader nears its watermark. Far from it the + // rebuild is all cost (whole-chapter re-layout from page 0) and no benefit this session. + if (!backgroundBuildYieldForInput.load(std::memory_order_relaxed) && section && !section->isBuilding() && + section->isPartial() && !RenderLock::peek() && buildViewportWidth > 0 && !partialRebuildStartFailed && + !partialRebuildAbortedForLowMemory && + section->currentPage + PARTIAL_REBUILD_START_MARGIN >= static_cast(section->pageCount)) { + RenderLock lock(*this); + releaseGrayscaleStripScratch(); + if (section && !section->isBuilding() && section->isPartial() && backgroundSectionBuildHasHeap()) { + const int renderFontId = activeSectionFontId != 0 ? activeSectionFontId : SETTINGS.getReaderFontId(); + const SectionBuildProfile profile = buildProfileForRenderMode(normalizeRenderMode(SETTINGS.epubRenderMode)); + if (!section->startBuild( + readerRenderSpecForProfile(renderFontId, buildViewportWidth, buildViewportHeight, profile))) { + partialRebuildStartFailed = true; + LOG_ERR("ERS", "Failed to start deferred partial extension build"); + } else { + LOG_DBG("ERS", "Reader near partial watermark (%d/%d), resuming extension build", section->currentPage, + section->pageCount); + } + } + } + + // Drive any in-progress incremental section build forward, off the page-turn critical path, + // but only within a small window ahead of the reader: an unbounded build monopolized the + // RenderLock and locked out page turns. The build follows the reader instead, and instant + // reopen comes from suspendBuild() persisting the laid-out pages as a partial on exit. + // Skip while the render mutex is busy so we never delay a pending render; re-check + // isBuilding() under the lock since render() may have just finished it. + // While extending a partial, pageCount is pinned at the partial watermark until the rebuild + // catches up, so keep ticking it even before activeBuildHasCaughtReadablePages() turns true; + // the window check below would compare against the pinned watermark and stall the catch-up. + // Once the extension has caught up, the window applies again: without that, a resumed partial + // rebuilt its whole chapter in one hot-loop burst instead of following the reader. + // sectionBuildWantsTick() holds the catch-up/window logic and is shared with + // skipLoopDelay(), so the loop only runs hot while a tick can actually happen. + if (!backgroundBuildYieldForInput.load(std::memory_order_relaxed) && sectionBuildWantsTick() && !RenderLock::peek() && + (section->isPartial() || section->activeBuildHasCaughtReadablePages())) { + RenderLock lock(*this); + releaseGrayscaleStripScratch(); + // Re-check under the lock: render() may have finalized the build between the outer + // isBuilding() check and acquiring the lock here. + if (section && section->isBuilding() && (section->isPartial() || section->activeBuildHasCaughtReadablePages()) && + backgroundSectionBuildHasHeap()) { + if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) { + LOG_ERR("ERS", "Background section build failed"); + if (section->lastBuildLayoutAbortedForLowMemory() && section->pageCount > 0) { + partialRebuildAbortedForLowMemory = true; + LOG_ERR("ERS", "Background section build suspended for low heap; not retrying for this section"); + return; + } + section.reset(); + requestUpdate(); + return; + } + if (section->isBuildComplete()) { + const bool repositioned = applyDeferredReposition(); + if (repositioned || progressSaveRequiredAfterRelayout) { + requestUpdate(); + } + } + } + } + if (completionPromptQueued) { completionPromptQueued = false; completionPromptShown = true; @@ -2024,6 +2511,15 @@ void EpubReaderActivity::loop() { // finished. Two independent finished-book features key off this same condition. const bool atEndOfBook = currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount(); + // Collect suggestions before arming the /Read move or handling an input that may + // leave the reader. render() normally gets here first, but its update is asynchronous; + // a queued page-turn/home input can otherwise exit and move the EPUB before the render + // task has scanned the book's original folder. + if (atEndOfBook && !endOfBookOptions.loaded()) { + RenderLock lock(*this); + endOfBookOptions.loadOnce(epub->getPath()); + } + // Drop this book from the Recent Books list; if the reader then pages back into the book, // re-add it. So removal only sticks if the reader leaves while still on the End-of-Book // screen. Acts only on the transition (guarded by recentsEntryRemoved) — no per-frame writes. @@ -2047,7 +2543,8 @@ void EpubReaderActivity::loop() { if (automaticPageTurnActive) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || - mappedInput.wasReleased(MappedInputManager::Button::Back)) { + (!touch.prev && !touch.next && mappedInput.wasReleased(MappedInputManager::Button::Back)) || + ReaderUtils::isTouchMenuGesture(mappedInput)) { automaticPageTurnActive = false; // updates chapter title space to indicate page turn disabled requestUpdate(); @@ -2071,6 +2568,12 @@ void EpubReaderActivity::loop() { } } +#if CROSSINK_APP_CAP_TOUCH + if (touch.tapped && handleTouchFootnoteLink(touch.x, touch.y)) { + return; + } +#endif + // Long-press Confirm: execute the configured reader action without opening the menu if (longPressMenuHandled) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || @@ -2125,10 +2628,14 @@ void EpubReaderActivity::loop() { } // Enter reader menu activity. - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || ReaderUtils::isTouchMenuGesture(mappedInput)) { openReaderMenu(); } + if (handleTouchDictionaryLookup()) { + return; + } + if (longPressBackHandled) { if (mappedInput.wasReleased(MappedInputManager::Button::Back) || !mappedInput.isPressed(MappedInputManager::Button::Back)) { @@ -2146,7 +2653,7 @@ void EpubReaderActivity::loop() { } // Short press BACK goes directly to home (or restores position if viewing footnote) - if (mappedInput.wasReleased(MappedInputManager::Button::Back) && + if (!touch.prev && !touch.next && mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { if (footnoteDepth > 0) { restoreSavedPosition(); @@ -2269,6 +2776,8 @@ void EpubReaderActivity::loop() { } auto [prevTriggered, nextTriggered, fromSideBtn, fromTilt] = ReaderUtils::detectPageTurn(mappedInput); + prevTriggered = prevTriggered || touch.prev; + nextTriggered = nextTriggered || touch.next; const bool powerReleased = mappedInput.wasReleased(MappedInputManager::Button::Power); const bool shortPowerTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && powerReleased && mappedInput.getHeldTime() < SETTINGS.getPowerButtonLongPressDuration(); @@ -2283,9 +2792,17 @@ void EpubReaderActivity::loop() { heldLongPowerTurn = true; } if (!prevTriggered && !nextTriggered) { + idlePrewarmNextPage(); return; } + if (nextTriggered && silentPrefetchBuildActive.load(std::memory_order_relaxed)) { + // This turn still advances to the visible next page. The speculative build + // sees this at its next parser checkpoint and leaves no partial .bin behind. + silentPrefetchCancelRequested.store(true, std::memory_order_relaxed); + LOG_DBG("ERS", "Forward page turn requested while silent next-chapter indexing is busy; cancelling prefetch"); + } + // At end of the book with no suggestion menu, forward button goes home and back // button returns to last page if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) { @@ -2353,7 +2870,8 @@ void EpubReaderActivity::loop() { return; } - const char* pageTurnSource = fromTilt ? "tilt" : (fromSideBtn ? "side" : "front"); + const char* pageTurnSource = + (touch.prev || touch.next) ? "touch" : (fromTilt ? "tilt" : (fromSideBtn ? "side" : "front")); if (shortPowerTurn || releasedLongPowerTurn || heldLongPowerTurn) { pageTurnSource = "power"; } @@ -2457,46 +2975,196 @@ void EpubReaderActivity::handleClippingJump(const ClippingJumpResult& clipping) pauseReadingPaceTimer("clipping_jump"); } -void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { - switch (action) { - case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: { - const int spineIdx = currentSpineIndex; - const std::string path = epub->getPath(); - pauseReadingPaceTimer("chapter_selection"); - startActivityForResult( - std::make_unique(renderer, mappedInput, epub, path, spineIdx), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - const auto& chapterResult = std::get(result.data); - RenderLock lock(*this); +bool EpubReaderActivity::handleTouchDictionaryLookup() { + if (!SETTINGS.touchReaderControls || !mappedInput.hasTouch() || RenderLock::peek() || activeFootnotePreview || + !epub) { + return false; + } - clearFootnotePreviewState(); - currentSpineIndex = chapterResult.spineIndex; + int touchX = 0; + int touchY = 0; + unsigned long heldMs = 0; + if (!mappedInput.isScreenTouchTapCandidate(touchX, touchY, heldMs)) { + touchDictionaryLookupHandled = false; + return false; + } + if (touchDictionaryLookupHandled || heldMs < TOUCH_DICTIONARY_LOOKUP_HOLD_MS) { + return false; + } - // If anchor is not empty, it will be used later to calculate the page number. - pendingAnchor = chapterResult.anchor; + touchDictionaryLookupHandled = true; + if (!Dictionary::exists(epub->getCachePath().c_str())) { + return false; + } - // Otherwise page 0 will be used. - nextPageNumber = 0; + openWordSelect(/*framebufferContainsPage=*/true, touchX, touchY, /*autoLookupInitialWord=*/true); + return true; +} - section.reset(); - armReadingPaceWarmup("chapter_jump"); - pauseReadingPaceTimer("chapter_jump"); - } else { - resumeReadingPaceTimer("chapter_selection_cancel"); - } - }); - break; - } - case EpubReaderMenuActivity::MenuAction::FOOTNOTES: { - pauseReadingPaceTimer("footnotes"); - startActivityForResult(std::make_unique(renderer, mappedInput, currentPageFootnotes), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - const auto& footnoteResult = std::get(result.data); - navigateToHref(footnoteResult.href, true); - } else { - resumeReadingPaceTimer("footnotes_cancel"); +std::unique_ptr EpubReaderActivity::reloadDictionaryLookupPage() { + if (!section) return nullptr; + // A Page is variable-sized and can reach tens of KB, so it must remain a + // fallible heap object. It exists only while rebuilding the parent selection + // or a rare full-screen modal background, then is released immediately. + return section->loadPageFromSectionFile(); +} + +std::unique_ptr EpubReaderActivity::reloadDictionaryLookupPageCallback(void* context) { + return static_cast(context)->reloadDictionaryLookupPage(); +} + +void EpubReaderActivity::renderDictionaryLookupBackground() { + auto backgroundPage = reloadDictionaryLookupPage(); + if (!backgroundPage) { + LOG_ERR("DICT", "Failed to reload reader page for dictionary modal background"); + renderer.clearScreen(); + return; + } + + const ReaderViewportLayout layout = computeReaderViewportLayout(renderer, automaticPageTurnActive); + renderer.clearScreen(); + auto* fcm = renderer.getFontCacheManager(); + if (!fcm) { + backgroundPage->render(renderer, SETTINGS.getReaderFontId(), layout.marginLeft, layout.marginTop); + return; + } + auto scope = fcm->createPrewarmScope(); + backgroundPage->render(renderer, SETTINGS.getReaderFontId(), layout.marginLeft, layout.marginTop); + scope.endScanAndPrewarm(); + backgroundPage->render(renderer, SETTINGS.getReaderFontId(), layout.marginLeft, layout.marginTop); +} + +void EpubReaderActivity::renderDictionaryLookupBackgroundCallback(void* context) { + static_cast(context)->renderDictionaryLookupBackground(); +} + +void EpubReaderActivity::openWordSelect(bool framebufferContainsPage, int initialTouchX, int initialTouchY, + bool autoLookupInitialWord) { + std::unique_ptr pageForLookup; + ReaderViewportLayout layout{}; + std::string bookCachePath; + std::string nextPageFirstWord; + + { + RenderLock lock(*this); + if (!section || !epub) { + requestUpdate(); + return; + } + + pageForLookup = section->loadPageFromSectionFile(); + if (!pageForLookup) { + requestUpdate(); + return; + } + + layout = computeReaderViewportLayout(renderer, automaticPageTurnActive); + bookCachePath = epub->getCachePath(); + + if (section->currentPage < section->pageCount - 1) { + const int savedPage = section->currentPage; + section->currentPage = savedPage + 1; + auto nextPage = section->loadPageFromSectionFile(); + section->currentPage = savedPage; + if (nextPage) { + const auto it = std::find_if(nextPage->elements.begin(), nextPage->elements.end(), + [](const auto& element) { return element->getTag() == TAG_PageLine; }); + if (it != nextPage->elements.end()) { + const auto* firstLine = static_cast(it->get()); + if (firstLine->getBlock() && !firstLine->getBlock()->isEmpty()) { + nextPageFirstWord = firstLine->getBlock()->wordText(0); + } + } + } + } + } + + pauseReadingPaceTimer("dictionary_lookup"); + const BookReaderSettingsData bookSettings = loadBookReaderSettingsFile(bookCachePath); + // The activity outlives this call, so it must be heap-owned; make the fixed-size + // object allocation fallible instead of aborting the firmware when memory is tight. + auto wordSelect = makeUniqueNoThrow( + renderer, mappedInput, std::move(pageForLookup), layout.marginLeft, layout.marginTop, std::move(bookCachePath), + std::move(nextPageFirstWord), framebufferContainsPage, layout.marginBottom, initialTouchX, initialTouchY, + autoLookupInitialWord, bookSettings.dictionarySdFontFamilyName, bookSettings.dictionaryFontPointSize, this, + &EpubReaderActivity::renderDictionaryLookupBackgroundCallback, + &EpubReaderActivity::reloadDictionaryLookupPageCallback); + if (!wordSelect) { + LOG_ERR("DICT", "OOM allocating DictionaryWordSelectActivity (%u bytes)", + static_cast(sizeof(DictionaryWordSelectActivity))); + resumeReadingPaceTimer("dictionary_lookup_alloc_failed"); + drawToast(renderer, tr(STR_MEMORY_ERROR)); + delay(1000); + requestUpdate(); + return; + } + startActivityForResult(std::move(wordSelect), [this](const ActivityResult&) { + resumeReadingPaceTimer("dictionary_lookup_return"); + MemoryBudget::logHeapShape("dict.child_destroyed"); + // Dictionary lookup warms multiple SD-font styles and large definition glyph + // sets. The child activity has been destroyed before this callback runs, so + // release those renderer-owned caches before the reader rebuilds its page cache. + releaseReaderSdFontCachesForLowMemory(renderer, "DICT", "dictionary lookup exit"); + MemoryBudget::logHeapShape("dict.after_font_release"); + pendingHeapShapeReaderRedrawStages.fetch_or(HEAP_SHAPE_REDRAW_DICT, std::memory_order_relaxed); + requestUpdate(); + }); +} + +void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { + switch (action) { + case EpubReaderMenuActivity::MenuAction::SEND_NEARBY_BOOK: { + if (!epub) break; + const int page = section ? section->currentPage : nextPageNumber; + const int pageCount = section ? section->estimatedTotalPages() : cachedChapterTotalPageCount; + if (!saveProgress(currentSpineIndex, page, pageCount)) { + LOG_ERR("NBOOK", "Could not save EPUB progress before transfer"); + drawToast(renderer, tr(STR_NEARBY_TRANSFER_PROGRESS_SAVE_FAILED)); + delay(1200); + requestUpdate(); + break; + } + activityManager.goToNearbyBookSend(epub->getPath(), true); + return; + } + case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: { + const int spineIdx = currentSpineIndex; + const std::string path = epub->getPath(); + pauseReadingPaceTimer("chapter_selection"); + startActivityForResult( + std::make_unique(renderer, mappedInput, epub, path, spineIdx), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& chapterResult = std::get(result.data); + RenderLock lock(*this); + + clearFootnotePreviewState(); + currentSpineIndex = chapterResult.spineIndex; + + // If anchor is not empty, it will be used later to calculate the page number. + pendingAnchor = chapterResult.anchor; + + // Otherwise page 0 will be used. + nextPageNumber = 0; + + section.reset(); + armReadingPaceWarmup("chapter_jump"); + pauseReadingPaceTimer("chapter_jump"); + } else { + resumeReadingPaceTimer("chapter_selection_cancel"); + } + }); + break; + } + case EpubReaderMenuActivity::MenuAction::FOOTNOTES: { + pauseReadingPaceTimer("footnotes"); + startActivityForResult(std::make_unique(renderer, mappedInput, currentPageFootnotes), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& footnoteResult = std::get(result.data); + navigateToHref(footnoteResult.href, true); + } else { + resumeReadingPaceTimer("footnotes_cancel"); } requestUpdate(); }); @@ -2524,7 +3192,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } case EpubReaderMenuActivity::MenuAction::DISPLAY_QR: { if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) { - auto p = section->loadPageFromSectionFile(); + auto p = section->loadPage(section->currentPage); if (p) { std::string fullText; for (const auto& el : p->elements) { @@ -2556,6 +3224,31 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction startClipSelection(); break; } + case EpubReaderMenuActivity::MenuAction::LOOKUP: { + openWordSelect(/*framebufferContainsPage=*/false); + break; + } + case EpubReaderMenuActivity::MenuAction::LOOKUP_HISTORY: { + pauseReadingPaceTimer("lookup_history"); + const BookReaderSettingsData bookSettings = loadBookReaderSettingsFile(epub->getCachePath()); + startActivityForResult(std::make_unique(renderer, mappedInput, epub->getCachePath(), + bookSettings.dictionarySdFontFamilyName, + bookSettings.dictionaryFontPointSize), + [this](const ActivityResult&) { + resumeReadingPaceTimer("lookup_history_return"); + requestUpdate(); + }); + break; + } + case EpubReaderMenuActivity::MenuAction::SET_BOOK_DICTIONARY: { + pauseReadingPaceTimer("dictionary_select"); + startActivityForResult(std::make_unique(renderer, mappedInput, epub->getCachePath()), + [this](const ActivityResult&) { + resumeReadingPaceTimer("dictionary_select_return"); + requestUpdate(); + }); + break; + } case EpubReaderMenuActivity::MenuAction::GO_HOME: { onGoHome(); return; @@ -2593,7 +3286,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction pauseReadingPaceTimer("delete_cache_confirm"); startActivityForResult( std::make_unique(renderer, mappedInput, confirmationHeading(StrId::STR_DELETE_CACHE), - epub ? epub->getTitle() : std::string{}), + epub ? epub->getTitle() : std::string{}, false, true), [this](const ActivityResult& result) { if (result.isCancelled) { resumeReadingPaceTimer("delete_cache_cancel"); @@ -2607,7 +3300,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction if (epub && section) { uint16_t backupSpine = currentSpineIndex; uint16_t backupPage = section->currentPage; - const int backupPageCount = section->pageCount; + const int backupPageCount = section->estimatedTotalPages(); if (!saveProgress(backupSpine, backupPage, backupPageCount)) { LOG_ERR("ERS", "Failed to save progress before cache clear"); } @@ -2692,27 +3385,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } if (KOREADER_STORE.hasCredentials()) { const int currentPage = section ? section->currentPage : nextPageNumber; - const int totalPages = section ? section->pageCount : cachedChapterTotalPageCount; - std::optional paragraphIndex; - std::optional listItemIndex; - if (section) { - getSyncPageAnchors(*section, currentPage, paragraphIndex, listItemIndex); - } - - // Pre-compute local KO position and chapter name while Epub is still in RAM. - CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPages}; - if (paragraphIndex.has_value()) { - localPos.paragraphIndex = *paragraphIndex; - localPos.hasParagraphIndex = true; - } - if (listItemIndex.has_value()) { - localPos.liIndex = *listItemIndex; - localPos.hasLiIndex = true; - } - KOReaderPosition localKoPos = ProgressMapper::toKOReader(epub, localPos); - const int tocIdx = epub->getTocIndexForSpineIndex(currentSpineIndex); - std::string localChapterName = (tocIdx >= 0) ? epub->getTocItem(tocIdx).title : ""; - const std::string savedEpubPath = epub->getPath(); + const int totalPages = section ? section->estimatedTotalPages() : cachedChapterTotalPageCount; // Persist current position so the reader resumes at the right page on return. // goToReader() depends on this file, so abort the sync if the write fails. @@ -2723,28 +3396,24 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction return; } - // Release the heavy Section now. Keep Epub alive until onExit(), which still - // needs it for stats/cache cleanup before the sync activity starts. - LOG_DBG("KOSync", "Releasing section for sync (heap before: %u)", (unsigned)ESP.getFreeHeap()); - { - RenderLock lock(*this); - if (section) { - nextPageNumber = section->currentPage; - } - section.reset(); + // ActivityManager owns this one-shot handoff across deferred reader teardown. + auto restartActivity = makeUniqueNoThrow(renderer, mappedInput); + if (!restartActivity) { + LOG_ERR("KOSync", "OOM: restart handoff (free=%u maxAlloc=%u)", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + drawToast(renderer, tr(STR_KOREADER_SYNC_LOW_MEMORY)); + delay(1200); + requestUpdate(); + break; } - LOG_DBG("KOSync", "Section released for sync (heap after: %u)", (unsigned)ESP.getFreeHeap()); pauseReadingPaceTimer("sync_progress"); - activityManager.replaceActivity(std::make_unique( - renderer, mappedInput, savedEpubPath, currentSpineIndex, currentPage, totalPages, std::move(localKoPos), - std::move(localChapterName), paragraphIndex)); + activityManager.replaceActivity(std::move(restartActivity)); } break; } case EpubReaderMenuActivity::MenuAction::NEARBY_POSITION_SYNC: { const int currentPage = section ? section->currentPage : nextPageNumber; - const int totalPages = section ? section->pageCount : std::max(1, cachedChapterTotalPageCount); + const int totalPages = section ? section->estimatedTotalPages() : std::max(1, cachedChapterTotalPageCount); std::optional paragraphIndex; std::optional listItemIndex; if (section) { @@ -2752,6 +3421,12 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPages}; + if (section && currentPage >= 0 && currentPage < section->pageCount) { + if (const auto offset = section->getVisibleTextOffsetForPage(static_cast(currentPage))) { + localPos.visibleTextOffset = *offset; + localPos.hasVisibleTextOffset = true; + } + } if (paragraphIndex.has_value()) { localPos.paragraphIndex = *paragraphIndex; localPos.hasParagraphIndex = true; @@ -2760,7 +3435,18 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction localPos.liIndex = *listItemIndex; localPos.hasLiIndex = true; } - KOReaderPosition localKoPos = ProgressMapper::toKOReader(epub, localPos); + const DocumentMatchMethod matchMethod = KOREADER_STORE.getMatchMethod(); + const PositionCoordinateSpace coordinateSpace = matchMethod == DocumentMatchMethod::FILENAME + ? PositionCoordinateSpace::SourceDocument + : PositionCoordinateSpace::CurrentDocument; + KOReaderPosition localKoPos = ProgressMapper::toKOReader(epub, localPos, coordinateSpace); + if (!localKoPos.valid) { + LOG_ERR("NBPS", "Exact filename sync needs a source map; re-optimize this split EPUB"); + drawToast(renderer, tr(STR_SYNC_REOPTIMIZE_REQUIRED)); + delay(1200); + requestUpdate(); + break; + } const int tocIdx = epub->getTocIndexForSpineIndex(currentSpineIndex); std::string localChapterName = (tocIdx >= 0) ? epub->getTocItem(tocIdx).title : ""; const std::string savedEpubPath = epub->getPath(); @@ -2785,11 +3471,11 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction pauseReadingPaceTimer("nearby_position_sync"); activityManager.replaceActivity(std::make_unique( renderer, mappedInput, epub, savedEpubPath, currentSpineIndex, currentPage, totalPages, std::move(localKoPos), - std::move(localChapterName), paragraphIndex, listItemIndex)); + std::move(localChapterName), matchMethod, paragraphIndex, listItemIndex)); break; } case EpubReaderMenuActivity::MenuAction::BOOKMARK_TOGGLE: { - const int bookmarkPageCount = section ? section->pageCount : 0; + const int bookmarkPageCount = section ? section->estimatedTotalPages() : 0; if (activeFootnotePreview || !section || bookmarkPageCount == 0 || section->pageCount == 0) break; const uint16_t spine = static_cast(currentSpineIndex); const float progress = static_cast(section->currentPage) / static_cast(bookmarkPageCount); @@ -2810,7 +3496,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction paragraphIndex = *pIdx; } char snippet[BOOKMARK_SNIPPET_MAX] = {}; - if (auto page = section->loadPageFromSectionFile()) { + if (auto page = section->loadPage(section->currentPage)) { buildBookmarkSnippet(*page, snippet, sizeof(snippet)); } const auto addResult = @@ -2834,20 +3520,15 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction clearFootnotePreviewState(); if (section && currentSpineIndex == bm.spineIndex) { bool resolved = false; + const uint16_t fallbackPage = + pageFromStoredProgress(bm.progress, static_cast(section->estimatedTotalPages())); if (bm.paragraphIndex != UINT16_MAX) { - if (const auto paragraphPage = section->getPageForParagraphIndex(bm.paragraphIndex)) { - section->currentPage = *paragraphPage; - resolved = true; - LOG_DBG("ERS", "Resolved bookmark paragraph %u to page %u", bm.paragraphIndex, *paragraphPage); - } + section->currentPage = resolveParagraphJumpPage(*section, bm.paragraphIndex, fallbackPage); + resolved = true; + LOG_DBG("ERS", "Resolved bookmark paragraph %u to page %d", bm.paragraphIndex, section->currentPage); } if (!resolved) { - const int totalPages = section->pageCount; - int targetPage = static_cast(bm.progress * static_cast(totalPages)); - if (totalPages > 0 && targetPage >= totalPages) { - targetPage = totalPages - 1; - } - section->currentPage = std::max(0, targetPage); + section->currentPage = fallbackPage; } nextPageNumber = section->currentPage; pendingPercentJump = false; @@ -2870,17 +3551,16 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } case EpubReaderMenuActivity::MenuAction::VIEW_CLIPPINGS: { pauseReadingPaceTimer("clipping_list"); - startActivityForResult( - std::make_unique(renderer, mappedInput, CLIPPINGS.getClippings()), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - const auto& clipping = std::get(result.data); - handleClippingJump(clipping); - } else { - resumeReadingPaceTimer("clipping_list_cancel"); - } - requestUpdate(); - }); + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& clipping = std::get(result.data); + handleClippingJump(clipping); + } else { + resumeReadingPaceTimer("clipping_list_cancel"); + } + requestUpdate(); + }); break; } case EpubReaderMenuActivity::MenuAction::DELETE_BOOKMARKS: { @@ -2909,19 +3589,27 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } void EpubReaderActivity::reindexCurrentSection() { - saveCurrentBookReaderSettings(); - sdFontSystem.ensureLoaded(renderer); - if (activeFootnotePreview) { - restoreSavedPosition(); - return; - } + const bool restorePreviewPosition = activeFootnotePreview; { RenderLock lock(*this); - GUI.drawPopup(renderer, tr(STR_INDEXING)); - if (section) { - cacheCurrentSectionPosition(); + // Saving releases the incremental parser's SD handle. Serialize that close + // with parseStep() so the render task cannot read a just-closed HalFile. + saveCurrentBookReaderSettings(); + ensureReaderSdFontLoaded(renderer); + if (!restorePreviewPosition) { + GUI.drawPopup(renderer, tr(STR_INDEXING)); + prepareCurrentSectionForRelayout(); + section.reset(); + // The newly selected SD font can still hold glyph and advance-table caches from + // the previous page. Release them before the relayout so the parser gets a + // contiguous allocation window instead of reporting a memory failure as an + // invalid book. The renderer reloads the active font lazily while parsing. + releaseReaderSdFontCachesForLowMemory(renderer, "ERS", "reader setting reindex"); } - section.reset(); + } + if (restorePreviewPosition) { + restoreSavedPosition(); + return; } requestUpdate(); } @@ -2929,7 +3617,7 @@ void EpubReaderActivity::reindexCurrentSection() { void EpubReaderActivity::openFileTransfer() { pauseReadingPaceTimer("file_transfer"); if (epub && section) { - saveProgress(currentSpineIndex, section->currentPage, section->pageCount); + saveProgress(currentSpineIndex, section->currentPage, section->estimatedTotalPages()); } activityManager.goToFileTransfer(epub ? epub->getPath() : std::string{}); @@ -2942,7 +3630,9 @@ void EpubReaderActivity::openAutoPageTurnIntervalPicker(const bool ignoreInitial renderer, mappedInput, "EpubReaderAutoPageTurnInterval", StrId::STR_AUTO_TURN_INTERVAL_SECONDS, getAutoPageTurnIntervalSeconds(), MIN_AUTO_PAGE_TURN_INTERVAL_S, MAX_AUTO_PAGE_TURN_INTERVAL_S, 1, 5, StrId::STR_NONE_OPT, /*readerActivity=*/true, - /*allowPowerAsConfirm=*/true, ignoreInitialConfirmRelease), + /*allowPowerAsConfirm=*/true, ignoreInitialConfirmRelease, + /*showPercentValue=*/false, StrId::STR_NONE_OPT, + /*overrideDisabledReaderTouchscreen=*/true), [this](const ActivityResult& result) { if (!result.isCancelled) { setAutoPageTurnIntervalSeconds(static_cast(std::get(result.data).value)); @@ -2960,12 +3650,17 @@ void EpubReaderActivity::startClipSelection() { } ReaderViewportLayout layout{}; - std::vector words; + ClipWordStore wordStore; int readerFontId = 0; int startPage = 0; std::string bookTitle; std::string author; std::string chapterTitle; + std::unique_ptr advanceCollector; + uint8_t clipAdvanceCapLoggedStyles = 0; + uint32_t clippingLayoutSignature = activeSectionLayoutSignature; + + MemoryBudget::logHeapShape("clip.before"); { RenderLock lock(*this); @@ -2976,15 +3671,29 @@ void EpubReaderActivity::startClipSelection() { layout = computeReaderViewportLayout(renderer, automaticPageTurnActive); readerFontId = activeSectionFontId > 0 ? activeSectionFontId : SETTINGS.getReaderFontId(); - const int lineHeight = renderer.getLineHeight(readerFontId); startPage = section->currentPage; + if (renderer.isSdCardFont(readerFontId)) { + // Four 256-entry buckets (~4.2 KB plus reusable RTL strings) cannot fit + // on the reader task stack. They live only while clipping pages are scanned. + advanceCollector = makeUniqueNoThrow(); + if (!advanceCollector) { + LOG_ERR("CLIP", "OOM: clipping advance collector (%u bytes)", + static_cast(sizeof(ClipAdvanceCollector))); + section->currentPage = startPage; + drawToast(renderer, tr(STR_MEMORY_ERROR)); + requestUpdate(); + return; + } + } + const int lineHeight = renderer.getLineHeight(readerFontId); const int pagesToLoad = std::min(3, section->pageCount - startPage); std::array pageWordCounts{}; - static constexpr size_t CLIP_SELECTION_WORDS_PER_PAGE = 80; static constexpr size_t MAX_CLIP_SELECTION_WORDS = 240; static constexpr uint32_t CLIP_SELECTION_WORD_RESERVE_HEADROOM = 16U * 1024U; - const size_t maxSelectableWords = - std::min(MAX_CLIP_SELECTION_WORDS, static_cast(pagesToLoad) * CLIP_SELECTION_WORDS_PER_PAGE); + static constexpr size_t CLIP_SELECTION_INITIAL_TEXT_RESERVE = 4U * 1024U; + // Page density varies with font size and layout. Keep the fixed memory cap, + // but do not mistake the old 80-words-per-page reserve estimate for a limit. + const size_t maxSelectableWords = pagesToLoad > 0 ? MAX_CLIP_SELECTION_WORDS : 0; const uint32_t wordReserveBytes = static_cast(maxSelectableWords * sizeof(WordRef)); const auto heapBeforeWords = MemoryBudget::snapshot(); if (heapBeforeWords.maxAllocHeap < wordReserveBytes + CLIP_SELECTION_WORD_RESERVE_HEADROOM) { @@ -2995,31 +3704,92 @@ void EpubReaderActivity::startClipSelection() { requestUpdate(); return; } - words.reserve(maxSelectableWords); + wordStore.words.reserve(maxSelectableWords); + wordStore.textPool.reserve(CLIP_SELECTION_INITIAL_TEXT_RESERVE); bool wordLimitLogged = false; + bool textPoolLimitLogged = false; + bool textPoolFull = false; for (int pageIdx = 0; pageIdx < pagesToLoad; ++pageIdx) { + if (textPoolFull) break; section->currentPage = startPage + pageIdx; - auto page = section->loadPageFromSectionFile(); + auto page = section->loadPage(section->currentPage); if (!page) break; + if (advanceCollector) { + advanceCollector->reset(); + for (const auto& element : page->elements) { + if (element->getTag() != TAG_PageLine) continue; + const auto& line = static_cast(*element); + if (!line.getBlock()) continue; + + const auto& block = *line.getBlock(); + for (uint16_t i = 0; i < block.wordCount(); ++i) { + const uint8_t style = static_cast(block.wordStyle(i)) & 0x03; + const char* wordText = block.wordText(i); + advanceCollector->addLogicalText(style, wordText); + if ((advanceCollector->truncatedStyles & static_cast(1U << style)) == 0) { + if (renderer.collectSdCardFontShapedRtlCodepoints( + wordText, advanceCollector->codepoints[style], advanceCollector->counts[style], + CLIP_ADVANCE_CODEPOINT_CAPACITY, advanceCollector->rtlTokenScratch, + advanceCollector->rtlVisualScratch)) { + advanceCollector->truncatedStyles |= static_cast(1U << style); + } + } + } + } + for (uint8_t style = 0; style < ClipAdvanceCollector::STYLE_COUNT; ++style) { + if ((advanceCollector->truncatedStyles & static_cast(1U << style)) && + (clipAdvanceCapLoggedStyles & static_cast(1U << style)) == 0) { + LOG_ERR("CLIP", "SD-font advance bucket cap hit for style %u; using glyph-miss fallback", style); + clipAdvanceCapLoggedStyles |= static_cast(1U << style); + } + if (advanceCollector->counts[style] > 0) { + renderer.ensureSdCardFontReady(readerFontId, advanceCollector->codepoints[style], + advanceCollector->counts[style], false, false, + static_cast(1U << style)); + } + } + MemoryBudget::logHeapShape("clip.after_advance_batch"); + } + + // The pool keeps each word NUL-terminated, so estimate the page once + // and grow once before collecting it instead of reallocating per word. + size_t pageTextBytes = 0; + const size_t remainingWords = maxSelectableWords - wordStore.words.size(); + size_t estimatedWords = 0; + for (const auto& element : page->elements) { + if (estimatedWords >= remainingWords || element->getTag() != TAG_PageLine) continue; + const auto& line = static_cast(*element); + if (!line.getBlock()) continue; + const auto& block = *line.getBlock(); + for (uint16_t i = 0; i < block.wordCount() && estimatedWords < remainingWords; ++i) { + const char* wordText = block.wordText(i); + if (!hasVisibleWordText(wordText)) continue; + const size_t wordBytes = strlen(wordText) + 1; + if (wordBytes > ClipWordStore::MAX_TEXT_POOL_BYTES - pageTextBytes) { + pageTextBytes = ClipWordStore::MAX_TEXT_POOL_BYTES; + break; + } + pageTextBytes += wordBytes; + estimatedWords++; + } + } + const size_t remainingTextBytes = ClipWordStore::MAX_TEXT_POOL_BYTES - wordStore.textPool.size(); + wordStore.textPool.reserve(wordStore.textPool.size() + std::min(pageTextBytes, remainingTextBytes)); + for (const auto& element : page->elements) { + if (textPoolFull) break; if (element->getTag() != TAG_PageLine) continue; const auto& line = static_cast(*element); if (!line.getBlock()) continue; const auto& block = *line.getBlock(); const uint16_t count = block.wordCount(); - if (renderer.isSdCardFont(readerFontId) && count > 0) { - for (uint16_t i = 0; i < count; ++i) { - const uint8_t styleMask = static_cast(1u << (static_cast(block.wordStyle(i)) & 0x03)); - renderer.ensureSdCardFontReady(readerFontId, block.wordText(i), styleMask); - } - } for (uint16_t i = 0; i < count; ++i) { const char* wordText = block.wordText(i); if (!hasVisibleWordText(wordText)) continue; - if (words.size() >= maxSelectableWords) { + if (wordStore.words.size() >= maxSelectableWords) { if (!wordLimitLogged) { LOG_ERR("CLIP", "Selectable word cap hit (%u words); clipping range truncated", static_cast(maxSelectableWords)); @@ -3042,30 +3812,42 @@ void EpubReaderActivity::startClipSelection() { word.h = lineHeight; word.pageIdx = pageIdx; word.pageWordIndex = pageWordCounts[pageIdx]++; - word.text = wordText; + if (!wordStore.appendText(word, wordText)) { + if (!textPoolLimitLogged) { + LOG_ERR("CLIP", "Selectable text pool reached its 64 KB limit; clipping range truncated"); + textPoolLimitLogged = true; + } + textPoolFull = true; + break; + } word.style = textStyle; word.endsWithInsertedHyphen = block.wordEndsWithInsertedHyphen(i); word.lineIsRtl = block.getBlockStyle().isRtl; - words.push_back(std::move(word)); + wordStore.words.push_back(word); } } + MemoryBudget::logHeapShape("clip.after_word_store"); } section->currentPage = startPage; - auto endsWithHyphen = [](const std::string& word) { return !word.empty() && word.back() == '-'; }; + auto endsWithHyphen = [&wordStore](const WordRef& word) { + const char* text = wordStore.text(word); + return word.textLength > 0 && text[word.textLength - 1] == '-'; + }; const int indentThreshold = renderer.getLineHeight(readerFontId) / 2; int previousLineFirstIdx = -1; - for (int i = 0; i < static_cast(words.size()); ++i) { - const bool newLine = i == 0 || words[i].pageIdx != words[i - 1].pageIdx || words[i].y != words[i - 1].y; + for (int i = 0; i < static_cast(wordStore.words.size()); ++i) { + const bool newLine = i == 0 || wordStore.words[i].pageIdx != wordStore.words[i - 1].pageIdx || + wordStore.words[i].y != wordStore.words[i - 1].y; if (!newLine) continue; - const bool byEmSpace = hasEmSpacePrefix(words[i].text); + const bool byEmSpace = hasEmSpacePrefix(wordStore.text(wordStore.words[i])); const bool byIndent = !byEmSpace && previousLineFirstIdx >= 0 && - words[i].x > words[previousLineFirstIdx].x + indentThreshold && - !endsWithHyphen(words[i - 1].text); + wordStore.words[i].x > wordStore.words[previousLineFirstIdx].x + indentThreshold && + !endsWithHyphen(wordStore.words[i - 1]); if (byEmSpace || byIndent) { - words[i].paragraphStart = true; + wordStore.words[i].paragraphStart = true; } previousLineFirstIdx = i; } @@ -3078,44 +3860,69 @@ void EpubReaderActivity::startClipSelection() { author = epub->getAuthor(); } - if (words.empty()) { + if (wordStore.words.empty()) { LOG_ERR("CLIP", "No selectable words on current EPUB page"); requestUpdate(); return; } + advanceCollector.reset(); pauseReadingPaceTimer("clip_selection"); - startActivityForResult( - std::make_unique(renderer, mappedInput, std::move(words), readerFontId, *section, - startPage, layout.marginTop, layout.marginLeft), - [this, bookTitle = std::move(bookTitle), author = std::move(author), - chapterTitle = std::move(chapterTitle)](const ActivityResult& result) { - if (!result.isCancelled) { - const auto& clip = std::get(result.data); - if (!clip.text.empty()) { - const size_t clippingIndex = CLIPPINGS.getClippings().size(); - const auto addResult = - CLIPPINGS.addClipping(static_cast(currentSpineIndex), clip.sectionPage, clip.endSectionPage, - clip.sectionPageCount, clip.startPageWordIndex, clip.endPageWordIndex, - clip.wordCount, chapterTitle.c_str(), clip.paragraphIndex, clip.text); - bool exported = false; - if (addResult == ClippingStore::AddResult::Added) { - exported = ClippingsManager::saveClipping(bookTitle, author, chapterTitle, - static_cast(clip.sectionPage) + 1, clip.text); - if (!exported && !CLIPPINGS.removeClippingAt(clippingIndex)) { - LOG_ERR("CLIP", "Failed to roll back clipping after export failure"); - } - } - const bool saved = addResult == ClippingStore::AddResult::Added && exported; - drawToast(renderer, addResult == ClippingStore::AddResult::LimitReached ? tr(STR_CLIPPING_LIMIT_REACHED) - : saved ? tr(STR_CLIPPING_SAVED) - : tr(STR_CLIPPING_FAILED)); - delay(1000); + auto clipSelection = + makeUniqueNoThrow(renderer, mappedInput, std::move(wordStore), readerFontId, *section, + startPage, layout.marginTop, layout.marginLeft); + if (!clipSelection) { + LOG_ERR("CLIP", "OOM: failed to allocate clip selection activity"); + resumeReadingPaceTimer("clip_selection_alloc_failed"); + requestUpdate(); + return; + } + startActivityForResult(std::move(clipSelection), [this, bookTitle = std::move(bookTitle), author = std::move(author), + chapterTitle = std::move(chapterTitle), + clippingLayoutSignature](const ActivityResult& result) { + MemoryBudget::logHeapShape("clip.child_destroyed"); + const char* clippingFeedback = nullptr; + bool saved = false; + if (!result.isCancelled) { + const auto& clip = std::get(result.data); + if (!clip.text.empty()) { + const size_t clippingIndex = CLIPPINGS.clippingCount(); + const auto addResult = + CLIPPINGS.addClipping(static_cast(currentSpineIndex), clip.sectionPage, clip.endSectionPage, + clip.sectionPageCount, clip.startPageWordIndex, clip.endPageWordIndex, clip.wordCount, + chapterTitle.c_str(), clip.paragraphIndex, clip.text, clippingLayoutSignature); + bool exported = false; + if (addResult == ClippingStore::AddResult::Added) { + exported = ClippingsManager::saveClipping(bookTitle, author, chapterTitle, + static_cast(clip.sectionPage) + 1, clip.text); + if (!exported && !CLIPPINGS.removeClippingAt(clippingIndex)) { + LOG_ERR("CLIP", "Failed to roll back clipping after export failure"); } } - resumeReadingPaceTimer("clip_selection_return"); - requestUpdate(); - }); + saved = addResult == ClippingStore::AddResult::Added && exported; + clippingFeedback = addResult == ClippingStore::AddResult::LimitReached ? tr(STR_CLIPPING_LIMIT_REACHED) + : saved ? tr(STR_CLIPPING_SAVED) + : tr(STR_CLIPPING_FAILED); + } + } + resumeReadingPaceTimer("clip_selection_return"); + releaseReaderSdFontCachesForLowMemory(renderer, "CLIP", "clipping selection exit"); + MemoryBudget::logHeapShape("clip.after_font_release"); + pendingHeapShapeReaderRedrawStages.fetch_or(HEAP_SHAPE_REDRAW_CLIP, std::memory_order_relaxed); + if (clippingFeedback) { +#if CROSSINK_APP_CAP_TOUCH + if (saved && mappedInput.hasTouchHardware() && requestUpdateAndWait() != RequestUpdateResult::Rendered) { + LOG_ERR("CLIP", "Could not render saved highlight before clipping toast"); + } +#endif + { + RenderLock lock(*this); + drawToast(renderer, clippingFeedback); + } + delay(1000); + } + requestUpdate(); + }); } void EpubReaderActivity::resetReadingPaceData() { @@ -3172,7 +3979,7 @@ void EpubReaderActivity::executeReaderQuickAction(CrossPointSettings::LONG_PRESS onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction::BOOKMARK_TOGGLE); break; case CrossPointSettings::LONG_MENU_REFRESH_SCREEN: - pagesUntilFullRefresh = 1; // Forces HALF_REFRESH on next render + prepareManualRefresh(); requestUpdate(); break; case CrossPointSettings::LONG_MENU_SYNC_PROGRESS: @@ -3225,11 +4032,15 @@ void EpubReaderActivity::executeReaderQuickAction(CrossPointSettings::LONG_PRESS requestUpdate(); } break; - case CrossPointSettings::LONG_MENU_TOGGLE_DARK_MODE: - SETTINGS.readerDarkMode = !SETTINGS.readerDarkMode; - saveCurrentBookReaderSettings(); + case CrossPointSettings::LONG_MENU_TOGGLE_DARK_MODE: { + { + RenderLock lock(*this); + SETTINGS.readerDarkMode = !SETTINGS.readerDarkMode; + saveCurrentBookReaderSettings(); + } requestUpdate(); break; + } case CrossPointSettings::LONG_MENU_FOOTNOTES: executeFootnoteQuickAction(); break; @@ -3239,6 +4050,15 @@ void EpubReaderActivity::executeReaderQuickAction(CrossPointSettings::LONG_PRESS case CrossPointSettings::LONG_MENU_CREATE_CLIPPING: startClipSelection(); break; + case CrossPointSettings::LONG_MENU_LOOKUP_WORD: + if (epub && Dictionary::exists(epub->getCachePath().c_str())) { + openWordSelect(/*framebufferContainsPage=*/true); + } else { + drawToast(renderer, tr(STR_DICT_NO_DICT_SET)); + delay(1000); + requestUpdate(); + } + break; case CrossPointSettings::LONG_MENU_OFF: default: break; @@ -3250,6 +4070,7 @@ bool EpubReaderActivity::quickActionUsesConfirmRelease(const CrossPointSettings: case CrossPointSettings::LONG_MENU_READING_STATS: case CrossPointSettings::LONG_MENU_CYCLE_PAGE_TURN: case CrossPointSettings::LONG_MENU_CREATE_CLIPPING: + case CrossPointSettings::LONG_MENU_LOOKUP_WORD: return true; case CrossPointSettings::LONG_MENU_FOOTNOTES: return currentPageFootnotes.size() > 1; @@ -3368,6 +4189,10 @@ bool EpubReaderActivity::executeShortPowerButtonAction() { mappedInput.suppressNextPowerConfirmRelease(); executeReaderQuickAction(CrossPointSettings::LONG_MENU_CREATE_CLIPPING); return true; + case CrossPointSettings::SHORT_PWRBTN::LOOKUP_WORD: + mappedInput.suppressNextPowerConfirmRelease(); + executeReaderQuickAction(CrossPointSettings::LONG_MENU_LOOKUP_WORD); + return true; default: return false; } @@ -3461,6 +4286,10 @@ bool EpubReaderActivity::executeLongPowerButtonAction() { mappedInput.suppressNextPowerConfirmRelease(); executeReaderQuickAction(CrossPointSettings::LONG_MENU_CREATE_CLIPPING); return true; + case CrossPointSettings::SHORT_PWRBTN::LOOKUP_WORD: + mappedInput.suppressNextPowerConfirmRelease(); + executeReaderQuickAction(CrossPointSettings::LONG_MENU_LOOKUP_WORD); + return true; default: return false; } @@ -3602,7 +4431,7 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) { // Preserve current reading position only when we need a live re-layout. if (rendererChanged && section) { - cacheCurrentSectionPosition(); + prepareCurrentSectionForRelayout(); } if (settingsChanged) { @@ -3638,10 +4467,19 @@ void EpubReaderActivity::setAutoPageTurnIntervalSeconds(uint16_t seconds) { lastAutoPageTurnIntervalSeconds = seconds; bookHasAutoPageTurnInterval = true; if (epub) { - ReaderSettingsSnapshot snapshot; - captureReaderSettings(snapshot); - saveBookReaderSettingsFile(epub->getCachePath(), bookHasAutoPageTurnInterval, seconds, bookHasCustomReaderSettings, - bookHasRenderModeOverride, SETTINGS.epubRenderMode, snapshot); + BookReaderSettingsData data = loadBookReaderSettingsFile(epub->getCachePath()); + captureReaderSettings(data.readerSettings); + initialBookReaderSettings.hasAutoPageTurnInterval = true; + initialBookReaderSettings.autoPageTurnSeconds = seconds; + initialBookReaderSettings.hasCustomReaderSettings = bookHasCustomReaderSettings; + initialBookReaderSettings.hasRenderModeOverride = bookHasRenderModeOverride; + initialBookReaderSettings.renderMode = SETTINGS.epubRenderMode; + data.hasAutoPageTurnInterval = true; + data.autoPageTurnSeconds = seconds; + data.hasCustomReaderSettings = bookHasCustomReaderSettings; + data.hasRenderModeOverride = bookHasRenderModeOverride; + data.renderMode = SETTINGS.epubRenderMode; + saveBookReaderSettingsFile(epub->getCachePath(), data); } lastPageTurnTime = millis(); pageTurnDuration = static_cast(seconds) * 1000UL; @@ -3652,9 +4490,7 @@ void EpubReaderActivity::setAutoPageTurnIntervalSeconds(uint16_t seconds) { if (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight()) { // Preserve current reading position so we can restore after reflow. RenderLock lock(*this); - if (section) { - cacheCurrentSectionPosition(); - } + prepareCurrentSectionForRelayout(); section.reset(); } } @@ -3670,6 +4506,9 @@ void EpubReaderActivity::pageTurn(bool isForwardTurn, const char* source) { armReadingPaceWarmup("preview_back_page"); if (section && section->currentPage > 0) { section->currentPage--; + } else if (source && strcmp(source, "touch") == 0) { + restoreSavedPosition(); + return; } } lastPageTurnTime = millis(); @@ -3680,8 +4519,14 @@ void EpubReaderActivity::pageTurn(bool isForwardTurn, const char* source) { uint32_t forwardReadSeconds = 0; const bool shouldRecordForwardRead = forwardPageReadElapsed(forwardReadSeconds, source); recordCurrentPageReadingTime(source); - const bool exitingChapter = section && section->pageCount > 0 && section->currentPage >= section->pageCount - 1; - if (section->currentPage < section->pageCount - 1) { + const bool exitingChapter = + section && !section->isBuilding() && section->pageCount > 0 && section->currentPage >= section->pageCount - 1; + // Advance within the section while there are (or may still be) more pages: either a built + // page ahead, or the section is still building (windowed), in which case more pages exist + // beyond the current watermark and render()'s ensure-built pump will lay them out. Only when + // the section is fully built AND we're on its last page do we move to the next spine -- using + // the live pageCount alone would mistake the build watermark for the end of a giant spine. + if (section->currentPage < section->pageCount - 1 || section->isBuilding() || section->isPartial()) { section->currentPage++; } else { if (shouldQueueCompletionPromptOnChapterExit()) { @@ -3727,6 +4572,9 @@ void EpubReaderActivity::pageTurn(bool isForwardTurn, const char* source) { // TODO: Failure handling void EpubReaderActivity::render(RenderLock&& lock) { + // The render task now owns the mutex requested by the input action. Background + // indexing may resume only after this render releases it. + backgroundBuildYieldForInput.store(false, std::memory_order_relaxed); if (!epub) { return; } @@ -3737,14 +4585,69 @@ void EpubReaderActivity::render(RenderLock&& lock) { GUI.drawPopup(renderer, tr(STR_SAVE_PROGRESS_FAILED)); }; - const auto showLowMemoryLayoutError = [this]() { + const auto queueLowMemoryLayoutAlert = [this](const bool goHomeOnBack) { snprintf(APP_STATE.pendingAlertTitle, sizeof(APP_STATE.pendingAlertTitle), "%s", tr(STR_EPUB_LAYOUT_MEMORY_TITLE)); snprintf(APP_STATE.pendingAlertBody, sizeof(APP_STATE.pendingAlertBody), "%s", tr(STR_EPUB_LAYOUT_MEMORY_BODY)); - APP_STATE.pendingAlertGoHomeOnBack.store(true, std::memory_order_relaxed); + APP_STATE.pendingAlertGoHomeOnBack.store(goHomeOnBack, std::memory_order_relaxed); APP_STATE.hasPendingAlert.store(true, std::memory_order_release); + }; + + const auto showLowMemoryLayoutError = [this, &queueLowMemoryLayoutAlert]() { + queueLowMemoryLayoutAlert(true); GUI.drawPopup(renderer, tr(STR_EPUB_LAYOUT_MEMORY_TITLE)); }; + // A section build failure (e.g. an invalid/corrupt EPUB that fails XML parsing) leaves the + // "Indexing" popup on screen with no way forward. Surface an explicit error instead of hanging. + // clearScreen first so the error popup doesn't overlay the stale "Indexing" popup. + const auto showBuildError = [this]() { + renderer.clearScreen(ReaderUtils::readerBackgroundColor()); + GUI.drawPopup(renderer, tr(STR_INDEX_FAILED)); + automaticPageTurnActive = false; + }; + + const auto showIndexingPopup = [this]() { + GUI.drawPopup(renderer, tr(STR_INDEXING)); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + pagesUntilFullRefresh = 1; + }; + + bool buildCancelledForBack = false; + const auto cancelBuildForBack = [this, &buildCancelledForBack]() { + if (!sectionBuildCancelRequested.load(std::memory_order_relaxed)) { + return false; + } + if (section && section->isBuilding()) { + section->suspendBuild(); + } + buildCancelledForBack = true; + automaticPageTurnActive = false; + LOG_DBG("ERS", "EPUB section build cancelled by Back"); + return true; + }; + + // A freshly restarted reader has a less fragmented heap than one that has just + // built and displayed several pages. Try that once before changing layout modes; + // the RTC token marks the resumed attempt so it proceeds through the fallbacks. + const auto restartForLowMemoryLayout = [this](const int targetPage, const int savedPage, const int pageCount, + const char* stage) { + if (lowMemoryPartialRestartAttempted || targetPage < 0 || targetPage >= std::numeric_limits::max() || + currentSpineIndex < 0 || currentSpineIndex > std::numeric_limits::max()) { + return false; + } + LOG_ERR("ERS", "Low heap during %s; silent restarting to retry page %d (spine=%d)", stage, targetPage, + currentSpineIndex); + if (!saveProgress(currentSpineIndex, std::max(0, savedPage), std::max(0, pageCount))) { + LOG_ERR("ERS", "Skipping silent restart because progress save failed"); + return false; + } + armSilentRestartReaderPageBuild(epub->getPath(), static_cast(currentSpineIndex), + static_cast(targetPage), automaticPageTurnActive); + lowMemoryPartialRestartAttempted = true; + silentRestartToReader(); + return true; + }; + // edge case handling for sub-zero spine index if (currentSpineIndex < 0) { currentSpineIndex = 0; @@ -3756,8 +4659,8 @@ void EpubReaderActivity::render(RenderLock&& lock) { // Show end of book screen if (currentSpineIndex == epub->getSpineItemsCount()) { - // Sole load site: runs on the render task (serialized by RenderLock); the main - // task only reads the suggestions once the loaded flag is published + // Usually preloaded by loop() before the /Read move is armed. Keep this fallback + // for an initial render that wins the race with the main task. endOfBookOptions.loadOnce(epub->getPath()); renderer.clearScreen(); endOfBookOptions.render(renderer, mappedInput); @@ -3767,20 +4670,30 @@ void EpubReaderActivity::render(RenderLock&& lock) { return; } - const ReaderViewportLayout layout = computeReaderViewportLayout(renderer, automaticPageTurnActive); + const ReaderViewportLayout layout = computeReaderViewportLayout( + renderer, automaticPageTurnActive, activeFootnotePreview || !pendingFootnotePreviewAnchor.empty()); const uint16_t viewportWidth = layout.viewportWidth; const uint16_t viewportHeight = layout.viewportHeight; + buildViewportWidth = viewportWidth; + buildViewportHeight = viewportHeight; if (!section) { + preparedNextSpineIndex = -1; + // Section loading/indexing can need a large contiguous block. Return the + // render-only strip before starting that work. + releaseGrayscaleStripScratch(); const auto filepath = epub->getSpineItem(currentSpineIndex).href; LOG_DBG("ERS", "Loading file: %s, index: %d (free=%u, maxAlloc=%u)", filepath.c_str(), currentSpineIndex, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); const int readerFontId = SETTINGS.getReaderFontId(); const EpubRenderMode selectedRenderMode = normalizeRenderMode(SETTINGS.epubRenderMode); + const bool fullSectionIndexing = SETTINGS.indexingMethod == CrossPointSettings::INDEXING_FULL_SECTION; EpubRenderMode usedRenderMode = selectedRenderMode; const bool buildingFootnotePreview = !pendingFootnotePreviewAnchor.empty(); bool loadedSection = false; bool safeModeBuildSucceeded = false; + partialRebuildStartFailed = false; + partialRebuildAbortedForLowMemory = false; auto loadSectionWithFont = [&](const int fontId, const EpubRenderMode renderMode) { const std::string cacheSuffix = buildingFootnotePreview ? footnotePreviewCacheSuffix(renderMode, pendingFootnotePreviewAnchor) @@ -3791,15 +4704,14 @@ void EpubReaderActivity::render(RenderLock&& lock) { fontId, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); return false; } - if (!section->loadSectionFile(fontId, SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, - SETTINGS.forceParagraphIndents, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, - SETTINGS.imageRendering, SETTINGS.bionicReadingEnabled, - SETTINGS.guideReadingEnabled, renderMode)) { + const ReaderRenderSpec spec = + readerRenderSpecForProfile(fontId, viewportWidth, viewportHeight, buildProfileForRenderMode(renderMode)); + if (!section->loadSectionFile(spec)) { section.reset(); return false; } activeSectionFontId = fontId; + activeSectionLayoutSignature = readerRenderSpecSignature(spec); usedRenderMode = renderMode; return true; }; @@ -3808,26 +4720,55 @@ void EpubReaderActivity::render(RenderLock&& lock) { if (loadedSection && !pendingRelayoutReposition) { cachedChapterTotalPageCount = 0; } + const bool partialCacheLoaded = loadedSection && section && section->isPartial(); - if (!loadedSection) { - LOG_DBG("ERS", "Cache not found, building... (free=%u, maxAlloc=%u)", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + if (!loadedSection || partialCacheLoaded) { + if (!loadedSection) { + // Font selection previews can leave glyph and advance-table caches resident. + // Drop them before allocating a replacement layout after a render-spec mismatch. + releaseReaderSdFontCachesForLowMemory(renderer, "ERS", "uncached section build"); + } + if (partialCacheLoaded) { + LOG_DBG("ERS", "Partial cache found (%u pages), resuming build... (free=%u, maxAlloc=%u)", section->pageCount, + ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + } else { + LOG_DBG("ERS", "Cache not found, building... (free=%u, maxAlloc=%u)", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + } - const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); }; + const auto popupFn = [this]() { + if (renderer.hasFrameBuffer()) GUI.drawPopup(renderer, tr(STR_INDEXING)); + }; bool imagesWereSuppressed = false; bool layoutAbortedForLowMemory = false; bool fallbackBuildSucceeded = false; + bool usedReadablePartialFallback = false; + EpubRenderMode lastAttemptedRenderMode = selectedRenderMode; + bool lastAttemptUsedSafeMode = false; + std::unique_ptr
    readablePartialFallback; + if (fullSectionIndexing && partialCacheLoaded) { + // Keep the known-readable partial alive until its full replacement has + // been built and atomically promoted. A failed full build must not turn + // a mode change or reopen into an invalid-book dead end. + readablePartialFallback = std::move(section); + } auto buildSectionWithProfile = [&](const int fontId, const SectionBuildProfile& profile) { + lastAttemptedRenderMode = profile.renderMode; + lastAttemptUsedSafeMode = profile.safeMode; const std::string cacheSuffix = buildingFootnotePreview ? footnotePreviewCacheSuffix(profile.renderMode, pendingFootnotePreviewAnchor) : std::string(sectionCacheSuffixForRenderMode(profile.renderMode)); - section.reset(); - section = makeUniqueNoThrow
    (epub, currentSpineIndex, renderer, cacheSuffix.c_str()); - if (!section) { - LOG_ERR("ERS", "Failed to allocate %s section builder for spine %d (free=%u, maxAlloc=%u)", profile.label, - currentSpineIndex, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); - layoutAbortedForLowMemory = true; - return false; + const bool reuseLoadedPartial = !fullSectionIndexing && section && section->isPartial() && + fontId == activeSectionFontId && profile.renderMode == usedRenderMode; + if (!reuseLoadedPartial) { + section.reset(); + section = makeUniqueNoThrow
    (epub, currentSpineIndex, renderer, cacheSuffix.c_str()); + if (!section) { + LOG_ERR("ERS", "Failed to allocate %s section builder for spine %d (free=%u, maxAlloc=%u)", profile.label, + currentSpineIndex, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + layoutAbortedForLowMemory = true; + return false; + } } bool attemptImagesWereSuppressed = false; @@ -3835,53 +4776,187 @@ void EpubReaderActivity::render(RenderLock&& lock) { const SectionBuildOptions buildOptions{ buildingFootnotePreview ? pendingFootnotePreviewAnchor.c_str() : nullptr, static_cast(buildingFootnotePreview ? FOOTNOTE_PREVIEW_MAX_PAGES : 0)}; - GUI.drawPopup(renderer, tr(STR_INDEXING)); - // The popup's own refresh is a plain FAST, so force the page that replaces it onto the HALF - // ghost-cleanup path -- otherwise the "INDEXING" text ghosts under the rendered page. - pagesUntilFullRefresh = 1; - const bool buildSucceeded = section->createSectionFile( - fontId, SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.forceParagraphIndents, - SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, - profile.embeddedStyle, SETTINGS.imageRendering, profile.bionicReadingEnabled, profile.guideReadingEnabled, - popupFn, &attemptImagesWereSuppressed, &attemptLayoutAbortedForLowMemory, profile.renderMode, buildOptions); - imagesWereSuppressed = imagesWereSuppressed || attemptImagesWereSuppressed; - layoutAbortedForLowMemory = attemptLayoutAbortedForLowMemory; - if (buildSucceeded) { - activeSectionFontId = fontId; - usedRenderMode = profile.renderMode; - safeModeBuildSucceeded = profile.safeMode; - LOG_DBG("ERS", - "%s section cache built: spine=%d font=%d mode=%u embedded=%u bionic=%u guide=%u pages=%u free=%u " - "maxAlloc=%u", - profile.label, currentSpineIndex, fontId, static_cast(profile.renderMode), - static_cast(profile.embeddedStyle), static_cast(profile.bionicReadingEnabled), + const ReaderRenderSpec spec = readerRenderSpecForProfile(fontId, viewportWidth, viewportHeight, profile); + const bool needsFullBuild = fullSectionIndexing || buildingFootnotePreview || pendingPercentJump || + pendingClippingIndex != UINT16_MAX || pendingParagraphIndex != UINT16_MAX; + bool buildSucceeded = false; + if (needsFullBuild) { + showIndexingPopup(); + { + GfxRenderer::FrameBufferLoan loan(renderer); + buildSucceeded = section->createSectionFile(spec, popupFn, &attemptImagesWereSuppressed, + &attemptLayoutAbortedForLowMemory, buildOptions); + } + } else { + const int target = pendingPageJump.has_value() ? *pendingPageJump : (nextPageNumber < 0 ? 0 : nextPageNumber); + const size_t spineBytes = + epub->getCumulativeSpineItemSize(currentSpineIndex) - + (currentSpineIndex > 0 ? epub->getCumulativeSpineItemSize(currentSpineIndex - 1) : 0); + const bool willInflate = !section->hasHtmlCache(); + const bool anchorJump = !pendingAnchor.empty(); + const auto anchorPageReady = [&]() { + const auto page = section->findAnchor(pendingAnchor); + return page.has_value() && + (static_cast(*page) < static_cast(section->pageCount) || section->isBuildComplete()); + }; + const bool deferPartialBuild = + section->isPartial() && + (anchorJump ? anchorPageReady() + : target + PARTIAL_REBUILD_START_MARGIN < static_cast(section->pageCount)); + if (deferPartialBuild) { + LOG_DBG("ERS", "Partial covers target %d of %d; deferring extension build", target, section->pageCount); + buildSucceeded = true; + } else { + bool showPopup = false; + if (anchorJump) { + showPopup = !anchorPageReady() && spineBytes > BUILD_POPUP_BYTE_THRESHOLD; + } else { + const bool targetAvailable = target < static_cast(section->pageCount); + showPopup = !targetAvailable && ((spineBytes > BUILD_POPUP_BYTE_THRESHOLD && willInflate) || + target > BUILD_POPUP_PAGE_THRESHOLD); + } + if (showPopup) { + showIndexingPopup(); + } + buildPopupPending = !showPopup; + const unsigned long buildStartMs = millis(); + bool started; + { + GfxRenderer::FrameBufferLoan loan(renderer); + started = section->startBuild(spec, buildOptions, [this] { showBuildPopup(); }); + } + if (started) { + bool buildFailed = false; + while (!section->isBuildComplete() && + (anchorJump ? !anchorPageReady() + : pendingRelayoutReposition ? !isRelayoutCatchUpComplete() + : static_cast(section->pageCount) <= target)) { + if (cancelBuildForBack()) { + break; + } + if (buildPopupPending && millis() - buildStartMs >= BUILD_POPUP_DEADLINE_MS) { + showBuildPopup(); + } + if (!section->buildSomeMore(INTERACTIVE_BUILD_PAGES_PER_CHUNK)) { + LOG_ERR("ERS", "Failed during incremental section build"); + buildFailed = true; + break; + } + } + if (!buildFailed && pendingRelayoutReposition && section->isBuilding() && isRelayoutCatchUpComplete()) { + LOG_DBG("ERS", "Incremental relayout reached prior watermark: pages=%u target=%d", section->pageCount, + cachedChapterPageWatermark); + } + attemptImagesWereSuppressed = attemptImagesWereSuppressed || section->lastBuildImagesWereSuppressed(); + attemptLayoutAbortedForLowMemory = + attemptLayoutAbortedForLowMemory || section->lastBuildLayoutAbortedForLowMemory(); + const bool requestedPageAvailable = anchorJump ? anchorPageReady() + : pendingRelayoutReposition + ? isRelayoutCatchUpComplete() + : target >= 0 && target < static_cast(section->pageCount); + if (buildCancelledForBack) { + buildFailed = false; + } + if (buildFailed && attemptLayoutAbortedForLowMemory && requestedPageAvailable) { + LOG_ERR("ERS", "Incremental section build paused for low heap after reaching requested page"); + attemptLayoutAbortedForLowMemory = false; + buildFailed = false; + } + buildSucceeded = + buildCancelledForBack || (!buildFailed && (section->pageCount > 0 || section->isBuildComplete())); + } else { + attemptImagesWereSuppressed = attemptImagesWereSuppressed || section->lastBuildImagesWereSuppressed(); + attemptLayoutAbortedForLowMemory = + attemptLayoutAbortedForLowMemory || section->lastBuildLayoutAbortedForLowMemory(); + } + buildPopupPending = false; + } + } + imagesWereSuppressed = imagesWereSuppressed || attemptImagesWereSuppressed; + layoutAbortedForLowMemory = attemptLayoutAbortedForLowMemory; + if (buildSucceeded) { + activeSectionFontId = fontId; + activeSectionLayoutSignature = readerRenderSpecSignature(spec); + usedRenderMode = profile.renderMode; + safeModeBuildSucceeded = profile.safeMode; + LOG_DBG("ERS", + "%s section cache built: spine=%d font=%d mode=%u embedded=%u bionic=%u guide=%u pages=%u free=%u " + "maxAlloc=%u building=%u", + profile.label, currentSpineIndex, fontId, static_cast(profile.renderMode), + static_cast(profile.embeddedStyle), static_cast(profile.bionicReadingEnabled), static_cast(profile.guideReadingEnabled), section->pageCount, ESP.getFreeHeap(), - ESP.getMaxAllocHeap()); + ESP.getMaxAllocHeap(), section->isBuilding() ? 1U : 0U); } return buildSucceeded; }; - uint8_t fallbackCount = 0; - const auto fallbackModes = fallbackModesForSelection(selectedRenderMode, fallbackCount); - for (uint8_t i = 0; i < fallbackCount && !fallbackBuildSucceeded; ++i) { - const EpubRenderMode attemptMode = fallbackModes[i]; - if (i > 0) { - if (!layoutAbortedForLowMemory) { - break; - } - LOG_ERR("ERS", "EPUB section layout aborted for low heap; retrying mode %u", - static_cast(attemptMode)); - releaseReaderSdFontCachesForLowMemory(renderer, "ERS", "fallback section rebuild"); - } + auto buildWithFallback = [&](const SectionBuildProfile& profile) { layoutAbortedForLowMemory = false; - fallbackBuildSucceeded = buildSectionWithProfile(readerFontId, buildProfileForRenderMode(attemptMode)); + const bool succeeded = buildSectionWithProfile(readerFontId, profile); + return SectionBuildAttempt{succeeded, layoutAbortedForLowMemory}; + }; + auto beforeFallbackRetry = [&](const SectionBuildProfile& profile) { + if (profile.safeMode) { + LOG_ERR("ERS", "EPUB section layout aborted for low heap; retrying Safe Mode"); + releaseReaderSdFontCachesForLowMemory(renderer, "ERS", "safe mode section rebuild"); + return; + } + LOG_ERR("ERS", "EPUB section layout aborted for low heap; retrying mode %u", + static_cast(profile.renderMode)); + releaseReaderSdFontCachesForLowMemory(renderer, "ERS", "fallback section rebuild"); + }; + const SectionBuildAttempt initialAttempt = buildWithFallback(buildProfileForRenderMode(selectedRenderMode)); + const bool canRestartInitialLayout = !buildingFootnotePreview && !pendingPercentJump && pendingAnchor.empty() && + pendingClippingIndex == UINT16_MAX && pendingParagraphIndex == UINT16_MAX; + if (!initialAttempt.succeeded && initialAttempt.lowMemory && canRestartInitialLayout) { + const int target = pendingPageJump.has_value() ? *pendingPageJump : std::max(0, nextPageNumber); + const int savedPage = + section && section->pageCount > 0 ? std::min(target, static_cast(section->pageCount) - 1) : target; + const int estimatedPages = section ? section->estimatedTotalPages() : 0; + if (restartForLowMemoryLayout(target, savedPage, estimatedPages, "initial EPUB layout")) { + return; + } } + const SectionFallbackResult fallbackResult = runSectionBuildFallbacks( + selectedRenderMode, shouldAttemptSafeModeFallback(), buildWithFallback, beforeFallbackRetry, &initialAttempt); + fallbackBuildSucceeded = fallbackResult.succeeded; + layoutAbortedForLowMemory = fallbackResult.lastAttemptLowMemory; - if (!fallbackBuildSucceeded && layoutAbortedForLowMemory && shouldAttemptSafeModeFallback()) { - LOG_ERR("ERS", "EPUB section layout aborted for low heap; retrying Safe Mode"); - releaseReaderSdFontCachesForLowMemory(renderer, "ERS", "safe mode section rebuild"); - layoutAbortedForLowMemory = false; - fallbackBuildSucceeded = buildSectionWithProfile(readerFontId, safeModeBuildProfile()); + if (buildCancelledForBack) { + return; + } + + if (!fallbackBuildSucceeded && readablePartialFallback && !buildingFootnotePreview && !pendingPercentJump && + pendingClippingIndex == UINT16_MAX && pendingParagraphIndex == UINT16_MAX && !pendingRelayoutReposition) { + const int target = pendingPageJump.has_value() ? *pendingPageJump : std::max(0, nextPageNumber); + bool targetAvailable = target < static_cast(readablePartialFallback->pageCount); + if (!pendingAnchor.empty()) { + const auto anchorPage = readablePartialFallback->findAnchor(pendingAnchor); + targetAvailable = anchorPage.has_value() && *anchorPage < readablePartialFallback->pageCount; + } + if (targetAvailable) { + LOG_ERR("ERS", "Full-section build failed; retaining readable partial cache (%u pages)", + readablePartialFallback->pageCount); + section.reset(); + section = std::move(readablePartialFallback); + fallbackBuildSucceeded = true; + usedReadablePartialFallback = true; + layoutAbortedForLowMemory = false; + } + } + + if (!fallbackBuildSucceeded && layoutAbortedForLowMemory && section && section->isPartial() && + section->pageCount > 0 && !buildingFootnotePreview && !pendingPercentJump && pendingAnchor.empty() && + pendingClippingIndex == UINT16_MAX && pendingParagraphIndex == UINT16_MAX && !pendingRelayoutReposition) { + LOG_ERR("ERS", "Incremental build stopped for low heap; retaining readable partial cache (%u pages)", + section->pageCount); + fallbackBuildSucceeded = true; + usedReadablePartialFallback = true; + partialRebuildAbortedForLowMemory = true; + activeSectionFontId = readerFontId; + usedRenderMode = lastAttemptedRenderMode; + safeModeBuildSucceeded = lastAttemptUsedSafeMode; + queueLowMemoryLayoutAlert(false); } if (!fallbackBuildSucceeded) { @@ -3913,13 +4988,20 @@ void EpubReaderActivity::render(RenderLock&& lock) { if (layoutAbortedForLowMemory) { showLowMemoryLayoutError(); } else { - showPendingSyncSaveError(); + showBuildError(); } return; } - releaseReaderSdFontCachesForLowMemory(renderer, "ERS", "section cache build"); - LOG_DBG("ERS", "Cache build complete: pages=%u font=%d mode=%u free=%u maxAlloc=%u", section->pageCount, - activeSectionFontId, static_cast(usedRenderMode), ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + if (!section->isBuilding()) { + releaseReaderSdFontCachesForLowMemory(renderer, "ERS", "section cache build"); + } + if (usedReadablePartialFallback) { + LOG_DBG("ERS", "Continuing from readable partial cache after failed build: pages=%u free=%u maxAlloc=%u", + section->pageCount, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + } else { + LOG_DBG("ERS", "Cache build complete: pages=%u font=%d mode=%u free=%u maxAlloc=%u", section->pageCount, + activeSectionFontId, static_cast(usedRenderMode), ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + } if (!buildingFootnotePreview && safeModeBuildSucceeded) { applySafeModeReaderSettings(); @@ -3966,18 +5048,19 @@ void EpubReaderActivity::render(RenderLock&& lock) { if (pendingPageJump.has_value()) { section->currentPage = *pendingPageJump; - if (pendingClippingIndex != UINT16_MAX && pendingClippingIndex < CLIPPINGS.getClippings().size()) { - const Clipping& clipping = CLIPPINGS.getClippings()[pendingClippingIndex]; + if (pendingClippingIndex != UINT16_MAX && pendingClippingIndex < CLIPPINGS.clippingCount()) { + const Clipping* clipping = CLIPPINGS.clippingAt(pendingClippingIndex); const uint16_t fallbackPage = static_cast(std::max(0, section->currentPage)); - section->currentPage = resolveClippingJumpPage(*section, clipping, fallbackPage); + std::string clippingText; + clippingText.reserve(CLIPPING_TEXT_MAX); + if (clipping) CLIPPINGS.readClippingText(*clipping, clippingText); + section->currentPage = + clipping ? resolveClippingJumpPage(*section, *clipping, clippingText, fallbackPage) : fallbackPage; LOG_DBG("ERS", "Resolved clipping %u to page %d", pendingClippingIndex, section->currentPage); } else if (pendingParagraphIndex != UINT16_MAX) { - if (const auto paragraphPage = section->getPageForParagraphIndex(pendingParagraphIndex)) { - section->currentPage = *paragraphPage; - LOG_DBG("ERS", "Resolved paragraph %u to page %u", pendingParagraphIndex, *paragraphPage); - } else { - LOG_DBG("ERS", "Paragraph %u not found; using saved section page", pendingParagraphIndex); - } + const uint16_t fallbackPage = static_cast(std::max(0, section->currentPage)); + section->currentPage = resolveParagraphJumpPage(*section, pendingParagraphIndex, fallbackPage); + LOG_DBG("ERS", "Resolved paragraph %u to page %d", pendingParagraphIndex, section->currentPage); } pendingClippingIndex = UINT16_MAX; pendingParagraphIndex = UINT16_MAX; @@ -3990,8 +5073,8 @@ void EpubReaderActivity::render(RenderLock&& lock) { } if (!pendingAnchor.empty()) { - // Resolve from the finalized on-disk anchor map. - const auto page = section->getPageForAnchor(pendingAnchor); + // Resolve from the pages laid out so far and/or the on-disk map (finalized or partial). + const auto page = section->findAnchor(pendingAnchor); if (page && static_cast(*page) < static_cast(section->pageCount)) { section->currentPage = *page; LOG_DBG("ERS", "Resolved anchor '%s' to page %d", pendingAnchor.c_str(), *page); @@ -4012,35 +5095,143 @@ void EpubReaderActivity::render(RenderLock&& lock) { newPage = section->pageCount - 1; } section->currentPage = newPage; - if (pendingClippingIndex != UINT16_MAX && pendingClippingIndex < CLIPPINGS.getClippings().size()) { - const Clipping& clipping = CLIPPINGS.getClippings()[pendingClippingIndex]; + if (pendingClippingIndex != UINT16_MAX && pendingClippingIndex < CLIPPINGS.clippingCount()) { + const Clipping* clipping = CLIPPINGS.clippingAt(pendingClippingIndex); const uint16_t fallbackPage = static_cast(std::max(0, section->currentPage)); - section->currentPage = resolveClippingJumpPage(*section, clipping, fallbackPage); + std::string clippingText; + clippingText.reserve(CLIPPING_TEXT_MAX); + if (clipping) CLIPPINGS.readClippingText(*clipping, clippingText); + section->currentPage = + clipping ? resolveClippingJumpPage(*section, *clipping, clippingText, fallbackPage) : fallbackPage; LOG_DBG("ERS", "Resolved clipping %u to page %d", pendingClippingIndex, section->currentPage); } else if (pendingParagraphIndex != UINT16_MAX) { - if (const auto paragraphPage = section->getPageForParagraphIndex(pendingParagraphIndex)) { - section->currentPage = *paragraphPage; - LOG_DBG("ERS", "Resolved paragraph %u to page %u", pendingParagraphIndex, *paragraphPage); - } else { - LOG_DBG("ERS", "Paragraph %u not found; using saved chapter progress", pendingParagraphIndex); - } + const uint16_t fallbackPage = static_cast(std::max(0, section->currentPage)); + section->currentPage = resolveParagraphJumpPage(*section, pendingParagraphIndex, fallbackPage); + LOG_DBG("ERS", "Resolved paragraph %u to page %d", pendingParagraphIndex, section->currentPage); } pendingClippingIndex = UINT16_MAX; pendingParagraphIndex = UINT16_MAX; pendingPercentJump = false; } + // Keep negative page numbers in bounds now. Upper-bound clamping waits until after + // lazy build catch-up because pageCount may still be only a partial-build watermark. if (section->currentPage < 0) { section->currentPage = 0; } } - if (section->pageCount > 0 && section->currentPage >= static_cast(section->pageCount)) { + // Extend the build to the requested page if needed. This covers a partial cache that is + // already loaded but not actively building; pages already available do no work here. + const int requestedPageBeforeCatchUp = section->currentPage; + if (!activeFootnotePreview && partialRebuildAbortedForLowMemory && section->pageCount > 0 && + section->currentPage >= static_cast(section->pageCount)) { + const bool shouldSilentRestartForPartialLowMemory = + !lowMemoryPartialRestartAttempted && section->lastBuildLayoutAbortedForLowMemory() && + section->currentPage == static_cast(section->pageCount) && + section->pageCount < std::numeric_limits::max() && currentSpineIndex >= 0 && + currentSpineIndex <= std::numeric_limits::max(); + if (shouldSilentRestartForPartialLowMemory) { + const uint16_t lastReadablePage = section->pageCount - 1; + const uint16_t targetPage = section->pageCount; + const int estimatedPages = section->estimatedTotalPages(); + if (restartForLowMemoryLayout(targetPage, lastReadablePage, estimatedPages, "partial EPUB layout")) { + return; + } + } + LOG_ERR("ERS", "Requested page %d exceeds low-memory partial watermark %u; showing last readable page", + section->currentPage, section->pageCount); + section->currentPage = section->pageCount - 1; + } + if (!activeFootnotePreview && section->isPartial() && section->currentPage >= static_cast(section->pageCount)) { + showIndexingPopup(); + } + const bool needsBlockingCatchUp = section->currentPage >= static_cast(section->pageCount) && + ((!activeFootnotePreview && section->isPartial()) || section->isBuilding()); + bool catchUpCancelled = false; + bool catchUpFailed = false; + const auto runBlockingCatchUp = [&]() { + while (!activeFootnotePreview && section->isPartial() && + section->currentPage >= static_cast(section->pageCount)) { + if (!section->isBuilding()) { + const int renderFontId = activeSectionFontId != 0 ? activeSectionFontId : SETTINGS.getReaderFontId(); + const SectionBuildProfile profile = buildProfileForRenderMode(normalizeRenderMode(SETTINGS.epubRenderMode)); + if (!section->startBuild(readerRenderSpecForProfile(renderFontId, viewportWidth, viewportHeight, profile))) { + LOG_ERR("ERS", "Failed to start partial extension build"); + catchUpFailed = true; + return; + } + } + while (!section->isBuildComplete() && section->currentPage >= static_cast(section->pageCount)) { + if (cancelBuildForBack()) { + catchUpCancelled = true; + return; + } + if (!section->buildSomeMore(INTERACTIVE_BUILD_PAGES_PER_CHUNK)) { + LOG_ERR("ERS", "Failed during incremental section build"); + catchUpFailed = true; + return; + } + } + } + + if (section->isBuilding()) { + while (!section->isBuildComplete() && section->currentPage >= static_cast(section->pageCount)) { + if (cancelBuildForBack()) { + catchUpCancelled = true; + return; + } + if (!section->buildSomeMore(INTERACTIVE_BUILD_PAGES_PER_CHUNK)) { + LOG_ERR("ERS", "Failed during incremental section build"); + catchUpFailed = true; + return; + } + } + } + }; + + if (needsBlockingCatchUp) { + // The panel keeps its current image, and the page below is redrawn from + // scratch. Let miniz use the framebuffer while this catch-up blocks. + releaseGrayscaleStripScratch(); + GfxRenderer::FrameBufferLoan loan(renderer); + runBlockingCatchUp(); + } + if (catchUpCancelled) { + return; + } + if (catchUpFailed) { + if (section->lastBuildLayoutAbortedForLowMemory() && section->pageCount > 0) { + partialRebuildAbortedForLowMemory = true; + const int partialWatermark = static_cast(section->pageCount); + if (requestedPageBeforeCatchUp == partialWatermark && + restartForLowMemoryLayout(partialWatermark, partialWatermark - 1, section->estimatedTotalPages(), + "partial EPUB catch-up")) { + return; + } + section->currentPage = std::min(section->currentPage, static_cast(section->pageCount) - 1); + LOG_ERR("ERS", "Blocking build stopped for low heap; retaining %u readable partial pages", section->pageCount); + queueLowMemoryLayoutAlert(false); + } else { + section.reset(); + showBuildError(); + return; + } + } + + // The requested page is now as built as it will get. Clamp any past-the-end target + // against the final count, including the UINT16_MAX "last page" sentinel. + if (!section->isBuilding() && section->pageCount > 0 && + section->currentPage >= static_cast(section->pageCount)) { section->currentPage = section->pageCount - 1; } applyDeferredReposition(); + if (!activeFootnotePreview) { + refreshChapterGroupEstimate(viewportWidth, viewportHeight); + } + renderer.clearScreen(ReaderUtils::readerBackgroundColor()); if (section->pageCount == 0) { @@ -4066,11 +5257,14 @@ void EpubReaderActivity::render(RenderLock&& lock) { } { - auto p = section->loadPageFromSectionFile(); + // Unified page read: the in-progress build's in-RAM table if it has reached the page, + // otherwise the on-disk file (finalized section, or a partial from a previous session). + auto p = section->loadPage(section->currentPage); if (!p) { pageLoadRetryCount++; - if (pageLoadRetryCount < MAX_PAGE_LOAD_RETRIES) { + if (pageLoadRetryCount <= MAX_PAGE_LOAD_RETRIES) { LOG_ERR("ERS", "Failed to load page from SD (retry %d) - clearing section cache", pageLoadRetryCount); + section->abandonBuild(); section->clearCache(); section.reset(); requestUpdate(); @@ -4080,6 +5274,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { } LOG_ERR("ERS", "Failed to load page from SD after %d retries", pageLoadRetryCount); + pageLoadRetryCount = 0; renderer.clearScreen(ReaderUtils::readerBackgroundColor()); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_PAGE_LOAD_ERROR), ReaderUtils::readerForegroundBlack(), EpdFontFamily::BOLD); @@ -4089,7 +5284,6 @@ void EpubReaderActivity::render(RenderLock&& lock) { showPendingSyncSaveError(); return; } - pageLoadRetryCount = 0; // Preview pages are transient note windows, not full chapter pages with reusable footnote metadata. @@ -4099,17 +5293,32 @@ void EpubReaderActivity::render(RenderLock&& lock) { currentPageFootnotes = std::move(p->footnotes); } - const auto start = millis(); const int renderFontId = activeSectionFontId != 0 ? activeSectionFontId : SETTINGS.getReaderFontId(); renderContents(std::move(p), renderFontId, layout.marginTop, layout.marginRight, layout.marginBottom, - layout.marginLeft); + layout.marginLeft, /*updatePanel=*/true); + lastRenderCompleteMs = millis(); + const uint8_t heapShapeRedrawStages = pendingHeapShapeReaderRedrawStages.exchange(0, std::memory_order_relaxed); + if (heapShapeRedrawStages & HEAP_SHAPE_REDRAW_CLIP) { + MemoryBudget::logHeapShape("clip.reader_redrawn"); + } + if (heapShapeRedrawStages & HEAP_SHAPE_REDRAW_DICT) { + MemoryBudget::logHeapShape("dict.reader_redrawn"); + } pageShownAtMs = activeFootnotePreview ? 0UL : millis(); - LOG_DBG("ERS", "Rendered page in %dms", millis() - start); } if (!activeFootnotePreview) { - if (!saveProgress(currentSpineIndex, section->currentPage, section->pageCount)) { - pendingSyncSaveError = true; + const int totalPages = section->estimatedTotalPages(); + // render() also runs on menu/bookmark/screenshot re-renders. Avoid repeating + // the same progress.bin write unless the rendered position or estimated total changed. + if (progressSaveRequiredAfterRelayout || currentSpineIndex != lastSavedSpineIndex || + section->currentPage != lastSavedPage || totalPages != lastSavedPageCount) { + if (!queueProgressSave(currentSpineIndex, section->currentPage, totalPages, progressSaveRequiredAfterRelayout)) { + pendingSyncSaveError = true; + } else if (progressSaveRequiredAfterRelayout) { + progressSaveRequiredAfterRelayout = false; + } } + silentIndexNextChapterIfNeeded(viewportWidth, viewportHeight); queueCompletionPromptIfNeeded(); } @@ -4121,15 +5330,211 @@ void EpubReaderActivity::render(RenderLock&& lock) { } } +void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportWidth, const uint16_t viewportHeight) { + if (SETTINGS.indexingMethod != CrossPointSettings::INDEXING_FULL_SECTION || activeFootnotePreview || !epub || + !section || section->isBuilding() || section->isPartial() || section->pageCount == 0) { + return; + } + + // Start on the penultimate page, including one-page chapters, and catch up + // after a direct jump to the last page. + const int triggerPage = section->pageCount > 1 ? section->pageCount - 2 : 0; + if (section->currentPage < triggerPage) { + return; + } + + const int nextSpineIndex = currentSpineIndex + 1; + if (nextSpineIndex < 0 || nextSpineIndex >= epub->getSpineItemsCount()) { + return; + } + if (preparedNextSpineIndex == nextSpineIndex && preparedNextViewportWidth == viewportWidth && + preparedNextViewportHeight == viewportHeight) { + return; + } + + const int readerFontId = SETTINGS.getReaderFontId(); + const EpubRenderMode selectedRenderMode = normalizeRenderMode(SETTINGS.epubRenderMode); + auto nextSection = + makeUniqueNoThrow
    (epub, nextSpineIndex, renderer, sectionCacheSuffixForRenderMode(selectedRenderMode)); + if (!nextSection) { + LOG_ERR("ERS", "Failed to allocate next-chapter cache reader for spine %d", nextSpineIndex); + return; + } + + if (nextSection->loadSectionFile(readerRenderSpecForProfile(readerFontId, viewportWidth, viewportHeight, + buildProfileForRenderMode(selectedRenderMode))) && + !nextSection->isPartial()) { + preparedNextSpineIndex = nextSpineIndex; + preparedNextViewportWidth = viewportWidth; + preparedNextViewportHeight = viewportHeight; + return; + } + // Close any loaded partial before rebuilding the same cache path. Real SdFat + // hardware permits only one reader for a file path at a time. + nextSection.reset(); + + releaseGrayscaleStripScratch(); + const bool releasedSdFontCaches = + releaseReaderSdFontCachesForLowMemory(renderer, "ERS", "preparing silent next-chapter indexing"); + const uint32_t minFreeForPrefetch = releasedSdFontCaches + ? MemoryBudget::OPTIONAL_EPUB_PREFETCH_AFTER_SD_FONT_RELEASE_MIN_FREE + : MemoryBudget::OPTIONAL_EPUB_REBUILD_MIN_FREE; + if (!MemoryBudget::hasHeapForOptionalEpubRebuild("ERS", "silent next-chapter indexing", nextSpineIndex, + minFreeForPrefetch, + MemoryBudget::OPTIONAL_EPUB_REBUILD_MIN_MAX_ALLOC)) { + return; + } + + LOG_DBG("ERS", "Silently indexing next chapter: %d (free=%u, maxAlloc=%u)", nextSpineIndex, ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + silentPrefetchCancelRequested.store(false, std::memory_order_relaxed); + silentPrefetchBuildActive.store(true, std::memory_order_release); + struct ClearSilentPrefetchBuildActive { + std::atomic& active; + ~ClearSilentPrefetchBuildActive() { active.store(false, std::memory_order_release); } + } clearSilentPrefetchBuildActive{silentPrefetchBuildActive}; + + bool layoutAbortedForLowMemory = false; + bool buildSucceeded = false; + bool safeModeBuildSucceeded = false; + bool prefetchCancelled = false; + EpubRenderMode usedRenderMode = selectedRenderMode; + + const auto buildNextSection = [&](const SectionBuildProfile& profile) { + auto attemptSection = + makeUniqueNoThrow
    (epub, nextSpineIndex, renderer, sectionCacheSuffixForRenderMode(profile.renderMode)); + if (!attemptSection) { + LOG_ERR("ERS", "Failed to allocate next-chapter section builder for spine %d", nextSpineIndex); + layoutAbortedForLowMemory = true; + return false; + } + + bool attemptAbortedForLowMemory = false; + bool attemptCancelled = false; + SectionBuildOptions buildOptions; + buildOptions.shouldCancel = [](void* context) { + return static_cast(context)->silentPrefetchCancelRequested.load(std::memory_order_relaxed); + }; + buildOptions.cancelContext = this; + buildOptions.cancellationObserved = &attemptCancelled; + // The page is already on the panel, so lend its framebuffer to miniz while + // preparing the next chapter. Without this, a large EPUB entry makes the + // inflater take its workspace from the same constrained heap as layout. + GfxRenderer::FrameBufferLoan loan(renderer); + const bool succeeded = attemptSection->createSectionFile( + readerRenderSpecForProfile(readerFontId, viewportWidth, viewportHeight, profile), nullptr, nullptr, + &attemptAbortedForLowMemory, buildOptions); + if (attemptCancelled) { + prefetchCancelled = true; + LOG_DBG("ERS", "Silent next-chapter indexing cancelled: chapter=%d", nextSpineIndex); + return false; + } + layoutAbortedForLowMemory = attemptAbortedForLowMemory; + if (succeeded) { + usedRenderMode = profile.renderMode; + LOG_DBG("ERS", "Silent indexing complete: chapter=%d pages=%u mode=%u free=%u maxAlloc=%u", nextSpineIndex, + attemptSection->pageCount, static_cast(usedRenderMode), ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + } + return succeeded; + }; + + auto buildWithFallback = [&](const SectionBuildProfile& profile) { + layoutAbortedForLowMemory = false; + const bool succeeded = buildNextSection(profile); + return SectionBuildAttempt{succeeded, layoutAbortedForLowMemory}; + }; + auto beforeFallbackRetry = [&](const SectionBuildProfile& profile) { + releaseReaderSdFontCachesForLowMemory( + renderer, "ERS", + profile.safeMode ? "silent next-chapter safe mode indexing" : "silent next-chapter fallback indexing"); + }; + SectionFallbackResult fallbackResult; + const SectionBuildAttempt initialAttempt = buildWithFallback(buildProfileForRenderMode(selectedRenderMode)); + if (!prefetchCancelled) { + fallbackResult = runSectionBuildFallbacks(selectedRenderMode, shouldAttemptSafeModeFallback(), buildWithFallback, + beforeFallbackRetry, &initialAttempt); + buildSucceeded = fallbackResult.succeeded; + layoutAbortedForLowMemory = fallbackResult.lastAttemptLowMemory; + safeModeBuildSucceeded = fallbackResult.usedSafeMode; + } + + releaseReaderSdFontCachesForLowMemory(renderer, "ERS", "silent next-chapter indexing"); + + if (prefetchCancelled) { + return; + } + + if (!buildSucceeded) { + LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex); + return; + } + + // A grouped sibling just changed from missing/partial to exact. Re-scan on the + // next reader render so the tilde can disappear without doing file I/O in the + // status-bar drawing function itself. + chapterGroupEstimate.valid = false; + + if (safeModeBuildSucceeded) { + applySafeModeReaderSettings(); + bookHasCustomReaderSettings = true; + bookHasRenderModeOverride = true; + if (!saveRuntimeReaderSettingsForCache(epub->getCachePath())) { + LOG_ERR("ERS", "Failed to save Safe Mode reader settings after silent indexing"); + } + } else if (usedRenderMode != selectedRenderMode) { + SETTINGS.epubRenderMode = static_cast(usedRenderMode); + bookHasRenderModeOverride = true; + if (!saveBookRenderModeForCache(epub->getCachePath(), SETTINGS.epubRenderMode)) { + LOG_ERR("ERS", "Failed to save render mode after silent indexing"); + } + } + preparedNextSpineIndex = nextSpineIndex; + preparedNextViewportWidth = viewportWidth; + preparedNextViewportHeight = viewportHeight; +} + +bool EpubReaderActivity::isRelayoutCatchUpComplete() const { + if (!pendingRelayoutReposition || !section) { + return !pendingRelayoutReposition; + } + + const bool watermarkReached = static_cast(section->pageCount) >= std::max(1, cachedChapterPageWatermark); + bool positionResolved = static_cast(section->pageCount) > cachedChapterPageNumber; + if (cachedVisibleTextOffset) { + positionResolved = section->getPageForVisibleTextOffset(*cachedVisibleTextOffset).has_value(); + } else if (cachedPageParagraphIndex != UINT16_MAX) { + positionResolved = section->isBuilding() ? section->findParagraphDuringBuild(cachedPageParagraphIndex).has_value() + : section->getPageForParagraphIndex(cachedPageParagraphIndex).has_value(); + } + return watermarkReached && positionResolved; +} + bool EpubReaderActivity::applyDeferredReposition() { - if (cachedChapterTotalPageCount == 0 || !section) { + if ((!cachedVisibleTextOffset && cachedChapterTotalPageCount == 0) || !section) { return false; } + if (section->isBuilding() && !isRelayoutCatchUpComplete()) { + return false; + } + + const bool completedRelayout = pendingRelayoutReposition; bool changed = false; if (currentSpineIndex == cachedSpineIndex) { + bool restoredFromContent = false; + if (cachedVisibleTextOffset) { + if (const auto contentPage = section->getPageForVisibleTextOffset(*cachedVisibleTextOffset, true)) { + section->currentPage = *contentPage; + restoredFromContent = true; + changed = true; + LOG_DBG("ERS", "Resolved visible text offset %lu to page %d", + static_cast(*cachedVisibleTextOffset), section->currentPage); + } + } + bool restoredFromParagraph = false; - if (cachedPageParagraphIndex != UINT16_MAX) { + if (!restoredFromContent && cachedPageParagraphIndex != UINT16_MAX) { if (const auto paragraphPage = section->getPageForParagraphIndex(cachedPageParagraphIndex)) { uint16_t newStartPage = *paragraphPage; if (newStartPage >= section->pageCount) { @@ -4162,7 +5567,8 @@ bool EpubReaderActivity::applyDeferredReposition() { } } - if (!restoredFromParagraph && section->pageCount != cachedChapterTotalPageCount) { + if (!restoredFromContent && !restoredFromParagraph && !section->isBuilding() && + section->pageCount != cachedChapterTotalPageCount) { const float progress = static_cast(cachedChapterPageNumber) / static_cast(cachedChapterTotalPageCount); int newPage = static_cast(progress * static_cast(section->pageCount)); @@ -4179,10 +5585,15 @@ bool EpubReaderActivity::applyDeferredReposition() { cachedChapterPageNumber = 0; cachedChapterTotalPageCount = 0; + cachedChapterPageWatermark = 0; + cachedVisibleTextOffset.reset(); pendingRelayoutReposition = false; cachedPageParagraphIndex = UINT16_MAX; cachedPageParagraphOffset = 0; cachedPageParagraphSpan = 0; + if (completedRelayout) { + progressSaveRequiredAfterRelayout = true; + } return changed; } @@ -4190,7 +5601,84 @@ bool EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC if (activeFootnotePreview) { return true; } - return EpubReaderUtils::saveProgress(*epub, spineIndex, currentPage, pageCount); + if (!epub) { + return false; + } + std::optional visibleTextOffset; + if (section && spineIndex == currentSpineIndex && currentPage >= 0 && currentPage < section->pageCount) { + visibleTextOffset = section->getVisibleTextOffsetForPage(static_cast(currentPage)); + } + if (section && section->isBuilding() && spineIndex == currentSpineIndex) { + // Free lazy-build cache files before opening progress.bin; X4/SdFat can reject + // another file open while a still-building EPUB section keeps files open. + section->releaseBuildFile(); + } + const bool saved = EpubReaderUtils::saveProgress(*epub, spineIndex, currentPage, pageCount, visibleTextOffset); + if (saved) { + lastSavedSpineIndex = spineIndex; + lastSavedPage = currentPage; + lastSavedPageCount = pageCount; + // Home reads this lightweight value instead of opening the EPUB, so keep it + // in sync with the position file written above. + RecentBookProgress::saveCachedEpubPercent(*epub, spineIndex, currentPage, pageCount); + const uint32_t positionKey = (static_cast(spineIndex) << 16) | static_cast(currentPage); + progressSaveDebouncer.markPersisted(positionKey, static_cast(pageCount)); + } + return saved; +} + +bool EpubReaderActivity::queueProgressSave(const int spineIndex, const int currentPage, const int pageCount, + const bool forceSave) { + if (activeFootnotePreview) { + return true; + } + const uint32_t positionKey = (static_cast(spineIndex) << 16) | static_cast(currentPage); + if (!progressSaveDebouncer.observe(positionKey, static_cast(pageCount)) && !forceSave) { + return true; + } + return saveProgress(spineIndex, currentPage, pageCount); +} + +bool EpubReaderActivity::flushQueuedProgress() { + if (!progressSaveDebouncer.hasPending()) { + return true; + } + if (!epub || !section) { + return false; + } + const uint32_t positionKey = progressSaveDebouncer.lastObservedPosition(); + const int spineIndex = static_cast(positionKey >> 16); + const int pageNumber = static_cast(positionKey & 0xFFFFU); + const int pageCount = static_cast(progressSaveDebouncer.lastObservedMetadata()); + return saveProgress(spineIndex, pageNumber, pageCount); +} + +bool EpubReaderActivity::ensureGrayscaleStripScratch() { + if (!renderer.supportsStripGrayscale()) { + return false; + } + + const size_t requiredSize = static_cast(renderer.getDisplayWidthBytes()) * GRAYSCALE_STRIP_ROWS; + if (grayscaleStripScratch && grayscaleStripScratchSize >= requiredSize) { + return true; + } + + releaseGrayscaleStripScratch(); + // About 8 KB at 800-pixel width: too large for the render task stack and + // runtime-sized, so allocate fallibly once and reuse it while the section is stable. + grayscaleStripScratch = makeUniqueNoThrow(requiredSize); + if (!grayscaleStripScratch) { + LOG_ERR("ERS", "OOM: grayscale strip scratch (%u bytes); falling back to BW snapshot", + static_cast(requiredSize)); + return false; + } + grayscaleStripScratchSize = requiredSize; + return true; +} + +void EpubReaderActivity::releaseGrayscaleStripScratch() { + grayscaleStripScratch.reset(); + grayscaleStripScratchSize = 0; } void EpubReaderActivity::cacheCurrentSectionPosition() { @@ -4199,7 +5687,9 @@ void EpubReaderActivity::cacheCurrentSectionPosition() { } cachedSpineIndex = currentSpineIndex; cachedChapterPageNumber = section->currentPage; - cachedChapterTotalPageCount = section->pageCount; + cachedChapterTotalPageCount = section->estimatedTotalPages(); + cachedChapterPageWatermark = section->pageCount; + cachedVisibleTextOffset.reset(); pendingRelayoutReposition = true; cachedPageParagraphIndex = UINT16_MAX; cachedPageParagraphOffset = 0; @@ -4208,6 +5698,7 @@ void EpubReaderActivity::cacheCurrentSectionPosition() { if (section->currentPage >= 0 && section->currentPage < section->pageCount) { const uint16_t currentPage = static_cast(section->currentPage); + cachedVisibleTextOffset = section->getVisibleTextOffsetForPage(currentPage); if (const auto pIdx = section->getParagraphIndexForPage(currentPage)) { cachedPageParagraphIndex = *pIdx; @@ -4235,40 +5726,47 @@ void EpubReaderActivity::cacheCurrentSectionPosition() { } } +void EpubReaderActivity::prepareCurrentSectionForRelayout() { + if (!section) return; + cacheCurrentSectionPosition(); + if (!CLIPPINGS.stampMissingLayoutSignature(activeSectionLayoutSignature)) { + LOG_ERR("CLIP", "Failed to stamp legacy clipping layout before relayout"); + } +} + void EpubReaderActivity::renderContents(std::unique_ptr page, const int fontId, const int orientedMarginTop, const int orientedMarginRight, const int orientedMarginBottom, - const int orientedMarginLeft) { - const auto t0 = millis(); - + const int orientedMarginLeft, const bool updatePanel) { // Font prewarm: scan pass accumulates text, then prewarm, then real render auto* fcm = renderer.getFontCacheManager(); - fcm->resetStats(); - const auto heapBefore = MemoryBudget::snapshot(); auto scope = fcm->createPrewarmScope(); page->renderText(renderer, fontId, orientedMarginLeft, orientedMarginTop); // scan pass scope.endScanAndPrewarm(); - const auto heapAfter = MemoryBudget::snapshot(); - fcm->logStats("prewarm"); - const auto tPrewarm = millis(); - - LOG_DBG( - "ERS", "Heap prewarm: free=%u->%u delta=%ld maxAlloc=%u->%u delta=%ld largestPct=%u->%u", heapBefore.freeHeap, - heapAfter.freeHeap, - static_cast(static_cast(heapAfter.freeHeap) - static_cast(heapBefore.freeHeap)), - heapBefore.maxAllocHeap, heapAfter.maxAllocHeap, - static_cast(static_cast(heapAfter.maxAllocHeap) - static_cast(heapBefore.maxAllocHeap)), - largestBlockPercent(heapBefore), largestBlockPercent(heapAfter)); + +#if CROSSINK_APP_CAP_TOUCH + buildFootnoteTouchTargets(*page, fontId, orientedMarginTop, orientedMarginLeft); +#endif const bool pageHasImages = page->hasImages(); + const bool pageHasImagesNeedingDecode = pageHasImages && page->hasImagesNeedingDecode(); const bool foregroundBlack = ReaderUtils::readerForegroundBlack(); const bool needsImageGrayscale = pageHasImages; const bool needsTextGrayscale = SETTINGS.textAntiAliasing && foregroundBlack; const bool needsAnyGrayscale = needsTextGrayscale || needsImageGrayscale; + const bool tiledGrayscale = needsAnyGrayscale && renderer.supportsStripGrayscale(); + const bool overlapRefresh = + tiledGrayscale && !pageHasImages && pagesUntilFullRefresh > 1 && renderer.supportsAsyncGrayscaleBase(); const int contentBottom = renderer.getScreenHeight() - orientedMarginBottom; const auto finalizeBufferComposition = [&]() { drawClippingHighlights(*page, fontId, orientedMarginTop, orientedMarginLeft); drawPublisherPageMarkers(renderer, *page, orientedMarginTop, contentBottom, foregroundBlack); +#if CROSSINK_APP_CAP_TOUCH + if (activeFootnotePreview) { + TouchHeaderBackButton::draw(renderer, TouchHeaderBackButton::headerRect(renderer, mappedInput), tr(STR_FOOTNOTES), + /*readerContext=*/true); + } +#endif }; const auto composePageBuffer = [&]() { @@ -4284,7 +5782,13 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int fo } finalizeBufferComposition(); }; - + if (updatePanel && pageHasImagesNeedingDecode) { + page->renderWithImagePlaceholders(renderer, fontId, orientedMarginLeft, orientedMarginTop, foregroundBlack); + finalizeBufferComposition(); + renderStatusBar(); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + renderer.clearScreen(ReaderUtils::readerBackgroundColor()); + } composePageBuffer(); renderStatusBar(); if (pendingBookmarkFeedback) { @@ -4315,14 +5819,9 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int fo } else if (pendingRenderModeToast) { drawRenderModeToastBuffer(labelForRenderModeToast(normalizeRenderMode(renderModeToastMode))); } - fcm->logStats("bw_render"); - const auto tBwRender = millis(); - const auto logImagePageProfile = [](const uint32_t imageBlankDisplayMs, const uint32_t imageRestoreRenderMs, - const uint32_t imageFinalDisplayMs) { - LOG_DBG("ERS", "Image page profile: blank_display=%lums restore_render=%lums final_display=%lums", - imageBlankDisplayMs, imageRestoreRenderMs, imageFinalDisplayMs); - }; - + if (!updatePanel) { + return; + } if (pageHasImages) { // Double FAST_REFRESH with selective image blanking (pablohc's technique): // HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust. @@ -4331,20 +5830,24 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int fo // Step 2: Re-render with images and display again (images appear clean) int16_t imgX, imgY, imgW, imgH; if (page->getImageBoundingBox(imgX, imgY, imgW, imgH)) { + // Blank the image before any panel update so a pending clean pass does + // not briefly show the decoded image before the final grayscale pass. renderer.fillRect(imgX + orientedMarginLeft, imgY + orientedMarginTop, imgW, imgH, false); - const auto tImageBlankDisplay = millis(); + // Image pages intentionally bypass the regular refresh cadence. Preserve + // a pending clean base before their double-FAST grayscale pipeline. + if (cleanImageBasePending) { + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + cleanImageBasePending = false; + } renderer.displayBuffer(HalDisplay::FAST_REFRESH); - const uint32_t imageBlankDisplayMs = millis() - tImageBlankDisplay; // Re-render page content to restore images into the blanked area // Status bar is not re-rendered here to avoid reading stale dynamic values (e.g. battery %) - const auto tImageRestoreRender = millis(); composePageBuffer(); - const uint32_t imageRestoreRenderMs = millis() - tImageRestoreRender; - const auto tImageFinalDisplay = millis(); - renderer.displayBuffer(HalDisplay::FAST_REFRESH); - const uint32_t imageFinalDisplayMs = millis() - tImageFinalDisplay; - logImagePageProfile(imageBlankDisplayMs, imageRestoreRenderMs, imageFinalDisplayMs); + // The restored image frame becomes the base for the grayscale image + // planes below. On X3, use the same grayscale-aware base waveform as + // text-only grayscale turns; other panels keep the FAST fallback behavior. + renderer.displayGrayscaleBase(HalDisplay::FAST_REFRESH); } else { renderer.displayBuffer(HalDisplay::HALF_REFRESH); } @@ -4363,6 +5866,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int fo renderer.displayBuffer(HalDisplay::HALF_REFRESH); renderer.preconditionGrayscale(); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); + } else if (overlapRefresh) { + ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh, /*async=*/true); } else { // Use the grayscale-aware base waveform so the first visible pass is // closer to the final anti-aliased result instead of flashing darker @@ -4373,26 +5878,17 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int fo } else { ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh); } - const auto tDisplay = millis(); - - TiledGrayscaleTimings tiledTimings; + if (needsAnyGrayscale) { + ensureGrayscaleStripScratch(); + } if (runTiledGrayscalePass(renderer, *page, fontId, orientedMarginLeft, orientedMarginTop, foregroundBlack, - needsTextGrayscale, needsImageGrayscale, tiledTimings)) { - const auto tEnd = millis(); - LOG_DBG("ERS", - "Page render (tiled): prewarm=%lums bw_render=%lums display=%lums " - "gray_lsb=%lums gray_msb=%lums gray_display=%lums cleanup=%lums total=%lums", - tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tiledTimings.grayLsb - tDisplay, - tiledTimings.grayMsb - tiledTimings.grayLsb, tiledTimings.grayDisplay - tiledTimings.grayMsb, - tiledTimings.cleanup - tiledTimings.grayDisplay, tEnd - t0); + needsTextGrayscale, needsImageGrayscale, grayscaleStripScratch.get(), + grayscaleStripScratchSize, overlapRefresh)) { return; } // Save bw buffer to reset buffer state after grayscale data sync - const auto bwStoreHeapBefore = MemoryBudget::snapshot(); const bool storedBwBuffer = needsAnyGrayscale && renderer.storeBwBuffer(); - const auto bwStoreHeapAfter = MemoryBudget::snapshot(); - const auto tBwStore = millis(); const bool canApplyGrayscale = needsAnyGrayscale && storedBwBuffer; if (needsAnyGrayscale && !storedBwBuffer) { LOG_ERR("ERS", "Skipping grayscale enhancement: failed to store BW backup"); @@ -4404,60 +5900,122 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int fo renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); composeGrayscaleBuffer(); renderer.copyGrayscaleLsbBuffers(); - const auto tGrayLsb = millis(); // Render and copy to MSB buffer renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); composeGrayscaleBuffer(); renderer.copyGrayscaleMsbBuffers(); - const auto tGrayMsb = millis(); // display grayscale part renderer.displayGrayBuffer(); - const auto tGrayDisplay = millis(); renderer.setRenderMode(GfxRenderer::BW); // restore the bw data renderer.restoreBwBuffer(); - const auto tBwRestore = millis(); - - const auto tEnd = millis(); - LOG_DBG("ERS", - "Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_store_ok=%d " - "bw_store_free=%u->%u delta=%ld bw_store_maxAlloc=%u->%u delta=%ld bw_store_largestPct=%u->%u " - "gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums", - tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, storedBwBuffer, - bwStoreHeapBefore.freeHeap, bwStoreHeapAfter.freeHeap, - static_cast(static_cast(bwStoreHeapAfter.freeHeap) - - static_cast(bwStoreHeapBefore.freeHeap)), - bwStoreHeapBefore.maxAllocHeap, bwStoreHeapAfter.maxAllocHeap, - static_cast(static_cast(bwStoreHeapAfter.maxAllocHeap) - - static_cast(bwStoreHeapBefore.maxAllocHeap)), - largestBlockPercent(bwStoreHeapBefore), largestBlockPercent(bwStoreHeapAfter), tGrayLsb - tBwStore, - tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0); } else { if (storedBwBuffer) { // Restore the BW data when we skipped grayscale entirely. renderer.restoreBwBuffer(); } - const auto tBwRestore = millis(); + } +} - const auto tEnd = millis(); - LOG_DBG("ERS", - "Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_store_ok=%d " - "bw_store_free=%u->%u delta=%ld bw_store_maxAlloc=%u->%u delta=%ld bw_store_largestPct=%u->%u " - "bw_restore=%lums total=%lums", - tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, storedBwBuffer, - bwStoreHeapBefore.freeHeap, bwStoreHeapAfter.freeHeap, - static_cast(static_cast(bwStoreHeapAfter.freeHeap) - - static_cast(bwStoreHeapBefore.freeHeap)), - bwStoreHeapBefore.maxAllocHeap, bwStoreHeapAfter.maxAllocHeap, - static_cast(static_cast(bwStoreHeapAfter.maxAllocHeap) - - static_cast(bwStoreHeapBefore.maxAllocHeap)), - largestBlockPercent(bwStoreHeapBefore), largestBlockPercent(bwStoreHeapAfter), tBwRestore - tBwStore, - tEnd - t0); +#if CROSSINK_APP_CAP_TOUCH +void EpubReaderActivity::buildFootnoteTouchTargets(const Page& page, const int fontId, const int orientedMarginTop, + const int orientedMarginLeft) { + currentPageFootnoteTouchTargets.fill({}); + if (activeFootnotePreview || currentPageFootnotes.empty()) { + return; } + + const int lineHeight = renderer.getLineHeight(fontId); + for (const auto& element : page.elements) { + if (!element || element->getTag() != TAG_PageLine) continue; + const auto& line = static_cast(*element); + if (!line.getBlock()) continue; + + const auto& block = *line.getBlock(); + for (uint16_t wordIndex = 0; wordIndex < block.wordCount(); ++wordIndex) { + const uint8_t linkId = block.wordLinkId(wordIndex); + if (linkId == 0) continue; + + const auto footnoteIt = std::find_if(currentPageFootnotes.begin(), currentPageFootnotes.end(), + [linkId](const FootnoteEntry& entry) { return entry.linkId == linkId; }); + if (footnoteIt == currentPageFootnotes.end()) continue; + const size_t footnoteIndex = static_cast(footnoteIt - currentPageFootnotes.begin()); + if (footnoteIndex >= currentPageFootnoteTouchTargets.size()) continue; + + const auto style = static_cast(block.wordStyle(wordIndex) & ~EpdFontFamily::UNDERLINE); + const int wordX = orientedMarginLeft + line.xPos + block.wordXpos(wordIndex); + int wordY = orientedMarginTop + line.yPos; + if ((style & EpdFontFamily::SUP) != 0) { + wordY -= renderer.getFontAscenderSize(fontId) * 2 / 5; + } else if ((style & EpdFontFamily::SUB) != 0) { + wordY += renderer.getFontAscenderSize(fontId) / 4; + } + int wordWidth = renderer.getTextAdvanceX(fontId, block.wordText(wordIndex), style); + if (wordIndex + 1 < block.wordCount() && block.wordXpos(wordIndex + 1) > block.wordXpos(wordIndex)) { + wordWidth = std::min(wordWidth, static_cast(block.wordXpos(wordIndex + 1) - block.wordXpos(wordIndex))); + } + if (wordWidth <= 0) continue; + + auto& target = currentPageFootnoteTouchTargets[footnoteIndex]; + if (target.width <= 0 || target.height <= 0) { + target = {static_cast(wordX), static_cast(wordY), static_cast(wordWidth), + static_cast(lineHeight)}; + continue; + } + + const int left = std::min(target.x, wordX); + const int top = std::min(target.y, wordY); + const int right = std::max(target.x + target.width, wordX + wordWidth); + const int bottom = std::max(target.y + target.height, wordY + lineHeight); + target = {static_cast(left), static_cast(top), static_cast(right - left), + static_cast(bottom - top)}; + } + } +} + +bool EpubReaderActivity::handleTouchFootnoteLink(const int touchX, const int touchY) { + if (!SETTINGS.touchReaderControls || !mappedInput.hasTouch() || RenderLock::peek() || activeFootnotePreview || + currentPageFootnotes.empty()) { + return false; + } + + int bestIndex = -1; + int64_t bestDistance = std::numeric_limits::max(); + const int screenWidth = renderer.getScreenWidth(); + const int screenHeight = renderer.getScreenHeight(); + for (size_t i = 0; i < currentPageFootnotes.size() && i < currentPageFootnoteTouchTargets.size(); ++i) { + const auto& target = currentPageFootnoteTouchTargets[i]; + if (target.width <= 0 || target.height <= 0) continue; + + const int expandedWidth = std::max(target.width, TOUCH_FOOTNOTE_TARGET_SIZE); + const int expandedHeight = std::max(target.height, TOUCH_FOOTNOTE_TARGET_SIZE); + const int left = std::max( + 0, std::min(target.x - (expandedWidth - target.width) / 2, screenWidth - std::min(screenWidth, expandedWidth))); + const int top = std::max(0, std::min(target.y - (expandedHeight - target.height) / 2, + screenHeight - std::min(screenHeight, expandedHeight))); + const int right = std::min(screenWidth, left + expandedWidth); + const int bottom = std::min(screenHeight, top + expandedHeight); + if (touchX < left || touchX >= right || touchY < top || touchY >= bottom) continue; + + const int centerX = target.x + target.width / 2; + const int centerY = target.y + target.height / 2; + const int64_t deltaX = static_cast(touchX) - centerX; + const int64_t deltaY = static_cast(touchY) - centerY; + const int64_t distance = deltaX * deltaX + deltaY * deltaY; + if (distance < bestDistance) { + bestDistance = distance; + bestIndex = static_cast(i); + } + } + + if (bestIndex < 0) return false; + navigateToHref(currentPageFootnotes[bestIndex].href, true, /*preferFootnotePreview=*/true); + return true; } +#endif void EpubReaderActivity::drawClippingHighlights(const Page& page, const int fontId, const int orientedMarginTop, const int orientedMarginLeft) const { @@ -4465,22 +6023,34 @@ void EpubReaderActivity::drawClippingHighlights(const Page& page, const int font return; } - std::array matches; + std::array matches; uint16_t matchCount = 0; const bool canUseStoredRanges = section->pageCount > 0 && section->pageCount <= UINT16_MAX && section->currentPage >= 0 && section->currentPage < section->pageCount; const uint16_t currentPage = canUseStoredRanges ? static_cast(section->currentPage) : 0; const uint16_t currentPageCount = canUseStoredRanges ? static_cast(section->pageCount) : 0; + std::string clippingText; + clippingText.reserve(CLIPPING_TEXT_MAX); for (const Clipping& clipping : CLIPPINGS.getClippings()) { if (clipping.spineIndex != static_cast(currentSpineIndex)) { continue; } ClippingPageMatch match; + const bool storedLayoutMatches = + canUseStoredRanges && + clippingStoredRangeMatchesLayout(clipping, currentPageCount, activeSectionLayoutSignature); const bool matchedStoredRange = - canUseStoredRanges && findClippingStoredRangeOnPage(page, clipping, currentPage, currentPageCount, match); - const bool shouldSearchText = !canUseStoredRanges || clipping.pageCount != currentPageCount || - (currentPage >= clipping.startPage && currentPage <= clipping.endPage); - if (matchedStoredRange || (shouldSearchText && findClippingTextOnPage(page, clipping, match))) { + storedLayoutMatches && findClippingStoredRangeOnPage(page, clipping, currentPage, currentPageCount, match); + const bool shouldSearchText = + !storedLayoutMatches || (currentPage >= clipping.startPage && currentPage <= clipping.endPage); + bool matchedText = false; + if (!matchedStoredRange && shouldSearchText) { + clippingText.clear(); + if (CLIPPINGS.readClippingText(clipping, clippingText)) { + matchedText = findClippingTextOnPage(page, clippingText, match); + } + } + if (matchedStoredRange || matchedText) { matches[matchCount++] = match; if (matchCount >= matches.size()) { break; @@ -4491,7 +6061,6 @@ void EpubReaderActivity::drawClippingHighlights(const Page& page, const int font return; } - const bool foregroundBlack = ReaderUtils::readerForegroundBlack(); const auto isHighlightedWord = [&matches, matchCount](const uint16_t pageWordIndex) { for (uint16_t matchIndex = 0; matchIndex < matchCount; ++matchIndex) { if (pageWordIndex >= matches[matchIndex].startWord && pageWordIndex <= matches[matchIndex].endWord) { @@ -4507,11 +6076,11 @@ void EpubReaderActivity::drawClippingHighlights(const Page& page, const int font return true; } - const auto wordIndex = static_cast(i); - if (wordIndex >= block.wordCount()) { + if (i >= block.wordCount()) { return true; } + const auto wordIndex = static_cast(i); const char* wordText = block.wordText(wordIndex); const bool hasEmSpace = hasEmSpacePrefix(wordText); const char* visibleText = wordText + (hasEmSpace ? 3 : 0); @@ -4522,7 +6091,7 @@ void EpubReaderActivity::drawClippingHighlights(const Page& page, const int font int wordW = renderer.getTextAdvanceX(fontId, wordText, textStyle) - skipX; const int wordH = renderer.getLineHeight(fontId); if (wordIndex + 1 < block.wordCount()) { - const uint16_t nextIndex = wordIndex + 1; + const uint16_t nextIndex = static_cast(wordIndex + 1); const char* nextWordText = block.wordText(nextIndex); const bool nextHasEmSpace = hasEmSpacePrefix(nextWordText); const auto nextTextStyle = @@ -4537,20 +6106,28 @@ void EpubReaderActivity::drawClippingHighlights(const Page& page, const int font } if (wordW > 0) { renderer.fillRectDither(wordX, wordY, wordW, wordH, Color::LightGray); - renderer.drawText(fontId, wordX, wordY, visibleText, foregroundBlack, textStyle); + // A saved clipping always uses black text on its light-gray marker. + // The ordinary reader foreground is white in dark mode, which makes the + // text fade into this marker. + renderer.drawText(fontId, wordX, wordY, visibleText, true, textStyle); } return true; }); } void EpubReaderActivity::renderStatusBar() const { - const int totalPageCount = section->pageCount; + const int estimatedPageCount = section->estimatedTotalPages(); int currentPage = section->currentPage + 1; - int pageCount = totalPageCount; + int pageCount = estimatedPageCount; const float bookProgress = activeFootnotePreview ? 0.0f : getCurrentBookProgressPercent(); - const float sectionProgress = - (totalPageCount > 0) ? static_cast(section->currentPage) / static_cast(totalPageCount) : 0.0f; + const float sectionProgress = (estimatedPageCount > 0) + ? static_cast(section->currentPage) / static_cast(estimatedPageCount) + : 0.0f; float chapterProgress = sectionProgress; + bool pageCountEstimated = section->isBuilding() || section->isPartial(); + if (!activeFootnotePreview) { + resolveChapterGroupPageProgress(currentPage, pageCount, chapterProgress, pageCountEstimated); + } uint32_t referencePage = 0; uint32_t referencePageCount = 0; @@ -4575,11 +6152,22 @@ void EpubReaderActivity::renderStatusBar() const { textYOffset += UITheme::getInstance().getMetrics().statusBarVerticalMargin; } - } else if (activeFootnotePreview && SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE) { + } +#if CROSSINK_APP_CAP_TOUCH + else if (activeFootnotePreview) { + // The touch header owns the preview title; keep the footer from repeating it. + } +#else + else if (activeFootnotePreview && SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE) { title = tr(STR_FOOTNOTES); - } else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { + } +#endif + else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { title = tr(STR_UNNAMED); - const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); + int titleSpineIndex = currentSpineIndex; + int groupLastSpineIndex = currentSpineIndex; + epub->resolveChapterGroupRange(currentSpineIndex, titleSpineIndex, groupLastSpineIndex); + const int tocIndex = epub->getTocIndexForSpineIndex(titleSpineIndex); if (tocIndex != -1) { const auto tocItem = epub->getTocItem(tocIndex); title = tocItem.title; @@ -4589,9 +6177,10 @@ void EpubReaderActivity::renderStatusBar() const { title = epub->getTitle(); } - const int bookmarkPageCount = totalPageCount > 0 ? totalPageCount : 1; - const float rawProgress = - (totalPageCount > 0) ? (static_cast(section->currentPage) / static_cast(totalPageCount)) : 0.0f; + const int bookmarkPageCount = estimatedPageCount > 0 ? estimatedPageCount : 1; + const float rawProgress = (estimatedPageCount > 0) + ? (static_cast(section->currentPage) / static_cast(estimatedPageCount)) + : 0.0f; const bool bookmarked = !activeFootnotePreview && BOOKMARKS.hasBookmarkForPage(static_cast(currentSpineIndex), rawProgress, bookmarkPageCount); @@ -4600,11 +6189,128 @@ void EpubReaderActivity::renderStatusBar() const { (!activeFootnotePreview && formatTimeLeftLabel(timeLeftLabel, sizeof(timeLeftLabel))) ? timeLeftLabel : nullptr; GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, bookmarked, timeLeft, ReaderUtils::readerDarkModeEnabled(), chapterProgress * 100.0f, static_cast(referencePage), - static_cast(referencePageCount), !activeFootnotePreview); + static_cast(referencePageCount), !activeFootnotePreview, pageCountEstimated); GUI.drawTopStatusBarClock(renderer, UITheme::getInstance().getMetrics().topPadding, nullptr, true, 0, ReaderUtils::readerDarkModeEnabled()); } +void EpubReaderActivity::refreshChapterGroupEstimate(const uint16_t viewportWidth, const uint16_t viewportHeight) { + if (!epub || !section) { + chapterGroupEstimate = {}; + return; + } + + int firstSpineIndex = currentSpineIndex; + int lastSpineIndex = currentSpineIndex; + if (!epub->resolveChapterGroupRange(currentSpineIndex, firstSpineIndex, lastSpineIndex) || + firstSpineIndex == lastSpineIndex) { + chapterGroupEstimate = {}; + return; + } + + uint32_t signature = 2166136261U; + const auto mix = [&signature](const uint32_t value) { + signature ^= value; + signature *= 16777619U; + }; + mix(static_cast(activeSectionFontId != 0 ? activeSectionFontId : SETTINGS.getReaderFontId())); + mix(viewportWidth); + mix(viewportHeight); + mix(static_cast(SETTINGS.getReaderLineCompression() * 1000.0f)); + mix(SETTINGS.extraParagraphSpacing); + mix(SETTINGS.forceParagraphIndents); + mix(SETTINGS.paragraphAlignment); + mix(SETTINGS.hyphenationEnabled); + mix(SETTINGS.embeddedStyle); + mix(SETTINGS.imageRendering); + mix(SETTINGS.bionicReadingEnabled); + mix(SETTINGS.guideReadingEnabled); + mix(SETTINGS.wordSpacing); + mix(SETTINGS.epubRenderMode); + if (chapterGroupEstimate.valid && chapterGroupEstimate.currentSpineIndex == currentSpineIndex && + chapterGroupEstimate.settingsSignature == signature) { + return; + } + + ChapterGroupEstimateCache refreshed; + refreshed.currentSpineIndex = currentSpineIndex; + refreshed.firstSpineIndex = firstSpineIndex; + refreshed.lastSpineIndex = lastSpineIndex; + refreshed.settingsSignature = signature; + const int readerFontId = activeSectionFontId != 0 ? activeSectionFontId : SETTINGS.getReaderFontId(); + const EpubRenderMode renderMode = normalizeRenderMode(SETTINGS.epubRenderMode); + + for (int spineIndex = firstSpineIndex; spineIndex <= lastSpineIndex; ++spineIndex) { + if (spineIndex == currentSpineIndex) continue; + const size_t previousCumulative = spineIndex > 0 ? epub->getCumulativeSpineItemSize(spineIndex - 1) : 0; + const size_t cumulative = epub->getCumulativeSpineItemSize(spineIndex); + const uint32_t spineBytes = + cumulative > previousCumulative + ? static_cast(std::min(cumulative - previousCumulative, UINT32_MAX)) + : 0; + + Section sibling(epub, spineIndex, renderer, sectionCacheSuffixForRenderMode(renderMode)); + const bool loaded = sibling.loadSectionFile( + readerRenderSpecForProfile(readerFontId, viewportWidth, viewportHeight, buildProfileForRenderMode(renderMode))); + const uint32_t siblingPages = loaded ? sibling.estimatedTotalPages() : 0; + if (siblingPages > 0 && spineBytes > 0) { + refreshed.knownSiblingPages += siblingPages; + refreshed.knownSiblingBytes += spineBytes; + refreshed.siblingEstimateUsed = refreshed.siblingEstimateUsed || sibling.isPartial() || sibling.isBuilding(); + if (spineIndex < currentSpineIndex) refreshed.precedingKnownPages += siblingPages; + } else { + refreshed.unknownSiblingBytes += spineBytes; + refreshed.unknownSiblingCount++; + if (spineIndex < currentSpineIndex) { + refreshed.precedingUnknownBytes += spineBytes; + refreshed.precedingUnknownCount++; + } + } + } + refreshed.valid = true; + chapterGroupEstimate = refreshed; + LOG_DBG("ERS", "Chapter group estimate: spine=%d range=%d-%d known=%lu pages/%lu bytes unknown=%u/%lu bytes", + currentSpineIndex, firstSpineIndex, lastSpineIndex, static_cast(refreshed.knownSiblingPages), + static_cast(refreshed.knownSiblingBytes), refreshed.unknownSiblingCount, + static_cast(refreshed.unknownSiblingBytes)); +} + +bool EpubReaderActivity::resolveChapterGroupPageProgress(int& currentPage, int& pageCount, float& chapterProgress, + bool& pageCountEstimated) const { + if (!chapterGroupEstimate.valid || !section || chapterGroupEstimate.currentSpineIndex != currentSpineIndex) { + return false; + } + const uint32_t currentPages = section->estimatedTotalPages(); + const size_t previousCumulative = currentSpineIndex > 0 ? epub->getCumulativeSpineItemSize(currentSpineIndex - 1) : 0; + const size_t cumulative = epub->getCumulativeSpineItemSize(currentSpineIndex); + const uint32_t currentBytes = + cumulative > previousCumulative + ? static_cast(std::min(cumulative - previousCumulative, UINT32_MAX)) + : 0; + const uint64_t knownPages = static_cast(chapterGroupEstimate.knownSiblingPages) + currentPages; + const uint64_t knownBytes = static_cast(chapterGroupEstimate.knownSiblingBytes) + currentBytes; + if (knownPages == 0 || knownBytes == 0) return false; + + const auto estimateUnknownPages = [knownPages, knownBytes](const uint32_t bytes, const uint16_t itemCount) { + if (bytes == 0 || itemCount == 0) return uint32_t{0}; + const uint64_t projected = (static_cast(bytes) * knownPages + knownBytes / 2U) / knownBytes; + return static_cast(std::max(itemCount, projected)); + }; + const uint32_t unknownPages = + estimateUnknownPages(chapterGroupEstimate.unknownSiblingBytes, chapterGroupEstimate.unknownSiblingCount); + const uint32_t precedingUnknownPages = + estimateUnknownPages(chapterGroupEstimate.precedingUnknownBytes, chapterGroupEstimate.precedingUnknownCount); + const uint64_t groupedTotal = knownPages + unknownPages; + const uint64_t groupedCurrent = static_cast(chapterGroupEstimate.precedingKnownPages) + + precedingUnknownPages + static_cast(section->currentPage) + 1U; + pageCount = static_cast(std::min(60000, std::max(1, groupedTotal))); + currentPage = static_cast(std::min(groupedCurrent, static_cast(pageCount))); + chapterProgress = pageCount > 0 ? static_cast(currentPage - 1) / static_cast(pageCount) : 0.0f; + pageCountEstimated = section->isBuilding() || section->isPartial() || chapterGroupEstimate.siblingEstimateUsed || + chapterGroupEstimate.unknownSiblingCount > 0; + return true; +} + bool EpubReaderActivity::shouldUseFootnotePreview(const int targetSpineIndex, const std::string& anchor) const { if (!epub || anchor.empty() || targetSpineIndex < 0 || targetSpineIndex >= epub->getSpineItemsCount()) { return false; @@ -4626,7 +6332,8 @@ void EpubReaderActivity::clearFootnotePreviewState() { activeFootnotePreview = false; } -void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { +void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition, + const bool preferFootnotePreview) { pageLoadRetryCount = 0; if (!epub) return; @@ -4634,7 +6341,6 @@ void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool s if (savePosition && section && footnoteDepth < MAX_FOOTNOTE_DEPTH) { savedPositions[footnoteDepth] = {currentSpineIndex, section->currentPage}; footnoteDepth++; - LOG_DBG("ERS", "Saved position [%d]: spine %d, page %d", footnoteDepth, currentSpineIndex, section->currentPage); } // Extract fragment anchor (e.g. "#note1" or "chapter2.xhtml#note1") @@ -4662,7 +6368,8 @@ void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool s { RenderLock lock(*this); - const bool useFootnotePreview = savePosition && shouldUseFootnotePreview(targetSpineIndex, anchor); + const bool useFootnotePreview = savePosition && !anchor.empty() && + (preferFootnotePreview || shouldUseFootnotePreview(targetSpineIndex, anchor)); pendingAnchor = anchor; pendingFootnotePreviewAnchor = useFootnotePreview ? anchor : std::string{}; activeFootnotePreview = false; @@ -4672,7 +6379,6 @@ void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool s } armReadingPaceWarmup(savePosition ? "href_navigation" : "href_restore"); requestUpdate(); - LOG_DBG("ERS", "Navigated to spine %d for href: %s", targetSpineIndex, hrefStr.c_str()); } void EpubReaderActivity::restoreSavedPosition() { @@ -4680,7 +6386,6 @@ void EpubReaderActivity::restoreSavedPosition() { if (footnoteDepth <= 0) return; footnoteDepth--; const auto& pos = savedPositions[footnoteDepth]; - LOG_DBG("ERS", "Restoring position [%d]: spine %d, page %d", footnoteDepth, pos.spineIndex, pos.pageNumber); { RenderLock lock(*this); @@ -4694,24 +6399,31 @@ void EpubReaderActivity::restoreSavedPosition() { requestUpdate(); } bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) { - auto epub = std::make_shared(filePath, "/.crosspoint"); + auto epub = makeUniqueNoThrow(filePath, "/.crosspoint"); + if (!epub) { + LOG_ERR("SLP", "EPUB: failed to allocate book for sleep-page rendering"); + return false; + } epub->setupCacheDir(); ScopedReaderSettingsRestore restoreReaderSettings; - const auto readerSettings = loadBookReaderSettingsFile(epub->getCachePath()); + const auto readerSettings = readBookReaderSettings(*epub); if (readerSettings.hasCustomReaderSettings) { applyReaderSettings(readerSettings.readerSettings); } SETTINGS.epubRenderMode = readerSettings.hasRenderModeOverride ? normalizeRenderModeRaw(readerSettings.renderMode) : static_cast(EpubRenderMode::CrossInkDefault); - sdFontSystem.ensureLoaded(renderer); // Load CSS when embeddedStyle is enabled, as createSectionFile may need it to rebuild the cache. - if (!epub->load(true, SETTINGS.embeddedStyle == 0)) { - LOG_DBG("SLP", "EPUB: failed to load %s", filePath.c_str()); - return false; + { + GfxRenderer::FrameBufferLoan loan(renderer); + if (!epub->load(true, SETTINGS.embeddedStyle == 0, Epub::XLocationLoadMode::Skip)) { + LOG_DBG("SLP", "EPUB: failed to load %s", filePath.c_str()); + return false; + } } + ensureReaderSdFontLoaded(renderer); // Load saved spine index and page number int spineIndex = 0, pageNumber = 0; @@ -4735,81 +6447,83 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf int renderFontId = readerFontId; const EpubRenderMode selectedRenderMode = normalizeRenderMode(SETTINGS.epubRenderMode); auto section = - makeUniqueNoThrow
    (epub, spineIndex, renderer, sectionCacheSuffixForRenderMode(selectedRenderMode)); - if (!section) { + makeUniqueNoThrow
    (*epub, spineIndex, renderer, sectionCacheSuffixForRenderMode(selectedRenderMode)); + bool loadedSection = false; + if (section) { + loadedSection = section->loadSectionFile(readerRenderSpecForProfile(readerFontId, viewportWidth, viewportHeight, + buildProfileForRenderMode(selectedRenderMode))); + } else { LOG_ERR("SLP", "EPUB: failed to allocate section for spine %d", spineIndex); - return false; } - bool loadedSection = section->loadSectionFile( - readerFontId, SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.forceParagraphIndents, - SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, - SETTINGS.imageRendering, SETTINGS.bionicReadingEnabled, SETTINGS.guideReadingEnabled, selectedRenderMode); - if (!loadedSection) { - if (!MemoryBudget::hasHeapForOptionalEpubRebuild("SLP", "EPUB sleep-page cache rebuild", spineIndex)) { - return false; - } - - LOG_DBG("SLP", "EPUB: section cache not found for spine %d, rebuilding (free=%u, maxAlloc=%u)", spineIndex, - ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + bool sectionRebuilt = false; + bool safeModeBuildSucceeded = false; + EpubRenderMode usedRenderMode = selectedRenderMode; + const auto rebuildSectionWithFallback = [&]() { bool layoutAbortedForLowMemory = false; - bool buildSucceeded = false; - bool safeModeBuildSucceeded = false; - EpubRenderMode usedRenderMode = selectedRenderMode; - uint8_t fallbackCount = 0; - const auto fallbackModes = fallbackModesForSelection(selectedRenderMode, fallbackCount); - for (uint8_t i = 0; i < fallbackCount && !buildSucceeded; ++i) { - const EpubRenderMode attemptMode = fallbackModes[i]; - if (i > 0) { - if (!layoutAbortedForLowMemory) { - break; - } - LOG_DBG("SLP", "EPUB: retrying sleep-page rebuild with mode %u for spine %d", - static_cast(attemptMode), spineIndex); - releaseReaderSdFontCachesForLowMemory(renderer, "SLP", "sleep-page fallback rebuild"); - } + auto buildWithFallback = [&](const SectionBuildProfile& profile) { layoutAbortedForLowMemory = false; - const SectionBuildProfile profile = buildProfileForRenderMode(attemptMode); section = - makeUniqueNoThrow
    (epub, spineIndex, renderer, sectionCacheSuffixForRenderMode(profile.renderMode)); + makeUniqueNoThrow
    (*epub, spineIndex, renderer, sectionCacheSuffixForRenderMode(profile.renderMode)); if (!section) { LOG_ERR("SLP", "EPUB: failed to allocate section builder for spine %d", spineIndex); - return false; - } - buildSucceeded = section->createSectionFile( - readerFontId, SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, - SETTINGS.forceParagraphIndents, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, - SETTINGS.hyphenationEnabled, profile.embeddedStyle, SETTINGS.imageRendering, profile.bionicReadingEnabled, - profile.guideReadingEnabled, []() {}, nullptr, &layoutAbortedForLowMemory, profile.renderMode); - if (buildSucceeded) { - usedRenderMode = profile.renderMode; - } - } - if (!buildSucceeded && layoutAbortedForLowMemory && shouldAttemptSafeModeFallback()) { - LOG_DBG("SLP", "EPUB: retrying sleep-page rebuild with Safe Mode for spine %d", spineIndex); - releaseReaderSdFontCachesForLowMemory(renderer, "SLP", "sleep-page safe mode rebuild"); - layoutAbortedForLowMemory = false; - const SectionBuildProfile profile = safeModeBuildProfile(); - section = - makeUniqueNoThrow
    (epub, spineIndex, renderer, sectionCacheSuffixForRenderMode(profile.renderMode)); - if (!section) { - LOG_ERR("SLP", "EPUB: failed to allocate Safe Mode section builder for spine %d", spineIndex); - return false; + return SectionBuildAttempt{false, true}; } - buildSucceeded = section->createSectionFile( - readerFontId, SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, - SETTINGS.forceParagraphIndents, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, - SETTINGS.hyphenationEnabled, profile.embeddedStyle, SETTINGS.imageRendering, profile.bionicReadingEnabled, - profile.guideReadingEnabled, []() {}, nullptr, &layoutAbortedForLowMemory, profile.renderMode); - if (buildSucceeded) { - safeModeBuildSucceeded = true; - usedRenderMode = profile.renderMode; + const bool succeeded = section->createSectionFile( + readerRenderSpecForProfile(readerFontId, viewportWidth, viewportHeight, profile), []() {}, nullptr, + &layoutAbortedForLowMemory); + if (succeeded) usedRenderMode = profile.renderMode; + return SectionBuildAttempt{succeeded, layoutAbortedForLowMemory}; + }; + auto beforeFallbackRetry = [&](const SectionBuildProfile& profile) { + if (profile.safeMode) { + LOG_DBG("SLP", "EPUB: retrying sleep-page rebuild with Safe Mode for spine %d", spineIndex); + releaseReaderSdFontCachesForLowMemory(renderer, "SLP", "sleep-page safe mode rebuild"); + } else { + LOG_DBG("SLP", "EPUB: retrying sleep-page rebuild with mode %u for spine %d", + static_cast(profile.renderMode), spineIndex); + releaseReaderSdFontCachesForLowMemory(renderer, "SLP", "sleep-page fallback rebuild"); } + }; + const SectionFallbackResult result = runSectionBuildFallbacks(selectedRenderMode, shouldAttemptSafeModeFallback(), + buildWithFallback, beforeFallbackRetry); + safeModeBuildSucceeded = result.usedSafeMode; + return result.succeeded; + }; + + if (!loadedSection) { + if (!MemoryBudget::hasHeapForOptionalEpubRebuild("SLP", "EPUB sleep-page cache rebuild", spineIndex)) { + return false; } - if (!buildSucceeded) { + + LOG_DBG("SLP", "EPUB: section cache not found for spine %d, rebuilding (free=%u, maxAlloc=%u)", spineIndex, + ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + if (!rebuildSectionWithFallback()) { LOG_ERR("SLP", "EPUB: failed to rebuild section cache for spine %d", spineIndex); return false; } + sectionRebuilt = true; + } + + if (pageNumber >= section->pageCount && loadedSection && section->isPartial()) { + if (!MemoryBudget::hasHeapForOptionalEpubRebuild("SLP", "EPUB sleep-page partial catch-up", spineIndex)) { + return false; + } + bool catchUpSucceeded = section->startBuild(readerRenderSpecForProfile( + readerFontId, viewportWidth, viewportHeight, buildProfileForRenderMode(selectedRenderMode))); + while (catchUpSucceeded && !section->isBuildComplete() && pageNumber >= section->pageCount) { + catchUpSucceeded = section->buildSomeMore(BUILD_PAGES_PER_CHUNK); + } + if (!catchUpSucceeded) { + if (!section->lastBuildLayoutAbortedForLowMemory()) return false; + LOG_DBG("SLP", "EPUB: partial catch-up hit low heap; rebuilding with fallback modes"); + releaseReaderSdFontCachesForLowMemory(renderer, "SLP", "sleep-page partial fallback rebuild"); + section.reset(); + if (!rebuildSectionWithFallback()) return false; + sectionRebuilt = true; + } + } + if (sectionRebuilt) { if (safeModeBuildSucceeded) { applySafeModeReaderSettings(); if (!saveRuntimeReaderSettingsForCache(epub->getCachePath())) { @@ -4824,11 +6538,10 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf spineIndex, section->pageCount, renderFontId, static_cast(usedRenderMode), ESP.getFreeHeap(), ESP.getMaxAllocHeap()); } - if (pageNumber < 0 || pageNumber >= section->pageCount) return false; section->currentPage = pageNumber; - auto page = section->loadPageFromSectionFile(); + auto page = section->loadPage(pageNumber); if (!page) { LOG_DBG("SLP", "EPUB: failed to load page %d", pageNumber); return false; @@ -4850,7 +6563,7 @@ ScreenshotInfo EpubReaderActivity::getScreenshotInfo() const { info.spineIndex = currentSpineIndex; } if (section) { - const int totalPages = section->pageCount; + const int totalPages = section->estimatedTotalPages(); info.currentPage = section->currentPage + 1; info.totalPages = totalPages; if (epub && epub->getBookSize() > 0 && totalPages > 0) { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 2fc9bdc547..87cc6fcd97 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include @@ -14,6 +16,7 @@ #include "EndOfBookOptions.h" #include "EpubReaderMenuActivity.h" #include "GlobalReadingStats.h" +#include "ReaderProgressSaveDebouncer.h" #include "activities/Activity.h" struct ToastRect { @@ -25,10 +28,13 @@ struct ToastRect { class EpubReaderActivity final : public Activity { public: + bool usesFullScreenReaderVerticalSwipes() const override { return true; } + struct ReaderSettingsSnapshot { uint8_t fontFamily = 0; - uint8_t fontSize = 0; + uint8_t readerFontPointSize = 14; uint8_t lineHeightPercent = 100; + uint8_t wordSpacing = 0; uint8_t orientation = 0; uint8_t screenMargin = 5; uint8_t publisherPageNumbers = 0; @@ -43,15 +49,55 @@ class EpubReaderActivity final : public Activity { uint8_t bionicReadingEnabled = 0; uint8_t guideReadingEnabled = 0; uint8_t epubRenderMode = 0; + uint8_t indexingMethod = CrossPointSettings::INDEXING_FULL_SECTION; char sdFontFamilyName[64] = ""; }; + struct BookReaderSettingsData { + bool hasAutoPageTurnInterval = false; + uint16_t autoPageTurnSeconds = 0; + bool hasCustomReaderSettings = false; + bool hasRenderModeOverride = false; + bool hasDictionaryFontOverride = false; + uint8_t renderMode = 0; + // Fixed-size per-book state lives inside the already heap-owned reader + // activity. It avoids a separate string allocation during dictionary use. + char dictionarySdFontFamilyName[64] = ""; + // Zero follows the reader's current physical point size. + uint8_t dictionaryFontPointSize = 0; + ReaderSettingsSnapshot readerSettings; + }; + private: + // The on-disk settings record also carries the dictionary family. Keeping it + // out of the long-lived reader object matters on the C3: this activity is + // allocated immediately before an EPUB section needs its largest block. + // Dictionary children load and own the fixed name only while they are open. + struct ActiveBookReaderSettingsData { + bool hasAutoPageTurnInterval = false; + uint16_t autoPageTurnSeconds = 0; + bool hasCustomReaderSettings = false; + bool hasRenderModeOverride = false; + uint8_t renderMode = 0; + ReaderSettingsSnapshot readerSettings; + + ActiveBookReaderSettingsData() = default; + explicit ActiveBookReaderSettingsData(const BookReaderSettingsData& source) + : hasAutoPageTurnInterval(source.hasAutoPageTurnInterval), + autoPageTurnSeconds(source.autoPageTurnSeconds), + hasCustomReaderSettings(source.hasCustomReaderSettings), + hasRenderModeOverride(source.hasRenderModeOverride), + renderMode(source.renderMode), + readerSettings(source.readerSettings) {} + }; + std::shared_ptr epub; + ActiveBookReaderSettingsData initialBookReaderSettings; std::unique_ptr
    section = nullptr; int currentSpineIndex = 0; int nextPageNumber = 0; int activeSectionFontId = 0; + uint32_t activeSectionLayoutSignature = 0; std::optional pendingPageJump; // Set when navigating to a footnote href with a fragment (e.g. #note1). // Cleared on the next render after the new section loads and resolves it to a page. @@ -59,16 +105,44 @@ class EpubReaderActivity final : public Activity { std::string pendingFootnotePreviewAnchor; bool activeFootnotePreview = false; int pagesUntilFullRefresh = 0; + // A Sync Progress return can leave non-reader UI on the panel. This is a + // one-shot clean base for its first image page; normal image-page cleanup + // uses pagesUntilFullRefresh independently. + bool cleanImageBasePending = false; int cachedSpineIndex = 0; int cachedChapterPageNumber = 0; int cachedChapterTotalPageCount = 0; + int cachedChapterPageWatermark = 0; + std::optional cachedVisibleTextOffset; + struct ChapterGroupEstimateCache { + int currentSpineIndex = -1; + int firstSpineIndex = -1; + int lastSpineIndex = -1; + uint32_t settingsSignature = 0; + uint32_t knownSiblingPages = 0; + uint32_t knownSiblingBytes = 0; + uint32_t unknownSiblingBytes = 0; + uint32_t precedingKnownPages = 0; + uint32_t precedingUnknownBytes = 0; + uint16_t unknownSiblingCount = 0; + uint16_t precedingUnknownCount = 0; + bool siblingEstimateUsed = false; + bool valid = false; + } chapterGroupEstimate; bool pendingRelayoutReposition = false; uint16_t cachedPageParagraphIndex = UINT16_MAX; uint16_t cachedPageParagraphOffset = 0; uint16_t cachedPageParagraphSpan = 0; + std::atomic pendingHeapShapeReaderRedrawStages{0}; + static constexpr uint8_t HEAP_SHAPE_REDRAW_CLIP = 1U << 0; + static constexpr uint8_t HEAP_SHAPE_REDRAW_DICT = 1U << 1; unsigned long lastPageTurnTime = 0UL; unsigned long pageTurnDuration = 0UL; unsigned long pageShownAtMs = 0UL; + unsigned long lastRenderCompleteMs = 0UL; + int idlePrewarmSpine = -1; + int idlePrewarmPage = -1; + int idlePrewarmFontId = 0; bool paceSampleWarmupPending = true; uint32_t sessionPaceSampleSeconds = 0; uint16_t sessionPaceSampleCount = 0; @@ -101,6 +175,7 @@ class EpubReaderActivity final : public Activity { bool longPowerButtonHandled = false; bool sideButtonLongPressHandled = false; bool frontButtonLongPressHandled = false; + bool touchDictionaryLookupHandled = false; int pageLoadRetryCount = 0; enum class BookmarkFeedbackType : uint8_t { Added, @@ -145,6 +220,15 @@ class EpubReaderActivity final : public Activity { // Footnote support std::vector currentPageFootnotes; +#if CROSSINK_APP_CAP_TOUCH + struct FootnoteTouchTarget { + int16_t x = 0; + int16_t y = 0; + int16_t width = 0; + int16_t height = 0; + }; + std::array currentPageFootnoteTouchTargets{}; +#endif struct SavedPosition { int spineIndex; int pageNumber; @@ -153,18 +237,102 @@ class EpubReaderActivity final : public Activity { SavedPosition savedPositions[MAX_FOOTNOTE_DEPTH] = {}; int footnoteDepth = 0; + // Viewport of the last render(), captured so loop()'s lazy partial-extension start + // builds with identical layout parameters to the pages already rendered. + uint16_t buildViewportWidth = 0; + uint16_t buildViewportHeight = 0; + // Set when the lazy extension start failed, so loop() does not retry every tick. + bool partialRebuildStartFailed = false; + // Set when a background extension build aborted for low heap. startBuild() still succeeds in + // that state -- it is the layout inside buildSomeMore() that runs out of memory -- so without + // this flag loop() would restart the same doomed build every tick, and skipLoopDelay() would + // hold the loop at full speed while it did. The reader keeps the pages already laid out; a + // build is only re-attempted from render() if the reader actually pages past the watermark. + bool partialRebuildAbortedForLowMemory = false; + // One-shot guard for the silent restart used before EPUB layout fallback modes. The restart token + // restores this guard after boot so the resumed attempt can fall through to those modes. + bool lowMemoryPartialRestartAttempted = false; + bool backgroundBuildPausedForLowMemory = false; + // Input should win the next RenderLock race. Keep the incremental parser alive, + // but do not start another background chunk until the requested render begins. + std::atomic backgroundBuildYieldForInput{false}; + // Full-section next-chapter prefetch is speculative. A forward page turn may + // stop it, but the visible build at the chapter boundary remains full-section. + std::atomic silentPrefetchBuildActive{false}; + std::atomic silentPrefetchCancelRequested{false}; + std::atomic sectionBuildCancelRequested{false}; + std::atomic goHomeAfterBuildCancel{false}; + + // Last position successfully persisted by saveProgress, used to skip redundant + // writeAtomic calls on no-op re-renders. + int lastSavedSpineIndex = -1; + int lastSavedPage = -1; + int lastSavedPageCount = -1; + ReaderProgressSaveDebouncer progressSaveDebouncer; + bool progressSaveRequiredAfterRelayout = false; + // Adapted from Sichroteph/YACP commit 3f3c5fc42e794c021edb9832856ef98c2d2065b9 + // (MIT): retain one render-only strip instead of reallocating it on every + // grayscale page. Released before section/index work that needs heap headroom. + std::unique_ptr grayscaleStripScratch; + size_t grayscaleStripScratchSize = 0; + // Trigger/memoization concept adapted from Sichroteph/YACP commit + // 3f3c5fc42e794c021edb9832856ef98c2d2065b9 (MIT). + int preparedNextSpineIndex = -1; + uint16_t preparedNextViewportWidth = 0; + uint16_t preparedNextViewportHeight = 0; + void renderContents(std::unique_ptr page, int fontId, int orientedMarginTop, int orientedMarginRight, - int orientedMarginBottom, int orientedMarginLeft); + int orientedMarginBottom, int orientedMarginLeft, bool updatePanel); + bool ensureGrayscaleStripScratch(); + void releaseGrayscaleStripScratch(); void drawClippingHighlights(const Page& page, int fontId, int orientedMarginTop, int orientedMarginLeft) const; void renderStatusBar() const; + void refreshChapterGroupEstimate(uint16_t viewportWidth, uint16_t viewportHeight); + bool resolveChapterGroupPageProgress(int& currentPage, int& pageCount, float& chapterProgress, + bool& pageCountEstimated) const; bool shouldUseFootnotePreview(int targetSpineIndex, const std::string& anchor) const; std::string footnotePreviewCacheSuffix(EpubRenderMode renderMode, const std::string& anchor) const; void clearFootnotePreviewState(); - // Remap the cached relative reading position once the section's real page count is known + void silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight); + // Larger batches are reserved for non-interactive work such as sleep-page preparation. + static constexpr int BUILD_PAGES_PER_CHUNK = 8; + // Interactive builds stop as soon as the requested page is ready and give the + // main loop a chance to observe input between pages. + static constexpr int INTERACTIVE_BUILD_PAGES_PER_CHUNK = 1; + static constexpr int BACKGROUND_BUILD_PAGES_PER_TICK = 1; + // How many pages to keep laid out ahead of the reader for a still-building section. A page + // turn is ~1s on e-ink and a page builds in ~30ms, so the reader can't out-click the builder + // -- a tiny buffer is enough. The background build stops once the watermark is this far + // ahead and resumes as the reader advances; building unbounded instead locked up input by + // monopolizing the RenderLock. A giant single-spine book therefore never finalizes its .bin + // in one sitting -- instant reopen comes from Section::suspendBuild() persisting the pages + // already laid out as a partial file on exit/sleep. + static constexpr int BUILD_WINDOW_AHEAD = 5; + // Reopening a partial does not immediately restart its whole-chapter extension build. + // Start it only when the reader is close enough to need pages past the watermark. + static constexpr int PARTIAL_REBUILD_START_MARGIN = 15; + // Show the indexing popup when an initial build must lay out more than this many pages up front + // (a deep resume/jump into a not-yet-built section), so it isn't a silent wait. Kept independent + // of the small look-ahead window so ordinary landings stay popup-free. + static constexpr int BUILD_POPUP_PAGE_THRESHOLD = 20; + // Also show the popup when first building a spine larger than this (uncompressed bytes): its + // whole HTML must be inflated before page 1 can lay out (the giant single-spine case), which is + // a multi-second wait. Normal chapters are well under this and stay popup-free. + static constexpr size_t BUILD_POPUP_BYTE_THRESHOLD = 96 * 1024; + // If a build predicted to be fast still has not produced the requested page within this + // window, show the popup while the blocking build continues. + static constexpr unsigned long BUILD_POPUP_DEADLINE_MS = 1000; + // Only true during the blocking build-to-target phase. The parser retains the callback during + // background indexing, so this guard prevents it from drawing over an already-visible page. + bool buildPopupPending = false; + void showBuildPopup(); + // Remap the cached reading position once the saved paragraph and prior readable watermark are rebuilt // (used after a settings change re-paginates a chapter). Returns true if currentPage moved. - // No-op when pagination is unchanged (plain resume). + bool isRelayoutCatchUpComplete() const; bool applyDeferredReposition(); bool saveProgress(int spineIndex, int currentPage, int pageCount); + bool queueProgressSave(int spineIndex, int currentPage, int pageCount, bool forceSave = false); + bool flushQueuedProgress(); void cacheCurrentSectionPosition(); void pauseReadingPaceTimer(const char* reason = "unknown"); void resumeReadingPaceTimer(const char* reason = "unknown"); @@ -192,27 +360,41 @@ class EpubReaderActivity final : public Activity { void restoreGlobalReaderSettings(); void loadBookReaderSettings(); void saveCurrentBookReaderSettings(); + void saveDictionaryFontForBook(const char* familyName, uint8_t pointSize); void saveGlobalSettingsPreservingBookOverrides(); void beginGlobalSettingsEdit(); void endGlobalSettingsEdit(); static void saveReaderOptionsForBook(void* ctx); + static void saveDictionaryFontForBookReader(void* ctx, const char* familyName, uint8_t pointSize); static void saveGlobalSettingsForBookReader(void* ctx); static void beginGlobalSettingsEditForBookReader(void* ctx); static void endGlobalSettingsEditForBookReader(void* ctx); // Jump to a percentage of the book (0-100), mapping it to spine and page. void jumpToPercent(int percent); void reindexCurrentSection(); + void prepareCurrentSectionForRelayout(); void executeReaderQuickAction(CrossPointSettings::LONG_PRESS_MENU_ACTION action); bool quickActionUsesConfirmRelease(CrossPointSettings::LONG_PRESS_MENU_ACTION action) const; bool quickActionUsesPowerRelease(CrossPointSettings::LONG_PRESS_MENU_ACTION action) const; void suppressConfirmShortcutRelease(CrossPointSettings::LONG_PRESS_MENU_ACTION action); void executeFootnoteQuickAction(bool suppressInitialPowerRelease = false); +#if CROSSINK_APP_CAP_TOUCH + void buildFootnoteTouchTargets(const Page& page, int fontId, int orientedMarginTop, int orientedMarginLeft); + bool handleTouchFootnoteLink(int touchX, int touchY); +#endif void suppressPowerShortcutRelease(); bool consumeLongPowerButtonRelease(); bool consumeLongPowerButtonHold(); bool executeShortPowerButtonAction(); bool executeLongPowerButtonAction(); void handleClippingJump(const ClippingJumpResult& clipping); + bool handleTouchDictionaryLookup(); + void openWordSelect(bool framebufferContainsPage, int initialTouchX = -1, int initialTouchY = -1, + bool autoLookupInitialWord = false); + std::unique_ptr reloadDictionaryLookupPage(); + void renderDictionaryLookupBackground(); + static std::unique_ptr reloadDictionaryLookupPageCallback(void* context); + static void renderDictionaryLookupBackgroundCallback(void* context); void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); // Opens the reader menu for the current position (short-press Confirm) void openReaderMenu(); @@ -233,19 +415,53 @@ class EpubReaderActivity final : public Activity { bool restoreRenderModeToastRegion(); // Footnote navigation - void navigateToHref(const std::string& href, bool savePosition = false); + void navigateToHref(const std::string& href, bool savePosition = false, bool preferFootnotePreview = false); void restoreSavedPosition(); public: - explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub) - : Activity("EpubReader", renderer, mappedInput), epub(std::move(epub)) {} + explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub, + const BookReaderSettingsData& readerSettings, int initialRefreshCountdown, + bool cleanImageBaseOnEntry = false) + : Activity("EpubReader", renderer, mappedInput), + epub(std::move(epub)), + initialBookReaderSettings(readerSettings), + pagesUntilFullRefresh(initialRefreshCountdown), + cleanImageBasePending(cleanImageBaseOnEntry) {} void onEnter() override; void onExit() override; void loop() override; void render(RenderLock&& lock) override; + bool prepareManualRefresh() override { + pagesUntilFullRefresh = 1; + cleanImageBasePending = true; + return true; + } bool preventAutoSleep() override { return automaticPageTurnActive; } + // Hold the loop hot only while the build has work this loop would do: a kept-alive + // build sitting outside the lookahead window is dormant, and reporting it here would + // pin the CPU at full clock (no power saving, yield-only loop) for the whole read. + // Mirrors the tick condition in loop(): catch-up phase, or watermark inside the window. + bool sectionBuildWantsTick() const { + return section && section->isBuilding() && + (!section->activeBuildHasCaughtReadablePages() || + static_cast(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD); + } + bool backgroundSectionBuildHasHeap(); + void idlePrewarmNextPage(); + bool skipLoopDelay() override { + return sectionBuildWantsTick() && !backgroundBuildPausedForLowMemory && + !backgroundBuildYieldForInput.load(std::memory_order_relaxed); + } bool isReaderActivity() const override { return true; } bool canSnapshotForSleepOverlay() const override { return true; } + bool handlesReaderPowerSettingsOverride() const override { return true; } + bool openReaderSettingsMenu() override { + if (!epub) { + return false; + } + openReaderMenu(); + return true; + } std::string getCurrentBookPath() const override { return epub ? epub->getPath() : std::string{}; } void setAutoPageTurnIntervalSeconds(uint16_t seconds); uint16_t getAutoPageTurnIntervalSeconds() const; @@ -254,6 +470,7 @@ class EpubReaderActivity final : public Activity { // Used by SleepActivity to prepare the background for the overlay sleep mode. // Returns false if the page cannot be loaded (missing cache / file error). static bool drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer); + static BookReaderSettingsData readBookReaderSettings(const Epub& epub); static uint8_t loadBookRenderMode(const std::string& filePath); static bool saveBookRenderMode(const std::string& filePath, uint8_t renderMode); static bool resetBookReaderSettings(const std::string& filePath); diff --git a/src/activities/reader/EpubReaderBookmarkListActivity.cpp b/src/activities/reader/EpubReaderBookmarkListActivity.cpp index b8f65522ca..7b52cbf819 100644 --- a/src/activities/reader/EpubReaderBookmarkListActivity.cpp +++ b/src/activities/reader/EpubReaderBookmarkListActivity.cpp @@ -3,31 +3,36 @@ #include #include -#include #include -#include #include "MappedInputManager.h" -#include "activities/home/FileBrowserActionActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" -#include "fontIds.h" - -static constexpr int ROW_HEIGHT = 68; -static constexpr int LIST_START_Y = 60; -static constexpr unsigned long BOOKMARK_DELETE_HOLD_MS = 1000; - -int EpubReaderBookmarkListActivity::getPageItems() const { - const auto orientation = renderer.getOrientation(); - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; - const int startY = LIST_START_Y + hintGutterHeight; - const int available = renderer.getScreenHeight() - startY - ROW_HEIGHT; - return std::max(1, available / ROW_HEIGHT); -} +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" + +namespace fui = freeink::ui; +namespace { +constexpr unsigned long BOOKMARK_DELETE_HOLD_MS = 1000; +constexpr fui::ActionId ACTION_ROW = 1; +} // namespace + +EpubReaderBookmarkListActivity::EpubReaderBookmarkListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::vector& bookmarks) + : Activity("EpubReaderBookmarkList", renderer, mappedInput), + bookmarks(bookmarks), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} void EpubReaderBookmarkListActivity::onEnter() { Activity::onEnter(); selectedIndex = 0; + topIndex = 0; + visibleRows = 1; + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &EpubReaderBookmarkListActivity::onRowEvent, this); + app.setScreen(&EpubReaderBookmarkListActivity::listScreen, this); requestUpdate(); } @@ -35,170 +40,187 @@ void EpubReaderBookmarkListActivity::onExit() { Activity::onExit(); } void EpubReaderBookmarkListActivity::deleteSelectedBookmark() { if (bookmarks.empty() || selectedIndex < 0 || selectedIndex >= static_cast(bookmarks.size())) return; - if (!BOOKMARKS.removeBookmarkAt(static_cast(selectedIndex))) return; - bookmarks = BOOKMARKS.getBookmarks(); - if (bookmarks.empty()) { + if (bookmarks.empty()) selectedIndex = 0; - } else if (selectedIndex >= static_cast(bookmarks.size())) { - selectedIndex = static_cast(bookmarks.size()) - 1; - } + else if (selectedIndex >= static_cast(bookmarks.size())) + selectedIndex = bookmarks.size() - 1; + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, static_cast(bookmarks.size())); requestUpdate(); } -void EpubReaderBookmarkListActivity::showBookmarkActionMenu(bool ignoreInitialConfirmRelease) { +void EpubReaderBookmarkListActivity::showBookmarkDeletePopup() { if (bookmarks.empty() || selectedIndex < 0 || selectedIndex >= static_cast(bookmarks.size())) return; + confirmingDelete = true; + const char* options[] = {tr(STR_CANCEL), tr(STR_DELETE)}; + confirmPopup.show(tr(STR_CONFIRM_DELETE_BOOKMARK), options, 2, 0, [this](const int optionIndex) { + confirmingDelete = false; + if (optionIndex == 1) deleteSelectedBookmark(); + requestUpdate(); + }); + requestUpdate(); +} - const Bookmark selectedBookmark = bookmarks[selectedIndex]; - const char* chapter = (selectedBookmark.chapterTitle[0] != '\0') ? selectedBookmark.chapterTitle : tr(STR_BOOKMARKS); - std::vector items; - items.reserve(1); - items.push_back({FileBrowserAction::Delete, StrId::STR_DELETE}); - - startActivityForResult( - std::make_unique(renderer, mappedInput, chapter, std::move(items), - ignoreInitialConfirmRelease), - [this, selectedBookmark](const ActivityResult& result) { - longPressConfirmHandled = false; - if (result.isCancelled) { - requestUpdate(); - return; - } - - const auto* actionResult = std::get_if(&result.data); - if (!actionResult || static_cast(actionResult->action) != FileBrowserAction::Delete) { - requestUpdate(); - return; - } +void EpubReaderBookmarkListActivity::selectBookmark() { + if (bookmarks.empty() || selectedIndex < 0 || selectedIndex >= static_cast(bookmarks.size())) return; + setResult(BookmarkResult{bookmarks[selectedIndex].spineIndex, bookmarks[selectedIndex].progress, + bookmarks[selectedIndex].paragraphIndex}); + finish(); +} - const auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&selectedBookmark](const Bookmark& bm) { - return bm.spineIndex == selectedBookmark.spineIndex && bm.progress == selectedBookmark.progress; - }); - if (it != bookmarks.end()) { - selectedIndex = static_cast(std::distance(bookmarks.begin(), it)); - deleteSelectedBookmark(); - } else { - requestUpdate(); - } - }); +void EpubReaderBookmarkListActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(self->bookmarks.size())) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->selectBookmark(); } void EpubReaderBookmarkListActivity::loop() { - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + if (confirmPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return; + if (confirmingDelete) { + confirmingDelete = false; + requestUpdate(); + return; + } + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header{safe.x, safe.y + metrics.topPadding, safe.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (TouchHeaderBackButton::wasTapped(mappedInput, header) || + mappedInput.wasReleased(MappedInputManager::Button::Back)) { ActivityResult result; result.isCancelled = true; setResult(std::move(result)); finish(); return; } - - if (!bookmarks.empty() && !longPressConfirmHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && + if (!bookmarks.empty() && mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= BOOKMARK_DELETE_HOLD_MS) { - longPressConfirmHandled = true; - showBookmarkActionMenu(true); + showBookmarkDeletePopup(); return; } - - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (longPressConfirmHandled) { - longPressConfirmHandled = false; - return; + int tx = 0; + int ty = 0; + if (mappedInput.isScreenTouchLongPress(tx, ty, BOOKMARK_DELETE_HOLD_MS) && listRowStep > 0 && ty >= listTop && + ty < listBottom) { + const int offset = ty - listTop; + const int row = offset / listRowStep; + const int touched = topIndex + row; + if (row < visibleRows && offset % listRowStep < listRowHeight && touched < static_cast(bookmarks.size())) { + selectedIndex = touched; + mappedInput.suppressNextTouchTap(); + showBookmarkDeletePopup(); } - if (!bookmarks.empty() && selectedIndex >= 0 && selectedIndex < static_cast(bookmarks.size())) { - setResult(BookmarkResult{bookmarks[selectedIndex].spineIndex, bookmarks[selectedIndex].progress, - bookmarks[selectedIndex].paragraphIndex}); - finish(); + return; + } + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; } + } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + selectBookmark(); return; } - const int total = static_cast(bookmarks.size()); - if (total == 0) return; - - const int pageItems = getPageItems(); - - buttonNavigator.onNextRelease([this, total] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, total); - requestUpdate(); - }); - - buttonNavigator.onPreviousRelease([this, total] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, total); - requestUpdate(); - }); - - buttonNavigator.onNextContinuous([this, total, pageItems] { - selectedIndex = ButtonNavigator::nextPageIndex(selectedIndex, total, pageItems); - requestUpdate(); - }); - - buttonNavigator.onPreviousContinuous([this, total, pageItems] { - selectedIndex = ButtonNavigator::previousPageIndex(selectedIndex, total, pageItems); + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int next = scrollListBy(topIndex, swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows, + visibleRows, total); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + const auto move = [this, total](const int next) { + selectedIndex = next; + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, total); requestUpdate(); - }); + }; + buttonNavigator.onNextRelease([this, total, &move] { move(ButtonNavigator::nextIndex(selectedIndex, total)); }); + buttonNavigator.onPreviousRelease( + [this, total, &move] { move(ButtonNavigator::previousIndex(selectedIndex, total)); }); + buttonNavigator.onNextContinuous( + [this, total, &move] { move(ButtonNavigator::nextPageIndex(selectedIndex, total, visibleRows)); }); + buttonNavigator.onPreviousContinuous( + [this, total, &move] { move(ButtonNavigator::previousPageIndex(selectedIndex, total, visibleRows)); }); } -void EpubReaderBookmarkListActivity::render(RenderLock&&) { - renderer.clearScreen(); - - const auto pageWidth = renderer.getScreenWidth(); - const auto orientation = renderer.getOrientation(); - const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; - const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; - const int contentX = isLandscapeCw ? hintGutterWidth : 0; - const int contentWidth = pageWidth - hintGutterWidth; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; - const int contentY = hintGutterHeight; - - const int titleX = - contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_BOOKMARKS), EpdFontFamily::BOLD)) / 2; - renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_BOOKMARKS), true, EpdFontFamily::BOLD); +void EpubReaderBookmarkListActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} +void EpubReaderBookmarkListActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + screen.setContentMargin(fui::Insets{ + static_cast(safe.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), + static_cast(renderer.getScreenWidth() - safe.x - safe.width), + static_cast(renderer.getScreenHeight() - safe.y - safe.height), static_cast(safe.x)}); + screen.spacer(static_cast(metrics.verticalSpacing)); if (bookmarks.empty()) { - renderer.drawCenteredText(UI_10_FONT_ID, LIST_START_Y + contentY + 20, tr(STR_NO_BOOKMARKS)); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); + screen.centeredText(tr(STR_NO_BOOKMARKS), screen.theme().bodyText); return; } - - const int pageItems = getPageItems(); - const int total = static_cast(bookmarks.size()); - const int pageStartIndex = (selectedIndex / pageItems) * pageItems; - const int marginLeft = contentX + 20; - - for (int i = 0; i < pageItems; i++) { - const int itemIndex = pageStartIndex + i; - if (itemIndex >= total) break; - - const int rowY = LIST_START_Y + contentY + i * ROW_HEIGHT; - const bool isSelected = (itemIndex == selectedIndex); - - if (isSelected) { - renderer.fillRect(contentX, rowY, contentWidth - 1, ROW_HEIGHT, true); - } - - const Bookmark& bm = bookmarks[itemIndex]; - const char* chapter = (bm.chapterTitle[0] != '\0') ? bm.chapterTitle : tr(STR_UNKNOWN_CHAPTER); - const bool hasSnippet = bm.snippet[0] != '\0'; - if (hasSnippet) { - const std::string snippetTrunc = renderer.truncatedText(UI_10_FONT_ID, bm.snippet, contentWidth - 40); - renderer.drawText(UI_10_FONT_ID, marginLeft, rowY + 5, snippetTrunc.c_str(), !isSelected); + std::vector values(bookmarks.size()); + std::vector items; + items.reserve(bookmarks.size()); + for (size_t i = 0; i < bookmarks.size(); ++i) { + const Bookmark& bookmark = bookmarks[i]; + values[i] = std::to_string(static_cast(std::lround(bookmark.progress * 100.0))) + "%"; + fui::ListItem item; + item.label = bookmark.snippet[0] != '\0' + ? bookmark.snippet + : (bookmark.chapterTitle[0] != '\0' ? bookmark.chapterTitle : tr(STR_UNKNOWN_CHAPTER)); + if (bookmark.snippet[0] != '\0') { + item.subtitle = bookmark.chapterTitle[0] != '\0' ? bookmark.chapterTitle : tr(STR_UNKNOWN_CHAPTER); } - - const std::string chapterTrunc = renderer.truncatedText(SMALL_FONT_ID, chapter, contentWidth - 40); - renderer.drawText(SMALL_FONT_ID, marginLeft, rowY + (hasSnippet ? 27 : 10), chapterTrunc.c_str(), !isSelected); - - char pageBuf[24]; - snprintf(pageBuf, sizeof(pageBuf), "%d%%", static_cast(std::lround(bm.progress * 100.0))); - renderer.drawText(SMALL_FONT_ID, marginLeft, rowY + (hasSnippet ? 47 : 36), pageBuf, !isSelected); + item.value = values[i].c_str(); + item.actionValue = static_cast(i); + items.push_back(item); } + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.valueInset = 8; + const fui::Rect bounds = screen.body(); + listTop = bounds.y; + listBottom = bounds.bottom(); + const auto rows = configureUiList(props, screen.theme(), bounds, UiListRowType::WithSubtitle); + listRowHeight = props.rowHeight; + listRowStep = props.rowHeight + props.rowGap; + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(bookmarks.size())); + props.topIndex = static_cast(topIndex); + screen.list(props); +} - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); +void EpubReaderBookmarkListActivity::render(RenderLock&&) { + renderer.clearScreen(); + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header{safe.x, safe.y + metrics.topPadding, safe.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_BOOKMARKS), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_BOOKMARKS), nullptr, true); + } + uiReady = false; + app.render(); + uiReady = true; + if (confirmPopup.processRender(renderer, mappedInput)) return; + const auto labels = + mappedInput.mapLabels(tr(STR_BACK), bookmarks.empty() ? "" : tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); } diff --git a/src/activities/reader/EpubReaderBookmarkListActivity.h b/src/activities/reader/EpubReaderBookmarkListActivity.h index 67e4b3976e..37e44ac6a3 100644 --- a/src/activities/reader/EpubReaderBookmarkListActivity.h +++ b/src/activities/reader/EpubReaderBookmarkListActivity.h @@ -1,16 +1,20 @@ #pragma once +#include +#include + +#include #include #include "../Activity.h" #include "BookmarkStore.h" +#include "components/OptionPopup.h" #include "util/ButtonNavigator.h" class EpubReaderBookmarkListActivity final : public Activity { public: - explicit EpubReaderBookmarkListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::vector& bookmarks) - : Activity("EpubReaderBookmarkList", renderer, mappedInput), bookmarks(bookmarks) {} + EpubReaderBookmarkListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::vector& bookmarks); void onEnter() override; void onExit() override; @@ -22,10 +26,25 @@ class EpubReaderBookmarkListActivity final : public Activity { private: std::vector bookmarks; int selectedIndex = 0; - bool longPressConfirmHandled = false; + bool confirmingDelete = false; ButtonNavigator buttonNavigator; + OptionPopup confirmPopup; + using UiApp = freeink::ui::FreeInkApp<20, 4>; + freeink::ui::GfxRendererTarget uiTarget; + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + int listTop = 0; + int listBottom = 0; + int listRowHeight = 0; + int listRowStep = 0; + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); + void selectBookmark(); void deleteSelectedBookmark(); - void showBookmarkActionMenu(bool ignoreInitialConfirmRelease = false); - int getPageItems() const; + void showBookmarkDeletePopup(); }; diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 45da6d1329..936c339ab3 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -4,94 +4,184 @@ #include #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" -#include "fontIds.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" + +namespace fui = freeink::ui; + +namespace { +constexpr fui::ActionId ACTION_ROW = 1; +} + +EpubReaderChapterSelectionActivity::EpubReaderChapterSelectionActivity(GfxRenderer& renderer, + MappedInputManager& mappedInput, + const std::shared_ptr& epub, + const std::string& epubPath, + const int currentSpineIndex) + : Activity("EpubReaderChapterSelection", renderer, mappedInput), + epub(epub), + epubPath(epubPath), + currentSpineIndex(currentSpineIndex), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); } void EpubReaderChapterSelectionActivity::onEnter() { Activity::onEnter(); - - if (!epub) { - return; - } + mappedInput.setReaderTouchscreenOverride(true); + if (!epub) return; selectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); - if (selectorIndex == -1) { - selectorIndex = 0; - } - - // Trigger first update + if (selectorIndex < 0) selectorIndex = 0; + topIndex = 0; + visibleRows = 1; + initialViewportPending = true; + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &EpubReaderChapterSelectionActivity::onRowEvent, this); + app.setScreen(&EpubReaderChapterSelectionActivity::chapterScreen, this); requestUpdate(); } -void EpubReaderChapterSelectionActivity::onExit() { Activity::onExit(); } +void EpubReaderChapterSelectionActivity::onExit() { + mappedInput.setReaderTouchscreenOverride(false); + Activity::onExit(); +} + +void EpubReaderChapterSelectionActivity::selectChapter() { + const auto tocItem = epub->getTocItem(selectorIndex); + if (tocItem.spineIndex < 0) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + } else { + setResult(ChapterResult{tocItem.spineIndex, tocItem.anchor}); + } + finish(); +} + +void EpubReaderChapterSelectionActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= self->getTotalItems()) return; + self->selectorIndex = event.value; + self->app.clearTapFlash(); + self->selectChapter(); +} void EpubReaderChapterSelectionActivity::loop() { - const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); const int totalItems = getTotalItems(); - - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - const auto tocItem = epub->getTocItem(selectorIndex); - if (tocItem.spineIndex == -1) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); - } else { - setResult(ChapterResult{tocItem.spineIndex, tocItem.anchor}); - finish(); - } - } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header{safe.x, safe.y + metrics.topPadding, safe.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (TouchHeaderBackButton::wasTapped(mappedInput, header) || + mappedInput.wasReleased(MappedInputManager::Button::Back)) { ActivityResult result; result.isCancelled = true; setResult(std::move(result)); finish(); + return; } - buttonNavigator.onNextRelease([this, totalItems] { - selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); - requestUpdate(); - }); + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } - buttonNavigator.onPreviousRelease([this, totalItems] { - selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); - requestUpdate(); - }); + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, totalItems); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } - buttonNavigator.onNextContinuous([this, totalItems, pageItems] { - selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); + const auto moveSelection = [this, totalItems](const int index) { + selectorIndex = index; + topIndex = followListSelection(selectorIndex, topIndex, visibleRows, totalItems); requestUpdate(); + }; + buttonNavigator.onNextRelease( + [this, totalItems, &moveSelection] { moveSelection(ButtonNavigator::nextIndex(selectorIndex, totalItems)); }); + buttonNavigator.onPreviousRelease( + [this, totalItems, &moveSelection] { moveSelection(ButtonNavigator::previousIndex(selectorIndex, totalItems)); }); + buttonNavigator.onNextContinuous([this, totalItems, &moveSelection] { + moveSelection(ButtonNavigator::nextPageIndex(selectorIndex, totalItems, visibleRows)); }); - - buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { - selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); - requestUpdate(); + buttonNavigator.onPreviousContinuous([this, totalItems, &moveSelection] { + moveSelection(ButtonNavigator::previousPageIndex(selectorIndex, totalItems, visibleRows)); }); -} - -void EpubReaderChapterSelectionActivity::render(RenderLock&&) { - renderer.clearScreen(); - auto metrics = UITheme::getInstance().getMetrics(); - Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) selectChapter(); +} - GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, - tr(STR_SELECT_CHAPTER), nullptr, true); +void EpubReaderChapterSelectionActivity::chapterScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildChapterScreen(screen); +} - const int contentTop = screen.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; - const int contentHeight = screen.height - contentTop - metrics.verticalSpacing; +void EpubReaderChapterSelectionActivity::buildChapterScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + screen.setContentMargin(fui::Insets{ + static_cast(safe.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), + static_cast(renderer.getScreenWidth() - safe.x - safe.width), + static_cast(renderer.getScreenHeight() - safe.y - safe.height), static_cast(safe.x)}); + screen.spacer(static_cast(metrics.verticalSpacing)); const int totalItems = getTotalItems(); - GUI.drawList(renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, totalItems, selectorIndex, - [this](int index) { - auto item = epub->getTocItem(index); - std::string indent((item.level - 1) * 2, ' '); - return indent + item.title; - }); + std::vector labels(totalItems); + std::vector items; + items.reserve(totalItems); + for (int i = 0; i < totalItems; ++i) { + const auto item = epub->getTocItem(i); + labels[i] = std::string((item.level - 1) * 2, ' ') + item.title; + fui::ListItem row; + row.label = labels[i].c_str(); + row.actionValue = static_cast(i); + items.push_back(row); + } + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectorIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.labelText = screen.theme().bodyText; + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = initialViewportPending ? followListSelection(selectorIndex, 0, visibleRows, totalItems) + : scrollListBy(topIndex, 0, visibleRows, totalItems); + initialViewportPending = false; + props.topIndex = static_cast(topIndex); + screen.list(props); +} + +void EpubReaderChapterSelectionActivity::render(RenderLock&&) { + renderer.clearScreen(); + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header{safe.x, safe.y + metrics.topPadding, safe.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_SELECT_CHAPTER), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_SELECT_CHAPTER), nullptr, true); + } + uiReady = false; + app.render(); + uiReady = true; const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); } diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.h b/src/activities/reader/EpubReaderChapterSelectionActivity.h index 792049a0be..bb1786db45 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.h +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.h @@ -1,33 +1,41 @@ #pragma once #include +#include +#include +#include #include #include "activities/Activity.h" #include "util/ButtonNavigator.h" class EpubReaderChapterSelectionActivity final : public Activity { + using UiApp = freeink::ui::FreeInkApp<20, 4>; + std::shared_ptr epub; std::string epubPath; ButtonNavigator buttonNavigator; int currentSpineIndex = 0; int selectorIndex = 0; + freeink::ui::GfxRendererTarget uiTarget; + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + bool initialViewportPending = true; - // Number of items that fit on a page, derived from logical screen height. - // This adapts automatically when switching between portrait and landscape. - int getPageItems() const; + static void chapterScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildChapterScreen(UiApp::ScreenType& screen); + void selectChapter(); // Total TOC items count int getTotalItems() const; public: - explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::shared_ptr& epub, const std::string& epubPath, - const int currentSpineIndex) - : Activity("EpubReaderChapterSelection", renderer, mappedInput), - epub(epub), - epubPath(epubPath), - currentSpineIndex(currentSpineIndex) {} + EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::shared_ptr& epub, const std::string& epubPath, + int currentSpineIndex); void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/reader/EpubReaderClippingListActivity.cpp b/src/activities/reader/EpubReaderClippingListActivity.cpp index 922afaa5a4..341394654d 100644 --- a/src/activities/reader/EpubReaderClippingListActivity.cpp +++ b/src/activities/reader/EpubReaderClippingListActivity.cpp @@ -9,18 +9,34 @@ #include "MappedInputManager.h" #include "activities/ActivityResult.h" #include "activities/home/FileBrowserActionActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" +namespace fui = freeink::ui; + namespace { -constexpr int ROW_HEIGHT = 56; -constexpr int LIST_START_Y = 60; +constexpr fui::ActionId ACTION_ROW = 1; constexpr int DETAIL_START_Y = 70; constexpr int DETAIL_SIDE_MARGIN = 20; constexpr int DETAIL_BOTTOM_RESERVE = 55; constexpr int DETAIL_LINE_GAP = 6; +constexpr int TOUCH_DETAIL_BUTTON_HEIGHT = 52; +constexpr int TOUCH_DETAIL_PAGE_LABEL_RESERVE = 24; constexpr unsigned long CLIPPING_DELETE_HOLD_MS = 1000; +Rect clippingHeaderRect(const Rect& safe, const ThemeMetrics& metrics, const MappedInputManager& mappedInput) { + return Rect{safe.x, safe.y + metrics.topPadding, safe.width, TouchHeaderBackButton::height(metrics, mappedInput)}; +} + +Rect touchDetailOpenButtonRect(const Rect& safe, const ThemeMetrics& metrics) { + const int sidePadding = std::min(metrics.contentSidePadding, std::max(0, safe.width / 2 - 1)); + return Rect{safe.x + sidePadding, safe.y + safe.height - metrics.verticalSpacing - TOUCH_DETAIL_BUTTON_HEIGHT, + std::max(1, safe.width - sidePadding * 2), TOUCH_DETAIL_BUTTON_HEIGHT}; +} + bool isUtf8SpaceAt(const std::string& text, const size_t index, size_t& advance) { const auto c = static_cast(text[index]); if (c == 0xC2 && index + 1 < text.size() && static_cast(text[index + 1]) == 0xA0) { @@ -143,37 +159,45 @@ void buildWrappedDetailLines(const GfxRenderer& renderer, const int fontId, cons } } // namespace -int EpubReaderClippingListActivity::getPageItems() const { - const auto orientation = renderer.getOrientation(); - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; - const int startY = LIST_START_Y + hintGutterHeight; - const int available = renderer.getScreenHeight() - startY - ROW_HEIGHT; - return std::max(1, available / ROW_HEIGHT); -} +EpubReaderClippingListActivity::EpubReaderClippingListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("EpubClippingList", renderer, mappedInput), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} void EpubReaderClippingListActivity::onEnter() { Activity::onEnter(); selectedIndex = 0; + topIndex = 0; + visibleRows = 1; + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &EpubReaderClippingListActivity::onRowEvent, this); + app.setScreen(&EpubReaderClippingListActivity::listScreen, this); detailText.reserve(CLIPPING_TEXT_MAX); detailLines.reserve(32); + uiItems.resize(CLIPPINGS.clippingCount()); requestUpdate(); } int EpubReaderClippingListActivity::getDetailTextWidth() const { - const auto orientation = renderer.getOrientation(); - const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; - const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; - const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; - return std::max(1, renderer.getScreenWidth() - hintGutterWidth - DETAIL_SIDE_MARGIN * 2); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + return std::max(1, safe.width - DETAIL_SIDE_MARGIN * 2); } int EpubReaderClippingListActivity::getDetailLinesPerPage() const { - const auto orientation = renderer.getOrientation(); - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const auto& metrics = UITheme::getInstance().getMetrics(); const int lineStep = renderer.getLineHeight(UI_10_FONT_ID) + DETAIL_LINE_GAP; - const int available = renderer.getScreenHeight() - (DETAIL_START_Y + hintGutterHeight) - DETAIL_BOTTOM_RESERVE; +#if CROSSINK_APP_CAP_TOUCH + if (mappedInput.hasTouchHardware()) { + const Rect header = clippingHeaderRect(safe, metrics, mappedInput); + const Rect openButton = touchDetailOpenButtonRect(safe, metrics); + const int textStart = header.y + header.height + metrics.verticalSpacing; + const int textBottom = openButton.y - metrics.verticalSpacing - TOUCH_DETAIL_PAGE_LABEL_RESERVE; + return std::max(1, (textBottom - textStart) / std::max(1, lineStep)); + } +#endif + const int available = safe.height - DETAIL_START_Y - DETAIL_BOTTOM_RESERVE; return std::max(1, available / std::max(1, lineStep)); } @@ -193,18 +217,24 @@ void EpubReaderClippingListActivity::closeDetail() { } void EpubReaderClippingListActivity::jumpToSelectedClipping() { - if (clippings.empty() || selectedIndex < 0 || selectedIndex >= static_cast(clippings.size())) return; + if (selectedIndex < 0 || selectedIndex >= static_cast(CLIPPINGS.clippingCount())) return; - const Clipping& clipping = clippings[selectedIndex]; - setResult(ClippingJumpResult{clipping.spineIndex, clipping.startPage, clipping.pageCount, clipping.paragraphIndex, + const Clipping* clipping = CLIPPINGS.clippingAt(static_cast(selectedIndex)); + if (!clipping) return; + setResult(ClippingJumpResult{clipping->spineIndex, clipping->startPage, clipping->pageCount, clipping->paragraphIndex, static_cast(selectedIndex)}); finish(); } void EpubReaderClippingListActivity::openSelectedDetail() { - if (clippings.empty() || selectedIndex < 0 || selectedIndex >= static_cast(clippings.size())) return; + if (selectedIndex < 0 || selectedIndex >= static_cast(CLIPPINGS.clippingCount())) return; - buildOneLineSnippetText(clippings[selectedIndex].text, detailText); + std::string text; + text.reserve(CLIPPING_TEXT_MAX); + if (!CLIPPINGS.readClippingText(static_cast(selectedIndex), text)) { + text.clear(); + } + buildOneLineSnippetText(text, detailText); detailMode = true; detailPage = 0; detailLayoutWidth = 0; @@ -226,7 +256,7 @@ void EpubReaderClippingListActivity::rebuildDetailLayoutIfNeeded() { } void EpubReaderClippingListActivity::deleteSelectedClipping() { - if (clippings.empty() || selectedIndex < 0 || selectedIndex >= static_cast(clippings.size())) return; + if (selectedIndex < 0 || selectedIndex >= static_cast(CLIPPINGS.clippingCount())) return; if (!CLIPPINGS.removeClippingAt(static_cast(selectedIndex))) return; @@ -235,20 +265,26 @@ void EpubReaderClippingListActivity::deleteSelectedClipping() { detailLines.clear(); detailLayoutWidth = 0; detailLinesPerPage = 0; - clippings = CLIPPINGS.getClippings(); - if (clippings.empty()) { + if (CLIPPINGS.clippingCount() == 0) { selectedIndex = 0; - } else if (selectedIndex >= static_cast(clippings.size())) { - selectedIndex = static_cast(clippings.size()) - 1; + } else if (selectedIndex >= static_cast(CLIPPINGS.clippingCount())) { + selectedIndex = static_cast(CLIPPINGS.clippingCount()) - 1; } + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, static_cast(CLIPPINGS.clippingCount())); + uiItems.resize(CLIPPINGS.clippingCount()); requestUpdate(); } void EpubReaderClippingListActivity::showClippingActionMenu(const bool ignoreInitialConfirmRelease) { - if (clippings.empty() || selectedIndex < 0 || selectedIndex >= static_cast(clippings.size())) return; - - const Clipping selectedClipping = clippings[selectedIndex]; - const char* title = selectedClipping.chapterTitle[0] != '\0' ? selectedClipping.chapterTitle : tr(STR_CLIPPINGS); + if (selectedIndex < 0 || selectedIndex >= static_cast(CLIPPINGS.clippingCount())) return; + + const Clipping* selectedClipping = CLIPPINGS.clippingAt(static_cast(selectedIndex)); + if (!selectedClipping) return; + const char* title = selectedClipping->chapterTitle[0] != '\0' ? selectedClipping->chapterTitle : tr(STR_CLIPPINGS); + const uint16_t selectedSpineIndex = selectedClipping->spineIndex; + const uint16_t selectedStartPage = selectedClipping->startPage; + const uint16_t selectedStartWordIndex = selectedClipping->startWordIndex; + const uint32_t selectedTimestamp = selectedClipping->timestamp; std::vector items; items.reserve(1); items.push_back({FileBrowserAction::Delete, StrId::STR_DELETE}); @@ -256,7 +292,8 @@ void EpubReaderClippingListActivity::showClippingActionMenu(const bool ignoreIni startActivityForResult( std::make_unique(renderer, mappedInput, title, std::move(items), ignoreInitialConfirmRelease), - [this, selectedClipping](const ActivityResult& result) { + [this, selectedSpineIndex, selectedStartPage, selectedStartWordIndex, + selectedTimestamp](const ActivityResult& result) { longPressConfirmHandled = false; if (result.isCancelled) { requestUpdate(); @@ -269,22 +306,35 @@ void EpubReaderClippingListActivity::showClippingActionMenu(const bool ignoreIni return; } - const auto it = std::find_if(clippings.begin(), clippings.end(), [&selectedClipping](const Clipping& clipping) { - return clipping.spineIndex == selectedClipping.spineIndex && - clipping.startPage == selectedClipping.startPage && - clipping.startWordIndex == selectedClipping.startWordIndex && - clipping.timestamp == selectedClipping.timestamp; - }); - if (it != clippings.end()) { - selectedIndex = static_cast(std::distance(clippings.begin(), it)); - deleteSelectedClipping(); - } else { - requestUpdate(); + for (size_t i = 0; i < CLIPPINGS.clippingCount(); ++i) { + const Clipping* clipping = CLIPPINGS.clippingAt(i); + if (!clipping) continue; + if (clipping->spineIndex == selectedSpineIndex && clipping->startPage == selectedStartPage && + clipping->startWordIndex == selectedStartWordIndex && clipping->timestamp == selectedTimestamp) { + selectedIndex = static_cast(i); + deleteSelectedClipping(); + return; + } } + requestUpdate(); }); } void EpubReaderClippingListActivity::loop() { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header = clippingHeaderRect(safe, metrics, mappedInput); + if (TouchHeaderBackButton::wasTapped(mappedInput, header)) { + if (detailMode) { + closeDetail(); + } else { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + } + return; + } if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { if (detailMode) { closeDetail(); @@ -297,7 +347,8 @@ void EpubReaderClippingListActivity::loop() { return; } - if (!clippings.empty() && !longPressConfirmHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && + if (CLIPPINGS.clippingCount() > 0 && !longPressConfirmHandled && + mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= CLIPPING_DELETE_HOLD_MS) { longPressConfirmHandled = true; showClippingActionMenu(true); @@ -309,7 +360,8 @@ void EpubReaderClippingListActivity::loop() { longPressConfirmHandled = false; return; } - if (!clippings.empty() && selectedIndex >= 0 && selectedIndex < static_cast(clippings.size())) { + if (CLIPPINGS.clippingCount() > 0 && selectedIndex >= 0 && + selectedIndex < static_cast(CLIPPINGS.clippingCount())) { if (detailMode) { jumpToSelectedClipping(); } else { @@ -319,12 +371,45 @@ void EpubReaderClippingListActivity::loop() { return; } - const int total = static_cast(clippings.size()); + const int total = static_cast(CLIPPINGS.clippingCount()); if (total == 0) return; if (detailMode) { + int touchX = 0; + int touchY = 0; + int detailTouchTop = safe.y + DETAIL_START_Y; + int detailTouchBottom = safe.y + safe.height - DETAIL_BOTTOM_RESERVE; +#if CROSSINK_APP_CAP_TOUCH + if (mappedInput.hasTouchHardware()) { + const Rect openButton = touchDetailOpenButtonRect(safe, metrics); + if (mappedInput.wasTapInRect(openButton.x, openButton.y, openButton.width, openButton.height)) { + jumpToSelectedClipping(); + return; + } + detailTouchTop = header.y + header.height + metrics.verticalSpacing; + detailTouchBottom = openButton.y; + } +#endif + if (!longPressConfirmHandled && mappedInput.isScreenTouchLongPress(touchX, touchY, CLIPPING_DELETE_HOLD_MS) && + touchY >= detailTouchTop && touchY < detailTouchBottom) { + mappedInput.suppressNextTouchTap(); + longPressConfirmHandled = true; + showClippingActionMenu(false); + return; + } rebuildDetailLayoutIfNeeded(); const int detailPageCount = getDetailPageCount(); + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up && detailPage < detailPageCount - 1) { + detailPage++; + requestUpdate(); + return; + } + if (swipe == MappedInputManager::SwipeDir::Down && detailPage > 0) { + detailPage--; + requestUpdate(); + return; + } buttonNavigator.onNextRelease([this, detailPageCount] { if (detailPage < detailPageCount - 1) { detailPage++; @@ -352,56 +437,157 @@ void EpubReaderClippingListActivity::loop() { return; } - const int pageItems = getPageItems(); - buttonNavigator.onNextRelease([this, total] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, total); - requestUpdate(); - }); - buttonNavigator.onPreviousRelease([this, total] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, total); - requestUpdate(); - }); - buttonNavigator.onNextContinuous([this, total, pageItems] { - selectedIndex = ButtonNavigator::nextPageIndex(selectedIndex, total, pageItems); + int tx = 0; + int ty = 0; + if (!longPressConfirmHandled && mappedInput.isScreenTouchLongPress(tx, ty, CLIPPING_DELETE_HOLD_MS) && + listRowStep > 0 && ty >= listTop && ty < listBottom) { + const int offset = ty - listTop; + const int row = offset / listRowStep; + const int touchedIndex = topIndex + row; + if (row < visibleRows && offset % listRowStep < listRowHeight && touchedIndex < total) { + selectedIndex = touchedIndex; + mappedInput.suppressNextTouchTap(); + longPressConfirmHandled = true; + showClippingActionMenu(false); + } + return; + } + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } + + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int next = scrollListBy(topIndex, swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows, + visibleRows, total); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + + const auto moveSelection = [this, total](const int next) { + selectedIndex = next; + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, total); requestUpdate(); + }; + buttonNavigator.onNextRelease( + [this, total, &moveSelection] { moveSelection(ButtonNavigator::nextIndex(selectedIndex, total)); }); + buttonNavigator.onPreviousRelease( + [this, total, &moveSelection] { moveSelection(ButtonNavigator::previousIndex(selectedIndex, total)); }); + buttonNavigator.onNextContinuous([this, total, &moveSelection] { + moveSelection(ButtonNavigator::nextPageIndex(selectedIndex, total, visibleRows)); }); - buttonNavigator.onPreviousContinuous([this, total, pageItems] { - selectedIndex = ButtonNavigator::previousPageIndex(selectedIndex, total, pageItems); - requestUpdate(); + buttonNavigator.onPreviousContinuous([this, total, &moveSelection] { + moveSelection(ButtonNavigator::previousPageIndex(selectedIndex, total, visibleRows)); }); } +void EpubReaderClippingListActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(CLIPPINGS.clippingCount())) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->openSelectedDetail(); +} + +void EpubReaderClippingListActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} + +void EpubReaderClippingListActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + screen.setContentMargin(fui::Insets{ + static_cast(safe.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), + static_cast(renderer.getScreenWidth() - safe.x - safe.width), + static_cast(renderer.getScreenHeight() - safe.y - safe.height), static_cast(safe.x)}); + screen.spacer(static_cast(metrics.verticalSpacing)); + const size_t count = CLIPPINGS.clippingCount(); + if (count == 0) { + screen.centeredText(tr(STR_NO_CLIPPINGS), screen.theme().bodyText); + return; + } + fui::ListProps props; + props.items = uiItems.data(); + props.count = static_cast(uiItems.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + const fui::Rect bounds = screen.body(); + listTop = bounds.y; + listBottom = bounds.bottom(); + const auto rows = configureUiList(props, screen.theme(), bounds, UiListRowType::WithSubtitle); + listRowHeight = props.rowHeight; + listRowStep = props.rowHeight + props.rowGap; + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(count)); + props.topIndex = static_cast(topIndex); + const int end = std::min(static_cast(count), topIndex + visibleRows); + for (int i = topIndex; i < end; ++i) { + const size_t slot = static_cast(i - topIndex); + uiRawText[slot].clear(); + CLIPPINGS.readClippingText(static_cast(i), uiRawText[slot]); + buildOneLineSnippetText(uiRawText[slot], uiLabels[slot]); + const Clipping* clipping = CLIPPINGS.clippingAt(static_cast(i)); + fui::ListItem& item = uiItems[static_cast(i)]; + item = fui::ListItem{}; + item.label = uiLabels[slot].c_str(); + if (clipping) item.subtitle = clipping->chapterTitle[0] != '\0' ? clipping->chapterTitle : tr(STR_UNKNOWN_CHAPTER); + item.actionValue = static_cast(i); + } + screen.list(props); +} + void EpubReaderClippingListActivity::renderDetail() { rebuildDetailLayoutIfNeeded(); - const auto pageWidth = renderer.getScreenWidth(); - const auto orientation = renderer.getOrientation(); - const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; - const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; - const int contentX = isLandscapeCw ? hintGutterWidth : 0; - const int contentWidth = pageWidth - hintGutterWidth; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; - const int contentY = hintGutterHeight; + // Front-button hints move to a different physical edge for each reader + // orientation. Keep all detail content inside the same safe area used by + // the list view so it cannot render beneath that hint band. + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const int contentX = safe.x; + const int contentWidth = safe.width; + const int contentY = safe.y; + const auto& metrics = UITheme::getInstance().getMetrics(); const char* chapter = tr(STR_CLIPPINGS); - if (!clippings.empty() && selectedIndex >= 0 && selectedIndex < static_cast(clippings.size()) && - clippings[selectedIndex].chapterTitle[0] != '\0') { - chapter = clippings[selectedIndex].chapterTitle; + const Clipping* selectedClipping = + selectedIndex >= 0 ? CLIPPINGS.clippingAt(static_cast(selectedIndex)) : nullptr; + if (selectedClipping && selectedClipping->chapterTitle[0] != '\0') { + chapter = selectedClipping->chapterTitle; } - const std::string title = - renderer.truncatedText(UI_12_FONT_ID, chapter, contentWidth - DETAIL_SIDE_MARGIN * 2, EpdFontFamily::BOLD); - const int titleX = - contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, title.c_str(), EpdFontFamily::BOLD)) / 2; - renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, title.c_str(), true, EpdFontFamily::BOLD); + int textStartY = DETAIL_START_Y + contentY; +#if CROSSINK_APP_CAP_TOUCH + const bool showTouchControls = mappedInput.hasTouchHardware(); + Rect openButton{}; + if (showTouchControls) { + const Rect header = clippingHeaderRect(safe, metrics, mappedInput); + TouchHeaderBackButton::draw(renderer, uiTarget, header, chapter, true); + textStartY = header.y + header.height + metrics.verticalSpacing; + openButton = touchDetailOpenButtonRect(safe, metrics); + } else +#endif + { + const std::string title = + renderer.truncatedText(UI_12_FONT_ID, chapter, contentWidth - DETAIL_SIDE_MARGIN * 2, EpdFontFamily::BOLD); + const int titleX = + contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, title.c_str(), EpdFontFamily::BOLD)) / 2; + renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, title.c_str(), true, EpdFontFamily::BOLD); + } const int lineStep = renderer.getLineHeight(UI_10_FONT_ID) + DETAIL_LINE_GAP; const int textX = contentX + DETAIL_SIDE_MARGIN; const int firstLine = detailPage * detailLinesPerPage; const int lastLine = std::min(static_cast(detailLines.size()), firstLine + detailLinesPerPage); - int y = DETAIL_START_Y + contentY; + int y = textStartY; for (int i = firstLine; i < lastLine; i++) { renderer.drawText(UI_10_FONT_ID, textX, y, detailLines[i].c_str()); y += lineStep; @@ -412,9 +598,28 @@ void EpubReaderClippingListActivity::renderDetail() { char pageBuf[16]; snprintf(pageBuf, sizeof(pageBuf), "%d/%d", detailPage + 1, detailPageCount); const int pageLabelWidth = renderer.getTextWidth(SMALL_FONT_ID, pageBuf); - renderer.drawText(SMALL_FONT_ID, contentX + contentWidth - DETAIL_SIDE_MARGIN - pageLabelWidth, - renderer.getScreenHeight() - 35, pageBuf); + int pageLabelY = safe.y + safe.height - 35; +#if CROSSINK_APP_CAP_TOUCH + if (showTouchControls) { + pageLabelY = openButton.y - metrics.verticalSpacing - renderer.getLineHeight(SMALL_FONT_ID); + } +#endif + renderer.drawText(SMALL_FONT_ID, contentX + contentWidth - DETAIL_SIDE_MARGIN - pageLabelWidth, pageLabelY, + pageBuf); + } + +#if CROSSINK_APP_CAP_TOUCH + if (showTouchControls) { + renderer.fillRectDither(openButton.x, openButton.y, openButton.width, openButton.height, Color::White); + renderer.drawRect(openButton.x, openButton.y, openButton.width, openButton.height, true); + const char* label = tr(STR_OPEN); + const int labelX = + openButton.x + (openButton.width - renderer.getTextWidth(UI_10_FONT_ID, label, EpdFontFamily::BOLD)) / 2; + const int labelY = openButton.y + (openButton.height - renderer.getLineHeight(UI_10_FONT_ID)) / 2; + renderer.drawText(UI_10_FONT_ID, labelX, labelY, label, true, EpdFontFamily::BOLD); + return; } +#endif const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_OPEN), detailPage > 0 ? tr(STR_DIR_UP) : "", detailPage < detailPageCount - 1 ? tr(STR_DIR_DOWN) : ""); @@ -424,66 +629,26 @@ void EpubReaderClippingListActivity::renderDetail() { void EpubReaderClippingListActivity::render(RenderLock&&) { renderer.clearScreen(); - const auto pageWidth = renderer.getScreenWidth(); - const auto orientation = renderer.getOrientation(); - const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; - const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; - const int contentX = isLandscapeCw ? hintGutterWidth : 0; - const int contentWidth = pageWidth - hintGutterWidth; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; - const int contentY = hintGutterHeight; - - if (clippings.empty()) { - const int titleX = - contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_CLIPPINGS), EpdFontFamily::BOLD)) / 2; - renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_CLIPPINGS), true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, LIST_START_Y + contentY + 20, tr(STR_NO_CLIPPINGS)); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); - return; - } - if (detailMode) { renderDetail(); renderer.displayBuffer(); return; } - const int titleX = - contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_CLIPPINGS), EpdFontFamily::BOLD)) / 2; - renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_CLIPPINGS), true, EpdFontFamily::BOLD); - - const int pageItems = getPageItems(); - const int total = static_cast(clippings.size()); - const int pageStartIndex = (selectedIndex / pageItems) * pageItems; - const int marginLeft = contentX + 20; - std::string snippetText; - snippetText.reserve(CLIPPING_TEXT_MAX); - - for (int i = 0; i < pageItems; i++) { - const int itemIndex = pageStartIndex + i; - if (itemIndex >= total) break; - - const int rowY = LIST_START_Y + contentY + i * ROW_HEIGHT; - const bool isSelected = itemIndex == selectedIndex; - if (isSelected) { - renderer.fillRect(contentX, rowY, contentWidth - 1, ROW_HEIGHT, true); - } - - const Clipping& clipping = clippings[itemIndex]; - buildOneLineSnippetText(clipping.text, snippetText); - const std::string snippetTrunc = renderer.truncatedText(UI_10_FONT_ID, snippetText.c_str(), contentWidth - 40); - renderer.drawText(UI_10_FONT_ID, marginLeft, rowY + 5, snippetTrunc.c_str(), !isSelected); - - const char* chapter = clipping.chapterTitle[0] != '\0' ? clipping.chapterTitle : tr(STR_UNKNOWN_CHAPTER); - const std::string chapterTrunc = renderer.truncatedText(SMALL_FONT_ID, chapter, contentWidth - 40); - renderer.drawText(SMALL_FONT_ID, marginLeft, rowY + 31, chapterTrunc.c_str(), !isSelected); + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header = clippingHeaderRect(safe, metrics, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_CLIPPINGS), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_CLIPPINGS), nullptr, true); } + uiReady = false; + app.render(); + uiReady = true; - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_OPEN), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), CLIPPINGS.clippingCount() == 0 ? "" : tr(STR_OPEN), + tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); renderer.displayBuffer(); } diff --git a/src/activities/reader/EpubReaderClippingListActivity.h b/src/activities/reader/EpubReaderClippingListActivity.h index 962b1283c5..d13802955e 100644 --- a/src/activities/reader/EpubReaderClippingListActivity.h +++ b/src/activities/reader/EpubReaderClippingListActivity.h @@ -1,5 +1,10 @@ #pragma once +#include +#include + +#include +#include #include #include @@ -9,16 +14,13 @@ class EpubReaderClippingListActivity final : public Activity { public: - EpubReaderClippingListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - std::vector clippings) - : Activity("EpubClippingList", renderer, mappedInput), clippings(std::move(clippings)) {} + EpubReaderClippingListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput); void onEnter() override; void loop() override; void render(RenderLock&&) override; private: - std::vector clippings; std::string detailText; std::vector detailLines; ButtonNavigator buttonNavigator; @@ -28,8 +30,23 @@ class EpubReaderClippingListActivity final : public Activity { int detailLinesPerPage = 0; bool longPressConfirmHandled = false; bool detailMode = false; + using UiApp = freeink::ui::FreeInkApp<20, 4>; + freeink::ui::GfxRendererTarget uiTarget; + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + int listTop = 0; + int listBottom = 0; + int listRowHeight = 0; + int listRowStep = 0; + std::vector uiItems; + std::array uiRawText; + std::array uiLabels; - int getPageItems() const; + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); int getDetailTextWidth() const; int getDetailLinesPerPage() const; int getDetailPageCount() const; diff --git a/src/activities/reader/EpubReaderFootnotesActivity.cpp b/src/activities/reader/EpubReaderFootnotesActivity.cpp index e636c705d1..3d649ef6a9 100644 --- a/src/activities/reader/EpubReaderFootnotesActivity.cpp +++ b/src/activities/reader/EpubReaderFootnotesActivity.cpp @@ -3,109 +3,151 @@ #include #include -#include - #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" -#include "fontIds.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" + +namespace fui = freeink::ui; +namespace { +constexpr fui::ActionId ACTION_ROW = 1; +} + +EpubReaderFootnotesActivity::EpubReaderFootnotesActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::vector& footnotes) + : Activity("EpubReaderFootnotes", renderer, mappedInput), + footnotes(footnotes), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} void EpubReaderFootnotesActivity::onEnter() { Activity::onEnter(); selectedIndex = 0; + topIndex = 0; + visibleRows = 1; + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &EpubReaderFootnotesActivity::onRowEvent, this); + app.setScreen(&EpubReaderFootnotesActivity::listScreen, this); requestUpdate(); } void EpubReaderFootnotesActivity::onExit() { Activity::onExit(); } +void EpubReaderFootnotesActivity::selectFootnote() { + if (selectedIndex < 0 || selectedIndex >= static_cast(footnotes.size())) return; + setResult(FootnoteResult{footnotes[selectedIndex].href}); + finish(); +} + +void EpubReaderFootnotesActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(self->footnotes.size())) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->selectFootnote(); +} + void EpubReaderFootnotesActivity::loop() { - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header{safe.x, safe.y + metrics.topPadding, safe.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (TouchHeaderBackButton::wasTapped(mappedInput, header) || + mappedInput.wasReleased(MappedInputManager::Button::Back)) { ActivityResult result; result.isCancelled = true; setResult(std::move(result)); finish(); return; } - + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || mappedInput.wasReleased(MappedInputManager::Button::Power)) { - if (selectedIndex >= 0 && selectedIndex < static_cast(footnotes.size())) { - setResult(FootnoteResult{footnotes[selectedIndex].href}); - finish(); - } + selectFootnote(); return; } - - buttonNavigator.onNext([this] { - if (!footnotes.empty()) { - selectedIndex = (selectedIndex + 1) % footnotes.size(); + const int count = static_cast(footnotes.size()); + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int next = scrollListBy(topIndex, swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows, + visibleRows, count); + if (next != topIndex) { + topIndex = next; requestUpdate(); } - }); - - buttonNavigator.onPrevious([this] { - if (!footnotes.empty()) { - selectedIndex = (selectedIndex - 1 + footnotes.size()) % footnotes.size(); - requestUpdate(); - } - }); + return; + } + const auto move = [this, count](const int next) { + selectedIndex = next; + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, count); + requestUpdate(); + }; + buttonNavigator.onNext([this, count, &move] { move(ButtonNavigator::nextIndex(selectedIndex, count)); }); + buttonNavigator.onPrevious([this, count, &move] { move(ButtonNavigator::previousIndex(selectedIndex, count)); }); } -void EpubReaderFootnotesActivity::render(RenderLock&&) { - renderer.clearScreen(); - - const auto pageWidth = renderer.getScreenWidth(); - const auto orientation = renderer.getOrientation(); - // Landscape orientation: reserve a horizontal gutter for button hints. - const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; - const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; - // Inverted portrait: reserve vertical space for hints at the top. - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; - // Landscape CW places hints on the left edge; CCW keeps them on the right. - const int contentX = isLandscapeCw ? hintGutterWidth : 0; - const int contentWidth = pageWidth - hintGutterWidth; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; - const int contentY = hintGutterHeight; - - // Manual centering to honor content gutters. - const int titleX = - contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_FOOTNOTES), EpdFontFamily::BOLD)) / 2; - renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_FOOTNOTES), true, EpdFontFamily::BOLD); +void EpubReaderFootnotesActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} +void EpubReaderFootnotesActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + screen.setContentMargin(fui::Insets{ + static_cast(safe.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), + static_cast(renderer.getScreenWidth() - safe.x - safe.width), + static_cast(renderer.getScreenHeight() - safe.y - safe.height), static_cast(safe.x)}); + screen.spacer(static_cast(metrics.verticalSpacing)); if (footnotes.empty()) { - renderer.drawCenteredText(UI_10_FONT_ID, 90 + contentY, tr(STR_NO_FOOTNOTES)); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); + screen.centeredText(tr(STR_NO_FOOTNOTES), screen.theme().bodyText); return; } - - constexpr int lineHeight = 36; - const int screenWidth = renderer.getScreenWidth(); - const int marginLeft = contentX + 20; - - const int visibleCount = std::max(1, (renderer.getScreenHeight() - contentY) / lineHeight); - if (selectedIndex < scrollOffset) scrollOffset = selectedIndex; - if (selectedIndex >= scrollOffset + visibleCount) scrollOffset = selectedIndex - visibleCount + 1; - - for (int i = scrollOffset; i < static_cast(footnotes.size()) && i < scrollOffset + visibleCount; i++) { - const int y = 60 + contentY + (i - scrollOffset) * lineHeight; - const bool isSelected = (i == selectedIndex); - - if (isSelected) { - renderer.fillRect(0, y, screenWidth, lineHeight, true); - } - - // Show footnote number and abbreviated href - std::string label = footnotes[i].number; - if (label.empty()) { - label = tr(STR_LINK); - } - renderer.drawText(UI_10_FONT_ID, marginLeft, y + 4, label.c_str(), !isSelected); + std::vector items; + items.reserve(footnotes.size()); + for (size_t i = 0; i < footnotes.size(); ++i) { + fui::ListItem item; + item.label = footnotes[i].number[0] == '\0' ? tr(STR_LINK) : footnotes[i].number; + item.actionValue = static_cast(i); + items.push_back(item); } + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.labelText = screen.theme().bodyText; + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(footnotes.size())); + props.topIndex = static_cast(topIndex); + screen.list(props); +} - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); +void EpubReaderFootnotesActivity::render(RenderLock&&) { + renderer.clearScreen(); + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header{safe.x, safe.y + metrics.topPadding, safe.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_FOOTNOTES), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_FOOTNOTES), nullptr, true); + } + uiReady = false; + app.render(); + uiReady = true; + const auto labels = mappedInput.mapLabels(tr(STR_BACK), footnotes.empty() ? "" : tr(STR_SELECT), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); } diff --git a/src/activities/reader/EpubReaderFootnotesActivity.h b/src/activities/reader/EpubReaderFootnotesActivity.h index d760b69f88..ae2134ff78 100644 --- a/src/activities/reader/EpubReaderFootnotesActivity.h +++ b/src/activities/reader/EpubReaderFootnotesActivity.h @@ -1,7 +1,10 @@ #pragma once #include +#include +#include +#include #include #include #include @@ -11,9 +14,8 @@ class EpubReaderFootnotesActivity final : public Activity { public: - explicit EpubReaderFootnotesActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::vector& footnotes) - : Activity("EpubReaderFootnotes", renderer, mappedInput), footnotes(footnotes) {} + EpubReaderFootnotesActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::vector& footnotes); void onEnter() override; void onExit() override; @@ -25,6 +27,16 @@ class EpubReaderFootnotesActivity final : public Activity { private: const std::vector& footnotes; int selectedIndex = 0; - int scrollOffset = 0; ButtonNavigator buttonNavigator; + using UiApp = freeink::ui::FreeInkApp<20, 4>; + freeink::ui::GfxRendererTarget uiTarget; + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); + void selectFootnote(); }; diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index bfd289c44c..01399f06d1 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -1,5 +1,6 @@ #include "EpubReaderMenuActivity.h" +#include #include #include @@ -7,31 +8,63 @@ #include #include "ClippingStore.h" +#include "CrossInkHalFrontlight.h" #include "CrossPointSettings.h" #include "EpubReaderClippingListActivity.h" #include "MappedInputManager.h" +#include "ReaderUtils.h" +#include "components/TouchHeaderBackButton.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" -#include "components/icons/settings2.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" +namespace fui = freeink::ui; + namespace { -constexpr uint8_t MenuIcon24[] = { - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, - 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, - 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, - 0xf3, 0xe7, 0xcf, 0xf3, 0xe7, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; -static_assert(sizeof(MenuIcon24) == 24 * ((24 + 7) / 8), "MenuIcon24 must contain 24 rows of 1-bit icon data"); +constexpr fui::ActionId ACTION_ROW = 1; constexpr int tabIconSize = 24; constexpr int selectedTabBoxWidth = 50; constexpr int selectedTabBoxHeight = 34; constexpr int selectedTabBoxRadius = 2; +constexpr int headerActionHitSize = 44; +constexpr int headerActionTouchSize = 60; +constexpr int headerActionGap = 10; +constexpr int headerActionRightPadding = 10; +constexpr int inlineBatteryReserve = 72; +constexpr int touchReaderMenuRowHeightScale = 2; +constexpr int touchReaderMenuTabBarHeightScale = 2; +int readerMenuRowHeightScale(const bool hasTouch) { return hasTouch ? touchReaderMenuRowHeightScale : 1; } + +int readerMenuTabBarHeight(const int baseTabBarHeight, const bool hasTouch) { + return baseTabBarHeight * (hasTouch ? touchReaderMenuTabBarHeightScale : 1); +} + +bool readerMenuTabsAtBottom(const MappedInputManager& mappedInput) { + // Frontlight boards reserve the top-edge down-swipe for the quick panel, so + // the reader menu opens from the bottom and its tabs should stay thumb-close. + return mappedInput.hasTouch() && Frontlight.present(); +} + +Rect readerMenuHeaderRect(const GfxRenderer& renderer, const MappedInputManager& mappedInput) { + const Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, !mappedInput.hasTouch(), false); + const auto& metrics = UITheme::getInstance().getMetrics(); + return Rect{screen.x, screen.y + metrics.topPadding, screen.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; +} + +bool rectContains(const Rect& rect, const int x, const int y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} struct ReaderLayoutSettingsSnapshot { uint8_t fontFamily; - uint8_t fontSize; + uint8_t readerFontPointSize; uint8_t lineHeightPercent; + uint8_t wordSpacing; uint8_t orientation; uint8_t screenMargin; uint8_t publisherPageNumbers; @@ -46,16 +79,18 @@ struct ReaderLayoutSettingsSnapshot { uint8_t bionicReadingEnabled; uint8_t guideReadingEnabled; uint8_t epubRenderMode; + // Indexing method is a build policy, not a layout input. A mode-only change + // keeps the live section/parser and takes effect when the next chapter opens. char sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName)] = {}; bool operator==(const ReaderLayoutSettingsSnapshot& other) const { - return fontFamily == other.fontFamily && fontSize == other.fontSize && - lineHeightPercent == other.lineHeightPercent && orientation == other.orientation && - screenMargin == other.screenMargin && publisherPageNumbers == other.publisherPageNumbers && - paragraphAlignment == other.paragraphAlignment && embeddedStyle == other.embeddedStyle && - hyphenationEnabled == other.hyphenationEnabled && textAntiAliasing == other.textAntiAliasing && - readerDarkMode == other.readerDarkMode && imageRendering == other.imageRendering && - extraParagraphSpacing == other.extraParagraphSpacing && + return fontFamily == other.fontFamily && readerFontPointSize == other.readerFontPointSize && + lineHeightPercent == other.lineHeightPercent && wordSpacing == other.wordSpacing && + orientation == other.orientation && screenMargin == other.screenMargin && + publisherPageNumbers == other.publisherPageNumbers && paragraphAlignment == other.paragraphAlignment && + embeddedStyle == other.embeddedStyle && hyphenationEnabled == other.hyphenationEnabled && + textAntiAliasing == other.textAntiAliasing && readerDarkMode == other.readerDarkMode && + imageRendering == other.imageRendering && extraParagraphSpacing == other.extraParagraphSpacing && forceParagraphIndents == other.forceParagraphIndents && bionicReadingEnabled == other.bionicReadingEnabled && guideReadingEnabled == other.guideReadingEnabled && epubRenderMode == other.epubRenderMode && std::strncmp(sdFontFamilyName, other.sdFontFamilyName, sizeof(sdFontFamilyName)) == 0; @@ -65,23 +100,12 @@ struct ReaderLayoutSettingsSnapshot { ReaderLayoutSettingsSnapshot captureReaderLayoutSettings() { ReaderLayoutSettingsSnapshot snapshot{ - SETTINGS.fontFamily, - SETTINGS.fontSize, - SETTINGS.lineHeightPercent, - SETTINGS.orientation, - SETTINGS.screenMargin, - SETTINGS.publisherPageNumbers, - SETTINGS.paragraphAlignment, - SETTINGS.embeddedStyle, - SETTINGS.hyphenationEnabled, - SETTINGS.textAntiAliasing, - SETTINGS.readerDarkMode, - SETTINGS.imageRendering, - SETTINGS.extraParagraphSpacing, - SETTINGS.forceParagraphIndents, - SETTINGS.bionicReadingEnabled, - SETTINGS.guideReadingEnabled, - SETTINGS.epubRenderMode, + SETTINGS.fontFamily, SETTINGS.readerFontPointSize, SETTINGS.lineHeightPercent, + SETTINGS.wordSpacing, SETTINGS.orientation, SETTINGS.screenMargin, + SETTINGS.publisherPageNumbers, SETTINGS.paragraphAlignment, SETTINGS.embeddedStyle, + SETTINGS.hyphenationEnabled, SETTINGS.textAntiAliasing, SETTINGS.readerDarkMode, + SETTINGS.imageRendering, SETTINGS.extraParagraphSpacing, SETTINGS.forceParagraphIndents, + SETTINGS.bionicReadingEnabled, SETTINGS.guideReadingEnabled, SETTINGS.epubRenderMode, }; std::strncpy(snapshot.sdFontFamilyName, SETTINGS.sdFontFamilyName, sizeof(snapshot.sdFontFamilyName) - 1); snapshot.sdFontFamilyName[sizeof(snapshot.sdFontFamilyName) - 1] = '\0'; @@ -105,27 +129,33 @@ void drawBookmarkTabIcon(const GfxRenderer& renderer, int x, int y, const bool f renderer.fillPolygon(polyX, polyY, 5, foregroundBlack); } -void drawReaderMenuBitmapIcon(const GfxRenderer& renderer, const uint8_t bitmap[], const int x, const int y, - const int width, const int height, const bool foregroundBlack = true) { - if (bitmap == nullptr || width <= 0 || height <= 0) { - return; - } +Rect readerMenuHeaderActionRect(const Rect& header, const ThemeMetrics& metrics) { + const int actionHeight = std::min(header.height, headerActionHitSize); + // Compact headers keep the battery inline at the right edge, so leave its + // widest percentage-and-glyph band untouched. Tall detached headers place + // Home in the lower title band, as in the touch-menu design. + const int rightInset = (metrics.headerBatteryDetached ? 0 : inlineBatteryReserve) + headerActionRightPadding; + return Rect{header.x + header.width - rightInset - headerActionHitSize, header.y + header.height - actionHeight, + headerActionHitSize, actionHeight}; +} - const int stride = (width + 7) / 8; - for (int row = 0; row < height; ++row) { - const int srcOffset = row * stride; - for (int col = 0; col < width; ++col) { - const uint8_t mask = static_cast(0x80 >> (col & 7)); - if ((bitmap[srcOffset + (col >> 3)] & mask) != 0) { - continue; - } - - // Icon assets are authored for the legacy portrait blitter. Keep that - // source rotation, but route placement through logical coordinates so - // landscape and inverted reader menus keep the tabs centered. - renderer.drawPixel(x + width - 1 - row, y + col, foregroundBlack); - } - } +Rect readerMenuHeaderActionTouchRect(const Rect& header, const Rect& actionRect) { + const int touchWidth = std::min(headerActionTouchSize, header.width); + const int touchHeight = std::min(headerActionTouchSize, header.height); + // Keep the expanded target clear of the right-side battery reserve. The + // visual icon stays centered in actionRect; only the tappable area grows. + return Rect{actionRect.x + actionRect.width - touchWidth, actionRect.y + actionRect.height - touchHeight, touchWidth, + touchHeight}; +} + +void drawSdkIcon(fui::GfxRendererTarget& target, const freeink::Icon& icon, const int x, const int y, + const bool foregroundBlack = true) { + // FreeInkUI's target maps logical pixels through the renderer, keeping the + // non-pre-rotated SDK assets upright in every reader orientation. + target.bitmap(fui::Rect{static_cast(x), static_cast(y), static_cast(icon.w), + static_cast(icon.h)}, + fui::bitmapFromIcon(icon), fui::BitmapMode::Center, + fui::Paint::solid(foregroundBlack ? fui::Color::Black : fui::Color::White)); } } // namespace @@ -133,16 +163,19 @@ void drawReaderMenuBitmapIcon(const GfxRenderer& renderer, const uint8_t bitmap[ EpubReaderMenuActivity::EpubReaderMenuActivity( GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, const uint8_t currentOrientation, const bool hasFootnotes, - const bool hasBookmarks, const bool hasClippings, const bool isCurrentPageBookmarked, const bool isBookCompleted, - const bool autoPageTurnActive, const uint16_t autoPageTurnIntervalSeconds, const bool showReadingPaceReset, - ReaderOptionsActivity::SaveSettingsCallback saveReaderSettingsCallback, void* saveReaderSettingsContext, - ReaderOptionsActivity::SaveGlobalSettingsCallback saveGlobalSettingsCallback, void* saveGlobalSettingsContext, - ReaderOptionsActivity::GlobalSettingsEditCallback beginGlobalSettingsEditCallback, + const bool hasDictionary, const bool hasBookmarks, const bool hasClippings, const bool isCurrentPageBookmarked, + const bool isBookCompleted, const bool autoPageTurnActive, const uint16_t autoPageTurnIntervalSeconds, + const bool showReadingPaceReset, ReaderOptionsActivity::SaveSettingsCallback saveReaderSettingsCallback, + void* saveReaderSettingsContext, ReaderOptionsActivity::SaveGlobalSettingsCallback saveGlobalSettingsCallback, + void* saveGlobalSettingsContext, ReaderOptionsActivity::GlobalSettingsEditCallback beginGlobalSettingsEditCallback, void* beginGlobalSettingsEditContext, const bool stablePageNumbersAvailable, - ReaderOptionsActivity::GlobalSettingsEditCallback endGlobalSettingsEditCallback, void* endGlobalSettingsEditContext) + ReaderOptionsActivity::GlobalSettingsEditCallback endGlobalSettingsEditCallback, void* endGlobalSettingsEditContext, + const char* dictionaryFontFamilyName, const uint8_t dictionaryFontPointSize, const bool hasDictionaryFontOverride, + ReaderOptionsActivity::DictionaryFontChangedCallback dictionaryFontChangedCallback, + void* dictionaryFontChangedContext) : Activity("EpubReaderMenu", renderer, mappedInput), menuItems(buildMenuItems(hasFootnotes, hasBookmarks, hasClippings, isCurrentPageBookmarked, isBookCompleted, - showReadingPaceReset)), + showReadingPaceReset, hasDictionary)), title(title), pendingOrientation(currentOrientation), currentPage(currentPage), @@ -158,25 +191,37 @@ EpubReaderMenuActivity::EpubReaderMenuActivity( beginGlobalSettingsEditContext(beginGlobalSettingsEditContext), stablePageNumbersAvailable(stablePageNumbersAvailable), endGlobalSettingsEditCallback(endGlobalSettingsEditCallback), - endGlobalSettingsEditContext(endGlobalSettingsEditContext) {} + endGlobalSettingsEditContext(endGlobalSettingsEditContext), + dictionaryFontPointSize(dictionaryFontPointSize), + hasDictionaryFontOverride(hasDictionaryFontOverride), + dictionaryFontChangedCallback(dictionaryFontChangedCallback), + dictionaryFontChangedContext(dictionaryFontChangedContext), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) { + if (dictionaryFontFamilyName) { + std::strncpy(this->dictionaryFontFamilyName, dictionaryFontFamilyName, sizeof(this->dictionaryFontFamilyName) - 1); + } +} -EpubReaderMenuActivity::TabMenuItems EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasBookmarks, - bool hasClippings, - bool isCurrentPageBookmarked, - bool isBookCompleted, - bool showReadingPaceReset) { +EpubReaderMenuActivity::TabMenuItems EpubReaderMenuActivity::buildMenuItems( + bool hasFootnotes, bool hasBookmarks, bool hasClippings, bool isCurrentPageBookmarked, bool isBookCompleted, + bool showReadingPaceReset, bool hasDictionary) { TabMenuItems items; auto& mainItems = items[MAIN_TAB_INDEX]; auto& bookmarkItems = items[BOOKMARKS_TAB_INDEX]; auto& settingsItems = items[SETTINGS_TAB_INDEX]; - mainItems.reserve(8 + (hasFootnotes ? 1u : 0u)); - bookmarkItems.reserve(8 + (hasBookmarks ? 2u : 0u) + (hasClippings ? 1u : 0u)); - settingsItems.reserve(2 + (showReadingPaceReset ? 1u : 0u)); + mainItems.reserve(9 + (hasFootnotes ? 1u : 0u) + (hasDictionary ? 2u : 0u)); + bookmarkItems.reserve(9 + (hasBookmarks ? 2u : 0u) + (hasClippings ? 1u : 0u)); + settingsItems.reserve(3 + (showReadingPaceReset ? 1u : 0u)); if (hasFootnotes) { mainItems.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); } + if (hasDictionary) { + mainItems.push_back({MenuAction::LOOKUP, StrId::STR_LOOKUP}); + mainItems.push_back({MenuAction::LOOKUP_HISTORY, StrId::STR_LOOKUP_HISTORY}); + } mainItems.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); mainItems.push_back({MenuAction::READER_OPTIONS, StrId::STR_READER_OPTIONS}); mainItems.push_back({MenuAction::CONTROLS_OPTIONS, StrId::STR_CAT_CONTROLS}); @@ -185,9 +230,6 @@ EpubReaderMenuActivity::TabMenuItems EpubReaderMenuActivity::buildMenuItems(bool mainItems.push_back({MenuAction::READING_STATS, StrId::STR_READING_STATS}); mainItems.push_back( {MenuAction::TOGGLE_COMPLETED, isBookCompleted ? StrId::STR_MARK_UNFINISHED : StrId::STR_MARK_FINISHED}); - - bookmarkItems.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}); - bookmarkItems.push_back({MenuAction::NEARBY_POSITION_SYNC, StrId::STR_NEARBY_POSITION_SYNC}); bookmarkItems.push_back({MenuAction::SAVE_CLIPPING, StrId::STR_SAVE_CLIPPING}); if (hasClippings) { bookmarkItems.push_back({MenuAction::VIEW_CLIPPINGS, StrId::STR_VIEW_CLIPPINGS}); @@ -198,25 +240,60 @@ EpubReaderMenuActivity::TabMenuItems EpubReaderMenuActivity::buildMenuItems(bool bookmarkItems.push_back({MenuAction::VIEW_BOOKMARKS, StrId::STR_VIEW_BOOKMARKS}); bookmarkItems.push_back({MenuAction::DELETE_BOOKMARKS, StrId::STR_DELETE_BOOKMARKS}); } + bookmarkItems.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}); + bookmarkItems.push_back({MenuAction::NEARBY_POSITION_SYNC, StrId::STR_NEARBY_POSITION_SYNC}); + bookmarkItems.push_back({MenuAction::SEND_NEARBY_BOOK, StrId::STR_SEND_NEARBY_BOOK}); bookmarkItems.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); bookmarkItems.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); settingsItems.push_back({MenuAction::DELETE_STATS, StrId::STR_DELETE_BOOK_STATS}); settingsItems.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}); + settingsItems.push_back({MenuAction::SET_BOOK_DICTIONARY, StrId::STR_BOOK_DICTIONARY}); if (showReadingPaceReset) { settingsItems.push_back({MenuAction::RESET_READING_PACE, StrId::STR_RESET_READING_PACE}); } return items; } +void EpubReaderMenuActivity::dictionaryFontChangedForMenu(void* ctx, const char* familyName, const uint8_t pointSize) { + auto* self = static_cast(ctx); + if (!self) return; + + if (familyName && familyName[0] != '\0') { + self->hasDictionaryFontOverride = true; + std::strncpy(self->dictionaryFontFamilyName, familyName, sizeof(self->dictionaryFontFamilyName) - 1); + self->dictionaryFontFamilyName[sizeof(self->dictionaryFontFamilyName) - 1] = '\0'; + } else { + self->hasDictionaryFontOverride = false; + std::strncpy(self->dictionaryFontFamilyName, SETTINGS.dictionarySdFontFamilyName, + sizeof(self->dictionaryFontFamilyName) - 1); + self->dictionaryFontFamilyName[sizeof(self->dictionaryFontFamilyName) - 1] = '\0'; + self->dictionaryFontPointSize = SETTINGS.dictionaryFontPointSize; + } + if (self->hasDictionaryFontOverride) { + self->dictionaryFontPointSize = pointSize; + } + if (self->dictionaryFontChangedCallback) { + self->dictionaryFontChangedCallback(self->dictionaryFontChangedContext, + self->hasDictionaryFontOverride ? self->dictionaryFontFamilyName : nullptr, + self->dictionaryFontPointSize); + } +} + const std::vector& EpubReaderMenuActivity::activeMenuItems() const { return menuItems[activeTabIndex()]; } -void EpubReaderMenuActivity::focusTabRow() { selectedIndex = -1; } +void EpubReaderMenuActivity::focusTabRow() { + selectedIndex = -1; + topIndex = 0; +} -void EpubReaderMenuActivity::cycleActiveTab() { - const auto nextTabIndex = ButtonNavigator::nextIndex(static_cast(activeTabIndex()), MENU_TAB_COUNT); +void EpubReaderMenuActivity::cycleActiveTab() { moveActiveTab(true); } + +void EpubReaderMenuActivity::moveActiveTab(const bool forward) { + const int nextTabIndex = forward ? ButtonNavigator::nextIndex(static_cast(activeTabIndex()), MENU_TAB_COUNT) + : ButtonNavigator::previousIndex(static_cast(activeTabIndex()), MENU_TAB_COUNT); activeTab = static_cast(nextTabIndex); focusTabRow(); requestUpdate(); @@ -230,17 +307,147 @@ void EpubReaderMenuActivity::finishCancelled() { finish(); } -void EpubReaderMenuActivity::drawIconTabBar(const Rect rect) const { +bool EpubReaderMenuActivity::activateSelectedItem() { + if (selectedIndex < 0) { + cycleActiveTab(); + return true; + } + + const auto& items = activeMenuItems(); + if (selectedIndex >= static_cast(items.size())) { + focusTabRow(); + requestUpdate(); + return true; + } + + const auto selectedAction = items[selectedIndex].action; + if (selectedAction == MenuAction::ROTATE_SCREEN) { + optionPopup.show(StrId::STR_ORIENTATION, orientationLabels.data(), static_cast(orientationLabels.size()), + pendingOrientation, [this](int idx) { + pendingOrientation = idx; + // Rotate the menu immediately while leaving the saved + // reader orientation for the result handler to apply. + ReaderUtils::applyOrientation(renderer, pendingOrientation); + app.setDevice(uiTarget.deviceContext()); + requestUpdate(true); + }); + requestUpdate(); + return true; + } + + if (selectedAction == MenuAction::READER_OPTIONS) { + const auto before = captureReaderLayoutSettings(); + startActivityForResult(std::make_unique( + renderer, mappedInput, saveReaderSettingsCallback, saveReaderSettingsContext, + saveGlobalSettingsCallback, saveGlobalSettingsContext, beginGlobalSettingsEditCallback, + beginGlobalSettingsEditContext, endGlobalSettingsEditCallback, + endGlobalSettingsEditContext, stablePageNumbersAvailable, dictionaryFontFamilyName, + dictionaryFontPointSize, hasDictionaryFontOverride, dictionaryFontChangedForMenu, this), + [this, before](const ActivityResult& result) { + settingsChanged = settingsChanged || haveReaderLayoutSettingsChanged(before); + pendingOrientation = SETTINGS.orientation; // sync in case orientation changed + if (result.isCancelled) { + finishCancelled(); + return; + } + requestUpdate(); + }); + return true; + } + + if (selectedAction == MenuAction::CONTROLS_OPTIONS) { + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult&) { + ActivityResult result; + result.isCancelled = true; + result.data = MenuResult{-1, pendingOrientation, settingsChanged}; + setResult(std::move(result)); + finish(); + }); + return true; + } + + if (selectedAction == MenuAction::VIEW_CLIPPINGS) { + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { + if (result.isCancelled) { + requestUpdate(); + return; + } + + const auto* clipping = std::get_if(&result.data); + if (clipping == nullptr) { + requestUpdate(); + return; + } + + ClippingJumpResult menuResult = *clipping; + menuResult.orientation = pendingOrientation; + menuResult.settingsChanged = settingsChanged; + setResult(std::move(menuResult)); + finish(); + }); + return true; + } + + setResult(MenuResult{static_cast(selectedAction), pendingOrientation, settingsChanged}); + finish(); + return true; +} + +bool EpubReaderMenuActivity::handleTouchInput() { + int tabIndex = -1; + if (mappedInput.wasTabTapped(tabIndex) && tabIndex >= 0) { + if (mappedInput.hasTouchHardware() && tabIndex == static_cast(TOUCH_LOCK_ICON_INDEX)) { + SETTINGS.disableReaderTouchscreen = SETTINGS.disableReaderTouchscreen ? 0 : 1; + SETTINGS.saveToFile(); + requestUpdate(); + return true; + } + if (mappedInput.hasTouchHardware() && tabIndex == static_cast(TOUCH_HOME_ICON_INDEX)) { + setResult(MenuResult{static_cast(MenuAction::GO_HOME), pendingOrientation, settingsChanged}); + finish(); + return true; + } + if (tabIndex < static_cast(MENU_TAB_COUNT)) { + activeTab = static_cast(tabIndex); + focusTabRow(); + requestUpdate(); + return true; + } + } + return false; +} + +void EpubReaderMenuActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + const auto& items = self->activeMenuItems(); + if (self->optionPopup.isActive() || event.value < 0 || event.value >= static_cast(items.size())) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->activateSelectedItem(); +} + +void EpubReaderMenuActivity::drawIconTabBar(const Rect rect, const bool drawBottomBorder) { renderer.drawLine(rect.x, rect.y, rect.x + rect.width - 1, rect.y, true); - renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width - 1, rect.y + rect.height - 1, true); + if (drawBottomBorder) { + renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width - 1, rect.y + rect.height - 1, true); + } - for (size_t i = 0; i < MENU_TAB_COUNT; i++) { - const int slotX = rect.x + static_cast((i * rect.width) / MENU_TAB_COUNT); - const int nextSlotX = rect.x + static_cast(((i + 1) * rect.width) / MENU_TAB_COUNT); +#if CROSSINK_APP_CAP_TOUCH + const size_t iconCount = mappedInput.hasTouchHardware() ? TOUCH_ICON_COUNT : MENU_TAB_COUNT; +#else + constexpr size_t iconCount = MENU_TAB_COUNT; +#endif + for (size_t i = 0; i < iconCount; i++) { + const int slotX = rect.x + static_cast((i * rect.width) / iconCount); + const int nextSlotX = rect.x + static_cast(((i + 1) * rect.width) / iconCount); const int slotWidth = nextSlotX - slotX; const int centerX = slotX + slotWidth / 2; - const bool selected = i == activeTabIndex(); + const bool selected = i < MENU_TAB_COUNT && i == activeTabIndex(); const bool tabFocused = selected && selectedIndex < 0; + TouchRegistry::getInstance().add(Rect{slotX, rect.y, slotWidth, rect.height}, static_cast(i), + TouchRegistry::Tab); const int boxX = centerX - selectedTabBoxWidth / 2; const int boxY = rect.y + (rect.height - selectedTabBoxHeight) / 2; const int iconX = centerX - tabIconSize / 2; @@ -254,115 +461,109 @@ void EpubReaderMenuActivity::drawIconTabBar(const Rect rect) const { } if (i == static_cast(MenuTab::Main)) { - drawReaderMenuBitmapIcon(renderer, MenuIcon24, iconX, iconY, tabIconSize, tabIconSize, !tabFocused); + drawSdkIcon(uiTarget, icon_menu_24, iconX, iconY, !tabFocused); } else if (i == static_cast(MenuTab::Bookmarks)) { drawBookmarkTabIcon(renderer, iconX, iconY, !tabFocused); - } else { - drawReaderMenuBitmapIcon(renderer, Settings2Icon24, iconX, iconY, tabIconSize, tabIconSize, !tabFocused); + } else if (i == static_cast(MenuTab::Settings)) { + drawSdkIcon(uiTarget, icon_cog_24, iconX, iconY, !tabFocused); + } +#if CROSSINK_APP_CAP_TOUCH + else { + drawSdkIcon(uiTarget, SETTINGS.disableReaderTouchscreen ? icon_pointer_off_24 : icon_pointer_24, iconX, iconY); } +#endif } } void EpubReaderMenuActivity::onEnter() { Activity::onEnter(); + mappedInput.setReaderTouchscreenOverride(true); + uiReady = false; + visibleRows = 1; + topIndex = 0; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &EpubReaderMenuActivity::onRowEvent, this); + app.setScreen(&EpubReaderMenuActivity::menuScreen, this); requestUpdate(); } -void EpubReaderMenuActivity::onExit() { Activity::onExit(); } +void EpubReaderMenuActivity::onExit() { + mappedInput.setReaderTouchscreenOverride(false); + Activity::onExit(); +} void EpubReaderMenuActivity::loop() { if (optionPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return; + if (TouchHeaderBackButton::wasTapped(mappedInput, readerMenuHeaderRect(renderer, mappedInput))) { + finishCancelled(); + return; + } + if (handleTouchInput()) return; + if (mappedInput.wasHomeGesture()) { + finishCancelled(); + return; + } - // Handle navigation - buttonNavigator.onNextRelease([this] { - const int menuCount = static_cast(activeMenuItems().size()); - selectedIndex = ButtonNavigator::nextIndex(selectedIndex + 1, menuCount + 1) - 1; - requestUpdate(); - }); + // On X4 Pro, the top-edge down-swipe is the opposite of the reader menu's + // upward opening gesture. It dismisses before generic swipe scrolling. + if (ReaderUtils::isTouchMenuDismissGesture(mappedInput)) { + finishCancelled(); + return; + } - buttonNavigator.onPreviousRelease([this] { - const int menuCount = static_cast(activeMenuItems().size()); - selectedIndex = ButtonNavigator::previousIndex(selectedIndex + 1, menuCount + 1) - 1; - requestUpdate(); - }); + // A home-key long press toggles the reader menu: the same hold that opens it + // closes it. The SDK fires the long event once per hold, so the opening hold + // (still down as the menu appears) does not immediately re-close it — only a + // fresh press-and-hold does. + if (mappedInput.wasReaderMenuHold()) { + finishCancelled(); + return; + } - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (selectedIndex < 0) { - cycleActiveTab(); - return; + // Touch goes through the FreeInkApp: render() registered the row hit rects; + // route the snapshot and let onRowEvent dispatch. + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + // No pressed-state repaint: the render it triggers would drop a slow + // tap's release inside the uiReady window (tap-to-activate needed two + // taps), and it costs a second e-ink refresh per tap. + if (app.invalidated()) requestUpdate(); + if (event) return; // dispatched to onRowEvent } + } - const auto& items = activeMenuItems(); - if (selectedIndex >= static_cast(items.size())) { - focusTabRow(); + // Swipes scroll the viewport; the selection stays put (it may scroll + // off-screen) and button navigation pulls the view back to it. + const int menuCount = static_cast(activeMenuItems().size()); + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, menuCount); + if (next != topIndex) { + topIndex = next; requestUpdate(); - return; - } - - const auto selectedAction = items[selectedIndex].action; - if (selectedAction == MenuAction::ROTATE_SCREEN) { - optionPopup.show(StrId::STR_ORIENTATION, orientationLabels.data(), static_cast(orientationLabels.size()), - pendingOrientation, [this](int idx) { - pendingOrientation = idx; - requestUpdate(); - }); - requestUpdate(); - return; - } - - if (selectedAction == MenuAction::READER_OPTIONS) { - const auto before = captureReaderLayoutSettings(); - startActivityForResult( - std::make_unique( - renderer, mappedInput, saveReaderSettingsCallback, saveReaderSettingsContext, saveGlobalSettingsCallback, - saveGlobalSettingsContext, beginGlobalSettingsEditCallback, beginGlobalSettingsEditContext, - endGlobalSettingsEditCallback, endGlobalSettingsEditContext, stablePageNumbersAvailable), - [this, before](const ActivityResult&) { - settingsChanged = settingsChanged || haveReaderLayoutSettingsChanged(before); - pendingOrientation = SETTINGS.orientation; // sync in case orientation changed - requestUpdate(); - }); - return; - } - - if (selectedAction == MenuAction::CONTROLS_OPTIONS) { - startActivityForResult(std::make_unique(renderer, mappedInput), - [this](const ActivityResult&) { - ActivityResult result; - result.isCancelled = true; - result.data = MenuResult{-1, pendingOrientation, settingsChanged}; - setResult(std::move(result)); - finish(); - }); - return; } + return; + } - if (selectedAction == MenuAction::VIEW_CLIPPINGS) { - startActivityForResult( - std::make_unique(renderer, mappedInput, CLIPPINGS.getClippings()), - [this](const ActivityResult& result) { - if (result.isCancelled) { - requestUpdate(); - return; - } - - const auto* clipping = std::get_if(&result.data); - if (clipping == nullptr) { - requestUpdate(); - return; - } - - ClippingJumpResult menuResult = *clipping; - menuResult.orientation = pendingOrientation; - menuResult.settingsChanged = settingsChanged; - setResult(std::move(menuResult)); - finish(); - }); - return; - } + const auto moveSelection = [this, menuCount](const int index) { + selectedIndex = index - 1; + if (selectedIndex >= 0) topIndex = followListSelection(selectedIndex, topIndex, visibleRows, menuCount); + requestUpdate(); + }; + buttonNavigator.onNextRelease([this, menuCount, &moveSelection] { + moveSelection(ButtonNavigator::nextIndex(selectedIndex + 1, menuCount + 1)); + }); + buttonNavigator.onPreviousRelease([this, menuCount, &moveSelection] { + moveSelection(ButtonNavigator::previousIndex(selectedIndex + 1, menuCount + 1)); + }); + buttonNavigator.onNextContinuous([this] { moveActiveTab(true); }); + buttonNavigator.onPreviousContinuous([this] { moveActiveTab(false); }); - setResult(MenuResult{static_cast(selectedAction), pendingOrientation, settingsChanged}); - finish(); + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + activateSelectedItem(); return; } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { if (selectedIndex >= 0) { @@ -375,16 +576,84 @@ void EpubReaderMenuActivity::loop() { } } +void EpubReaderMenuActivity::menuScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildMenuScreen(screen); +} + +void EpubReaderMenuActivity::buildMenuScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, !mappedInput.hasTouch(), false); + const int tabBarHeight = readerMenuTabBarHeight(metrics.tabBarHeight, mappedInput.hasTouch()); + const bool tabsAtBottom = readerMenuTabsAtBottom(mappedInput); + const int contentTop = safe.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + + metrics.tabBarHeight + (tabsAtBottom ? 0 : tabBarHeight) + metrics.verticalSpacing; + const int contentBottom = + renderer.getScreenHeight() - (safe.y + safe.height) + (tabsAtBottom ? tabBarHeight + metrics.verticalSpacing : 0); + // The legacy header, progress band, and icon tabs remain outside the app; + // FreeInkUI owns the scalable list between them. + screen.setContentMargin(fui::Insets{static_cast(contentTop), + static_cast(renderer.getScreenWidth() - (safe.x + safe.width)), + static_cast(contentBottom), static_cast(safe.x)}); + + const auto& activeItems = activeMenuItems(); + std::vector values(activeItems.size()); + std::vector items; + items.reserve(activeItems.size()); + for (size_t i = 0; i < activeItems.size(); i++) { + const auto& menuItem = activeItems[i]; + fui::ListItem item; + item.label = I18N.get(menuItem.labelId); + if (menuItem.action == MenuAction::ROTATE_SCREEN) { + item.value = I18N.get(orientationLabels[pendingOrientation]); + } else if (menuItem.action == MenuAction::AUTO_PAGE_TURN) { + if (autoPageTurnActive) values[i] = std::to_string(autoPageTurnIntervalSeconds); + item.value = values[i].empty() ? nullptr : values[i].c_str(); + } + item.actionValue = static_cast(items.size()); + items.push_back(item); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; // physical buttons stay in loop() + props.valueInset = 8; // air between the value and the row edge + props.labelText = screen.theme().bodyText; + props.labelText.maxLines = 2; + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(activeItems.size())); // clamp to range + props.topIndex = static_cast(topIndex); + screen.list(props); +} + void EpubReaderMenuActivity::render(RenderLock&&) { if (optionPopup.processRender(renderer, mappedInput)) return; renderer.clearScreen(); auto metrics = UITheme::getInstance().getMetrics(); - Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); - - GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, - title.c_str(), nullptr, true); + const bool hasTouch = mappedInput.hasTouch(); + Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, !hasTouch, false); + const int tabBarHeight = readerMenuTabBarHeight(metrics.tabBarHeight, hasTouch); + const bool tabsAtBottom = readerMenuTabsAtBottom(mappedInput); + + // Header via GUI.drawHeader (already FreeInkUI-themed) for the battery + // indicator; the rest of the screen renders through the app. + const Rect headerRect = readerMenuHeaderRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + const Rect homeRect = readerMenuHeaderActionRect(headerRect, metrics); + const Rect homeTouchRect = readerMenuHeaderActionTouchRect(headerRect, homeRect); + const int titleRightReserve = headerRect.x + headerRect.width - homeRect.x + headerActionGap; + TouchHeaderBackButton::draw(renderer, uiTarget, headerRect, title.c_str(), true, titleRightReserve); + TouchRegistry::getInstance().add(homeTouchRect, static_cast(TOUCH_HOME_ICON_INDEX), TouchRegistry::Tab); + drawSdkIcon(uiTarget, icon_home_24, homeRect.x + (homeRect.width - tabIconSize) / 2, + homeRect.y + (homeRect.height - tabIconSize) / 2); + } else { + GUI.drawHeader(renderer, headerRect, title.c_str(), nullptr, true); + } // Progress summary std::string progressLine; @@ -393,42 +662,21 @@ void EpubReaderMenuActivity::render(RenderLock&&) { std::to_string(totalPages) + std::string(tr(STR_PAGES_SEPARATOR)); } progressLine += std::string(tr(STR_BOOK_PREFIX)) + std::to_string(bookProgressPercent) + "%"; - GUI.drawSubHeader( - renderer, - Rect{screen.x, screen.y + metrics.topPadding + metrics.headerHeight, screen.width, metrics.tabBarHeight}, - progressLine.c_str()); - - const Rect tabRect{screen.x, screen.y + metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight, - screen.width, metrics.tabBarHeight}; - drawIconTabBar(tabRect); - - const int contentTop = - screen.y + metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight * 2 + metrics.verticalSpacing; - const int contentHeight = screen.height - contentTop - metrics.verticalSpacing; - const auto& items = activeMenuItems(); + GUI.drawSubHeader(renderer, + Rect{screen.x, screen.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput), + screen.width, metrics.tabBarHeight}, + progressLine.c_str()); + + const int tabBarY = tabsAtBottom ? screen.y + screen.height - tabBarHeight + : screen.y + metrics.topPadding + + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.tabBarHeight; + const Rect tabRect{screen.x, tabBarY, screen.width, tabBarHeight}; + drawIconTabBar(tabRect, !tabsAtBottom); + + uiReady = false; + app.render(); + uiReady = true; - GUI.drawList( - renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, items.size(), selectedIndex, - [&items](int index) { return I18N.get(items[index].labelId); }, nullptr, nullptr, - [this](int index) -> std::string { - const auto& items = activeMenuItems(); - if (index < 0 || index >= static_cast(items.size())) { - return ""; - } - const auto value = items[index].action; - if (value == MenuAction::ROTATE_SCREEN) { - // Render current orientation value on the right edge of the content area. - return I18N.get(orientationLabels[pendingOrientation]); - } else if (value == MenuAction::AUTO_PAGE_TURN) { - // Render current page turn value on the right edge of the content area. - return autoPageTurnActive ? std::to_string(autoPageTurnIntervalSeconds) : ""; - } else { - return ""; - } - }, - true); - - // Footer / Hints const auto confirmLabel = selectedIndex < 0 ? tr(STR_NEXT_FIELD) : tr(STR_SELECT); const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 74feb8bf57..e992017bef 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -1,8 +1,11 @@ #pragma once #include +#include +#include #include #include +#include #include #include @@ -28,6 +31,7 @@ class EpubReaderMenuActivity final : public Activity { GO_HOME, SYNC, NEARBY_POSITION_SYNC, + SEND_NEARBY_BOOK, DELETE_STATS, DELETE_CACHE, RESET_READING_PACE, @@ -39,14 +43,17 @@ class EpubReaderMenuActivity final : public Activity { VIEW_BOOKMARKS, DELETE_BOOKMARKS, SAVE_CLIPPING, - VIEW_CLIPPINGS + VIEW_CLIPPINGS, + LOOKUP, + LOOKUP_HISTORY, + SET_BOOK_DICTIONARY }; explicit EpubReaderMenuActivity( GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, const uint8_t currentOrientation, const bool hasFootnotes, - const bool hasBookmarks, const bool hasClippings, const bool isCurrentPageBookmarked, const bool isBookCompleted, - const bool autoPageTurnActive = false, const uint16_t autoPageTurnIntervalSeconds = 0, + const bool hasDictionary, const bool hasBookmarks, const bool hasClippings, const bool isCurrentPageBookmarked, + const bool isBookCompleted, const bool autoPageTurnActive = false, const uint16_t autoPageTurnIntervalSeconds = 0, const bool showReadingPaceReset = false, ReaderOptionsActivity::SaveSettingsCallback saveReaderSettingsCallback = nullptr, void* saveReaderSettingsContext = nullptr, @@ -55,14 +62,19 @@ class EpubReaderMenuActivity final : public Activity { ReaderOptionsActivity::GlobalSettingsEditCallback beginGlobalSettingsEditCallback = nullptr, void* beginGlobalSettingsEditContext = nullptr, bool stablePageNumbersAvailable = false, ReaderOptionsActivity::GlobalSettingsEditCallback endGlobalSettingsEditCallback = nullptr, - void* endGlobalSettingsEditContext = nullptr); + void* endGlobalSettingsEditContext = nullptr, const char* dictionaryFontFamilyName = nullptr, + uint8_t dictionaryFontPointSize = 0, bool hasDictionaryFontOverride = false, + ReaderOptionsActivity::DictionaryFontChangedCallback dictionaryFontChangedCallback = nullptr, + void* dictionaryFontChangedContext = nullptr); void onEnter() override; void onExit() override; void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } + bool allowFrontlightPanelGesture() const override { return false; } bool allowPowerAsConfirmInReaderMode() const override { return true; } + bool allowGlobalHomeGesture() const override { return false; } private: struct MenuItem { @@ -75,19 +87,36 @@ class EpubReaderMenuActivity final : public Activity { static constexpr size_t BOOKMARKS_TAB_INDEX = 1; static constexpr size_t SETTINGS_TAB_INDEX = 2; static constexpr size_t MENU_TAB_COUNT = 3; + static constexpr size_t TOUCH_LOCK_ICON_INDEX = MENU_TAB_COUNT; + static constexpr size_t TOUCH_HOME_ICON_INDEX = MENU_TAB_COUNT + 1; + static constexpr size_t TOUCH_ICON_COUNT = MENU_TAB_COUNT + 1; using TabMenuItems = std::array, MENU_TAB_COUNT>; static TabMenuItems buildMenuItems(bool hasFootnotes, bool hasBookmarks, bool hasClippings, - bool isCurrentPageBookmarked, bool isBookCompleted, bool showReadingPaceReset); + bool isCurrentPageBookmarked, bool isBookCompleted, bool showReadingPaceReset, + bool hasDictionary); [[nodiscard]] const std::vector& activeMenuItems() const; [[nodiscard]] size_t activeTabIndex() const { return static_cast(activeTab); } void cycleActiveTab(); + void moveActiveTab(bool forward); void focusTabRow(); void finishCancelled(); - void drawIconTabBar(Rect rect) const; + bool activateSelectedItem(); + bool handleTouchInput(); + void drawIconTabBar(Rect rect, bool drawBottomBorder); + static void dictionaryFontChangedForMenu(void* ctx, const char* familyName, uint8_t pointSize); - // Fixed menu layout - const TabMenuItems menuItems; + // FreeInkApp hosts the menu list (themed rows, touch routing); the header + // stays on GUI.drawHeader for the battery indicator, and OptionPopup keeps + // its legacy overlay rendering. + using UiApp = freeink::ui::FreeInkApp<20, 4>; + + static void menuScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildMenuScreen(UiApp::ScreenType& screen); + + // Fixed menu layout, except for rows tied to toggles inside this menu. + TabMenuItems menuItems; int selectedIndex = -1; MenuTab activeTab = MenuTab::Main; @@ -112,5 +141,18 @@ class EpubReaderMenuActivity final : public Activity { bool stablePageNumbersAvailable = false; ReaderOptionsActivity::GlobalSettingsEditCallback endGlobalSettingsEditCallback = nullptr; void* endGlobalSettingsEditContext = nullptr; + char dictionaryFontFamilyName[64] = ""; + uint8_t dictionaryFontPointSize = 0; + bool hasDictionaryFontOverride = false; + ReaderOptionsActivity::DictionaryFontChangedCallback dictionaryFontChangedCallback = nullptr; + void* dictionaryFontChangedContext = nullptr; bool settingsChanged = false; + + freeink::ui::GfxRendererTarget uiTarget; // must precede `app`: the app holds a reference to it + UiApp app; + // render() rebuilds the app's interaction table; loop() only routes touch + // snapshots against it while this is true (the two run on different tasks). + std::atomic uiReady{false}; + int visibleRows = 1; // rows per page at the current scale; set by the screen builder + int topIndex = 0; // viewport scroll position, decoupled from the selection }; diff --git a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp index 5405a94583..bd2553a121 100644 --- a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp @@ -4,9 +4,12 @@ #include #include +#include #include +#include "DeviceCapabilities.h" #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" @@ -14,6 +17,27 @@ namespace { // Fine/coarse slider step sizes for percent adjustments. constexpr int kSmallStep = 1; constexpr int kLargeStep = 10; +constexpr int kTouchStepButtonSize = 56; +constexpr int kTouchStepButtonGap = 32; +constexpr int kTouchActionButtonWidth = 120; +constexpr int kTouchActionButtonHeight = 48; + +Rect touchStepButtonRect(const Rect& screen, const int y, const int index) { + const int totalWidth = kTouchStepButtonSize * 4 + kTouchStepButtonGap * 3; + const int x = screen.x + (screen.width - totalWidth) / 2 + index * (kTouchStepButtonSize + kTouchStepButtonGap); + return Rect{x, y, kTouchStepButtonSize, kTouchStepButtonSize}; +} + +Rect touchActionButtonRect(const Rect& screen, const bool confirm) { + constexpr int sideMargin = 46; + return Rect{confirm ? screen.x + screen.width - sideMargin - kTouchActionButtonWidth : screen.x + sideMargin, + screen.y + screen.height - kTouchActionButtonHeight - 28, kTouchActionButtonWidth, + kTouchActionButtonHeight}; +} + +bool contains(const Rect& rect, const int x, const int y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} } // namespace void EpubReaderPercentSelectionActivity::onEnter() { @@ -25,17 +49,76 @@ void EpubReaderPercentSelectionActivity::onEnter() { void EpubReaderPercentSelectionActivity::onExit() { Activity::onExit(); } void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) { - // Apply delta and clamp within 0-100. - percent += delta; - if (percent < 0) { - percent = 0; - } else if (percent > 100) { + // Wrap using a 100-value ring (0% and 100% are the same wrap point), but keep 100 as the + // natural landing value when reached without crossing the boundary (e.g. 90 + 10 = 100). + const int raw = percent + delta; + if (raw > 0 && raw % 100 == 0) { percent = 100; + } else { + percent = ((raw % 100) + 100) % 100; } requestUpdate(); } void EpubReaderPercentSelectionActivity::loop() { + auto& theme = UITheme::getInstance(); + auto metrics = theme.getMetrics(); + Rect screen = theme.getScreenSafeArea(renderer, true, false); + const int contentTop = + screen.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing * 4; + constexpr int barWidth = 360; + constexpr int barHeight = 16; + const int barX = screen.x + (screen.width - barWidth) / 2; + const int barY = contentTop + metrics.verticalSpacing * 2; + int tx = 0; + int ty = 0; + + // Live drag on the slider: once a touch lands on the bar, the percent follows the + // finger until release. Runs before the Back handler because the release of a drag + // can also register as a swipe (e.g. the left-edge rightward back gesture) — the + // drag must consume it so it can't cancel the dialog or step the percent. + if (mappedInput.isScreenTouchHeld(tx, ty)) { + if (draggingBar || + (tx >= barX - 20 && tx < barX + barWidth + 20 && ty >= barY - 24 && ty < barY + barHeight + 24)) { + draggingBar = true; + const int dragged = std::clamp((tx - barX) * 100 / barWidth, 0, 100); + if (dragged != percent) { + percent = dragged; + requestUpdate(); + } + return; + } + } else if (draggingBar) { + // Release frame of a drag: swallow the tap/swipe events it produced. + draggingBar = false; + return; + } + + const Rect header{screen.x, screen.y + metrics.topPadding, screen.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (TouchHeaderBackButton::wasTapped(mappedInput, header)) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return; + } + + if (mappedInput.hasTouch() && mappedInput.wasScreenTouchDown(tx, ty)) { + if (contains(touchActionButtonRect(screen, false), tx, ty)) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return; + } + if (contains(touchActionButtonRect(screen, true), tx, ty)) { + setResult(PercentResult{percent}); + finish(); + return; + } + } + // Back cancels, confirm selects, arrows adjust the percent. if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { ActivityResult result; @@ -45,6 +128,34 @@ void EpubReaderPercentSelectionActivity::loop() { return; } + if (mappedInput.wasScreenTapped(tx, ty)) { + if (tx >= barX - 20 && tx < barX + barWidth + 20 && ty >= barY - 24 && ty < barY + barHeight + 24) { + percent = std::clamp((tx - barX) * 100 / barWidth, 0, 100); + requestUpdate(); + return; + } + if (mappedInput.hasTouch()) { + constexpr int deltas[] = {-1, -1, 1, 1}; + for (int index = 0; index < 4; ++index) { + if (!contains(touchStepButtonRect(screen, barY + 80, index), tx, ty)) continue; + const int step = (index == 0 || index == 3) ? kLargeStep : kSmallStep; + adjustPercent(deltas[index] * step); + return; + } + return; + } + } + + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Right) { + adjustPercent(kLargeStep); + return; + } + if (swipe == MappedInputManager::SwipeDir::Left) { + adjustPercent(-kLargeStep); + return; + } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { setResult(PercentResult{percent}); finish(); @@ -54,11 +165,11 @@ void EpubReaderPercentSelectionActivity::loop() { buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustPercent(-kSmallStep); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustPercent(kSmallStep); }); - // On X3 the side buttons sit on the left/right edges of the screen rather than as a vertical up/down - // rocker (X4), so BTN_UP is physically the left button and BTN_DOWN the right one. Flip the large-step - // direction there so the left button decreases and the right button increases, matching the layout. - const int upDelta = gpio.deviceIsX3() ? -kLargeStep : kLargeStep; - const int downDelta = gpio.deviceIsX3() ? kLargeStep : -kLargeStep; + // On edge-button boards (X3, X4 Pro) the side buttons sit on the left/right edges of the screen rather + // than as a vertical up/down rocker (X4), so BTN_UP is physically the left button and BTN_DOWN the right + // one. Flip the large-step direction there so the left button decreases and the right button increases. + const int upDelta = deviceHasEdgeSideButtons(gpio) ? -kLargeStep : kLargeStep; + const int downDelta = deviceHasEdgeSideButtons(gpio) ? kLargeStep : -kLargeStep; buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this, upDelta] { adjustPercent(upDelta); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this, downDelta] { adjustPercent(downDelta); }); @@ -71,10 +182,16 @@ void EpubReaderPercentSelectionActivity::render(RenderLock&&) { auto metrics = theme.getMetrics(); Rect screen = theme.getScreenSafeArea(renderer, true, false); - GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, - tr(STR_GO_TO_PERCENT), nullptr, true); + const Rect header{screen.x, screen.y + metrics.topPadding, screen.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_GO_TO_PERCENT), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_GO_TO_PERCENT), nullptr, true); + } - const int contentTop = screen.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing * 4; + const int contentTop = + screen.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing * 4; const std::string percentText = std::to_string(percent) + "%"; UITheme::drawCenteredText(renderer, screen, UI_12_FONT_ID, contentTop, percentText.c_str(), true, @@ -98,17 +215,56 @@ void EpubReaderPercentSelectionActivity::render(RenderLock&&) { const int knobX = barX + 2 + fillWidth - 2; renderer.fillRect(knobX, barY - 4, 4, barHeight + 8, true); - // Two-line step hint built from separate label + value strings (front buttons = fine step, side - // buttons = coarse step), so the layout doesn't depend on a separator hidden in translated text. - char line[64]; - snprintf(line, sizeof(line), "%s %d%%", I18N.get(StrId::STR_STEP_HINT_FRONT), kSmallStep); - UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, barY + 30, line, true); - snprintf(line, sizeof(line), "%s %d%%", I18N.get(StrId::STR_STEP_HINT_SIDE), kLargeStep); - UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, barY + 52, line, true); - - // Button hints follow the current front button layout. - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "-", "+"); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); + if (mappedInput.hasTouch()) { + auto drawButton = [&](const Rect& rect) { + renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::White); + renderer.drawRect(rect.x, rect.y, rect.width, rect.height, true); + }; + auto drawChevron = [&](const Rect& rect, const bool pointsRight, const bool doubleChevron) { + const int centreY = rect.y + rect.height / 2; + const int firstX = rect.x + (doubleChevron ? 13 : 20); + const int chevronCount = doubleChevron ? 2 : 1; + for (int i = 0; i < chevronCount; ++i) { + const int x = firstX + i * 14; + if (pointsRight) { + renderer.drawLine(x, centreY - 12, x + 12, centreY, 2, true); + renderer.drawLine(x + 12, centreY, x, centreY + 12, 2, true); + } else { + renderer.drawLine(x + 12, centreY - 12, x, centreY, 2, true); + renderer.drawLine(x, centreY, x + 12, centreY + 12, 2, true); + } + } + }; + for (int index = 0; index < 4; ++index) { + const Rect rect = touchStepButtonRect(screen, barY + 80, index); + drawButton(rect); + drawChevron(rect, index >= 2, index == 0 || index == 3); + } + + const Rect cancelRect = touchActionButtonRect(screen, false); + const Rect confirmRect = touchActionButtonRect(screen, true); + drawButton(cancelRect); + drawButton(confirmRect); + auto drawButtonLabel = [&](const Rect& rect, const char* label) { + const int x = rect.x + (rect.width - renderer.getTextWidth(UI_10_FONT_ID, label)) / 2; + const int y = rect.y + (rect.height - renderer.getLineHeight(UI_10_FONT_ID)) / 2; + renderer.drawText(UI_10_FONT_ID, x, y, label); + }; + drawButtonLabel(cancelRect, tr(STR_CANCEL)); + drawButtonLabel(confirmRect, tr(STR_CONFIRM)); + } else { + // Two-line step hint built from separate label + value strings (front buttons = fine step, side + // buttons = coarse step), so the layout doesn't depend on a separator hidden in translated text. + char line[64]; + snprintf(line, sizeof(line), "%s %d%%", I18N.get(StrId::STR_STEP_HINT_FRONT), kSmallStep); + UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, barY + 30, line, true); + snprintf(line, sizeof(line), "%s %d%%", I18N.get(StrId::STR_STEP_HINT_SIDE), kLargeStep); + UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, barY + 52, line, true); + + // Button hints follow the current front button layout. + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "-", "+"); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); + } renderer.displayBuffer(); } diff --git a/src/activities/reader/EpubReaderPercentSelectionActivity.h b/src/activities/reader/EpubReaderPercentSelectionActivity.h index 2936df1fe0..1d1bf97844 100644 --- a/src/activities/reader/EpubReaderPercentSelectionActivity.h +++ b/src/activities/reader/EpubReaderPercentSelectionActivity.h @@ -22,6 +22,9 @@ class EpubReaderPercentSelectionActivity final : public Activity { // Current percent value (0-100) shown on the slider. int percent = 0; + // True while a touch that landed on the slider bar is being dragged. + bool draggingBar = false; + ButtonNavigator buttonNavigator; // Change the current percent by a delta and clamp within bounds. diff --git a/src/activities/reader/EpubReaderUtils.h b/src/activities/reader/EpubReaderUtils.h index 79864e3569..49a1a5e9fa 100644 --- a/src/activities/reader/EpubReaderUtils.h +++ b/src/activities/reader/EpubReaderUtils.h @@ -5,6 +5,7 @@ #include #include +#include #include namespace EpubReaderUtils { @@ -14,6 +15,8 @@ struct Progress { int pageNumber = 0; int pageCount = 0; bool hasPageCount = false; + uint32_t visibleTextOffset = 0; + bool hasVisibleTextOffset = false; }; inline bool readProgressFile(const char* moduleName, const std::string& path, Progress& progress) { @@ -26,10 +29,10 @@ inline bool readProgressFile(const char* moduleName, const std::string& path, Pr return false; } - uint8_t data[6]; + uint8_t data[10]; const int dataSize = f.read(data, sizeof(data)); f.close(); - if (dataSize != 4 && dataSize != 6) { + if (dataSize != 4 && dataSize != 6 && dataSize != 10) { LOG_ERR(moduleName, "Progress file has unexpected size: %d", dataSize); return false; } @@ -39,13 +42,18 @@ inline bool readProgressFile(const char* moduleName, const std::string& path, Pr if (progress.pageNumber == UINT16_MAX) { progress.pageNumber = 0; } - if (dataSize == 6) { + if (dataSize == 6 || dataSize == 10) { progress.pageCount = static_cast(static_cast(data[4]) | (static_cast(data[5]) << 8)); progress.hasPageCount = true; } else { progress.pageCount = 0; progress.hasPageCount = false; } + if (dataSize == 10) { + progress.visibleTextOffset = static_cast(data[6]) | (static_cast(data[7]) << 8) | + (static_cast(data[8]) << 16) | (static_cast(data[9]) << 24); + progress.hasVisibleTextOffset = true; + } return true; } @@ -64,7 +72,8 @@ inline bool loadProgress(const Epub& epub, Progress& progress, const char* modul } // Persists reader progress for an EPUB to its cache directory. Returns true on success. -inline bool saveProgress(Epub& epub, int spineIndex, int pageNumber, int pageCount) { +inline bool saveProgress(Epub& epub, int spineIndex, int pageNumber, int pageCount, + const std::optional visibleTextOffset = std::nullopt) { if (spineIndex < 0 || spineIndex > 0xFFFF || pageNumber < 0 || pageNumber > 0xFFFF || pageCount < 0 || pageCount > 0xFFFF) { LOG_ERR("ERS", "Progress values out of range: spine=%d page=%d count=%d", spineIndex, pageNumber, pageCount); @@ -84,16 +93,24 @@ inline bool saveProgress(Epub& epub, int spineIndex, int pageNumber, int pageCou LOG_ERR("ERS", "Could not open progress temp file for write!"); return false; } - uint8_t data[6]; + uint8_t data[10]; data[0] = spineIndex & 0xFF; data[1] = (spineIndex >> 8) & 0xFF; data[2] = pageNumber & 0xFF; data[3] = (pageNumber >> 8) & 0xFF; data[4] = pageCount & 0xFF; data[5] = (pageCount >> 8) & 0xFF; - const size_t written = f.write(data, sizeof(data)); - if (written != sizeof(data)) { - LOG_ERR("ERS", "Short write saving progress: %u/%u bytes", (unsigned)written, (unsigned)sizeof(data)); + size_t dataSize = 6; + if (visibleTextOffset.has_value()) { + data[6] = *visibleTextOffset & 0xFF; + data[7] = (*visibleTextOffset >> 8) & 0xFF; + data[8] = (*visibleTextOffset >> 16) & 0xFF; + data[9] = (*visibleTextOffset >> 24) & 0xFF; + dataSize = sizeof(data); + } + const size_t written = f.write(data, dataSize); + if (written != dataSize) { + LOG_ERR("ERS", "Short write saving progress: %u/%u bytes", (unsigned)written, (unsigned)dataSize); f.close(); Storage.remove(tmpPath.c_str()); return false; @@ -129,7 +146,6 @@ inline bool saveProgress(Epub& epub, int spineIndex, int pageNumber, int pageCou Storage.remove(tmpPath.c_str()); return false; } - LOG_DBG("ERS", "Progress saved: spine=%d page=%d", spineIndex, pageNumber); return true; } diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index aa9112e1de..80cae2029d 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -5,15 +5,16 @@ #include #include #include -#include #include #include #include +#include #include "CrossPointSettings.h" #include "Epub/Section.h" #include "EpubReaderUtils.h" +#include "HalClock.h" #include "KOReaderCredentialStore.h" #include "KOReaderDocumentId.h" #include "MappedInputManager.h" @@ -21,41 +22,69 @@ #include "SdCardFontSystem.h" #include "SilentRestart.h" #include "activities/ActivityManager.h" +#include "activities/home/RecentBookProgress.h" #include "activities/network/WifiSelectionActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" +#include "network/WifiUtils.h" namespace { -void syncTimeWithNTP() { - // Stop SNTP if already running (can't reconfigure while running) - if (esp_sntp_enabled()) { - esp_sntp_stop(); - } +constexpr int RESULT_LOCAL_PAGE_Y_OFFSET = 200; +constexpr int RESULT_ACTION_MARGIN_TOP = 20; +constexpr int RESULT_ACTION_HEIGHT = 48; +constexpr int RESULT_ACTION_GAP = 10; +constexpr int RESULT_NON_TOUCH_ACTION_MARGIN_TOP = 8; +constexpr int RESULT_NON_TOUCH_ACTION_HEIGHT = 40; +constexpr int RESULT_NON_TOUCH_ACTION_GAP = 8; + +struct ResultActionLayout { + Rect buttons[2]; + int rowStep; + int rowHeight; +}; + +ResultActionLayout resultActionLayout(const Rect& screen, const ThemeMetrics& metrics, const int contentTop, + const int lineHeight, const bool hasTouch) { + const int buttonX = screen.x + metrics.contentSidePadding; + const int buttonWidth = std::max(1, screen.width - metrics.contentSidePadding * 2); + const int buttonHeight = hasTouch ? RESULT_ACTION_HEIGHT : RESULT_NON_TOUCH_ACTION_HEIGHT; + const int buttonGap = hasTouch ? RESULT_ACTION_GAP : RESULT_NON_TOUCH_ACTION_GAP; + const int marginTop = hasTouch ? RESULT_ACTION_MARGIN_TOP : RESULT_NON_TOUCH_ACTION_MARGIN_TOP; + const int desiredButtonY = contentTop + RESULT_LOCAL_PAGE_Y_OFFSET + lineHeight + marginTop; + const int reservedBottom = hasTouch ? metrics.verticalSpacing : metrics.buttonHintsHeight + metrics.verticalSpacing; + const int latestButtonY = screen.y + screen.height - reservedBottom - buttonHeight * 2 - buttonGap; + const int firstButtonY = std::min(desiredButtonY, latestButtonY); + return { + {Rect{buttonX, firstButtonY, buttonWidth, buttonHeight}, + Rect{buttonX, firstButtonY + buttonHeight + buttonGap, buttonWidth, buttonHeight}}, + buttonHeight + buttonGap, + buttonHeight, + }; +} - // Configure SNTP - esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); - esp_sntp_setservername(0, "pool.ntp.org"); - esp_sntp_init(); +std::string calculateDocumentHashForMethod(const std::string& path, const DocumentMatchMethod method) { + return method == DocumentMatchMethod::FILENAME ? KOReaderDocumentId::calculateFromFilename(path) + : KOReaderDocumentId::calculate(path); +} - // Wait for time to sync (with timeout) - int retry = 0; - const int maxRetries = 50; // 5 seconds max - while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) { - vTaskDelay(100 / portTICK_PERIOD_MS); - retry++; - } +DocumentMatchMethod alternateMatchMethod(const DocumentMatchMethod method) { + return method == DocumentMatchMethod::FILENAME ? DocumentMatchMethod::BINARY : DocumentMatchMethod::FILENAME; +} - if (retry < maxRetries) { - LOG_DBG("KOSync", "NTP time synced"); - } else { - LOG_DBG("KOSync", "NTP sync timeout, using fallback"); +const char* matchMethodName(const DocumentMatchMethod method) { + return method == DocumentMatchMethod::FILENAME ? "filename" : "binary"; +} + +void syncTimeWithNTP() { +#ifndef SIMULATOR + if (!halClock.syncSystemTimeFromNTP()) { + LOG_DBG("KOSync", "NTP sync unavailable, using fallback"); } +#endif } void wifiOff() { - if (esp_sntp_enabled()) { - esp_sntp_stop(); - } WiFi.disconnect(false); delay(100); WiFi.mode(WIFI_OFF); @@ -65,7 +94,6 @@ void wifiOff() { void KOReaderSyncActivity::ensureEpubLoaded() { if (!epub) { - LOG_DBG("KOSync", "Loading epub for progress mapping (heap: %u)", (unsigned)ESP.getFreeHeap()); epub = std::make_shared(epubPath, "/.crosspoint"); epub->setupCacheDir(); // Load metadata only (no CSS needed for progress mapping, don't rebuild if cache is missing). @@ -74,10 +102,38 @@ void KOReaderSyncActivity::ensureEpubLoaded() { epub.reset(); return; } - LOG_DBG("KOSync", "Epub loaded (heap: %u)", (unsigned)ESP.getFreeHeap()); } } +bool KOReaderSyncActivity::ensureLocalProgressLoaded() { + if (!localProgressDeferred) return localProgress.valid; + + ensureEpubLoaded(); + if (!epub) return false; + + EpubReaderUtils::Progress progress; + if (EpubReaderUtils::loadProgress(*epub, progress, "KOSync")) { + currentSpineIndex = progress.spineIndex; + currentPage = progress.pageNumber; + if (progress.hasPageCount) totalPagesInSpine = std::max(1, progress.pageCount); + } + + if (currentSpineIndex < 0 || currentSpineIndex >= epub->getSpineItemsCount()) currentSpineIndex = 0; + CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine}; + if (progress.hasVisibleTextOffset) { + localPos.visibleTextOffset = progress.visibleTextOffset; + localPos.hasVisibleTextOffset = true; + } + const PositionCoordinateSpace coordinateSpace = primaryMatchMethod == DocumentMatchMethod::FILENAME + ? PositionCoordinateSpace::SourceDocument + : PositionCoordinateSpace::CurrentDocument; + localProgress = ProgressMapper::toKOReader(epub, localPos, coordinateSpace); + const int tocIdx = epub->getTocIndexForSpineIndex(currentSpineIndex); + localChapterName = tocIdx >= 0 ? epub->getTocItem(tocIdx).title : ""; + localProgressDeferred = false; + return localProgress.valid; +} + void KOReaderSyncActivity::saveProgressAndReturn(const CrossPointPosition& position) { // epub is guaranteed non-null here: ensureEpubLoaded() was called in performSync() before // SHOWING_RESULT state is entered, and this method is only called from that state. @@ -87,7 +143,9 @@ void KOReaderSyncActivity::saveProgressAndReturn(const CrossPointPosition& posit LOG_DBG("KOSync", "Adjusted remote page count before save: page=%d count=%d -> %d", position.pageNumber, position.totalPages, pageCount); } - if (!EpubReaderUtils::saveProgress(*epub, position.spineIndex, position.pageNumber, pageCount)) { + const std::optional visibleTextOffset = + position.hasVisibleTextOffset ? std::optional(position.visibleTextOffset) : std::nullopt; + if (!EpubReaderUtils::saveProgress(*epub, position.spineIndex, position.pageNumber, pageCount, visibleTextOffset)) { { RenderLock lock(*this); state = SYNC_FAILED; @@ -96,10 +154,11 @@ void KOReaderSyncActivity::saveProgressAndReturn(const CrossPointPosition& posit requestUpdate(true); return; } + RecentBookProgress::saveCachedEpubPercent(*epub, position.spineIndex, position.pageNumber, pageCount); returnToReader(); } -void KOReaderSyncActivity::returnToReader() { activityManager.goToReader(epubPath); } +void KOReaderSyncActivity::returnToReader() { activityManager.goToReader(epubPath, false, false, true); } bool KOReaderSyncActivity::consumeInitialConfirmRelease() { if (!lockInitialConfirmRelease) { @@ -113,6 +172,21 @@ bool KOReaderSyncActivity::consumeInitialConfirmRelease() { return true; } +bool KOReaderSyncActivity::smartSyncEnabled() const { + return KOREADER_STORE.getSyncBehavior() == KOReaderSyncBehavior::SMART; +} + +void KOReaderSyncActivity::markAutoReturn() { autoReturnAt = millis() + AUTO_RETURN_DELAY_MS; } + +void KOReaderSyncActivity::completeAlreadySynced() { + { + RenderLock lock(*this); + state = SYNC_COMPLETE; + } + markAutoReturn(); + requestUpdate(true); +} + void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { if (!success) { LOG_DBG("KOSync", "WiFi connection failed, exiting"); @@ -120,7 +194,6 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { return; } - LOG_DBG("KOSync", "WiFi connected, starting sync"); sdFontSystem.releaseForNetwork(renderer); { @@ -143,12 +216,9 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { } void KOReaderSyncActivity::performSync() { - // Calculate document hash based on user's preferred method - if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) { - documentHash = KOReaderDocumentId::calculateFromFilename(epubPath); - } else { - documentHash = KOReaderDocumentId::calculate(epubPath); - } + const DocumentMatchMethod primaryMethod = primaryMatchMethod; + remoteMatchMethod = primaryMethod; + documentHash = calculateDocumentHashForMethod(epubPath, primaryMethod); if (documentHash.empty()) { { RenderLock lock(*this); @@ -158,8 +228,7 @@ void KOReaderSyncActivity::performSync() { requestUpdate(true); return; } - - LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str()); + const std::string primaryHash = documentHash; { RenderLock lock(*this); @@ -177,10 +246,56 @@ void KOReaderSyncActivity::performSync() { return; } - // Fetch remote progress - const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress); + // Fetch remote progress. In smart mode, also probe the alternate document-id + // method and use the furthest remote state we can find. This avoids a stale + // local upload when another KOReader device synced the same book with a + // different document matching method. + auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress); + LOG_DBG("KOSync", "Primary remote (%s): result=%d http=%d doc=%s remote=%.6f xpath=%s", + matchMethodName(primaryMethod), result, KOReaderSyncClient::lastHttpCode, documentHash.c_str(), + remoteProgress.percentage, remoteProgress.progress.c_str()); + + if (smartSyncEnabled()) { + const DocumentMatchMethod altMethod = alternateMatchMethod(primaryMethod); + const std::string altHash = calculateDocumentHashForMethod(epubPath, altMethod); + if (!altHash.empty() && altHash != documentHash) { + KOReaderProgress altProgress; + const auto altResult = KOReaderSyncClient::getProgress(altHash, altProgress); + LOG_DBG("KOSync", "Alternate remote (%s): result=%d http=%d doc=%s remote=%.6f xpath=%s", + matchMethodName(altMethod), altResult, KOReaderSyncClient::lastHttpCode, altHash.c_str(), + altProgress.percentage, altProgress.progress.c_str()); + + if (altResult == KOReaderSyncClient::OK && + (result == KOReaderSyncClient::NOT_FOUND || altProgress.percentage > remoteProgress.percentage)) { + documentHash = altHash; + remoteProgress = std::move(altProgress); + remoteMatchMethod = altMethod; + result = KOReaderSyncClient::OK; + } + } + } + + // A minimal network boot intentionally reaches this point without loading the EPUB. + // Reconstruct local progress only after all remote TLS probes have completed. + if (!ensureLocalProgressLoaded()) { + LOG_ERR("KOSync", "Failed to reconstruct local progress after network boot"); + { + RenderLock lock(*this); + state = SYNC_FAILED; + statusMessage = tr(STR_SYNC_REOPTIMIZE_REQUIRED); + } + requestUpdate(true); + return; + } if (result == KOReaderSyncClient::NOT_FOUND) { + if (smartSyncEnabled()) { + LOG_DBG("KOSync", "Smart sync: no remote progress found for known document hashes; uploading local %.6f", + localProgress.percentage); + performUpload(); + return; + } + // No remote progress - offer to upload { RenderLock lock(*this); @@ -201,27 +316,56 @@ void KOReaderSyncActivity::performSync() { return; } - // Epub was released before sync to free RAM for the TLS handshake — reload it now. hasRemoteProgress = true; - ensureEpubLoaded(); - if (!epub) { + + const PositionCoordinateSpace remoteCoordinateSpace = remoteMatchMethod == DocumentMatchMethod::FILENAME + ? PositionCoordinateSpace::SourceDocument + : PositionCoordinateSpace::CurrentDocument; + bool usedRichPosition = false; + // The client only accepts rich positions from the official CrossPoint Sync server. + // Filename matching still needs source-document mapping because optimized books can diverge. + if (remoteCoordinateSpace == PositionCoordinateSpace::CurrentDocument && remoteProgress.position.has_value()) { + const auto richMapped = ProgressMapper::fromRichPosition(epub, *remoteProgress.position, renderer); + if (richMapped.has_value()) { + remotePosition = *richMapped; + usedRichPosition = true; + } + } + if (!usedRichPosition) { + const KOReaderPosition koPos = {remoteProgress.progress, remoteProgress.percentage}; + remotePosition = + ProgressMapper::toCrossPoint(epub, koPos, currentSpineIndex, totalPagesInSpine, remoteCoordinateSpace); + } + if (!remotePosition.valid) { { RenderLock lock(*this); state = SYNC_FAILED; - statusMessage = ""; + statusMessage = tr(STR_SYNC_REOPTIMIZE_REQUIRED); } requestUpdate(true); return; } - KOReaderPosition koPos = {remoteProgress.progress, remoteProgress.percentage}; - remotePosition = ProgressMapper::toCrossPoint(epub, koPos, currentSpineIndex, totalPagesInSpine); - - // Refine page using section cache LUTs: li index, anchor, or paragraph index. - if (remotePosition.hasLiIndex || remotePosition.xpathAnchorId[0] != '\0' || remotePosition.hasParagraphIndex) { + // Refine page using the content-offset LUT first, then structural anchors. + // A partial cache deliberately returns no page for an offset outside its + // watermark; preserving that offset lets the reader index through to it. + if (!usedRichPosition && (remotePosition.hasVisibleTextOffset || remotePosition.hasLiIndex || + remotePosition.xpathAnchorId[0] != '\0' || remotePosition.hasParagraphIndex)) { Section tempSection(epub, remotePosition.spineIndex, renderer); bool refined = false; - if (remotePosition.hasLiIndex) { + if (remotePosition.hasVisibleTextOffset) { + const auto contentPage = tempSection.getPageForVisibleTextOffset(remotePosition.visibleTextOffset, true); + if (contentPage.has_value()) { + LOG_DBG("KOSync", "Visible offset %lu -> page %d (was %d)", + static_cast(remotePosition.visibleTextOffset), *contentPage, remotePosition.pageNumber); + remotePosition.pageNumber = *contentPage; + refined = true; + } else { + LOG_DBG("KOSync", "Visible offset %lu is beyond the cached section watermark", + static_cast(remotePosition.visibleTextOffset)); + } + } + if (!refined && remotePosition.hasLiIndex) { const auto liPage = tempSection.getPageForListItemIndex(remotePosition.liIndex); if (liPage.has_value()) { LOG_DBG("KOSync", "Li index %u -> page %d (was %d)", remotePosition.liIndex, *liPage, @@ -269,8 +413,29 @@ void KOReaderSyncActivity::performSync() { } } } - // localProgress was pre-computed in EpubReaderActivity before the Epub was released. + if (smartSyncEnabled()) { + static constexpr float SAME_PROGRESS_EPSILON = 0.001f; // 0.1 percentage points + const float delta = localProgress.percentage - remoteProgress.percentage; + LOG_DBG("KOSync", "Smart decision: doc=%s local=%.6f remote=%.6f delta=%.6f remoteXpath=%s mapped=%d/%d", + documentHash.c_str(), localProgress.percentage, remoteProgress.percentage, delta, + remoteProgress.progress.c_str(), remotePosition.spineIndex, remotePosition.pageNumber); + if (std::fabs(delta) <= SAME_PROGRESS_EPSILON) { + completeAlreadySynced(); + return; + } + + if (delta > 0) { + // Alternate hashes are only probes for newer remote state. Keep uploads + // on the user's configured matching method so its primary record heals. + documentHash = primaryHash; + performUpload(); + return; + } + + saveProgressAndReturn(remotePosition); + return; + } { RenderLock lock(*this); state = SHOWING_RESULT; @@ -305,7 +470,6 @@ void KOReaderSyncActivity::performUpload() { if (epub) { epub.reset(); - LOG_DBG("KOSync", "Released epub before upload (heap: %u)", (unsigned)ESP.getFreeHeap()); } // localProgress was pre-computed in EpubReaderActivity before the Epub was released. @@ -315,6 +479,46 @@ void KOReaderSyncActivity::performUpload() { progress.percentage = localProgress.percentage; progress.device = SETTINGS.getEffectiveDeviceName(); + // Rich CrossPoint position for the default CrossPoint sync server (lossless + // CrossPoint<->CrossPoint sync). The HTTP client also enforces this boundary + // before serializing the extension. + if (KOREADER_STORE.usesCrossPointSyncServer()) { + KOReaderRichPosition pos; + const float pct = localProgress.percentage < 0.0f ? 0.0f + : localProgress.percentage > 1.0f ? 1.0f + : localProgress.percentage; + pos.pctQ = static_cast(pct * 1000000.0f + 0.5f); + pos.spineIndex = static_cast(currentSpineIndex); + pos.pageNumber = static_cast(currentPage); + pos.totalPages = static_cast(totalPagesInSpine > 0 ? totalPagesInSpine : 1); + pos.paragraphIndex = currentParagraphIndex; + pos.xpath = localProgress.xpath; + progress.position = std::move(pos); + } + + // Optionally include document metadata (KOReader PR #15306) + if (KOREADER_STORE.getSendMetadata()) { + // The Epub is released before the sync network calls and is only reloaded on the + // remote-progress path (performSync). When uploading from NO_REMOTE_PROGRESS the + // Epub is still null, so reload it here and guard the title/author reads to avoid + // dereferencing a null Epub. Filename is derived from the path and is always safe. + ensureEpubLoaded(); + KOReaderMetadata meta; + const auto lastSlash = epubPath.rfind('/'); + meta.filename = (lastSlash != std::string::npos) ? epubPath.substr(lastSlash + 1) : epubPath; + if (epub) { + meta.title = epub->getTitle(); + meta.authors = epub->getAuthor(); + } else { + LOG_ERR("KOSync", "Epub unavailable for metadata; sending filename only"); + } + progress.metadata = std::move(meta); + } + + // Release the Epub before the network call so the TLS handshake has enough free heap + // (consistent with the release-before-sync pattern in performSync); nothing below needs it. + epub.reset(); + const auto result = KOReaderSyncClient::updateProgress(progress); // Drop the radio while user reads the result; full teardown happens at silent reboot. @@ -334,14 +538,33 @@ void KOReaderSyncActivity::performUpload() { RenderLock lock(*this); state = UPLOAD_COMPLETE; } + if (smartSyncEnabled()) { + markAutoReturn(); + } requestUpdate(true); } void KOReaderSyncActivity::onEnter() { Activity::onEnter(); + + // The reader uses this activity as a tiny handoff so ActivityManager can run + // reader onExit() before rebooting. Network boot uses the other constructor. + if (restartBeforeNetwork) { + silentRestartToNetwork(NetworkBootTarget::KOREADER_SYNC); + return; + } + ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); lockInitialConfirmRelease = mappedInput.isPressed(MappedInputManager::Button::Confirm); + if (!localProgressDeferred && !localProgress.valid) { + LOG_ERR("KOSync", "Source position map unavailable; re-optimize the EPUB before filename-based sync"); + state = SYNC_FAILED; + statusMessage = tr(STR_SYNC_REOPTIMIZE_REQUIRED); + requestUpdate(); + return; + } + // Check for credentials first if (!KOREADER_STORE.hasCredentials()) { state = NO_CREDENTIALS; @@ -354,15 +577,13 @@ void KOReaderSyncActivity::onEnter() { wifiActivated = true; // Check if already connected (e.g. from settings page auth) - if (WiFi.status() == WL_CONNECTED) { - LOG_DBG("KOSync", "Already connected to WiFi"); + if (hasActiveStationWifiConnection()) { onWifiSelectionComplete(true); return; } // Launch WiFi selection subactivity - LOG_DBG("KOSync", "Launching WifiSelectionActivity..."); - startActivityForResult(std::make_unique(renderer, mappedInput), + startActivityForResult(std::make_unique(renderer, mappedInput, true, true), [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); } @@ -381,8 +602,13 @@ void KOReaderSyncActivity::render(RenderLock&&) { auto metrics = UITheme::getInstance().getMetrics(); Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); - GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, - tr(STR_KOREADER_SYNC)); + const Rect header{screen.x, screen.y + metrics.topPadding, screen.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_KOREADER_SYNC), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_KOREADER_SYNC)); + } int top = screen.y + screen.height / 2 - 40; if (state == NO_CREDENTIALS) { @@ -393,19 +619,19 @@ void KOReaderSyncActivity::render(RenderLock&&) { const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); return; } if (state == SYNCING || state == UPLOADING) { UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top, statusMessage.c_str(), true, EpdFontFamily::BOLD); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); return; } if (state == SHOWING_RESULT) { // Show comparison - top = screen.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + top = screen.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_PROGRESS_FOUND), true, EpdFontFamily::BOLD); // Remote chapter name requires Epub (loaded lazily in performSync before this state). @@ -442,29 +668,28 @@ void KOReaderSyncActivity::render(RenderLock&&) { char localPageStr[64]; snprintf(localPageStr, sizeof(localPageStr), tr(STR_PAGE_TOTAL_OVERALL_FORMAT), currentPage + 1, totalPagesInSpine, localProgress.percentage * 100); - renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, top + 200, localPageStr); - - const int optionY = top + 230; - const int optionHeight = 30; + renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, top + RESULT_LOCAL_PAGE_Y_OFFSET, + localPageStr); - // Apply option - if (selectedOption == 0) { - renderer.fillRect(screen.x, optionY - 2, screen.width - 1, optionHeight); - } - renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, optionY, tr(STR_APPLY_REMOTE), - selectedOption != 0); - - // Upload option - if (selectedOption == 1) { - renderer.fillRect(screen.x, optionY + optionHeight - 2, screen.width - 1, optionHeight); + const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); + const auto actions = resultActionLayout(screen, metrics, top, lineHeight, mappedInput.hasTouch()); + const char* actionLabels[] = {tr(STR_APPLY_REMOTE), tr(STR_UPLOAD_LOCAL)}; + for (int option = 0; option < 2; ++option) { + const Rect& button = actions.buttons[option]; + const bool selected = selectedOption == option; + if (selected) { + renderer.fillRect(button.x, button.y, button.width, button.height); + } + renderer.drawRect(button.x, button.y, button.width, button.height, true); + const int textX = button.x + (button.width - renderer.getTextWidth(UI_10_FONT_ID, actionLabels[option])) / 2; + const int textY = button.y + (button.height - lineHeight) / 2; + renderer.drawText(UI_10_FONT_ID, textX, textY, actionLabels[option], !selected); } - renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, optionY + optionHeight, - tr(STR_UPLOAD_LOCAL), selectedOption != 1); // Bottom button hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); return; } @@ -474,16 +699,18 @@ void KOReaderSyncActivity::render(RenderLock&&) { const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_UPLOAD), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); return; } - if (state == UPLOAD_COMPLETE) { - UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top, tr(STR_UPLOAD_SUCCESS), true, EpdFontFamily::BOLD); + if (state == UPLOAD_COMPLETE || state == SYNC_COMPLETE) { + UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top, + state == UPLOAD_COMPLETE ? tr(STR_UPLOAD_SUCCESS) : tr(STR_ALREADY_SYNCED), true, + EpdFontFamily::BOLD); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_DONE), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); return; } @@ -500,7 +727,7 @@ void KOReaderSyncActivity::render(RenderLock&&) { const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); return; } } @@ -510,14 +737,61 @@ void KOReaderSyncActivity::loop() { return; } - if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) { - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header{screen.x, screen.y + metrics.topPadding, screen.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (TouchHeaderBackButton::wasTapped(mappedInput, header)) { + returnToReader(); + return; + } + + if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE || state == SYNC_COMPLETE) { + if (autoReturnAt != 0 && millis() >= autoReturnAt) { + returnToReader(); + return; + } + if (mappedInput.wasReleased(MappedInputManager::Button::Back) || + mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { returnToReader(); } return; } if (state == SHOWING_RESULT) { + auto chooseSelected = [this] { + if (selectedOption == 0) { + saveProgressAndReturn(remotePosition); + } else if (selectedOption == 1) { + performUpload(); + } + }; + + { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const int top = + screen.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; + const auto actions = + resultActionLayout(screen, metrics, top, renderer.getLineHeight(UI_10_FONT_ID), mappedInput.hasTouch()); + int touchedOption = -1; + const auto touch = + mappedInput.rowTouch(touchedOption, actions.buttons[0].y, actions.rowStep, 2, actions.buttons[0].x, + actions.buttons[0].x + actions.buttons[0].width, actions.rowHeight); + if (touch == MappedInputManager::RowTouch::Down) { + if (selectedOption != touchedOption) { + selectedOption = touchedOption; + requestUpdate(); + } + return; + } + if (touch == MappedInputManager::RowTouch::Tap) { + selectedOption = touchedOption; + chooseSelected(); + return; + } + } + // Navigate options if (mappedInput.wasReleased(MappedInputManager::Button::Up) || mappedInput.wasReleased(MappedInputManager::Button::Left)) { @@ -548,7 +822,7 @@ void KOReaderSyncActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { // Calculate hash if not done yet if (documentHash.empty()) { - if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) { + if (primaryMatchMethod == DocumentMatchMethod::FILENAME) { documentHash = KOReaderDocumentId::calculateFromFilename(epubPath); } else { documentHash = KOReaderDocumentId::calculate(epubPath); diff --git a/src/activities/reader/KOReaderSyncActivity.h b/src/activities/reader/KOReaderSyncActivity.h index 7ad56e144e..8e5335111d 100644 --- a/src/activities/reader/KOReaderSyncActivity.h +++ b/src/activities/reader/KOReaderSyncActivity.h @@ -5,9 +5,11 @@ #include #include +#include "KOReaderCredentialStore.h" #include "KOReaderSyncClient.h" #include "ProgressMapper.h" #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" /** * Activity for syncing reading progress with KOReader sync server. @@ -21,26 +23,39 @@ */ class KOReaderSyncActivity final : public Activity { public: - explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& epubPath, - int currentSpineIndex, int currentPage, int totalPagesInSpine, - KOReaderPosition localKoPos, std::string localChapterName, - std::optional currentParagraphIndex = std::nullopt) - : Activity("KOReaderSync", renderer, mappedInput), - epubPath(epubPath), - currentSpineIndex(currentSpineIndex), - currentPage(currentPage), - totalPagesInSpine(totalPagesInSpine), - currentParagraphIndex(currentParagraphIndex), - localChapterName(std::move(localChapterName)), + static constexpr const char* NAME = "KOReaderSync"; + + explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity(NAME, renderer, mappedInput), + currentSpineIndex(0), + currentPage(0), + totalPagesInSpine(1), + primaryMatchMethod(DocumentMatchMethod::FILENAME), + remoteMatchMethod(DocumentMatchMethod::FILENAME), remoteProgress{}, remotePosition{}, - localProgress(std::move(localKoPos)) {} + localProgress{}, + restartBeforeNetwork(true) {} + + explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string epubPath, + DocumentMatchMethod matchMethod) + : Activity(NAME, renderer, mappedInput), + epubPath(std::move(epubPath)), + currentSpineIndex(0), + currentPage(0), + totalPagesInSpine(1), + primaryMatchMethod(matchMethod), + remoteMatchMethod(matchMethod), + remoteProgress{}, + remotePosition{}, + localProgress{}, + localProgressDeferred(true) {} void onEnter() override; void onExit() override; void loop() override; void render(RenderLock&&) override; - bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; } + bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING || state == UPLOADING; } bool isReaderActivity() const override { return true; } bool allowPowerAsConfirmInReaderMode() const override { return true; } @@ -52,6 +67,7 @@ class KOReaderSyncActivity final : public Activity { SHOWING_RESULT, UPLOADING, UPLOAD_COMPLETE, + SYNC_COMPLETE, NO_REMOTE_PROGRESS, SYNC_FAILED, NO_CREDENTIALS @@ -64,8 +80,11 @@ class KOReaderSyncActivity final : public Activity { int currentPage; int totalPagesInSpine; std::optional currentParagraphIndex; + DocumentMatchMethod primaryMatchMethod; + DocumentMatchMethod remoteMatchMethod; State state = WIFI_SELECTION; + ScreenTransitionRefresh screenTransitionRefresh; std::string statusMessage; std::string documentHash; @@ -74,12 +93,18 @@ class KOReaderSyncActivity final : public Activity { KOReaderProgress remoteProgress; CrossPointPosition remotePosition; - // Local progress as KOReader format (pre-computed before Epub was released) + // Loaded from saved reader progress after the remote TLS probe completes. KOReaderPosition localProgress; + bool localProgressDeferred = false; + bool restartBeforeNetwork = false; // Selection in result screen (0=Apply, 1=Upload) int selectedOption = 0; + // Timed return for successful smart-sync terminal states. + unsigned long autoReturnAt = 0; + static constexpr unsigned long AUTO_RETURN_DELAY_MS = 1200; + // Tracks whether this session activated WiFi. Set in onEnter past the credentials // check; checked in onExit to decide whether to silent-reboot. Can't rely on // WiFi.getMode() because performUpload() calls esp_wifi_stop() on the way out, @@ -91,7 +116,11 @@ class KOReaderSyncActivity final : public Activity { void performSync(); void performUpload(); bool consumeInitialConfirmRelease(); + bool smartSyncEnabled() const; + void markAutoReturn(); + void completeAlreadySynced(); void ensureEpubLoaded(); + bool ensureLocalProgressLoaded(); void saveProgressAndReturn(const CrossPointPosition& position); void returnToReader(); }; diff --git a/src/activities/reader/LookedUpWordsActivity.cpp b/src/activities/reader/LookedUpWordsActivity.cpp new file mode 100644 index 0000000000..5c34edd4b4 --- /dev/null +++ b/src/activities/reader/LookedUpWordsActivity.cpp @@ -0,0 +1,310 @@ +#include "LookedUpWordsActivity.h" + +#include +#include + +#include + +#include "DictionaryDefinitionActivity.h" +#include "MappedInputManager.h" +#include "Memory.h" +#include "activities/util/ConfirmationActivity.h" +#include "components/TouchHeaderBackButton.h" +#include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" +#include "fontIds.h" +#include "util/Dictionary.h" +#include "util/DictionaryActivityUtils.h" +#include "util/LookupHistory.h" + +namespace fui = freeink::ui; + +LookedUpWordsActivity::LookedUpWordsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + std::string bookCachePath, const char* dictionaryFontFamilyName, + const uint8_t dictionaryFontPointSize) + : Activity("LookedUpWords", renderer, mappedInput), + cachePath(std::move(bookCachePath)), + controller(renderer, mappedInput, *this, cachePath), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) { + this->dictionaryFontPointSize = dictionaryFontPointSize; + if (dictionaryFontFamilyName) { + std::strncpy(this->dictionaryFontFamilyName, dictionaryFontFamilyName, sizeof(this->dictionaryFontFamilyName) - 1); + } +} + +const char* LookedUpWordsActivity::glyphFor(LookupHistory::Status s) { + switch (s) { + case LookupHistory::Status::Direct: + return "\xe2\x88\x9a"; // √ U+221A + case LookupHistory::Status::Stem: + return "~"; + case LookupHistory::Status::AltForm: + return "~"; + case LookupHistory::Status::Suggestion: + return "?"; + case LookupHistory::Status::NotFound: + return "\xc3\x97"; // × U+00D7 + default: + return "?"; + } +} + +void LookedUpWordsActivity::onEnter() { + Activity::onEnter(); + selectedIndex = 0; + topIndex = 0; + visibleRows = 1; + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &LookedUpWordsActivity::onRowEvent, this); + app.setScreen(&LookedUpWordsActivity::historyScreen, this); + reloadEntries(); + requestUpdate(); +} + +void LookedUpWordsActivity::reloadEntries() { + entries = LookupHistory::load(cachePath); + labels.clear(); + labels.reserve(entries.size()); + uiItems.clear(); + uiItems.reserve(entries.size()); + for (size_t i = 0; i < entries.size(); ++i) { + labels.push_back(std::string(glyphFor(entries[i].status)) + " " + entries[i].word); + fui::ListItem item; + item.label = labels.back().c_str(); + item.actionValue = static_cast(i); + uiItems.push_back(item); + } + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(entries.size())); +} + +void LookedUpWordsActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(self->entries.size())) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->controller.startLookup(self->entries[self->selectedIndex].word); +} + +void LookedUpWordsActivity::historyScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildHistoryScreen(screen); +} + +void LookedUpWordsActivity::buildHistoryScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + + metrics.verticalSpacing), + 0, static_cast(metrics.buttonHintsHeight + metrics.verticalSpacing), 0}); + + fui::ListProps props; + props.items = uiItems.data(); + props.count = static_cast(uiItems.size()); + props.selectedIndex = static_cast(selectedIndex); + props.topIndex = static_cast(topIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.labelText = screen.theme().bodyText; + props.labelText.maxLines = 2; + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, static_cast(entries.size())); + props.topIndex = static_cast(topIndex); + screen.list(props); +} + +void LookedUpWordsActivity::onExit() { + controller.onExit(); + Activity::onExit(); +} + +void LookedUpWordsActivity::showDeleteConfirmation(const bool ignoreInitialConfirmRelease) { + if (entries.empty() || selectedIndex < 0 || selectedIndex >= static_cast(entries.size())) return; + + const std::string word = entries[selectedIndex].word; + auto confirmation = makeUniqueNoThrow(renderer, mappedInput, std::string(tr(STR_DELETE)) + "?", + word, ignoreInitialConfirmRelease, true); + if (!confirmation) { + LOG_ERR("LOOKUP", "OOM: ConfirmationActivity"); + return; + } + startActivityForResult(std::move(confirmation), [this](const ActivityResult& result) { + if (!result.isCancelled) { + LookupHistory::removeRecentAt(cachePath, selectedIndex); + reloadEntries(); + if (selectedIndex >= static_cast(entries.size())) { + selectedIndex = std::max(0, static_cast(entries.size()) - 1); + } + } + requestUpdate(); + }); +} + +void LookedUpWordsActivity::loop() { + if (controller.isActive()) { + switch (controller.handleInput()) { + case DictionaryLookupController::LookupEvent::FoundDefinition: { + startActivityForResult(std::make_unique( + renderer, mappedInput, controller.getFoundWord(), controller.getFoundLocation(), + true, cachePath, controller.getRecordHistory(), controller.getLookupWord(), + DictionaryLookupController::toHistStatus(controller.getFoundStatus()), nullptr, + nullptr, dictionaryFontFamilyName, dictionaryFontPointSize), + [this](const ActivityResult& result) { + reloadEntries(); + if (!result.isCancelled) { + setResult(ActivityResult{}); + finish(); + } else { + requestUpdate(); + } + }); + break; + } + case DictionaryLookupController::LookupEvent::NotFoundDismissedBack: + reloadEntries(); + requestUpdate(); + break; + case DictionaryLookupController::LookupEvent::NotFoundDismissedDone: + setResult(ActivityResult{}); + finish(); + break; + case DictionaryLookupController::LookupEvent::Cancelled: + requestUpdate(); + break; + default: + break; + } + return; + } + + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + DictUtils::cancelAndFinish(*this); + return; + } + + if (entries.empty()) { + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + DictUtils::cancelAndFinish(*this); + } + return; + } + + // Long press Confirm: open the delete confirmation at the hold threshold. + if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && + mappedInput.getHeldTime() >= Dictionary::LONG_PRESS_MS) { + showDeleteConfirmation(true); + return; + } + + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } + + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, static_cast(entries.size())); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + + const auto& metrics = UITheme::getInstance().getMetrics(); + int touchX = 0; + int touchY = 0; + if (mappedInput.isScreenTouchLongPress(touchX, touchY, Dictionary::LONG_PRESS_MS)) { + const int contentTop = + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; + const int rowHeight = uiListRowHeight(app.theme(), UiListRowType::SingleLine); + const int rowStep = rowHeight + app.theme().listRowGap; + const int row = rowStep > 0 ? (touchY - contentTop) / rowStep : -1; + const int heldIndex = topIndex + row; + if (touchY >= contentTop && row >= 0 && touchY - contentTop - row * rowStep < rowHeight && row < visibleRows && + heldIndex < static_cast(entries.size())) { + selectedIndex = heldIndex; + mappedInput.suppressNextTouchTap(); + showDeleteConfirmation(false); + return; + } + } + + const int totalItems = static_cast(entries.size()); + const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false); + + buttonNavigator.onNextRelease([this, totalItems] { + selectedIndex = ButtonNavigator::nextIndex(selectedIndex, totalItems); + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, totalItems); + requestUpdate(); + }); + buttonNavigator.onPreviousRelease([this, totalItems] { + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, totalItems); + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, totalItems); + requestUpdate(); + }); + buttonNavigator.onNextContinuous([this, totalItems, pageItems] { + selectedIndex = ButtonNavigator::nextPageIndex(selectedIndex, totalItems, pageItems); + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, totalItems); + requestUpdate(); + }); + buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { + selectedIndex = ButtonNavigator::previousPageIndex(selectedIndex, totalItems, pageItems); + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, totalItems); + requestUpdate(); + }); + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + controller.startLookup(entries[selectedIndex].word); + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + DictUtils::cancelAndFinish(*this); + return; + } +} + +void LookedUpWordsActivity::render(RenderLock&&) { + renderer.clearScreen(); + if (controller.render()) return; + + const int pageWidth = renderer.getScreenWidth(); + const int pageHeight = renderer.getScreenHeight(); + const auto& metrics = UITheme::getInstance().getMetrics(); + + const Rect header{0, metrics.topPadding, pageWidth, TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_LOOKUP_HISTORY), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_LOOKUP_HISTORY)); + } + + const int contentTop = + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; + + if (entries.empty()) { + const int midY = contentTop + (pageHeight - contentTop - metrics.buttonHintsHeight) / 2; + renderer.drawCenteredText(UI_10_FONT_ID, midY, tr(STR_LOOKUP_HISTORY_EMPTY)); + const auto buttonLabels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, buttonLabels.btn1, buttonLabels.btn2, buttonLabels.btn3, buttonLabels.btn4); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + return; + } + + uiReady = false; + app.render(); + uiReady = true; + + const auto buttonLabels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + GUI.drawButtonHints(renderer, buttonLabels.btn1, buttonLabels.btn2, buttonLabels.btn3, buttonLabels.btn4); + + renderer.displayBuffer(HalDisplay::FAST_REFRESH); +} diff --git a/src/activities/reader/LookedUpWordsActivity.h b/src/activities/reader/LookedUpWordsActivity.h new file mode 100644 index 0000000000..287d365913 --- /dev/null +++ b/src/activities/reader/LookedUpWordsActivity.h @@ -0,0 +1,53 @@ +#pragma once +#include +#include + +#include +#include +#include + +#include "../Activity.h" +#include "util/ButtonNavigator.h" +#include "util/DictionaryLookupController.h" +#include "util/LookupHistory.h" + +class LookedUpWordsActivity final : public Activity { + public: + explicit LookedUpWordsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string bookCachePath, + const char* dictionaryFontFamilyName = nullptr, uint8_t dictionaryFontPointSize = 0); + + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; + + private: + using UiApp = freeink::ui::FreeInkApp<20, 4>; + static constexpr freeink::ui::ActionId ACTION_ROW = 1; + + static void historyScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildHistoryScreen(UiApp::ScreenType& screen); + void reloadEntries(); + + std::string cachePath; + char dictionaryFontFamilyName[64] = ""; + uint8_t dictionaryFontPointSize = 0; + std::vector entries; + std::vector labels; + std::vector uiItems; + int selectedIndex = 0; + + DictionaryLookupController controller; + ButtonNavigator buttonNavigator; + freeink::ui::GfxRendererTarget uiTarget; + UiApp app; + bool uiReady = false; + int visibleRows = 1; + int topIndex = 0; + + bool skipLoopDelay() override { return controller.skipLoopDelay(); } + + void showDeleteConfirmation(bool ignoreInitialConfirmRelease); + static const char* glyphFor(LookupHistory::Status s); +}; diff --git a/src/activities/reader/NearbyBookPositionSyncActivity.cpp b/src/activities/reader/NearbyBookPositionSyncActivity.cpp index 28a8a6163d..b13741cea7 100644 --- a/src/activities/reader/NearbyBookPositionSyncActivity.cpp +++ b/src/activities/reader/NearbyBookPositionSyncActivity.cpp @@ -1,5 +1,54 @@ #include "NearbyBookPositionSyncActivity.h" +#include +#include + +#include + +#include "components/TouchHeaderBackButton.h" +#include "components/UITheme.h" +#include "fontIds.h" + +namespace { + +constexpr int TOUCH_ACTION_HEIGHT = 48; +constexpr int TOUCH_ACTION_GAP = 10; + +struct TouchActionLayout { + Rect buttons[2]; + int rowStep; + int rowHeight; +}; + +TouchActionLayout touchActionLayout(const Rect& screen, const ThemeMetrics& metrics) { + const int buttonX = screen.x + metrics.contentSidePadding; + const int buttonWidth = std::max(1, screen.width - metrics.contentSidePadding * 2); + const int firstButtonY = + screen.y + screen.height - metrics.verticalSpacing - TOUCH_ACTION_HEIGHT * 2 - TOUCH_ACTION_GAP; + return { + {Rect{buttonX, firstButtonY, buttonWidth, TOUCH_ACTION_HEIGHT}, + Rect{buttonX, firstButtonY + TOUCH_ACTION_HEIGHT + TOUCH_ACTION_GAP, buttonWidth, TOUCH_ACTION_HEIGHT}}, + TOUCH_ACTION_HEIGHT + TOUCH_ACTION_GAP, + TOUCH_ACTION_HEIGHT, + }; +} + +void drawTouchActionButtons(GfxRenderer& renderer, const Rect& screen, const ThemeMetrics& metrics, + const char* confirmLabel) { + const auto actions = touchActionLayout(screen, metrics); + const char* labels[] = {tr(STR_CANCEL), confirmLabel}; + const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); + for (int action = 0; action < 2; ++action) { + const Rect& button = actions.buttons[action]; + renderer.drawRect(button.x, button.y, button.width, button.height, true); + const int textX = button.x + (button.width - renderer.getTextWidth(UI_10_FONT_ID, labels[action])) / 2; + const int textY = button.y + (button.height - lineHeight) / 2; + renderer.drawText(UI_10_FONT_ID, textX, textY, labels[action]); + } +} + +} // namespace + #ifdef SIMULATOR #include @@ -31,21 +80,18 @@ float qToPercentage(const uint32_t percentageQ) { return static_cast(std::min(percentageQ, SIM_PERCENTAGE_SCALE)) / static_cast(SIM_PERCENTAGE_SCALE); } -std::string documentMatchingDetail() { - const StrId methodLabel = - KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? StrId::STR_FILENAME : StrId::STR_BINARY; +std::string documentMatchingDetail(const DocumentMatchMethod matchMethod) { + const StrId methodLabel = matchMethod == DocumentMatchMethod::FILENAME ? StrId::STR_FILENAME : StrId::STR_BINARY; return std::string(tr(STR_DOCUMENT_MATCHING)) + ": " + I18N.get(methodLabel); } } // namespace -NearbyBookPositionSyncActivity::NearbyBookPositionSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - std::shared_ptr epub, const std::string& epubPath, - int currentSpineIndex, int currentPage, - int totalPagesInSpine, KOReaderPosition localKoPos, - std::string localChapterName, - std::optional currentParagraphIndex, - std::optional currentListItemIndex) +NearbyBookPositionSyncActivity::NearbyBookPositionSyncActivity( + GfxRenderer& renderer, MappedInputManager& mappedInput, std::shared_ptr epub, const std::string& epubPath, + int currentSpineIndex, int currentPage, int totalPagesInSpine, KOReaderPosition localKoPos, + std::string localChapterName, const DocumentMatchMethod matchMethod, std::optional currentParagraphIndex, + std::optional currentListItemIndex) : Activity("NearbyBookPositionSync", renderer, mappedInput), epub_(std::move(epub)), epubPath_(epubPath), @@ -55,19 +101,52 @@ NearbyBookPositionSyncActivity::NearbyBookPositionSyncActivity(GfxRenderer& rend totalPagesInSpine_(totalPagesInSpine), currentParagraphIndex_(currentParagraphIndex), currentListItemIndex_(currentListItemIndex), - localKoPosition_(std::move(localKoPos)) {} + localKoPosition_(std::move(localKoPos)), + matchMethod_(matchMethod) {} NearbyBookPositionSyncActivity::~NearbyBookPositionSyncActivity() = default; void NearbyBookPositionSyncActivity::onEnter() { Activity::onEnter(); - prepareLocalPosition(); + if (!prepareLocalPosition()) return; setState(State::READY); } void NearbyBookPositionSyncActivity::onExit() { Activity::onExit(); } void NearbyBookPositionSyncActivity::loop() { + const auto& headerMetrics = UITheme::getInstance().getMetrics(); + const Rect headerScreen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header{headerScreen.x, headerScreen.y + headerMetrics.topPadding, headerScreen.width, + TouchHeaderBackButton::height(headerMetrics, mappedInput)}; + if (TouchHeaderBackButton::wasTapped(mappedInput, header)) { + returnToReader(true); + return; + } + + const bool canShare = state_ == State::READY || state_ == State::SYNCED || state_ == State::ERROR; + const bool canApplyReceivedPosition = state_ == State::SHOWING_RESULT && !sourceMode_; + if (mappedInput.hasTouch() && (canShare || canApplyReceivedPosition)) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const auto actions = touchActionLayout(screen, metrics); + int touchedAction = -1; + const auto touch = + mappedInput.rowTouch(touchedAction, actions.buttons[0].y, actions.rowStep, 2, actions.buttons[0].x, + actions.buttons[0].x + actions.buttons[0].width, actions.rowHeight); + if (touch == MappedInputManager::RowTouch::Down) return; + if (touch == MappedInputManager::RowTouch::Tap) { + if (touchedAction == 0) { + returnToReader(true); + } else if (canShare) { + startSync(); + } else { + applyPeerPosition(); + } + return; + } + } + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { returnToReader(true); return; @@ -92,12 +171,17 @@ void NearbyBookPositionSyncActivity::render(RenderLock&&) { Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); renderer.clearScreen(); - GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, - tr(STR_NEARBY_POSITION_SYNC)); + const Rect header{screen.x, screen.y + metrics.topPadding, screen.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_NEARBY_POSITION_SYNC), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_NEARBY_POSITION_SYNC)); + } if (state_ == State::SHOWING_RESULT) { renderComparison(); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh_.modeFor(static_cast(state_))); return; } @@ -111,7 +195,7 @@ void NearbyBookPositionSyncActivity::render(RenderLock&&) { case State::READY: primary = tr(STR_NEARBY_POSITION_READY); detail = std::string(tr(STR_DEVICE_NAME)) + ": " + SETTINGS.getEffectiveDeviceName(); - detailSecondary = documentMatchingDetail(); + detailSecondary = documentMatchingDetail(matchMethod_); break; case State::DISCOVERING: primary = tr(STR_NEARBY_POSITION_SCANNING); @@ -137,9 +221,13 @@ void NearbyBookPositionSyncActivity::render(RenderLock&&) { if (state_ == State::READY || state_ == State::SYNCED || state_ == State::ERROR) { renderReady(primary, detail, detailSecondary); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_NEARBY_POSITION_SHARE_BUTTON), "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); + if (mappedInput.hasTouch()) { + drawTouchActionButtons(renderer, screen, metrics, tr(STR_NEARBY_POSITION_SHARE_BUTTON)); + } else { + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_NEARBY_POSITION_SHARE_BUTTON), "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); + } + renderer.displayBuffer(screenTransitionRefresh_.modeFor(static_cast(state_))); return; } @@ -151,17 +239,25 @@ void NearbyBookPositionSyncActivity::render(RenderLock&&) { } const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh_.modeFor(static_cast(state_))); } void NearbyBookPositionSyncActivity::enqueueEspNowPacket(const uint8_t*, const uint8_t*, int) {} bool NearbyBookPositionSyncActivity::prepareLocalPosition() { if (localPrepared_) return true; + if (!localKoPosition_.valid) { + setError(tr(STR_SYNC_REOPTIMIZE_REQUIRED)); + return false; + } + localPosition_.percentageQ = percentageToQ(localKoPosition_.percentage); localPosition_.spineIndex = std::max(0, currentSpineIndex_); localPosition_.pageNumber = std::max(0, currentPage_); localPosition_.totalPages = std::max(1, totalPagesInSpine_); + if (localKoPosition_.xpath.size() <= MAX_XPATH_BYTES) { + std::snprintf(localPosition_.xpath.data(), localPosition_.xpath.size(), "%s", localKoPosition_.xpath.c_str()); + } if (currentParagraphIndex_.has_value() && *currentParagraphIndex_ != UINT16_MAX) { localPosition_.paragraphIndex = *currentParagraphIndex_; localPosition_.hasParagraphIndex = true; @@ -175,6 +271,7 @@ bool NearbyBookPositionSyncActivity::prepareLocalPosition() { peerPosition_.spineIndex = peerCrossPoint_.spineIndex; peerPosition_.pageNumber = peerCrossPoint_.pageNumber; peerPosition_.totalPages = peerCrossPoint_.totalPages; + peerPosition_.xpath = localPosition_.xpath; peerName_ = "Simulator X4"; peerId_ = "simulator"; @@ -205,7 +302,27 @@ bool NearbyBookPositionSyncActivity::applyPeerPosition() { setState(State::SYNCED); return true; } -bool NearbyBookPositionSyncActivity::mapPeerPosition() { return true; } +bool NearbyBookPositionSyncActivity::mapPeerPosition() { + KOReaderPosition koPos{peerPosition_.xpath.data(), qToPercentage(peerPosition_.percentageQ)}; + const PositionCoordinateSpace coordinateSpace = matchMethod_ == DocumentMatchMethod::FILENAME + ? PositionCoordinateSpace::SourceDocument + : PositionCoordinateSpace::CurrentDocument; + peerCrossPoint_ = ProgressMapper::toCrossPoint(epub_, koPos, currentSpineIndex_, totalPagesInSpine_, coordinateSpace); + if (!peerCrossPoint_.valid) { + setError(tr(STR_SYNC_REOPTIMIZE_REQUIRED)); + return false; + } + if (peerCrossPoint_.totalPages <= 0) { + if (matchMethod_ == DocumentMatchMethod::FILENAME) { + setError(tr(STR_SYNC_REOPTIMIZE_REQUIRED)); + return false; + } + peerCrossPoint_.spineIndex = peerPosition_.spineIndex; + peerCrossPoint_.pageNumber = peerPosition_.pageNumber; + peerCrossPoint_.totalPages = std::max(1, peerPosition_.totalPages); + } + return true; +} void NearbyBookPositionSyncActivity::updateSyncProgress() { if (state_ != State::DISCOVERING && state_ != State::SYNCING) return; @@ -216,6 +333,7 @@ void NearbyBookPositionSyncActivity::updateSyncProgress() { return; } if (elapsedMs >= SIM_RESULT_MS) { + if (!mapPeerPosition()) return; peerPositionReceived_ = true; setState(State::SHOWING_RESULT); } @@ -238,13 +356,18 @@ void NearbyBookPositionSyncActivity::renderReady(const std::string& primary, con const std::string& detailSecondary) const { const auto& metrics = UITheme::getInstance().getMetrics(); Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); - int y = screen.y + metrics.topPadding + metrics.headerHeight + 70; + int y = screen.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + 70; UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, y, primary.c_str(), true, EpdFontFamily::BOLD); y += renderer.getLineHeight(UI_10_FONT_ID) + metrics.verticalSpacing; if (!detailPrimary.empty()) { - UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, y, detailPrimary.c_str(), true); - y += renderer.getLineHeight(SMALL_FONT_ID) + metrics.verticalSpacing; + const auto detailLines = + renderer.wrappedText(SMALL_FONT_ID, detailPrimary.c_str(), screen.width - metrics.contentSidePadding * 2, 3); + for (const auto& line : detailLines) { + UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, y, line.c_str(), true); + y += renderer.getLineHeight(SMALL_FONT_ID); + } + y += metrics.verticalSpacing; } if (!detailSecondary.empty()) { UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, y, detailSecondary.c_str(), true); @@ -257,7 +380,8 @@ void NearbyBookPositionSyncActivity::renderReady(const std::string& primary, con void NearbyBookPositionSyncActivity::renderComparison() const { const auto& metrics = UITheme::getInstance().getMetrics(); Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); - int top = screen.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + int top = + screen.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NEARBY_POSITION_FOUND), true, EpdFontFamily::BOLD); @@ -299,8 +423,12 @@ void NearbyBookPositionSyncActivity::renderComparison() const { renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, optionY, tr(STR_APPLY_NEARBY_POSITION), false); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); + if (mappedInput.hasTouch() && !sourceMode_) { + drawTouchActionButtons(renderer, screen, metrics, tr(STR_CONFIRM)); + } else { + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); + } } #else @@ -330,6 +458,7 @@ void NearbyBookPositionSyncActivity::renderComparison() const { #include "SdCardFontSystem.h" #include "SilentRestart.h" #include "activities/ActivityManager.h" +#include "activities/home/RecentBookProgress.h" #include "components/UITheme.h" #include "fontIds.h" @@ -407,9 +536,8 @@ float qToPercentage(const uint32_t percentageQ) { return static_cast(std::min(percentageQ, PERCENTAGE_SCALE)) / static_cast(PERCENTAGE_SCALE); } -std::string documentMatchingDetail() { - const StrId methodLabel = - KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? StrId::STR_FILENAME : StrId::STR_BINARY; +std::string documentMatchingDetail(const DocumentMatchMethod matchMethod) { + const StrId methodLabel = matchMethod == DocumentMatchMethod::FILENAME ? StrId::STR_FILENAME : StrId::STR_BINARY; return std::string(tr(STR_DOCUMENT_MATCHING)) + ": " + I18N.get(methodLabel); } @@ -522,13 +650,11 @@ void onEspNowReceive(const esp_now_recv_info_t* info, const uint8_t* data, int l } // namespace -NearbyBookPositionSyncActivity::NearbyBookPositionSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - std::shared_ptr epub, const std::string& epubPath, - int currentSpineIndex, int currentPage, - int totalPagesInSpine, KOReaderPosition localKoPos, - std::string localChapterName, - std::optional currentParagraphIndex, - std::optional currentListItemIndex) +NearbyBookPositionSyncActivity::NearbyBookPositionSyncActivity( + GfxRenderer& renderer, MappedInputManager& mappedInput, std::shared_ptr epub, const std::string& epubPath, + int currentSpineIndex, int currentPage, int totalPagesInSpine, KOReaderPosition localKoPos, + std::string localChapterName, const DocumentMatchMethod matchMethod, std::optional currentParagraphIndex, + std::optional currentListItemIndex) : Activity("NearbyBookPositionSync", renderer, mappedInput), eventMutex_(xSemaphoreCreateMutex()), epub_(std::move(epub)), @@ -539,7 +665,8 @@ NearbyBookPositionSyncActivity::NearbyBookPositionSyncActivity(GfxRenderer& rend totalPagesInSpine_(std::max(1, totalPagesInSpine)), currentParagraphIndex_(currentParagraphIndex), currentListItemIndex_(currentListItemIndex), - localKoPosition_(std::move(localKoPos)) {} + localKoPosition_(std::move(localKoPos)), + matchMethod_(matchMethod) {} NearbyBookPositionSyncActivity::~NearbyBookPositionSyncActivity() { if (eventMutex_) { @@ -557,7 +684,13 @@ bool NearbyBookPositionSyncActivity::ensureEpubLoaded() { bool NearbyBookPositionSyncActivity::prepareLocalPosition() { if (localPrepared_) return true; - const std::string documentHash = (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) + if (!localKoPosition_.valid) { + LOG_ERR(LOG_TAG, "Source position map unavailable; re-optimize the EPUB before filename-based nearby sync"); + setError(tr(STR_SYNC_REOPTIMIZE_REQUIRED)); + return false; + } + + const std::string documentHash = (matchMethod_ == DocumentMatchMethod::FILENAME) ? KOReaderDocumentId::calculateFromFilename(epubPath_) : KOReaderDocumentId::calculate(epubPath_); if (documentHash.size() != DOCUMENT_HASH_BYTES) { @@ -625,6 +758,39 @@ void NearbyBookPositionSyncActivity::onExit() { void NearbyBookPositionSyncActivity::loop() { processEvents(); + const auto& headerMetrics = UITheme::getInstance().getMetrics(); + const Rect headerScreen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header{headerScreen.x, headerScreen.y + headerMetrics.topPadding, headerScreen.width, + TouchHeaderBackButton::height(headerMetrics, mappedInput)}; + if (TouchHeaderBackButton::wasTapped(mappedInput, header)) { + returnToReader(true); + return; + } + + const bool canShare = state_ == State::READY || state_ == State::SYNCED || state_ == State::ERROR; + const bool canApplyReceivedPosition = state_ == State::SHOWING_RESULT && !sourceMode_; + if (mappedInput.hasTouch() && (canShare || canApplyReceivedPosition)) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const auto actions = touchActionLayout(screen, metrics); + int touchedAction = -1; + const auto touch = + mappedInput.rowTouch(touchedAction, actions.buttons[0].y, actions.rowStep, 2, actions.buttons[0].x, + actions.buttons[0].x + actions.buttons[0].width, actions.rowHeight); + if (touch == MappedInputManager::RowTouch::Down) return; + if (touch == MappedInputManager::RowTouch::Tap) { + if (touchedAction == 0) { + returnToReader(true); + } else if (canShare) { + startSync(); + } else if (applyPeerPosition()) { + sendAck(peerSourceMac_.data()); + returnToReader(); + } + return; + } + } + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { returnToReader(true); return; @@ -949,8 +1115,20 @@ bool NearbyBookPositionSyncActivity::mapPeerPosition() { KOReaderPosition koPos; koPos.xpath = peerPosition_.xpath.data(); koPos.percentage = qToPercentage(peerPosition_.percentageQ); - peerCrossPoint_ = ProgressMapper::toCrossPoint(epub_, koPos, currentSpineIndex_, totalPagesInSpine_); + const PositionCoordinateSpace coordinateSpace = matchMethod_ == DocumentMatchMethod::FILENAME + ? PositionCoordinateSpace::SourceDocument + : PositionCoordinateSpace::CurrentDocument; + peerCrossPoint_ = ProgressMapper::toCrossPoint(epub_, koPos, currentSpineIndex_, totalPagesInSpine_, coordinateSpace); + if (!peerCrossPoint_.valid) { + setError(tr(STR_SYNC_REOPTIMIZE_REQUIRED)); + return false; + } if (peerCrossPoint_.totalPages <= 0) { + if (matchMethod_ == DocumentMatchMethod::FILENAME) { + LOG_ERR(LOG_TAG, "Refusing optimized raw spine fallback for filename-based nearby sync"); + setError(tr(STR_SYNC_REOPTIMIZE_REQUIRED)); + return false; + } peerCrossPoint_.spineIndex = peerPosition_.spineIndex; peerCrossPoint_.pageNumber = peerPosition_.pageNumber; peerCrossPoint_.totalPages = std::max(1, peerPosition_.totalPages); @@ -1006,6 +1184,7 @@ bool NearbyBookPositionSyncActivity::applyPeerPosition() { setError(tr(STR_SAVE_PROGRESS_FAILED)); return false; } + RecentBookProgress::saveCachedEpubPercent(*epub_, peerCrossPoint_.spineIndex, peerCrossPoint_.pageNumber, pageCount); setState(State::SYNCED); return true; } @@ -1059,12 +1238,17 @@ void NearbyBookPositionSyncActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); - GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, - tr(STR_NEARBY_POSITION_SYNC)); + const Rect header{screen.x, screen.y + metrics.topPadding, screen.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_NEARBY_POSITION_SYNC), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_NEARBY_POSITION_SYNC)); + } if (state_ == State::SHOWING_RESULT) { renderComparison(); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh_.modeFor(static_cast(state_))); return; } @@ -1078,7 +1262,7 @@ void NearbyBookPositionSyncActivity::render(RenderLock&&) { case State::READY: primary = tr(STR_NEARBY_POSITION_READY); detail = std::string(tr(STR_DEVICE_NAME)) + ": " + SETTINGS.getEffectiveDeviceName(); - detailSecondary = documentMatchingDetail(); + detailSecondary = documentMatchingDetail(matchMethod_); break; case State::DISCOVERING: primary = tr(STR_NEARBY_POSITION_SCANNING); @@ -1104,9 +1288,13 @@ void NearbyBookPositionSyncActivity::render(RenderLock&&) { if (state_ == State::READY || state_ == State::SYNCED || state_ == State::ERROR) { renderReady(primary, detail, detailSecondary); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_NEARBY_POSITION_SHARE_BUTTON), "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); + if (mappedInput.hasTouch()) { + drawTouchActionButtons(renderer, screen, metrics, tr(STR_NEARBY_POSITION_SHARE_BUTTON)); + } else { + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_NEARBY_POSITION_SHARE_BUTTON), "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); + } + renderer.displayBuffer(screenTransitionRefresh_.modeFor(static_cast(state_))); return; } @@ -1118,20 +1306,25 @@ void NearbyBookPositionSyncActivity::render(RenderLock&&) { } const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh_.modeFor(static_cast(state_))); } void NearbyBookPositionSyncActivity::renderReady(const std::string& primary, const std::string& detailPrimary, const std::string& detailSecondary) const { const auto& metrics = UITheme::getInstance().getMetrics(); Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); - int y = screen.y + metrics.topPadding + metrics.headerHeight + 70; + int y = screen.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + 70; UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, y, primary.c_str(), true, EpdFontFamily::BOLD); y += renderer.getLineHeight(UI_10_FONT_ID) + metrics.verticalSpacing; if (!detailPrimary.empty()) { - UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, y, detailPrimary.c_str(), true); - y += renderer.getLineHeight(SMALL_FONT_ID) + metrics.verticalSpacing; + const auto detailLines = + renderer.wrappedText(SMALL_FONT_ID, detailPrimary.c_str(), screen.width - metrics.contentSidePadding * 2, 3); + for (const auto& line : detailLines) { + UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, y, line.c_str(), true); + y += renderer.getLineHeight(SMALL_FONT_ID); + } + y += metrics.verticalSpacing; } if (!detailSecondary.empty()) { UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, y, detailSecondary.c_str(), true); @@ -1145,7 +1338,8 @@ void NearbyBookPositionSyncActivity::renderReady(const std::string& primary, con void NearbyBookPositionSyncActivity::renderComparison() const { const auto& metrics = UITheme::getInstance().getMetrics(); Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); - int top = screen.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + int top = + screen.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NEARBY_POSITION_FOUND), true, EpdFontFamily::BOLD); @@ -1187,8 +1381,12 @@ void NearbyBookPositionSyncActivity::renderComparison() const { renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, optionY, tr(STR_APPLY_NEARBY_POSITION), false); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); + if (mappedInput.hasTouch() && !sourceMode_) { + drawTouchActionButtons(renderer, screen, metrics, tr(STR_CONFIRM)); + } else { + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); + } } #endif diff --git a/src/activities/reader/NearbyBookPositionSyncActivity.h b/src/activities/reader/NearbyBookPositionSyncActivity.h index 51ed6a4f21..252620e593 100644 --- a/src/activities/reader/NearbyBookPositionSyncActivity.h +++ b/src/activities/reader/NearbyBookPositionSyncActivity.h @@ -9,8 +9,10 @@ #include #include +#include "KOReaderCredentialStore.h" #include "ProgressMapper.h" #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" class NearbyBookPositionSyncActivity final : public Activity { public: @@ -18,6 +20,7 @@ class NearbyBookPositionSyncActivity final : public Activity { std::shared_ptr epub, const std::string& epubPath, int currentSpineIndex, int currentPage, int totalPagesInSpine, KOReaderPosition localKoPos, std::string localChapterName, + DocumentMatchMethod matchMethod, std::optional currentParagraphIndex = std::nullopt, std::optional currentListItemIndex = std::nullopt); ~NearbyBookPositionSyncActivity() override; @@ -69,6 +72,7 @@ class NearbyBookPositionSyncActivity final : public Activity { }; State state_ = State::STARTING; + ScreenTransitionRefresh screenTransitionRefresh_; SemaphoreHandle_t eventMutex_ = nullptr; std::array events_ = {}; uint8_t eventHead_ = 0; @@ -95,6 +99,7 @@ class NearbyBookPositionSyncActivity final : public Activity { std::optional currentParagraphIndex_; std::optional currentListItemIndex_; KOReaderPosition localKoPosition_; + DocumentMatchMethod matchMethod_ = DocumentMatchMethod::FILENAME; CompactPosition localPosition_ = {}; CompactPosition peerPosition_ = {}; CrossPointPosition peerCrossPoint_ = {}; diff --git a/src/activities/reader/QrDisplayActivity.cpp b/src/activities/reader/QrDisplayActivity.cpp index df4dfa0d0e..161810c7c9 100644 --- a/src/activities/reader/QrDisplayActivity.cpp +++ b/src/activities/reader/QrDisplayActivity.cpp @@ -4,6 +4,7 @@ #include #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" #include "util/QrUtils.h" @@ -16,8 +17,11 @@ void QrDisplayActivity::onEnter() { void QrDisplayActivity::onExit() { Activity::onExit(); } void QrDisplayActivity::loop() { - if (mappedInput.wasReleased(MappedInputManager::Button::Back) || - mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + int x = 0; + int y = 0; + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer) || + mappedInput.wasReleased(MappedInputManager::Button::Back) || + mappedInput.wasReleased(MappedInputManager::Button::Confirm) || mappedInput.wasScreenTapped(x, y)) { finish(); return; } @@ -29,11 +33,17 @@ void QrDisplayActivity::render(RenderLock&&) { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_DISPLAY_QR), nullptr); + const Rect header{0, metrics.topPadding, pageWidth, TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_DISPLAY_QR), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_DISPLAY_QR), nullptr); + } const int availableWidth = pageWidth - 40; - const int availableHeight = pageHeight - metrics.topPadding - metrics.headerHeight - metrics.verticalSpacing * 2 - 40; - const int startY = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int availableHeight = pageHeight - metrics.topPadding - TouchHeaderBackButton::height(metrics, mappedInput) - + metrics.verticalSpacing * 2 - 40; + const int startY = metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; const Rect qrBounds(20, startY, availableWidth, availableHeight); QrUtils::drawQrCode(renderer, qrBounds, textPayload); diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 2ce00f9252..51ea575d43 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -6,9 +6,9 @@ #include #include "CrossPointSettings.h" +#include "CrossPointState.h" #include "Epub.h" #include "EpubReaderActivity.h" -#include "SdCardFontSystem.h" #include "Txt.h" #include "TxtReaderActivity.h" #include "Xtc.h" @@ -40,29 +40,62 @@ bool ReaderActivity::shouldShowLoadingPopup(const std::string& path) { return !Epub::hasCache(path, "/.crosspoint"); } -std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { +int ReaderActivity::initialRefreshCountdown() const { + if (!allowFastInitialRefresh) return 0; + + const int refreshFrequency = SETTINGS.getRefreshFrequency(); + return refreshFrequency > 1 ? refreshFrequency : 2; +} + +ReaderActivity::EpubOpenResult ReaderActivity::loadEpub(const std::string& path) { + EpubOpenResult result; if (!Storage.exists(path.c_str())) { LOG_ERR("READER", "File does not exist: %s", path.c_str()); - return nullptr; + return result; } auto epub = makeUniqueNoThrow(path, "/.crosspoint"); if (!epub) { LOG_ERR("READER", "Failed to allocate EPUB object"); - return nullptr; + result.failure = Epub::OpenFailure::OutOfMemory; + return result; } // First open: building the spine/TOC index (book.bin) takes a couple of seconds. Show the // indexing popup so it isn't a silent wait on the home screen. The cachePath/hash is known at // construction, so this check is valid before load(); a cached open loads in a blink -> no popup. - if (!Storage.exists((epub->getCachePath() + "/book.bin").c_str())) { + const bool uncached = !Storage.exists((epub->getCachePath() + "/book.bin").c_str()); + if (uncached) { + // The popup replaces the restored Quick Resume frame, so the reader must clean it. + allowFastInitialRefresh = false; GUI.drawPopup(renderer, tr(STR_INDEXING)); } - if (epub->load(true, SETTINGS.embeddedStyle == 0)) { - return epub; + // Keep one settings snapshot for both EPUB preparation and the reader handoff. + result.readerSettings = EpubReaderActivity::readBookReaderSettings(*epub); + // Lend the framebuffer's 48 KB for every EPUB load: even a cached book may + // rebuild stale/missing CSS and need miniz's ~43 KB streaming workspace. The + // panel keeps showing its last image, and the next activity redraws fully. + GfxRenderer::FrameBufferLoan loan(renderer); + const bool loaded = epub->load(true, result.readerSettings.readerSettings.embeddedStyle == 0); + loan.end(); + if (loaded) { + result.epub = std::move(epub); + result.failure = Epub::OpenFailure::None; + return result; } LOG_ERR("READER", "Failed to load epub"); - return nullptr; + result.failure = epub->getLastLoadFailure(); + return result; +} + +void ReaderActivity::queueEpubOpenAlert(const Epub::OpenFailure failure) { + const bool outOfMemory = failure == Epub::OpenFailure::OutOfMemory; + const char* title = outOfMemory ? tr(STR_MEMORY_ERROR) : tr(STR_INDEX_FAILED); + const char* body = outOfMemory ? tr(STR_EPUB_OPEN_MEMORY_BODY) : tr(STR_EPUB_OPEN_FAILED_BODY); + snprintf(APP_STATE.pendingAlertTitle, sizeof(APP_STATE.pendingAlertTitle), "%s", title); + snprintf(APP_STATE.pendingAlertBody, sizeof(APP_STATE.pendingAlertBody), "%s", body); + APP_STATE.pendingAlertGoHomeOnBack.store(false, std::memory_order_relaxed); + APP_STATE.hasPendingAlert.store(true, std::memory_order_release); } std::unique_ptr ReaderActivity::loadXtc(const std::string& path) { @@ -109,10 +142,13 @@ void ReaderActivity::goToLibrary(const std::string& fromBookPath) { activityManager.goToFileBrowser(std::move(initialPath)); } -void ReaderActivity::onGoToEpubReader(std::unique_ptr epub) { +void ReaderActivity::onGoToEpubReader(std::unique_ptr epub, + EpubReaderActivity::BookReaderSettingsData readerSettings) { const auto epubPath = epub->getPath(); currentBookPath = epubPath; - activityManager.replaceActivity(std::make_unique(renderer, mappedInput, std::move(epub))); + activityManager.replaceActivity( + std::make_unique(renderer, mappedInput, std::move(epub), std::move(readerSettings), + initialRefreshCountdown(), cleanImageBaseOnEntry)); } void ReaderActivity::onGoToBmpViewer(const std::string& path) { @@ -122,13 +158,15 @@ void ReaderActivity::onGoToBmpViewer(const std::string& path) { void ReaderActivity::onGoToXtcReader(std::unique_ptr xtc) { const auto xtcPath = xtc->getPath(); currentBookPath = xtcPath; - activityManager.replaceActivity(std::make_unique(renderer, mappedInput, std::move(xtc))); + activityManager.replaceActivity( + std::make_unique(renderer, mappedInput, std::move(xtc), initialRefreshCountdown())); } void ReaderActivity::onGoToTxtReader(std::unique_ptr txt) { const auto txtPath = txt->getPath(); currentBookPath = txtPath; - activityManager.replaceActivity(std::make_unique(renderer, mappedInput, std::move(txt))); + activityManager.replaceActivity( + std::make_unique(renderer, mappedInput, std::move(txt), initialRefreshCountdown())); } void ReaderActivity::onEnter() { @@ -152,8 +190,6 @@ void ReaderActivity::onEnter() { return; } - sdFontSystem.ensureLoaded(renderer); - currentBookPath = initialBookPath; if (isXtcFile(initialBookPath)) { auto xtc = loadXtc(initialBookPath); @@ -170,12 +206,13 @@ void ReaderActivity::onEnter() { } onGoToTxtReader(std::move(txt)); } else { - auto epub = loadEpub(initialBookPath); - if (!epub) { + auto result = loadEpub(initialBookPath); + if (!result.epub) { + queueEpubOpenAlert(result.failure); onGoBack(); return; } - onGoToEpubReader(std::move(epub)); + onGoToEpubReader(std::move(result.epub), std::move(result.readerSettings)); } } diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h index c4af7f019a..e649fbfa8d 100644 --- a/src/activities/reader/ReaderActivity.h +++ b/src/activities/reader/ReaderActivity.h @@ -1,6 +1,7 @@ #pragma once #include +#include "EpubReaderActivity.h" #include "activities/Activity.h" #include "activities/home/FileBrowserActivity.h" @@ -9,11 +10,19 @@ class Xtc; class Txt; class ReaderActivity final : public Activity { + struct EpubOpenResult { + std::unique_ptr epub; + EpubReaderActivity::BookReaderSettingsData readerSettings; + Epub::OpenFailure failure = Epub::OpenFailure::InvalidOrUnreadable; + }; + std::string initialBookPath; std::string currentBookPath; // Track current book path for navigation bool suppressInitialBackRelease = false; + bool allowFastInitialRefresh = false; + bool cleanImageBaseOnEntry = false; // Non-static (unlike the other loaders): draws the first-open indexing popup, which needs the renderer. - std::unique_ptr loadEpub(const std::string& path); + EpubOpenResult loadEpub(const std::string& path); static std::unique_ptr loadXtc(const std::string& path); static std::unique_ptr loadTxt(const std::string& path); static bool isXtcFile(const std::string& path); @@ -24,19 +33,24 @@ class ReaderActivity final : public Activity { static bool shouldShowLoadingPopup(const std::string& path); void goToLibrary(const std::string& fromBookPath = ""); - void onGoToEpubReader(std::unique_ptr epub); + void onGoToEpubReader(std::unique_ptr epub, EpubReaderActivity::BookReaderSettingsData readerSettings); void onGoToXtcReader(std::unique_ptr xtc); void onGoToTxtReader(std::unique_ptr txt); void onGoToBmpViewer(const std::string& path); + void queueEpubOpenAlert(Epub::OpenFailure failure); void onGoBack(); + int initialRefreshCountdown() const; public: explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath, - bool suppressInitialBackRelease = false) + bool suppressInitialBackRelease = false, bool allowFastInitialRefresh = false, + bool cleanImageBaseOnEntry = false) : Activity("Reader", renderer, mappedInput), initialBookPath(std::move(initialBookPath)), - suppressInitialBackRelease(suppressInitialBackRelease) {} + suppressInitialBackRelease(suppressInitialBackRelease), + allowFastInitialRefresh(allowFastInitialRefresh), + cleanImageBaseOnEntry(cleanImageBaseOnEntry) {} void onEnter() override; bool isReaderActivity() const override { return true; } }; diff --git a/src/activities/reader/ReaderOptionsActivity.cpp b/src/activities/reader/ReaderOptionsActivity.cpp index 39f1f0f0e6..2797fceed4 100644 --- a/src/activities/reader/ReaderOptionsActivity.cpp +++ b/src/activities/reader/ReaderOptionsActivity.cpp @@ -11,13 +11,16 @@ #include "MappedInputManager.h" #include "SdCardFontSystem.h" #include "SettingsList.h" -#include "activities/settings/FontDownloadActivity.h" +#include "SilentRestart.h" #include "activities/settings/FontSelectionActivity.h" #include "activities/settings/StatusBarSettingsActivity.h" #include "activities/util/IntervalSelectionActivity.h" #include "activities/util/OptionSelectionActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" -#include "fontIds.h" +#include "util/DictionaryRegistry.h" + +namespace fui = freeink::ui; namespace { uint8_t enumDisplayIndexForRawValue(const SettingInfo& setting, uint8_t rawValue) { @@ -84,6 +87,12 @@ void ReaderOptionsActivity::onEnter() { activeSubmenu = SettingAction::None; settingsDirty = false; rebuildSettingsList(); + uiReady = false; + visibleRows = 1; + topIndex = 0; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &ReaderOptionsActivity::onRowEvent, this); + app.setScreen(&ReaderOptionsActivity::optionsScreen, this); requestUpdate(); } @@ -93,8 +102,15 @@ void ReaderOptionsActivity::rebuildSettingsList() { pageLayoutSettings.clear(); sdFontSystem.refreshIfDirty(); const auto allSettings = getSettingsList(&sdFontSystem.registry()); - settings = buildReaderSettingsParentList(allSettings); - settings.push_back(buildReaderRenderModeSetting()); + settings = buildBookReaderSettingsParentList(allSettings); + const auto indexingMethod = std::find_if(settings.begin(), settings.end(), [](const SettingInfo& setting) { + return setting.nameId == StrId::STR_INDEXING_METHOD; + }); + if (indexingMethod == settings.end()) { + settings.push_back(buildReaderRenderModeSetting()); + } else { + settings.insert(indexingMethod, buildReaderRenderModeSetting()); + } fontSettings = buildReaderFontSettingsList(allSettings); pageLayoutSettings = buildReaderPageLayoutSettingsList(allSettings); fontSettings.erase(std::remove_if(fontSettings.begin(), fontSettings.end(), @@ -104,6 +120,72 @@ void ReaderOptionsActivity::rebuildSettingsList() { }), fontSettings.end()); + // Dictionary-specific font controls are useful only when an installed dictionary can use them. + DictionaryRegistry installedDictionaryRegistry; + const bool hasInstalledDictionaries = installedDictionaryRegistry.discover(); + installedDictionaryRegistry.clear(); + if (!hasInstalledDictionaries) { + setCurrentSettings(); + selectedIndex = 0; + return; + } + + SettingInfo dictionaryFont; + dictionaryFont.nameId = StrId::STR_DICTIONARY_FONT; + dictionaryFont.type = SettingType::ENUM; + dictionaryFont.key = "dictionaryFont"; + dictionaryFont.category = StrId::STR_CAT_READER; + const auto& families = sdFontSystem.registry().getFamilies(); + dictionaryFont.enumStringValues.reserve(families.size() + 2); + dictionaryFont.enumStringValues.push_back(tr(STR_DICT_USE_GLOBAL)); + bool selectedFamilyIsInstalled = false; + for (const auto& family : families) { + dictionaryFont.enumStringValues.push_back(family.name); + if (family.name == dictionaryFontFamilyName) { + selectedFamilyIsInstalled = true; + } + } + if (dictionaryFontFamilyName[0] != '\0' && !selectedFamilyIsInstalled) { + dictionaryFont.enumStringValues.push_back(std::string(dictionaryFontFamilyName) + " (" + tr(STR_UNAVAILABLE) + ")"); + } + const auto fontSize = std::find_if(fontSettings.begin(), fontSettings.end(), + [](const SettingInfo& setting) { return setting.nameId == StrId::STR_FONT_SIZE; }); + const size_t dictionaryFontIndex = + fontSize == fontSettings.end() ? 0 : static_cast(std::distance(fontSettings.begin(), fontSize) + 1); + fontSettings.insert(fontSettings.begin() + dictionaryFontIndex, std::move(dictionaryFont)); + + SettingInfo dictionaryFontSize; + dictionaryFontSize.nameId = StrId::STR_DICTIONARY_FONT_SIZE; + dictionaryFontSize.type = SettingType::ENUM; + dictionaryFontSize.key = "dictionaryFontSize"; + dictionaryFontSize.category = StrId::STR_CAT_READER; + dictionaryFontSize.enumStringValues.push_back(tr(STR_DICT_USE_GLOBAL)); + dictionaryFontSize.enumRawValues.push_back(0); + const char* dictionarySizeFamily = + dictionaryFontFamilyName[0] != '\0' ? dictionaryFontFamilyName : SETTINGS.sdFontFamilyName; + if (dictionarySizeFamily[0] != '\0') { + if (const auto* family = sdFontSystem.registry().findFamily(dictionarySizeFamily)) { + dictionaryFontSize.enumStringValues.reserve(family->files.size() + 1); + dictionaryFontSize.enumRawValues.reserve(family->files.size() + 1); + // Build a sorted, unique list directly from the catalog already resident + // for this screen. Dictionary lookup itself never retains a size list. + for (const auto& file : family->files) { + if (file.style != 0 || + std::find(dictionaryFontSize.enumRawValues.begin(), dictionaryFontSize.enumRawValues.end(), + file.pointSize) != dictionaryFontSize.enumRawValues.end()) { + continue; + } + const auto insertAt = std::lower_bound(dictionaryFontSize.enumRawValues.begin() + 1, + dictionaryFontSize.enumRawValues.end(), file.pointSize); + const size_t index = static_cast(std::distance(dictionaryFontSize.enumRawValues.begin(), insertAt)); + dictionaryFontSize.enumRawValues.insert(insertAt, file.pointSize); + dictionaryFontSize.enumStringValues.insert(dictionaryFontSize.enumStringValues.begin() + index, + fontSizePointLabel(file.pointSize)); + } + } + } + fontSettings.insert(fontSettings.begin() + dictionaryFontIndex + 1, std::move(dictionaryFontSize)); + setCurrentSettings(); selectedIndex = 0; } @@ -166,15 +248,20 @@ void ReaderOptionsActivity::openSubmenu(SettingAction action) { activeSubmenu = action; setCurrentSettings(); selectedIndex = 0; + topIndex = 0; } void ReaderOptionsActivity::closeSubmenu() { activeSubmenu = SettingAction::None; setCurrentSettings(); selectedIndex = 0; + topIndex = 0; } -void ReaderOptionsActivity::onExit() { Activity::onExit(); } +void ReaderOptionsActivity::onExit() { + sdFontSystem.releaseRegistry(); + Activity::onExit(); +} void ReaderOptionsActivity::moveSelection(bool forward) { if (settingsCount <= 0) return; @@ -183,6 +270,7 @@ void ReaderOptionsActivity::moveSelection(bool forward) { selectedIndex = forward ? ButtonNavigator::nextIndex(selectedIndex, settingsCount) : ButtonNavigator::previousIndex(selectedIndex, settingsCount); if ((*currentSettings)[selectedIndex].type != SettingType::SECTION_HEADER) { + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, settingsCount); break; } } @@ -227,6 +315,94 @@ void ReaderOptionsActivity::openEnumOptionPicker(const SettingInfo& setting) { requestUpdate(); } +void ReaderOptionsActivity::openDictionaryFontPicker(const SettingInfo& setting) { + const size_t optionCount = setting.enumStringValues.size(); + if (optionCount == 0) return; + + uint8_t currentIndex = 0; + if (hasDictionaryFontOverride && dictionaryFontFamilyName[0] != '\0') { + for (size_t i = 1; i < optionCount; ++i) { + if (setting.enumStringValues[i] == dictionaryFontFamilyName) { + currentIndex = static_cast(i); + break; + } + } + if (currentIndex == 0) currentIndex = static_cast(optionCount - 1); + } + + const SettingInfo selectedSetting = setting; + optionPopup.show(setting.nameId, selectedSetting.enumStringValues, currentIndex, [this, selectedSetting](int index) { + const char* familyName = nullptr; + if (index > 0 && index < static_cast(selectedSetting.enumStringValues.size())) { + const std::string& candidate = selectedSetting.enumStringValues[static_cast(index)]; + // An unavailable entry is only the retained current value. Keep it rather + // than saving its decorated display label. + familyName = sdFontSystem.registry().findFamily(candidate.c_str()) ? candidate.c_str() : dictionaryFontFamilyName; + } + + // ReaderOptions owns this name so the EPUB reader does not retain the + // extra 64 bytes during section layout. Update that owned copy before + // rebuilding the screen; otherwise it redraws the previously selected + // family even though the new value was persisted successfully. + if (familyName && familyName[0] != '\0') { + hasDictionaryFontOverride = true; + if (familyName != dictionaryFontFamilyName) { + std::strncpy(dictionaryFontFamilyName, familyName, sizeof(dictionaryFontFamilyName) - 1); + dictionaryFontFamilyName[sizeof(dictionaryFontFamilyName) - 1] = '\0'; + } + } else { + hasDictionaryFontOverride = false; + std::strncpy(dictionaryFontFamilyName, SETTINGS.dictionarySdFontFamilyName, sizeof(dictionaryFontFamilyName) - 1); + dictionaryFontFamilyName[sizeof(dictionaryFontFamilyName) - 1] = '\0'; + dictionaryFontPointSize = SETTINGS.dictionaryFontPointSize; + } + if (dictionaryFontChangedCallback) { + dictionaryFontChangedCallback(dictionaryFontChangedContext, + hasDictionaryFontOverride ? dictionaryFontFamilyName : nullptr, + dictionaryFontPointSize); + } + rebuildSettingsList(); + requestUpdate(); + }); + requestUpdate(); +} + +void ReaderOptionsActivity::openDictionaryFontSizePicker(const SettingInfo& setting) { + const size_t optionCount = setting.enumRawValues.size(); + if (optionCount == 0) return; + + uint8_t currentIndex = hasDictionaryFontOverride ? enumDisplayIndexForRawValue(setting, dictionaryFontPointSize) : 0; + if (currentIndex >= optionCount) currentIndex = 0; + const SettingInfo selectedSetting = setting; + optionPopup.show(setting.nameId, selectedSetting.enumStringValues, currentIndex, [this, selectedSetting](int index) { + const uint8_t pointSize = enumRawValueForDisplayIndex(selectedSetting, static_cast(index)); + if (!hasDictionaryFontOverride) { + if (index == 0) { + dictionaryFontPointSize = SETTINGS.dictionaryFontPointSize; + } else if (dictionaryFontFamilyName[0] != '\0') { + hasDictionaryFontOverride = true; + dictionaryFontPointSize = pointSize; + } else if (SETTINGS.sdFontFamilyName[0] != '\0') { + // Persist the reader family explicitly for this book so its size can + // differ from the global dictionary default after the menu closes. + std::strncpy(dictionaryFontFamilyName, SETTINGS.sdFontFamilyName, sizeof(dictionaryFontFamilyName) - 1); + dictionaryFontFamilyName[sizeof(dictionaryFontFamilyName) - 1] = '\0'; + hasDictionaryFontOverride = true; + dictionaryFontPointSize = pointSize; + } + } else { + dictionaryFontPointSize = pointSize; + } + if (dictionaryFontChangedCallback) { + dictionaryFontChangedCallback(dictionaryFontChangedContext, + hasDictionaryFontOverride ? dictionaryFontFamilyName : nullptr, + dictionaryFontPointSize); + } + requestUpdate(); + }); + requestUpdate(); +} + void ReaderOptionsActivity::openScreenMarginPicker(const SettingInfo& setting) { const uint8_t optionCount = valueOptionCount(setting); if (optionCount == 0 || setting.valuePtr == nullptr) return; @@ -243,7 +419,7 @@ void ReaderOptionsActivity::openScreenMarginPicker(const SettingInfo& setting) { const SettingInfo selectedSetting = setting; startActivityForResult( std::make_unique(renderer, mappedInput, "ReaderOptionsValueSelect", - selectedSetting.nameId, std::move(options), currentIndex, true), + selectedSetting.nameId, std::move(options), currentIndex, true, true), [this, selectedSetting](const ActivityResult& result) { if (result.isCancelled) { requestUpdate(); @@ -276,6 +452,16 @@ void ReaderOptionsActivity::toggleCurrentSetting() { return; } + if (setting.nameId == StrId::STR_DICTIONARY_FONT && setting.type == SettingType::ENUM) { + openDictionaryFontPicker(setting); + return; + } + + if (setting.nameId == StrId::STR_DICTIONARY_FONT_SIZE && setting.type == SettingType::ENUM) { + openDictionaryFontSizePicker(setting); + return; + } + if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { const bool cur = SETTINGS.*(setting.valuePtr); SETTINGS.*(setting.valuePtr) = !cur; @@ -325,13 +511,7 @@ void ReaderOptionsActivity::toggleCurrentSetting() { persistReaderSettings(); settingsDirty = false; } - startActivityForResult(std::make_unique(renderer, mappedInput), - [this](const ActivityResult&) { - persistGlobalSettings(); - sdFontSystem.refreshIfDirty(); - rebuildSettingsList(); - requestUpdate(); - }); + silentRestartToManageFonts(); return; } if (setting.action == SettingAction::CustomiseStatusBar) { @@ -360,7 +540,8 @@ void ReaderOptionsActivity::openLineHeightPicker() { renderer, mappedInput, "ReaderOptionsLineHeightInterval", StrId::STR_LINE_SPACING, SETTINGS.lineHeightPercent, CrossPointSettings::MIN_LINE_HEIGHT_PERCENT, CrossPointSettings::MAX_LINE_HEIGHT_PERCENT, 1, 10, StrId::STR_NONE_OPT, /*readerActivity=*/true, - /*allowPowerAsConfirm=*/true, /*ignoreInitialConfirmRelease=*/false, /*showPercentValue=*/true), + /*allowPowerAsConfirm=*/true, /*ignoreInitialConfirmRelease=*/false, /*showPercentValue=*/true, + StrId::STR_NONE_OPT, /*overrideDisabledReaderTouchscreen=*/false, /*showTouchHeaderBackButton=*/true), [this](const ActivityResult& result) { if (!result.isCancelled) { SETTINGS.lineHeightPercent = CrossPointSettings::clampedLineHeightPercent( @@ -373,6 +554,58 @@ void ReaderOptionsActivity::openLineHeightPicker() { void ReaderOptionsActivity::loop() { if (optionPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return; + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + if (activeSubmenu != SettingAction::None) { + closeSubmenu(); + requestUpdate(); + return; + } + if (settingsDirty) { + persistReaderSettings(); + settingsDirty = false; + } + finish(); + return; + } + if (mappedInput.wasHomeGesture()) { + if (settingsDirty) { + persistReaderSettings(); + settingsDirty = false; + } + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return; + } + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } + + if (mappedInput.hasTouch()) { + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up) { + const int next = scrollListBy(topIndex, visibleRows, visibleRows, settingsCount); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + if (swipe == MappedInputManager::SwipeDir::Down) { + const int next = scrollListBy(topIndex, -visibleRows, visibleRows, settingsCount); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + } buttonNavigator.onNextRelease([this] { moveSelection(true); @@ -405,73 +638,143 @@ void ReaderOptionsActivity::loop() { } } -void ReaderOptionsActivity::render(RenderLock&&) { - if (optionPopup.processRender(renderer, mappedInput)) return; +void ReaderOptionsActivity::optionsScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildOptionsScreen(screen); +} - renderer.clearScreen(); +void ReaderOptionsActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (self->optionPopup.isActive() || event.value < 0 || event.value >= self->settingsCount) return; + if ((*self->currentSettings)[event.value].type == SettingType::SECTION_HEADER) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->toggleCurrentSetting(); +} - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); +void ReaderOptionsActivity::buildOptionsScreen(UiApp::ScreenType& screen) { const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, !mappedInput.hasTouchHardware(), false); + screen.setContentMargin(fui::Insets{ + static_cast(safe.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), + static_cast(renderer.getScreenWidth() - safe.x - safe.width), + static_cast(renderer.getScreenHeight() - safe.y - safe.height), static_cast(safe.x)}); + screen.spacer(static_cast(metrics.verticalSpacing)); - const auto orientation = renderer.getOrientation(); - const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; - const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; - const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? metrics.buttonHintsHeight : 0; - const int contentX = isLandscapeCw ? hintGutterWidth : 0; - const int contentWidth = pageWidth - hintGutterWidth; - - GUI.drawHeader(renderer, Rect{contentX, metrics.topPadding, contentWidth, metrics.headerHeight}, - tr(STR_READER_OPTIONS), nullptr, true); - - const auto& visibleSettings = *currentSettings; - Rect listRect{contentX, metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing, contentWidth, - pageHeight - (metrics.topPadding + metrics.headerHeight + metrics.buttonHintsHeight + - metrics.verticalSpacing * 2)}; const StrId submenuTitleId = activeSubmenuTitleId(); if (submenuTitleId != StrId::STR_NONE_OPT) { - constexpr int submenuHeaderFontId = UI_10_FONT_ID; - const int headerLineHeight = renderer.getLineHeight(submenuHeaderFontId); - const int headerOffset = headerLineHeight + metrics.verticalSpacing; - const int headerMaxWidth = listRect.width - metrics.contentSidePadding * 2; - const auto headerLabel = - renderer.truncatedText(submenuHeaderFontId, I18N.get(submenuTitleId), headerMaxWidth, EpdFontFamily::BOLD); - renderer.drawText(submenuHeaderFontId, listRect.x + metrics.contentSidePadding, listRect.y, headerLabel.c_str(), - true, EpdFontFamily::BOLD); - listRect.y += headerOffset; - listRect.height = std::max(0, listRect.height - headerOffset); - } - - GUI.drawList( - renderer, listRect, settingsCount, selectedIndex, - [&visibleSettings](int i) { return std::string(I18N.get(visibleSettings[i].nameId)); }, nullptr, nullptr, - [&visibleSettings](int i) { - const auto& setting = visibleSettings[i]; - std::string valueText; - if (settingShowsNavigationCaret(setting)) { - valueText = ">"; - } else if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - valueText = SETTINGS.*(setting.valuePtr) ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); - } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { - const uint8_t value = SETTINGS.*(setting.valuePtr); - const uint8_t displayValue = enumDisplayIndexForRawValue(setting, value); - const size_t optionCount = settingEnumOptionCount(setting); - const uint8_t safeValue = displayValue < optionCount ? displayValue : 0; - valueText = settingEnumOptionLabel(setting, safeValue); - } else if (setting.type == SettingType::ENUM && setting.valueGetter) { - const uint8_t value = setting.valueGetter(); - valueText = settingEnumOptionLabel(setting, value); - } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - valueText = formatSettingValue(setting); + fui::TextStyle titleStyle = screen.theme().smallText; + titleStyle.bold = true; + titleStyle.maxLines = 1; + const int16_t titleHeight = screen.target().lineHeight(titleStyle.font); + fui::Rect titleRect = screen.takeTop(titleHeight, static_cast(metrics.verticalSpacing)); + const int16_t sidePadding = static_cast(metrics.contentSidePadding); + titleRect.x = static_cast(titleRect.x + sidePadding); + titleRect.width = static_cast(titleRect.width > sidePadding * 2 ? titleRect.width - sidePadding * 2 : 0); + screen.target().text(titleRect, I18N.get(submenuTitleId), titleStyle); + } + + const auto& currentSettingsList = *currentSettings; + std::vector values(currentSettingsList.size()); + std::vector items; + items.reserve(currentSettingsList.size()); + for (size_t i = 0; i < currentSettingsList.size(); ++i) { + const auto& setting = currentSettingsList[i]; + if (settingShowsNavigationCaret(setting)) { + values[i] = ">"; + } else if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { + values[i] = SETTINGS.*(setting.valuePtr) ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); + } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { + const uint8_t displayValue = enumDisplayIndexForRawValue(setting, SETTINGS.*(setting.valuePtr)); + values[i] = settingEnumOptionLabel(setting, displayValue < settingEnumOptionCount(setting) ? displayValue : 0); + } else if (setting.type == SettingType::ENUM && setting.valueGetter) { + values[i] = settingEnumOptionLabel(setting, setting.valueGetter()); + } else if (setting.nameId == StrId::STR_DICTIONARY_FONT) { + if (!hasDictionaryFontOverride) { + values[i] = tr(STR_DICT_USE_GLOBAL); + if (dictionaryFontFamilyName[0] != '\0') { + values[i] += " ("; + values[i] += dictionaryFontFamilyName; + values[i] += ')'; } - return valueText; - }, - true, nullptr, [&visibleSettings](int i) { return visibleSettings[i].type == SettingType::SECTION_HEADER; }); + } else if (dictionaryFontFamilyName[0] != '\0') { + values[i] = dictionaryFontFamilyName; + if (!sdFontSystem.registry().findFamily(dictionaryFontFamilyName)) { + values[i] += " ("; + values[i] += tr(STR_UNAVAILABLE); + values[i] += ')'; + } + } else { + values[i] = tr(STR_USE_READER_FONT); + } + } else if (setting.nameId == StrId::STR_DICTIONARY_FONT_SIZE) { + if (!hasDictionaryFontOverride) { + values[i] = tr(STR_DICT_USE_GLOBAL); + if (dictionaryFontPointSize != 0) { + values[i] += " ("; + values[i] += fontSizePointLabel(dictionaryFontPointSize); + values[i] += ')'; + } + } else { + const uint8_t displayValue = enumDisplayIndexForRawValue(setting, dictionaryFontPointSize); + values[i] = settingEnumOptionLabel(setting, displayValue < settingEnumOptionCount(setting) ? displayValue : 0); + } + } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { + values[i] = formatSettingValue(setting); + } + + const bool isSectionHeader = setting.type == SettingType::SECTION_HEADER; + fui::ListItem item; + item.label = + isSectionHeader ? uiListSectionHeaderLabel(values[i], I18N.get(setting.nameId)) : I18N.get(setting.nameId); + if (!isSectionHeader && !values[i].empty()) item.value = values[i].c_str(); + item.isHeader = isSectionHeader; + item.actionValue = static_cast(i); + items.push_back(item); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.valueInset = 8; + props.labelText = screen.theme().bodyText; + props.labelText.maxLines = 2; + configureUiListSectionHeaders(props, screen.theme()); + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, settingsCount); + props.topIndex = static_cast(topIndex); + screen.list(props); +} + +void ReaderOptionsActivity::render(RenderLock&&) { + if (optionPopup.processRender(renderer, mappedInput)) return; + + renderer.clearScreen(); + + const auto& metrics = UITheme::getInstance().getMetrics(); + Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, !mappedInput.hasTouchHardware(), false); + header.x = safe.x; + header.width = safe.width; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_READER_OPTIONS), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_READER_OPTIONS), nullptr, true); + } + + uiReady = false; + app.render(); + uiReady = true; const bool currentIsAction = selectedIndex >= 0 && selectedIndex < settingsCount && ((*currentSettings)[selectedIndex].type == SettingType::ACTION || (*currentSettings)[selectedIndex].type == SettingType::SUBMENU || (*currentSettings)[selectedIndex].nameId == StrId::STR_FONT_FAMILY || + (*currentSettings)[selectedIndex].nameId == StrId::STR_DICTIONARY_FONT || + (*currentSettings)[selectedIndex].nameId == StrId::STR_DICTIONARY_FONT_SIZE || currentSettingUsesOptionMenu((*currentSettings)[selectedIndex])); const bool selectedLineHeight = selectedIndex >= 0 && selectedIndex < settingsCount && (*currentSettings)[selectedIndex].valuePtr == &CrossPointSettings::lineHeightPercent; diff --git a/src/activities/reader/ReaderOptionsActivity.h b/src/activities/reader/ReaderOptionsActivity.h index dc5dbf9174..189be752d7 100644 --- a/src/activities/reader/ReaderOptionsActivity.h +++ b/src/activities/reader/ReaderOptionsActivity.h @@ -1,11 +1,17 @@ #pragma once +#include +#include #include +#include +#include #include #include "../Activity.h" #include "../settings/SettingsActivity.h" #include "components/OptionPopup.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "util/ButtonNavigator.h" class ReaderOptionsActivity final : public Activity { @@ -13,6 +19,7 @@ class ReaderOptionsActivity final : public Activity { using SaveSettingsCallback = void (*)(void* ctx); using SaveGlobalSettingsCallback = void (*)(void* ctx); using GlobalSettingsEditCallback = void (*)(void* ctx); + using DictionaryFontChangedCallback = void (*)(void* ctx, const char* familyName, uint8_t pointSize); private: ButtonNavigator buttonNavigator; @@ -32,9 +39,22 @@ class ReaderOptionsActivity final : public Activity { void* beginGlobalSettingsEditContext = nullptr; GlobalSettingsEditCallback endGlobalSettingsEditCallback = nullptr; void* endGlobalSettingsEditContext = nullptr; + char dictionaryFontFamilyName[64] = ""; + uint8_t dictionaryFontPointSize = 0; + bool hasDictionaryFontOverride = false; + DictionaryFontChangedCallback dictionaryFontChangedCallback = nullptr; + void* dictionaryFontChangedContext = nullptr; bool settingsDirty = false; bool stablePageNumbersAvailable = false; + using UiApp = freeink::ui::FreeInkApp<20, 4>; + static constexpr freeink::ui::ActionId ACTION_ROW = 1; + freeink::ui::GfxRendererTarget uiTarget; // Must precede app: the app holds a reference to it. + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + void rebuildSettingsList(); void setCurrentSettings(); StrId activeSubmenuTitleId() const; @@ -43,6 +63,8 @@ class ReaderOptionsActivity final : public Activity { void moveSelection(bool forward); bool currentSettingUsesOptionMenu(const SettingInfo& setting) const; void openEnumOptionPicker(const SettingInfo& setting); + void openDictionaryFontPicker(const SettingInfo& setting); + void openDictionaryFontSizePicker(const SettingInfo& setting); void openScreenMarginPicker(const SettingInfo& setting); void toggleCurrentSetting(); void openLineHeightPicker(); @@ -50,17 +72,21 @@ class ReaderOptionsActivity final : public Activity { void persistGlobalSettings(); void beginGlobalSettingsEdit(); void endGlobalSettingsEdit(); + static void optionsScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildOptionsScreen(UiApp::ScreenType& screen); public: - explicit ReaderOptionsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - SaveSettingsCallback saveSettingsCallback = nullptr, - void* saveSettingsContext = nullptr, - SaveGlobalSettingsCallback saveGlobalSettingsCallback = nullptr, - void* saveGlobalSettingsContext = nullptr, - GlobalSettingsEditCallback beginGlobalSettingsEditCallback = nullptr, - void* beginGlobalSettingsEditContext = nullptr, - GlobalSettingsEditCallback endGlobalSettingsEditCallback = nullptr, - void* endGlobalSettingsEditContext = nullptr, bool stablePageNumbersAvailable = false) + explicit ReaderOptionsActivity( + GfxRenderer& renderer, MappedInputManager& mappedInput, SaveSettingsCallback saveSettingsCallback = nullptr, + void* saveSettingsContext = nullptr, SaveGlobalSettingsCallback saveGlobalSettingsCallback = nullptr, + void* saveGlobalSettingsContext = nullptr, GlobalSettingsEditCallback beginGlobalSettingsEditCallback = nullptr, + void* beginGlobalSettingsEditContext = nullptr, + GlobalSettingsEditCallback endGlobalSettingsEditCallback = nullptr, void* endGlobalSettingsEditContext = nullptr, + bool stablePageNumbersAvailable = false, const char* dictionaryFontFamilyName = nullptr, + uint8_t dictionaryFontPointSize = 0, bool hasDictionaryFontOverride = false, + DictionaryFontChangedCallback dictionaryFontChangedCallback = nullptr, + void* dictionaryFontChangedContext = nullptr) : Activity("ReaderOptions", renderer, mappedInput), saveSettingsCallback(saveSettingsCallback), saveSettingsContext(saveSettingsContext), @@ -70,10 +96,22 @@ class ReaderOptionsActivity final : public Activity { beginGlobalSettingsEditContext(beginGlobalSettingsEditContext), endGlobalSettingsEditCallback(endGlobalSettingsEditCallback), endGlobalSettingsEditContext(endGlobalSettingsEditContext), - stablePageNumbersAvailable(stablePageNumbersAvailable) {} + dictionaryFontChangedCallback(dictionaryFontChangedCallback), + dictionaryFontChangedContext(dictionaryFontChangedContext), + dictionaryFontPointSize(dictionaryFontPointSize), + hasDictionaryFontOverride(hasDictionaryFontOverride), + stablePageNumbersAvailable(stablePageNumbersAvailable), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) { + if (dictionaryFontFamilyName) { + std::strncpy(this->dictionaryFontFamilyName, dictionaryFontFamilyName, + sizeof(this->dictionaryFontFamilyName) - 1); + } + } void onEnter() override; void onExit() override; void loop() override; void render(RenderLock&&) override; bool allowPowerAsConfirmInReaderMode() const override { return true; } + bool allowGlobalHomeGesture() const override { return false; } }; diff --git a/src/activities/reader/ReaderProgressSaveDebouncer.h b/src/activities/reader/ReaderProgressSaveDebouncer.h new file mode 100644 index 0000000000..3704ae5cca --- /dev/null +++ b/src/activities/reader/ReaderProgressSaveDebouncer.h @@ -0,0 +1,71 @@ +#pragma once + +#include + +#include + +// Allocation-free save policy adapted from Sichroteph/YACP commit +// 3f3c5fc42e794c021edb9832856ef98c2d2065b9 (MIT). Callers retain ownership +// of their persistence format and explicitly flush pending state on exit. +class ReaderProgressSaveDebouncer { + static constexpr uint8_t PAGE_CHANGE_INTERVAL = 10; + static constexpr unsigned long MAX_SAVE_INTERVAL_MS = 5UL * 60UL * 1000UL; + + uint32_t lastPositionKey_ = 0; + uint32_t lastMetadataKey_ = 0; + unsigned long lastPersistedAtMs_ = 0; + uint8_t pendingPageChanges_ = 0; + bool initialized_ = false; + bool pending_ = false; + + public: + bool observe(const uint32_t positionKey) { return observe(positionKey, 0); } + + bool observe(const uint32_t positionKey, const uint32_t metadataKey) { + const unsigned long now = millis(); + if (!initialized_) { + initialized_ = true; + lastPositionKey_ = positionKey; + lastMetadataKey_ = metadataKey; + lastPersistedAtMs_ = now; + pendingPageChanges_ = 1; + pending_ = true; + return false; + } + + if (positionKey != lastPositionKey_) { + lastPositionKey_ = positionKey; + pending_ = true; + if (pendingPageChanges_ < UINT8_MAX) { + ++pendingPageChanges_; + } + } + if (metadataKey != lastMetadataKey_) { + lastMetadataKey_ = metadataKey; + pending_ = true; + } + + return pending_ && + (pendingPageChanges_ >= PAGE_CHANGE_INTERVAL || now - lastPersistedAtMs_ >= MAX_SAVE_INTERVAL_MS); + } + + bool hasPending() const { return pending_; } + uint32_t lastObservedPosition() const { return lastPositionKey_; } + uint32_t lastObservedMetadata() const { return lastMetadataKey_; } + + void markPersisted(const uint32_t positionKey) { markPersisted(positionKey, 0); } + + void markPersisted(const uint32_t positionKey, const uint32_t metadataKey) { + if (!initialized_) { + initialized_ = true; + lastPositionKey_ = positionKey; + lastMetadataKey_ = metadataKey; + } else if (positionKey != lastPositionKey_ || metadataKey != lastMetadataKey_) { + return; + } + + pending_ = false; + pendingPageChanges_ = 0; + lastPersistedAtMs_ = millis(); + } +}; diff --git a/src/activities/reader/ReaderUtils.h b/src/activities/reader/ReaderUtils.h index 3cfd14e768..0634efd18a 100644 --- a/src/activities/reader/ReaderUtils.h +++ b/src/activities/reader/ReaderUtils.h @@ -80,6 +80,76 @@ struct PageTurnResult { bool fromTilt; }; +struct TouchPageTurn { + bool prev; + bool next; + bool tapped; + int x; + int y; + unsigned long heldMs; +}; + +inline TouchPageTurn detectTouchPageTurn(const GfxRenderer& renderer, const MappedInputManager& input) { +#if !CROSSINK_APP_CAP_TOUCH + (void)renderer; + (void)input; + return {false, false, false, 0, 0, 0}; +#else + TouchPageTurn result{false, false, false, 0, 0, 0}; + if (!SETTINGS.touchReaderControls || !input.hasTouch()) { + return result; + } + + const int width = renderer.getScreenWidth(); + if (width <= 0) { + return result; + } + + const auto swipe = input.wasSwipe(); + if (swipe != MappedInputManager::SwipeDir::None) { + // A horizontal reader swipe turns pages wherever it starts. Edge-only + // navigation remains handled by the activities that explicitly use it. + result.prev = swipe == MappedInputManager::SwipeDir::Right; + result.next = swipe == MappedInputManager::SwipeDir::Left; + return result; + } + + int x = 0; + int y = 0; + if (!input.wasScreenTapped(x, y)) { + return result; + } + result.tapped = true; + result.x = x; + result.y = y; + // Reserve the top/bottom gesture bands for vertical edge swipes. If the touch + // controller loses part of a short edge swipe, do not reinterpret it as a page tap. + if (input.isInVerticalEdgeGestureZone(y)) { + return result; + } + + const int previousZoneWidth = width / 3; + result.prev = x < previousZoneWidth; + result.next = x >= previousZoneWidth; + result.heldMs = input.getHeldTime(); + return result; +#endif +} + +// Reader menu opens on its board-specific vertical swipe anywhere on the open +// page, or a long press of the capacitive home key (a short home tap still goes home). +inline bool isTouchMenuGesture(const MappedInputManager& input) { + return SETTINGS.touchReaderControls && input.hasTouch() && + (input.wasReaderMenuGesture() || input.wasReaderMenuHold()); +} + +// X4 Pro opens the reader menu with an upward swipe. Its top-edge downward +// swipe is the opposite gesture and dismisses the menu instead of opening the +// frontlight panel. Other touch boards retain their existing menu behavior. +inline bool isTouchMenuDismissGesture(const MappedInputManager& input) { + return SETTINGS.touchReaderControls && input.hasTouch() && input.hasHomeKey() && input.wasLightPanelGesture(); +} + inline PageTurnResult detectPageTurn(const MappedInputManager& input) { // Side buttons fire on press only when long-press action is OFF (nothing to detect). const bool sideUsePress = SETTINGS.sideButtonLongPress == CrossPointSettings::SIDE_LONG_PRESS::SIDE_LONG_OFF; @@ -105,12 +175,21 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) { return {tiltPrev || sidePrev || frontPrev, tiltNext || sideNext || frontNext, fromSide, tiltPrev || tiltNext}; } -inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) { +// One helper, blocking or deferred: the async form starts the refresh and +// returns so the caller can overlap CPU work with the panel's refresh time. +// Async callers must not touch the framebuffer until +// renderer.waitRefreshComplete() and must rebuild the differential baseline +// before the next page turn (the tiled grayscale cleanup does). +inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh, bool async = false) { + const auto mode = (pagesUntilFullRefresh <= 1) ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH; + if (async) { + renderer.displayBufferAsync(mode); + } else { + renderer.displayBuffer(mode); + } if (pagesUntilFullRefresh <= 1) { - renderer.displayBuffer(HalDisplay::HALF_REFRESH); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { - renderer.displayBuffer(); pagesUntilFullRefresh--; } } diff --git a/src/activities/reader/ReadingStatsUtils.cpp b/src/activities/reader/ReadingStatsUtils.cpp index 1957f0d6e3..21ac203e00 100644 --- a/src/activities/reader/ReadingStatsUtils.cpp +++ b/src/activities/reader/ReadingStatsUtils.cpp @@ -306,6 +306,30 @@ void formatReadingStatsMonthToken(const ReadingStatsDate& date, char* buf, const snprintf(buf, len, "%s", MONTH_NAMES[date.month - 1]); } +void formatCompactReadingDuration(const uint32_t seconds, char* buf, const size_t len) { + if (!buf || len == 0) { + return; + } + if (seconds < 60) { + snprintf(buf, len, "<1m"); + return; + } + + const uint32_t minutes = (seconds + 30U) / 60U; + if (minutes < 60) { + snprintf(buf, len, "%lum", static_cast(minutes)); + return; + } + + const uint32_t hours = minutes / 60U; + const uint32_t remainingMinutes = minutes % 60U; + if (remainingMinutes == 0) { + snprintf(buf, len, "%luh", static_cast(hours)); + } else { + snprintf(buf, len, "%luh %lum", static_cast(hours), static_cast(remainingMinutes)); + } +} + void recordReadingSpanIntoBuckets(std::array& timeOfDaySeconds, std::array& dayOfWeekSeconds, const ReadingStatsDateTime& localStart, const uint32_t seconds) { diff --git a/src/activities/reader/ReadingStatsUtils.h b/src/activities/reader/ReadingStatsUtils.h index 671d5cd654..d428e5cc8b 100644 --- a/src/activities/reader/ReadingStatsUtils.h +++ b/src/activities/reader/ReadingStatsUtils.h @@ -44,6 +44,7 @@ uint16_t readingSpanDaysInclusive(const ReadingStatsDate& start, const ReadingSt uint16_t readingSpanDaysElapsed(const ReadingStatsDate& start, const ReadingStatsDate& end); void formatReadingStatsShortDate(const ReadingStatsDate& date, char* buf, size_t len); void formatReadingStatsMonthToken(const ReadingStatsDate& date, char* buf, size_t len); +void formatCompactReadingDuration(uint32_t seconds, char* buf, size_t len); void recordReadingSpanIntoBuckets(std::array& timeOfDaySeconds, std::array& dayOfWeekSeconds, diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 30badbc59f..6bd88f7bd9 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -18,6 +19,7 @@ #include "RecentBooksStore.h" #include "SdCardFontSystem.h" #include "activities/boot_sleep/SleepCoverAssets.h" +#include "activities/home/FileBrowserActionActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -103,6 +105,7 @@ void TxtReaderActivity::onEnter() { return; } + sdFontSystem.ensureLoaded(renderer); ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); // Activate reader-specific front button mapping (if configured). @@ -129,6 +132,10 @@ void TxtReaderActivity::onExit() { // Deactivate reader-specific front button mapping. mappedInput.setReaderMode(false); + if (!flushQueuedProgress()) { + LOG_ERR("TRS", "Failed to flush debounced reader progress on exit"); + } + // Reset orientation back to portrait for the rest of the UI renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -139,6 +146,27 @@ void TxtReaderActivity::onExit() { txt.reset(); } +void TxtReaderActivity::openReaderMenu() { + if (!txt) return; + std::vector items; + items.push_back({FileBrowserAction::SendNearby, StrId::STR_SEND_NEARBY_BOOK}); + auto menu = makeUniqueNoThrow(renderer, mappedInput, txt->getTitle(), std::move(items)); + if (!menu) { + LOG_ERR("NBOOK", "OOM: TXT nearby transfer menu"); + return; + } + startActivityForResult(std::move(menu), [this](const ActivityResult& result) { + const auto* action = std::get_if(&result.data); + if (!result.isCancelled && action && + static_cast(action->action) == FileBrowserAction::SendNearby) { + saveProgress(currentPage); + activityManager.goToNearbyBookSend(txt ? txt->getPath() : std::string{}, true); + } else { + requestUpdate(); + } + }); +} + void TxtReaderActivity::loop() { if (consumeLongPowerButtonRelease()) { return; @@ -147,6 +175,11 @@ void TxtReaderActivity::loop() { return; } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + openReaderMenu(); + return; + } + if (longPressBackHandled) { if (mappedInput.wasReleased(MappedInputManager::Button::Back) || !mappedInput.isPressed(MappedInputManager::Button::Back)) { @@ -403,8 +436,6 @@ void TxtReaderActivity::initializeReader() { linesPerPage = viewportHeight / lineHeight; if (linesPerPage < 1) linesPerPage = 1; - LOG_DBG("TRS", "Viewport: %dx%d, lines per page: %d", viewportWidth, viewportHeight, linesPerPage); - // Try to load cached page index first if (!loadPageIndexCache()) { // Cache not found, build page index @@ -426,8 +457,6 @@ void TxtReaderActivity::buildPageIndex() { size_t offset = 0; const size_t fileSize = txt->getFileSize(); - LOG_DBG("TRS", "Building page index for %zu bytes...", fileSize); - GUI.drawPopup(renderer, tr(STR_INDEXING)); while (offset < fileSize) { @@ -455,7 +484,6 @@ void TxtReaderActivity::buildPageIndex() { } totalPages = pageOffsets.size(); - LOG_DBG("TRS", "Built page index: %d pages", totalPages); } bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector& outLines, size_t& nextOffset) { @@ -530,8 +558,9 @@ void TxtReaderActivity::render(RenderLock&&) { renderer.clearScreen(ReaderUtils::readerBackgroundColor()); renderPage(); - // Save progress - saveProgress(); + if (!queueProgressSave()) { + LOG_ERR("TRS", "Failed to save debounced reader progress"); + } } void TxtReaderActivity::renderPage() { @@ -601,29 +630,51 @@ void TxtReaderActivity::renderPage() { void TxtReaderActivity::renderStatusBar() const { const float progress = totalPages > 0 ? (currentPage + 1) * 100.0f / totalPages : 0; std::string title; - if (SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE) { + if (SETTINGS.statusBarSpec().showsTitle()) { title = txt->getTitle(); } GUI.drawStatusBar(renderer, progress, currentPage + 1, totalPages, title, 0, 0, false, nullptr, ReaderUtils::readerDarkModeEnabled()); } -void TxtReaderActivity::saveProgress() const { +bool TxtReaderActivity::saveProgress(const int page) { + if (!txt) { + return false; + } HalFile f; - if (Storage.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) { - // 6-byte format: page(2 bytes LE) + file offset(4 bytes LE) - // The offset lets drawCurrentPageToBuffer render without requiring index.bin. - const size_t offset = (currentPage < static_cast(pageOffsets.size())) ? pageOffsets[currentPage] : 0; - uint8_t data[6]; - data[0] = currentPage & 0xFF; - data[1] = (currentPage >> 8) & 0xFF; - data[2] = offset & 0xFF; - data[3] = (offset >> 8) & 0xFF; - data[4] = (offset >> 16) & 0xFF; - data[5] = (offset >> 24) & 0xFF; - f.write(data, 6); - f.close(); + if (!Storage.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) { + return false; + } + // 6-byte format: page(2 bytes LE) + file offset(4 bytes LE) + // The offset lets drawCurrentPageToBuffer render without requiring index.bin. + const size_t offset = (page >= 0 && page < static_cast(pageOffsets.size())) ? pageOffsets[page] : 0; + uint8_t data[6]; + data[0] = page & 0xFF; + data[1] = (page >> 8) & 0xFF; + data[2] = offset & 0xFF; + data[3] = (offset >> 8) & 0xFF; + data[4] = (offset >> 16) & 0xFF; + data[5] = (offset >> 24) & 0xFF; + const bool written = f.write(data, sizeof(data)) == sizeof(data); + f.close(); + if (!written) { + LOG_ERR("TRS", "Short write saving reader progress"); + return false; } + progressSaveDebouncer.markPersisted(static_cast(page)); + return true; +} + +bool TxtReaderActivity::queueProgressSave() { + if (!progressSaveDebouncer.observe(static_cast(currentPage))) { + return true; + } + return saveProgress(currentPage); +} + +bool TxtReaderActivity::flushQueuedProgress() { + return !progressSaveDebouncer.hasPending() || + saveProgress(static_cast(progressSaveDebouncer.lastObservedPosition())); } void TxtReaderActivity::loadProgress() { @@ -638,7 +689,6 @@ void TxtReaderActivity::loadProgress() { if (currentPage < 0) { currentPage = 0; } - LOG_DBG("TRS", "Loaded progress: page %d/%d", currentPage, totalPages); } } } @@ -739,7 +789,6 @@ bool TxtReaderActivity::loadPageIndexCache() { } totalPages = pageOffsets.size(); - LOG_DBG("TRS", "Loaded page index cache: %d pages", totalPages); return true; } @@ -766,8 +815,6 @@ void TxtReaderActivity::savePageIndexCache() const { for (size_t offset : pageOffsets) { serialization::writePod(f, static_cast(offset)); } - - LOG_DBG("TRS", "Saved page index cache: %d pages", totalPages); } bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) { diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index 46d89ac1f3..dc4e4a123c 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -5,6 +5,7 @@ #include #include "CrossPointSettings.h" +#include "ReaderProgressSaveDebouncer.h" #include "activities/Activity.h" class TxtReaderActivity final : public Activity { @@ -17,6 +18,7 @@ class TxtReaderActivity final : public Activity { bool frontButtonLongPressHandled = false; bool longPowerButtonHandled = false; bool longPressBackHandled = false; + ReaderProgressSaveDebouncer progressSaveDebouncer; // Streaming text reader - stores file offsets for each page std::vector pageOffsets; // File offset for start of each page @@ -42,23 +44,34 @@ class TxtReaderActivity final : public Activity { void buildPageIndex(); bool loadPageIndexCache(); void savePageIndexCache() const; - void saveProgress() const; + bool saveProgress(int page); + bool queueProgressSave(); + bool flushQueuedProgress(); void loadProgress(); void toggleDarkMode(); bool consumeLongPowerButtonRelease(); bool consumeLongPowerButtonHold(); bool executePowerButtonAction(); bool executeLongPressBackAction(); + void openReaderMenu(); public: - explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr txt) - : Activity("TxtReader", renderer, mappedInput), txt(std::move(txt)) {} + explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr txt, + int initialRefreshCountdown) + : Activity("TxtReader", renderer, mappedInput), + txt(std::move(txt)), + pagesUntilFullRefresh(initialRefreshCountdown) {} void onEnter() override; void onExit() override; void loop() override; void render(RenderLock&&) override; + bool prepareManualRefresh() override { + pagesUntilFullRefresh = 1; + return true; + } bool isReaderActivity() const override { return true; } bool canSnapshotForSleepOverlay() const override { return true; } + bool handlesReaderPowerSettingsOverride() const override { return true; } std::string getCurrentBookPath() const override { return txt ? txt->getPath() : std::string{}; } // Renders the last saved page to the frame buffer without flushing to display. diff --git a/src/activities/reader/WordRef.h b/src/activities/reader/WordRef.h index cbc7b54f3c..620b6c60a9 100644 --- a/src/activities/reader/WordRef.h +++ b/src/activities/reader/WordRef.h @@ -3,7 +3,10 @@ #include #include +#include #include +#include +#include struct WordRef { int x = 0; @@ -12,9 +15,39 @@ struct WordRef { int h = 0; int pageIdx = 0; uint16_t pageWordIndex = 0; - std::string text; + uint16_t textOffset = 0; + uint16_t textLength = 0; EpdFontFamily::Style style = EpdFontFamily::REGULAR; bool paragraphStart = false; bool endsWithInsertedHyphen = false; bool lineIsRtl = false; }; + +static_assert(std::is_trivially_copyable_v, "Clip word metadata must stay allocation-free"); + +struct ClipWordStore { + static constexpr size_t MAX_TEXT_POOL_BYTES = UINT16_MAX; + + std::vector words; + std::string textPool; + + const char* text(const WordRef& word) const { + return word.textOffset < textPool.size() ? textPool.data() + word.textOffset : ""; + } + + bool canAppend(const size_t textLength) const { + return textLength <= UINT16_MAX && textPool.size() <= MAX_TEXT_POOL_BYTES && + textLength + 1 <= MAX_TEXT_POOL_BYTES - textPool.size(); + } + + bool appendText(WordRef& word, const char* text) { + const size_t length = strlen(text); + if (!canAppend(length)) return false; + + word.textOffset = static_cast(textPool.size()); + word.textLength = static_cast(length); + textPool.append(text, length); + textPool.push_back('\0'); + return true; + } +}; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index a2984eff54..821efc9739 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -32,6 +32,7 @@ namespace { constexpr unsigned long MIN_READING_STATS_PAGE_MS = 2000UL; +constexpr uint16_t MIN_TIME_LEFT_PACE_SAMPLE_COUNT = 3; std::string confirmationHeading(const StrId actionLabelId) { return std::string(tr(STR_CONFIRM)) + ": " + std::string(I18N.get(actionLabelId)); @@ -52,6 +53,54 @@ void drawToast(const GfxRenderer& renderer, const char* msg) { renderer.drawText(UI_10_FONT_ID, toastX + toastPadX, toastY + toastPadY, msg, !toastBackgroundBlack); renderer.displayBuffer(); } + +enum class XtchRenderPass { Base, Lsb, Msb }; + +bool streamXtchRenderPass(const Xtc& xtc, const uint32_t pageIndex, const uint16_t pageWidth, const uint16_t pageHeight, + GfxRenderer& renderer, const XtchRenderPass pass) { + const size_t planeSize = (static_cast(pageWidth) * pageHeight + 7) / 8; + const size_t colBytes = (pageHeight + 7) / 8; + const xtc::XtcError error = + xtc.loadPageStreaming(pageIndex, [&](const uint8_t* data, const size_t size, const size_t offset) { + for (size_t i = 0; i < size; i++) { + const size_t absoluteOffset = offset + i; + const bool secondPlane = absoluteOffset >= planeSize; + const size_t planeOffset = secondPlane ? absoluteOffset - planeSize : absoluteOffset; + const size_t colIndex = planeOffset / colBytes; + if (colIndex >= pageWidth) continue; + + const uint16_t x = static_cast(pageWidth - 1 - colIndex); + const uint16_t yBase = static_cast((planeOffset % colBytes) * 8); + for (uint8_t bit = 0; bit < 8 && yBase + bit < pageHeight; bit++) { + const uint16_t y = static_cast(yBase + bit); + const bool bitSet = ((data[i] >> (7 - bit)) & 1) != 0; + switch (pass) { + case XtchRenderPass::Base: + // Applying both planes onto a white buffer produces bit1 OR bit2. + if (bitSet) renderer.drawPixel(x, y, true); + break; + case XtchRenderPass::Lsb: + // Starting black, plane 1 clears candidates and plane 2 removes + // false positives, leaving white only for XTH value 1. + if (!bitSet) renderer.drawPixel(x, y, secondPlane); + break; + case XtchRenderPass::Msb: + // XTH values 1 and 2 are bit1 XOR bit2. Plane 1 seeds the + // framebuffer; set plane-2 bits then toggle those pixels. + if (!secondPlane && bitSet) { + renderer.drawPixel(x, y, false); + } else if (secondPlane && bitSet) { + renderer.drawPixel(x, y, !renderer.isPixelBlack(x, y)); + } + break; + } + } + } + }); + if (error == xtc::XtcError::OK) return true; + LOG_ERR("XTR", "Failed to stream XTCH page %lu: %s", pageIndex, xtc::errorToString(error)); + return false; +} } // namespace void XtcReaderActivity::onEnter() { @@ -89,6 +138,10 @@ void XtcReaderActivity::onExit() { mappedInput.setReaderMode(false); + if (!flushQueuedProgress()) { + LOG_ERR("XTR", "Failed to flush debounced reader progress on exit"); + } + APP_STATE.readerActivityLoadCount = 0; APP_STATE.saveToFile(); @@ -105,11 +158,28 @@ void XtcReaderActivity::onExit() { xtc.reset(); } +void XtcReaderActivity::openReaderMenu() { + const bool hasChapters = xtc->hasChapters() && !xtc->getChapters().empty(); + pauseReadingStatsTimer("reader_menu"); + startActivityForResult( + std::make_unique(renderer, mappedInput, xtc->getTitle(), hasChapters, stats.isCompleted), + [this](const ActivityResult& result) { + const auto* menu = std::get_if(&result.data); + if (result.isCancelled || menu == nullptr) { + resumeReadingStatsTimer("reader_menu_return"); + requestUpdate(); + return; + } + onReaderMenuConfirm(menu->action); + }); +} + void XtcReaderActivity::loop() { if (!xtc) { return; } + const auto touch = ReaderUtils::detectTouchPageTurn(renderer, mappedInput); const bool atEndOfBook = currentPage >= xtc->getPageCount(); // While the end screen suggestion menu is showing it owns Confirm/Back/navigation @@ -137,20 +207,8 @@ void XtcReaderActivity::loop() { } } - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - const bool hasChapters = xtc->hasChapters() && !xtc->getChapters().empty(); - pauseReadingStatsTimer("reader_menu"); - startActivityForResult( - std::make_unique(renderer, mappedInput, xtc->getTitle(), hasChapters, stats.isCompleted), - [this](const ActivityResult& result) { - const auto* menu = std::get_if(&result.data); - if (result.isCancelled || menu == nullptr) { - resumeReadingStatsTimer("reader_menu_return"); - requestUpdate(); - return; - } - onReaderMenuConfirm(menu->action); - }); + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || ReaderUtils::isTouchMenuGesture(mappedInput)) { + openReaderMenu(); return; } @@ -171,7 +229,7 @@ void XtcReaderActivity::loop() { } // Short press BACK goes directly to home - if (mappedInput.wasReleased(MappedInputManager::Button::Back) && + if (!touch.prev && !touch.next && mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { onGoHome(); return; @@ -282,7 +340,7 @@ void XtcReaderActivity::loop() { currentPage = xtc->getPageCount(); } if (shouldRecordForwardRead) { - recordForwardPageTurn(forwardReadSeconds); + recordForwardPageTurn(forwardReadSeconds, false); } } requestUpdate(); @@ -292,8 +350,10 @@ void XtcReaderActivity::loop() { const bool fromSideBtn = (sidePrev || sideNext) && !(frontPrev || frontNext); const bool fromTilt = tiltPrev || tiltNext; - const bool prevTriggered = tiltPrev || sidePrev || frontPrev; - const bool nextTriggered = tiltNext || sideNext || frontNext; + bool prevTriggered = tiltPrev || sidePrev || frontPrev; + bool nextTriggered = tiltNext || sideNext || frontNext; + prevTriggered = prevTriggered || touch.prev; + nextTriggered = nextTriggered || touch.next; if (!prevTriggered && !nextTriggered) { return; @@ -347,7 +407,7 @@ void XtcReaderActivity::loop() { currentPage = xtc->getPageCount(); // Allow showing "End of book" } if (shouldRecordForwardRead) { - recordForwardPageTurn(forwardReadSeconds); + recordForwardPageTurn(forwardReadSeconds, !skipPages); } requestUpdate(); } @@ -380,9 +440,6 @@ bool XtcReaderActivity::currentPageReadingSecondsForStats(uint32_t& seconds, con const uint32_t thresholdSeconds = SETTINGS.getReadingIdleTimeThresholdSeconds(); if (elapsedSeconds > thresholdSeconds) { - LOG_DBG("XTR", "Reading time interval rejected as idle: source=%s seconds=%lu threshold=%lu", - source ? source : "unknown", static_cast(elapsedSeconds), - static_cast(thresholdSeconds)); return false; } @@ -418,12 +475,39 @@ void XtcReaderActivity::recordCurrentPageReadingTime(const char* source) { pageShownAtMs = 0UL; } -void XtcReaderActivity::recordForwardPageTurn(uint32_t seconds) { - (void)seconds; +void XtcReaderActivity::recordForwardPageTurn(const uint32_t seconds, const bool recordPace) { + if (recordPace) { + stats.recordForwardPageRead(seconds); + } stats.totalPagesTurned++; globalStats.totalPagesTurned++; } +bool XtcReaderActivity::formatTimeLeftLabel(char* buf, const size_t len) const { + if (!buf || len == 0 || !xtc || + SETTINGS.statusBarTimeLeft == CrossPointSettings::STATUS_BAR_TIME_LEFT::TIME_LEFT_HIDE) { + return false; + } + + const bool bookEstimate = SETTINGS.statusBarTimeLeft == CrossPointSettings::STATUS_BAR_TIME_LEFT::TIME_LEFT_BOOK; + const auto pageInfo = getStatusBarInfo(); + const uint32_t current = bookEstimate ? currentPage + 1U : static_cast(pageInfo.currentPage); + const uint32_t total = bookEstimate ? xtc->getPageCount() : static_cast(pageInfo.pageCount); + if (current >= total) { + return false; + } + + if (stats.avgSecondsPerForwardPage == 0 || stats.paceSampleCount < MIN_TIME_LEFT_PACE_SAMPLE_COUNT) { + snprintf(buf, len, "%s", tr(STR_TIME_LEFT_CALCULATING)); + return true; + } + + const uint64_t estimatedSeconds = + static_cast(total - current) * static_cast(stats.avgSecondsPerForwardPage); + formatCompactReadingDuration(static_cast(std::min(estimatedSeconds, UINT32_MAX)), buf, len); + return true; +} + void XtcReaderActivity::commitReadingStats() { if (!xtc || !SETTINGS.shouldTrackReadingStats()) { return; @@ -584,7 +668,7 @@ void XtcReaderActivity::deleteBookStats() { void XtcReaderActivity::deleteBookCache() { startActivityForResult( std::make_unique(renderer, mappedInput, confirmationHeading(StrId::STR_DELETE_CACHE), - xtc ? xtc->getTitle() : std::string{}), + xtc ? xtc->getTitle() : std::string{}, false, true), [this](const ActivityResult& result) { if (!result.isCancelled && xtc) { bool cacheDeleted = false; @@ -626,6 +710,12 @@ void XtcReaderActivity::onReaderMenuConfirm(const int action) { case XtcReaderMenuActivity::MenuAction::DELETE_CACHE: deleteBookCache(); break; + case XtcReaderMenuActivity::MenuAction::SEND_NEARBY_BOOK: + saveProgress(currentPage); + activityManager.goToNearbyBookSend(xtc ? xtc->getPath() : std::string{}, true); + return; + case XtcReaderMenuActivity::MenuAction::DISABLE_TOUCHSCREEN: + break; } } @@ -678,10 +768,13 @@ void XtcReaderActivity::render(RenderLock&&) { renderPage(); pageShownAtMs = millis(); - saveProgress(); + if (!queueProgressSave()) { + LOG_ERR("XTR", "Failed to save debounced reader progress"); + } } XtcReaderActivity::StatusBarInfo XtcReaderActivity::getStatusBarInfo() const { + const auto statusBar = SETTINGS.statusBarSpec(); const int bookPageCount = static_cast(xtc->getPageCount()); const int bookPage = static_cast(currentPage) + 1; std::string title = @@ -700,7 +793,7 @@ XtcReaderActivity::StatusBarInfo XtcReaderActivity::getStatusBarInfo() const { return StatusBarInfo{bookPage, bookPageCount, std::move(title)}; } - if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { + if (statusBar.titleMode == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { title = chapterIt->name[0] == '\0' ? tr(STR_UNNAMED) : chapterIt->name; } @@ -748,7 +841,10 @@ void XtcReaderActivity::renderStatusBarOverlay(const StatusBarOverlayPosition po const int displayPage = static_cast(currentPage) + 1; const float progress = pageCount > 0 ? (static_cast(displayPage) * 100.0f) / pageCount : 0.0f; const auto pageInfo = getStatusBarInfo(); - GUI.drawStatusBar(renderer, progress, pageInfo.currentPage, pageInfo.pageCount, pageInfo.title, paddingBottom); + char timeLeftLabel[24] = {}; + const char* timeLeft = formatTimeLeftLabel(timeLeftLabel, sizeof(timeLeftLabel)) ? timeLeftLabel : nullptr; + GUI.drawStatusBar(renderer, progress, pageInfo.currentPage, pageInfo.pageCount, pageInfo.title, paddingBottom, 0, + false, timeLeft); } void XtcReaderActivity::renderPage() { @@ -756,16 +852,60 @@ void XtcReaderActivity::renderPage() { const uint16_t pageHeight = xtc->getPageHeight(); const uint8_t bitDepth = xtc->getBitDepth(); - // Calculate buffer size for one page - // XTG (1-bit): Row-major, ((width+7)/8) * height bytes - // XTH (2-bit): Two bit planes, column-major, ((width * height + 7) / 8) * 2 bytes - size_t pageBufferSize; if (bitDepth == 2) { - pageBufferSize = ((static_cast(pageWidth) * pageHeight + 7) / 8) * 2; - } else { - pageBufferSize = ((pageWidth + 7) / 8) * pageHeight; + auto showStreamError = [&]() { + renderer.clearScreen(); + const char* message = + xtc->getLastError() == xtc::XtcError::MEMORY_ERROR ? tr(STR_MEMORY_ERROR) : tr(STR_PAGE_LOAD_ERROR); + renderer.drawCenteredText(UI_12_FONT_ID, 300, message, true, EpdFontFamily::BOLD); + renderer.displayBuffer(); + }; + + // XTCH stores two 48 KB planes. Stream each rendering pass through a 1 KB + // scratch chunk so fragmented C3 heaps never need one contiguous 96 KB block. + renderer.clearScreen(); + if (!streamXtchRenderPass(*xtc, currentPage, pageWidth, pageHeight, renderer, XtchRenderPass::Base)) { + showStreamError(); + return; + } + + if (pagesUntilFullRefresh <= 1) { + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + renderer.preconditionGrayscale(); + pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); + } else { + renderer.displayGrayscaleBase(HalDisplay::FAST_REFRESH); + pagesUntilFullRefresh--; + } + + renderer.clearScreen(0x00); + if (!streamXtchRenderPass(*xtc, currentPage, pageWidth, pageHeight, renderer, XtchRenderPass::Lsb)) { + showStreamError(); + return; + } + renderer.copyGrayscaleLsbBuffers(); + + renderer.clearScreen(0x00); + if (!streamXtchRenderPass(*xtc, currentPage, pageWidth, pageHeight, renderer, XtchRenderPass::Msb)) { + showStreamError(); + return; + } + renderer.copyGrayscaleMsbBuffers(); + renderer.displayGrayBuffer(); + + renderer.clearScreen(); + if (!streamXtchRenderPass(*xtc, currentPage, pageWidth, pageHeight, renderer, XtchRenderPass::Base)) { + showStreamError(); + return; + } + renderer.cleanupGrayscaleWithFrameBuffer(); + return; } + // Calculate buffer size for one page + // XTG (1-bit): Row-major, ((width+7)/8) * height bytes. + const size_t pageBufferSize = ((pageWidth + 7) / 8) * pageHeight; + // Allocate page buffer uint8_t* pageBuffer = static_cast(malloc(pageBufferSize)); if (!pageBuffer) { @@ -795,127 +935,20 @@ void XtcReaderActivity::renderPage() { // XTC/XTCH pages are pre-rendered with status bar included, so render full page const uint16_t maxSrcY = pageHeight; - if (bitDepth == 2) { - // XTH 2-bit mode: Two bit planes, column-major order - // - Columns scanned right to left (x = width-1 down to 0) - // - 8 vertical pixels per byte (MSB = topmost pixel in group) - // - First plane: Bit1, Second plane: Bit2 - // - Pixel value = (bit1 << 1) | bit2 - // - Grayscale: 0=White, 1=Dark Grey, 2=Light Grey, 3=Black - - const size_t planeSize = (static_cast(pageWidth) * pageHeight + 7) / 8; - const uint8_t* plane1 = pageBuffer; // Bit1 plane - const uint8_t* plane2 = pageBuffer + planeSize; // Bit2 plane - const size_t colBytes = (pageHeight + 7) / 8; // Bytes per column (100 for 800 height) - - // Lambda to get pixel value at (x, y) - auto getPixelValue = [&](uint16_t x, uint16_t y) -> uint8_t { - const size_t colIndex = pageWidth - 1 - x; - const size_t byteInCol = y / 8; - const size_t bitInByte = 7 - (y % 8); - const size_t byteOffset = colIndex * colBytes + byteInCol; - const uint8_t bit1 = (plane1[byteOffset] >> bitInByte) & 1; - const uint8_t bit2 = (plane2[byteOffset] >> bitInByte) & 1; - return (bit1 << 1) | bit2; - }; - - // Optimized grayscale rendering without storeBwBuffer (saves 48KB peak memory) - // Flow: BW display → LSB/MSB passes → grayscale display → re-render BW for next frame + // 1-bit mode: 8 pixels per byte, MSB first + const size_t srcRowBytes = (pageWidth + 7) / 8; // 60 bytes for 480 width - // Count pixel distribution for debugging - uint32_t pixelCounts[4] = {0, 0, 0, 0}; - for (uint16_t y = 0; y < pageHeight; y++) { - for (uint16_t x = 0; x < pageWidth; x++) { - pixelCounts[getPixelValue(x, y)]++; - } - } - LOG_DBG("XTR", "Pixel distribution: White=%lu, DarkGrey=%lu, LightGrey=%lu, Black=%lu", pixelCounts[0], - pixelCounts[1], pixelCounts[2], pixelCounts[3]); + for (uint16_t srcY = 0; srcY < maxSrcY; srcY++) { + const size_t srcRowStart = srcY * srcRowBytes; - // Pass 1: BW buffer - draw all non-white pixels as black - for (uint16_t y = 0; y < pageHeight; y++) { - for (uint16_t x = 0; x < pageWidth; x++) { - if (getPixelValue(x, y) >= 1) { - renderer.drawPixel(x, y, true); - } - } - } + for (uint16_t srcX = 0; srcX < pageWidth; srcX++) { + // Read source pixel (MSB first, bit 7 = leftmost pixel) + const size_t srcByte = srcRowStart + srcX / 8; + const size_t srcBit = 7 - (srcX % 8); + const bool isBlack = !((pageBuffer[srcByte] >> srcBit) & 1); // XTC: 0 = black, 1 = white - if (pagesUntilFullRefresh <= 1) { - // Periodic ghost cleanup: scrub via the normal path, then run the - // settle flavor of the grayscale base pass (DTM planes are equal after - // the display sync, so only the gentle reinforcement cells fire). - renderer.displayBuffer(HalDisplay::HALF_REFRESH); - renderer.preconditionGrayscale(); - pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); - } else { - // OEM grayscale pipeline base: differential "AA-pre-BW(mid)" update as - // the page turn on X3; plain FAST refresh on X4 (previous behavior). - renderer.displayGrayscaleBase(HalDisplay::FAST_REFRESH); - pagesUntilFullRefresh--; - } - - // Pass 2: LSB buffer - mark DARK gray only (XTH value 1) - // In LUT: 0 bit = apply gray effect, 1 bit = untouched - renderer.clearScreen(0x00); - for (uint16_t y = 0; y < pageHeight; y++) { - for (uint16_t x = 0; x < pageWidth; x++) { - if (getPixelValue(x, y) == 1) { // Dark grey only - renderer.drawPixel(x, y, false); - } - } - } - renderer.copyGrayscaleLsbBuffers(); - - // Pass 3: MSB buffer - mark LIGHT AND DARK gray (XTH value 1 or 2) - // In LUT: 0 bit = apply gray effect, 1 bit = untouched - renderer.clearScreen(0x00); - for (uint16_t y = 0; y < pageHeight; y++) { - for (uint16_t x = 0; x < pageWidth; x++) { - const uint8_t pv = getPixelValue(x, y); - if (pv == 1 || pv == 2) { // Dark grey or Light grey - renderer.drawPixel(x, y, false); - } - } - } - renderer.copyGrayscaleMsbBuffers(); - - // Display grayscale overlay - renderer.displayGrayBuffer(); - - // Pass 4: Re-render BW to framebuffer (restore for next frame, instead of restoreBwBuffer) - renderer.clearScreen(); - for (uint16_t y = 0; y < pageHeight; y++) { - for (uint16_t x = 0; x < pageWidth; x++) { - if (getPixelValue(x, y) >= 1) { - renderer.drawPixel(x, y, true); - } - } - } - - // Cleanup grayscale buffers with current frame buffer - renderer.cleanupGrayscaleWithFrameBuffer(); - - free(pageBuffer); - - LOG_DBG("XTR", "Rendered page %lu/%lu (2-bit grayscale)", currentPage + 1, xtc->getPageCount()); - return; - } else { - // 1-bit mode: 8 pixels per byte, MSB first - const size_t srcRowBytes = (pageWidth + 7) / 8; // 60 bytes for 480 width - - for (uint16_t srcY = 0; srcY < maxSrcY; srcY++) { - const size_t srcRowStart = srcY * srcRowBytes; - - for (uint16_t srcX = 0; srcX < pageWidth; srcX++) { - // Read source pixel (MSB first, bit 7 = leftmost pixel) - const size_t srcByte = srcRowStart + srcX / 8; - const size_t srcBit = 7 - (srcX % 8); - const bool isBlack = !((pageBuffer[srcByte] >> srcBit) & 1); // XTC: 0 = black, 1 = white - - if (isBlack) { - renderer.drawPixel(srcX, srcY, true); - } + if (isBlack) { + renderer.drawPixel(srcX, srcY, true); } } } @@ -931,21 +964,40 @@ void XtcReaderActivity::renderPage() { // Display with appropriate refresh ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh); - - LOG_DBG("XTR", "Rendered page %lu/%lu (%u-bit)", currentPage + 1, xtc->getPageCount(), bitDepth); } -void XtcReaderActivity::saveProgress() const { +bool XtcReaderActivity::saveProgress(const uint32_t page) { + if (!xtc) { + return false; + } HalFile f; - if (Storage.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) { - uint8_t data[4]; - data[0] = currentPage & 0xFF; - data[1] = (currentPage >> 8) & 0xFF; - data[2] = (currentPage >> 16) & 0xFF; - data[3] = (currentPage >> 24) & 0xFF; - f.write(data, 4); - f.close(); + if (!Storage.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) { + return false; + } + uint8_t data[4]; + data[0] = page & 0xFF; + data[1] = (page >> 8) & 0xFF; + data[2] = (page >> 16) & 0xFF; + data[3] = (page >> 24) & 0xFF; + const bool written = f.write(data, sizeof(data)) == sizeof(data); + f.close(); + if (!written) { + LOG_ERR("XTR", "Short write saving reader progress"); + return false; } + progressSaveDebouncer.markPersisted(page); + return true; +} + +bool XtcReaderActivity::queueProgressSave() { + if (!progressSaveDebouncer.observe(currentPage)) { + return true; + } + return saveProgress(currentPage); +} + +bool XtcReaderActivity::flushQueuedProgress() { + return !progressSaveDebouncer.hasPending() || saveProgress(progressSaveDebouncer.lastObservedPosition()); } void XtcReaderActivity::loadProgress() { @@ -954,7 +1006,6 @@ void XtcReaderActivity::loadProgress() { uint8_t data[4]; if (f.read(data, 4) == 4) { currentPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); - LOG_DBG("XTR", "Loaded progress: page %lu", currentPage); // Validate page number if (currentPage >= xtc->getPageCount()) { diff --git a/src/activities/reader/XtcReaderActivity.h b/src/activities/reader/XtcReaderActivity.h index c07cea201c..891c6d28ef 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -15,6 +15,7 @@ #include "BookReadingStats.h" #include "EndOfBookOptions.h" #include "GlobalReadingStats.h" +#include "ReaderProgressSaveDebouncer.h" #include "activities/Activity.h" class XtcReaderActivity final : public Activity { @@ -31,6 +32,7 @@ class XtcReaderActivity final : public Activity { bool longPowerPageTurnHandled = false; bool frontButtonLongPressHandled = false; bool longPressBackHandled = false; + ReaderProgressSaveDebouncer progressSaveDebouncer; // Next-book suggestion menu for the End-of-Book screen EndOfBookOptions endOfBookOptions; @@ -44,14 +46,17 @@ class XtcReaderActivity final : public Activity { void renderPage(); void renderStatusBarOverlay(StatusBarOverlayPosition position) const; StatusBarInfo getStatusBarInfo() const; - void saveProgress() const; + bool saveProgress(uint32_t page); + bool queueProgressSave(); + bool flushQueuedProgress(); void loadProgress(); void pauseReadingStatsTimer(const char* source = "unknown"); void resumeReadingStatsTimer(const char* source = "unknown"); bool currentPageReadingSecondsForStats(uint32_t& seconds, const char* source) const; bool forwardPageReadElapsed(uint32_t& seconds, const char* source) const; void recordCurrentPageReadingTime(const char* source = "unknown"); - void recordForwardPageTurn(uint32_t seconds); + void recordForwardPageTurn(uint32_t seconds, bool recordPace); + bool formatTimeLeftLabel(char* buf, size_t len) const; void commitReadingStats(); void resetCurrentBookStatsAfterDelete(); void setBookCompleted(bool isCompleted); @@ -60,18 +65,30 @@ class XtcReaderActivity final : public Activity { void openReadingStats(); void deleteBookStats(); void deleteBookCache(); + void openReaderMenu(); void onReaderMenuConfirm(int action); bool executeLongPressBackAction(); public: - explicit XtcReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr xtc) - : Activity("XtcReader", renderer, mappedInput), xtc(std::move(xtc)) {} + explicit XtcReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr xtc, + int initialRefreshCountdown) + : Activity("XtcReader", renderer, mappedInput), + xtc(std::move(xtc)), + pagesUntilFullRefresh(initialRefreshCountdown) {} void onEnter() override; void onExit() override; void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } bool canSnapshotForSleepOverlay() const override { return true; } + bool handlesReaderPowerSettingsOverride() const override { return true; } + bool openReaderSettingsMenu() override { + if (!xtc) { + return false; + } + openReaderMenu(); + return true; + } std::string getCurrentBookPath() const override { return xtc ? xtc->getPath() : std::string{}; } // Renders the last saved page to the frame buffer without flushing to display. diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index fc6d139583..6d39821818 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -3,138 +3,182 @@ #include #include -#include - #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" -#include "fontIds.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" -int XtcReaderChapterSelectionActivity::getPageItems() const { - constexpr int lineHeight = 30; +namespace fui = freeink::ui; - const int screenHeight = renderer.getScreenHeight(); - const auto orientation = renderer.getOrientation(); - // In inverted portrait, the hint row is drawn near the logical top. - // Reserve vertical space so the list starts below the hints. - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; - const int startY = 60 + hintGutterHeight; - const int availableHeight = screenHeight - startY - lineHeight; - // Clamp to at least one item to prevent empty page math. - return std::max(1, availableHeight / lineHeight); +namespace { +constexpr fui::ActionId ACTION_ROW = 1; } -int XtcReaderChapterSelectionActivity::findChapterIndexForPage(uint32_t page) const { - if (!xtc) { - return 0; - } - +XtcReaderChapterSelectionActivity::XtcReaderChapterSelectionActivity(GfxRenderer& renderer, + MappedInputManager& mappedInput, + const std::shared_ptr& xtc, + const uint32_t currentPage) + : Activity("XtcReaderChapterSelection", renderer, mappedInput), + xtc(xtc), + currentPage(currentPage), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + +int XtcReaderChapterSelectionActivity::findChapterIndexForPage(const uint32_t page) const { + if (!xtc) return 0; const auto chapters = xtc->getChapters(); for (size_t i = 0; i < chapters.size(); i++) { - if (page >= chapters[i].startPage && page <= chapters[i].endPage) { - return static_cast(i); - } + if (page >= chapters[i].startPage && page <= chapters[i].endPage) return static_cast(i); } return 0; } void XtcReaderChapterSelectionActivity::onEnter() { Activity::onEnter(); - - if (!xtc) { - return; - } + mappedInput.setReaderTouchscreenOverride(true); + if (!xtc) return; selectorIndex = findChapterIndexForPage(currentPage); - + topIndex = 0; + visibleRows = 1; + initialViewportPending = true; + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &XtcReaderChapterSelectionActivity::onRowEvent, this); + app.setScreen(&XtcReaderChapterSelectionActivity::chapterScreen, this); requestUpdate(); } -void XtcReaderChapterSelectionActivity::onExit() { Activity::onExit(); } +void XtcReaderChapterSelectionActivity::onExit() { + mappedInput.setReaderTouchscreenOverride(false); + Activity::onExit(); +} -void XtcReaderChapterSelectionActivity::loop() { - const int pageItems = getPageItems(); - const auto chapters = xtc->getChapters(); - const int totalItems = static_cast(chapters.size()); +void XtcReaderChapterSelectionActivity::selectChapter() { + const auto& chapters = xtc->getChapters(); + if (selectorIndex >= 0 && selectorIndex < static_cast(chapters.size())) { + setResult(PageResult{chapters[selectorIndex].startPage}); + finish(); + } +} - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast(chapters.size())) { - setResult(PageResult{chapters[selectorIndex].startPage}); - finish(); - } - } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { +void XtcReaderChapterSelectionActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + const int totalItems = static_cast(self->xtc->getChapters().size()); + if (event.value < 0 || event.value >= totalItems) return; + self->selectorIndex = event.value; + self->app.clearTapFlash(); + self->selectChapter(); +} + +void XtcReaderChapterSelectionActivity::loop() { + const int totalItems = static_cast(xtc->getChapters().size()); + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header{safe.x, safe.y + metrics.topPadding, safe.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (TouchHeaderBackButton::wasTapped(mappedInput, header) || + mappedInput.wasReleased(MappedInputManager::Button::Back)) { ActivityResult result; result.isCancelled = true; setResult(std::move(result)); finish(); + return; } - - buttonNavigator.onNextRelease([this, totalItems] { - selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); - requestUpdate(); - }); - - buttonNavigator.onPreviousRelease([this, totalItems] { - selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); - requestUpdate(); - }); - - buttonNavigator.onNextContinuous([this, totalItems, pageItems] { - selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, totalItems); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + const auto moveSelection = [this, totalItems](const int index) { + selectorIndex = index; + topIndex = followListSelection(selectorIndex, topIndex, visibleRows, totalItems); requestUpdate(); + }; + buttonNavigator.onNextRelease( + [this, totalItems, &moveSelection] { moveSelection(ButtonNavigator::nextIndex(selectorIndex, totalItems)); }); + buttonNavigator.onPreviousRelease( + [this, totalItems, &moveSelection] { moveSelection(ButtonNavigator::previousIndex(selectorIndex, totalItems)); }); + buttonNavigator.onNextContinuous([this, totalItems, &moveSelection] { + moveSelection(ButtonNavigator::nextPageIndex(selectorIndex, totalItems, visibleRows)); }); - - buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { - selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); - requestUpdate(); + buttonNavigator.onPreviousContinuous([this, totalItems, &moveSelection] { + moveSelection(ButtonNavigator::previousPageIndex(selectorIndex, totalItems, visibleRows)); }); + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) selectChapter(); } -void XtcReaderChapterSelectionActivity::render(RenderLock&&) { - renderer.clearScreen(); - - const auto pageWidth = renderer.getScreenWidth(); - const auto orientation = renderer.getOrientation(); - // Landscape orientation: reserve a horizontal gutter for button hints. - const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; - const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; - // Inverted portrait: reserve vertical space for hints at the top. - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; - // Landscape CW places hints on the left edge; CCW keeps them on the right. - const int contentX = isLandscapeCw ? hintGutterWidth : 0; - const int contentWidth = pageWidth - hintGutterWidth; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; - const int contentY = hintGutterHeight; - const int pageItems = getPageItems(); - // Manual centering to honor content gutters. - const int titleX = - contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_SELECT_CHAPTER), EpdFontFamily::BOLD)) / 2; - renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_SELECT_CHAPTER), true, EpdFontFamily::BOLD); +void XtcReaderChapterSelectionActivity::chapterScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildChapterScreen(screen); +} - const auto chapters = xtc->getChapters(); +void XtcReaderChapterSelectionActivity::buildChapterScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + screen.setContentMargin(fui::Insets{ + static_cast(safe.y + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), + static_cast(renderer.getScreenWidth() - safe.x - safe.width), + static_cast(renderer.getScreenHeight() - safe.y - safe.height), static_cast(safe.x)}); + screen.spacer(static_cast(metrics.verticalSpacing)); + const auto& chapters = xtc->getChapters(); if (chapters.empty()) { - // Center the empty state within the gutter-safe content region. - const int emptyX = contentX + (contentWidth - renderer.getTextWidth(UI_10_FONT_ID, tr(STR_NO_CHAPTERS))) / 2; - renderer.drawText(UI_10_FONT_ID, emptyX, 120 + contentY, tr(STR_NO_CHAPTERS)); - renderer.displayBuffer(); + screen.centeredText(tr(STR_NO_CHAPTERS), screen.theme().bodyText); return; } - - const auto pageStartIndex = selectorIndex / pageItems * pageItems; - // Highlight only the content area, not the hint gutters. - renderer.fillRect(contentX, 60 + contentY + (selectorIndex % pageItems) * 30 - 2, contentWidth - 1, 30); - for (int i = pageStartIndex; i < static_cast(chapters.size()) && i < pageStartIndex + pageItems; i++) { - const auto& chapter = chapters[i]; - const char* title = chapter.name[0] == '\0' ? tr(STR_UNNAMED) : chapter.name; - renderer.drawText(UI_10_FONT_ID, contentX + 20, 60 + contentY + (i % pageItems) * 30, title, i != selectorIndex); + std::vector items; + items.reserve(chapters.size()); + for (size_t i = 0; i < chapters.size(); ++i) { + fui::ListItem row; + row.label = chapters[i].name[0] == '\0' ? tr(STR_UNNAMED) : chapters[i].name; + row.actionValue = static_cast(i); + items.push_back(row); } + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectorIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.labelText = screen.theme().bodyText; + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + const int chapterCount = static_cast(chapters.size()); + topIndex = initialViewportPending ? followListSelection(selectorIndex, 0, visibleRows, chapterCount) + : scrollListBy(topIndex, 0, visibleRows, chapterCount); + initialViewportPending = false; + props.topIndex = static_cast(topIndex); + screen.list(props); +} - // Skip button hints in landscape CW mode (they overlap content) - if (renderer.getOrientation() != GfxRenderer::LandscapeClockwise) { - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); +void XtcReaderChapterSelectionActivity::render(RenderLock&&) { + renderer.clearScreen(); + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + const Rect header{safe.x, safe.y + metrics.topPadding, safe.width, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_SELECT_CHAPTER), true); + } else { + GUI.drawHeader(renderer, header, tr(STR_SELECT_CHAPTER), nullptr, true); } - + uiReady = false; + app.render(); + uiReady = true; + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); renderer.displayBuffer(); } diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.h b/src/activities/reader/XtcReaderChapterSelectionActivity.h index 3996b04a5c..2686962449 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.h +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.h @@ -1,24 +1,38 @@ #pragma once +#include +#include #include +#include #include #include "activities/Activity.h" #include "util/ButtonNavigator.h" class XtcReaderChapterSelectionActivity final : public Activity { + using UiApp = freeink::ui::FreeInkApp<20, 4>; + std::shared_ptr xtc; ButtonNavigator buttonNavigator; uint32_t currentPage = 0; int selectorIndex = 0; + freeink::ui::GfxRendererTarget uiTarget; + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + bool initialViewportPending = true; + + static void chapterScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildChapterScreen(UiApp::ScreenType& screen); + void selectChapter(); - int getPageItems() const; int findChapterIndexForPage(uint32_t page) const; public: - explicit XtcReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::shared_ptr& xtc, uint32_t currentPage) - : Activity("XtcReaderChapterSelection", renderer, mappedInput), xtc(xtc), currentPage(currentPage) {} + XtcReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::shared_ptr& xtc, uint32_t currentPage); void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/reader/XtcReaderMenuActivity.cpp b/src/activities/reader/XtcReaderMenuActivity.cpp index 578ecb527d..a6435c3ac9 100644 --- a/src/activities/reader/XtcReaderMenuActivity.cpp +++ b/src/activities/reader/XtcReaderMenuActivity.cpp @@ -6,7 +6,9 @@ #include #include +#include "CrossPointSettings.h" #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" @@ -21,12 +23,13 @@ XtcReaderMenuActivity::XtcReaderMenuActivity(GfxRenderer& renderer, MappedInputM const bool hasChapters, const bool isBookCompleted) : Activity("XtcReaderMenu", renderer, mappedInput), title(std::move(title)), - items(buildMenuItems(hasChapters, isBookCompleted)) {} + items(buildMenuItems(hasChapters, isBookCompleted, mappedInput.hasTouchHardware())) {} std::vector XtcReaderMenuActivity::buildMenuItems(const bool hasChapters, - const bool isBookCompleted) { + const bool isBookCompleted, + const bool hasTouch) { std::vector menuItems; - menuItems.reserve(5); + menuItems.reserve(6 + (hasTouch ? 1u : 0u)); if (hasChapters) { menuItems.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); } @@ -35,25 +38,65 @@ std::vector XtcReaderMenuActivity::buildMenuIte {MenuAction::TOGGLE_COMPLETED, isBookCompleted ? StrId::STR_MARK_UNFINISHED : StrId::STR_MARK_FINISHED}); menuItems.push_back({MenuAction::DELETE_STATS, StrId::STR_DELETE_BOOK_STATS}); menuItems.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}); + menuItems.push_back({MenuAction::SEND_NEARBY_BOOK, StrId::STR_SEND_NEARBY_BOOK}); + if (hasTouch) { + menuItems.push_back({MenuAction::DISABLE_TOUCHSCREEN, StrId::STR_DISABLE_TOUCHSCREEN}); + } return menuItems; } void XtcReaderMenuActivity::onEnter() { Activity::onEnter(); + mappedInput.setReaderTouchscreenOverride(true); selectedIndex = 0; requestUpdate(); } +void XtcReaderMenuActivity::onExit() { + mappedInput.setReaderTouchscreenOverride(false); + Activity::onExit(); +} + +void XtcReaderMenuActivity::finishCancelled() { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); +} + void XtcReaderMenuActivity::loop() { - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer) || + mappedInput.wasReleased(MappedInputManager::Button::Back)) { + finishCancelled(); + return; + } + + if (mappedInput.wasHomeGesture()) { + finishCancelled(); return; } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (items[selectedIndex].action == MenuAction::DISABLE_TOUCHSCREEN) { + SETTINGS.disableReaderTouchscreen = SETTINGS.disableReaderTouchscreen ? 0 : 1; + SETTINGS.saveToFile(); + requestUpdate(); + return; + } + setResult(MenuResult{static_cast(items[selectedIndex].action)}); + finish(); + return; + } + + int touchedIndex = -1; + if (mappedInput.wasItemTapped(touchedIndex) && touchedIndex >= 0 && touchedIndex < static_cast(items.size())) { + selectedIndex = touchedIndex; + if (items[selectedIndex].action == MenuAction::DISABLE_TOUCHSCREEN) { + SETTINGS.disableReaderTouchscreen = SETTINGS.disableReaderTouchscreen ? 0 : 1; + SETTINGS.saveToFile(); + requestUpdate(); + return; + } setResult(MenuResult{static_cast(items[selectedIndex].action)}); finish(); return; @@ -76,7 +119,9 @@ void XtcReaderMenuActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); - const int titleX = metrics.contentSidePadding; + const Rect standardHeader{0, metrics.topPadding, pageWidth, metrics.headerHeight}; + const int titleX = mappedInput.hasTouchHardware() ? TouchHeaderBackButton::layout(standardHeader).titleX + : metrics.contentSidePadding; const int titleMaxWidth = std::max(0, pageWidth - titleX - metrics.contentSidePadding - kBatteryTextReserveWidth); const auto titleLines = renderer.wrappedText(kTitleFontId, title.c_str(), titleMaxWidth, kTitleMaxLines, EpdFontFamily::BOLD); @@ -84,7 +129,12 @@ void XtcReaderMenuActivity::render(RenderLock&&) { const int titleBlockHeight = static_cast(titleLines.size()) * titleLineHeight + std::max(0, static_cast(titleLines.size()) - 1) * kTitleLineGap; const int headerHeight = std::max(metrics.headerHeight, metrics.batteryBarHeight + titleBlockHeight + 16); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, headerHeight}, ""); + const Rect header{0, metrics.topPadding, pageWidth, headerHeight}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, "", true, 0, nullptr, 0); + } else { + GUI.drawHeader(renderer, header, ""); + } const int titleY = metrics.topPadding + metrics.batteryBarHeight + 3; for (int i = 0; i < static_cast(titleLines.size()); ++i) { @@ -94,8 +144,15 @@ void XtcReaderMenuActivity::render(RenderLock&&) { const int contentTop = metrics.topPadding + headerHeight + metrics.verticalSpacing; const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; - GUI.drawList(renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(items.size()), selectedIndex, - [this](int index) { return std::string(I18N.get(items[index].labelId)); }); + GUI.drawList( + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(items.size()), selectedIndex, + [this](int index) { return std::string(I18N.get(items[index].labelId)); }, nullptr, nullptr, + [this](int index) -> std::string { + if (items[index].action == MenuAction::DISABLE_TOUCHSCREEN) { + return I18N.get(SETTINGS.disableReaderTouchscreen ? StrId::STR_ON : StrId::STR_OFF); + } + return ""; + }); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/reader/XtcReaderMenuActivity.h b/src/activities/reader/XtcReaderMenuActivity.h index afe1830097..f2e1be7b1a 100644 --- a/src/activities/reader/XtcReaderMenuActivity.h +++ b/src/activities/reader/XtcReaderMenuActivity.h @@ -16,16 +16,20 @@ class XtcReaderMenuActivity final : public Activity { TOGGLE_COMPLETED, DELETE_STATS, DELETE_CACHE, + SEND_NEARBY_BOOK, + DISABLE_TOUCHSCREEN, }; XtcReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string title, bool hasChapters, bool isBookCompleted); void onEnter() override; + void onExit() override; void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } bool allowPowerAsConfirmInReaderMode() const override { return true; } + bool allowGlobalHomeGesture() const override { return false; } private: struct MenuItem { @@ -33,7 +37,8 @@ class XtcReaderMenuActivity final : public Activity { StrId labelId; }; - static std::vector buildMenuItems(bool hasChapters, bool isBookCompleted); + static std::vector buildMenuItems(bool hasChapters, bool isBookCompleted, bool hasTouch); + void finishCancelled(); ButtonNavigator buttonNavigator; std::string title; diff --git a/src/activities/settings/BackupStatsActivity.cpp b/src/activities/settings/BackupStatsActivity.cpp index f39e294ca7..4e2b71a124 100644 --- a/src/activities/settings/BackupStatsActivity.cpp +++ b/src/activities/settings/BackupStatsActivity.cpp @@ -6,6 +6,7 @@ #include "MappedInputManager.h" #include "activities/reader/StatsBackup.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" @@ -24,7 +25,12 @@ void BackupStatsActivity::render(RenderLock&&) { const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_BACKUP_NOW)); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_BACKUP_NOW), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_BACKUP_NOW)); + } if (state == WARNING) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_BACKUP_STATS_CONFIRM), true); @@ -54,12 +60,15 @@ void BackupStatsActivity::render(RenderLock&&) { } void BackupStatsActivity::runBackup() { - LOG_DBG("BACKUP_STATS", "Creating reading-stats backup"); state = backupGlobalStats(true, backupFileName, sizeof(backupFileName)) ? SUCCESS : FAILED; requestUpdate(); } void BackupStatsActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + goBack(); + return; + } if (state == WARNING) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { runBackup(); diff --git a/src/activities/settings/ButtonRemapActivity.cpp b/src/activities/settings/ButtonRemapActivity.cpp index 3842dfba53..ff78461978 100644 --- a/src/activities/settings/ButtonRemapActivity.cpp +++ b/src/activities/settings/ButtonRemapActivity.cpp @@ -5,6 +5,7 @@ #include "CrossPointSettings.h" #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" @@ -34,6 +35,10 @@ void ButtonRemapActivity::onEnter() { void ButtonRemapActivity::onExit() { Activity::onExit(); } void ButtonRemapActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + finish(); + return; + } // Clear any temporary warning after its timeout. if (errorUntil > 0 && millis() > errorUntil) { errorMessage.clear(); @@ -118,13 +123,20 @@ void ButtonRemapActivity::render(RenderLock&&) { renderer.clearScreen(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, - readerMode ? tr(STR_REMAP_FRONT_BUTTONS_READER) : tr(STR_REMAP_FRONT_BUTTONS), nullptr, - headerReaderContext); - GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, + const char* header = readerMode ? tr(STR_REMAP_FRONT_BUTTONS_READER) : tr(STR_REMAP_FRONT_BUTTONS); + const Rect headerRect = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, headerRect, header, headerReaderContext); + } else { + GUI.drawHeader(renderer, headerRect, header, nullptr, headerReaderContext); + } + GUI.drawSubHeader(renderer, + Rect{0, metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput), pageWidth, + metrics.tabBarHeight}, tr(STR_REMAP_PROMPT)); - int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing; + int topOffset = metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.tabBarHeight + + metrics.verticalSpacing; int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing; GUI.drawList( renderer, Rect{0, topOffset, pageWidth, contentHeight}, kRoleCount, currentStep, diff --git a/src/activities/settings/ClearCacheActivity.cpp b/src/activities/settings/ClearCacheActivity.cpp index 4ba9150db6..448ed02081 100644 --- a/src/activities/settings/ClearCacheActivity.cpp +++ b/src/activities/settings/ClearCacheActivity.cpp @@ -6,6 +6,7 @@ #include #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" #include "util/BookCacheUtils.h" @@ -26,7 +27,12 @@ void ClearCacheActivity::render(RenderLock&&) { renderer.clearScreen(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLEAR_READING_CACHE)); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_CLEAR_READING_CACHE), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_CLEAR_READING_CACHE)); + } if (state == WARNING) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, tr(STR_CLEAR_CACHE_WARNING_1), true); @@ -74,8 +80,6 @@ void ClearCacheActivity::render(RenderLock&&) { } void ClearCacheActivity::clearCache() { - LOG_DBG("CLEAR_CACHE", "Clearing cache..."); - // Open .crosspoint directory auto root = Storage.open("/.crosspoint"); if (!root || !root.isDirectory()) { @@ -99,7 +103,6 @@ void ClearCacheActivity::clearCache() { // such as global_stats.bin are intentionally skipped. if (file.isDirectory() && isBookCacheDirectoryName(itemName.c_str())) { String fullPath = "/.crosspoint/" + itemName; - LOG_DBG("CLEAR_CACHE", "Removing cache: %s", fullPath.c_str()); file.close(); // Close before attempting to delete @@ -122,9 +125,12 @@ void ClearCacheActivity::clearCache() { } void ClearCacheActivity::loop() { + if (state != CLEARING && TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + goBack(); + return; + } if (state == WARNING) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - LOG_DBG("CLEAR_CACHE", "User confirmed, starting cache clear"); { RenderLock lock(*this); state = CLEARING; @@ -143,14 +149,15 @@ void ClearCacheActivity::loop() { } if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - LOG_DBG("CLEAR_CACHE", "User cancelled"); goBack(); } return; } if (state == SUCCESS || state == FAILED) { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + int x = 0; + int y = 0; + if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasScreenTapped(x, y)) { goBack(); } return; diff --git a/src/activities/settings/ClockOffsetActivity.cpp b/src/activities/settings/ClockOffsetActivity.cpp index 9202830cda..eb7753b2b1 100644 --- a/src/activities/settings/ClockOffsetActivity.cpp +++ b/src/activities/settings/ClockOffsetActivity.cpp @@ -1,5 +1,6 @@ #include "ClockOffsetActivity.h" +#include #include #include #include @@ -9,7 +10,9 @@ #include "CrossPointSettings.h" #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/icons/listIcons.h" #include "fontIds.h" namespace { @@ -18,6 +21,64 @@ constexpr uint8_t MAX_NEG_HOURS = 12; constexpr uint8_t MINUTE_STEPS = 4; // 0, 15, 30, 45 constexpr uint8_t MINUTES_PER_QUARTER = 15; constexpr uint8_t BIAS_QUARTER_HOURS = 48; // 0 stored = UTC-12, 48 stored = UTC+0 +constexpr int TOUCH_BUTTON_SIZE = 60; +constexpr int TOUCH_BUTTON_STACK_GAP = 12; +constexpr int TOUCH_BUTTON_GAP = 24; +constexpr int FIELD_HEIGHT = 60; +constexpr int SIGN_FIELD_MIN_WIDTH = 60; +constexpr int VALUE_FIELD_MIN_WIDTH = 72; +constexpr int LABEL_GAP = 18; +constexpr int FIELD_GAP = 14; +constexpr int COLON_GAP = 8; + +struct PickerLayout { + Rect signRect; + Rect hoursRect; + Rect minutesRect; + Rect upRect; + Rect downRect; + int labelX; + int colonX; + int textY; +}; + +PickerLayout getPickerLayout(const GfxRenderer& renderer, const bool showTouchControls) { + auto widthOf = [&](const char* text) { return renderer.getTextWidth(UI_12_FONT_ID, text, EpdFontFamily::BOLD); }; + + const int labelWidth = widthOf("UTC"); + const int signWidth = std::max(SIGN_FIELD_MIN_WIDTH, std::max(widthOf("+"), widthOf("-")) + 24); + const int hoursWidth = std::max(VALUE_FIELD_MIN_WIDTH, std::max(widthOf("14"), widthOf("12")) + 24); + const int colonWidth = widthOf(":"); + const int minutesWidth = + std::max(VALUE_FIELD_MIN_WIDTH, std::max({widthOf("00"), widthOf("15"), widthOf("30"), widthOf("45")}) + 24); + const int pickerWidth = + labelWidth + LABEL_GAP + signWidth + FIELD_GAP + hoursWidth + COLON_GAP + colonWidth + COLON_GAP + minutesWidth; + const int arrowsWidth = showTouchControls ? TOUCH_BUTTON_GAP + TOUCH_BUTTON_SIZE : 0; + const int contentWidth = pickerWidth + arrowsWidth; + const int contentX = (renderer.getScreenWidth() - contentWidth) / 2; + const int stackHeight = TOUCH_BUTTON_SIZE * 2 + TOUCH_BUTTON_STACK_GAP; + const int stackY = renderer.getScreenHeight() / 2 - 25 - stackHeight / 2; + const int fieldY = stackY + (stackHeight - FIELD_HEIGHT) / 2; + const int textY = fieldY + (FIELD_HEIGHT - renderer.getLineHeight(UI_12_FONT_ID)) / 2; + + int x = contentX; + const int labelX = x; + x += labelWidth + LABEL_GAP; + const Rect signRect{x, fieldY, signWidth, FIELD_HEIGHT}; + x += signWidth + FIELD_GAP; + const Rect hoursRect{x, fieldY, hoursWidth, FIELD_HEIGHT}; + x += hoursWidth + COLON_GAP; + const int colonX = x; + x += colonWidth + COLON_GAP; + const Rect minutesRect{x, fieldY, minutesWidth, FIELD_HEIGHT}; + x += minutesWidth; + + const int buttonX = x + TOUCH_BUTTON_GAP; + const Rect upRect{buttonX, stackY, TOUCH_BUTTON_SIZE, TOUCH_BUTTON_SIZE}; + const Rect downRect{buttonX, stackY + TOUCH_BUTTON_SIZE + TOUCH_BUTTON_STACK_GAP, TOUCH_BUTTON_SIZE, + TOUCH_BUTTON_SIZE}; + return PickerLayout{signRect, hoursRect, minutesRect, upRect, downRect, labelX, colonX, textY}; +} // Convert a (sign, hours, quarter) triple into the biased storage value. // Returns a value in [0, 104]. @@ -43,6 +104,10 @@ void decodeOffset(uint8_t biased, uint8_t& sign, uint8_t& hours, uint8_t& quarte hours = static_cast(signedQuarter / 4); quarter = static_cast(signedQuarter % 4); } + +bool contains(const Rect& rect, const int x, const int y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} } // namespace void ClockOffsetActivity::onEnter() { @@ -108,7 +173,34 @@ void ClockOffsetActivity::adjustActiveField(int delta) { } } +bool ClockOffsetActivity::fieldFromPoint(const int x, const int y, Field& field) const { + const PickerLayout layout = getPickerLayout(renderer, mappedInput.hasTouch()); + if (contains(layout.signRect, x, y)) { + field = FIELD_SIGN; + return true; + } + if (contains(layout.hoursRect, x, y)) { + field = FIELD_HOURS; + return true; + } + if (contains(layout.minutesRect, x, y)) { + field = FIELD_MINUTES; + return true; + } + return false; +} + +void ClockOffsetActivity::getTouchControlRects(Rect& upRect, Rect& downRect) const { + const PickerLayout layout = getPickerLayout(renderer, true); + upRect = layout.upRect; + downRect = layout.downRect; +} + void ClockOffsetActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + finish(); + return; + } if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { finish(); return; @@ -120,6 +212,48 @@ void ClockOffsetActivity::loop() { return; } + if (mappedInput.hasTouch()) { + int tx = 0; + int ty = 0; + Rect upRect; + Rect downRect; + getTouchControlRects(upRect, downRect); + + if (mappedInput.wasScreenTouchDown(tx, ty)) { + if (contains(downRect, tx, ty) || contains(upRect, tx, ty)) { + return; + } + Field touchedField = FIELD_HOURS; + if (fieldFromPoint(tx, ty, touchedField)) { + if (activeField != touchedField) { + activeField = touchedField; + requestUpdate(); + } + return; + } + } + + if (mappedInput.wasScreenTapped(tx, ty)) { + if (contains(downRect, tx, ty)) { + adjustActiveField(-1); + requestUpdate(); + return; + } + if (contains(upRect, tx, ty)) { + adjustActiveField(+1); + requestUpdate(); + return; + } + + Field touchedField = FIELD_HOURS; + if (fieldFromPoint(tx, ty, touchedField)) { + activeField = touchedField; + requestUpdate(); + return; + } + } + } + buttonNavigator.onNextRelease([this] { adjustActiveField(+1); requestUpdate(); @@ -143,18 +277,16 @@ void ClockOffsetActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLOCK_UTC_OFFSET)); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_CLOCK_UTC_OFFSET), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_CLOCK_UTC_OFFSET)); + } - const int centreY = pageHeight / 2 - 40; + const PickerLayout layout = getPickerLayout(renderer, mappedInput.hasTouch()); auto widthOf = [&](const char* s) { return renderer.getTextWidth(UI_12_FONT_ID, s, EpdFontFamily::BOLD); }; - constexpr int fieldPaddingX = 6; - constexpr int labelGap = 16; - constexpr int fieldGap = 12; - constexpr int colonGap = 5; - const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); - const int fieldHeight = lineHeight + 2; char signStr[2] = {sign == 1 ? '-' : '+', '\0'}; char hoursStr[8]; @@ -162,48 +294,52 @@ void ClockOffsetActivity::render(RenderLock&&) { char minutesStr[8]; snprintf(minutesStr, sizeof(minutesStr), "%02d", minutesQuarter * MINUTES_PER_QUARTER); - const int labelWidth = widthOf("UTC"); - const int signBoxW = std::max(widthOf("+"), widthOf("-")) + fieldPaddingX * 2; - const int hoursBoxW = std::max(widthOf("14"), widthOf("12")) + fieldPaddingX * 2; - const int colonWidth = widthOf(":"); - const int minutesBoxW = std::max({widthOf("00"), widthOf("15"), widthOf("30"), widthOf("45")}) + fieldPaddingX * 2; - const int totalWidth = - labelWidth + labelGap + signBoxW + fieldGap + hoursBoxW + colonGap + colonWidth + colonGap + minutesBoxW; - - int x = (pageWidth - totalWidth) / 2; - renderer.drawText(UI_12_FONT_ID, x, centreY, "UTC", true, EpdFontFamily::BOLD); - x += labelWidth + labelGap; + renderer.drawText(UI_12_FONT_ID, layout.labelX, layout.textY, "UTC", true, EpdFontFamily::BOLD); - auto drawField = [&](const char* text, const int boxX, const int boxWidth, const Field field) { + auto drawField = [&](const char* text, const Rect& rect, const Field field) { const bool selected = activeField == field; - renderer.fillRectDither(boxX, centreY, boxWidth, fieldHeight, selected ? Color::LightGray : Color::White); - renderer.drawRect(boxX, centreY, boxWidth, fieldHeight, true); + renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, selected ? Color::LightGray : Color::White); + renderer.drawRect(rect.x, rect.y, rect.width, rect.height, true); if (selected) { - renderer.drawRect(boxX + 1, centreY + 1, boxWidth - 2, fieldHeight - 2, true); + renderer.drawRect(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2, true); } - const int textX = boxX + (boxWidth - widthOf(text)) / 2; - renderer.drawText(UI_12_FONT_ID, textX, centreY, text, true, EpdFontFamily::BOLD); + const int textX = rect.x + (rect.width - widthOf(text)) / 2; + renderer.drawText(UI_12_FONT_ID, textX, layout.textY, text, true, EpdFontFamily::BOLD); }; - drawField(signStr, x, signBoxW, FIELD_SIGN); - x += signBoxW + fieldGap; - - drawField(hoursStr, x, hoursBoxW, FIELD_HOURS); - x += hoursBoxW + colonGap; + drawField(signStr, layout.signRect, FIELD_SIGN); + drawField(hoursStr, layout.hoursRect, FIELD_HOURS); + renderer.drawText(UI_12_FONT_ID, layout.colonX, layout.textY, ":", true, EpdFontFamily::BOLD); + drawField(minutesStr, layout.minutesRect, FIELD_MINUTES); - renderer.drawText(UI_12_FONT_ID, x, centreY, ":", true, EpdFontFamily::BOLD); - x += colonWidth + colonGap; - - drawField(minutesStr, x, minutesBoxW, FIELD_MINUTES); + if (mappedInput.hasTouch()) { + Rect upRect; + Rect downRect; + getTouchControlRects(upRect, downRect); + auto drawTouchButton = [&](const Rect& rect, const freeink::Icon& icon) { + renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::White); + renderer.drawRect(rect.x, rect.y, rect.width, rect.height, true); + const freeink::ui::BitmapRef bitmap{icon.bits, icon.w, icon.h, freeink::ui::BitmapFormat::Mask1, true}; + freeink::ui::forEachBitmapPixel( + freeink::ui::Rect{static_cast(rect.x), static_cast(rect.y), + static_cast(rect.width), static_cast(rect.height)}, + bitmap, freeink::ui::BitmapMode::Center, + [this](const int16_t px, const int16_t py) { renderer.drawPixel(px, py, true); }); + }; + drawTouchButton(upRect, icon_chevron_up_32); + drawTouchButton(downRect, icon_chevron_down_32); + } // Live preview of the resulting wall-clock time, so users can verify against a watch. if (halClock.isAvailable()) { char timeBuf[9]; const uint8_t encoded = encodeOffset(sign, hours, minutesQuarter); if (halClock.formatTime(timeBuf, sizeof(timeBuf), encoded, SETTINGS.clockFormat == 1)) { - char preview[24]; + // STR_CURRENT_TIME alone is 26 bytes in Russian and 24 in Arabic and + // Ukrainian, before the separator and formatted time are appended. + char preview[64]; snprintf(preview, sizeof(preview), "%s %s", tr(STR_CURRENT_TIME), timeBuf); - renderer.drawCenteredText(UI_10_FONT_ID, centreY + 60, preview); + renderer.drawCenteredText(UI_10_FONT_ID, layout.downRect.y + layout.downRect.height + 24, preview); } } diff --git a/src/activities/settings/ClockOffsetActivity.h b/src/activities/settings/ClockOffsetActivity.h index 95a1a943ad..64063b4a79 100644 --- a/src/activities/settings/ClockOffsetActivity.h +++ b/src/activities/settings/ClockOffsetActivity.h @@ -3,6 +3,8 @@ #include "activities/Activity.h" #include "util/ButtonNavigator.h" +struct Rect; + // Dedicated UTC offset picker for the status bar clock. // Three editable fields (sign, hours, minutes); Confirm cycles fields, Up/Down adjust the active one. // Supports the full IANA UTC offset range in 15 minute steps, including oddball zones like Nepal (+5:45). @@ -34,4 +36,6 @@ class ClockOffsetActivity final : public Activity { void saveToSettings() const; void adjustActiveField(int delta); void clampForSign(); + bool fieldFromPoint(int x, int y, Field& field) const; + void getTouchControlRects(Rect& upRect, Rect& downRect) const; }; diff --git a/src/activities/settings/ClockSyncActivity.cpp b/src/activities/settings/ClockSyncActivity.cpp index c49226dda0..0f18f13d0b 100644 --- a/src/activities/settings/ClockSyncActivity.cpp +++ b/src/activities/settings/ClockSyncActivity.cpp @@ -12,6 +12,7 @@ #include "MappedInputManager.h" #include "SdCardFontSystem.h" #include "activities/network/WifiSelectionActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" @@ -95,7 +96,14 @@ void ClockSyncActivity::loop() { return; } - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + finish(); + return; + } + + int x = 0; + int y = 0; + if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasScreenTapped(x, y)) { finish(); } } @@ -107,7 +115,12 @@ void ClockSyncActivity::render(RenderLock&&) { renderer.clearScreen(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLOCK_SYNC)); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_CLOCK_SYNC), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_CLOCK_SYNC)); + } const int midY = pageHeight / 2; @@ -118,7 +131,9 @@ void ClockSyncActivity::render(RenderLock&&) { case SUCCESS: { renderer.drawCenteredText(UI_12_FONT_ID, midY - 20, tr(STR_CLOCK_SYNC_OK), true, EpdFontFamily::BOLD); if (syncedTime[0] != '\0') { - char line[32]; + // Sized for the longest translated label plus a 12-hour time. UTF-8 + // translations can use multiple bytes per displayed character. + char line[64]; snprintf(line, sizeof(line), "%s %s", tr(STR_CURRENT_TIME), syncedTime); renderer.drawCenteredText(UI_10_FONT_ID, midY + 10, line); } @@ -139,5 +154,5 @@ void ClockSyncActivity::render(RenderLock&&) { GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); } diff --git a/src/activities/settings/ClockSyncActivity.h b/src/activities/settings/ClockSyncActivity.h index 06ec956e06..edcbe38a40 100644 --- a/src/activities/settings/ClockSyncActivity.h +++ b/src/activities/settings/ClockSyncActivity.h @@ -1,6 +1,7 @@ #pragma once #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" // Manual NTP resync action. Runs a forced sync (bypassing the once-per-device debounce), // reports success/failure, then waits for Back. If WiFi is not connected yet, it reuses the @@ -19,6 +20,7 @@ class ClockSyncActivity final : public Activity { private: enum State { SYNCING, SUCCESS, NO_WIFI, FAILED }; State state = SYNCING; + ScreenTransitionRefresh screenTransitionRefresh; char syncedTime[16] = {0}; bool shouldTearDownWifiOnExit = false; diff --git a/src/activities/settings/DictionarySelectActivity.cpp b/src/activities/settings/DictionarySelectActivity.cpp new file mode 100644 index 0000000000..01a2e4af61 --- /dev/null +++ b/src/activities/settings/DictionarySelectActivity.cpp @@ -0,0 +1,400 @@ +#include "DictionarySelectActivity.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "CrossPointSettings.h" +#include "I18nKeys.h" +#include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" +#include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" +#include "util/DictionaryRegistry.h" + +namespace fui = freeink::ui; +namespace { +constexpr fui::ActionId ACTION_ROW = 1; +} // namespace + +DictionarySelectActivity::DictionarySelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + std::string bookCachePath, const bool disableCurrentSelection, + const bool temporarySelection) + : Activity("DictionarySelect", renderer, mappedInput), + bookCachePath(std::move(bookCachePath)), + disableCurrentSelection(disableCurrentSelection), + temporarySelection(temporarySelection), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + +// --------------------------------------------------------------------------- +// Lifecycle +// --------------------------------------------------------------------------- + +void DictionarySelectActivity::onEnter() { + Activity::onEnter(); + + // Suppress Confirm bleed-through only in settings mode: when launched from a list, + // the Confirm release that opened the picker fires again in the picker's first loop. + // In per-book mode (launched via reader menu) the menu already consumed the event. + ignoreNextConfirmRelease = bookCachePath.empty(); + + scanDictionaries(); + + if (bookCachePath.empty()) { + // Settings mode: validate global path, pre-select from SETTINGS. + Dictionary::isValidDictionary(); + + selectedIndex = 0; // default: None + { + const std::string activePath = Dictionary::readDictPath(); + if (!activePath.empty()) { + for (int i = 0; i < static_cast(dictFolders.size()); i++) { + if (folderForIndex(i + 1) == activePath) { + selectedIndex = i + 1; + break; + } + } + } + } + } else { + // Per-book mode: read saved per-book path, pre-select it. + currentBookDictPath = ""; + HalFile f; + if (Storage.openFileForRead("DSEL", bookCachePath + "/dictionary.bin", f)) { + const int sz = static_cast(f.fileSize()); + if (sz > 0) { + std::string path(sz, '\0'); + const int n = f.read(&path[0], sz); + if (n > 0) { + path.resize(n); + currentBookDictPath = path; + } + } + f.close(); + } + + selectedIndex = 0; // default: Use Global + if (!currentBookDictPath.empty()) { + for (int i = 0; i < static_cast(dictFolders.size()); i++) { + if (folderForIndex(i + 1) == currentBookDictPath) { + selectedIndex = i + 1; + break; + } + } + } + + // Build augmented "Use Global" label showing the active global dictionary name. + // Path format: // — extract . + const std::string globalPath = Dictionary::readConfiguredDictPath(); + std::string globalFolderName; + if (globalPath.empty()) { + globalFolderName = tr(STR_DICT_NONE); + } else { + const size_t lastSlash = globalPath.rfind('/'); + if (lastSlash != std::string::npos && lastSlash > 0) { + const size_t prevSlash = globalPath.rfind('/', lastSlash - 1); + globalFolderName = (prevSlash != std::string::npos) + ? globalPath.substr(prevSlash + 1, lastSlash - prevSlash - 1) + : globalPath.substr(0, lastSlash); + } else { + globalFolderName = globalPath; + } + } + useGlobalLabel = std::string(tr(STR_DICT_USE_GLOBAL)) + " (" + globalFolderName + ")"; + currentEffectiveDictPath = Dictionary::readDictPath(bookCachePath.c_str()); + } + + totalItems = 1 + static_cast(dictFolders.size()); + if (disableCurrentSelection) { + selectedIndex = firstSelectableIndexFrom(selectedIndex); + } + if (usesPopup()) { + std::vector options; + options.reserve(totalItems); + for (int i = 0; i < totalItems; ++i) options.emplace_back(nameForIndex(i)); + optionPopup.show(StrId::STR_BOOK_DICTIONARY, options, selectedIndex, [this](const int index) { + selectedIndex = index; + popupSelectionMade = true; + finishSelection(); + }); + requestUpdate(); + return; + } + topIndex = 0; + visibleRows = 1; + initialViewportPending = true; + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &DictionarySelectActivity::onRowEvent, this); + app.setScreen(&DictionarySelectActivity::listScreen, this); + requestUpdate(); +} + +void DictionarySelectActivity::onExit() { + dictionaryRegistry.clear(); + Activity::onExit(); +} + +// --------------------------------------------------------------------------- +// SD card scan +// --------------------------------------------------------------------------- + +void DictionarySelectActivity::scanDictionaries() { + // Discovery lives in DictionaryRegistry (shared with the settings list and web UI). + // Re-scan on every picker open (matches prior behaviour), then mirror the results into + // the activity's parallel vectors so folderForIndex()/metadata/per-book logic is unchanged. + dictionaryRegistry.discover(); + dictRoot = dictionaryRegistry.root(); + dictFolders.clear(); + dictStems.clear(); + const auto& entries = dictionaryRegistry.getEntries(); + dictFolders.reserve(entries.size()); + dictStems.reserve(entries.size()); + for (const auto& e : entries) { + dictFolders.push_back(e.name); + dictStems.push_back(e.stem); + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +std::string DictionarySelectActivity::folderForIndex(int index) const { + if (index <= 0 || index > static_cast(dictFolders.size())) return ""; + return dictRoot + "/" + dictFolders[index - 1] + "/" + dictStems[index - 1]; +} + +std::string DictionarySelectActivity::effectiveFolderForIndex(int index) const { + if (index == 0 && !bookCachePath.empty()) return Dictionary::readConfiguredDictPath(); + return folderForIndex(index); +} + +bool DictionarySelectActivity::rowIsDisabled(int index) const { + if (!disableCurrentSelection || currentEffectiveDictPath.empty()) return false; + return effectiveFolderForIndex(index) == currentEffectiveDictPath; +} + +int DictionarySelectActivity::firstSelectableIndexFrom(int start) const { + if (totalItems <= 0) return 0; + for (int offset = 0; offset < totalItems; offset++) { + const int idx = (start + offset) % totalItems; + if (!rowIsDisabled(idx)) return idx; + } + return start; +} + +const char* DictionarySelectActivity::nameForIndex(int index) const { + if (index == 0) return bookCachePath.empty() ? tr(STR_DICT_NONE) : useGlobalLabel.c_str(); + if (index <= static_cast(dictFolders.size())) return dictFolders[index - 1].c_str(); + return ""; +} + +bool DictionarySelectActivity::applySelection() { + if (rowIsDisabled(selectedIndex)) return false; + + std::string folder = folderForIndex(selectedIndex); + + if (bookCachePath.empty()) { + // Settings mode: update global dictionary.bin. + if (Dictionary::readDictPath() == folder) return false; + Dictionary::saveGlobalDictPath(folder.c_str()); + } else { + // Per-book mode: save to book cache. + if (currentBookDictPath == folder) return false; + HalFile f; + if (Storage.openFileForWrite("DSEL", bookCachePath + "/dictionary.bin", f)) { + f.write(reinterpret_cast(folder.c_str()), folder.size()); + f.close(); + } else { + LOG_ERR("DSEL", "Could not save per-book dictionary"); + } + currentBookDictPath = folder; + } + return true; +} + +// --------------------------------------------------------------------------- +// Input +// --------------------------------------------------------------------------- + +void DictionarySelectActivity::finishSelection() { + if (temporarySelection) { + if (rowIsDisabled(selectedIndex)) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + } else { + setResult(ActivityResult{FilePathResult{effectiveFolderForIndex(selectedIndex)}}); + } + finish(); + return; + } + + if (!applySelection()) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + } + finish(); +} + +void DictionarySelectActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= self->totalItems || self->rowIsDisabled(event.value)) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->finishSelection(); +} + +void DictionarySelectActivity::loop() { + if (usesPopup()) { + optionPopup.handleInput(mappedInput, [this] { requestUpdate(); }); + if (!optionPopup.isActive() && !popupSelectionMade) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + } + return; + } + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + finish(); + return; + } + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + finish(); + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (ignoreNextConfirmRelease) { + ignoreNextConfirmRelease = false; + return; + } + + finishSelection(); + return; + } + + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } + + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int next = scrollListBy(topIndex, swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows, + visibleRows, totalItems); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + + const auto moveForward = [this](const int next) { + selectedIndex = firstSelectableIndexFrom(next); + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, totalItems); + requestUpdate(); + }; + const auto moveBackward = [this](int next) { + if (totalItems > 0) { + for (int offset = 0; offset < totalItems && rowIsDisabled(next); offset++) { + next = ButtonNavigator::previousIndex(next, totalItems); + } + } + selectedIndex = next; + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, totalItems); + requestUpdate(); + }; + buttonNavigator.onNextRelease( + [this, &moveForward] { moveForward(ButtonNavigator::nextIndex(selectedIndex, totalItems)); }); + buttonNavigator.onPreviousRelease( + [this, &moveBackward] { moveBackward(ButtonNavigator::previousIndex(selectedIndex, totalItems)); }); + buttonNavigator.onNextContinuous( + [this, &moveForward] { moveForward(ButtonNavigator::nextPageIndex(selectedIndex, totalItems, visibleRows)); }); + buttonNavigator.onPreviousContinuous([this, &moveBackward] { + moveBackward(ButtonNavigator::previousPageIndex(selectedIndex, totalItems, visibleRows)); + }); +} + +// --------------------------------------------------------------------------- +// Rendering +// --------------------------------------------------------------------------- + +void DictionarySelectActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} + +void DictionarySelectActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + screen.spacer(static_cast(metrics.verticalSpacing)); + const std::string activePath = disableCurrentSelection + ? currentEffectiveDictPath + : (bookCachePath.empty() ? Dictionary::readDictPath() : currentBookDictPath); + std::vector items; + items.reserve(totalItems); + for (int i = 0; i < totalItems; ++i) { + const std::string folder = folderForIndex(i); + fui::ListItem item; + item.label = nameForIndex(i); + if ((folder.empty() && activePath.empty()) || (!folder.empty() && folder == activePath)) + item.value = tr(STR_SELECTED); + if (rowIsDisabled(i)) item.state = fui::StateDisabled; + item.actionValue = static_cast(i); + items.push_back(item); + } + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.valueInset = 8; + props.labelText = screen.theme().bodyText; + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = initialViewportPending ? followListSelection(selectedIndex, 0, visibleRows, totalItems) + : scrollListBy(topIndex, 0, visibleRows, totalItems); + initialViewportPending = false; + props.topIndex = static_cast(topIndex); + screen.list(props); +} + +void DictionarySelectActivity::render(RenderLock&&) { + if (usesPopup()) { + optionPopup.processRender(renderer, mappedInput); + return; + } + renderer.clearScreen(); + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_DICTIONARY), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_DICTIONARY)); + } + uiReady = false; + app.render(); + uiReady = true; + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} diff --git a/src/activities/settings/DictionarySelectActivity.h b/src/activities/settings/DictionarySelectActivity.h new file mode 100644 index 0000000000..5f59100d54 --- /dev/null +++ b/src/activities/settings/DictionarySelectActivity.h @@ -0,0 +1,91 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include "../Activity.h" +#include "components/OptionPopup.h" +#include "util/ButtonNavigator.h" +#include "util/Dictionary.h" + +class DictionarySelectActivity final : public Activity { + public: + DictionarySelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string bookCachePath = "", + bool disableCurrentSelection = false, bool temporarySelection = false); + + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; + + private: + // Active SD card root directory for dictionaries (resolved in scanDictionaries()). + std::string dictRoot; + + // Discovered dictionary folder names and file stems (parallel vectors, excluding "None"). + // e.g. dictFolders[i] = "dict-en-en", dictStems[i] = "dict-data" + // folderForIndex() combines them with dictRoot into the full base path used for file access. + std::vector dictFolders; + std::vector dictStems; + + // Index into the full list including "None" at position 0. + int selectedIndex = 0; + int totalItems = 0; + + // Suppresses the Confirm release that bleeds through from the parent activity launch. + bool ignoreNextConfirmRelease = false; + + // Non-empty when launched from reader menu (per-book override mode). + std::string bookCachePath; + // Active per-book dictionary path loaded on enter; empty = "Use Global". + std::string currentBookDictPath; + // Effective dictionary used by the current reader lookup. Non-empty only when + // launched as a switcher from a definition view. + std::string currentEffectiveDictPath; + // Augmented "Use Global" label showing the global dict name, e.g. "Use Global (dict-en-en)". + // Only populated in per-book mode. + std::string useGlobalLabel; + bool disableCurrentSelection = false; + // Lookup switchers return the chosen path without writing book/global settings. + bool temporarySelection = false; + + ButtonNavigator buttonNavigator; + using UiApp = freeink::ui::FreeInkApp<20, 4>; + freeink::ui::GfxRendererTarget uiTarget; + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + bool initialViewportPending = true; + OptionPopup optionPopup; + bool popupSelectionMade = false; + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); + void finishSelection(); + bool usesPopup() const { return !bookCachePath.empty() && !disableCurrentSelection; } + + // Scans the first available dictionary root directory on the SD card and populates dictFolders. + void scanDictionaries(); + + // Returns the folder path for a given list index (0 = None → empty string). + std::string folderForIndex(int index) const; + + // Returns the dictionary that selecting an item would make active. In per-book + // mode, index 0 means "Use Global" and resolves to the global dictionary path. + std::string effectiveFolderForIndex(int index) const; + + bool rowIsDisabled(int index) const; + int firstSelectableIndexFrom(int start) const; + + // Returns the display name for a given list index. + const char* nameForIndex(int index) const; + + // Applies the currently highlighted selection to settings and Dictionary. + bool applySelection(); +}; diff --git a/src/activities/settings/FontDownloadActivity.cpp b/src/activities/settings/FontDownloadActivity.cpp index 96603f68d0..fad2c510b9 100644 --- a/src/activities/settings/FontDownloadActivity.cpp +++ b/src/activities/settings/FontDownloadActivity.cpp @@ -15,16 +15,24 @@ #include "CrossPointSettings.h" #include "MappedInputManager.h" +#include "Memory.h" #include "SdCardFontSystem.h" #include "SilentRestart.h" #include "activities/network/WifiSelectionActivity.h" #include "activities/util/ConfirmationActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" #include "network/HttpDownloader.h" +namespace fui = freeink::ui; + namespace { +constexpr fui::ActionId ACTION_ROW = 1; + constexpr int FONT_DOWNLOAD_MAX_ATTEMPTS = 3; constexpr int FONT_MANIFEST_MAX_ATTEMPTS = 5; constexpr uint32_t FONT_DOWNLOAD_RETRY_DELAY_MS = 500; @@ -107,7 +115,21 @@ int fontListRowHeight(const GfxRenderer& renderer, const ThemeMetrics& metrics) } // namespace FontDownloadActivity::FontDownloadActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("FontDownload", renderer, mappedInput), fontInstaller_(sdFontSystem.registry()) {} + : Activity("FontDownload", renderer, mappedInput), + fontInstaller_(sdFontSystem.registry()), + uiTarget_(makeUiTarget(renderer)), + app_(uiTarget_, uiTarget_.deviceContext()) {} + +void FontDownloadActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (self->state_ != FAMILY_LIST) return; + if (event.value < 0 || event.value >= static_cast(self->listItemCount())) return; + self->selectedIndex_ = event.value; + // Activation starts a download or opens the delete prompt; a lingering + // flash would gray an unrelated row. + self->app_.clearTapFlash(); + self->activateSelected(); // ends with requestUpdateAndWait itself +} // --- Lifecycle --- @@ -119,6 +141,12 @@ void FontDownloadActivity::onEnter() { // once and exhaust the heap, aborting during manifest parse. Matches the // pre-network release done by the KOReader sync/auth activities. sdFontSystem.releaseForNetwork(renderer); + uiReady_ = false; + visibleRows_ = 1; + topIndex_ = 0; + app_.setTheme(uiThemeTokens(uiTarget_)); + app_.on(ACTION_ROW, &FontDownloadActivity::onRowEvent, this); + app_.setScreen(&FontDownloadActivity::listScreen, this); WiFi.mode(WIFI_STA); startActivityForResult(std::make_unique(renderer, mappedInput), [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); @@ -130,10 +158,15 @@ void FontDownloadActivity::onExit() { if (WiFi.getMode() != WIFI_MODE_NULL) { WiFi.disconnect(false); delay(30); - silentRestart(); + if (fontsChanged_) { + silentRestart(); + } else { + WiFi.mode(WIFI_OFF); + } } sdFontSystem.ensureLoaded(renderer); + sdFontSystem.releaseRegistry(); } void FontDownloadActivity::onWifiSelectionComplete(const bool success) { @@ -149,6 +182,10 @@ void FontDownloadActivity::onWifiSelectionComplete(const bool success) { requestUpdateAndWait(); if (!fetchAndParseManifest()) { + if (goHomeRequested_) { + onGoHome(); + return; + } if (cancelRequested_) { finishAfterBackPress(); return; @@ -177,6 +214,7 @@ bool FontDownloadActivity::fetchAndParseManifest() { baseUrl_.clear(); clearManifestFamilies(); cancelRequested_ = false; + goHomeRequested_ = false; // Poll the Cancel (Back) button while the manifest downloads so a slow or // failing network can be backed out of. HttpDownloader checks shouldCancel on @@ -188,6 +226,10 @@ bool FontDownloadActivity::fetchAndParseManifest() { return true; } mappedInput.update(); + if (mappedInput.wasHomeGesture()) { + goHomeRequested_ = true; + cancelRequested_ = true; + } if (mappedInput.isPressed(MappedInputManager::Button::Back) || mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasReleased(MappedInputManager::Button::Back)) { @@ -267,18 +309,17 @@ bool FontDownloadActivity::fetchAndParseManifest() { JsonArray familiesArr = doc["families"].as(); parsedFamilies.reserve(familiesArr.size()); - for (JsonObject fObj : familiesArr) { + // ArduinoJson owns a second copy of every manifest string. Consume the + // array from the front and remove each family after copying it so those + // strings are released before the next family's vectors grow. Keeping the + // whole JSON tree alive here can exhaust the heap partway through the list. + while (!familiesArr.isNull() && familiesArr.size() > 0) { + JsonObject fObj = familiesArr[0]; ManifestFamily family; family.name = fObj["name"] | ""; family.description = fObj["description"] | ""; family.languages = fObj["languages"] | ""; - JsonArray stylesArr = fObj["styles"].as(); - family.styles.reserve(stylesArr.size()); - for (JsonVariant s : stylesArr) { - family.styles.push_back(s.as()); - } - family.totalSize = 0; JsonArray filesArr = fObj["files"].as(); family.files.reserve(filesArr.size()); @@ -308,10 +349,12 @@ bool FontDownloadActivity::fetchAndParseManifest() { } if (family.files.empty()) { + familiesArr.remove(0); continue; } parsedFamilies.push_back(std::move(family)); + familiesArr.remove(0); } families_.swap(parsedFamilies); @@ -320,11 +363,24 @@ bool FontDownloadActivity::fetchAndParseManifest() { // Second pass: load the installed-font registry and resolve installed/update // state now that the manifest JsonDocument has been released, keeping peak // heap usage down on devices with many SD fonts installed. - fontInstaller_.refreshRegistry(); + if (!fontInstaller_.refreshRegistry()) { + LOG_ERR("FONT", "Not enough contiguous heap to scan installed fonts (free=%u maxAlloc=%u)", ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + errorMessage_ = tr(STR_MEMORY_ERROR); + return false; + } for (auto& family : families_) { resolveInstalledFamilyName(family); } + if (!rebuildListItems()) { + // Do not leave FAMILY_LIST with a populated manifest but no renderable + // rows: button navigation would still act on families the UI cannot show. + clearManifestFamilies(); + errorMessage_ = tr(STR_MEMORY_ERROR); + return false; + } + LOG_DBG("FONT", "Manifest loaded: %zu families", families_.size()); return true; } @@ -391,13 +447,15 @@ void FontDownloadActivity::resolveInstalledFamilyName(ManifestFamily& family) co // --- Download --- -void FontDownloadActivity::clearManifestFamilies() { std::vector().swap(families_); } - -void FontDownloadActivity::downloadAll() { downloadBatch(false); } - -void FontDownloadActivity::updateAll() { downloadBatch(true); } +void FontDownloadActivity::clearManifestFamilies() { + // Storage remains allocated for the activity lifetime, but none of its + // borrowed family-string pointers may be used until the next manifest fills + // it again. + listItemCount_ = 0; + std::vector().swap(families_); +} -void FontDownloadActivity::downloadBatch(bool updatesOnly) { +void FontDownloadActivity::updateAll() { cancelRequested_ = false; hasRetryFamily_ = false; retryFamily_ = ManifestFamily(); @@ -407,7 +465,7 @@ void FontDownloadActivity::downloadBatch(bool updatesOnly) { int nextFamilyIndex = -1; for (int i = 0; i < static_cast(families_.size()); i++) { const auto& family = families_[i]; - if (updatesOnly ? family.hasUpdate : !family.installed) { + if (family.hasUpdate) { nextFamilyIndex = i; break; } @@ -456,13 +514,6 @@ void FontDownloadActivity::downloadBatch(bool updatesOnly) { } } -bool FontDownloadActivity::showDownloadAllRow() const { - for (const auto& f : families_) { - if (!f.installed) return true; - } - return false; -} - bool FontDownloadActivity::showUpdateAllRow() const { for (const auto& f : families_) { if (f.hasUpdate) return true; @@ -470,34 +521,74 @@ bool FontDownloadActivity::showUpdateAllRow() const { return false; } -int FontDownloadActivity::specialRowCount() const { - return (showDownloadAllRow() ? 1 : 0) + (showUpdateAllRow() ? 1 : 0); -} +int FontDownloadActivity::specialRowCount() const { return showUpdateAllRow() ? 1 : 0; } -bool FontDownloadActivity::isDownloadAllRow(int index) const { return showDownloadAllRow() && index == 0; } - -bool FontDownloadActivity::isUpdateAllRow(int index) const { - return showUpdateAllRow() && index == (showDownloadAllRow() ? 1 : 0); -} +bool FontDownloadActivity::isUpdateAllRow(int index) const { return showUpdateAllRow() && index == 0; } int FontDownloadActivity::listItemCount() const { return families_.empty() ? 0 : static_cast(families_.size()) + specialRowCount(); } -size_t FontDownloadActivity::totalDownloadSize() const { +size_t FontDownloadActivity::totalUpdateSize() const { size_t total = 0; for (const auto& f : families_) { - if (!f.installed) total += f.totalSize; + if (f.hasUpdate) total += f.totalSize; } return total; } -size_t FontDownloadActivity::totalUpdateSize() const { - size_t total = 0; - for (const auto& f : families_) { - if (f.hasUpdate) total += f.totalSize; +bool FontDownloadActivity::rebuildListItems() { + const int count = listItemCount(); + if (count <= 0) { + listItemCount_ = 0; + return true; } - return total; + + const size_t required = static_cast(count); + if (required > listItemCapacity_) { + // This activity-lifetime buffer avoids redraw-time allocation and has an + // explicit low-memory fallback. The log reports its exact byte size. + auto items = makeUniqueNoThrow(required); + if (!items) { + LOG_ERR("FONT", "Failed to allocate %zu-byte font list (heap=%u maxAlloc=%u)", required * sizeof(fui::ListItem), + ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + listItemCount_ = 0; + return false; + } + listItems_ = std::move(items); + listItemCapacity_ = required; + } + + size_t itemIndex = 0; + if (showUpdateAllRow()) { + char sizeLabel[32]; + formatSize(totalUpdateSize(), sizeLabel, sizeof(sizeLabel)); + snprintf(updateAllLabel_, sizeof(updateAllLabel_), "%s (%s)", tr(STR_UPDATE_ALL), sizeLabel); + listItems_[itemIndex] = fui::ListItem{}; + listItems_[itemIndex].label = updateAllLabel_; + listItems_[itemIndex].actionValue = static_cast(itemIndex); + ++itemIndex; + } + + for (const auto& family : families_) { + fui::ListItem& item = listItems_[itemIndex]; + item = fui::ListItem{}; + item.label = family.name.c_str(); + if (!family.description.empty()) item.subtitle = family.description.c_str(); + if (family.hasUpdate) { + item.value = tr(STR_UPDATE_AVAILABLE); + } else if (family.installed) { + item.value = tr(STR_INSTALLED); + // Dimmed but still tappable (opens the delete prompt): visual-only + // disabled state, the row stays enabled for hit registration. + item.state = fui::StateDisabled; + } + item.actionValue = static_cast(itemIndex); + ++itemIndex; + } + + listItemCount_ = itemIndex; + return true; } // Standard CRC32 matching zlib/Python zlib.crc32(). @@ -618,6 +709,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { downloadAttempt_ = 0; downloadAttemptTotal_ = 0; cancelRequested_ = false; + goHomeRequested_ = false; } requestUpdateAndWait(); @@ -648,7 +740,6 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { uint32_t existingCrc = 0; if (computeFileCrc32(destPath, existingCrc) && existingCrc == file.crc32 && fontInstaller_.validateCpfontFile(destPath)) { - LOG_DBG("FONT", "Skipping existing %s (crc32=%08x)", file.name.c_str(), existingCrc); { RenderLock lock(*this); fileProgress_ = file.size; @@ -675,6 +766,10 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { return true; } mappedInput.update(); + if (mappedInput.wasHomeGesture()) { + goHomeRequested_ = true; + cancelRequested_ = true; + } if (mappedInput.isPressed(MappedInputManager::Button::Back) || mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasReleased(MappedInputManager::Button::Back)) { @@ -692,8 +787,6 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { if (attempt > 1) { LOG_DBG("FONT", "Retrying %s (%d/%d)", file.name.c_str(), attempt, FONT_DOWNLOAD_MAX_ATTEMPTS); } - LOG_DBG("FONT", "Download attempt %d/%d: %s (%zu bytes)", attempt, FONT_DOWNLOAD_MAX_ATTEMPTS, file.name.c_str(), - file.size); requestUpdateAndWait(); if (attempt > 1) delay(FONT_DOWNLOAD_RETRY_DELAY_MS); @@ -708,6 +801,10 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { if (result == HttpDownloader::ABORTED) { LOG_INF("FONT", "Download cancelled: %s", file.name.c_str()); Storage.remove(tempPath); + if (goHomeRequested_) { + onGoHome(); + return; + } // The Back release that confirmed the cancel would otherwise be seen by // the family list and treated as a request to leave the screen. mappedInput.suppressNextBackRelease(); @@ -721,8 +818,6 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { return; } if (result == HttpDownloader::OK) { - LOG_DBG("FONT", "Download attempt succeeded: %s (%d/%d)", file.name.c_str(), attempt, - FONT_DOWNLOAD_MAX_ATTEMPTS); break; } LOG_ERR("FONT", "Download attempt failed: %s (%d/%d, error=%d)", file.name.c_str(), attempt, @@ -748,7 +843,6 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { failDownload("Downloaded file did not match: " + file.name, tr(STR_FONT_DOWNLOAD_CHECKSUM_HINT)); return; } - LOG_DBG("FONT", "Downloaded %s (size=%zu crc32=%08x)", file.name.c_str(), file.size, actualCrc); if (!fontInstaller_.validateCpfontFile(tempPath)) { LOG_ERR("FONT", "Invalid .cpfont: %s", tempPath); @@ -790,6 +884,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { if (hadExistingFile) { Storage.remove(backupPath); } + fontsChanged_ = true; currentFileIndex_++; } @@ -831,90 +926,152 @@ void FontDownloadActivity::onDeleteConfirmationResult(const ActivityResult& resu state_ = ERROR; errorMessage_ = "Failed to delete font"; } else { + fontsChanged_ = true; fontInstaller_.refreshRegistry(); family.installed = false; family.hasUpdate = false; + // Deletion changes the row's visual state. Reuse the existing storage; + // the row count cannot grow on this path. + rebuildListItems(); } requestUpdate(); } bool FontDownloadActivity::isSelectedFamilyDeletable() const { - if (isDownloadAllRow(selectedIndex_) || isUpdateAllRow(selectedIndex_)) return false; + if (isUpdateAllRow(selectedIndex_)) return false; if (selectedIndex_ < specialRowCount() || selectedIndex_ >= listItemCount()) return false; const auto& family = families_[familyIndexFromList(selectedIndex_)]; return family.installed && !family.hasUpdate; } +void FontDownloadActivity::activateSelected() { + if (families_.empty()) return; + if (isUpdateAllRow(selectedIndex_)) { + currentFileIndex_ = 0; + currentFileTotal_ = 0; + for (const auto& f : families_) { + if (f.hasUpdate) currentFileTotal_ += f.files.size(); + } + updateAll(); + } else { + auto& family = families_[familyIndexFromList(selectedIndex_)]; + if (!family.installed || family.hasUpdate) { + currentFileIndex_ = 0; + currentFileTotal_ = family.files.size(); + downloadFamily(family); + } else { + promptDeleteSelectedFamily(); + return; + } + } + requestUpdateAndWait(); +} + +void FontDownloadActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} + +void FontDownloadActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + // Content below the GUI.drawHeader band, above the button hints. + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + screen.spacer(static_cast(metrics.verticalSpacing)); + + if (families_.empty()) { + screen.centeredText(tr(STR_NO_FONTS_AVAILABLE), screen.theme().bodyText); + return; + } + + const int listSize = static_cast(listItemCount_); + if (listSize <= 0 || !listItems_) { + screen.centeredText(tr(STR_NO_FONTS_AVAILABLE), screen.theme().bodyText); + return; + } + + fui::ListProps props; + props.items = listItems_.get(); + props.count = static_cast(listItemCount_); + props.selectedIndex = static_cast(selectedIndex_); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; // physical buttons stay in loop() + props.valueInset = 8; // air between the status and the row edge + const auto rows = configureUiList(props, screen.theme(), screen.body(), UiListRowType::WithSubtitle); + visibleRows_ = rows > 0 ? rows : 1; + topIndex_ = scrollListBy(topIndex_, 0, visibleRows_, listSize); // clamp to range + props.topIndex = static_cast(topIndex_); + screen.list(props); +} + // --- Input handling --- void FontDownloadActivity::loop() { if (state_ == FAMILY_LIST) { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer) || + mappedInput.wasPressed(MappedInputManager::Button::Back)) { finishAfterBackPress(); return; } const int listSize = listItemCount(); - const int pageItems = fontListPageItems(); - buttonNavigator_.onNextRelease([this, listSize] { - selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, listSize); - requestUpdate(); - }); + // Touch goes through the FreeInkApp: render() registered the row hit + // rects; route the snapshot and let onRowEvent dispatch. + if (uiReady_) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app_.route(snap); + if (app_.invalidated()) requestUpdate(); + if (event) return; // dispatched to onRowEvent + } + } - buttonNavigator_.onPreviousRelease([this, listSize] { - selectedIndex_ = ButtonNavigator::previousIndex(selectedIndex_, listSize); - requestUpdate(); - }); + if (!families_.empty()) { + // Swipes scroll the viewport; the selection stays put and button + // navigation pulls the view back to it. + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows_ : -visibleRows_; + const int next = scrollListBy(topIndex_, delta, visibleRows_, listSize); + if (next != topIndex_) { + topIndex_ = next; + requestUpdate(); + } + return; + } + } - buttonNavigator_.onNextContinuous([this, listSize, pageItems] { - selectedIndex_ = ButtonNavigator::nextPageIndex(selectedIndex_, listSize, pageItems); + const auto moveSelection = [this, listSize](const int index) { + selectedIndex_ = index; + topIndex_ = followListSelection(selectedIndex_, topIndex_, visibleRows_, listSize); requestUpdate(); + }; + buttonNavigator_.onNextRelease( + [this, listSize, &moveSelection] { moveSelection(ButtonNavigator::nextIndex(selectedIndex_, listSize)); }); + buttonNavigator_.onPreviousRelease( + [this, listSize, &moveSelection] { moveSelection(ButtonNavigator::previousIndex(selectedIndex_, listSize)); }); + buttonNavigator_.onNextContinuous([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::nextPageIndex(selectedIndex_, listSize, visibleRows_)); }); - - buttonNavigator_.onPreviousContinuous([this, listSize, pageItems] { - selectedIndex_ = ButtonNavigator::previousPageIndex(selectedIndex_, listSize, pageItems); - requestUpdate(); + buttonNavigator_.onPreviousContinuous([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::previousPageIndex(selectedIndex_, listSize, visibleRows_)); }); if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - if (!families_.empty()) { - if (isDownloadAllRow(selectedIndex_)) { - currentFileIndex_ = 0; - currentFileTotal_ = 0; - for (const auto& f : families_) { - if (!f.installed) currentFileTotal_ += f.files.size(); - } - - downloadAll(); - } else if (isUpdateAllRow(selectedIndex_)) { - currentFileIndex_ = 0; - currentFileTotal_ = 0; - for (const auto& f : families_) { - if (f.hasUpdate) currentFileTotal_ += f.files.size(); - } - updateAll(); - } else { - const int familyIndex = familyIndexFromList(selectedIndex_); - auto& family = families_[familyIndex]; - if (!family.installed || family.hasUpdate) { - currentFileIndex_ = 0; - currentFileTotal_ = family.files.size(); - downloadSelectedFamily(familyIndex); - } else { - promptDeleteSelectedFamily(); - return; - } - } - requestUpdateAndWait(); - return; - } + activateSelected(); + return; } } else if (state_ == COMPLETE) { + int x = 0; + int y = 0; if (mappedInput.wasPressed(MappedInputManager::Button::Back) || - mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - returnToFamilyList(); + mappedInput.wasPressed(MappedInputManager::Button::Confirm) || mappedInput.wasScreenTapped(x, y)) { + { + RenderLock lock(*this); + state_ = FAMILY_LIST; + } requestUpdate(); } } else if (state_ == ERROR) { @@ -960,28 +1117,42 @@ void FontDownloadActivity::loop() { returnToFamilyList(); requestUpdate(); } + } else { + int x = 0; + int y = 0; + if (mappedInput.wasScreenTapped(x, y)) { + if (downloadingFamilyIndex_ >= 0 && downloadingFamilyIndex_ < static_cast(families_.size())) { + downloadFamily(families_[downloadingFamilyIndex_]); + requestUpdateAndWait(); + return; + } + { + RenderLock lock(*this); + state_ = FAMILY_LIST; + } + requestUpdate(); + } } } } // --- Rendering --- -std::string FontDownloadActivity::formatSize(size_t bytes) { - char buf[32]; +void FontDownloadActivity::formatSize(const size_t bytes, char* const buffer, const size_t bufferSize) { + if (bufferSize == 0) return; if (bytes >= 1024 * 1024) { - snprintf(buf, sizeof(buf), "%.1f MB", static_cast(bytes) / (1024.0 * 1024.0)); + snprintf(buffer, bufferSize, "%.1f MB", static_cast(bytes) / (1024.0 * 1024.0)); } else if (bytes >= 1024) { - snprintf(buf, sizeof(buf), "%.0f KB", static_cast(bytes) / 1024.0); + snprintf(buffer, bufferSize, "%.0f KB", static_cast(bytes) / 1024.0); } else { - snprintf(buf, sizeof(buf), "%zu B", bytes); + snprintf(buffer, bufferSize, "%zu B", bytes); } - return buf; } int FontDownloadActivity::fontListPageItems() const { const auto& metrics = UITheme::getInstance().getMetrics(); - const int reservedHeight = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + - metrics.buttonHintsHeight + metrics.verticalSpacing; + const int reservedHeight = metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + + metrics.verticalSpacing + metrics.buttonHintsHeight + metrics.verticalSpacing; const int availableHeight = renderer.getScreenHeight() - reservedHeight; return std::max(1, availableHeight / fontListRowHeight(renderer, metrics)); } @@ -1025,7 +1196,7 @@ void FontDownloadActivity::drawFontList(Rect rect) { for (int index = pageStartIndex; index < pageEndIndex; ++index) { const int rowY = rect.y + (index - pageStartIndex) * rowHeight; const bool selected = index == selectedIndex_; - const bool familyRow = !isDownloadAllRow(index) && !isUpdateAllRow(index); + const bool familyRow = !isUpdateAllRow(index); const bool dimmed = familyRow && families_[familyIndexFromList(index)].installed && !families_[familyIndexFromList(index)].hasUpdate; @@ -1038,10 +1209,10 @@ void FontDownloadActivity::drawFontList(Rect rect) { std::string languages; std::string value; - if (isDownloadAllRow(index)) { - title = std::string(tr(STR_DOWNLOAD_ALL)) + " (" + formatSize(totalDownloadSize()) + ")"; - } else if (isUpdateAllRow(index)) { - title = std::string(tr(STR_UPDATE_ALL)) + " (" + formatSize(totalUpdateSize()) + ")"; + if (isUpdateAllRow(index)) { + char sizeLabel[32]; + formatSize(totalUpdateSize(), sizeLabel, sizeof(sizeLabel)); + title = std::string(tr(STR_UPDATE_ALL)) + " (" + sizeLabel + ")"; } else { const auto& family = families_[familyIndexFromList(index)]; title = family.name; @@ -1098,10 +1269,16 @@ void FontDownloadActivity::render(RenderLock&&) { renderer.clearScreen(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FONT_BROWSER)); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget_, header, tr(STR_FONT_BROWSER), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_FONT_BROWSER)); + } const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID); - const auto contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const auto contentTop = + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; const auto centerY = (pageHeight - lineHeight) / 2; if (state_ == LOADING_MANIFEST) { @@ -1109,21 +1286,17 @@ void FontDownloadActivity::render(RenderLock&&) { const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } else if (state_ == FAMILY_LIST) { - if (families_.empty()) { - renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_NO_FONTS_AVAILABLE)); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - } else { - drawFontList(Rect{0, contentTop, pageWidth, - pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing}); - - const auto labels = mappedInput.mapLabels(tr(STR_BACK), - isSelectedFamilyDeletable() ? tr(STR_DELETE) - : isUpdateAllRow(selectedIndex_) ? tr(STR_UPDATE) - : tr(STR_DOWNLOAD), - tr(STR_DIR_UP), tr(STR_DIR_DOWN)); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - } + uiReady_ = false; + app_.render(); + uiReady_ = true; + + const char* confirmLabel = families_.empty() ? "" + : isSelectedFamilyDeletable() ? tr(STR_DELETE) + : isUpdateAllRow(selectedIndex_) ? tr(STR_UPDATE) + : tr(STR_DOWNLOAD); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, families_.empty() ? "" : tr(STR_DIR_UP), + families_.empty() ? "" : tr(STR_DIR_DOWN)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } else if (state_ == DOWNLOADING) { const char* familyName = activeDownloadFamilyName_.empty() ? "" : activeDownloadFamilyName_.c_str(); @@ -1179,5 +1352,5 @@ void FontDownloadActivity::render(RenderLock&&) { GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh_.modeFor(static_cast(state_))); } diff --git a/src/activities/settings/FontDownloadActivity.h b/src/activities/settings/FontDownloadActivity.h index 56a7f31066..57858e9b85 100644 --- a/src/activities/settings/FontDownloadActivity.h +++ b/src/activities/settings/FontDownloadActivity.h @@ -1,11 +1,17 @@ #pragma once +#include +#include + +#include +#include #include #include #include "FontInstaller.h" #include "SdCardFont.h" #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" #include "util/ButtonNavigator.h" struct Rect; @@ -67,7 +73,6 @@ class FontDownloadActivity : public Activity { std::string installName; std::string description; std::string languages; - std::vector styles; std::vector files; size_t totalSize = 0; bool installed = false; @@ -75,17 +80,25 @@ class FontDownloadActivity : public Activity { }; State state_ = WIFI_SELECTION; + ScreenTransitionRefresh screenTransitionRefresh_; FontInstaller fontInstaller_; ButtonNavigator buttonNavigator_; // Manifest data std::string baseUrl_; std::vector families_; + // Built once after each manifest load. The renderer borrows these pointers, + // so keeping them activity-owned avoids heap growth on every redraw. + std::unique_ptr listItems_; + size_t listItemCapacity_ = 0; + size_t listItemCount_ = 0; + char updateAllLabel_[96] = {}; int selectedIndex_ = 0; ManifestFamily retryFamily_; bool hasRetryFamily_ = false; bool manifestReloadNeeded_ = false; std::string activeDownloadFamilyName_; + bool fontsChanged_ = false; // Download progress size_t currentFileIndex_ = 0; @@ -98,9 +111,29 @@ class FontDownloadActivity : public Activity { std::string errorMessage_; std::string errorHint_; bool cancelRequested_ = false; + // Set when a blocking download consumed Home; exit only after its file and + // network resources have unwound. + bool goHomeRequested_ = false; + + // FreeInkApp hosts the family list (themed rows, touch routing); the other + // states keep their legacy centered-text rendering. + using UiApp = freeink::ui::FreeInkApp<20, 4>; + freeink::ui::GfxRendererTarget uiTarget_; // must precede `app_`: the app holds a reference to it + UiApp app_; + // render() rebuilds the app's interaction table; loop() only routes touch + // snapshots against it while this is true (the two run on different tasks). + std::atomic uiReady_{false}; + int visibleRows_ = 1; // rows per page at the current scale; set by the screen builder + int topIndex_ = 0; // viewport scroll position, decoupled from the selection + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); + void activateSelected(); void onWifiSelectionComplete(bool success); bool fetchAndParseManifest(); + bool rebuildListItems(); const SdCardFontFamilyInfo* findInstalledFamilyCandidate(const char* familyName) const; bool installedFilesMatch(const char* familyName, const std::vector& files, bool& hasUpdate, std::string* resolvedFamilyName = nullptr) const; @@ -108,24 +141,19 @@ class FontDownloadActivity : public Activity { void clearManifestFamilies(); void downloadFamily(ManifestFamily& family); void downloadSelectedFamily(int familyIndex); - void downloadBatch(bool updatesOnly); void returnToFamilyList(); - void downloadAll(); void updateAll(); static bool computeFileCrc32(const char* path, uint32_t& outCrc); - bool showDownloadAllRow() const; bool showUpdateAllRow() const; int specialRowCount() const; - bool isDownloadAllRow(int index) const; bool isUpdateAllRow(int index) const; bool isSelectedFamilyDeletable() const; void promptDeleteSelectedFamily(); void onDeleteConfirmationResult(const ActivityResult& result); int familyIndexFromList(int listIndex) const { return listIndex - specialRowCount(); } int listItemCount() const; - size_t totalDownloadSize() const; size_t totalUpdateSize() const; - static std::string formatSize(size_t bytes); + static void formatSize(size_t bytes, char* buffer, size_t bufferSize); int fontListPageItems() const; void drawFontList(Rect rect); }; diff --git a/src/activities/settings/FontSelectionActivity.cpp b/src/activities/settings/FontSelectionActivity.cpp index 0f40f91855..099eff8c11 100644 --- a/src/activities/settings/FontSelectionActivity.cpp +++ b/src/activities/settings/FontSelectionActivity.cpp @@ -11,12 +11,17 @@ #include "CrossPointSettings.h" #include "MappedInputManager.h" #include "SdCardFontSystem.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" +namespace fui = freeink::ui; namespace { constexpr uint8_t INVALID_STORED_FONT_SIZE = 0xFF; constexpr const char* ELLIPSIS_UTF8 = "\xe2\x80\xa6"; +constexpr fui::ActionId ACTION_ROW = 1; uint8_t closestSizeIndex(const std::vector& sizes, const uint8_t targetPointSize) { if (sizes.empty()) return 0; @@ -56,18 +61,8 @@ uint8_t closestBuiltinStoredSize(const uint8_t targetPointSize) { } uint8_t currentFontPointSize(const SdCardFontRegistry* registry) { - if (registry && SETTINGS.sdFontFamilyName[0] != '\0') { - const SdCardFontFamilyInfo* family = registry->findFamily(SETTINGS.sdFontFamilyName); - if (family) { - const std::vector sizes = family->availableSizes(); - if (!sizes.empty()) { - const uint8_t index = - SETTINGS.fontSize < sizes.size() ? SETTINGS.fontSize : static_cast(sizes.size() - 1); - return sizes[index]; - } - } - } - return CrossPointSettings::getReaderFontPointSize(SETTINGS.getEffectiveReaderFontSize()); + (void)registry; + return SETTINGS.readerFontPointSize; } int findCurrentFontIndex(const SdCardFontRegistry* registry, const char* sdFontFamilyName, uint8_t fontFamily) { @@ -86,14 +81,17 @@ int findCurrentFontIndex(const SdCardFontRegistry* registry, const char* sdFontF FontSelectionActivity::FontSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const SdCardFontRegistry* registry) - : Activity("FontSelect", renderer, mappedInput), registry_(registry) {} + : Activity("FontSelect", renderer, mappedInput), + registry_(registry), + uiTarget_(makeUiTarget(renderer)), + app_(uiTarget_, uiTarget_.deviceContext()) {} void FontSelectionActivity::onEnter() { Activity::onEnter(); // Get metrics and calculate layout dimensions metrics_ = UITheme::getInstance().getMetrics(); - afterHeader = metrics_.topPadding + metrics_.headerHeight + metrics_.verticalSpacing; + afterHeader = metrics_.topPadding + TouchHeaderBackButton::height(metrics_, mappedInput) + metrics_.verticalSpacing; bottomReserved = metrics_.buttonHintsHeight + metrics_.verticalSpacing; usableHeight = renderer.getScreenHeight() - afterHeader - bottomReserved; previewHeight = usableHeight * metrics_.previewHeightPercent / 100; @@ -117,14 +115,52 @@ void FontSelectionActivity::onEnter() { selectedIndex_ = findCurrentFontIndex(registry_, SETTINGS.sdFontFamilyName, SETTINGS.fontFamily); previewFontIndex_ = selectedIndex_; + topIndex_ = 0; + visibleRows_ = 1; + initialViewportPending_ = true; + uiReady_ = false; + app_.setTheme(uiThemeTokens(uiTarget_)); + app_.on(ACTION_ROW, &FontSelectionActivity::onRowEvent, this); + app_.setScreen(&FontSelectionActivity::listScreen, this); requestUpdate(); } void FontSelectionActivity::onExit() { Activity::onExit(); } +void FontSelectionActivity::activateSelected() { + if (selectedIndex_ == previewFontIndex_) { + handleSelection(); + return; + } + previewFontIndex_ = selectedIndex_; + const auto& font = fonts_[selectedIndex_]; + if (font.isBuiltin) { + SETTINGS.fontFamily = font.settingIndex; + SETTINGS.sdFontFamilyName[0] = '\0'; + } else if (registry_) { + const int sdIdx = font.settingIndex - CrossPointSettings::BUILTIN_FONT_COUNT; + const auto& families = registry_->getFamilies(); + if (sdIdx < static_cast(families.size())) { + strncpy(SETTINGS.sdFontFamilyName, families[sdIdx].name.c_str(), sizeof(SETTINGS.sdFontFamilyName) - 1); + SETTINGS.sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName) - 1] = '\0'; + sdFontSystem.ensureLoaded(renderer); + } + } + requestUpdate(); +} + +void FontSelectionActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(self->fonts_.size())) return; + self->selectedIndex_ = event.value; + self->app_.clearTapFlash(); + self->activateSelected(); +} + void FontSelectionActivity::loop() { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer) || + mappedInput.wasPressed(MappedInputManager::Button::Back)) { mappedInput.suppressNextBackRelease(); SETTINGS.fontFamily = originalFontFamily_; strncpy(SETTINGS.sdFontFamilyName, originalSdFontFamilyName_, sizeof(SETTINGS.sdFontFamilyName) - 1); @@ -135,51 +171,44 @@ void FontSelectionActivity::loop() { } if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - if (selectedIndex_ == previewFontIndex_) { - handleSelection(); - } else { - previewFontIndex_ = selectedIndex_; - const auto& font = fonts_[selectedIndex_]; - if (font.isBuiltin) { - SETTINGS.fontFamily = font.settingIndex; - SETTINGS.sdFontFamilyName[0] = '\0'; - } else if (registry_) { - const int sdIdx = font.settingIndex - CrossPointSettings::BUILTIN_FONT_COUNT; - const auto& families = registry_->getFamilies(); - if (sdIdx < static_cast(families.size())) { - strncpy(SETTINGS.sdFontFamilyName, families[sdIdx].name.c_str(), sizeof(SETTINGS.sdFontFamilyName) - 1); - SETTINGS.sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName) - 1] = '\0'; - sdFontSystem.ensureLoaded(renderer); - } - } - requestUpdate(); - } + activateSelected(); return; } const int listSize = static_cast(fonts_.size()); - const int pageItems = - UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false, previewHeight + metrics_.verticalSpacing); - - buttonNavigator_.onNextRelease([this, listSize] { - selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, listSize); - requestUpdate(); - }); - - buttonNavigator_.onPreviousRelease([this, listSize] { - selectedIndex_ = ButtonNavigator::previousIndex(selectedIndex_, listSize); - requestUpdate(); - }); + if (uiReady_) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app_.route(snap); + if (app_.invalidated()) requestUpdate(); + if (event) return; + } + } - buttonNavigator_.onNextContinuous([this, listSize, pageItems] { - selectedIndex_ = ButtonNavigator::nextPageIndex(selectedIndex_, listSize, pageItems); - requestUpdate(); - }); + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int next = scrollListBy(topIndex_, swipe == MappedInputManager::SwipeDir::Up ? visibleRows_ : -visibleRows_, + visibleRows_, listSize); + if (next != topIndex_) { + topIndex_ = next; + requestUpdate(); + } + return; + } - buttonNavigator_.onPreviousContinuous([this, listSize, pageItems] { - selectedIndex_ = ButtonNavigator::previousPageIndex(selectedIndex_, listSize, pageItems); + const auto move = [this, listSize](const int next) { + selectedIndex_ = next; + topIndex_ = followListSelection(selectedIndex_, topIndex_, visibleRows_, listSize); requestUpdate(); - }); + }; + buttonNavigator_.onNextRelease( + [this, listSize, &move] { move(ButtonNavigator::nextIndex(selectedIndex_, listSize)); }); + buttonNavigator_.onPreviousRelease( + [this, listSize, &move] { move(ButtonNavigator::previousIndex(selectedIndex_, listSize)); }); + buttonNavigator_.onNextContinuous( + [this, listSize, &move] { move(ButtonNavigator::nextPageIndex(selectedIndex_, listSize, visibleRows_)); }); + buttonNavigator_.onPreviousContinuous( + [this, listSize, &move] { move(ButtonNavigator::previousPageIndex(selectedIndex_, listSize, visibleRows_)); }); } void FontSelectionActivity::handleSelection() { @@ -188,13 +217,14 @@ void FontSelectionActivity::handleSelection() { if (font.settingIndex < CrossPointSettings::BUILTIN_FONT_COUNT) { SETTINGS.fontFamily = font.settingIndex; SETTINGS.sdFontFamilyName[0] = '\0'; - SETTINGS.fontSize = closestBuiltinStoredSize(targetPointSize); + SETTINGS.readerFontPointSize = CrossPointSettings::getReaderFontPointSize( + static_cast(closestBuiltinStoredSize(targetPointSize))); } else if (registry_) { const int sdIdx = font.settingIndex - CrossPointSettings::BUILTIN_FONT_COUNT; const auto& families = registry_->getFamilies(); if (sdIdx < static_cast(families.size())) { const std::vector sizes = families[sdIdx].availableSizes(); - SETTINGS.fontSize = closestSizeIndex(sizes, targetPointSize); + SETTINGS.readerFontPointSize = sizes[closestSizeIndex(sizes, targetPointSize)]; strncpy(SETTINGS.sdFontFamilyName, families[sdIdx].name.c_str(), sizeof(SETTINGS.sdFontFamilyName) - 1); SETTINGS.sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName) - 1] = '\0'; } @@ -244,18 +274,60 @@ void FontSelectionActivity::renderPreviewPane(int top, int height, int fontId, c } } +void FontSelectionActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} + +void FontSelectionActivity::buildListScreen(UiApp::ScreenType& screen) { + const int listTop = afterHeader + previewHeight + metrics_.verticalSpacing; + const int listHeight = usableHeight - previewHeight - metrics_.verticalSpacing; + screen.setContentMargin(fui::Insets{static_cast(listTop), 0, + static_cast(renderer.getScreenHeight() - listTop - listHeight), 0}); + const int currentFontIndex = findCurrentFontIndex(registry_, originalSdFontFamilyName_, originalFontFamily_); + std::vector items; + items.reserve(fonts_.size()); + for (size_t i = 0; i < fonts_.size(); ++i) { + fui::ListItem item; + item.label = fonts_[i].name.c_str(); + if (static_cast(i) == previewFontIndex_ && static_cast(i) != currentFontIndex) { + item.value = tr(STR_PREVIEW); + } else if (static_cast(i) == currentFontIndex) { + item.value = tr(STR_SELECTED); + } + item.actionValue = static_cast(i); + items.push_back(item); + } + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex_); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.valueInset = 8; + props.labelText = screen.theme().bodyText; + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows_ = rows > 0 ? rows : 1; + const int fontCount = static_cast(fonts_.size()); + topIndex_ = initialViewportPending_ ? followListSelection(selectedIndex_, 0, visibleRows_, fontCount) + : scrollListBy(topIndex_, 0, visibleRows_, fontCount); + initialViewportPending_ = false; + props.topIndex = static_cast(topIndex_); + screen.list(props); +} + void FontSelectionActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - - GUI.drawHeader(renderer, Rect{0, metrics_.topPadding, pageWidth, metrics_.headerHeight}, tr(STR_FONT_FAMILY)); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget_, header, tr(STR_FONT_FAMILY), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_FONT_FAMILY)); + } const int previewTop = afterHeader; const int listTop = previewTop + previewHeight + metrics_.verticalSpacing; - const int listHeight = usableHeight - previewHeight - metrics_.verticalSpacing; - const int previewFontId = SETTINGS.getReaderFontId(); const char* previewFontName = (previewFontIndex_ >= 0 && previewFontIndex_ < static_cast(fonts_.size())) ? fonts_[previewFontIndex_].name.c_str() @@ -264,16 +336,9 @@ void FontSelectionActivity::render(RenderLock&&) { renderer.drawLine(0, listTop - metrics_.verticalSpacing / 2, pageWidth, listTop - metrics_.verticalSpacing / 2); - const int currentFontIndex = findCurrentFontIndex(registry_, originalSdFontFamilyName_, originalFontFamily_); - GUI.drawList( - renderer, Rect{0, listTop, pageWidth, listHeight}, static_cast(fonts_.size()), selectedIndex_, - [this](int index) { return fonts_[index].name; }, nullptr, nullptr, - [this, currentFontIndex](int index) -> std::string { - if (index == previewFontIndex_ && index != currentFontIndex) return tr(STR_PREVIEW); - if (index == currentFontIndex) return tr(STR_SELECTED); - return ""; - }, - true); + uiReady_ = false; + app_.render(); + uiReady_ = true; const bool onPreviewed = selectedIndex_ == previewFontIndex_; const char* confirmLabel = onPreviewed ? tr(STR_SELECT) : tr(STR_PREVIEW); diff --git a/src/activities/settings/FontSelectionActivity.h b/src/activities/settings/FontSelectionActivity.h index d3bfbbd2dd..d9a5052361 100644 --- a/src/activities/settings/FontSelectionActivity.h +++ b/src/activities/settings/FontSelectionActivity.h @@ -1,7 +1,10 @@ #pragma once +#include +#include #include +#include #include #include #include @@ -22,6 +25,7 @@ class FontSelectionActivity final : public Activity { private: void handleSelection(); + void activateSelected(); int getFontIdForPreview(int index) const; void renderPreviewPane(int top, int height, int fontId, const char* fontName) const; @@ -44,4 +48,15 @@ class FontSelectionActivity final : public Activity { int bottomReserved = 0; int usableHeight = 0; int previewHeight = 0; + using UiApp = freeink::ui::FreeInkApp<20, 4>; + freeink::ui::GfxRendererTarget uiTarget_; + UiApp app_; + std::atomic uiReady_{false}; + int visibleRows_ = 1; + int topIndex_ = 0; + bool initialViewportPending_ = true; + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); }; diff --git a/src/activities/settings/KOReaderAuthActivity.cpp b/src/activities/settings/KOReaderAuthActivity.cpp index 2f724a2d19..5cc513e70c 100644 --- a/src/activities/settings/KOReaderAuthActivity.cpp +++ b/src/activities/settings/KOReaderAuthActivity.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "KOReaderCredentialStore.h" @@ -10,8 +11,10 @@ #include "SdCardFontSystem.h" #include "SilentRestart.h" #include "activities/network/WifiSelectionActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" +#include "network/WifiUtils.h" void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) { if (!success) { @@ -29,24 +32,28 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) { { RenderLock lock(*this); state = AUTHENTICATING; - statusMessage = tr(STR_AUTHENTICATING); + statusMessage = mode == Mode::SIGN_UP ? tr(STR_CREATING_ACCOUNT) : tr(STR_AUTHENTICATING); + } + if (requestUpdateAndWait() != RequestUpdateResult::Rendered) { + LOG_ERR("KOSync", "Authentication screen could not be rendered before request"); + requestUpdate(true); } - requestUpdate(); performAuthentication(); } void KOReaderAuthActivity::performAuthentication() { - const auto result = KOReaderSyncClient::authenticate(); + const auto result = mode == Mode::SIGN_UP ? KOReaderSyncClient::createUser() : KOReaderSyncClient::authenticate(); { RenderLock lock(*this); if (result == KOReaderSyncClient::OK) { state = SUCCESS; - statusMessage = tr(STR_AUTH_SUCCESS); + statusMessage = mode == Mode::SIGN_UP ? tr(STR_ACCOUNT_CREATED) : tr(STR_AUTH_SUCCESS); } else { state = FAILED; - errorMessage = KOReaderSyncClient::errorString(result); + errorMessage = + result == KOReaderSyncClient::USER_EXISTS ? tr(STR_USERNAME_TAKEN) : KOReaderSyncClient::errorString(result); } } requestUpdate(); @@ -57,7 +64,7 @@ void KOReaderAuthActivity::onEnter() { sdFontSystem.releaseLoadedFont(renderer); // Check if already connected - if (WiFi.status() == WL_CONNECTED) { + if (hasActiveStationWifiConnection()) { onWifiSelectionComplete(true); return; } @@ -73,8 +80,10 @@ void KOReaderAuthActivity::onExit() { if (WiFi.getMode() != WIFI_MODE_NULL) { WiFi.disconnect(false); delay(30); - silentRestart(); } + // Authentication launches from minimal network boot, so restore the full + // app state even if setup failed before WiFi was started. + silentRestart(); } void KOReaderAuthActivity::render(RenderLock&&) { @@ -83,34 +92,54 @@ void KOReaderAuthActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); - - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_AUTH)); + const Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + + const Rect header{0, metrics.topPadding, pageWidth, TouchHeaderBackButton::height(metrics, mappedInput)}; + const char* title = mode == Mode::SIGN_UP ? tr(STR_SIGN_UP) : tr(STR_KOREADER_AUTH); + if ((state == SUCCESS || state == FAILED) && mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, title, false); + } else { + GUI.drawHeader(renderer, header, title); + } const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height) / 2; if (state == AUTHENTICATING) { renderer.drawCenteredText(UI_10_FONT_ID, top, statusMessage.c_str()); } else if (state == SUCCESS) { - renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, top, + mode == Mode::SIGN_UP ? tr(STR_ACCOUNT_CREATED) : tr(STR_AUTH_SUCCESS), true, + EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY)); } else if (state == FAILED) { - renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str()); + renderer.drawCenteredText(UI_10_FONT_ID, top, mode == Mode::SIGN_UP ? tr(STR_SIGNUP_FAILED) : tr(STR_AUTH_FAILED), + true, EpdFontFamily::BOLD); + const int messageWidth = screen.width - metrics.contentSidePadding * 2; + const auto errorLines = renderer.wrappedText(UI_10_FONT_ID, errorMessage.c_str(), messageWidth, 3); + int messageY = top + height + 10; + for (const auto& line : errorLines) { + UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, messageY, line.c_str()); + messageY += height + 4; + } } const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); } void KOReaderAuthActivity::loop() { if (state == SUCCESS || state == FAILED) { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (TouchHeaderBackButton::wasTapped(mappedInput, header) || + mappedInput.wasPressed(MappedInputManager::Button::Back)) { finishAfterBackPress(); return; } - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + int x = 0; + int y = 0; + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || mappedInput.wasScreenTapped(x, y)) { finish(); } } diff --git a/src/activities/settings/KOReaderAuthActivity.h b/src/activities/settings/KOReaderAuthActivity.h index b4e330e794..f1a8372c05 100644 --- a/src/activities/settings/KOReaderAuthActivity.h +++ b/src/activities/settings/KOReaderAuthActivity.h @@ -3,15 +3,19 @@ #include #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" /** - * Activity for testing KOReader credentials. - * Connects to WiFi and authenticates with the KOReader sync server. + * Activity for testing KOReader credentials, or — in sign-up mode — creating a + * new account on the sync server with the entered username/password. + * Connects to WiFi, then authenticates or registers. */ class KOReaderAuthActivity final : public Activity { public: - explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("KOReaderAuth", renderer, mappedInput) {} + enum class Mode { AUTHENTICATE, SIGN_UP }; + + explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, Mode mode = Mode::AUTHENTICATE) + : Activity("KOReaderAuth", renderer, mappedInput), mode(mode) {} void onEnter() override; void onExit() override; @@ -22,7 +26,9 @@ class KOReaderAuthActivity final : public Activity { private: enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, SUCCESS, FAILED }; + Mode mode = Mode::AUTHENTICATE; State state = WIFI_SELECTION; + ScreenTransitionRefresh screenTransitionRefresh; std::string statusMessage; std::string errorMessage; diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index 67cf042be3..a8df433635 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -5,47 +5,94 @@ #include -#include "KOReaderAuthActivity.h" #include "KOReaderCredentialStore.h" #include "MappedInputManager.h" +#include "SilentRestart.h" #include "activities/util/KeyboardEntryActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" +namespace fui = freeink::ui; + namespace { -constexpr int MENU_ITEMS = 5; -const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL, - StrId::STR_DOCUMENT_MATCHING, StrId::STR_AUTHENTICATE}; +constexpr int MENU_ITEMS = 8; +const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL, + StrId::STR_DOCUMENT_MATCHING, StrId::STR_SEND_METADATA, StrId::STR_SYNC_BEHAVIOR, + StrId::STR_SIGN_UP, StrId::STR_AUTHENTICATE}; +constexpr fui::ActionId ACTION_ROW = 1; } // namespace +KOReaderSettingsActivity::KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("KOReaderSettings", renderer, mappedInput), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + +void KOReaderSettingsActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= MENU_ITEMS) return; + self->selectedIndex = static_cast(event.value); + // Activation opens a keyboard/sub-activity or repaints a new value; a + // lingering flash would gray an unrelated row. + self->app.clearTapFlash(); + self->handleSelection(); +} + void KOReaderSettingsActivity::onEnter() { Activity::onEnter(); selectedIndex = 0; + uiReady = false; + visibleRows = 1; + topIndex = 0; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &KOReaderSettingsActivity::onRowEvent, this); + app.setScreen(&KOReaderSettingsActivity::listScreen, this); requestUpdate(); } void KOReaderSettingsActivity::onExit() { Activity::onExit(); } void KOReaderSettingsActivity::loop() { + auto activateSelected = [this] { handleSelection(); }; + + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + finishAfterBackPress(); + return; + } if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { finishAfterBackPress(); return; } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - handleSelection(); + activateSelected(); return; } + // Touch goes through the FreeInkApp: render() registered the row hit rects; + // route the snapshot and let onRowEvent dispatch. + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; // dispatched to onRowEvent + } + } + // Handle navigation buttonNavigator.onNext([this] { selectedIndex = (selectedIndex + 1) % MENU_ITEMS; + topIndex = followListSelection(static_cast(selectedIndex), topIndex, visibleRows, MENU_ITEMS); requestUpdate(); }); buttonNavigator.onPrevious([this] { selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; + topIndex = followListSelection(static_cast(selectedIndex), topIndex, visibleRows, MENU_ITEMS); requestUpdate(); }); } @@ -98,51 +145,120 @@ void KOReaderSettingsActivity::handleSelection() { KOREADER_STORE.saveToFile(); requestUpdate(); } else if (selectedIndex == 4) { + // Send Metadata - toggle on/off + KOREADER_STORE.setSendMetadata(!KOREADER_STORE.getSendMetadata()); + KOREADER_STORE.saveToFile(); + requestUpdate(); + } else if (selectedIndex == 5) { + // Sync behavior - toggle between Ask and Smart + const auto current = KOREADER_STORE.getSyncBehavior(); + const auto newBehavior = (current == KOReaderSyncBehavior::ASK_EVERY_TIME) ? KOReaderSyncBehavior::SMART + : KOReaderSyncBehavior::ASK_EVERY_TIME; + KOREADER_STORE.setSyncBehavior(newBehavior); + KOREADER_STORE.saveToFile(); + requestUpdate(); + } else if (selectedIndex == 6) { + // Sign Up - create a new account on the sync server with the entered credentials + if (!KOREADER_STORE.hasCredentials()) { + return; + } + silentRestartToNetwork(NetworkBootTarget::KOREADER_AUTH, 1); + } else if (selectedIndex == 7) { // Authenticate if (!KOREADER_STORE.hasCredentials()) { // Can't authenticate without credentials - just show message briefly return; } - startActivityForResult(std::make_unique(renderer, mappedInput), [](const ActivityResult&) {}); + silentRestartToNetwork(NetworkBootTarget::KOREADER_AUTH); } } +void KOReaderSettingsActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} + +void KOReaderSettingsActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + // Content below the GUI.drawHeader band, above the button hints. + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + screen.spacer(static_cast(metrics.verticalSpacing)); + + // Per-render owned value strings; items point into them for the draw only. + std::vector values(MENU_ITEMS); + for (int i = 0; i < MENU_ITEMS; i++) { + if (i == 0) { + const auto username = KOREADER_STORE.getUsername(); + values[i] = username.empty() ? tr(STR_NOT_SET) : username; + } else if (i == 1) { + values[i] = KOREADER_STORE.getPassword().empty() ? tr(STR_NOT_SET) : "******"; + } else if (i == 2) { + values[i] = KOREADER_STORE.getServerUrl(); + if (values[i].empty()) { + // Show which server the default actually is, scheme stripped for space + std::string defaultUrl = KOREADER_STORE.getBaseUrl(); + const auto schemeEnd = defaultUrl.find("://"); + if (schemeEnd != std::string::npos) { + defaultUrl.erase(0, schemeEnd + 3); + } + values[i] = std::string(tr(STR_DEFAULT_VALUE)) + ": " + defaultUrl; + } + } else if (i == 3) { + values[i] = KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? tr(STR_FILENAME) : tr(STR_BINARY); + } else if (i == 4) { + values[i] = KOREADER_STORE.getSendMetadata() ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); + } else if (i == 5) { + values[i] = + KOREADER_STORE.getSyncBehavior() == KOReaderSyncBehavior::SMART ? tr(STR_SMART_SYNC) : tr(STR_ASK_EVERY_TIME); + } else { + values[i] = KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; + } + } + + std::vector items; + items.reserve(MENU_ITEMS); + for (int i = 0; i < MENU_ITEMS; i++) { + fui::ListItem item; + item.label = I18N.get(menuNames[i]); + if (!values[i].empty()) item.value = values[i].c_str(); + item.actionValue = static_cast(i); + items.push_back(item); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; // physical buttons stay in loop() + props.valueInset = 8; // air between the value and the row edge + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, MENU_ITEMS); // clamp to range + props.topIndex = static_cast(topIndex); + screen.list(props); +} + void KOReaderSettingsActivity::render(RenderLock&&) { renderer.clearScreen(); const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_SYNC)); - - const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; - GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(MENU_ITEMS), - static_cast(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, - nullptr, - [this](int index) { - // Draw status for each setting - if (index == 0) { - auto username = KOREADER_STORE.getUsername(); - return username.empty() ? std::string(tr(STR_NOT_SET)) : username; - } else if (index == 1) { - return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******"); - } else if (index == 2) { - auto serverUrl = KOREADER_STORE.getServerUrl(); - return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl; - } else if (index == 3) { - return KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? std::string(tr(STR_FILENAME)) - : std::string(tr(STR_BINARY)); - } else if (index == 4) { - return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; - } - return std::string(tr(STR_NOT_SET)); - }, - true); - // Draw help text at bottom + // Header via GUI.drawHeader (already FreeInkUI-themed) for the battery + // indicator; the rest of the screen renders through the app. + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_KOREADER_SYNC), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_KOREADER_SYNC)); + } + + uiReady = false; + app.render(); + uiReady = true; + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/settings/KOReaderSettingsActivity.h b/src/activities/settings/KOReaderSettingsActivity.h index f32db8a587..5dd363bae9 100644 --- a/src/activities/settings/KOReaderSettingsActivity.h +++ b/src/activities/settings/KOReaderSettingsActivity.h @@ -1,5 +1,10 @@ #pragma once +#include +#include + +#include + #include "activities/Activity.h" #include "util/ButtonNavigator.h" @@ -9,8 +14,7 @@ */ class KOReaderSettingsActivity final : public Activity { public: - explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("KOReaderSettings", renderer, mappedInput) {} + explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput); void onEnter() override; void onExit() override; @@ -18,9 +22,25 @@ class KOReaderSettingsActivity final : public Activity { void render(RenderLock&&) override; private: + // FreeInkApp hosts the settings list (themed rows, touch routing); the + // header stays on GUI.drawHeader for the battery indicator. + using UiApp = freeink::ui::FreeInkApp<12, 4>; + ButtonNavigator buttonNavigator; size_t selectedIndex = 0; + freeink::ui::GfxRendererTarget uiTarget; // must precede `app`: the app holds a reference to it + UiApp app; + // render() rebuilds the app's interaction table; loop() only routes touch + // snapshots against it while this is true (the two run on different tasks). + std::atomic uiReady{false}; + int visibleRows = 1; // rows per page at the current scale; set by the screen builder + int topIndex = 0; // viewport scroll position, decoupled from the selection + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); + void handleSelection(); }; diff --git a/src/activities/settings/LanguageSelectActivity.cpp b/src/activities/settings/LanguageSelectActivity.cpp index b60459b32d..af57e903e1 100644 --- a/src/activities/settings/LanguageSelectActivity.cpp +++ b/src/activities/settings/LanguageSelectActivity.cpp @@ -9,96 +9,151 @@ #include "CrossPointSettings.h" #include "I18nKeys.h" #include "MappedInputManager.h" -#include "fontIds.h" +#include "components/TouchHeaderBackButton.h" +#include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" + +namespace fui = freeink::ui; +namespace { +constexpr fui::ActionId ACTION_ROW = 1; +} + +LanguageSelectActivity::LanguageSelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("LanguageSelect", renderer, mappedInput), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} void LanguageSelectActivity::onEnter() { Activity::onEnter(); - - // Set current selection based on current language const auto currentLang = static_cast(I18N.getLanguage()); - const auto* begin = std::begin(SORTED_LANGUAGE_INDICES); - const auto* end = std::end(SORTED_LANGUAGE_INDICES); - const auto* it = std::find(begin, end, currentLang); - selectedIndex = (it != end) ? std::distance(begin, it) : 0; - + const auto* it = std::find(std::begin(SORTED_LANGUAGE_INDICES), std::end(SORTED_LANGUAGE_INDICES), currentLang); + selectedIndex = it != std::end(SORTED_LANGUAGE_INDICES) ? std::distance(std::begin(SORTED_LANGUAGE_INDICES), it) : 0; + topIndex = 0; + visibleRows = 1; + initialViewportPending = true; + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &LanguageSelectActivity::onRowEvent, this); + app.setScreen(&LanguageSelectActivity::languageScreen, this); requestUpdate(); } void LanguageSelectActivity::onExit() { Activity::onExit(); } +void LanguageSelectActivity::handleSelection() { + const uint8_t langIndex = SORTED_LANGUAGE_INDICES[selectedIndex]; + { + RenderLock lock(*this); + I18N.setLanguage(static_cast(langIndex)); + } + SETTINGS.language = langIndex; + SETTINGS.saveToFile(); + onBack(); +} + +void LanguageSelectActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= totalItems) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->handleSelection(); +} + void LanguageSelectActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + onBack(); + return; + } if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { onBack(); return; } - + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { handleSelection(); return; } - - const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false); - - // Handle navigation - buttonNavigator.onNextRelease([this] { - selectedIndex = ButtonNavigator::nextIndex(static_cast(selectedIndex), totalItems); - requestUpdate(); - }); - - buttonNavigator.onPreviousRelease([this] { - selectedIndex = ButtonNavigator::previousIndex(static_cast(selectedIndex), totalItems); - requestUpdate(); - }); - - buttonNavigator.onNextContinuous([this, pageItems] { - selectedIndex = ButtonNavigator::nextPageIndex(static_cast(selectedIndex), totalItems, pageItems); - requestUpdate(); - }); - - buttonNavigator.onPreviousContinuous([this, pageItems] { - selectedIndex = ButtonNavigator::previousPageIndex(static_cast(selectedIndex), totalItems, pageItems); + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int next = scrollListBy(topIndex, swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows, + visibleRows, totalItems); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + const auto move = [this](const int index) { + selectedIndex = index; + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, totalItems); requestUpdate(); - }); + }; + buttonNavigator.onNextRelease([this, &move] { move(ButtonNavigator::nextIndex(selectedIndex, totalItems)); }); + buttonNavigator.onPreviousRelease([this, &move] { move(ButtonNavigator::previousIndex(selectedIndex, totalItems)); }); + buttonNavigator.onNextContinuous( + [this, &move] { move(ButtonNavigator::nextPageIndex(selectedIndex, totalItems, visibleRows)); }); + buttonNavigator.onPreviousContinuous( + [this, &move] { move(ButtonNavigator::previousPageIndex(selectedIndex, totalItems, visibleRows)); }); } -void LanguageSelectActivity::handleSelection() { - const uint8_t langIndex = SORTED_LANGUAGE_INDICES[selectedIndex]; +void LanguageSelectActivity::languageScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildLanguageScreen(screen); +} - { - RenderLock lock(*this); - I18N.setLanguage(static_cast(langIndex)); +void LanguageSelectActivity::buildLanguageScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + screen.spacer(static_cast(metrics.verticalSpacing)); + std::vector items; + items.reserve(totalItems); + const auto currentLang = static_cast(I18N.getLanguage()); + for (int i = 0; i < totalItems; ++i) { + fui::ListItem item; + item.label = I18N.getLanguageName(static_cast(SORTED_LANGUAGE_INDICES[i])); + item.value = SORTED_LANGUAGE_INDICES[i] == currentLang ? tr(STR_SELECTED) : nullptr; + item.actionValue = static_cast(i); + items.push_back(item); } - - SETTINGS.language = langIndex; - SETTINGS.saveToFile(); - - // Return to previous page - onBack(); + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.valueInset = 8; + props.labelText = screen.theme().bodyText; + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = initialViewportPending ? followListSelection(selectedIndex, 0, visibleRows, totalItems) + : scrollListBy(topIndex, 0, visibleRows, totalItems); + initialViewportPending = false; + props.topIndex = static_cast(topIndex); + screen.list(props); } void LanguageSelectActivity::render(RenderLock&&) { renderer.clearScreen(); - - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - auto metrics = UITheme::getInstance().getMetrics(); - - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_LANGUAGE)); - - // Current language marker - const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; - const auto currentLang = static_cast(I18N.getLanguage()); - GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, totalItems, selectedIndex, - [this](int index) { return I18N.getLanguageName(static_cast(SORTED_LANGUAGE_INDICES[index])); }, - nullptr, nullptr, - [this, currentLang](int index) { return SORTED_LANGUAGE_INDICES[index] == currentLang ? tr(STR_SELECTED) : ""; }, - true); - - // Button hints + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_LANGUAGE), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_LANGUAGE)); + } + uiReady = false; + app.render(); + uiReady = true; const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); } diff --git a/src/activities/settings/LanguageSelectActivity.h b/src/activities/settings/LanguageSelectActivity.h index aa2a68bbc2..78e252c48b 100644 --- a/src/activities/settings/LanguageSelectActivity.h +++ b/src/activities/settings/LanguageSelectActivity.h @@ -1,8 +1,11 @@ #pragma once +#include +#include #include #include +#include #include #include "activities/Activity.h" @@ -15,9 +18,10 @@ class MappedInputManager; * Activity for selecting UI language */ class LanguageSelectActivity final : public Activity { + using UiApp = freeink::ui::FreeInkApp<32, 4>; + public: - explicit LanguageSelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("LanguageSelect", renderer, mappedInput) {} + LanguageSelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput); void onEnter() override; void onExit() override; @@ -30,5 +34,15 @@ class LanguageSelectActivity final : public Activity { void onBack() { finish(); } ButtonNavigator buttonNavigator; int selectedIndex = 0; + freeink::ui::GfxRendererTarget uiTarget; + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + bool initialViewportPending = true; constexpr static uint8_t totalItems = getLanguageCount(); + + static void languageScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildLanguageScreen(UiApp::ScreenType& screen); }; diff --git a/src/activities/settings/OpdsServerListActivity.cpp b/src/activities/settings/OpdsServerListActivity.cpp index cdcff11701..de0aec4148 100644 --- a/src/activities/settings/OpdsServerListActivity.cpp +++ b/src/activities/settings/OpdsServerListActivity.cpp @@ -2,37 +2,88 @@ #include #include +#include +#include + +#include "CrossPointSettings.h" #include "MappedInputManager.h" #include "OpdsServerStore.h" #include "OpdsSettingsActivity.h" #include "activities/ActivityManager.h" -#include "activities/browser/OpdsBookBrowserActivity.h" +#include "activities/util/KeyboardEntryActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" +namespace fui = freeink::ui; + +namespace { +constexpr fui::ActionId ACTION_ROW = 1; + +std::string normalizeDownloadFolder(std::string folder) { + while (!folder.empty() && (folder.front() == ' ' || folder.front() == '\t')) folder.erase(folder.begin()); + while (!folder.empty() && (folder.back() == ' ' || folder.back() == '\t')) folder.pop_back(); + if (folder.empty() || folder == "/") return ""; + if (folder.front() != '/') folder.insert(folder.begin(), '/'); + while (folder.size() > 1 && folder.back() == '/') folder.pop_back(); + return folder; +} +} // namespace + int OpdsServerListActivity::getItemCount() const { int count = static_cast(OPDS_STORE.getCount()); - // In settings mode, append a virtual "Add Server" item; in picker mode, only show real servers + // In settings mode, append virtual "Add Server" and "Download Folder" items. if (!pickerMode) { - count++; + count += 2; } return count; } +OpdsServerListActivity::OpdsServerListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const bool pickerMode) + : Activity("OpdsServerList", renderer, mappedInput), + pickerMode(pickerMode), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + +void OpdsServerListActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(self->getItemCount())) return; + self->selectedIndex = event.value; + // Activation opens an editor/browser or repaints a new value; a lingering + // flash would gray an unrelated row. + self->app.clearTapFlash(); + self->handleSelection(); + self->requestUpdate(); +} + void OpdsServerListActivity::onEnter() { Activity::onEnter(); // Reload from disk in case servers were added/removed by a subactivity or the web UI OPDS_STORE.loadFromFile(); selectedIndex = 0; + uiReady = false; + visibleRows = 1; + topIndex = 0; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &OpdsServerListActivity::onRowEvent, this); + app.setScreen(&OpdsServerListActivity::listScreen, this); requestUpdate(); } void OpdsServerListActivity::onExit() { Activity::onExit(); } void OpdsServerListActivity::loop() { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + if (optionPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return; + + auto activateSelected = [this] { handleSelection(); }; + + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer) || + mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (pickerMode) { activityManager.goHome(HomeMenuItem::OPDS_BROWSER); } else { @@ -42,21 +93,44 @@ void OpdsServerListActivity::loop() { } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - handleSelection(); + activateSelected(); return; } + // Touch goes through the FreeInkApp: render() registered the row hit rects; + // route the snapshot and let onRowEvent dispatch. + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; // dispatched to onRowEvent + } + } + const int itemCount = getItemCount(); if (itemCount > 0) { - buttonNavigator.onNext([this, itemCount] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, itemCount); - requestUpdate(); - }); - - buttonNavigator.onPrevious([this, itemCount] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, itemCount); + // Swipes scroll the viewport; the selection stays put and button + // navigation pulls the view back to it. + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, itemCount); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + const auto moveSelection = [this, itemCount](const int index) { + selectedIndex = index; + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, itemCount); requestUpdate(); - }); + }; + buttonNavigator.onNext( + [this, itemCount, &moveSelection] { moveSelection(ButtonNavigator::nextIndex(selectedIndex, itemCount)); }); + buttonNavigator.onPrevious( + [this, itemCount, &moveSelection] { moveSelection(ButtonNavigator::previousIndex(selectedIndex, itemCount)); }); } } @@ -66,14 +140,32 @@ void OpdsServerListActivity::handleSelection() { if (pickerMode) { // Picker mode: selecting a server navigates to the OPDS browser if (selectedIndex < serverCount) { - const auto* server = OPDS_STORE.getServer(static_cast(selectedIndex)); - if (server) { - activityManager.replaceActivity(std::make_unique(renderer, mappedInput, *server)); - } + activityManager.goToOpdsServer(static_cast(selectedIndex)); } return; } + // Item layout: configured servers, Add Server, Download Folder. + if (selectedIndex == serverCount + 1) { + auto resultHandler = [this](const ActivityResult& result) { + if (result.isCancelled) return; + + const auto& keyboardResult = std::get(result.data); + const std::string folder = normalizeDownloadFolder(keyboardResult.text); + strncpy(SETTINGS.opdsDownloadFolder, folder.c_str(), sizeof(SETTINGS.opdsDownloadFolder) - 1); + SETTINGS.opdsDownloadFolder[sizeof(SETTINGS.opdsDownloadFolder) - 1] = '\0'; + if (!SETTINGS.saveToFile()) { + LOG_ERR("OPDS", "Could not save download folder setting"); + } + requestUpdate(); + }; + startActivityForResult(std::make_unique( + renderer, mappedInput, tr(STR_OPDS_DOWNLOAD_FOLDER), SETTINGS.opdsDownloadFolder, + sizeof(SETTINGS.opdsDownloadFolder) - 1, InputType::Text), + resultHandler); + return; + } + // Settings mode: open editor for selected server, or create a new one auto resultHandler = [this](const ActivityResult&) { // Reload server list when returning from editor @@ -88,44 +180,83 @@ void OpdsServerListActivity::handleSelection() { } } +void OpdsServerListActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} + +void OpdsServerListActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + // Content below the GUI.drawHeader band, above the button hints. + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + screen.spacer(static_cast(metrics.verticalSpacing)); + + const int itemCount = getItemCount(); + if (itemCount == 0) { + screen.centeredText(tr(STR_NO_SERVERS), screen.theme().bodyText); + return; + } + + const auto& servers = OPDS_STORE.getServers(); + const auto serverCount = static_cast(servers.size()); + + // Primary label: server name (falling back to URL if unnamed); subtitle is + // the URL when a name is set, or the current folder/format values. + std::vector items; + items.reserve(itemCount); + for (int i = 0; i < serverCount; i++) { + fui::ListItem item; + item.label = servers[i].name.empty() ? servers[i].url.c_str() : servers[i].name.c_str(); + if (!servers[i].name.empty()) item.subtitle = servers[i].url.c_str(); + item.actionValue = static_cast(i); + items.push_back(item); + } + if (!pickerMode) { + fui::ListItem addServer; + addServer.label = tr(STR_ADD_SERVER); + addServer.actionValue = static_cast(serverCount); + items.push_back(addServer); + + fui::ListItem folder; + folder.label = tr(STR_OPDS_DOWNLOAD_FOLDER); + folder.subtitle = SETTINGS.opdsDownloadFolder[0] ? SETTINGS.opdsDownloadFolder : tr(STR_OPDS_SD_ROOT); + folder.actionValue = static_cast(serverCount + 1); + items.push_back(folder); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; // physical buttons stay in loop() + const auto rows = configureUiList(props, screen.theme(), screen.body(), UiListRowType::WithSubtitle); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, itemCount); // clamp to range + props.topIndex = static_cast(topIndex); + screen.list(props); +} + void OpdsServerListActivity::render(RenderLock&&) { + if (optionPopup.processRender(renderer, mappedInput)) return; + renderer.clearScreen(); const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_OPDS_SERVERS)); - - const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; - const int itemCount = getItemCount(); - - if (itemCount == 0) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_NO_SERVERS)); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_OPDS_SERVERS), false); } else { - const auto& servers = OPDS_STORE.getServers(); - const auto serverCount = static_cast(servers.size()); - - // Primary label: server name (falling back to URL if unnamed). - // Secondary label: server URL (shown as subtitle when name is set). - GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, itemCount, selectedIndex, - [&servers, serverCount](int index) { - if (index < serverCount) { - const auto& server = servers[index]; - return server.name.empty() ? server.url : server.name; - } - return std::string(I18n::getInstance().get(StrId::STR_ADD_SERVER)); - }, - [&servers, serverCount](int index) { - if (index < serverCount && !servers[index].name.empty()) { - return servers[index].url; - } - return std::string(""); - }); + GUI.drawHeader(renderer, header, tr(STR_OPDS_SERVERS)); } + uiReady = false; + app.render(); + uiReady = true; + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/settings/OpdsServerListActivity.h b/src/activities/settings/OpdsServerListActivity.h index b03c3a92e5..2249e58373 100644 --- a/src/activities/settings/OpdsServerListActivity.h +++ b/src/activities/settings/OpdsServerListActivity.h @@ -1,6 +1,12 @@ #pragma once +#include +#include + +#include + #include "activities/Activity.h" +#include "components/OptionPopup.h" #include "util/ButtonNavigator.h" /** @@ -11,8 +17,7 @@ */ class OpdsServerListActivity final : public Activity { public: - explicit OpdsServerListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool pickerMode = false) - : Activity("OpdsServerList", renderer, mappedInput), pickerMode(pickerMode) {} + explicit OpdsServerListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool pickerMode = false); void onEnter() override; void onExit() override; @@ -20,9 +25,26 @@ class OpdsServerListActivity final : public Activity { void render(RenderLock&&) override; private: + // FreeInkApp hosts the server list (themed rows, touch routing); the header + // stays on GUI.drawHeader for the battery indicator. + using UiApp = freeink::ui::FreeInkApp<20, 4>; + ButtonNavigator buttonNavigator; int selectedIndex = 0; bool pickerMode = false; + OptionPopup optionPopup; + + freeink::ui::GfxRendererTarget uiTarget; // must precede `app`: the app holds a reference to it + UiApp app; + // render() rebuilds the app's interaction table; loop() only routes touch + // snapshots against it while this is true (the two run on different tasks). + std::atomic uiReady{false}; + int visibleRows = 1; // rows per page at the current scale; set by the screen builder + int topIndex = 0; // viewport scroll position, decoupled from the selection + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); int getItemCount() const; void handleSelection(); diff --git a/src/activities/settings/OpdsSettingsActivity.cpp b/src/activities/settings/OpdsSettingsActivity.cpp index 0af7896165..f5c76717e8 100644 --- a/src/activities/settings/OpdsSettingsActivity.cpp +++ b/src/activities/settings/OpdsSettingsActivity.cpp @@ -9,15 +9,38 @@ #include "MappedInputManager.h" #include "OpdsServerStore.h" #include "activities/util/KeyboardEntryActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" +namespace fui = freeink::ui; + namespace { // Editable fields: Name, URL, Username, Password, Filename. // Existing servers also show a Delete option (BASE_ITEMS + 1). constexpr int BASE_ITEMS = 5; +constexpr fui::ActionId ACTION_ROW = 1; } // namespace +OpdsSettingsActivity::OpdsSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const int serverIndex) + : Activity("OpdsSettings", renderer, mappedInput), + serverIndex(serverIndex), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + +void OpdsSettingsActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(self->getMenuItemCount())) return; + self->selectedIndex = static_cast(event.value); + // Activation opens a keyboard or leaves the screen; a lingering flash would + // gray an unrelated row. + self->app.clearTapFlash(); + self->handleSelection(); +} + int OpdsSettingsActivity::getMenuItemCount() const { return isNewServer ? BASE_ITEMS : BASE_ITEMS + 1; // +1 for Delete } @@ -42,12 +65,33 @@ void OpdsSettingsActivity::onEnter() { } } + uiReady = false; + visibleRows = 1; + topIndex = 0; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &OpdsSettingsActivity::onRowEvent, this); + app.setScreen(&OpdsSettingsActivity::listScreen, this); requestUpdate(); } void OpdsSettingsActivity::onExit() { Activity::onExit(); } void OpdsSettingsActivity::loop() { + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + finish(); + return; + } + // Touch goes through the FreeInkApp: render() registered the row hit rects; + // route the snapshot and let onRowEvent dispatch. + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; // dispatched to onRowEvent + } + } + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { finish(); return; @@ -174,51 +218,77 @@ void OpdsSettingsActivity::handleSelection() { } } +void OpdsSettingsActivity::listScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildListScreen(screen); +} + +void OpdsSettingsActivity::buildListScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + // Content below the GUI.drawHeader band, above the button hints. + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + + // URL hint where the old sub-header band sat. + const fui::Rect band = screen.takeTop(static_cast(metrics.tabBarHeight)); + const int16_t pad = screen.theme().headerSidePadding; + screen.target().text(band.inset(fui::Insets{0, pad, 0, pad}), tr(STR_CALIBRE_URL_HINT), screen.theme().smallText); + screen.spacer(static_cast(metrics.verticalSpacing)); + + const StrId fieldNames[] = {StrId::STR_SERVER_NAME, StrId::STR_OPDS_SERVER_URL, StrId::STR_USERNAME, + StrId::STR_PASSWORD, StrId::STR_FILENAME}; + const int menuItems = getMenuItemCount(); + const char* values[BASE_ITEMS] = { + editServer.name.empty() ? tr(STR_NOT_SET) : editServer.name.c_str(), + editServer.url.empty() ? tr(STR_NOT_SET) : editServer.url.c_str(), + editServer.username.empty() ? tr(STR_NOT_SET) : editServer.username.c_str(), + editServer.password.empty() ? tr(STR_NOT_SET) : "******", + editServer.filenameFormat == OpdsFilenameFormat::TITLE_AUTHOR ? tr(STR_TITLE_AUTHOR) : tr(STR_AUTHOR_TITLE), + }; + + std::vector items; + items.reserve(menuItems); + for (int i = 0; i < menuItems; i++) { + fui::ListItem item; + item.label = i < BASE_ITEMS ? I18N.get(fieldNames[i]) : tr(STR_DELETE_SERVER); + if (i < BASE_ITEMS) item.value = values[i]; + item.actionValue = static_cast(i); + items.push_back(item); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; // physical buttons stay in loop() + props.valueInset = 8; // air between the value and the row edge + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, menuItems); // clamp to range + props.topIndex = static_cast(topIndex); + screen.list(props); +} + void OpdsSettingsActivity::render(RenderLock&&) { renderer.clearScreen(); const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); // Reuse STR_OPDS_BROWSER as the "edit existing server" title. // New server creation uses STR_ADD_SERVER. const char* header = isNewServer ? tr(STR_ADD_SERVER) : tr(STR_OPDS_BROWSER); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, header); - GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, - tr(STR_CALIBRE_URL_HINT)); - - const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.tabBarHeight; - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; - const int menuItems = getMenuItemCount(); - - const StrId fieldNames[] = {StrId::STR_SERVER_NAME, StrId::STR_OPDS_SERVER_URL, StrId::STR_USERNAME, - StrId::STR_PASSWORD, StrId::STR_FILENAME}; - - GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, menuItems, static_cast(selectedIndex), - [this, &fieldNames](int index) { - if (index < BASE_ITEMS) { - return std::string(I18N.get(fieldNames[index])); - } - return std::string(tr(STR_DELETE_SERVER)); - }, - nullptr, nullptr, - [this](int index) { - if (index == 0) { - return editServer.name.empty() ? std::string(tr(STR_NOT_SET)) : editServer.name; - } else if (index == 1) { - return editServer.url.empty() ? std::string(tr(STR_NOT_SET)) : editServer.url; - } else if (index == 2) { - return editServer.username.empty() ? std::string(tr(STR_NOT_SET)) : editServer.username; - } else if (index == 3) { - return editServer.password.empty() ? std::string(tr(STR_NOT_SET)) : std::string("******"); - } else if (index == 4) { - return editServer.filenameFormat == OpdsFilenameFormat::TITLE_AUTHOR ? std::string(tr(STR_TITLE_AUTHOR)) - : std::string(tr(STR_AUTHOR_TITLE)); - } - return std::string(""); - }, - true); + // Header via GUI.drawHeader (already FreeInkUI-themed) for the battery + // indicator; the rest of the screen renders through the app. + const Rect headerRect = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, headerRect, header, false); + } else { + GUI.drawHeader(renderer, headerRect, header); + } + uiReady = false; + app.render(); + uiReady = true; const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/settings/OpdsSettingsActivity.h b/src/activities/settings/OpdsSettingsActivity.h index 8a117c2a67..1cf202d87c 100644 --- a/src/activities/settings/OpdsSettingsActivity.h +++ b/src/activities/settings/OpdsSettingsActivity.h @@ -1,5 +1,10 @@ #pragma once +#include +#include + +#include + #include "OpdsServerStore.h" #include "activities/Activity.h" #include "util/ButtonNavigator.h" @@ -14,8 +19,7 @@ class OpdsSettingsActivity final : public Activity { /** * @param serverIndex Index into OpdsServerStore, or -1 for a new server */ - explicit OpdsSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, int serverIndex = -1) - : Activity("OpdsSettings", renderer, mappedInput), serverIndex(serverIndex) {} + explicit OpdsSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, int serverIndex = -1); void onEnter() override; void onExit() override; @@ -23,6 +27,10 @@ class OpdsSettingsActivity final : public Activity { void render(RenderLock&&) override; private: + // FreeInkApp hosts the field list (themed rows, touch routing); the header + // stays on GUI.drawHeader for the battery indicator. + using UiApp = freeink::ui::FreeInkApp<12, 4>; + ButtonNavigator buttonNavigator; size_t selectedIndex = 0; @@ -31,6 +39,18 @@ class OpdsSettingsActivity final : public Activity { bool isNewServer = false; bool showSaveError = false; + freeink::ui::GfxRendererTarget uiTarget; // must precede `app`: the app holds a reference to it + UiApp app; + // render() rebuilds the app's interaction table; loop() only routes touch + // snapshots against it while this is true (the two run on different tasks). + std::atomic uiReady{false}; + int visibleRows = 1; // rows per page at the current scale; set by the screen builder + int topIndex = 0; // viewport scroll position, decoupled from the selection + + static void listScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildListScreen(UiApp::ScreenType& screen); + int getMenuItemCount() const; void handleSelection(); bool saveServer(); diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp index e67a9cf8c9..4b402ef21a 100644 --- a/src/activities/settings/OtaUpdateActivity.cpp +++ b/src/activities/settings/OtaUpdateActivity.cpp @@ -9,6 +9,7 @@ #include "SdCardFontSystem.h" #include "SilentRestart.h" #include "activities/network/WifiSelectionActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" #include "network/OtaUpdater.h" @@ -18,8 +19,24 @@ bool hasActiveWifiConnection() { return WiFi.status() == WL_CONNECTED && WiFi.lo StrId failureMessageFor(const OtaUpdater::OtaUpdaterError error) { if (error == OtaUpdater::HASH_MISMATCH_ERROR) return StrId::STR_UPDATE_HASH_MISMATCH; + if (error == OtaUpdater::WRONG_DEVICE_ERROR) return StrId::STR_FIRMWARE_WRONG_DEVICE; return StrId::STR_UPDATE_FAILED; } + +struct OtaActionRects { + Rect cancel; + Rect update; +}; + +OtaActionRects getOtaActionRects(const GfxRenderer& renderer) { + const int top = renderer.getScreenHeight() - 80; + const int width = renderer.getScreenWidth() / 2; + return {Rect{0, top, width, 80}, Rect{width, top, renderer.getScreenWidth() - width, 80}}; +} + +bool contains(const Rect& rect, const int x, const int y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} } // namespace void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { @@ -29,8 +46,6 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { return; } - LOG_DBG("OTA", "WiFi connected, checking for update"); - { RenderLock lock(*this); state = CHECKING_FOR_UPDATE; @@ -58,7 +73,6 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { } if (!updater.isUpdateNewer()) { - LOG_DBG("OTA", "No new update available"); { RenderLock lock(*this); state = NO_UPDATE; @@ -79,17 +93,14 @@ void OtaUpdateActivity::onEnter() { sdFontSystem.releaseLoadedFont(renderer); if (hasActiveWifiConnection()) { - LOG_DBG("OTA", "WiFi already connected, checking for update"); onWifiSelectionComplete(true); return; } // Turn on WiFi immediately - LOG_DBG("OTA", "Turning on WiFi..."); WiFi.mode(WIFI_STA); // Launch WiFi selection subactivity - LOG_DBG("OTA", "Launching WifiSelectionActivity..."); startActivityForResult(std::make_unique(renderer, mappedInput), [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); } @@ -115,13 +126,18 @@ void OtaUpdateActivity::render(RenderLock&&) { renderer.clearScreen(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_UPDATE)); + const Rect header{0, metrics.topPadding, pageWidth, TouchHeaderBackButton::height(metrics, mappedInput)}; + const bool canGoBack = state == WAITING_CONFIRMATION || state == FAILED || state == NO_UPDATE; + if (canGoBack && mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, tr(STR_UPDATE), false); + } else { + GUI.drawHeader(renderer, header, tr(STR_UPDATE)); + } const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height) / 2; float updaterProgress = 0; if (state == UPDATE_IN_PROGRESS) { - LOG_DBG("OTA", "Update progress: %d / %d", updater.getProcessedSize(), updater.getTotalSize()); updaterProgress = static_cast(updater.getProcessedSize()) / static_cast(updater.getTotalSize()); // Only update every 2% at the most if (static_cast(updaterProgress * 50) == lastUpdaterPercentage / 2) { @@ -139,6 +155,16 @@ void OtaUpdateActivity::render(RenderLock&&) { renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height * 2 + metrics.verticalSpacing * 2, (std::string(tr(STR_NEW_VERSION)) + updater.getLatestVersion()).c_str()); + if (mappedInput.hasTouch()) { + const auto actionRects = getOtaActionRects(renderer); + const int cancelTextWidth = renderer.getTextWidth(UI_10_FONT_ID, tr(STR_CANCEL)); + renderer.drawText(UI_10_FONT_ID, actionRects.cancel.x + (actionRects.cancel.width - cancelTextWidth) / 2, + actionRects.cancel.y + 28, tr(STR_CANCEL)); + const int updateTextWidth = renderer.getTextWidth(UI_10_FONT_ID, tr(STR_UPDATE)); + renderer.drawText(UI_10_FONT_ID, actionRects.update.x + (actionRects.update.width - updateTextWidth) / 2, + actionRects.update.y + 28, tr(STR_UPDATE)); + } + const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_UPDATE), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } else if (state == UPDATE_IN_PROGRESS) { @@ -170,62 +196,86 @@ void OtaUpdateActivity::render(RenderLock&&) { renderer.drawCenteredText(UI_10_FONT_ID, top + height + metrics.verticalSpacing, tr(STR_POWER_ON_HINT)); } - renderer.displayBuffer(); + renderer.displayBuffer(screenTransitionRefresh.modeFor(static_cast(state))); +} + +void OtaUpdateActivity::runUpdateInstall() { + { + RenderLock lock(*this); + failureMessage = StrId::STR_UPDATE_FAILED; + state = UPDATE_IN_PROGRESS; + } + if (requestUpdateAndWait() != RequestUpdateResult::Rendered) { + LOG_ERR("OTA", "Update progress screen could not be rendered synchronously; aborting OTA install"); + { + RenderLock lock(*this); + state = FAILED; + } + requestUpdate(true); + return; + } + const auto res = updater.installUpdate( + [](void* ctx) { + // immediate=true notifies the render task directly. The default deferred path only + // sets a flag consumed at the end of ActivityManager::loop(), which never runs while + // installUpdate() blocks this task. + static_cast(ctx)->requestUpdate(true); + }, + this); + + if (res != OtaUpdater::OK) { + LOG_DBG("OTA", "Update failed: %d", res); + { + RenderLock lock(*this); + failureMessage = failureMessageFor(res); + state = FAILED; + } + requestUpdate(); + return; + } + + { + RenderLock lock(*this); + state = FINISHED; + } + const auto renderResult = requestUpdateAndWait(); + if (renderResult == RequestUpdateResult::Rendered) { + // Hold the completion screen briefly so the user sees it, then restart. + delay(3000); + } else { + LOG_ERR("OTA", "Completion screen could not be rendered synchronously; restarting without sync confirmation"); + } + { + RenderLock lock(*this); + state = SHUTTING_DOWN; + } } void OtaUpdateActivity::loop() { + if ((state == WAITING_CONFIRMATION || state == FAILED || state == NO_UPDATE) && + TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + finish(); + return; + } + if (state == WAITING_CONFIRMATION) { - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - LOG_DBG("OTA", "New update available, starting download..."); - { - RenderLock lock(*this); - failureMessage = StrId::STR_UPDATE_FAILED; - state = UPDATE_IN_PROGRESS; - } - if (requestUpdateAndWait() != RequestUpdateResult::Rendered) { - LOG_ERR("OTA", "Update progress screen could not be rendered synchronously; aborting OTA install"); - { - RenderLock lock(*this); - state = FAILED; - } - requestUpdate(true); + int x = 0; + int y = 0; + if (mappedInput.wasScreenTapped(x, y)) { + const auto actionRects = getOtaActionRects(renderer); + if (contains(actionRects.cancel, x, y)) { + finish(); return; } - const auto res = updater.installUpdate( - [](void* ctx) { - // immediate=true notifies the render task directly. The default deferred path only - // sets a flag consumed at the end of ActivityManager::loop(), which never runs while - // installUpdate() blocks this task. - static_cast(ctx)->requestUpdate(true); - }, - this); - - if (res != OtaUpdater::OK) { - LOG_DBG("OTA", "Update failed: %d", res); - { - RenderLock lock(*this); - failureMessage = failureMessageFor(res); - state = FAILED; - } - requestUpdate(); + if (contains(actionRects.update, x, y)) { + runUpdateInstall(); return; } + } - { - RenderLock lock(*this); - state = FINISHED; - } - const auto renderResult = requestUpdateAndWait(); - if (renderResult == RequestUpdateResult::Rendered) { - // Hold the completion screen briefly so the user sees it, then restart. - delay(3000); - } else { - LOG_ERR("OTA", "Completion screen could not be rendered synchronously; restarting without sync confirmation"); - } - { - RenderLock lock(*this); - state = SHUTTING_DOWN; - } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + runUpdateInstall(); + return; } if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { @@ -236,14 +286,18 @@ void OtaUpdateActivity::loop() { } if (state == FAILED) { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + int x = 0; + int y = 0; + if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasScreenTapped(x, y)) { finish(); } return; } if (state == NO_UPDATE) { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + int x = 0; + int y = 0; + if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasScreenTapped(x, y)) { finish(); } return; diff --git a/src/activities/settings/OtaUpdateActivity.h b/src/activities/settings/OtaUpdateActivity.h index 445e121013..1d97183f3d 100644 --- a/src/activities/settings/OtaUpdateActivity.h +++ b/src/activities/settings/OtaUpdateActivity.h @@ -2,6 +2,7 @@ #include "I18nKeys.h" #include "activities/Activity.h" +#include "activities/ScreenTransitionRefresh.h" #include "network/OtaUpdater.h" class OtaUpdateActivity : public Activity { @@ -20,11 +21,13 @@ class OtaUpdateActivity : public Activity { static constexpr unsigned int UNINITIALIZED_PERCENTAGE = 111; State state = WIFI_SELECTION; + ScreenTransitionRefresh screenTransitionRefresh; unsigned int lastUpdaterPercentage = UNINITIALIZED_PERCENTAGE; StrId failureMessage = StrId::STR_UPDATE_FAILED; OtaUpdater updater; void onWifiSelectionComplete(bool success); + void runUpdateInstall(); public: explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) diff --git a/src/activities/settings/SdFirmwareUpdateActivity.cpp b/src/activities/settings/SdFirmwareUpdateActivity.cpp index b0ca1f6340..d2bfc05e61 100644 --- a/src/activities/settings/SdFirmwareUpdateActivity.cpp +++ b/src/activities/settings/SdFirmwareUpdateActivity.cpp @@ -10,6 +10,7 @@ #include "MappedInputManager.h" #include "activities/home/FileBrowserActivity.h" #include "activities/util/ConfirmationActivity.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" #include "network/FirmwareFlasher.h" @@ -103,6 +104,8 @@ bool SdFirmwareUpdateActivity::validateFirmware() { errorMessage = tr(STR_FIRMWARE_TOO_LARGE); } else if (vr == firmware_flash::Result::TOO_SMALL) { errorMessage = tr(STR_FIRMWARE_TOO_SMALL); + } else if (vr == firmware_flash::Result::BAD_CHIP) { + errorMessage = tr(STR_FIRMWARE_WRONG_DEVICE); } else { errorMessage = tr(STR_INVALID_FIRMWARE); } @@ -167,7 +170,8 @@ void SdFirmwareUpdateActivity::performUpdate() { const auto result = firmware_flash::flashFromSdPath(firmwarePath.c_str(), progressCb, this); if (result != firmware_flash::Result::OK) { LOG_ERR("FW", "flash failed: %s", firmware_flash::resultName(result)); - errorMessage = tr(STR_FIRMWARE_WRITE_FAILED); + errorMessage = + result == firmware_flash::Result::BAD_CHIP ? tr(STR_FIRMWARE_WRONG_DEVICE) : tr(STR_FIRMWARE_WRITE_FAILED); RenderLock lock(*this); state = State::FAILED; requestUpdate(); @@ -186,8 +190,11 @@ void SdFirmwareUpdateActivity::performUpdate() { void SdFirmwareUpdateActivity::loop() { if (state == State::FAILED) { - if (mappedInput.wasPressed(MappedInputManager::Button::Back) || - mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + int x = 0; + int y = 0; + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer) || + mappedInput.wasPressed(MappedInputManager::Button::Back) || + mappedInput.wasPressed(MappedInputManager::Button::Confirm) || mappedInput.wasScreenTapped(x, y)) { if (recoveryMode) { // Go back to picker so user can try a different .bin state = State::PICKING; @@ -207,7 +214,12 @@ void SdFirmwareUpdateActivity::render(RenderLock&&) { renderer.clearScreen(); const char* headerText = recoveryMode ? tr(STR_RECOVERY_MODE) : tr(STR_SD_FIRMWARE_UPDATE); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, headerText); + const Rect header{0, metrics.topPadding, pageWidth, TouchHeaderBackButton::height(metrics, mappedInput)}; + if (state == State::FAILED && mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, headerText, false); + } else { + GUI.drawHeader(renderer, header, headerText); + } const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - lineHeight) / 2; @@ -236,18 +248,36 @@ void SdFirmwareUpdateActivity::render(RenderLock&&) { renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF)); } else if (state == State::SUCCESS) { renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, top + lineHeight + metrics.verticalSpacing, tr(STR_RESTARTING_HINT)); + const auto hintLines = + renderer.wrappedText(UI_10_FONT_ID, tr(STR_RESTARTING_HINT), pageWidth - metrics.contentSidePadding * 2, 3); + int hintY = top + lineHeight + metrics.verticalSpacing; + for (const auto& line : hintLines) { + renderer.drawCenteredText(UI_10_FONT_ID, hintY, line.c_str()); + hintY += lineHeight; + } } else if (state == State::FAILED) { renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD); if (!errorMessage.empty()) { - renderer.drawCenteredText(UI_10_FONT_ID, top + lineHeight + metrics.verticalSpacing, errorMessage.c_str()); + const auto errorLines = + renderer.wrappedText(UI_10_FONT_ID, errorMessage.c_str(), pageWidth - metrics.contentSidePadding * 2, 3); + int errorY = top + lineHeight + metrics.verticalSpacing; + for (const auto& line : errorLines) { + renderer.drawCenteredText(UI_10_FONT_ID, errorY, line.c_str()); + errorY += lineHeight; + } } const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } else { // PICKING / CONFIRMING: a sub-activity is on top, nothing to draw. if (recoveryMode) { - renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_RECOVERY_MODE_HINT)); + const auto hintLines = renderer.wrappedText(UI_10_FONT_ID, tr(STR_RECOVERY_MODE_HINT), + pageWidth - metrics.contentSidePadding * 2, 3); + int hintY = top; + for (const auto& line : hintLines) { + renderer.drawCenteredText(UI_10_FONT_ID, hintY, line.c_str()); + hintY += lineHeight; + } } } diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 9263e49feb..8ee079a0f3 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -1,6 +1,8 @@ #include "SettingsActivity.h" +#include #include +#include #include #include @@ -16,15 +18,14 @@ #include "ClockOffsetActivity.h" #include "ClockSyncActivity.h" #include "CrossPointSettings.h" -#include "FontDownloadActivity.h" #include "FontSelectionActivity.h" #include "KOReaderSettingsActivity.h" #include "MappedInputManager.h" #include "OpdsServerListActivity.h" -#include "OtaUpdateActivity.h" #include "SdCardFontSystem.h" #include "SdFirmwareUpdateActivity.h" #include "SettingsList.h" +#include "SilentRestart.h" #include "StatusBarSettingsActivity.h" #include "activities/network/WifiSelectionActivity.h" #include "activities/reader/GlobalReadingStats.h" @@ -33,8 +34,19 @@ #include "activities/util/KeyboardEntryActivity.h" #include "activities/util/OptionSelectionActivity.h" #include "components/CompactHeader.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" +#include "util/DictionaryRegistry.h" + +namespace fui = freeink::ui; + +namespace { +constexpr fui::ActionId ACTION_ROW = 1; +constexpr fui::ActionId ACTION_TAB = 2; +} // namespace const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER, StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM}; @@ -47,6 +59,40 @@ constexpr size_t controlsPowerMinCount = 2; constexpr size_t controlsPowerMaxCount = 3; constexpr size_t controlsFrontButtonCount = 6; constexpr size_t controlsSideButtonCount = 3; +constexpr int touchSettingsRowHeightScale = 2; +constexpr int touchSettingsTabBarHeightScale = 2; + +int settingsTabBarTop(const ThemeMetrics& metrics) { return CompactHeader::headerBottomY(metrics); } + +int settingsRowHeightScale(const bool hasTouch) { return hasTouch ? touchSettingsRowHeightScale : 1; } + +int settingsTabBarHeight(const ThemeMetrics& metrics, const bool hasTouch) { + return metrics.tabBarHeight * (hasTouch ? touchSettingsTabBarHeightScale : 1); +} + +int settingsSubmenuHeaderOffset(const GfxRenderer& renderer, const ThemeMetrics& metrics, const bool hasSubmenuTitle) { + if (!hasSubmenuTitle) return 0; + return renderer.getLineHeight(UI_10_FONT_ID) + metrics.verticalSpacing; +} + +Rect settingsListRect(const ThemeMetrics& metrics, const int pageWidth, const int pageHeight, const bool hasTouch) { + const int tabBarTop = settingsTabBarTop(metrics); + const int listTop = tabBarTop + settingsTabBarHeight(metrics, hasTouch) + metrics.verticalSpacing; + return Rect{0, listTop, pageWidth, pageHeight - listTop - metrics.buttonHintsHeight - metrics.verticalSpacing}; +} + +Rect settingsHeaderRect(const ThemeMetrics& metrics, const int pageWidth) { + return Rect{0, metrics.topPadding, pageWidth, CompactHeader::headerBottomY(metrics) - metrics.topPadding}; +} + +Rect settingsRowsRect(const GfxRenderer& renderer, const ThemeMetrics& metrics, const int pageWidth, + const int pageHeight, const bool hasTouch, const bool hasSubmenuTitle) { + Rect rect = settingsListRect(metrics, pageWidth, pageHeight, hasTouch); + const int headerOffset = settingsSubmenuHeaderOffset(renderer, metrics, hasSubmenuTitle); + rect.y += headerOffset; + rect.height = std::max(0, rect.height - headerOffset); + return rect; +} uint8_t enumDisplayIndexForRawValue(const SettingInfo& setting, uint8_t rawValue) { if (setting.enumRawValues.empty()) { @@ -195,6 +241,12 @@ std::string trimAsciiSpaces(const std::string& value) { } } // namespace +SettingsActivity::SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const bool dismissOnUpSwipe) + : Activity("Settings", renderer, mappedInput), + dismissOnUpSwipe(dismissOnUpSwipe), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + void SettingsActivity::rebuildSettingsLists() { displaySettings.clear(); displaySleepSettings.clear(); @@ -215,8 +267,17 @@ void SettingsActivity::rebuildSettingsLists() { // reader activity ran — otherwise the font-family picker shows stale list. sdFontSystem.refreshIfDirty(); - const auto allSettings = getSettingsList(&sdFontSystem.registry()); + dictionaryRegistry.refreshIfDirty(); + const auto allSettings = getSettingsList(&sdFontSystem.registry(), &dictionaryRegistry); displaySettings = buildGroupedDisplaySettingsList(allSettings); +#ifndef SIMULATOR + if (BoardConfig::isX4Pro()) { + displaySettings.erase( + std::remove_if(displaySettings.begin(), displaySettings.end(), + [](const SettingInfo& setting) { return setting.valuePtr == &CrossPointSettings::fadingFix; }), + displaySettings.end()); + } +#endif displaySleepSettings = buildDisplaySleepSettingsList(allSettings); readerSettings = buildReaderSettingsParentList(allSettings); readerFontSettings = buildReaderFontSettingsList(allSettings); @@ -228,15 +289,29 @@ void SettingsActivity::rebuildSettingsLists() { systemGlobalStatsSettings = buildSystemGlobalStatsSettingsList(allSettings); controlsSettings = buildControlsSettingsParentList(allSettings); controlsPowerSettings = buildControlsPowerSettingsList(allSettings); +#if CROSSINK_APP_CAP_TOUCH + if (!gpio.hasTouch()) { + controlsFrontButtonSettings = buildControlsFrontButtonSettingsList(allSettings); + } + controlsSideButtonSettings = buildControlsSideButtonSettingsList(allSettings); + + const bool hasTouch = gpio.hasTouch(); + const size_t expectedControlsCount = controlsParentBaseCount - (hasTouch ? 1u : 0u) + + (hasSettingByName(allSettings, StrId::STR_TILT_PAGE_TURN) ? 1u : 0u) + + (hasSettingByName(allSettings, StrId::STR_TILT_PAGE_TURN_DIRECTION) ? 1u : 0u); + const size_t expectedFrontButtonCount = hasTouch ? 0u : controlsFrontButtonCount; +#else controlsFrontButtonSettings = buildControlsFrontButtonSettingsList(allSettings); controlsSideButtonSettings = buildControlsSideButtonSettingsList(allSettings); const size_t expectedControlsCount = controlsParentBaseCount + (hasSettingByName(allSettings, StrId::STR_TILT_PAGE_TURN) ? 1u : 0u) + (hasSettingByName(allSettings, StrId::STR_TILT_PAGE_TURN_DIRECTION) ? 1u : 0u); + constexpr size_t expectedFrontButtonCount = controlsFrontButtonCount; +#endif if (controlsSettings.size() != expectedControlsCount || controlsPowerSettings.size() < controlsPowerMinCount || controlsPowerSettings.size() > controlsPowerMaxCount || - controlsFrontButtonSettings.size() != controlsFrontButtonCount || + controlsFrontButtonSettings.size() != expectedFrontButtonCount || controlsSideButtonSettings.size() != controlsSideButtonCount) { LOG_ERR("SET", "Unexpected controls menu counts: controls=%u/%u power=%u front=%u side=%u", static_cast(controlsSettings.size()), static_cast(expectedControlsCount), @@ -310,6 +385,7 @@ void SettingsActivity::enterCategory(int categoryIndex) { activeSubmenu = SettingAction::None; parentSubmenu = SettingAction::None; setCurrentSettingsForCategory(); + showSettingSelection = true; } StrId SettingsActivity::activeSubmenuTitleId() const { @@ -344,6 +420,7 @@ void SettingsActivity::openSubmenu(SettingAction action) { activeSubmenu = action; setCurrentSettingsForCategory(); selectedSettingIndex = 1; + showSettingSelection = true; while (selectedSettingIndex > 0 && selectedSettingIndex <= settingsCount && (*currentSettings)[selectedSettingIndex - 1].type == SettingType::SECTION_HEADER) { selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1); @@ -355,6 +432,7 @@ void SettingsActivity::closeSubmenu() { parentSubmenu = SettingAction::None; setCurrentSettingsForCategory(); selectedSettingIndex = 1; + showSettingSelection = true; } bool SettingsActivity::currentSettingUsesOptionMenu(const SettingInfo& setting) const { @@ -416,7 +494,7 @@ void SettingsActivity::openScreenMarginPicker(const SettingInfo& setting) { const SettingInfo selectedSetting = setting; startActivityForResult( std::make_unique(renderer, mappedInput, "SettingsValueSelect", selectedSetting.nameId, - std::move(options), currentIndex), + std::move(options), currentIndex, false, true), [this, selectedSetting](const ActivityResult& result) { if (result.isCancelled) { requestUpdate(); @@ -518,6 +596,10 @@ void SettingsActivity::openStringEditor(const SettingInfo& setting) { void SettingsActivity::onEnter() { Activity::onEnter(); + // Dictionary names and paths are needed only while settings are open. Keep + // the catalog out of the reader's steady-state heap. + dictionaryRegistry.discover(); + // Reset selection to first category selectedCategoryIndex = 0; selectedSettingIndex = 0; @@ -530,23 +612,106 @@ void SettingsActivity::onEnter() { rebuildSettingsLists(); + uiReady = false; + visibleRows = 1; + topIndex = 0; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &SettingsActivity::onRowEvent, this); + app.on(ACTION_TAB, &SettingsActivity::onTabEvent, this); + app.setScreen(&SettingsActivity::settingsScreen, this); + // Trigger first update requestUpdate(); } +void SettingsActivity::selectCategory(const int categoryIndex) { + selectedCategoryIndex = categoryIndex; + switch (selectedCategoryIndex) { + case 0: + currentSettings = &displaySettings; + break; + case 1: + currentSettings = &readerSettings; + break; + case 2: + currentSettings = &controlsSettings; + break; + case 3: + currentSettings = &systemSettings; + break; + } + settingsCount = static_cast(currentSettings->size()); + topIndex = 0; +} + +void SettingsActivity::onTabEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (self->optionPopup.isActive()) return; + if (event.value < 0 || event.value >= categoryCount) return; + self->selectedSettingIndex = 0; + self->enterCategory(event.value); + self->topIndex = 0; + // The switched-to tab repaints as the selected pill; a flash overlay on top + // of it just repaints the pill in the focused style. + self->app.clearTapFlash(); +} + +void SettingsActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (self->optionPopup.isActive()) return; + if (event.value < 0 || event.value >= static_cast(self->settingsCount)) return; + if ((*self->currentSettings)[event.value].type == SettingType::SECTION_HEADER) return; + self->selectedSettingIndex = event.value + 1; + // Most rows repaint a different surface (popup, sub-activity, new value); + // a lingering tap flash would gray an unrelated element. + self->app.clearTapFlash(); + self->toggleCurrentSetting(); +} + void SettingsActivity::onExit() { + dictionaryRegistry.clear(); + sdFontSystem.releaseRegistry(); Activity::onExit(); UITheme::getInstance().reload(); // Re-apply theme in case it was changed } +void SettingsActivity::applyUiSettingChange(uint8_t CrossPointSettings::* valuePtr) { + // Theme and UI-scale changes take effect immediately, on this screen — + // reload the theme and re-derive the app's fonts and tokens so the very + // next repaint is in the new look. + if (valuePtr == &CrossPointSettings::uiTheme) { + UITheme::getInstance().reload(); + } else if (valuePtr != &CrossPointSettings::uiScale) { + return; + } + const auto spec = uiScaleSpec(); + uiTarget.setFont(fui::GfxRendererTarget::FONT_SMALL, spec.smallFontId); + uiTarget.setFont(fui::GfxRendererTarget::FONT_BODY, spec.bodyFontId); + uiTarget.setFont(fui::GfxRendererTarget::FONT_TITLE, spec.titleFontId); + app.setTheme(uiThemeTokens(uiTarget)); +} + void SettingsActivity::loop() { if (optionPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return; + const auto& metrics = UITheme::getInstance().getMetrics(); + if (TouchHeaderBackButton::wasTapped(mappedInput, settingsHeaderRect(metrics, renderer.getScreenWidth()))) { + if (activeSubmenu != SettingAction::None) { + closeSubmenu(); + requestUpdate(); + } else { + SETTINGS.saveToFile(); + onGoHome(); + } + return; + } + bool hasChangedCategory = false; // Handle actions with early return if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + showSettingSelection = true; if (selectedSettingIndex == 0) { enterCategory((selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0); hasChangedCategory = true; @@ -566,6 +731,7 @@ void SettingsActivity::loop() { } if (selectedSettingIndex > 0) { selectedSettingIndex = 0; + showSettingSelection = true; requestUpdate(); } else { SETTINGS.saveToFile(); @@ -574,33 +740,69 @@ void SettingsActivity::loop() { return; } - // Handle navigation - buttonNavigator.onNextRelease([this] { - selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1); - while (selectedSettingIndex > 0 && selectedSettingIndex <= settingsCount && - (*currentSettings)[selectedSettingIndex - 1].type == SettingType::SECTION_HEADER) { - selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1); + // Touch goes through the FreeInkApp: render() registered the tab and row + // hit rects; route the snapshot and let the handlers dispatch. + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + // No pressed-state repaint here: it would cost a second e-ink refresh + // per tap and paint a transient double pill on tab switches whose + // erased black leaves a partial-refresh ghost. + if (app.invalidated()) requestUpdate(); + if (event) return; // dispatched to onTabEvent/onRowEvent } - requestUpdate(); - }); + } - buttonNavigator.onPreviousRelease([this] { - selectedSettingIndex = ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1); - while (selectedSettingIndex > 0 && selectedSettingIndex <= settingsCount && - (*currentSettings)[selectedSettingIndex - 1].type == SettingType::SECTION_HEADER) { - selectedSettingIndex = ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1); + // Swipes scroll the viewport; the selection stays put (it may scroll + // off-screen) and button navigation pulls the view back to it. + const auto swipe = mappedInput.wasSwipe(); + if (dismissOnUpSwipe && swipe == MappedInputManager::SwipeDir::Up) { + SETTINGS.saveToFile(); + finish(); + return; + } + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, settingsCount); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + + // Buttons walk the tab band (index 0) plus the rows (1..settingsCount). + const auto moveSelection = [this](int index, const bool forward) { + while (index > 0 && index <= settingsCount && (*currentSettings)[index - 1].type == SettingType::SECTION_HEADER) { + index = forward ? ButtonNavigator::nextIndex(index, settingsCount + 1) + : ButtonNavigator::previousIndex(index, settingsCount + 1); + } + selectedSettingIndex = index; + if (selectedSettingIndex == 0) { + topIndex = 0; + } else { + topIndex = followListSelection(selectedSettingIndex - 1, topIndex, visibleRows, settingsCount); } requestUpdate(); + }; + buttonNavigator.onNextRelease([this, &moveSelection] { + moveSelection(ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1), true); + }); + buttonNavigator.onPreviousRelease([this, &moveSelection] { + moveSelection(ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1), false); }); buttonNavigator.onNextContinuous([this, &hasChangedCategory] { hasChangedCategory = true; + showSettingSelection = true; enterCategory(ButtonNavigator::nextIndex(selectedCategoryIndex, categoryCount)); requestUpdate(); }); buttonNavigator.onPreviousContinuous([this, &hasChangedCategory] { hasChangedCategory = true; + showSettingSelection = true; enterCategory(ButtonNavigator::previousIndex(selectedCategoryIndex, categoryCount)); requestUpdate(); }); @@ -745,17 +947,13 @@ void SettingsActivity::toggleCurrentSetting() { startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; case SettingAction::CheckForUpdates: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + silentRestartToNetwork(NetworkBootTarget::OTA); break; case SettingAction::SdFirmwareUpdate: startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; case SettingAction::DownloadFonts: - startActivityForResult(std::make_unique(renderer, mappedInput), - [this](const ActivityResult&) { - SETTINGS.saveToFile(); - rebuildSettingsLists(); - }); + silentRestartToManageFonts(); break; case SettingAction::Language: openLanguagePicker(); @@ -787,6 +985,11 @@ void SettingsActivity::toggleCurrentSetting() { syncQuickResumeTimeoutForSleepScreen(sleepScreenChanged, quickResumeTimeoutChanged); SETTINGS.saveToFile(); + // Apply this while `setting` still refers to the current list; rebuilding + // below clears its backing vector and invalidates the reference. + applyUiSettingChange(setting.valuePtr); + rebuildSettingsLists(); + selectedSettingIndex = std::min(selectedSettingIndex, settingsCount); } void SettingsActivity::syncQuickResumeTimeoutForSleepScreen(bool sleepScreenChanged, bool quickResumeTimeoutChanged) { @@ -819,7 +1022,8 @@ void SettingsActivity::openSleepTimeoutPicker() { CrossPointSettings::MIN_SLEEP_TIMEOUT_MINUTES, CrossPointSettings::MAX_SLEEP_TIMEOUT_MINUTES, 1, 5, StrId::STR_SLEEP_TIMER_VALUE_FORMAT, /*readerActivity=*/false, /*allowPowerAsConfirm=*/false, /*ignoreInitialConfirmRelease=*/true, - /*showPercentValue=*/false, StrId::STR_SLEEP_NEVER), + /*showPercentValue=*/false, StrId::STR_SLEEP_NEVER, /*overrideDisabledReaderTouchscreen=*/false, + /*showTouchHeaderBackButton=*/true), [this](const ActivityResult& result) { if (!result.isCancelled) { SETTINGS.sleepTimeoutMinutes = static_cast(std::get(result.data).value); @@ -835,7 +1039,8 @@ void SettingsActivity::openLineHeightPicker() { renderer, mappedInput, "LineHeightInterval", StrId::STR_LINE_SPACING, SETTINGS.lineHeightPercent, CrossPointSettings::MIN_LINE_HEIGHT_PERCENT, CrossPointSettings::MAX_LINE_HEIGHT_PERCENT, 1, 10, StrId::STR_NONE_OPT, /*readerActivity=*/false, - /*allowPowerAsConfirm=*/false, /*ignoreInitialConfirmRelease=*/false, /*showPercentValue=*/true), + /*allowPowerAsConfirm=*/false, /*ignoreInitialConfirmRelease=*/false, /*showPercentValue=*/true, + StrId::STR_NONE_OPT, /*overrideDisabledReaderTouchscreen=*/false, /*showTouchHeaderBackButton=*/true), [this](const ActivityResult& result) { if (!result.isCancelled) { SETTINGS.lineHeightPercent = CrossPointSettings::clampedLineHeightPercent( @@ -854,7 +1059,8 @@ void SettingsActivity::openIdleTimeThresholdPicker() { CrossPointSettings::MAX_READING_IDLE_TIME_THRESHOLD_SECONDS, CrossPointSettings::READING_IDLE_TIME_THRESHOLD_UNIT_SECONDS, 60, StrId::STR_SECONDS_VALUE_FORMAT, /*readerActivity=*/false, /*allowPowerAsConfirm=*/false, /*ignoreInitialConfirmRelease=*/false, - /*showPercentValue=*/false), + /*showPercentValue=*/false, StrId::STR_NONE_OPT, /*overrideDisabledReaderTouchscreen=*/false, + /*showTouchHeaderBackButton=*/true), [this](const ActivityResult& result) { if (!result.isCancelled) { SETTINGS.readingIdleTimeThresholdUnits = CrossPointSettings::readingIdleTimeThresholdUnitsForSeconds( @@ -865,86 +1071,187 @@ void SettingsActivity::openIdleTimeThresholdPicker() { }); } +std::string SettingsActivity::settingValueText(const SettingInfo& setting) { + if (settingShowsNavigationCaret(setting)) return ">"; + if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { + return SETTINGS.*(setting.valuePtr) ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); + } + if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { + const uint8_t displayIndex = enumDisplayIndexForRawValue(setting, SETTINGS.*(setting.valuePtr)); + return settingEnumOptionLabel(setting, displayIndex); + } + if (setting.type == SettingType::ENUM && setting.valueGetter) { + return settingEnumOptionLabel(setting, setting.valueGetter()); + } + if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { + return formatSettingValue(setting); + } + if (setting.type == SettingType::ACTION && setting.action == SettingAction::Language) { + return I18N.getLanguageName(I18N.getLanguage()); + } + if (setting.type == SettingType::STRING) { + if (setting.nameId == StrId::STR_DEVICE_NAME) return SETTINGS.getEffectiveDeviceName(); + if (setting.stringGetter) return setting.stringGetter(); + if (setting.stringMaxLen > 0) return reinterpret_cast(&SETTINGS) + setting.stringOffset; + } + return ""; +} + +void SettingsActivity::settingsScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildSettingsScreen(screen); +} + +void SettingsActivity::buildSettingsScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + // Content starts directly below the compact header divider. + screen.setContentMargin(fui::Insets{static_cast(settingsTabBarTop(metrics)), 0, + static_cast(metrics.buttonHintsHeight), 0}); + + // Category tabs. The selected pill dims to a dither when the selection is + // down in the list (the legacy focused/unfocused tab distinction). + fui::TabItem tabs[categoryCount]; + for (int i = 0; i < categoryCount; i++) { + tabs[i].label = I18N.get(categoryNames[i]); + tabs[i].value = static_cast(i); + tabs[i].selected = selectedCategoryIndex == i; + } + fui::TabBarProps tabProps; + tabProps.tabs = tabs; + tabProps.count = categoryCount; + tabProps.action = ACTION_TAB; + tabProps.inputMask = fui::InputTouch; + // Small text: four category labels share the band, the body font truncates. + tabProps.text = screen.theme().smallText; + // Pill wraps the label with real padding; the band grows with the scaled + // font when the theme's tabBarHeight is too short for it. Keep the horizontal + // padding tight: four equal slots share the band, so wide labels (e.g. + // "Controls") truncate to an ellipsis at large UI scales when the pill eats + // too much width. Vertical padding stays for the pill height. + tabProps.tabInset = fui::Insets{2, 2, 4, 2}; + tabProps.contentInset = fui::Insets{2, 4, 2, 4}; + const int16_t tabLineHeight = screen.target().lineHeight(screen.theme().smallText.font); + const int16_t tabBand = + static_cast(metrics.tabBarHeight > tabLineHeight + 10 ? metrics.tabBarHeight : tabLineHeight + 10); + // Legacy Lyra two-state treatment: with the selection on the tab band, the + // band fills gray and the active tab is a solid pill; with the selection + // down in the list, the band is plain and the active tab keeps a gray box + // with an underline. The 1px rule under the band is always there. + const bool tabsFocused = selectedSettingIndex == 0; + const bool borderedTabs = metrics.tabBarAppearance == ThemeTabBarAppearance::BorderedText; + const bool roundedRaffTabs = SETTINGS.uiTheme == CrossPointSettings::UI_THEME::ROUNDEDRAFF; + tabProps.divider = true; + fui::StyleSet tabStyles; + if (roundedRaffTabs) { + // RoundedRaff's tabs have always sat on white, with the selected pill + // turning dark gray after focus moves into the settings list. + tabStyles.explicitlySet = true; + tabStyles.normal.foreground = fui::Paint::solid(fui::Color::Black); + tabStyles.selected.background = fui::Paint::solid(tabsFocused ? fui::Color::Black : fui::Color::DarkGray); + tabStyles.selected.foreground = fui::Paint::solid(fui::Color::White); + tabStyles.selected.radius = 18; + tabStyles.focused = tabStyles.selected; + tabStyles.active = tabStyles.selected; + tabProps.tabStyles = tabStyles; + } else if (!borderedTabs) { + tabStyles.explicitlySet = true; + tabStyles.normal.foreground = fui::Paint::solid(fui::Color::Black); + if (tabsFocused) { + tabStyles.selected.background = fui::Paint::solid(fui::Color::Black); + tabStyles.selected.foreground = fui::Paint::solid(fui::Color::White); + tabStyles.selected.radius = screen.theme().listRowRadius; + } else { + tabStyles.selected.background = fui::Paint::dither(fui::Color::LightGray); + tabStyles.selected.foreground = fui::Paint::solid(fui::Color::Black); + // Let the selected underline meet the shared bottom divider, as in the + // original Lyra tab bar. The default bottom inset leaves a visible gap. + tabProps.tabInset.bottom = 0; + tabProps.selectedUnderline = 2; + } + // Focus/flash states keep the pill instead of falling back to an unset + // (blank) style. + tabStyles.focused = tabStyles.selected; + tabStyles.active = tabStyles.selected; + tabProps.tabStyles = tabStyles; + } + const fui::Rect tabRect = screen.takeTop(tabBand); + if (!roundedRaffTabs && !borderedTabs && tabsFocused) { + screen.target().fill(tabRect, fui::Paint::dither(fui::Color::LightGray)); + } + drawUiTabBar(screen, tabProps, tabRect, metrics.tabBarAppearance); + screen.spacer(static_cast(metrics.verticalSpacing)); + + const StrId submenuTitle = activeSubmenuTitleId(); + if (submenuTitle != StrId::STR_NONE_OPT) { + fui::TextStyle titleStyle = screen.theme().smallText; + titleStyle.bold = true; + titleStyle.maxLines = 1; + const int16_t titleHeight = screen.target().lineHeight(titleStyle.font); + fui::Rect titleRect = screen.takeTop(titleHeight, static_cast(metrics.verticalSpacing)); + const int16_t sidePadding = static_cast(metrics.contentSidePadding); + titleRect.x = static_cast(titleRect.x + sidePadding); + titleRect.width = static_cast(titleRect.width > sidePadding * 2 ? titleRect.width - sidePadding * 2 : 0); + screen.target().text(titleRect, I18N.get(submenuTitle), titleStyle); + } + + // Settings rows. Values are built per render and owned for the draw only. + const auto& settings = *currentSettings; + std::vector values(settings.size()); + std::vector items; + items.reserve(settings.size()); + for (size_t i = 0; i < settings.size(); i++) { + values[i] = settingValueText(settings[i]); + const bool isSectionHeader = settings[i].type == SettingType::SECTION_HEADER; + fui::ListItem item; + item.label = isSectionHeader ? uiListSectionHeaderLabel(values[i], I18N.get(settings[i].nameId)) + : I18N.get(settings[i].nameId); + if (!isSectionHeader && !values[i].empty()) item.value = values[i].c_str(); + item.isHeader = isSectionHeader; + item.actionValue = static_cast(i); + items.push_back(item); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedSettingIndex - 1); // -1 = tab band focused + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; // physical buttons stay in loop() + props.valueInset = 8; // air between the value and the row edge + // Use FreeInkUI's bodyText default so Settings scales consistently with + // File Browser, reader menus, and the other list-style screens. + props.labelText = screen.theme().bodyText; + props.labelText.maxLines = 2; + configureUiListSectionHeaders(props, screen.theme()); + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, settingsCount); // clamp to range + props.topIndex = static_cast(topIndex); + screen.list(props); +} + void SettingsActivity::render(RenderLock&&) { if (optionPopup.processRender(renderer, mappedInput)) return; renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - const auto& metrics = UITheme::getInstance().getMetrics(); - CompactHeader::drawTitle(renderer, tr(STR_SETTINGS_TITLE), true); - - std::vector tabs; - tabs.reserve(categoryCount); - for (int i = 0; i < categoryCount; i++) { - tabs.push_back({I18N.get(categoryNames[i]), selectedCategoryIndex == i}); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::drawCompact(renderer, tr(STR_SETTINGS_TITLE), false, true); + } else { + CompactHeader::drawTitle(renderer, tr(STR_SETTINGS_TITLE), true); } - const int tabBarTop = CompactHeader::headerBottomY(metrics); - GUI.drawTabBar(renderer, Rect{0, tabBarTop, pageWidth, metrics.tabBarHeight}, tabs, selectedSettingIndex == 0); - const auto& settings = *currentSettings; - Rect listRect{ - 0, tabBarTop + metrics.tabBarHeight + metrics.verticalSpacing, pageWidth, - pageHeight - (tabBarTop + metrics.tabBarHeight + metrics.buttonHintsHeight + metrics.verticalSpacing * 2)}; - const StrId submenuTitleId = activeSubmenuTitleId(); - if (submenuTitleId != StrId::STR_NONE_OPT) { - constexpr int submenuHeaderFontId = UI_10_FONT_ID; - const int headerLineHeight = renderer.getLineHeight(submenuHeaderFontId); - const int headerOffset = headerLineHeight + metrics.verticalSpacing; - const int headerMaxWidth = listRect.width - metrics.contentSidePadding * 2; - const auto headerLabel = - renderer.truncatedText(submenuHeaderFontId, I18N.get(submenuTitleId), headerMaxWidth, EpdFontFamily::BOLD); - renderer.drawText(submenuHeaderFontId, listRect.x + metrics.contentSidePadding, listRect.y, headerLabel.c_str(), - true, EpdFontFamily::BOLD); - listRect.y += headerOffset; - listRect.height = std::max(0, listRect.height - headerOffset); - } - GUI.drawList( - renderer, listRect, settingsCount, selectedSettingIndex - 1, - [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); }, nullptr, nullptr, - [this, &settings](int i) { - const auto& setting = settings[i]; - std::string valueText = ""; - if (settingShowsNavigationCaret(setting)) { - valueText = ">"; - } else if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - const bool value = SETTINGS.*(setting.valuePtr); - valueText = value ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); - } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { - const uint8_t value = SETTINGS.*(setting.valuePtr); - const uint8_t displayValue = enumDisplayIndexForRawValue(setting, value); - const size_t optionCount = settingEnumOptionCount(setting); - const uint8_t safeValue = displayValue < optionCount ? displayValue : 0; - valueText = settingEnumOptionLabel(setting, safeValue); - } else if (setting.type == SettingType::ENUM && setting.valueGetter) { - const uint8_t value = setting.valueGetter(); - valueText = settingEnumOptionLabel(setting, value); - } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - valueText = formatSettingValue(setting); - } else if (setting.type == SettingType::ACTION && setting.action == SettingAction::Language) { - valueText = I18N.getLanguageName(I18N.getLanguage()); - } else if (setting.type == SettingType::STRING) { - if (setting.nameId == StrId::STR_DEVICE_NAME) { - valueText = SETTINGS.getEffectiveDeviceName(); - } else if (setting.stringGetter) { - valueText = setting.stringGetter(); - } else if (setting.stringMaxLen > 0) { - valueText = reinterpret_cast(&SETTINGS) + setting.stringOffset; - } - } - return valueText; - }, - true, nullptr, [&settings](int i) { return settings[i].type == SettingType::SECTION_HEADER; }); + uiReady = false; + app.render(); + uiReady = true; - // Draw CrossInk version label at the bottom of the System tab + // Keep build information discoverable without crowding the common header. if (selectedCategoryIndex == 3) { - drawSystemVersionFooter(renderer, pageWidth, pageHeight, metrics); + drawSystemVersionFooter(renderer, pageWidth, renderer.getScreenHeight(), metrics); } - // Draw help text const auto confirmLabel = (selectedSettingIndex == 0) ? I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount]) diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 856274f824..b572d6ee1f 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -1,7 +1,10 @@ #pragma once +#include +#include #include #include +#include #include #include #include @@ -244,6 +247,10 @@ class SettingsActivity final : public Activity { bool preserveQuickResumeTimeoutOn = false; bool quickResumeTimeoutAutoEnabled = false; + // The frontlight-panel shortcut opens Settings as a transient Home menu. + // Its swipe-up closes the screen; regular Settings keeps swipe scrolling. + bool dismissOnUpSwipe = false; + bool showSettingSelection = true; SettingAction activeSubmenu = SettingAction::None; SettingAction parentSubmenu = SettingAction::None; @@ -252,6 +259,25 @@ class SettingsActivity final : public Activity { static constexpr int categoryCount = 4; static const StrId categoryNames[categoryCount]; + // FreeInkApp hosts the tab bar + settings list (themed, touch-routed); the + // header stays on GUI.drawHeader for the battery, OptionPopup stays legacy. + using UiApp = freeink::ui::FreeInkApp<24, 4>; + freeink::ui::GfxRendererTarget uiTarget; // must precede `app`: the app holds a reference to it + UiApp app; + // render() rebuilds the app's interaction table; loop() only routes touch + // snapshots against it while this is true (the two run on different tasks). + std::atomic uiReady{false}; + int visibleRows = 1; // rows per page at the current scale; set by the screen builder + int topIndex = 0; // viewport scroll position, decoupled from the selection + + static void settingsScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + static void onTabEvent(const freeink::ui::ActionEvent& event, void* user); + static std::string settingValueText(const SettingInfo& setting); + void buildSettingsScreen(UiApp::ScreenType& screen); + void selectCategory(int categoryIndex); + void applyUiSettingChange(uint8_t CrossPointSettings::* valuePtr); + void enterCategory(int categoryIndex); void setCurrentSettingsForCategory(); StrId activeSubmenuTitleId() const; @@ -270,8 +296,7 @@ class SettingsActivity final : public Activity { void syncQuickResumeTimeoutForSleepScreen(bool sleepScreenChanged, bool quickResumeTimeoutChanged); public: - explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("Settings", renderer, mappedInput) {} + explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool dismissOnUpSwipe = false); void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/settings/StatusBarSettingsActivity.cpp b/src/activities/settings/StatusBarSettingsActivity.cpp index 1ed964d3b9..69cbc943f1 100644 --- a/src/activities/settings/StatusBarSettingsActivity.cpp +++ b/src/activities/settings/StatusBarSettingsActivity.cpp @@ -12,9 +12,12 @@ #include "CrossPointSettings.h" #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" +namespace fui = freeink::ui; + namespace { enum MenuItem { ITEM_CHAPTER_PAGE_COUNT = 0, @@ -190,6 +193,12 @@ void StatusBarSettingsActivity::onEnter() { selectedIndex = 0; visibleItemCount = stablePageNumbersAvailable ? ITEM_COUNT : ITEM_COUNT - 1; + uiReady = false; + visibleRows = 1; + topIndex = 0; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_ROW, &StatusBarSettingsActivity::onRowEvent, this); + app.setScreen(&StatusBarSettingsActivity::settingsScreen, this); // Clamp statusBarProgressBar and statusBarTitle in case of corrupt/migrated data if (SETTINGS.statusBarProgressBar >= PROGRESS_BAR_ITEMS) { @@ -220,6 +229,33 @@ void StatusBarSettingsActivity::onExit() { Activity::onExit(); } void StatusBarSettingsActivity::loop() { if (optionPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return; + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + finishAfterBackPress(); + return; + } + + if (uiReady) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) return; + } + } + + if (mappedInput.hasTouch()) { + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows : -visibleRows; + const int next = scrollListBy(topIndex, delta, visibleRows, visibleItemCount); + if (next != topIndex) { + topIndex = next; + requestUpdate(); + } + return; + } + } + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { finishAfterBackPress(); return; @@ -234,21 +270,25 @@ void StatusBarSettingsActivity::loop() { // Handle navigation buttonNavigator.onNextRelease([this] { selectedIndex = ButtonNavigator::nextIndex(selectedIndex, visibleItemCount); + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, visibleItemCount); requestUpdate(); }); buttonNavigator.onPreviousRelease([this] { selectedIndex = ButtonNavigator::previousIndex(selectedIndex, visibleItemCount); + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, visibleItemCount); requestUpdate(); }); buttonNavigator.onNextContinuous([this] { selectedIndex = ButtonNavigator::nextIndex(selectedIndex, visibleItemCount); + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, visibleItemCount); requestUpdate(); }); buttonNavigator.onPreviousContinuous([this] { selectedIndex = ButtonNavigator::previousIndex(selectedIndex, visibleItemCount); + topIndex = followListSelection(selectedIndex, topIndex, visibleRows, visibleItemCount); requestUpdate(); }); } @@ -309,6 +349,69 @@ void StatusBarSettingsActivity::openOptionPicker() { requestUpdate(); } +void StatusBarSettingsActivity::settingsScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildSettingsScreen(screen); +} + +void StatusBarSettingsActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (self->optionPopup.isActive() || event.value < 0 || event.value >= self->visibleItemCount) return; + self->selectedIndex = event.value; + self->app.clearTapFlash(); + self->handleSelection(); +} + +void StatusBarSettingsActivity::buildSettingsScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const int pageWidth = renderer.getScreenWidth(); + const int pageHeight = renderer.getScreenHeight(); + const auto orientation = renderer.getOrientation(); + const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; + const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; + const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? metrics.buttonHintsHeight : 0; + const int contentX = isLandscapeCw ? hintGutterWidth : 0; + const int contentWidth = pageWidth - hintGutterWidth; + const int contentTop = + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; + const int previewLabelLineHeight = renderer.getLineHeight(UI_10_FONT_ID); + constexpr int previewLabelGap = 18; + const int previewSectionHeight = previewLabelLineHeight + previewLabelGap + UITheme::getStatusBarHeight(); + const int contentHeight = + pageHeight - contentTop - metrics.buttonHintsHeight - previewSectionHeight - metrics.verticalSpacing * 2; + + screen.setContentMargin( + fui::Insets{static_cast(contentTop), static_cast(pageWidth - (contentX + contentWidth)), + static_cast(pageHeight - (contentTop + contentHeight)), static_cast(contentX)}); + + std::vector values(visibleItemCount); + std::vector items; + items.reserve(visibleItemCount); + for (int i = 0; i < visibleItemCount; ++i) { + const int itemIndex = itemForVisibleIndex(i); + values[i] = valueTextForItem(itemIndex); + fui::ListItem item; + item.label = I18N.get(menuNames[itemIndex]); + item.value = values[i].c_str(); + item.actionValue = static_cast(i); + items.push_back(item); + } + + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.valueInset = 8; + props.labelText = screen.theme().bodyText; + props.labelText.maxLines = 2; + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows = rows > 0 ? rows : 1; + topIndex = scrollListBy(topIndex, 0, visibleRows, visibleItemCount); + props.topIndex = static_cast(topIndex); + screen.list(props); +} + void StatusBarSettingsActivity::render(RenderLock&&) { if (optionPopup.processRender(renderer, mappedInput)) return; @@ -326,27 +429,26 @@ void StatusBarSettingsActivity::render(RenderLock&&) { const int contentX = isLandscapeCw ? hintGutterWidth : 0; const int contentWidth = pageWidth - hintGutterWidth; - const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int previewLabelLineHeight = renderer.getLineHeight(UI_10_FONT_ID); constexpr int previewLabelGap = 18; - const int previewStatusBarHeight = UITheme::getStatusBarHeight(); - const int previewSectionHeight = previewLabelLineHeight + previewLabelGap + previewStatusBarHeight; - const int contentHeight = - pageHeight - contentTop - metrics.buttonHintsHeight - previewSectionHeight - metrics.verticalSpacing * 2; const auto labels = mappedInput.mapLabels( tr(STR_BACK), selectedItemUsesOptionMenu() ? tr(STR_SELECT) : tr(STR_TOGGLE), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); int bottomPreviewPadding = metrics.buttonHintsHeight + metrics.verticalSpacing; - GUI.drawHeader(renderer, Rect{contentX, metrics.topPadding, contentWidth, metrics.headerHeight}, - tr(STR_CUSTOMISE_STATUS_BAR), nullptr, readerContext); + const Rect header = TouchHeaderBackButton::headerRect(renderer, mappedInput); + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_CUSTOMISE_STATUS_BAR), readerContext); + } else { + GUI.drawHeader( + renderer, Rect{contentX, metrics.topPadding, contentWidth, TouchHeaderBackButton::height(metrics, mappedInput)}, + tr(STR_CUSTOMISE_STATUS_BAR), nullptr, readerContext); + } - GUI.drawList( - renderer, Rect{contentX, contentTop, contentWidth, contentHeight}, visibleItemCount, - static_cast(selectedIndex), - [this](int index) { return std::string(I18N.get(menuNames[itemForVisibleIndex(index)])); }, nullptr, nullptr, - [this](int index) -> std::string { return valueTextForItem(itemForVisibleIndex(index)); }, true); + uiReady = false; + app.render(); + uiReady = true; // Draw button hints GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); diff --git a/src/activities/settings/StatusBarSettingsActivity.h b/src/activities/settings/StatusBarSettingsActivity.h index 249aeed566..9e22b93736 100644 --- a/src/activities/settings/StatusBarSettingsActivity.h +++ b/src/activities/settings/StatusBarSettingsActivity.h @@ -1,10 +1,16 @@ #pragma once +#include +#include #include #include #include +#include + #include "activities/Activity.h" #include "components/OptionPopup.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "util/ButtonNavigator.h" // Reader status bar configuration activity @@ -14,7 +20,9 @@ class StatusBarSettingsActivity final : public Activity { bool stablePageNumbersAvailable = false) : Activity("StatusBarSettings", renderer, mappedInput), readerContext(readerContext), - stablePageNumbersAvailable(stablePageNumbersAvailable) {} + stablePageNumbersAvailable(stablePageNumbersAvailable), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} void onEnter() override; void onExit() override; @@ -30,8 +38,19 @@ class StatusBarSettingsActivity final : public Activity { bool readerContext = false; bool stablePageNumbersAvailable = false; + using UiApp = freeink::ui::FreeInkApp<20, 4>; + static constexpr freeink::ui::ActionId ACTION_ROW = 1; + freeink::ui::GfxRendererTarget uiTarget; // Must precede app: the app holds a reference to it. + UiApp app; + std::atomic uiReady{false}; + int visibleRows = 1; + int topIndex = 0; + int itemForVisibleIndex(int visibleIndex) const; bool selectedItemUsesOptionMenu() const; void handleSelection(); void openOptionPicker(); + static void settingsScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildSettingsScreen(UiApp::ScreenType& screen); }; diff --git a/src/activities/util/BmpViewerActivity.cpp b/src/activities/util/BmpViewerActivity.cpp index dc1de12b3f..5ba8a58aa4 100644 --- a/src/activities/util/BmpViewerActivity.cpp +++ b/src/activities/util/BmpViewerActivity.cpp @@ -8,7 +8,7 @@ #include -#include "CrossPointSettings.h" +#include "CrossPointState.h" #include "Epub/converters/PngToFramebufferConverter.h" #include "components/UITheme.h" #include "fontIds.h" @@ -211,29 +211,12 @@ void BmpViewerActivity::onExit() { void BmpViewerActivity::doSetSleepCover() { GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - bool success = false; - HalFile inFile, outFile; - if (Storage.openFileForRead("BMP", filePath, inFile)) { - if (Storage.openFileForWrite("BMP", "/sleep.bmp", outFile)) { - char buffer[2048]; - int bytesRead; - success = true; - while ((bytesRead = inFile.read(buffer, sizeof(buffer))) > 0) { - if (outFile.write(buffer, bytesRead) != bytesRead) { - success = false; - break; - } - } - outFile.close(); - } - inFile.close(); - } - - if (success) { - SETTINGS.sleepScreen = CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM; - SETTINGS.saveToFile(); + APP_STATE.favoriteSleepImagePath = filePath; + if (APP_STATE.saveToFile()) { + LOG_INF("BmpViewer", "Pinned favorite sleep image: %s", filePath.c_str()); GUI.drawPopup(renderer, tr(STR_DONE)); } else { + LOG_ERR("BmpViewer", "Failed to save favorite sleep image path: %s", filePath.c_str()); GUI.drawPopup(renderer, tr(STR_FAILED_LOWER)); } @@ -245,11 +228,37 @@ void BmpViewerActivity::loop() { // Keep CPU awake/polling so 1st click works Activity::loop(); + auto openSibling = [this](const int delta) { + if (currentImageIndex < 0) { + return false; + } + const int nextIndex = currentImageIndex + delta; + if (siblingImages.size() <= 1 || nextIndex < 0 || nextIndex >= static_cast(siblingImages.size())) { + return false; + } + currentImageIndex = nextIndex; + std::string dirPath = FsHelpers::extractFolderPath(filePath); + if (dirPath.back() != '/') dirPath += "/"; + filePath = dirPath + siblingImages[currentImageIndex]; + onEnter(); + return true; + }; + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { activityManager.goToFileBrowser(filePath); return; } + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Left) { + openSibling(1); + return; + } + if (swipe == MappedInputManager::SwipeDir::Right) { + openSibling(-1); + return; + } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (FsHelpers::hasBmpExtension(filePath)) { doSetSleepCover(); @@ -259,26 +268,13 @@ void BmpViewerActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Left) || mappedInput.wasReleased(MappedInputManager::Button::Up)) { - if (siblingImages.size() > 1 && currentImageIndex > 0) { - currentImageIndex--; - std::string dirPath = FsHelpers::extractFolderPath(filePath); - if (dirPath.back() != '/') dirPath += "/"; - filePath = dirPath + siblingImages[currentImageIndex]; - onEnter(); - } + openSibling(-1); return; } if (mappedInput.wasReleased(MappedInputManager::Button::Right) || mappedInput.wasReleased(MappedInputManager::Button::Down)) { - if (siblingImages.size() > 1 && currentImageIndex != -1 && - currentImageIndex < static_cast(siblingImages.size()) - 1) { - currentImageIndex++; - std::string dirPath = FsHelpers::extractFolderPath(filePath); - if (dirPath.back() != '/') dirPath += "/"; - filePath = dirPath + siblingImages[currentImageIndex]; - onEnter(); - } + openSibling(1); return; } } diff --git a/src/activities/util/ConfirmationActivity.cpp b/src/activities/util/ConfirmationActivity.cpp index 00bc5d1e69..ced6811404 100644 --- a/src/activities/util/ConfirmationActivity.cpp +++ b/src/activities/util/ConfirmationActivity.cpp @@ -2,75 +2,49 @@ #include -#include - -#include "HalDisplay.h" #include "components/UITheme.h" +#include "fontIds.h" ConfirmationActivity::ConfirmationActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& heading, const std::string& body, - bool ignoreInitialConfirmRelease) + bool ignoreInitialConfirmRelease, bool overrideDisabledReaderTouchscreen) : Activity("Confirmation", renderer, mappedInput), - heading(heading), - body(body), - ignoreConfirmRelease(ignoreInitialConfirmRelease) {} + ignoreConfirmRelease(ignoreInitialConfirmRelease), + overrideDisabledReaderTouchscreen(overrideDisabledReaderTouchscreen) { + popupTitle.reserve(heading.size() + body.size() + 1); + popupTitle = heading; + if (!heading.empty() && !body.empty()) { + popupTitle += ' '; + } + popupTitle += body; +} void ConfirmationActivity::onEnter() { Activity::onEnter(); - - const auto& metrics = UITheme::getInstance().getMetrics(); - lineHeight = renderer.getLineHeight(fontId); - const int maxWidth = renderer.getScreenWidth() - (margin * 2); - const int contentTop = margin; - const int contentBottom = renderer.getScreenHeight() - metrics.buttonHintsHeight - margin; - const int contentHeight = contentBottom - contentTop; - const int maxTotalLines = std::max(1, contentHeight / lineHeight); - - if (!heading.empty()) { - const int headingLineCap = body.empty() ? maxTotalLines : std::min(2, std::max(1, maxTotalLines - 1)); - headingLines = renderer.wrappedText(fontId, heading.c_str(), maxWidth, headingLineCap, EpdFontFamily::BOLD); - } - int totalHeight = 0; - if (!headingLines.empty()) totalHeight += static_cast(headingLines.size()) * lineHeight; - if (!body.empty()) { - const int bodyGap = headingLines.empty() ? 0 : spacing; - const int bodyHeight = std::max(lineHeight, contentHeight - totalHeight - bodyGap); - const int bodyLineCap = std::max(1, bodyHeight / lineHeight); - bodyLines = renderer.wrappedText(fontId, body.c_str(), maxWidth, bodyLineCap, EpdFontFamily::REGULAR); - if (!bodyLines.empty()) { - totalHeight += bodyGap + static_cast(bodyLines.size()) * lineHeight; - } + if (overrideDisabledReaderTouchscreen) { + mappedInput.setReaderTouchscreenOverride(true); } - // Center the dialog text inside the space above the bottom button hints. - startY = contentTop + std::max(0, (contentHeight - totalHeight) / 2); + const char* options[] = {I18N.get(StrId::STR_CANCEL), I18N.get(StrId::STR_CONFIRM)}; + confirmPopup.show(popupTitle.c_str(), options, 2, 0, [this](int idx) { + ActivityResult res; + res.isCancelled = (idx != 1); + setResult(std::move(res)); + finish(); + }); requestUpdate(true); } -void ConfirmationActivity::render(RenderLock&& lock) { - renderer.clearScreen(); - - int currentY = startY; - for (const auto& line : headingLines) { - renderer.drawCenteredText(fontId, currentY, line.c_str(), true, EpdFontFamily::BOLD); - currentY += lineHeight; - } - - if (!headingLines.empty() && !bodyLines.empty()) { - currentY += spacing; +void ConfirmationActivity::onExit() { + if (overrideDisabledReaderTouchscreen) { + mappedInput.setReaderTouchscreenOverride(false); } + Activity::onExit(); +} - for (const auto& line : bodyLines) { - renderer.drawCenteredText(fontId, currentY, line.c_str(), true, EpdFontFamily::REGULAR); - currentY += lineHeight; - } - - // Draw UI Elements - const auto labels = mappedInput.mapLabels(I18N.get(StrId::STR_CANCEL), I18N.get(StrId::STR_CONFIRM), "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - - renderer.displayBuffer(HalDisplay::RefreshMode::FAST_REFRESH); +void ConfirmationActivity::render(RenderLock&&) { + if (confirmPopup.processRender(renderer, mappedInput)) return; } void ConfirmationActivity::loop() { @@ -82,19 +56,11 @@ void ConfirmationActivity::loop() { } } - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - ActivityResult res; - res.isCancelled = false; - setResult(std::move(res)); - finish(); - return; - } + if (confirmPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return; - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult res; - res.isCancelled = true; - setResult(std::move(res)); - finish(); - return; - } + // Popup dismissed without a selection (Back button or tap outside): cancel. + ActivityResult res; + res.isCancelled = true; + setResult(std::move(res)); + finish(); } diff --git a/src/activities/util/ConfirmationActivity.h b/src/activities/util/ConfirmationActivity.h index 99e1c42359..26d234adc4 100644 --- a/src/activities/util/ConfirmationActivity.h +++ b/src/activities/util/ConfirmationActivity.h @@ -1,32 +1,24 @@ #pragma once #include #include -#include #include "activities/Activity.h" -#include "fontIds.h" +#include "components/OptionPopup.h" class ConfirmationActivity : public Activity { private: - // Input data - std::string heading; - std::string body; - - const int margin = 20; - const int spacing = 30; - const int fontId = UI_10_FONT_ID; - - std::vector headingLines; - std::vector bodyLines; - int startY = 0; - int lineHeight = 0; + std::string popupTitle; + OptionPopup confirmPopup; bool ignoreConfirmRelease = false; + bool overrideDisabledReaderTouchscreen = false; public: ConfirmationActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& heading, - const std::string& body, bool ignoreInitialConfirmRelease = false); + const std::string& body, bool ignoreInitialConfirmRelease = false, + bool overrideDisabledReaderTouchscreen = false); void onEnter() override; + void onExit() override; void loop() override; void render(RenderLock&& lock) override; bool allowPowerAsConfirmInReaderMode() const override { return true; } diff --git a/src/activities/util/FrontlightPanelActivity.cpp b/src/activities/util/FrontlightPanelActivity.cpp new file mode 100644 index 0000000000..64096c1cf2 --- /dev/null +++ b/src/activities/util/FrontlightPanelActivity.cpp @@ -0,0 +1,309 @@ +#include "FrontlightPanelActivity.h" + +#include +#include +#include + +#include +#include + +#include "CrossPointSettings.h" +#include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" +#include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" +#include "components/icons/frontlightHeaderIcons.h" + +namespace fui = freeink::ui; + +namespace { +constexpr fui::ActionId ACTION_BRIGHTNESS = 1; +constexpr fui::ActionId ACTION_WARMTH = 2; +constexpr fui::ActionId ACTION_TOGGLE = 3; +constexpr int BRIGHTNESS_STEP = 5; +constexpr int SETTINGS_ICON_SIZE = 28; +constexpr int SETTINGS_BUTTON_WIDTH = 64; +constexpr int SETTINGS_BUTTON_HEIGHT = 56; +constexpr int SETTINGS_BUTTON_RIGHT_INSET = 8; +constexpr int SETTINGS_BUTTON_DOWN_OFFSET = 5; + +uint8_t percentFromPermille(const int16_t permille) { + int value = (static_cast(permille) * 100 + 500) / 1000; + if (value < 0) value = 0; + if (value > 100) value = 100; + return static_cast(value); +} +} // namespace + +FrontlightPanelActivity::FrontlightPanelActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("FrontlightPanel", renderer, mappedInput), + uiTarget(makeUiTarget(renderer)), + app(uiTarget, uiTarget.deviceContext()) {} + +void FrontlightPanelActivity::onEnter() { + Activity::onEnter(); + + // The HAL is the live source of truth; SETTINGS only mirrors it for boot. + brightness = Frontlight.brightness(); + warmth = Frontlight.warmth(); + lightOn = Frontlight.isOn(); + + uiReady = false; + app.setTheme(uiThemeTokens(uiTarget)); + app.on(ACTION_BRIGHTNESS, &FrontlightPanelActivity::onBrightnessEvent, this); + app.on(ACTION_WARMTH, &FrontlightPanelActivity::onWarmthEvent, this); + app.on(ACTION_TOGGLE, &FrontlightPanelActivity::onToggleEvent, this); + app.setScreen(&FrontlightPanelActivity::panelScreen, this); + requestUpdate(); +} + +void FrontlightPanelActivity::onExit() { + // Debounced persistence: one SPIFFS write on close, never per slider tick. + if (SETTINGS.frontlightBrightness != brightness || SETTINGS.frontlightWarmth != warmth || + SETTINGS.frontlightOn != (lightOn ? 1 : 0)) { + SETTINGS.frontlightBrightness = brightness; + SETTINGS.frontlightWarmth = warmth; + SETTINGS.frontlightOn = lightOn ? 1 : 0; + SETTINGS.saveToFile(); + } + Activity::onExit(); +} + +void FrontlightPanelActivity::onBrightnessEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.dragPermille < 0) return; + self->brightness = percentFromPermille(event.dragPermille); + Frontlight.setBrightness(self->brightness); + // Adjusting brightness while off is an obvious "I want light" intent. + if (!self->lightOn) { + self->lightOn = true; + Frontlight.setOn(true); + } +} + +void FrontlightPanelActivity::onWarmthEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.dragPermille < 0) return; + self->warmth = percentFromPermille(event.dragPermille); + Frontlight.setWarmth(self->warmth); +} + +void FrontlightPanelActivity::onToggleEvent(const fui::ActionEvent&, void* user) { + static_cast(user)->toggleLight(); +} + +void FrontlightPanelActivity::adjustBrightness(const int delta) { + int next = static_cast(brightness) + delta; + if (next < 0) next = 0; + if (next > 100) next = 100; + if (next == brightness) return; + brightness = static_cast(next); + Frontlight.setBrightness(brightness); + if (!lightOn) { + lightOn = true; + Frontlight.setOn(true); + } + requestUpdate(); +} + +void FrontlightPanelActivity::toggleLight() { + lightOn = !lightOn; + Frontlight.setOn(lightOn); + requestUpdate(); +} + +void FrontlightPanelActivity::close() { finish(); } + +void FrontlightPanelActivity::openSettings() { + // Replace the overlay rather than leaving it on the stack. The Settings + // activity's swipe-up dismissal then returns directly to Home. + activityManager.goToSettings(true); +} + +bool FrontlightPanelActivity::handleHomeGesture() { + close(); + return true; +} + +void FrontlightPanelActivity::loop() { + // The X4 Pro power-button double-click can change the light globally while + // this panel is open. Keep the panel's state and eventual persistence in sync. + if (lightOn != Frontlight.isOn()) { + lightOn = Frontlight.isOn(); + requestUpdate(); + } + + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + close(); + return; + } + + const Rect settingsButton = settingsButtonRect(); + if (mappedInput.wasTapInRect(settingsButton.x, settingsButton.y, settingsButton.width, settingsButton.height)) { + openSettings(); + return; + } + + fui::InputSnapshot snap{}; + if (uiReady) { + snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchHeld || snap.touchReleased) { + const auto event = app.route(snap); + if (app.invalidated()) requestUpdate(); + if (event) { + if (event.dragPermille >= 0) draggingSlider = true; + return; + } + // No control took it. A tap released at/below the panel edge (in the + // preserved content underneath) dismisses — the drop-down behaves like a + // modal scrim. A drag-off release carries y = -1, so it never dismisses. + if (snap.touchReleased && !draggingSlider && snap.touchY >= panelBottom) { + close(); + return; + } + } + if (draggingSlider) { + // Drag ended (possibly off the slider): swallow the release's swipe so + // it can't double as the left-edge back gesture and close the panel. + if (!snap.touchHeld) draggingSlider = false; + return; + } + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + close(); + return; + } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + toggleLight(); + return; + } + + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, + [this] { adjustBrightness(-BRIGHTNESS_STEP); }); + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, + [this] { adjustBrightness(BRIGHTNESS_STEP); }); +} + +Rect FrontlightPanelActivity::settingsButtonRect() const { + const auto& metrics = UITheme::getInstance().getMetrics(); + const int buttonHeight = std::min(SETTINGS_BUTTON_HEIGHT, TouchHeaderBackButton::height(metrics, mappedInput)); + return Rect{renderer.getScreenWidth() - SETTINGS_BUTTON_RIGHT_INSET - SETTINGS_BUTTON_WIDTH, + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) - buttonHeight + + SETTINGS_BUTTON_DOWN_OFFSET, + SETTINGS_BUTTON_WIDTH, buttonHeight}; +} + +int FrontlightPanelActivity::computePanelBottom() const { + // Mirror buildPanelScreen's takeTop/spacer sequence so the frame, content + // margin, and dismiss threshold land on the same edge. + const auto& metrics = UITheme::getInstance().getMetrics(); + const auto tokens = uiThemeTokens(uiTarget); + const int16_t lh = uiTarget.lineHeight(tokens.bodyText.font); + int y = metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput); + y += tokens.spaceLg; // leading spacer + y += tokens.rowHeight + tokens.spaceSm; // brightness label + sun toggle row + y += tokens.rowHeight + tokens.spaceLg; // brightness slider + if (Frontlight.hasColorTemperature()) { + y += lh + tokens.spaceSm + tokens.rowHeight + tokens.spaceLg; // warmth label + slider + } + y += tokens.spaceLg; // trailing padding + return y; +} + +void FrontlightPanelActivity::panelScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildPanelScreen(screen); +} + +void FrontlightPanelActivity::buildPanelScreen(UiApp::ScreenType& screen) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const auto& theme = screen.theme(); + // Body spans from below the header down to the drop-down's bottom edge, so + // the app never lays out over the preserved content underneath. + const int16_t bottomInset = static_cast(renderer.getScreenHeight() - panelBottom); + screen.setContentMargin( + fui::Insets{static_cast(metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput)), 0, + bottomInset, 0}); + + const int16_t lh = screen.target().lineHeight(theme.bodyText.font); + const int16_t rowH = theme.rowHeight; + const fui::Insets sideInset{0, static_cast(theme.spaceLg * 2), 0, static_cast(theme.spaceLg * 2)}; + char line[48]; + + screen.spacer(theme.spaceLg); + + // Header row: "Brightness NN%" on the left, a tappable sun icon on the right + // that toggles the light — bright `sun` when on, `sun-dim` when off (the icon + // itself is the state indicator). Sharing a row with the label frees the + // whole bottom toggle row, shrinking the panel. + const fui::Rect headerRow = screen.takeTop(rowH, theme.spaceSm).inset(sideInset); + snprintf(line, sizeof(line), "%s %u%%", tr(STR_BRIGHTNESS), static_cast(brightness)); + const fui::BitmapRef sunIcon = fui::bitmapFromIcon(lightOn ? icon_sun_32 : icon_sun_dim_32); + const int16_t iconW = static_cast(sunIcon.width); + const int16_t iconH = static_cast(sunIcon.height); + const fui::Rect iconRect{static_cast(headerRow.x + headerRow.width - iconW), + static_cast(headerRow.y + (rowH - iconH) / 2), iconW, iconH}; + const fui::Rect labelRect{headerRow.x, static_cast(headerRow.y + (rowH - lh) / 2), + static_cast(headerRow.width - iconW - theme.spaceMd), lh}; + screen.target().text(labelRect, line, theme.bodyText); + // Generous hit target: the full header-row height and a wide band on the right + // (well beyond the 32px glyph) so the toggle is easy to hit. Stays within the + // header row so it never steals taps from the brightness slider below. + const int16_t hitW = static_cast(iconW + theme.spaceLg * 4); + const fui::Rect hitRect{static_cast(headerRow.right() - hitW), headerRow.y, hitW, rowH}; + screen.frame().hit(hitRect, ACTION_TOGGLE); + screen.target().bitmap(iconRect, sunIcon, fui::BitmapMode::Center); + + fui::SliderProps brightnessSlider; + brightnessSlider.value = brightness; + brightnessSlider.max = 100; + brightnessSlider.action = ACTION_BRIGHTNESS; + brightnessSlider.inputMask = fui::InputTouch | fui::InputDrag; + fui::slider(screen.frame(), screen.takeTop(theme.rowHeight, theme.spaceLg).inset(sideInset), brightnessSlider); + + if (Frontlight.hasColorTemperature()) { + snprintf(line, sizeof(line), "%s %u%%", tr(STR_WARMTH), static_cast(warmth)); + screen.target().text(screen.takeTop(lh, theme.spaceSm).inset(sideInset), line, theme.bodyText); + fui::SliderProps warmthSlider; + warmthSlider.value = warmth; + warmthSlider.max = 100; + warmthSlider.action = ACTION_WARMTH; + warmthSlider.inputMask = fui::InputTouch | fui::InputDrag; + fui::slider(screen.frame(), screen.takeTop(theme.rowHeight, theme.spaceLg).inset(sideInset), warmthSlider); + } + + screen.spacer(theme.spaceLg); +} + +void FrontlightPanelActivity::render(RenderLock&&) { + // Overlay drop-down: keep the framebuffer content (the reader/menu we opened + // over) intact below the panel; only the top third is repainted. No full + // clearScreen — same as the theme popups draw over the current frame. + panelBottom = computePanelBottom(); + const int pageWidth = renderer.getScreenWidth(); + renderer.fillRect(0, 0, pageWidth, panelBottom, false); // white the panel area only + + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect header{0, metrics.topPadding, pageWidth, TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget, header, tr(STR_FRONTLIGHT), false, SETTINGS_ICON_SIZE + 24); + } else { + GUI.drawHeader(renderer, header, tr(STR_FRONTLIGHT)); + } + const Rect settingsButton = settingsButtonRect(); + uiTarget.bitmap(fui::Rect{static_cast(settingsButton.x + (settingsButton.width - SETTINGS_ICON_SIZE) / 2), + static_cast(settingsButton.y + (settingsButton.height - SETTINGS_ICON_SIZE) / 2), + SETTINGS_ICON_SIZE, SETTINGS_ICON_SIZE}, + fui::bitmapFromIcon(icon_sliders_horizontal_28), fui::BitmapMode::Center); + + uiReady = false; + app.render(); + uiReady = true; + + // Bottom edge of the drop-down: a solid rule separating it from the content + // (the scrim) underneath, reinforcing that a tap below dismisses. + renderer.fillRect(0, panelBottom - 2, pageWidth, 2, true); + + renderer.displayBuffer(); +} diff --git a/src/activities/util/FrontlightPanelActivity.h b/src/activities/util/FrontlightPanelActivity.h new file mode 100644 index 0000000000..a609848cea --- /dev/null +++ b/src/activities/util/FrontlightPanelActivity.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +#include + +#include "activities/Activity.h" +#include "util/ButtonNavigator.h" + +struct Rect; + +// Frontlight quick panel (boards with a frontlight + home key, e.g. X4 Pro). +// Opened globally by the top-edge down-swipe: a top-anchored drop-down (only the +// upper third of the screen) with brightness and warmth sliders driving the +// light live, plus an on/off toggle with a sun icon. It renders as an overlay — +// the content underneath stays on screen below the panel, and a tap there +// dismisses it. State is persisted once on exit (SPIFFS write throttling). +class FrontlightPanelActivity final : public Activity { + using UiApp = freeink::ui::FreeInkApp<8, 4>; + + ButtonNavigator buttonNavigator; + + freeink::ui::GfxRendererTarget uiTarget; // must precede `app`: the app holds a reference to it + UiApp app; + std::atomic uiReady{false}; + + uint8_t brightness = 60; + uint8_t warmth = 50; + bool lightOn = false; + // Swallow the swipe/tap fallout of a slider drag so its release can't + // trigger the back gesture and close the panel mid-adjustment. + bool draggingSlider = false; + // Bottom edge of the drop-down (px). Content lays out above it; a tap at or + // below it dismisses the panel. Set by render() before the app lays out. + int panelBottom = 0; + + static void panelScreen(UiApp::ScreenType& screen, void* user); + static void onBrightnessEvent(const freeink::ui::ActionEvent& event, void* user); + static void onWarmthEvent(const freeink::ui::ActionEvent& event, void* user); + static void onToggleEvent(const freeink::ui::ActionEvent& event, void* user); + void buildPanelScreen(UiApp::ScreenType& screen); + Rect settingsButtonRect() const; + // Height of the drop-down, derived from the content it holds (header + + // sliders + toggle). Same layout math as buildPanelScreen so the frame, + // content margin, and dismiss threshold all agree. + int computePanelBottom() const; + void adjustBrightness(int delta); + void toggleLight(); + void close(); + void openSettings(); + + public: + explicit FrontlightPanelActivity(GfxRenderer& renderer, MappedInputManager& mappedInput); + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; + // The home key closes the panel back to whatever was underneath. + bool handleHomeGesture() override; +}; diff --git a/src/activities/util/IntervalSelectionActivity.cpp b/src/activities/util/IntervalSelectionActivity.cpp index 06b6be6da8..aa61e757c2 100644 --- a/src/activities/util/IntervalSelectionActivity.cpp +++ b/src/activities/util/IntervalSelectionActivity.cpp @@ -8,10 +8,34 @@ #include #include +#include "DeviceCapabilities.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" namespace { +constexpr int TOUCH_STEP_BUTTON_SIZE = 56; +constexpr int TOUCH_STEP_BUTTON_GAP = 32; +constexpr int TOUCH_ACTION_BUTTON_WIDTH = 120; +constexpr int TOUCH_ACTION_BUTTON_HEIGHT = 48; + +Rect touchStepButtonRect(const Rect& screen, const int index) { + const int totalWidth = TOUCH_STEP_BUTTON_SIZE * 4 + TOUCH_STEP_BUTTON_GAP * 3; + const int x = screen.x + (screen.width - totalWidth) / 2 + index * (TOUCH_STEP_BUTTON_SIZE + TOUCH_STEP_BUTTON_GAP); + return Rect{x, 220, TOUCH_STEP_BUTTON_SIZE, TOUCH_STEP_BUTTON_SIZE}; +} + +Rect touchActionButtonRect(const Rect& screen, const bool confirm) { + constexpr int sideMargin = 46; + return Rect{confirm ? screen.x + screen.width - sideMargin - TOUCH_ACTION_BUTTON_WIDTH : screen.x + sideMargin, + screen.y + screen.height - TOUCH_ACTION_BUTTON_HEIGHT - 28, TOUCH_ACTION_BUTTON_WIDTH, + TOUCH_ACTION_BUTTON_HEIGHT}; +} + +bool contains(const Rect& rect, const int x, const int y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} + void formatCompactSeconds(const int seconds, char* buf, const size_t len) { if (seconds < 60) { snprintf(buf, len, "%ds", seconds); @@ -29,10 +53,20 @@ int IntervalSelectionActivity::clampedValue(const int candidate) const { void IntervalSelectionActivity::onEnter() { Activity::onEnter(); + if (overrideDisabledReaderTouchscreen) { + mappedInput.setReaderTouchscreenOverride(true); + } value = clampedValue(value); requestUpdate(); } +void IntervalSelectionActivity::onExit() { + if (overrideDisabledReaderTouchscreen) { + mappedInput.setReaderTouchscreenOverride(false); + } + Activity::onExit(); +} + void IntervalSelectionActivity::adjustValue(const int delta) { value = clampedValue(value + delta); requestUpdate(); @@ -59,6 +93,63 @@ void IntervalSelectionActivity::loop() { } } + if (showTouchHeaderBackButton && TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return; + } + + int tx = 0; + int ty = 0; + const int screenWidth = renderer.getScreenWidth(); + const Rect touchScreen = UITheme::getInstance().getScreenSafeArea(renderer, !mappedInput.hasTouchHardware(), false); + const int barWidth = std::min(360, std::max(0, screenWidth - 40)); + constexpr int barHeight = 16; + const int barX = std::max(0, (screenWidth - barWidth) / 2); + const int barY = 140; + + // Live drag on the slider: once a touch lands on the bar, the value follows the + // finger until release. Runs before the Back/Confirm handlers because the release + // of a drag can also register as a swipe (e.g. the left-edge rightward back + // gesture) — the drag must consume it so it can't cancel or confirm the dialog. + if (mappedInput.isScreenTouchHeld(tx, ty)) { + if (draggingBar || (ty >= barY - 20 && ty < barY + barHeight + 20 && tx >= barX && tx < barX + barWidth)) { + draggingBar = true; + const int range = std::max(1, maxValue - minValue); + const int dragged = + clampedValue(minValue + std::clamp(tx - barX, 0, barWidth - 1) * range / std::max(1, barWidth - 1)); + if (dragged != value) { + value = dragged; + requestUpdate(); + } + return; + } + } else if (draggingBar) { + // Release frame of a drag: swallow the tap/swipe events it produced. + draggingBar = false; + return; + } + + // Cancel and Confirm act on touch-down. Unlike the adjustment controls, these + // are terminal actions, so waiting for a release can make a perfectly still + // tap feel ignored while the controller settles its release event. + if (mappedInput.hasTouch() && mappedInput.wasScreenTouchDown(tx, ty)) { + if (contains(touchActionButtonRect(touchScreen, false), tx, ty)) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return; + } + if (contains(touchActionButtonRect(touchScreen, true), tx, ty)) { + setResult(IntervalResult{static_cast(value)}); + finish(); + return; + } + } + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { ActivityResult result; result.isCancelled = true; @@ -73,14 +164,48 @@ void IntervalSelectionActivity::loop() { return; } + if (mappedInput.wasScreenTapped(tx, ty)) { + if (ty >= barY - 20 && ty < barY + barHeight + 20 && tx >= barX && tx < barX + barWidth) { + const int range = std::max(1, maxValue - minValue); + value = clampedValue(minValue + (tx - barX) * range / std::max(1, barWidth - 1)); + requestUpdate(); + return; + } + + if (mappedInput.hasTouch()) { + for (int index = 0; index < 4; ++index) { + if (!contains(touchStepButtonRect(touchScreen, index), tx, ty)) continue; + constexpr int deltas[] = {-1, -1, 1, 1}; + const int step = (index == 0 || index == 3) ? largeStep : smallStep; + adjustValue(deltas[index] * step); + return; + } + + return; + } + + if (ty >= renderer.getScreenHeight() - 80) { + if (tx < screenWidth / 3) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + } else if (tx > screenWidth * 2 / 3) { + setResult(IntervalResult{static_cast(value)}); + finish(); + } + return; + } + } + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustValue(-smallStep); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustValue(smallStep); }); - // On X3 the side buttons sit on the left/right edges of the screen rather than as a vertical up/down - // rocker (X4), so BTN_UP is physically the left button and BTN_DOWN the right one. Flip the large-step - // direction there so the left button decreases and the right button increases, matching the layout. - const int upDelta = gpio.deviceIsX3() ? -largeStep : largeStep; - const int downDelta = gpio.deviceIsX3() ? largeStep : -largeStep; + // On edge-button boards (X3, X4 Pro) the side buttons sit on the left/right edges of the screen rather + // than as a vertical up/down rocker (X4), so BTN_UP is physically the left button and BTN_DOWN the right + // one. Flip the large-step direction there so the left button decreases and the right button increases. + const int upDelta = deviceHasEdgeSideButtons(gpio) ? -largeStep : largeStep; + const int downDelta = deviceHasEdgeSideButtons(gpio) ? largeStep : -largeStep; buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this, upDelta] { adjustValue(upDelta); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this, downDelta] { adjustValue(downDelta); }); @@ -91,9 +216,21 @@ void IntervalSelectionActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const int screenWidth = renderer.getScreenWidth(); + const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, !mappedInput.hasTouchHardware(), false); + const Rect touchScreen = safe; - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, screenWidth, metrics.headerHeight}, I18N.get(titleId), nullptr, - readerActivity); + // The Auto Page Turn picker has no touch back button. Keep its title in the + // normal-height header rather than centering it in the taller touch-navigation + // header, which puts the title against the divider. + Rect header = showTouchHeaderBackButton ? TouchHeaderBackButton::headerRect(renderer, mappedInput) + : TouchHeaderBackButton::standardHeaderRect(renderer); + header.x = safe.x; + header.width = safe.width; + if (showTouchHeaderBackButton && mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, I18N.get(titleId), readerActivity); + } else { + GUI.drawHeader(renderer, header, I18N.get(titleId), nullptr, readerActivity); + } char formattedValue[32]; if (maxBoundaryLabelId != StrId::STR_NONE_OPT && value == maxValue) { @@ -125,14 +262,56 @@ void IntervalSelectionActivity::render(RenderLock&&) { const int knobX = std::max(barX + 2, barX + 2 + fillWidth - 2); renderer.fillRect(knobX, barY - 4, 4, barHeight + 8, true); - // Two-line step hint: front buttons do the small step, side buttons the large step. Built from - // separate label + value strings (rather than splitting one localized sentence) so the layout - // doesn't depend on translators preserving a hidden separator. - drawStepHintLine(barY + 30, StrId::STR_STEP_HINT_FRONT, smallStep); - drawStepHintLine(barY + 52, StrId::STR_STEP_HINT_SIDE, largeStep); + if (mappedInput.hasTouch()) { + auto drawButton = [&](const Rect& rect) { + renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::White); + renderer.drawRect(rect.x, rect.y, rect.width, rect.height, true); + }; + auto drawChevron = [&](const Rect& rect, const bool pointsRight, const bool doubleChevron) { + const int centreY = rect.y + rect.height / 2; + const int halfHeight = 12; + const int firstX = rect.x + (doubleChevron ? 13 : 20); + const int spacing = 14; + const int chevronCount = doubleChevron ? 2 : 1; + for (int i = 0; i < chevronCount; ++i) { + const int x = firstX + i * spacing; + if (pointsRight) { + renderer.drawLine(x, centreY - halfHeight, x + 12, centreY, 2, true); + renderer.drawLine(x + 12, centreY, x, centreY + halfHeight, 2, true); + } else { + renderer.drawLine(x + 12, centreY - halfHeight, x, centreY, 2, true); + renderer.drawLine(x, centreY, x + 12, centreY + halfHeight, 2, true); + } + } + }; + + for (int index = 0; index < 4; ++index) { + const Rect rect = touchStepButtonRect(touchScreen, index); + drawButton(rect); + drawChevron(rect, index >= 2, index == 0 || index == 3); + } + + const Rect cancelRect = touchActionButtonRect(touchScreen, false); + const Rect confirmRect = touchActionButtonRect(touchScreen, true); + drawButton(cancelRect); + drawButton(confirmRect); + auto drawButtonLabel = [&](const Rect& rect, const char* label) { + const int x = rect.x + (rect.width - renderer.getTextWidth(UI_10_FONT_ID, label)) / 2; + const int y = rect.y + (rect.height - renderer.getLineHeight(UI_10_FONT_ID)) / 2; + renderer.drawText(UI_10_FONT_ID, x, y, label); + }; + drawButtonLabel(cancelRect, tr(STR_CANCEL)); + drawButtonLabel(confirmRect, tr(STR_CONFIRM)); + } else { + // Two-line step hint: front buttons do the small step, side buttons the large step. Built from + // separate label + value strings (rather than splitting one localized sentence) so the layout + // doesn't depend on translators preserving a hidden separator. + drawStepHintLine(barY + 30, StrId::STR_STEP_HINT_FRONT, smallStep); + drawStepHintLine(barY + 52, StrId::STR_STEP_HINT_SIDE, largeStep); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "-", "+"); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, readerActivity); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "-", "+"); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, readerActivity); + } renderer.displayBuffer(); } diff --git a/src/activities/util/IntervalSelectionActivity.h b/src/activities/util/IntervalSelectionActivity.h index 4ca3e198a5..f39d27290b 100644 --- a/src/activities/util/IntervalSelectionActivity.h +++ b/src/activities/util/IntervalSelectionActivity.h @@ -15,7 +15,9 @@ class IntervalSelectionActivity final : public Activity { int largeStep, StrId valueFormatId = StrId::STR_NONE_OPT, bool readerActivity = false, bool allowPowerAsConfirm = false, bool ignoreInitialConfirmRelease = false, bool showPercentValue = false, - StrId maxBoundaryLabelId = StrId::STR_NONE_OPT) + StrId maxBoundaryLabelId = StrId::STR_NONE_OPT, + bool overrideDisabledReaderTouchscreen = false, + bool showTouchHeaderBackButton = false) : Activity(activityName, renderer, mappedInput), titleId(titleId), valueFormatId(valueFormatId), @@ -28,9 +30,12 @@ class IntervalSelectionActivity final : public Activity { readerActivity(readerActivity), allowPowerAsConfirm(allowPowerAsConfirm), ignoreConfirmRelease(ignoreInitialConfirmRelease), - showPercentValue(showPercentValue) {} + showPercentValue(showPercentValue), + overrideDisabledReaderTouchscreen(overrideDisabledReaderTouchscreen), + showTouchHeaderBackButton(showTouchHeaderBackButton) {} void onEnter() override; + void onExit() override; void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return readerActivity; } @@ -49,6 +54,9 @@ class IntervalSelectionActivity final : public Activity { bool allowPowerAsConfirm; bool ignoreConfirmRelease; bool showPercentValue; + bool overrideDisabledReaderTouchscreen; + bool showTouchHeaderBackButton; + bool draggingBar = false; ButtonNavigator buttonNavigator; void adjustValue(int delta); diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 8b244f8f80..50f8ce7018 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -4,24 +4,142 @@ #include #include +#include +#include "DeviceCapabilities.h" #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" #include "fontIds.h" -const char* const KeyboardEntryActivity::shiftString[2] = {"shift", "SHIFT"}; +namespace fui = freeink::ui; + +namespace { + +constexpr fui::ActionId ACTION_KEY = 1; + +// --------------------------------------------------------------------------- +// URL layers. The SDK builtin layouts have no URL variant (":", "/", ".", the +// snippet panel), so these are app-defined tables over the same public +// KeyboardLayout structs. URLs are ASCII, so the letter rows are EN-arranged +// regardless of UI language. +// --------------------------------------------------------------------------- + +#define UK(label, output, value) \ + fui::KeyboardKey { label, output, fui::KeyKind::Normal, fui::StateNormal, value, 1, true, nullptr } +#define UKA(label, output, value, alt) \ + fui::KeyboardKey { label, output, fui::KeyKind::Normal, fui::StateNormal, value, 1, true, alt } +#define UKW(label, output, value, units) \ + fui::KeyboardKey { label, output, fui::KeyKind::Normal, fui::StateNormal, value, units, true, nullptr } +#define UKS(label, kind, value, units) \ + fui::KeyboardKey { label, nullptr, kind, fui::StateNormal, value, units, true, nullptr } + +constexpr int16_t URL_PANEL_VALUE = -3; // mirrors KeyboardEntryActivity::URL_PANEL_KEY + +const fui::KeyboardKey URL_NUM_ROW[] = {UKA("1", "1", '1', "!"), UKA("2", "2", '2', "@"), UKA("3", "3", '3', "#"), + UKA("4", "4", '4', "$"), UKA("5", "5", '5', "%"), UKA("6", "6", '6', "^"), + UKA("7", "7", '7', "&"), UKA("8", "8", '8', "*"), UKA("9", "9", '9', "("), + UKA("0", "0", '0', ")")}; + +const fui::KeyboardKey URL_ROW1[] = {UK("q", "q", 'q'), UK("w", "w", 'w'), UK("e", "e", 'e'), UK("r", "r", 'r'), + UK("t", "t", 't'), UK("y", "y", 'y'), UK("u", "u", 'u'), UK("i", "i", 'i'), + UK("o", "o", 'o'), UK("p", "p", 'p')}; +const fui::KeyboardKey URL_ROW2[] = {UK("a", "a", 'a'), UK("s", "s", 's'), UK("d", "d", 'd'), + UK("f", "f", 'f'), UK("g", "g", 'g'), UK("h", "h", 'h'), + UK("j", "j", 'j'), UK("k", "k", 'k'), UK("l", "l", 'l')}; +const fui::KeyboardKey URL_ROW3[] = {UKS("Shift", fui::KeyKind::Shift, fui::QWERTY_KEY_SHIFT, 2), + UK("z", "z", 'z'), + UK("x", "x", 'x'), + UK("c", "c", 'c'), + UK("v", "v", 'v'), + UK("b", "b", 'b'), + UK("n", "n", 'n'), + UK("m", "m", 'm'), + UKS("Del", fui::KeyKind::Delete, fui::QWERTY_KEY_BACKSPACE, 2)}; +// URLs have no spaces, so the URL bottom row spends the space slot on ":", +// "/", "." and the snippet-panel toggle instead (the legacy keyboard did the +// same with its "URL" key). +const fui::KeyboardKey URL_BOTTOM[] = {UKS("?123", fui::KeyKind::Mode, fui::QWERTY_KEY_MODE, 2), + UK(":", ":", ':'), + UK("/", "/", '/'), + UK(".", ".", '.'), + UKW("URL", nullptr, URL_PANEL_VALUE, 2), + UKS("OK", fui::KeyKind::Ok, fui::QWERTY_KEY_ENTER, 2)}; + +const fui::KeyboardKey URL_SHIFT_ROW1[] = {UK("Q", "Q", 'Q'), UK("W", "W", 'W'), UK("E", "E", 'E'), UK("R", "R", 'R'), + UK("T", "T", 'T'), UK("Y", "Y", 'Y'), UK("U", "U", 'U'), UK("I", "I", 'I'), + UK("O", "O", 'O'), UK("P", "P", 'P')}; +const fui::KeyboardKey URL_SHIFT_ROW2[] = {UK("A", "A", 'A'), UK("S", "S", 'S'), UK("D", "D", 'D'), + UK("F", "F", 'F'), UK("G", "G", 'G'), UK("H", "H", 'H'), + UK("J", "J", 'J'), UK("K", "K", 'K'), UK("L", "L", 'L')}; +const fui::KeyboardKey URL_SHIFT_ROW3[] = {UKS("Shift", fui::KeyKind::Shift, fui::QWERTY_KEY_SHIFT, 2), + UK("Z", "Z", 'Z'), + UK("X", "X", 'X'), + UK("C", "C", 'C'), + UK("V", "V", 'V'), + UK("B", "B", 'B'), + UK("N", "N", 'N'), + UK("M", "M", 'M'), + UKS("Del", fui::KeyKind::Delete, fui::QWERTY_KEY_BACKSPACE, 2)}; + +// Snippet keys: multi-character outputs, stable ids above the localized-key +// range so they never collide with layout key ids. +const fui::KeyboardKey URL_SNIP_ROW1[] = {UK("https://", "https://", 2001), UK("www.", "www.", 2002), + UK(".com", ".com", 2003)}; +const fui::KeyboardKey URL_SNIP_ROW2[] = {UK("http://", "http://", 2004), UK("192.168.", "192.168.", 2005), + UK(".org", ".org", 2006)}; +const fui::KeyboardKey URL_SNIP_ROW3[] = {UK("/opds", "/opds", 2007), UK(":8080", ":8080", 2008), + UK(".net", ".net", 2009)}; +const fui::KeyboardKey URL_SNIP_BOTTOM[] = {UKS("abc", fui::KeyKind::Mode, fui::QWERTY_KEY_MODE, 2), + UKW("URL", nullptr, URL_PANEL_VALUE, 2), + UKS("Del", fui::KeyKind::Delete, fui::QWERTY_KEY_BACKSPACE, 2), + UKS("OK", fui::KeyKind::Ok, fui::QWERTY_KEY_ENTER, 2)}; + +#undef UK +#undef UKA +#undef UKW +#undef UKS + +const fui::KeyboardRow URL_ROWS[] = { + {URL_NUM_ROW, 10, 0}, {URL_ROW1, 10, 0}, {URL_ROW2, 9, 1}, {URL_ROW3, 9, 0}, {URL_BOTTOM, 6, 0}}; +const fui::KeyboardRow URL_SHIFT_ROWS[] = { + {URL_NUM_ROW, 10, 0}, {URL_SHIFT_ROW1, 10, 0}, {URL_SHIFT_ROW2, 9, 1}, {URL_SHIFT_ROW3, 9, 0}, {URL_BOTTOM, 6, 0}}; +const fui::KeyboardRow URL_SNIP_ROWS[] = { + {URL_SNIP_ROW1, 3, 0}, {URL_SNIP_ROW2, 3, 0}, {URL_SNIP_ROW3, 3, 0}, {URL_SNIP_BOTTOM, 4, 0}}; + +const fui::KeyboardLayout URL_LAYOUT{URL_ROWS, 5}; +const fui::KeyboardLayout URL_SHIFT_LAYOUT{URL_SHIFT_ROWS, 5}; +const fui::KeyboardLayout URL_SNIPPET_LAYOUT{URL_SNIP_ROWS, 4}; + +fui::KeyboardLayoutId layoutForLanguage(const Language language) { + switch (language) { + case Language::FR: + return fui::KeyboardLayoutId::AzertyFr; + case Language::DE: + return fui::KeyboardLayoutId::QwertzDe; + case Language::ES: + return fui::KeyboardLayoutId::SpanishEs; + default: + return fui::KeyboardLayoutId::QwertyEn; + } +} + +} // namespace void KeyboardEntryActivity::onEnter() { Activity::onEnter(); cursorPos = text.length(); - symMode = false; - urlMode = false; + // URL layers are EN-arranged app tables; everything else follows the UI + // language. + layoutId = inputType == InputType::Url ? fui::KeyboardLayoutId::QwertyEn : layoutForLanguage(I18N.getLanguage()); + shifted = false; + symbols = false; + urlPanel = false; cursorMode = false; togglePos = false; passwordVisible = false; - shiftState = 0; - selectedRow = 0; - selectedCol = 0; + selRow = 0; + selCol = 0; delPressCount = 0; hintVisible = false; hintShowTime = 0; @@ -29,168 +147,425 @@ void KeyboardEntryActivity::onEnter() { rightLongHandled = false; savedCursorPos = 0; rightStartCursorPos = 0; + touchRouter.reset(); + touchRouter.holdMs = TOUCH_LONG_PRESS_MS; + touchRouter.overrideHoldMs = TOUCH_DEL_LONG_PRESS_MS; + interactionsReady = false; requestUpdate(); } void KeyboardEntryActivity::onExit() { Activity::onExit(); } -int KeyboardEntryActivity::getContentRowCount() const { - if (urlMode) return 3; - return ABC_ROWS; +const fui::KeyboardLayout& KeyboardEntryActivity::currentLayout() const { + if (symbols) return fui::builtinKeyboardLayout(layoutId, shifted, true); + if (inputType == InputType::Url) { + if (urlPanel) return URL_SNIPPET_LAYOUT; + return shifted ? URL_SHIFT_LAYOUT : URL_LAYOUT; + } + return fui::builtinKeyboardLayout(layoutId, shifted, false, /*numberRow=*/true); } -int KeyboardEntryActivity::getContentColCount() const { - if (urlMode) return 3; - return COLS; +const fui::KeyboardKey* KeyboardEntryActivity::selectedKey() const { + const fui::KeyboardLayout& layout = currentLayout(); + if (selRow < 0 || selRow >= layout.rowCount) return nullptr; + const fui::KeyboardRow& row = layout.rows[selRow]; + if (selCol < 0 || selCol >= row.count) return nullptr; + return &row.keys[selCol]; } -int KeyboardEntryActivity::getTotalRowCount() const { return getContentRowCount() + 1; } - -bool KeyboardEntryActivity::isBottomRow(const int row) const { return row == getContentRowCount(); } - -char KeyboardEntryActivity::getSelectedChar() const { - const KeyDef(*layout)[COLS] = symMode ? symLayout : (inputType == InputType::Url ? urlLayout : abcLayout); +int KeyboardEntryActivity::selectedLogicalIndex() const { + const fui::KeyboardLayout& layout = currentLayout(); + int index = 0; + for (int r = 0; r < selRow && r < layout.rowCount; r++) { + index += layout.rows[r].count; + } + return index + selCol; +} - if (selectedRow < 0 || selectedRow >= getContentRowCount()) return '\0'; - if (selectedCol < 0 || selectedCol >= COLS) return '\0'; +void KeyboardEntryActivity::clampSelection() { + const fui::KeyboardLayout& layout = currentLayout(); + if (layout.rowCount == 0) { + selRow = 0; + selCol = 0; + return; + } + if (selRow < 0) selRow = 0; + if (selRow >= layout.rowCount) selRow = layout.rowCount - 1; + const int cols = layout.rows[selRow].count; + if (selCol < 0) selCol = 0; + if (selCol >= cols) selCol = cols > 0 ? cols - 1 : 0; +} - const KeyDef& key = layout[selectedRow][selectedCol]; - return (shiftState > 0 && key.secondary != '\0') ? key.secondary : key.primary; +void KeyboardEntryActivity::moveSelectionRow(const int delta) { + const fui::KeyboardLayout& layout = currentLayout(); + if (layout.rowCount == 0) return; + const int oldCols = selRow < layout.rowCount ? layout.rows[selRow].count : 1; + selRow = (selRow + delta + layout.rowCount) % layout.rowCount; + const int newCols = layout.rows[selRow].count; + // Proportional column mapping keeps vertical travel intuitive between rows + // of different key counts (e.g. a 10-key letter row over a 6-key bottom row). + if (oldCols > 0 && newCols > 0 && oldCols != newCols) { + selCol = selCol * newCols / oldCols; + } + clampSelection(); } -char KeyboardEntryActivity::getAlternativeChar() const { - if (symMode || urlMode) return '\0'; - if (inputType == InputType::Url && selectedRow > 0) return '\0'; +void KeyboardEntryActivity::moveSelectionCol(const int delta) { + const fui::KeyboardLayout& layout = currentLayout(); + if (selRow < 0 || selRow >= layout.rowCount) return; + const int cols = layout.rows[selRow].count; + if (cols <= 0) return; + selCol = (selCol + delta + cols) % cols; +} - const KeyDef(*layout)[COLS] = abcLayout; +bool KeyboardEntryActivity::syncSelectionToValue(const int16_t value) { + const fui::KeyboardLayout& layout = currentLayout(); + for (int r = 0; r < layout.rowCount; r++) { + for (int c = 0; c < layout.rows[r].count; c++) { + if (layout.rows[r].keys[c].value == value) { + selRow = r; + selCol = c; + return true; + } + } + } + return false; +} - if (selectedRow < 0 || selectedRow >= getContentRowCount()) return '\0'; - if (selectedCol < 0 || selectedCol >= COLS) return '\0'; +size_t KeyboardEntryActivity::utf8Prev(const std::string& s, size_t pos) { + if (pos == 0) return 0; + pos--; + while (pos > 0 && (static_cast(s[pos]) & 0xC0) == 0x80) pos--; + return pos; +} - const KeyDef& key = layout[selectedRow][selectedCol]; - const char current = getSelectedChar(); - if (current == key.primary && key.secondary != '\0') return key.secondary; - if (current == key.secondary) return key.primary; - return '\0'; +size_t KeyboardEntryActivity::utf8Next(const std::string& s, size_t pos) { + if (pos >= s.length()) return s.length(); + pos++; + while (pos < s.length() && (static_cast(s[pos]) & 0xC0) == 0x80) pos++; + return pos; } -bool KeyboardEntryActivity::insertChar(char c) { - if (c == '\0') return true; - if (maxLength != 0 && text.length() >= maxLength) return true; +void KeyboardEntryActivity::insertUtf8(const char* out) { + if (!out || !*out) return; + const size_t n = strlen(out); + if (maxLength != 0 && text.length() + n > maxLength) return; if (cursorPos > text.length()) cursorPos = text.length(); + text.insert(cursorPos, out, n); + cursorPos += n; +} - text.insert(cursorPos, 1, c); - cursorPos++; +bool KeyboardEntryActivity::backspaceUtf8() { + if (text.empty() || cursorPos == 0) return false; + const size_t prev = utf8Prev(text, cursorPos); + text.erase(prev, cursorPos - prev); + cursorPos = prev; return true; } -void KeyboardEntryActivity::insertString(const std::string& str) { - if (str.empty()) return; - if (maxLength != 0 && text.length() + str.length() > maxLength) return; - if (cursorPos > text.length()) cursorPos = text.length(); +bool KeyboardEntryActivity::activateValue(const int16_t value, const bool longPress) { + switch (value) { + case fui::QWERTY_KEY_SHIFT: + delPressCount = 0; + hintVisible = false; + // Letters: case toggle. Symbols: pages between "?123" and "#+=". + shifted = !shifted; + clampSelection(); + return true; + case fui::QWERTY_KEY_MODE: + delPressCount = 0; + hintVisible = false; + if (urlPanel) { + urlPanel = false; + } else { + symbols = !symbols; + shifted = false; + } + clampSelection(); + return true; + case URL_PANEL_KEY: + delPressCount = 0; + hintVisible = false; + urlPanel = !urlPanel; + symbols = false; + shifted = false; + clampSelection(); + return true; + case fui::QWERTY_KEY_ENTER: + if (text.length() < minLength) return true; + onComplete(text); + return false; + case fui::QWERTY_KEY_BACKSPACE: + if (longPress) { + text.clear(); + cursorPos = 0; + return true; + } + delPressCount++; + if (delPressCount >= 2) { + hintVisible = true; + hintShowTime = millis(); + } + backspaceUtf8(); + return true; + default: { + delPressCount = 0; + hintVisible = false; + const fui::KeyboardLayout& layer = currentLayout(); + // keyboardAltOutputFor covers explicit alts and the letter case-flip. + const char* out = longPress ? fui::keyboardAltOutputFor(layer, value) : nullptr; + if (!out) out = fui::keyboardOutputFor(layer, value); + if (!out) return false; + insertUtf8(out); + if (shifted && !symbols) { + shifted = false; // shift auto-releases after one character + clampSelection(); + } + return true; + } + } +} - text.insert(cursorPos, str); - cursorPos += str.length(); +bool KeyboardEntryActivity::clearAllOrAltOnSelected() { + const fui::KeyboardKey* key = selectedKey(); + if (!key) return false; + if (key->value == fui::QWERTY_KEY_BACKSPACE) { + text.clear(); + cursorPos = 0; + return true; + } + // Explicit alts and the letter case-flip, same as touch long-press. + const char* alt = fui::keyboardAltOutputFor(currentLayout(), key->value); + if (alt) { + insertUtf8(alt); + return true; + } + return false; } -bool KeyboardEntryActivity::handleKeyPress() { - if (isBottomRow(selectedRow)) { - switch (static_cast(selectedCol)) { - case SpecialKeyType::Shift: - delPressCount = 0; - hintVisible = false; - // Shift is meaningless in the URL-snippet panel and the symbol layout, but - // must work for the URL letter layout so uppercase letters can be entered (#2178). - if (urlMode) return true; - if (symMode) return true; - shiftState = (shiftState + 1) % 2; - return true; - case SpecialKeyType::Mode: { - delPressCount = 0; - hintVisible = false; - if (urlMode) { - urlMode = false; - symMode = false; - selectedRow = getTotalRowCount() - 1; - selectedCol = static_cast(SpecialKeyType::Mode); - requestUpdate(); - return true; - } - symMode = !symMode; - int maxRow = getTotalRowCount() - 1; - if (selectedRow > maxRow) selectedRow = maxRow; - if (isBottomRow(selectedRow)) { - if (selectedCol >= BOTTOM_KEY_COUNT) selectedCol = BOTTOM_KEY_COUNT - 1; - } else { - if (selectedCol >= getContentColCount()) selectedCol = getContentColCount() - 1; - } - return true; +std::string KeyboardEntryActivity::displayTextForCurrentState() const { + std::string displayText = text; + if (inputType != InputType::Password || passwordVisible) { + return displayText; + } + + size_t revealPos; + if (cursorMode) { + revealPos = text.length(); // no reveal in displayText; block draws actual char directly + } else { + revealPos = (text.length() > 0 && cursorPos > 0) ? cursorPos - 1 : std::string::npos; + } + for (size_t i = 0; i < displayText.length(); i++) { + if (i != revealPos) { + displayText[i] = '*'; + } + } + return displayText; +} + +int KeyboardEntryActivity::measureRange(std::string& s, const int start, const int end) const { + if (end <= start) return 0; + // s[end] is writable even at s.length() (the terminator slot); only '\0' may + // be written there, which is exactly what the measurement needs. + const char saved = s[end]; + s[end] = '\0'; + const int width = renderer.getTextAdvanceX(UI_12_FONT_ID, s.c_str() + start, EpdFontFamily::REGULAR); + s[end] = saved; + return width; +} + +int KeyboardEntryActivity::lineBreakEnd(std::string& s, const int start, const int maxWidth) const { + const int len = static_cast(s.length()); + if (measureRange(s, start, len) <= maxWidth) return len; + int lo = start + 1; + int hi = len - 1; + int best = start + 1; + while (lo <= hi) { + const int mid = lo + (hi - lo) / 2; + if (measureRange(s, start, mid) <= maxWidth) { + best = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + return best; +} + +KeyboardEntryActivity::InputFieldTouchTarget KeyboardEntryActivity::inputFieldTouchTargetFromPoint( + const int x, const int y, size_t& position) const { + // Key taps are the overwhelmingly common case; they land on the keyboard, + // never the text field, so skip the wrap/measure work entirely. + if (y >= keyboardRect().y) return InputFieldTouchTarget::None; + + const int pageWidth = renderer.getScreenWidth(); + const auto& metrics = UITheme::getInstance().getMetrics(); + + const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); + const int inputStartY = metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + + metrics.verticalSpacing + metrics.verticalSpacing * 4 + metrics.keyboardVerticalOffset; + + int availableWidth = pageWidth; + // Clear the side-button hint gutters, which only render on edge-button boards without touch. + if (deviceUsesSideButtonHintGutters(gpio)) { + availableWidth -= 2 * metrics.sideButtonHintsWidth; + } + const int effectiveMargin = (pageWidth - availableWidth * metrics.keyboardTextFieldWidthPercent / 100) / 2; + const int toggleGap = inputType == InputType::Password ? 4 : 0; + const int toggleReserve = inputType == InputType::Password ? std::max(renderer.getTextWidth(UI_12_FONT_ID, "[abc]"), + renderer.getTextWidth(UI_12_FONT_ID, "[***]")) + + toggleGap + : 0; + const int textAreaWidth = pageWidth - 2 * effectiveMargin - toggleReserve; + const int maxLineWidth = textAreaWidth; + const bool centerText = metrics.keyboardCenteredText; + std::string displayText = displayTextForCurrentState(); + + int lineStartIdx = 0; + int lineY = inputStartY; + int lastLineStartIdx = 0; + int lastLineEndIdx = static_cast(displayText.length()); + int lastLineStartX = effectiveMargin; + int lastLineWidth = 0; + + while (true) { + const int lineEndIdx = lineBreakEnd(displayText, lineStartIdx, maxLineWidth); + const int textWidth = measureRange(displayText, lineStartIdx, lineEndIdx); + const bool isLastLine = lineEndIdx == static_cast(displayText.length()); + if (isLastLine && inputType == InputType::Password && x >= effectiveMargin + maxLineWidth && + x < pageWidth - effectiveMargin && y >= lineY - metrics.verticalSpacing && + y < lineY + lineHeight + metrics.verticalSpacing) { + return InputFieldTouchTarget::PasswordToggle; + } + + const int lineStartX = centerText ? effectiveMargin + (maxLineWidth - textWidth) / 2 : effectiveMargin; + lastLineStartIdx = lineStartIdx; + lastLineEndIdx = lineEndIdx; + lastLineStartX = lineStartX; + lastLineWidth = textWidth; + + if (y >= lineY - metrics.verticalSpacing && y < lineY + lineHeight + metrics.verticalSpacing) { + if (x <= lineStartX) { + position = static_cast(lineStartIdx); + return InputFieldTouchTarget::Cursor; } - case SpecialKeyType::Space: - delPressCount = 0; - hintVisible = false; - if (inputType == InputType::Url) { - urlMode = !urlMode; - if (urlMode) { - symMode = false; - } - selectedRow = getTotalRowCount() - 1; - selectedCol = static_cast(SpecialKeyType::Space); - requestUpdate(); - } else { - return insertChar(' '); - } - return true; - case SpecialKeyType::Del: - delPressCount++; - if (delPressCount >= 2) { - hintVisible = true; - hintShowTime = millis(); - } - if (cursorPos > 0 && !text.empty()) { - text.erase(cursorPos - 1, 1); - cursorPos--; + if (x >= lineStartX + textWidth) { + position = static_cast(lineEndIdx); + return InputFieldTouchTarget::Cursor; + } + + int previousWidth = 0; + for (int i = lineStartIdx; i < lineEndIdx; i++) { + const int nextWidth = measureRange(displayText, lineStartIdx, i + 1); + const int midpoint = lineStartX + previousWidth + (nextWidth - previousWidth) / 2; + if (x < midpoint) { + position = static_cast(i); + return InputFieldTouchTarget::Cursor; } - return true; - case SpecialKeyType::Ok: - delPressCount = 0; - hintVisible = false; - if (text.length() < minLength) return true; - onComplete(text); - return false; - default: - return true; + previousWidth = nextWidth; + } + position = static_cast(lineEndIdx); + return InputFieldTouchTarget::Cursor; } - } - if (urlMode) { - delPressCount = 0; - hintVisible = false; - const int idx = selectedCol + selectedRow * 3; - if (idx < URL_SNIPPET_COUNT) { - insertString(urlSnippets[idx]); + if (lineEndIdx == static_cast(displayText.length())) { + break; } - return true; + + lineY += lineHeight; + lineStartIdx = lineEndIdx; } - delPressCount = 0; - hintVisible = false; + const int underlineBottom = lineY + lineHeight + metrics.verticalSpacing + 8; + if (y >= inputStartY - metrics.verticalSpacing && y < underlineBottom && x >= effectiveMargin && + x < effectiveMargin + maxLineWidth + toggleReserve) { + position = x < lastLineStartX + lastLineWidth ? static_cast(lastLineStartIdx) + : static_cast(lastLineEndIdx); + return InputFieldTouchTarget::Cursor; + } - return insertChar(getSelectedChar()); + return InputFieldTouchTarget::None; } -void KeyboardEntryActivity::mapColContentBottom(int& col, bool goingUp) const { - if (urlMode) { - col = goingUp ? col - 1 : col + 1; - if (col < 0) col = 0; - if (col >= 3) col = 2; - } else { - col = goingUp ? col * 2 : col / 2; - } +fui::Rect KeyboardEntryActivity::keyboardRect() const { + const auto& metrics = UITheme::getInstance().getMetrics(); + const int pageWidth = renderer.getScreenWidth(); + const int pageHeight = renderer.getScreenHeight(); + const int rows = currentLayout().rowCount; + const int gap = metrics.keyboardKeySpacing; + const int height = rows * metrics.keyboardKeyHeight + (rows > 1 ? (rows - 1) * gap : 0); + const int width = pageWidth * metrics.keyboardWidthPercent / 100; + const int x = (pageWidth - width) / 2; + const int y = + pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - height + metrics.keyboardVerticalOffset; + return fui::Rect{static_cast(x), static_cast(y), static_cast(width), + static_cast(height)}; } void KeyboardEntryActivity::loop() { - const int totalRows = getTotalRowCount(); +#if CROSSINK_APP_CAP_TOUCH + if (TouchHeaderBackButton::wasTapped(mappedInput, renderer)) { + onCancel(); + return; + } + + int tx = 0; + int ty = 0; + + if (mappedInput.wasScreenTapped(tx, ty)) { + size_t touchedCursorPos = 0; + const InputFieldTouchTarget inputTarget = inputFieldTouchTargetFromPoint(tx, ty, touchedCursorPos); + if (inputTarget == InputFieldTouchTarget::PasswordToggle) { + passwordVisible = !passwordVisible; + togglePos = false; + hintVisible = false; + requestUpdate(); + return; + } + if (inputTarget == InputFieldTouchTarget::Cursor) { + cursorPos = std::min(touchedCursorPos, text.length()); + cursorMode = false; + togglePos = false; + hintVisible = false; + // The masked text field maps taps per byte; snap back to a boundary so + // the cursor never lands inside a multi-byte character. + while (cursorPos > 0 && cursorPos < text.length() && (static_cast(text[cursorPos]) & 0xC0) == 0x80) { + cursorPos--; + } + touchRouter.reset(); + requestUpdate(); + return; + } + } + + if (!cursorMode && interactionsReady) { + unsigned long touchHeldMs = 0; + const bool tapCandidate = mappedInput.isScreenTouchTapCandidate(tx, ty, touchHeldMs); + int tapX = 0; + int tapY = 0; + const bool tapped = mappedInput.wasScreenTapped(tapX, tapY); + int hx = 0; + int hy = 0; + const bool inContact = mappedInput.isScreenTouchHeld(hx, hy); + + const fui::TouchHoldRouter::Result result = + touchRouter.update(interactions, tapCandidate, static_cast(tx), static_cast(ty), tapped, + static_cast(tapX), static_cast(tapY), inContact, millis()); + if (result.event) { + syncSelectionToValue(result.event.value); + if (activateValue(result.event.value, result.event.longPress)) { + requestUpdate(); + } + return; + } + if (result.activeChanged) { + requestUpdate(); + } + if (tapCandidate || tapped) { + return; + } + } +#endif if (!cursorMode && mappedInput.wasPressed(MappedInputManager::Button::Up)) { upHeld = true; @@ -208,16 +583,7 @@ void KeyboardEntryActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { if (upHeld && !upLongHandled && !cursorMode) { - bool wasBottom = isBottomRow(selectedRow); - const int contentCols = getContentColCount(); - selectedRow = ButtonNavigator::previousIndex(selectedRow, totalRows); - if (wasBottom && !isBottomRow(selectedRow)) { - mapColContentBottom(selectedCol, true); - } else if (!wasBottom && isBottomRow(selectedRow)) { - mapColContentBottom(selectedCol, false); - } - int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : contentCols - 1; - if (selectedCol > maxCol) selectedCol = maxCol; + moveSelectionRow(-1); requestUpdate(); } upHeld = false; @@ -240,16 +606,7 @@ void KeyboardEntryActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Down)) { if (downHeld && !downLongHandled && !cursorMode) { - bool wasBottom = isBottomRow(selectedRow); - const int contentCols = getContentColCount(); - selectedRow = ButtonNavigator::nextIndex(selectedRow, totalRows); - if (wasBottom && !isBottomRow(selectedRow)) { - mapColContentBottom(selectedCol, true); - } else if (!wasBottom && isBottomRow(selectedRow)) { - mapColContentBottom(selectedCol, false); - } - int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : contentCols - 1; - if (selectedCol > maxCol) selectedCol = maxCol; + moveSelectionRow(1); requestUpdate(); } downHeld = false; @@ -258,8 +615,7 @@ void KeyboardEntryActivity::loop() { buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { if (cursorMode) return; - int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : getContentColCount() - 1; - selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); + moveSelectionCol(-1); requestUpdate(); }); @@ -270,7 +626,7 @@ void KeyboardEntryActivity::loop() { togglePos = false; requestUpdate(); } else if (cursorPos > 0) { - cursorPos--; + cursorPos = utf8Prev(text, cursorPos); requestUpdate(); } } @@ -286,8 +642,7 @@ void KeyboardEntryActivity::loop() { buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { if (cursorMode) return; - int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : getContentColCount() - 1; - selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); + moveSelectionCol(1); requestUpdate(); }); @@ -307,7 +662,7 @@ void KeyboardEntryActivity::loop() { rightLongHandled = false; } if (cursorMode && !togglePos && cursorPos < text.length()) { - cursorPos++; + cursorPos = utf8Next(text, cursorPos); requestUpdate(); } if (cursorMode) return; @@ -320,20 +675,19 @@ void KeyboardEntryActivity::loop() { confirmLongHandled = false; } + const fui::KeyboardKey* selKey = selectedKey(); + const bool selectedDel = selKey && selKey->value == fui::QWERTY_KEY_BACKSPACE; + if (confirmHeld && !confirmLongHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && - mappedInput.getHeldTime() > DEL_LONG_PRESS_MS && isBottomRow(selectedRow) && - selectedCol == static_cast(SpecialKeyType::Del)) { - text.clear(); - cursorPos = 0; + mappedInput.getHeldTime() > DEL_LONG_PRESS_MS && selectedDel) { + clearAllOrAltOnSelected(); confirmLongHandled = true; requestUpdate(); } if (confirmHeld && !confirmLongHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() > LONG_PRESS_MS) { - char alt = getAlternativeChar(); - if (alt != '\0') { - insertChar(alt); + if (!selectedDel && clearAllOrAltOnSelected()) { requestUpdate(); confirmLongHandled = true; } @@ -341,7 +695,7 @@ void KeyboardEntryActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (confirmHeld && !confirmLongHandled && !cursorMode) { - if (handleKeyPress()) { + if (selKey && activateValue(selKey->value, false)) { requestUpdate(); } } else if (confirmHeld && !confirmLongHandled && cursorMode && inputType == InputType::Password && togglePos) { @@ -367,37 +721,26 @@ void KeyboardEntryActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); const auto& metrics = UITheme::getInstance().getMetrics(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str()); + const Rect header{0, metrics.topPadding, pageWidth, TouchHeaderBackButton::height(metrics, mappedInput)}; + if (mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, header, title.c_str(), false); + } else { + GUI.drawHeader(renderer, header, title.c_str()); + } const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); - const int inputStartY = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + - metrics.verticalSpacing * 4 + metrics.keyboardVerticalOffset; + const int inputStartY = metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + + metrics.verticalSpacing + metrics.verticalSpacing * 4 + metrics.keyboardVerticalOffset; int inputHeight = 0; - std::string displayText; - if (inputType == InputType::Password && !passwordVisible) { - size_t revealPos; - if (cursorMode) { - revealPos = text.length(); // no reveal in displayText; block draws actual char directly - } else { - revealPos = (text.length() > 0 && cursorPos > 0) ? cursorPos - 1 : std::string::npos; - } - displayText = text; - for (size_t i = 0; i < displayText.length(); i++) { - if (i != revealPos) { - displayText[i] = '*'; - } - } - } else { - displayText = text; - } + std::string displayText = displayTextForCurrentState(); const bool isPassword = (inputType == InputType::Password); int availableWidth = pageWidth; - if (gpio.deviceIsX3()) { + // Clear the side-button hint gutters, which only render on edge-button boards without touch. + if (deviceUsesSideButtonHintGutters(gpio)) { availableWidth -= 2 * metrics.sideButtonHintsWidth; } const int effectiveMargin = (pageWidth - availableWidth * metrics.keyboardTextFieldWidthPercent / 100) / 2; @@ -417,16 +760,16 @@ void KeyboardEntryActivity::render(RenderLock&&) { } int lineStartIdx = 0; - int lineEndIdx = displayText.length(); int textWidth = 0; int cursorPixelX = effectiveMargin; int cursorLineY = inputStartY; bool cursorDrawn = false; while (true) { - std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx); + const int lineEndIdx = lineBreakEnd(displayText, lineStartIdx, maxLineWidth); + const std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx); textWidth = renderer.getTextAdvanceX(UI_12_FONT_ID, lineText.c_str(), EpdFontFamily::REGULAR); - if (textWidth <= maxLineWidth) { + { const bool isLastLine = (lineEndIdx == static_cast(displayText.length())); bool isCursorLine = false; if (!cursorDrawn && cursorPos >= lineStartIdx && @@ -475,15 +818,12 @@ void KeyboardEntryActivity::render(RenderLock&&) { } else { renderer.drawText(UI_12_FONT_ID, lineStartX, inputStartY + inputHeight, lineText.c_str()); } - if (lineEndIdx == displayText.length()) { + if (lineEndIdx == static_cast(displayText.length())) { break; } inputHeight += lineHeight; lineStartIdx = lineEndIdx; - lineEndIdx = displayText.length(); - } else { - lineEndIdx -= 1; } } @@ -551,20 +891,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { } } - const int keyHeight = metrics.keyboardKeyHeight; - const int bottomKeyHeight = metrics.keyboardBottomKeyHeight; - const int keySpacing = metrics.keyboardKeySpacing; - const int contentCols = getContentColCount(); - const int keyboardWidth = pageWidth * metrics.keyboardWidthPercent / 100; - const int keyWidth = (keyboardWidth - (contentCols - 1) * keySpacing) / contentCols; - const int leftMargin = (pageWidth - (contentCols * keyWidth + (contentCols - 1) * keySpacing)) / 2; - - const int bottomRowGap = metrics.keyboardBottomKeySpacing > 0 ? 4 : 0; - const int keyboardStartY = metrics.keyboardBottomAligned - ? pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - - (keyHeight + keySpacing) * getContentRowCount() - bottomKeyHeight - - bottomRowGap + metrics.keyboardVerticalOffset - : inputStartY + inputHeight + lineHeight + metrics.verticalSpacing; + const fui::Rect kbRect = keyboardRect(); const int tipsLh = renderer.getLineHeight(SMALL_FONT_ID); const int underlineBottom = inputStartY + inputHeight + lineHeight + metrics.verticalSpacing + 4; @@ -573,27 +900,27 @@ void KeyboardEntryActivity::render(RenderLock&&) { int tipCount = 0; if (cursorMode) { tipCount = 1; - } else if (urlMode) { + } else if (urlPanel) { tipCount = 1 + (!text.empty() ? 1 : 0); - } else if (symMode) { + } else if (symbols) { tipCount = !text.empty() ? 1 : 0; } else { tipCount = 1 + (inputType == InputType::Url ? 1 : 0) + (!text.empty() ? 1 : 0); } if (tipCount > 0) { - int y = (underlineBottom + keyboardStartY) / 2 - (tipCount + 1) * tipsLh / 2; + int y = (underlineBottom + kbRect.y) / 2 - (tipCount + 1) * tipsLh / 2; drawTip(tr(STR_KB_TIPS), y); y += tipsLh; if (cursorMode) { drawTip(tr(STR_KB_HINT_RETURN_KEYBOARD), y); - } else if (urlMode) { + } else if (urlPanel) { drawTip(tr(STR_KB_HINT_EXIT_URL_MODE), y); y += tipsLh; if (!text.empty()) { drawTip(tr(STR_KB_HINT_CLEAR_TEXT), y); } - } else if (symMode) { + } else if (symbols) { if (!text.empty()) { drawTip(tr(STR_KB_HINT_CLEAR_TEXT), y); } @@ -601,7 +928,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { const char* altCharTip; if (inputType == InputType::Url) { altCharTip = tr(STR_KB_HINT_SECONDARY_CHAR); - } else if (shiftState > 0) { + } else if (shifted) { altCharTip = tr(STR_KB_HINT_LOWER_SECONDARY); } else { altCharTip = tr(STR_KB_HINT_UPPER_SECONDARY); @@ -618,123 +945,38 @@ void KeyboardEntryActivity::render(RenderLock&&) { } } - const int bkSpacing = metrics.keyboardBottomKeySpacing; - const int abcKeyWidth = (keyboardWidth - (COLS - 1) * keySpacing) / COLS; - const int contentTotalWidth = COLS * abcKeyWidth + (COLS - 1) * keySpacing; - const int bottomKeyWidth = (contentTotalWidth - (BOTTOM_KEY_COUNT - 1) * bkSpacing) / BOTTOM_KEY_COUNT; - const int bottomLeftMargin = - (pageWidth - (BOTTOM_KEY_COUNT * bottomKeyWidth + (BOTTOM_KEY_COUNT - 1) * bkSpacing)) / 2; - - int urlLeftMargin = leftMargin; - if (urlMode) { - const int urlTotalWidth = 3 * keyWidth + 2 * keySpacing; - const int urlCenterX = - bottomLeftMargin + static_cast(SpecialKeyType::Space) * (bottomKeyWidth + bkSpacing) + bottomKeyWidth / 2; - urlLeftMargin = urlCenterX - urlTotalWidth / 2; - } - - const KeyDef(*layout)[COLS] = symMode ? symLayout : (inputType == InputType::Url ? urlLayout : abcLayout); - const int contentRows = getContentRowCount(); - - for (int row = 0; row < contentRows; row++) { - const int rowY = keyboardStartY + row * (keyHeight + keySpacing); - const int rowLeftMargin = urlMode ? urlLeftMargin : leftMargin; - - for (int col = 0; col < contentCols; col++) { - const int keyX = rowLeftMargin + col * (keyWidth + keySpacing); - const bool isSelected = row == selectedRow && col == selectedCol; - const bool activeKeySelected = isSelected && !cursorMode; - - if (urlMode) { - const int snippetIdx = col + row * 3; - if (snippetIdx < URL_SNIPPET_COUNT) { - GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, urlSnippets[snippetIdx], - activeKeySelected, nullptr); - } - } else { - const KeyDef& key = layout[row][col]; - - char primaryChar = key.primary; - char secondaryChar = key.secondary; - - if (!symMode && shiftState > 0 && key.secondary != '\0') { - primaryChar = key.secondary; - secondaryChar = key.primary; - } - - const char primaryBuf[2] = {primaryChar, '\0'}; - const char secondaryBuf[2] = {secondaryChar, '\0'}; - const bool showSecondary = !symMode && row == 0 && secondaryChar != '\0'; - GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, primaryBuf, activeKeySelected, - showSecondary ? secondaryBuf : nullptr); - } - } - } - - const int bottomRowY = keyboardStartY + contentRows * (keyHeight + keySpacing) + bottomRowGap; - const bool bottomSelected = isBottomRow(selectedRow); - - struct BottomKeyInfo { - KeyboardKeyType themeType; - const char* label; - }; - const BottomKeyInfo bottomKeys[BOTTOM_KEY_COUNT] = { - {(symMode || urlMode) ? KeyboardKeyType::Disabled : KeyboardKeyType::Shift, - (symMode || urlMode) ? shiftString[0] : shiftString[shiftState]}, - {KeyboardKeyType::Mode, urlMode ? "abc" : (symMode ? "abc" : "#@!")}, - {inputType == InputType::Url ? KeyboardKeyType::Mode : KeyboardKeyType::Space, - inputType == InputType::Url ? "URL" : nullptr}, - {KeyboardKeyType::Del, nullptr}, - {KeyboardKeyType::Ok, tr(STR_OK_BUTTON)}, - }; - - for (int i = 0; i < BOTTOM_KEY_COUNT; i++) { - const int keyX = bottomLeftMargin + i * (bottomKeyWidth + bkSpacing); - const bool isSelected = bottomSelected && i == selectedCol; - - const bool activeKeySelected = isSelected && !cursorMode; - GUI.drawKeyboardKey(renderer, Rect{keyX, bottomRowY, bottomKeyWidth, bottomKeyHeight}, bottomKeys[i].label, - activeKeySelected, nullptr, bottomKeys[i].themeType); - } - - if (cursorMode) { - int selKeyX, selKeyY, selKeyW, selKeyH; - if (isBottomRow(selectedRow)) { - selKeyX = bottomLeftMargin + selectedCol * (bottomKeyWidth + bkSpacing); - selKeyY = bottomRowY; - selKeyW = bottomKeyWidth; - selKeyH = bottomKeyHeight; - } else { - const int rowLM = urlMode ? urlLeftMargin : leftMargin; - selKeyX = rowLM + selectedCol * (keyWidth + keySpacing); - selKeyY = keyboardStartY + selectedRow * (keyHeight + keySpacing); - selKeyW = keyWidth; - selKeyH = keyHeight; - } - if (isBottomRow(selectedRow)) { - GUI.drawKeyboardKey(renderer, Rect{selKeyX, selKeyY, selKeyW, selKeyH}, bottomKeys[selectedCol].label, true, - nullptr, bottomKeys[selectedCol].themeType, true); - } else if (urlMode) { - const int idx = selectedCol + selectedRow * 3; - if (idx < URL_SNIPPET_COUNT) { - GUI.drawKeyboardKey(renderer, Rect{selKeyX, selKeyY, selKeyW, selKeyH}, urlSnippets[idx], true, nullptr, - KeyboardKeyType::Normal, true); - } - } else { - const KeyDef& selKey = layout[selectedRow][selectedCol]; - char selPrimary = selKey.primary; - char selSecondary = selKey.secondary; - if (!symMode && shiftState > 0 && selKey.secondary != '\0') { - selPrimary = selKey.secondary; - selSecondary = selKey.primary; - } - const char selPrimaryBuf[2] = {selPrimary, '\0'}; - const char selSecondaryBuf[2] = {selSecondary, '\0'}; - const bool selShowSecondary = !symMode && selectedRow == 0 && selSecondary != '\0'; - GUI.drawKeyboardKey(renderer, Rect{selKeyX, selKeyY, selKeyW, selKeyH}, selPrimaryBuf, true, - selShowSecondary ? selSecondaryBuf : nullptr, KeyboardKeyType::Normal, true); - } - } + // The FreeInkUI keyboard draws the keys and registers their hit rects into + // `interactions`; loop() routes touch snapshots against that table. + interactionsReady = false; + fui::GfxRendererTarget target(renderer); + target.setFont(fui::GfxRendererTarget::FONT_SMALL, SMALL_FONT_ID); + target.setFont(fui::GfxRendererTarget::FONT_BODY, UI_12_FONT_ID); + const fui::DeviceContext device = target.deviceContext(); + const fui::InputSnapshot noInput{}; + fui::Frame<48> frame(target, device, noInput, interactions); + + fui::KeyboardProps props; + const fui::KeyboardLayout& layout = currentLayout(); + props.layout = &layout; + props.keyAction = ACTION_KEY; // one action id; loop() dispatches on key value + props.okLabel = tr(STR_OK_BUTTON); + props.shiftLabel = tr(STR_KEY_SHIFT); + // Match the label to the layer the mode key leads back from: the symbols + // layer and the URL snippet panel both label it "abc" in the static tables. + props.modeLabel = + (symbols || (inputType == InputType::Url && urlPanel)) ? tr(STR_KEY_MODE_ABC) : tr(STR_KEY_MODE_SYMBOLS); + props.inputMask = static_cast(fui::InputTouch | fui::InputLongPress); + props.selectedIndex = cursorMode ? -1 : static_cast(selectedLogicalIndex()); + props.labelText.font = fui::GfxRendererTarget::FONT_BODY; + props.altText.font = fui::GfxRendererTarget::FONT_SMALL; + props.gap = static_cast(metrics.keyboardKeySpacing); + props.padding = fui::Insets{0, 0, 0, 0}; + // Fingers land low on the bottom row (occlusion) and there is no key below + // to catch the miss — extend its hit band down to the button hints bar. + const int hintsTop = renderer.getScreenHeight() - metrics.buttonHintsHeight; + props.bottomHitOverflow = static_cast(std::max(0, hintsTop - (kbRect.y + kbRect.height))); + fui::keyboard(frame, kbRect, props); + interactionsReady = true; const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/util/KeyboardEntryActivity.h b/src/activities/util/KeyboardEntryActivity.h index 75383db447..1fabf08054 100644 --- a/src/activities/util/KeyboardEntryActivity.h +++ b/src/activities/util/KeyboardEntryActivity.h @@ -1,23 +1,21 @@ #pragma once +#include #include +#include #include -#include #include #include #include "activities/Activity.h" #include "util/ButtonNavigator.h" -struct KeyDef { - char primary; - char secondary; -}; - -enum class SpecialKeyType { Shift, Mode, Space, Del, Ok }; - enum class InputType { Text, Password, Url }; +// Text entry on the FreeInkUI keyboard component: the SDK layout tables and +// keyboard() do the key rendering and hit-rect registration, InteractionBuffer +// routes taps/long-presses, and this activity owns the text field, cursor +// editing, and the URL snippet layouts. class KeyboardEntryActivity : public Activity { public: explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, @@ -46,16 +44,30 @@ class KeyboardEntryActivity : public Activity { ButtonNavigator buttonNavigator; - int selectedRow = 0; - int selectedCol = 0; - int shiftState = 0; - bool symMode = false; + // Keyboard layers. The letter/symbol layers come from the SDK's builtin + // layouts (with the always-visible number row); the URL layers are + // app-defined tables in the .cpp. + freeink::ui::KeyboardLayoutId layoutId = freeink::ui::KeyboardLayoutId::QwertyEn; + bool shifted = false; + bool symbols = false; + bool urlPanel = false; // URL snippet panel replaces the letter layer + + // Key hit rects registered by the keyboard component during render(); + // loop() routes touch snapshots against them. 5-row EN layout registers 41 + // keys, so 48 leaves headroom. + freeink::ui::InteractionBuffer<48> interactions; + + // GPIO selection over the current layout grid (row/col in layout terms; + // the bottom action row is just the last row). + int selRow = 0; + int selCol = 0; + bool confirmHeld = false; bool confirmLongHandled = false; bool cursorMode = false; bool togglePos = false; - size_t cursorPos = 0; + size_t cursorPos = 0; // byte offset into text (always on a code point boundary) bool upHeld = false; bool upLongHandled = false; bool downHeld = false; @@ -65,165 +77,58 @@ class KeyboardEntryActivity : public Activity { size_t savedCursorPos = 0; size_t rightStartCursorPos = 0; - bool urlMode = false; - static constexpr int URL_SNIPPET_COUNT = 9; - static constexpr const char* const urlSnippets[URL_SNIPPET_COUNT] = { - "https://", "www.", ".com", "http://", "192.168.", ".org", "/opds", ":8080", ".net"}; + // Tap/hold routing (threshold long-press, release swallow, slide re-arm) + // lives in the SDK; loop() feeds it the level-triggered touch state. + freeink::ui::TouchHoldRouter touchRouter; + + // loop() runs on the main task while render() rebuilds the interaction + // table on the render task; routing against a half-built table would read + // torn entries, so taps are dropped during the rebuild window. atomic (not + // volatile) so the flag also orders the table writes on dual-core targets. + std::atomic interactionsReady{false}; int delPressCount = 0; bool hintVisible = false; unsigned long hintShowTime = 0; + enum class InputFieldTouchTarget { None, Cursor, PasswordToggle }; + void onComplete(std::string text); void onCancel(); + InputFieldTouchTarget inputFieldTouchTargetFromPoint(int x, int y, size_t& position) const; + std::string displayTextForCurrentState() const; + // Advance of s[start, end) measured in place by temporarily null-terminating + // at `end` — avoids a substr temporary per measurement. + int measureRange(std::string& s, int start, int end) const; + // Largest line end in (start, s.length()] whose advance fits maxWidth. + // Binary search over the monotonic prefix advance; always advances at least + // one byte so an oversized glyph cannot stall the wrap loop. + int lineBreakEnd(std::string& s, int start, int maxWidth) const; + + const freeink::ui::KeyboardLayout& currentLayout() const; + const freeink::ui::KeyboardKey* selectedKey() const; + int selectedLogicalIndex() const; + void clampSelection(); + void moveSelectionRow(int delta); + void moveSelectionCol(int delta); + bool syncSelectionToValue(int16_t value); + // Handles one key activation (by stable key id). Returns true when the + // screen needs a repaint; OK/cancel finish the activity instead. + bool activateValue(int16_t value, bool longPress); + bool clearAllOrAltOnSelected(); + + void insertUtf8(const char* out); + bool backspaceUtf8(); + static size_t utf8Prev(const std::string& s, size_t pos); + static size_t utf8Next(const std::string& s, size_t pos); + + freeink::ui::Rect keyboardRect() const; static constexpr uint16_t LONG_PRESS_MS = 500; static constexpr uint16_t DEL_LONG_PRESS_MS = 1500; + static constexpr uint16_t TOUCH_LONG_PRESS_MS = 350; + static constexpr uint16_t TOUCH_DEL_LONG_PRESS_MS = 900; - static constexpr int COLS = 10; - static constexpr int ABC_ROWS = 4; - static constexpr int SYM_ROWS = 4; - static constexpr int BOTTOM_KEY_COUNT = 5; - - static constexpr KeyDef abcLayout[ABC_ROWS][COLS] = { - {{'1', '!'}, - {'2', '@'}, - {'3', '#'}, - {'4', '$'}, - {'5', '%'}, - {'6', '^'}, - {'7', '&'}, - {'8', '*'}, - {'9', '('}, - {'0', ')'}}, - {{'q', 'Q'}, - {'w', 'W'}, - {'e', 'E'}, - {'r', 'R'}, - {'t', 'T'}, - {'y', 'Y'}, - {'u', 'U'}, - {'i', 'I'}, - {'o', 'O'}, - {'p', 'P'}}, - {{'a', 'A'}, - {'s', 'S'}, - {'d', 'D'}, - {'f', 'F'}, - {'g', 'G'}, - {'h', 'H'}, - {'j', 'J'}, - {'k', 'K'}, - {'l', 'L'}, - {'-', '_'}}, - {{'z', 'Z'}, - {'x', 'X'}, - {'c', 'C'}, - {'v', 'V'}, - {'b', 'B'}, - {'n', 'N'}, - {'m', 'M'}, - {'=', '+'}, - {'.', '>'}, - {',', '<'}}, - }; - - static constexpr KeyDef urlLayout[ABC_ROWS][COLS] = { - {{'1', '!'}, - {'2', '@'}, - {'3', '#'}, - {'4', '$'}, - {'5', '%'}, - {'6', '^'}, - {'7', '&'}, - {'8', '*'}, - {'9', '('}, - {'0', ')'}}, - {{'q', 'Q'}, - {'w', 'W'}, - {'e', 'E'}, - {'r', 'R'}, - {'t', 'T'}, - {'y', 'Y'}, - {'u', 'U'}, - {'i', 'I'}, - {'o', 'O'}, - {'p', 'P'}}, - {{'a', 'A'}, - {'s', 'S'}, - {'d', 'D'}, - {'f', 'F'}, - {'g', 'G'}, - {'h', 'H'}, - {'j', 'J'}, - {'k', 'K'}, - {'l', 'L'}, - {'-', '_'}}, - {{'z', 'Z'}, - {'x', 'X'}, - {'c', 'C'}, - {'v', 'V'}, - {'b', 'B'}, - {'n', 'N'}, - {'m', 'M'}, - {':', '+'}, - {'.', '>'}, - {'/', '<'}}, - }; - - static constexpr KeyDef symLayout[SYM_ROWS][COLS] = { - {{'1', '\0'}, - {'2', '\0'}, - {'3', '\0'}, - {'4', '\0'}, - {'5', '\0'}, - {'6', '\0'}, - {'7', '\0'}, - {'8', '\0'}, - {'9', '\0'}, - {'0', '\0'}}, - {{'!', '\0'}, - {'@', '\0'}, - {'#', '\0'}, - {'$', '\0'}, - {'%', '\0'}, - {'^', '\0'}, - {'&', '\0'}, - {'*', '\0'}, - {'(', '\0'}, - {')', '\0'}}, - {{'-', '\0'}, - {'_', '\0'}, - {'=', '\0'}, - {'+', '\0'}, - {'[', '\0'}, - {']', '\0'}, - {'{', '\0'}, - {'}', '\0'}, - {';', '\0'}, - {':', '\0'}}, - {{'\'', '\0'}, - {'"', '\0'}, - {'/', '\0'}, - {'\\', '\0'}, - {'|', '\0'}, - {'?', '\0'}, - {'.', '\0'}, - {',', '\0'}, - {'~', '\0'}, - {'`', '\0'}}, - }; - - static const char* const shiftString[2]; - - int getContentRowCount() const; - int getContentColCount() const; - int getTotalRowCount() const; - bool isBottomRow(int row) const; - char getSelectedChar() const; - char getAlternativeChar() const; - bool handleKeyPress(); - bool insertChar(char c); - void insertString(const std::string& str); - void mapColContentBottom(int& col, bool goingUp) const; + // App-specific key id: toggles the URL snippet panel (URL fields only). + static constexpr int16_t URL_PANEL_KEY = -3; }; diff --git a/src/activities/util/OptionSelectionActivity.cpp b/src/activities/util/OptionSelectionActivity.cpp index d04262e2ef..0530340302 100644 --- a/src/activities/util/OptionSelectionActivity.cpp +++ b/src/activities/util/OptionSelectionActivity.cpp @@ -6,70 +6,106 @@ #include #include "MappedInputManager.h" +#include "components/TouchHeaderBackButton.h" #include "components/UITheme.h" -#include "fontIds.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" + +namespace fui = freeink::ui; + +namespace { +constexpr fui::ActionId ACTION_ROW = 1; + +Rect optionListRect(const GfxRenderer& renderer, const MappedInputManager& mappedInput, const bool readerMode) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const auto orientation = renderer.getOrientation(); + const bool isLandscape = readerMode && (orientation == GfxRenderer::Orientation::LandscapeClockwise || + orientation == GfxRenderer::Orientation::LandscapeCounterClockwise); + const int hintGutterWidth = isLandscape ? metrics.buttonHintsHeight : 0; + const int contentX = orientation == GfxRenderer::Orientation::LandscapeClockwise ? hintGutterWidth : 0; + const int contentTop = + metrics.topPadding + TouchHeaderBackButton::height(metrics, mappedInput) + metrics.verticalSpacing; + return Rect{contentX, contentTop, renderer.getScreenWidth() - hintGutterWidth, + renderer.getScreenHeight() - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing}; +} +} // namespace OptionSelectionActivity::OptionSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - std::string activityName, StrId titleId, - std::vector options, uint8_t selectedIndex, - bool readerMode) + std::string activityName, const StrId titleId, + std::vector options, const uint8_t selectedIndex, + const bool readerMode, const bool showTouchHeaderBackButton) : Activity(std::move(activityName), renderer, mappedInput), titleId_(titleId), options_(std::move(options)), currentIndex_(selectedIndex), selectedIndex_(selectedIndex), - readerMode_(readerMode) {} + readerMode_(readerMode), + showTouchHeaderBackButton_(showTouchHeaderBackButton), + uiTarget_(makeUiTarget(renderer)), + app_(uiTarget_, uiTarget_.deviceContext()) {} void OptionSelectionActivity::onEnter() { Activity::onEnter(); - if (options_.empty()) { cancel(); return; } - - if (currentIndex_ < 0 || currentIndex_ >= static_cast(options_.size())) { - currentIndex_ = 0; - } - if (selectedIndex_ < 0 || selectedIndex_ >= static_cast(options_.size())) { - selectedIndex_ = 0; - } + if (currentIndex_ < 0 || currentIndex_ >= static_cast(options_.size())) currentIndex_ = 0; + if (selectedIndex_ < 0 || selectedIndex_ >= static_cast(options_.size())) selectedIndex_ = 0; + topIndex_ = 0; + visibleRows_ = 1; + initialViewportPending_ = true; + uiReady_ = false; + app_.setTheme(uiThemeTokens(uiTarget_)); + app_.on(ACTION_ROW, &OptionSelectionActivity::onRowEvent, this); + app_.setScreen(&OptionSelectionActivity::optionsScreen, this); requestUpdate(); } void OptionSelectionActivity::loop() { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + if ((showTouchHeaderBackButton_ && TouchHeaderBackButton::wasTapped(mappedInput, renderer)) || + mappedInput.wasPressed(MappedInputManager::Button::Back)) { mappedInput.suppressNextBackRelease(); cancel(); return; } - + if (uiReady_) { + const fui::InputSnapshot snap = touchSnapshotFrom(mappedInput); + if (snap.touchPressed || snap.touchReleased) { + const auto event = app_.route(snap); + if (app_.invalidated()) requestUpdate(); + if (event) return; + } + } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { select(); return; } - const int listSize = static_cast(options_.size()); - const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false); - - buttonNavigator_.onNextRelease([this, listSize] { - selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, listSize); - requestUpdate(); - }); - - buttonNavigator_.onPreviousRelease([this, listSize] { - selectedIndex_ = ButtonNavigator::previousIndex(selectedIndex_, listSize); - requestUpdate(); - }); - - buttonNavigator_.onNextContinuous([this, listSize, pageItems] { - selectedIndex_ = ButtonNavigator::nextPageIndex(selectedIndex_, listSize, pageItems); + const auto swipe = mappedInput.wasSwipe(); + if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { + const int delta = swipe == MappedInputManager::SwipeDir::Up ? visibleRows_ : -visibleRows_; + const int next = scrollListBy(topIndex_, delta, visibleRows_, listSize); + if (next != topIndex_) { + topIndex_ = next; + requestUpdate(); + } + return; + } + const auto moveSelection = [this, listSize](const int index) { + selectedIndex_ = index; + topIndex_ = followListSelection(selectedIndex_, topIndex_, visibleRows_, listSize); requestUpdate(); + }; + buttonNavigator_.onNextRelease( + [this, listSize, &moveSelection] { moveSelection(ButtonNavigator::nextIndex(selectedIndex_, listSize)); }); + buttonNavigator_.onPreviousRelease( + [this, listSize, &moveSelection] { moveSelection(ButtonNavigator::previousIndex(selectedIndex_, listSize)); }); + buttonNavigator_.onNextContinuous([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::nextPageIndex(selectedIndex_, listSize, visibleRows_)); }); - - buttonNavigator_.onPreviousContinuous([this, listSize, pageItems] { - selectedIndex_ = ButtonNavigator::previousPageIndex(selectedIndex_, listSize, pageItems); - requestUpdate(); + buttonNavigator_.onPreviousContinuous([this, listSize, &moveSelection] { + moveSelection(ButtonNavigator::previousPageIndex(selectedIndex_, listSize, visibleRows_)); }); } @@ -85,33 +121,69 @@ void OptionSelectionActivity::select() { finish(); } +void OptionSelectionActivity::onRowEvent(const fui::ActionEvent& event, void* user) { + auto* self = static_cast(user); + if (event.value < 0 || event.value >= static_cast(self->options_.size())) return; + self->selectedIndex_ = event.value; + self->app_.clearTapFlash(); + self->select(); +} + +void OptionSelectionActivity::optionsScreen(UiApp::ScreenType& screen, void* user) { + static_cast(user)->buildOptionsScreen(screen); +} + +void OptionSelectionActivity::buildOptionsScreen(UiApp::ScreenType& screen) { + const Rect bounds = optionListRect(renderer, mappedInput, readerMode_); + screen.setContentMargin(fui::Insets{ + static_cast(bounds.y), static_cast(renderer.getScreenWidth() - bounds.x - bounds.width), + static_cast(renderer.getScreenHeight() - bounds.y - bounds.height), static_cast(bounds.x)}); + std::vector items; + items.reserve(options_.size()); + for (size_t i = 0; i < options_.size(); ++i) { + fui::ListItem item; + item.label = options_[i].c_str(); + item.value = i == static_cast(currentIndex_) ? tr(STR_SELECTED) : nullptr; + item.actionValue = static_cast(i); + items.push_back(item); + } + fui::ListProps props; + props.items = items.data(); + props.count = static_cast(items.size()); + props.selectedIndex = static_cast(selectedIndex_); + props.action = ACTION_ROW; + props.inputMask = fui::InputTouch; + props.valueInset = 8; + props.labelText = screen.theme().bodyText; + const auto rows = configureUiList(props, screen.theme(), screen.body()); + visibleRows_ = rows > 0 ? rows : 1; + const int optionCount = static_cast(options_.size()); + topIndex_ = initialViewportPending_ ? followListSelection(selectedIndex_, 0, visibleRows_, optionCount) + : scrollListBy(topIndex_, 0, visibleRows_, optionCount); + initialViewportPending_ = false; + props.topIndex = static_cast(topIndex_); + screen.list(props); +} + void OptionSelectionActivity::render(RenderLock&&) { renderer.clearScreen(); - - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); const auto& metrics = UITheme::getInstance().getMetrics(); - const auto orientation = renderer.getOrientation(); - const bool isLandscapeCw = readerMode_ && orientation == GfxRenderer::Orientation::LandscapeClockwise; - const bool isLandscapeCcw = readerMode_ && orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; - const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? metrics.buttonHintsHeight : 0; - const int contentX = isLandscapeCw ? hintGutterWidth : 0; - const int contentWidth = pageWidth - hintGutterWidth; - - GUI.drawHeader(renderer, Rect{contentX, metrics.topPadding, contentWidth, metrics.headerHeight}, I18N.get(titleId_), - nullptr, readerMode_); - - const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; - - GUI.drawList( - renderer, Rect{contentX, contentTop, contentWidth, contentHeight}, static_cast(options_.size()), - selectedIndex_, [this](int index) { return options_[index]; }, nullptr, nullptr, - [this](int index) -> std::string { return index == currentIndex_ ? tr(STR_SELECTED) : ""; }, true); - + const bool landscape = readerMode_ && (orientation == GfxRenderer::Orientation::LandscapeClockwise || + orientation == GfxRenderer::Orientation::LandscapeCounterClockwise); + const int gutter = landscape ? metrics.buttonHintsHeight : 0; + const int contentX = orientation == GfxRenderer::Orientation::LandscapeClockwise ? gutter : 0; + const Rect header{contentX, metrics.topPadding, renderer.getScreenWidth() - gutter, + TouchHeaderBackButton::height(metrics, mappedInput)}; + if (showTouchHeaderBackButton_ && mappedInput.hasTouchHardware()) { + TouchHeaderBackButton::draw(renderer, uiTarget_, header, I18N.get(titleId_), readerMode_); + } else { + GUI.drawHeader(renderer, header, I18N.get(titleId_), nullptr, readerMode_); + } + uiReady_ = false; + app_.render(); + uiReady_ = true; const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, readerMode_); - renderer.displayBuffer(); } diff --git a/src/activities/util/OptionSelectionActivity.h b/src/activities/util/OptionSelectionActivity.h index 4f07fc33f8..5b5d66915b 100644 --- a/src/activities/util/OptionSelectionActivity.h +++ b/src/activities/util/OptionSelectionActivity.h @@ -1,7 +1,10 @@ #pragma once +#include +#include #include +#include #include #include @@ -12,7 +15,7 @@ class OptionSelectionActivity final : public Activity { public: OptionSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string activityName, StrId titleId, std::vector options, uint8_t selectedIndex, - bool readerMode = false); + bool readerMode = false, bool showTouchHeaderBackButton = false); void onEnter() override; void loop() override; @@ -29,4 +32,16 @@ class OptionSelectionActivity final : public Activity { int currentIndex_ = 0; int selectedIndex_ = 0; bool readerMode_ = false; + bool showTouchHeaderBackButton_ = false; + using UiApp = freeink::ui::FreeInkApp<20, 4>; + freeink::ui::GfxRendererTarget uiTarget_; + UiApp app_; + std::atomic uiReady_{false}; + int visibleRows_ = 1; + int topIndex_ = 0; + bool initialViewportPending_ = true; + + static void optionsScreen(UiApp::ScreenType& screen, void* user); + static void onRowEvent(const freeink::ui::ActionEvent& event, void* user); + void buildOptionsScreen(UiApp::ScreenType& screen); }; diff --git a/src/clippings/ClipTextBuilder.cpp b/src/clippings/ClipTextBuilder.cpp index a64a9d6e94..923d755880 100644 --- a/src/clippings/ClipTextBuilder.cpp +++ b/src/clippings/ClipTextBuilder.cpp @@ -8,10 +8,7 @@ namespace ClipTextBuilder { namespace { -bool hasEmSpace(const std::string& word) { - return word.size() >= 3 && static_cast(word[0]) == 0xE2 && - static_cast(word[1]) == 0x80 && static_cast(word[2]) == 0x83; -} +bool hasEmSpace(const char* word) { return word[0] == '\xe2' && word[1] == '\x80' && word[2] == '\x83'; } bool isUtf8SpaceAt(const std::string& text, const size_t index, size_t& advance) { const auto c = static_cast(text[index]); @@ -78,9 +75,9 @@ bool areWordsVisuallyAttached(const WordRef& previousWord, const WordRef& word) } // namespace -ClippingResult build(const std::vector& words, const std::vector& wordOrder, const int fromOrder, - const int toOrder, const int totalOrder, const int startPageInSection, - const int sectionPageCount) { +ClippingResult build(const ClipWordStore& wordStore, const uint16_t* wordOrder, const int fromOrder, const int toOrder, + const int totalOrder, const int startPageInSection, const int sectionPageCount) { + const auto& words = wordStore.words; std::string text; text.reserve(256); @@ -104,17 +101,24 @@ ClippingResult build(const std::vector& words, const std::vector& for (int orderIdx = fromOrder; orderIdx <= toOrder; ++orderIdx) { const WordRef& word = words[wordOrder[orderIdx]]; - const auto wordText = stripTrailingInsertedHyphen(cleanWordText(word.text), word.endsWithInsertedHyphen); + const auto wordText = stripTrailingInsertedHyphen(cleanWordText(wordStore.text(word)), word.endsWithInsertedHyphen); if (wordText.empty()) { continue; } const WordRef* previousWord = orderIdx > fromOrder ? &words[wordOrder[orderIdx - 1]] : nullptr; + const auto prevClean = previousWord ? cleanWordText(wordStore.text(*previousWord)) : std::string{}; + const bool joinsInsertedHyphen = + previousWord && previousWord->endsWithInsertedHyphen && !prevClean.empty() && prevClean.back() == '-'; + if (joinsInsertedHyphen) { + text += wordText; + continue; + } + const bool yGap = previousWord && word.pageIdx == previousWord->pageIdx && word.y > previousWord->y + previousWord->h; - const bool paragraphStart = previousWord && (hasEmSpace(word.text) || word.paragraphStart || yGap); + const bool paragraphStart = previousWord && (hasEmSpace(wordStore.text(word)) || word.paragraphStart || yGap); if (previousWord && !text.empty() && !paragraphStart) { - const auto prevClean = cleanWordText(previousWord->text); if (!prevClean.empty() && prevClean.back() == '-' && !std::isspace(static_cast(wordText[0])) && !std::ispunct(static_cast(wordText[0]))) { text += wordText; @@ -143,7 +147,7 @@ ClippingResult build(const std::vector& words, const std::vector& anchorCount = 0; for (int orderIdx = toOrder; orderIdx >= fromOrder && anchorCount < ANCHOR_WORDS; --orderIdx) { const WordRef& word = words[wordOrder[orderIdx]]; - const auto wordText = stripTrailingInsertedHyphen(cleanWordText(word.text), word.endsWithInsertedHyphen); + const auto wordText = stripTrailingInsertedHyphen(cleanWordText(wordStore.text(word)), word.endsWithInsertedHyphen); endAnchor = endAnchor.empty() ? wordText : wordText + ' ' + endAnchor; anchorCount++; } @@ -152,14 +156,14 @@ ClippingResult build(const std::vector& words, const std::vector& std::string beforeStart; for (int orderIdx = fromOrder - 1; orderIdx >= 0 && (fromOrder - orderIdx) <= CONTEXT_WORDS; --orderIdx) { const WordRef& word = words[wordOrder[orderIdx]]; - const auto stripped = stripTrailingInsertedHyphen(cleanWordText(word.text), word.endsWithInsertedHyphen); + const auto stripped = stripTrailingInsertedHyphen(cleanWordText(wordStore.text(word)), word.endsWithInsertedHyphen); if (stripped.find_first_not_of(' ') == std::string::npos) continue; beforeStart = beforeStart.empty() ? stripped : stripped + ' ' + beforeStart; } std::string afterEnd; for (int orderIdx = toOrder + 1; orderIdx < totalOrder && (orderIdx - toOrder) <= CONTEXT_WORDS; ++orderIdx) { const WordRef& word = words[wordOrder[orderIdx]]; - const auto stripped = stripTrailingInsertedHyphen(cleanWordText(word.text), word.endsWithInsertedHyphen); + const auto stripped = stripTrailingInsertedHyphen(cleanWordText(wordStore.text(word)), word.endsWithInsertedHyphen); if (stripped.find_first_not_of(' ') == std::string::npos) continue; afterEnd = afterEnd.empty() ? stripped : afterEnd + ' ' + stripped; } @@ -172,14 +176,11 @@ ClippingResult build(const std::vector& words, const std::vector& if (midEnd > toOrder) midEnd = toOrder; for (int orderIdx = midStart; orderIdx <= midEnd; ++orderIdx) { const WordRef& word = words[wordOrder[orderIdx]]; - const auto wordText = stripTrailingInsertedHyphen(cleanWordText(word.text), word.endsWithInsertedHyphen); + const auto wordText = stripTrailingInsertedHyphen(cleanWordText(wordStore.text(word)), word.endsWithInsertedHyphen); if (!midText.empty()) midText += ' '; midText += wordText; } - LOG_DBG("CLIP", "Built clipping: words=%d start=\"%.24s\" end=\"%.24s\"", toOrder - fromOrder + 1, - startAnchor.c_str(), endAnchor.c_str()); - return ClippingResult{std::move(text), wordOrder[fromOrder], wordOrder[toOrder], diff --git a/src/clippings/ClipTextBuilder.h b/src/clippings/ClipTextBuilder.h index b4136dd37f..d73670e0c4 100644 --- a/src/clippings/ClipTextBuilder.h +++ b/src/clippings/ClipTextBuilder.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "activities/ActivityResult.h" @@ -7,7 +8,7 @@ namespace ClipTextBuilder { -ClippingResult build(const std::vector& words, const std::vector& wordOrder, int fromOrder, int toOrder, +ClippingResult build(const ClipWordStore& wordStore, const uint16_t* wordOrder, int fromOrder, int toOrder, int totalOrder, int startPageInSection, int sectionPageCount); } // namespace ClipTextBuilder diff --git a/src/clippings/ClippingsManager.cpp b/src/clippings/ClippingsManager.cpp index e0fce1dc49..8f08738258 100644 --- a/src/clippings/ClippingsManager.cpp +++ b/src/clippings/ClippingsManager.cpp @@ -154,6 +154,5 @@ bool ClippingsManager::saveClipping(const std::string& bookTitle, const std::str return false; } - LOG_DBG("CLIP", "Saved clipping to %s (%zu bytes)", CLIPPINGS_PATH, textLen); return true; } diff --git a/src/components/CompactHeader.cpp b/src/components/CompactHeader.cpp index 796fea336b..7d5c76b277 100644 --- a/src/components/CompactHeader.cpp +++ b/src/components/CompactHeader.cpp @@ -27,7 +27,9 @@ int titleBaselineY(const GfxRenderer& renderer, const ThemeMetrics& metrics) { } // namespace namespace CompactHeader { -int headerBottomY(const ThemeMetrics& metrics) { return metrics.topPadding + visibleHeaderHeight(metrics); } +int height(const ThemeMetrics& metrics) { return visibleHeaderHeight(metrics); } + +int headerBottomY(const ThemeMetrics& metrics) { return metrics.topPadding + height(metrics); } int contentTop(const ThemeMetrics& metrics) { return headerBottomY(metrics) + kHeaderTopGap; } diff --git a/src/components/CompactHeader.h b/src/components/CompactHeader.h index d2e5ef8411..2a1b720c83 100644 --- a/src/components/CompactHeader.h +++ b/src/components/CompactHeader.h @@ -4,6 +4,7 @@ class GfxRenderer; struct ThemeMetrics; namespace CompactHeader { +int height(const ThemeMetrics& metrics); int headerBottomY(const ThemeMetrics& metrics); int contentTop(const ThemeMetrics& metrics); void drawTitle(const GfxRenderer& renderer, const char* title, bool showDate = false); diff --git a/src/components/HeaderDate.cpp b/src/components/HeaderDate.cpp index 6f3d669340..4d690fab99 100644 --- a/src/components/HeaderDate.cpp +++ b/src/components/HeaderDate.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -15,10 +14,41 @@ namespace { constexpr int kHeaderDateRightInset = 12; constexpr int kHeaderDateBottomGap = 10; +char dateSeparatorChar() { + switch (SETTINGS.dateSeparator) { + case CrossPointSettings::DATE_SEPARATOR_PERIOD: + return '.'; + case CrossPointSettings::DATE_SEPARATOR_HYPHEN: + return '-'; + case CrossPointSettings::DATE_SEPARATOR_SLASH: + default: + return '/'; + } +} + bool formatHeaderDate(char* buf, const size_t len) { - if (!gpio.deviceIsX3()) return false; + if (!halClock.isAvailable()) return false; if (!SETTINGS.clockDateHasBeenSynced) return false; +#if defined(SIMULATOR) && !defined(CROSSPOINT_SIMULATOR_HAS_DATE_FORMAT) + // Keep compatibility with older downloaded simulator libraries. return halClock.formatDate(buf, len, SETTINGS.clockUtcOffsetQ); +#elif defined(SIMULATOR) && !defined(CROSSPOINT_SIMULATOR_HAS_DATE_SEPARATOR) + // Older simulator libraries support date formats but always emit slashes. + if (!halClock.formatDate(buf, len, SETTINGS.clockUtcOffsetQ, + static_cast(SETTINGS.dateFormat))) { + return false; + } + const char separator = dateSeparatorChar(); + if (separator != '/') { + for (char* p = buf; *p != '\0'; ++p) { + if (*p == '/') *p = separator; + } + } + return true; +#else + return halClock.formatDate(buf, len, SETTINGS.clockUtcOffsetQ, static_cast(SETTINGS.dateFormat), + dateSeparatorChar()); +#endif } } // namespace diff --git a/src/components/OptionPopup.h b/src/components/OptionPopup.h index b4e4e5f215..00099dce78 100644 --- a/src/components/OptionPopup.h +++ b/src/components/OptionPopup.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -8,6 +9,7 @@ #include "GfxRenderer.h" #include "MappedInputManager.h" #include "components/UITheme.h" +#include "fontIds.h" class OptionPopup { public: @@ -41,6 +43,14 @@ class OptionPopup { activate(currentIndex); } + void show(const char* titleStr, const std::vector& options, int currentIndex, + std::function onSelect) { + title = titleStr; + ownedStrings = options; + onSelectCallback = std::move(onSelect); + activate(currentIndex); + } + bool handleInput(MappedInputManager& input, const std::function& requestUpdate) { if (!active) return false; @@ -49,13 +59,61 @@ class OptionPopup { active = false; return true; } + int tx = 0; + int ty = 0; + if (input.wasScreenTouchDown(tx, ty)) { + touchDownOptionIndex = -1; + const auto& hitLayout = getLayout(input.getRenderer()); + for (int i = 0; i < static_cast(hitLayout.options.size()); i++) { + if (contains(hitLayout.options[i], tx, ty)) { + const int optionIndex = hitLayout.firstOptionIndex + i; + touchDownOptionIndex = optionIndex; + if (selectedIndex != optionIndex) { + selectedIndex = optionIndex; + layoutValid = false; + requestUpdate(); + } + break; + } + } + return true; + } + + if (input.wasScreenTapped(tx, ty)) { + if (touchDownOptionIndex >= 0) { + selectedIndex = touchDownOptionIndex; + touchDownOptionIndex = -1; + active = false; + if (onSelectCallback) onSelectCallback(selectedIndex); + requestUpdate(); + return true; + } + const auto& hitLayout = getLayout(input.getRenderer()); + for (int i = 0; i < static_cast(hitLayout.options.size()); i++) { + if (contains(hitLayout.options[i], tx, ty)) { + selectedIndex = hitLayout.firstOptionIndex + i; + active = false; + if (onSelectCallback) onSelectCallback(selectedIndex); + requestUpdate(); + return true; + } + } + // Taps on the dialog chrome (title, padding) keep the popup open; taps outside dismiss it + if (contains(hitLayout.dialog, tx, ty)) return true; + active = false; + requestUpdate(); + return true; + } + if (input.wasPressed(MappedInputManager::Button::Up) || input.wasPressed(MappedInputManager::Button::Left)) { selectedIndex = (selectedIndex - 1 + count) % count; + layoutValid = false; requestUpdate(); return true; } else if (input.wasPressed(MappedInputManager::Button::Down) || input.wasPressed(MappedInputManager::Button::Right)) { selectedIndex = (selectedIndex + 1) % count; + layoutValid = false; requestUpdate(); return true; } else if (input.wasPressed(MappedInputManager::Button::Confirm)) { @@ -76,7 +134,7 @@ class OptionPopup { bool processRender(GfxRenderer& renderer, const MappedInputManager& input) const { if (!active) return false; const auto popupLabels = input.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); - GUI.drawButtonHints(renderer, popupLabels.btn1, popupLabels.btn2, popupLabels.btn3, popupLabels.btn4); + GUI.drawButtonHints(renderer, popupLabels.btn1, popupLabels.btn2, popupLabels.btn3, popupLabels.btn4, true); render(renderer); renderer.displayBuffer(); return true; @@ -90,13 +148,97 @@ class OptionPopup { bool isActive() const { return active; } private: + struct Layout { + Rect dialog{0, 0, 0, 0}; + std::vector options; + int firstOptionIndex = 0; + }; + + // Text measurement is expensive and wasScreenTouchDown() is level-triggered, so the + // layout is computed once per show() and cached rather than rebuilt every loop(). + const Layout& getLayout(const GfxRenderer& renderer) const { + if (layoutValid) return layout; + + const auto& metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + const int optionFontId = metrics.optionPopupUseSmallFont ? UI_10_FONT_ID : UI_12_FONT_ID; + const EpdFontFamily::Style optionStyle = + metrics.optionPopupOptionFontBold ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR; + + const int itemSpacing = metrics.optionPopupItemSpacing; + const int innerPadding = metrics.optionPopupInnerPadding; + const int selectionHPadding = metrics.optionPopupSelectionHPadding; + const int selectionVPadding = metrics.optionPopupSelectionVPadding; + + const int optionLineHeight = renderer.getLineHeight(optionFontId); + const int titleLineHeight = renderer.getLineHeight(UI_12_FONT_ID); + const int rowHeight = optionLineHeight + selectionVPadding * 2; + + int maxTextWidth = renderer.getTextWidth(UI_12_FONT_ID, title.c_str(), EpdFontFamily::BOLD); + for (const auto& opt : ownedStrings) { + const int width = renderer.getTextWidth(optionFontId, opt.c_str(), optionStyle); + if (width > maxTextWidth) maxTextWidth = width; + } + + const int optionCount = static_cast(ownedStrings.size()); + const int maxDialogH = std::max(rowHeight + titleLineHeight + metrics.optionPopupTitleGap + innerPadding * 2, + pageHeight - metrics.buttonHintsHeight - metrics.optionPopupDialogSideMargin * 2); + const int dialogW = std::min((maxTextWidth + innerPadding * 2 + selectionHPadding * 2 + metrics.scrollBarWidth + + metrics.scrollBarRightOffset + selectionHPadding) * + 12 / 10, + pageWidth - metrics.optionPopupDialogSideMargin * 2); + const int titleContentWidth = std::max(1, dialogW - innerPadding * 2); + const int maxTitleLines = + std::max(1, (maxDialogH - innerPadding * 2 - metrics.optionPopupTitleGap - rowHeight) / titleLineHeight); + const auto titleLines = + renderer.wrappedText(UI_12_FONT_ID, title.c_str(), titleContentWidth, maxTitleLines, EpdFontFamily::BOLD); + const int titleHeight = static_cast(titleLines.size()) * titleLineHeight; + const int maxListHeight = + std::max(rowHeight, maxDialogH - innerPadding * 2 - titleHeight - metrics.optionPopupTitleGap); + const int rowStep = rowHeight + itemSpacing; + const int visibleCount = std::max(1, std::min(optionCount, (maxListHeight + itemSpacing) / rowStep)); + const int safeSelectedIndex = std::clamp(selectedIndex, 0, optionCount - 1); + const int visibleStart = std::clamp(safeSelectedIndex - visibleCount / 2, 0, optionCount - visibleCount); + const int listHeight = rowHeight * visibleCount + itemSpacing * (visibleCount - 1); + const bool hasHiddenOptions = visibleCount < optionCount; + const int scrollBarGutter = + hasHiddenOptions ? metrics.scrollBarWidth + metrics.scrollBarRightOffset + selectionHPadding : 0; + const int contentHeight = titleHeight + metrics.optionPopupTitleGap + listHeight; + const int dialogH = contentHeight + innerPadding * 2; + const int dialogX = (pageWidth - dialogW) / 2; + const int dialogY = (pageHeight - dialogH) / 2; + const int itemRectX = dialogX + innerPadding; + const int itemRectW = std::max(1, dialogW - innerPadding * 2 - scrollBarGutter); + const int firstItemY = dialogY + innerPadding + titleHeight + metrics.optionPopupTitleGap; + + layout.dialog = Rect{dialogX, dialogY, dialogW, dialogH}; + layout.firstOptionIndex = visibleStart; + layout.options.clear(); + layout.options.reserve(visibleCount); + for (int i = 0; i < visibleCount; i++) { + layout.options.push_back(Rect{itemRectX, firstItemY + i * (rowHeight + itemSpacing), itemRectW, rowHeight}); + } + layoutValid = true; + return layout; + } + + static bool contains(const Rect& rect, const int x, const int y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; + } + bool active = false; std::string title; std::vector ownedStrings; int selectedIndex = 0; + int touchDownOptionIndex = -1; std::function onSelectCallback; + mutable Layout layout; + mutable bool layoutValid = false; void activate(int currentIndex) { + layoutValid = false; + touchDownOptionIndex = -1; if (ownedStrings.empty()) { active = false; onSelectCallback = nullptr; diff --git a/src/components/TouchHeaderBackButton.cpp b/src/components/TouchHeaderBackButton.cpp new file mode 100644 index 0000000000..04179a9821 --- /dev/null +++ b/src/components/TouchHeaderBackButton.cpp @@ -0,0 +1,127 @@ +#include "TouchHeaderBackButton.h" + +#include + +#include + +#include "UIThemeTokens.h" +#include "components/CompactHeader.h" +#include "components/HeaderDate.h" +#include "components/UITheme.h" +#include "components/UiAppHelpers.h" +#include "components/icons/touchHeaderIcons.h" +#include "fontIds.h" + +namespace fui = freeink::ui; + +namespace TouchHeaderBackButton { + +namespace { +constexpr int actionSize = 52; +constexpr int touchSize = 68; +constexpr int titleGap = 8; +constexpr int defaultRightReserve = 52; + +int effectiveVerticalOffset(const Layout& layout, const Rect& header, const int requestedOffset) { + const int iconBottom = layout.iconRect.y + (layout.iconRect.height + ICON_SIZE) / 2; + const int availableOffset = std::max(0, header.y + header.height - iconBottom); + return std::clamp(requestedOffset, 0, availableOffset); +} +} // namespace + +Layout layout(const Rect& header) { + const int actionWidth = std::min(actionSize, header.width); + const int actionHeight = std::min(actionSize, header.height); + // Center the action lane vertically so taller theme headers split their + // spare space above and below the back button/title instead of placing all + // of it above the title. + const int actionX = header.x; + const int actionY = header.y + (header.height - actionHeight) / 2; + const int touchWidth = std::min(touchSize, header.width); + const int touchHeight = touchSize; + const int touchX = std::max(header.x, actionX + (actionWidth - touchWidth) / 2); + // The common 45px header is shorter than the minimum touch target. Let the + // hit box extend above and below the visual band instead of shrinking it. + const int touchY = std::max(0, actionY + (actionHeight - touchHeight) / 2); + return {Rect{actionX, actionY, actionWidth, actionHeight}, Rect{touchX, touchY, touchWidth, touchHeight}, + actionX + actionWidth + titleGap}; +} + +Rect standardHeaderRect(const GfxRenderer& renderer) { + const auto& metrics = UITheme::getInstance().getMetrics(); + return Rect{0, metrics.topPadding, renderer.getScreenWidth(), metrics.headerHeight}; +} + +int height(const ThemeMetrics& metrics, const MappedInputManager& input) { + return input.hasTouchHardware() ? CompactHeader::height(metrics) : metrics.headerHeight; +} + +Rect headerRect(const GfxRenderer& renderer, const MappedInputManager& input) { + const auto& metrics = UITheme::getInstance().getMetrics(); + return Rect{0, metrics.topPadding, renderer.getScreenWidth(), height(metrics, input)}; +} + +Rect compactHeaderRect(const GfxRenderer& renderer) { + const auto& metrics = UITheme::getInstance().getMetrics(); + return Rect{0, metrics.topPadding, renderer.getScreenWidth(), + CompactHeader::headerBottomY(metrics) - metrics.topPadding}; +} + +bool wasTapped(const MappedInputManager& input, const Rect& header) { + if (!input.hasTouchHardware()) return false; + const Rect touchRect = layout(header).touchRect; + return input.wasTapInRect(touchRect.x, touchRect.y, touchRect.width, touchRect.height); +} + +bool wasTapped(const MappedInputManager& input, const GfxRenderer& renderer) { + return wasTapped(input, headerRect(renderer, input)); +} + +void draw(GfxRenderer& renderer, const Rect& header, const char* title, const bool readerContext, + const int rightReserve, const char* subtitle, const int verticalOffset) { + auto target = makeUiTarget(renderer); + draw(renderer, target, header, title, readerContext, rightReserve, subtitle, verticalOffset); +} + +void draw(GfxRenderer& renderer, fui::GfxRendererTarget& target, const Rect& header, const char* title, + const bool readerContext, const int rightReserve, const char* subtitle, const int verticalOffset) { + Layout back = layout(header); + const int offset = effectiveVerticalOffset(back, header, verticalOffset); + back.iconRect.y += offset; + // Keep the status row on the same baseline as Home. The optional offset only + // adds breathing room below it for the navigation icon and title. + GUI.drawHeader(renderer, header, "", subtitle, readerContext); + + fui::TextStyle titleStyle = uiThemeTokens(target).titleText; + titleStyle.align = fui::TextAlign::Left; + titleStyle.maxLines = 1; + const int reservedRight = rightReserve > 0 ? rightReserve : defaultRightReserve; + const int titleWidth = std::max(0, header.x + header.width - reservedRight - back.titleX); + target.text(fui::Rect{static_cast(back.titleX), static_cast(back.iconRect.y), + static_cast(titleWidth), static_cast(back.iconRect.height)}, + title, titleStyle); + + const int iconX = back.iconRect.x + (back.iconRect.width - ICON_SIZE) / 2; + const int iconY = back.iconRect.y + (back.iconRect.height - ICON_SIZE) / 2; + target.bitmap(fui::Rect{static_cast(iconX), static_cast(iconY), ICON_SIZE, ICON_SIZE}, + fui::bitmapFromIcon(icon_back_32), fui::BitmapMode::Center); +} + +void drawCompact(GfxRenderer& renderer, const char* title, const bool readerContext, const bool showDate, + const int verticalOffset) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect header = compactHeaderRect(renderer); + const int rightReserve = + metrics.batteryWidth + 2 * metrics.headerSidePadding + (showDate ? headerDateReservedWidth(renderer) : 0); + draw(renderer, header, title, readerContext, rightReserve, nullptr, verticalOffset); + if (showDate) { + const Layout back = layout(header); + const int offset = effectiveVerticalOffset(back, header, verticalOffset); + const int titleBaselineY = back.iconRect.y + offset + + std::max(0, (back.iconRect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2) + + renderer.getFontAscenderSize(UI_12_FONT_ID); + drawHeaderDateAtBaseline(renderer, renderer.getScreenWidth(), titleBaselineY); + } +} + +} // namespace TouchHeaderBackButton diff --git a/src/components/TouchHeaderBackButton.h b/src/components/TouchHeaderBackButton.h new file mode 100644 index 0000000000..5f48cff48f --- /dev/null +++ b/src/components/TouchHeaderBackButton.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "GfxRenderer.h" +#include "MappedInputManager.h" +#include "themes/BaseTheme.h" + +namespace TouchHeaderBackButton { + +constexpr int ICON_SIZE = 32; +// Keep navigation controls close to the divider while leaving the status row unchanged. +constexpr int TITLE_VERTICAL_OFFSET = 11; + +struct Layout { + Rect iconRect; + Rect touchRect; + int titleX; +}; + +Layout layout(const Rect& header); +int height(const ThemeMetrics& metrics, const MappedInputManager& input); +Rect headerRect(const GfxRenderer& renderer, const MappedInputManager& input); +Rect standardHeaderRect(const GfxRenderer& renderer); +Rect compactHeaderRect(const GfxRenderer& renderer); +bool wasTapped(const MappedInputManager& input, const Rect& header); +bool wasTapped(const MappedInputManager& input, const GfxRenderer& renderer); +void draw(GfxRenderer& renderer, const Rect& header, const char* title, bool readerContext, int rightReserve = 0, + const char* subtitle = nullptr, int verticalOffset = TITLE_VERTICAL_OFFSET); +void draw(GfxRenderer& renderer, freeink::ui::GfxRendererTarget& target, const Rect& header, const char* title, + bool readerContext, int rightReserve = 0, const char* subtitle = nullptr, + int verticalOffset = TITLE_VERTICAL_OFFSET); +void drawCompact(GfxRenderer& renderer, const char* title, bool readerContext = false, bool showDate = false, + int verticalOffset = TITLE_VERTICAL_OFFSET); + +} // namespace TouchHeaderBackButton diff --git a/src/components/TouchRegistry.cpp b/src/components/TouchRegistry.cpp new file mode 100644 index 0000000000..3ee7f31de8 --- /dev/null +++ b/src/components/TouchRegistry.cpp @@ -0,0 +1,54 @@ +#include "TouchRegistry.h" + +#if CROSSINK_APP_CAP_TOUCH + +namespace { +bool contains(const Rect& rect, const int x, const int y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} +} // namespace + +TouchRegistry& TouchRegistry::getInstance() { + static TouchRegistry instance; + return instance; +} + +void TouchRegistry::beginFrame() { + if (!enabled_) return; + counts_[backIndex()] = 0; +} + +void TouchRegistry::add(const Rect& rect, const int id, const Kind kind) { + if (!enabled_) return; + const uint8_t bufferIndex = backIndex(); + size_t& count = counts_[bufferIndex]; + if (count >= CAPACITY) return; + buffers_[bufferIndex][count] = Target{rect, static_cast(id), static_cast(kind)}; + ++count; +} + +void TouchRegistry::publish() { + if (!enabled_) return; + live_.store(backIndex(), std::memory_order_release); +} + +void TouchRegistry::clear() { + counts_[0] = 0; + counts_[1] = 0; + live_.store(0, std::memory_order_release); +} + +bool TouchRegistry::hitTest(const int x, const int y, const Kind kind, int& outId) const { + if (!enabled_) return false; + const uint8_t bufferIndex = live_.load(std::memory_order_acquire); + const size_t count = counts_[bufferIndex]; + const auto& buffer = buffers_[bufferIndex]; + for (size_t i = count; i-- > 0;) { + if (buffer[i].kind == kind && contains(buffer[i].rect, x, y)) { + outId = buffer[i].id; + return true; + } + } + return false; +} +#endif diff --git a/src/components/TouchRegistry.h b/src/components/TouchRegistry.h new file mode 100644 index 0000000000..98c641ecaf --- /dev/null +++ b/src/components/TouchRegistry.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +#include "AppCapabilities.h" +#include "components/themes/BaseTheme.h" + +#if CROSSINK_APP_CAP_TOUCH +class TouchRegistry { + public: + enum Kind : uint8_t { Item = 0, Back = 1, Tab = 2, Cover = 3, Button = 4 }; + + static TouchRegistry& getInstance(); + + void setEnabled(bool enabled) { enabled_ = enabled; } + bool isEnabled() const { return enabled_; } + + void beginFrame(); + void add(const Rect& rect, int id, Kind kind); + void publish(); + void clear(); + bool hitTest(int x, int y, Kind kind, int& outId) const; + + private: + static constexpr size_t CAPACITY = 64; + + struct Target { + Target() : rect(0, 0, 0, 0), id(-1), kind(Item) {} + Target(const Rect& rect, int16_t id, uint8_t kind) : rect(rect), id(id), kind(kind) {} + + Rect rect; + int16_t id; + uint8_t kind; + }; + + uint8_t backIndex() const { return live_.load(std::memory_order_relaxed) ^ 1u; } + + std::array, 2> buffers_{}; + std::array counts_{0, 0}; + std::atomic live_{0}; + bool enabled_ = false; +}; +#else +// Header-only empty type lets shared render code compile while the compiler +// removes every registration call from button-only images. +class TouchRegistry { + public: + enum Kind : uint8_t { Item = 0, Back = 1, Tab = 2, Cover = 3, Button = 4 }; + + static TouchRegistry& getInstance() { + static TouchRegistry instance; + return instance; + } + + constexpr void setEnabled(bool) {} + constexpr bool isEnabled() const { return false; } + constexpr void beginFrame() {} + constexpr void add(const Rect&, int, Kind) {} + constexpr void publish() {} + constexpr void clear() {} + constexpr bool hitTest(int, int, Kind, int&) const { return false; } +}; +#endif diff --git a/src/components/UIScale.h b/src/components/UIScale.h new file mode 100644 index 0000000000..e79dd8c9fe --- /dev/null +++ b/src/components/UIScale.h @@ -0,0 +1,33 @@ +#pragma once +#include "CrossPointSettings.h" +#include "fontIds.h" + +// Maps the uiScale setting to the FreeInkUI font slots. Small and body use the +// same font so list labels and their values have the same visible size. Row +// heights, header height, and touch sizes are derived from the body font's line +// height by FreeInkApp. +struct UIScaleSpec { + int smallFontId; + int bodyFontId; + int titleFontId; +}; + +inline UIScaleSpec uiScaleSpec() { + UIScaleSpec spec{}; + switch (SETTINGS.uiScale) { + case CrossPointSettings::UI_SCALE_SMALL: + spec.bodyFontId = UI_10_FONT_ID; + spec.titleFontId = UI_12_FONT_ID; + break; + case CrossPointSettings::UI_SCALE_LARGE: + spec.bodyFontId = UI_12_FONT_ID; + spec.titleFontId = UI_12_FONT_ID; + break; + default: + spec.bodyFontId = UI_12_FONT_ID; + spec.titleFontId = UI_12_FONT_ID; + break; + } + spec.smallFontId = spec.bodyFontId; + return spec; +} diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index b22250fdcf..39596a40cf 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -91,6 +92,29 @@ void UITheme::setTheme(CrossPointSettings::UI_THEME type) { currentMetrics = &BaseMetrics::values; break; } + metricsValid = false; +} + +const ThemeMetrics& UITheme::getMetrics() const { +#if CROSSINK_APP_CAP_TOUCH + // hasTouch() can flip once touch init completes after static construction, so the + // cached copy is refreshed when the flag differs instead of copying the struct per call. + const bool touch = gpio.hasTouch(); + if (!metricsValid || touch != metricsForTouch) { + adjustedMetrics = *currentMetrics; + if (touch) { + adjustedMetrics.buttonHintsHeight = 0; + } + metricsForTouch = touch; + metricsValid = true; + } +#else + if (!metricsValid) { + adjustedMetrics = *currentMetrics; + metricsValid = true; + } +#endif + return adjustedMetrics; } int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints, @@ -109,8 +133,7 @@ int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader reservedHeight += metrics.verticalSpacing + metrics.buttonHintsHeight; } const int availableHeight = renderer.getScreenHeight() - reservedHeight - extraReservedHeight; - int rowHeight = hasSubtitle ? metrics.listWithSubtitleRowHeight : metrics.listRowHeight; - return availableHeight / rowHeight; + return UITheme::getInstance().getTheme().getListPageItems(availableHeight, hasSubtitle); } // Screen area excluding the button hints @@ -217,24 +240,17 @@ UIIcon UITheme::getFileIcon(const std::string& filename) { int UITheme::getStatusBarHeight() { const ThemeMetrics& metrics = UITheme::getInstance().getMetrics(); - - // Add status bar margin - const bool showStatusBar = SETTINGS.statusBarChapterPageCount || SETTINGS.stablePageNumbers || - SETTINGS.statusBarBookProgressPercentage || - SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || - SETTINGS.statusBarTimeLeft != CrossPointSettings::STATUS_BAR_TIME_LEFT::TIME_LEFT_HIDE || - SETTINGS.statusBarBattery; - const bool showProgressBar = - SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS; - return (showStatusBar ? (metrics.statusBarVerticalMargin) : 0) + - (showProgressBar ? (((SETTINGS.statusBarProgressBarThickness + 1) * 2) + metrics.progressBarMarginTop) : 0); + const auto statusBar = SETTINGS.statusBarSpec(); + // Reserve the clock lane independently of the current board so orientation + // and layout do not change when the same settings are used on another device. + return (statusBar.textLaneVisible(true) ? metrics.statusBarVerticalMargin : 0) + + (statusBar.showsProgressBar() ? statusBar.progressBarHeightPx + metrics.progressBarMarginTop : 0); } int UITheme::getProgressBarHeight() { const ThemeMetrics& metrics = UITheme::getInstance().getMetrics(); - const bool showProgressBar = - SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS; - return (showProgressBar ? (((SETTINGS.statusBarProgressBarThickness + 1) * 2) + metrics.progressBarMarginTop) : 0); + const auto statusBar = SETTINGS.statusBarSpec(); + return statusBar.showsProgressBar() ? statusBar.progressBarHeightPx + metrics.progressBarMarginTop : 0; } // Centered text implementation that takes the safe area into account diff --git a/src/components/UITheme.h b/src/components/UITheme.h index 9eb2844f2a..6ef244bd5d 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -16,7 +16,7 @@ class UITheme { UITheme(); static UITheme& getInstance() { return instance; } - const ThemeMetrics& getMetrics() const { return *currentMetrics; } + const ThemeMetrics& getMetrics() const; const BaseTheme& getTheme() const { return *currentTheme; } Rect getScreenSafeArea(const GfxRenderer& renderer, bool hasFrontButtonHints = false, bool hasSideButtonHints = false); @@ -45,6 +45,9 @@ class UITheme { private: const ThemeMetrics* currentMetrics; std::unique_ptr currentTheme; + mutable ThemeMetrics adjustedMetrics; + mutable bool metricsValid = false; + mutable bool metricsForTouch = false; }; // Helper macro to access current theme diff --git a/src/components/UIThemeTokens.h b/src/components/UIThemeTokens.h new file mode 100644 index 0000000000..a608cb429a --- /dev/null +++ b/src/components/UIThemeTokens.h @@ -0,0 +1,109 @@ +#pragma once +#ifndef SIMULATOR +#include +#endif +#include +#include +#include + +#include +#include +#include + +#include "CrossPointSettings.h" +#include "UITheme.h" + +namespace UiThemeTokensDetail { +inline int16_t scaledListMetric(const int metric) { + constexpr int baseFontSize = 10; + int scaleFontSize = baseFontSize; + switch (SETTINGS.uiScale) { + case CrossPointSettings::UI_SCALE_LARGE: + scaleFontSize = 12; + break; + case CrossPointSettings::UI_SCALE_SMALL: + default: + break; + } + return static_cast((metric * scaleFontSize + baseFontSize / 2) / baseFontSize); +} +} // namespace UiThemeTokensDetail + +enum class UiListRowType : uint8_t { + SingleLine, + WithSubtitle, +}; + +inline int16_t uiListRowHeight(const freeink::ui::ThemeTokens& tokens, const UiListRowType rowType) { + if (gpio.hasTouch()) return tokens.rowHeight; + + const ThemeMetrics& metrics = UITheme::getInstance().getMetrics(); + switch (rowType) { + case UiListRowType::SingleLine: + return UiThemeTokensDetail::scaledListMetric(metrics.listRowHeight); + case UiListRowType::WithSubtitle: + return UiThemeTokensDetail::scaledListMetric(metrics.listWithSubtitleRowHeight); + } + return tokens.rowHeight; +} + +inline uint16_t configureUiList(freeink::ui::ListProps& props, const freeink::ui::ThemeTokens& tokens, + const freeink::ui::Rect rect, const UiListRowType rowType = UiListRowType::SingleLine) { + // Button-only menus used compact, single-line rows before their FreeInkUI + // migration. Keep that layout unless the caller explicitly needs a subtitle. +#if CROSSINK_APP_CAP_TOUCH + if (!gpio.hasTouch() && rowType == UiListRowType::SingleLine) props.labelText.maxLines = 1; +#else + if (rowType == UiListRowType::SingleLine) props.labelText.maxLines = 1; +#endif + if (props.rowHeight <= 0) props.rowHeight = uiListRowHeight(tokens, rowType); + // Subtitle rows may wrap their labels, so retain FreeInkUI's font-derived + // two-line height and avoid overlapping the following item. + if (props.labelText.maxLines > 1) props.rowHeight = std::max(props.rowHeight, tokens.rowHeight); + if (props.rowGap < 0) props.rowGap = tokens.listRowGap; + return freeink::ui::listVisibleRows(rect, props.rowHeight, props.rowGap); +} + +inline void configureUiListSectionHeaders(freeink::ui::ListProps& props, const freeink::ui::ThemeTokens& tokens) { + if (SETTINGS.uiTheme != CrossPointSettings::UI_THEME::ROUNDEDRAFF) return; + + props.headerText = tokens.titleText; + props.headerText.bold = true; + props.sectionGap = halTiltSensor.isAvailable() ? 10 : 20; +} + +inline const char* uiListSectionHeaderLabel(std::string& storage, const char* label) { + if (SETTINGS.uiTheme != CrossPointSettings::UI_THEME::ROUNDEDRAFF) return label; + + storage = label != nullptr ? label : ""; + std::transform(storage.begin(), storage.end(), storage.begin(), + [](const unsigned char c) { return static_cast(std::toupper(c)); }); + return storage.c_str(); +} + +// Merges the active UITheme's shape with uiScale-derived sizes. Touch builds +// retain FreeInkUI's larger font-derived rows; button devices use the theme's +// compact one-line/two-line metrics, scaled with the selected UI font size. +inline freeink::ui::ThemeTokens uiThemeTokens(const freeink::ui::GfxRendererTarget& target) { + namespace fui = freeink::ui; + const ThemeMetrics& metrics = UITheme::getInstance().getMetrics(); + + fui::ThemeTokens tokens = fui::themeTokensForLineHeight(target.lineHeight(fui::GfxRendererTarget::FONT_BODY)); + tokens.listRowGap = static_cast(metrics.listRowGap); + tokens.listRowRadius = static_cast(metrics.listRowRadius); + tokens.listInset = static_cast(metrics.listInset); + tokens.listSidePadding = static_cast(metrics.listSidePadding); + tokens.listSelectionStyle = static_cast(metrics.listSelectionStyle); + tokens.listScrollWidth = static_cast(metrics.listScrollWidth); + tokens.listScrollSide = static_cast(metrics.listScrollSide); + // The X4 Pro panel sits recessed behind the bezel, so an edge-hugging scroll + // indicator disappears under it. Push it inward far enough to clear the bezel. +#ifndef SIMULATOR + tokens.listScrollInset = BoardConfig::isX4Pro() ? 7 : 0; +#endif + tokens.headerSidePadding = static_cast(metrics.headerSidePadding); + tokens.headerUnderline = static_cast(metrics.headerUnderlineSize); + tokens.headerTitleAlign = static_cast(metrics.headerTitleAlign); + tokens.bodyText.bold = metrics.listTitleBold; + return tokens; +} diff --git a/src/components/UiAppHelpers.h b/src/components/UiAppHelpers.h new file mode 100644 index 0000000000..d8b9e64977 --- /dev/null +++ b/src/components/UiAppHelpers.h @@ -0,0 +1,192 @@ +#pragma once +#include +#include +#include + +#include "MappedInputManager.h" +#include "components/UIScale.h" +#include "components/UITheme.h" +#include "components/icons/listIcons.h" + +// Shared glue for activities hosting a FreeInkApp: the font-bound render +// target and the touch snapshot FreeInkApp routing consumes. + +// Held duration past which a stationary touch release routes as a long-press +// (InputSnapshot::longPress). Mirrors the physical-button hold-to-act +// convention but shorter, since a finger hold has no button travel to absorb. +// Rows must opt in via InputLongPress to receive it. +inline constexpr unsigned long UI_TOUCH_LONG_PRESS_MS = 500; + +// Bind the uiScale fonts before FreeInkApp's constructor derives its theme +// metrics from the body font's line height. +inline freeink::ui::GfxRendererTarget makeUiTarget(const GfxRenderer& renderer) { + freeink::ui::GfxRendererTarget target(renderer); + const auto spec = uiScaleSpec(); + target.setFont(freeink::ui::GfxRendererTarget::FONT_SMALL, spec.smallFontId); + target.setFont(freeink::ui::GfxRendererTarget::FONT_BODY, spec.bodyFontId); + target.setFont(freeink::ui::GfxRendererTarget::FONT_TITLE, spec.titleFontId); + return target; +} + +// Tap release with coords, plus the raw release the tap classifier never +// reports (swipe end, drag-off) delivered off-target: nothing dispatches, +// but routing drops its pressed-element state instead of ghosting it onto +// the next render. +// Firmware UIIcon -> FreeInkUI bitmap for list rows (SDK-format icons only; +// the legacy drawIcon assets use a different bit layout). Two crisp sizes: +// 24 for single-line rows, 32 for label+subtitle rows. +inline freeink::ui::BitmapRef listIconFor(const UIIcon icon, const int size = 24) { + if (size >= 32) { + switch (icon) { + case UIIcon::Folder: + return freeink::ui::bitmapFromIcon(icon_folder_32); + case UIIcon::Text: + return freeink::ui::bitmapFromIcon(icon_file_text_32); + case UIIcon::Image: + return freeink::ui::bitmapFromIcon(icon_image_32); + case UIIcon::Book: + return freeink::ui::bitmapFromIcon(icon_book_32); + case UIIcon::File: + return freeink::ui::bitmapFromIcon(icon_file_32); + case UIIcon::Wifi: + return freeink::ui::bitmapFromIcon(icon_wifi_32); + case UIIcon::Library: + return freeink::ui::bitmapFromIcon(icon_lyra_library_32); + case UIIcon::Hotspot: + return freeink::ui::bitmapFromIcon(icon_radio_tower_32); + default: + return {}; + } + } + switch (icon) { + case UIIcon::Folder: + return freeink::ui::bitmapFromIcon(icon_folder_24); + case UIIcon::Text: + return freeink::ui::bitmapFromIcon(icon_file_text_24); + case UIIcon::Image: + return freeink::ui::bitmapFromIcon(icon_image_24); + case UIIcon::Book: + return freeink::ui::bitmapFromIcon(icon_book_24); + case UIIcon::File: + return freeink::ui::bitmapFromIcon(icon_file_24); + case UIIcon::Wifi: + return freeink::ui::bitmapFromIcon(icon_wifi_24); + case UIIcon::Library: + return freeink::ui::bitmapFromIcon(icon_lyra_library_24); + case UIIcon::Hotspot: + return freeink::ui::bitmapFromIcon(icon_radio_tower_24); + default: + return {}; + } +} + +// Legacy GfxRenderer icon arrays are pre-rotated. Render SDK Lucide assets through +// its adapter instead so the same bitmap remains correct in every orientation. +inline void drawLucideIcon(const GfxRenderer& renderer, const freeink::Icon& icon, const int x, const int y, + const bool foregroundBlack = true) { + freeink::ui::GfxRendererTarget target(renderer); + target.bitmap(freeink::ui::Rect{static_cast(x), static_cast(y), static_cast(icon.w), + static_cast(icon.h)}, + freeink::ui::bitmapFromIcon(icon), freeink::ui::BitmapMode::Center, + freeink::ui::Paint::solid(foregroundBlack ? freeink::ui::Color::Black : freeink::ui::Color::White)); +} + +// Scroll semantics shared by every FreeInkUI list screen: swipes move the +// viewport (topIndex) without touching the selection; button navigation moves +// the selection and pulls the viewport along just enough to keep it visible. + +inline int scrollListBy(const int topIndex, const int delta, const int visibleRows, const int count) { + int maxTop = count - visibleRows; + if (maxTop < 0) maxTop = 0; + int next = topIndex + delta; + if (next > maxTop) next = maxTop; + if (next < 0) next = 0; + return next; +} + +inline int followListSelection(const int selectedIndex, const int topIndex, const int visibleRows, const int count) { + return static_cast(freeink::ui::listTopIndexFor( + static_cast(selectedIndex), static_cast(topIndex), + static_cast(visibleRows > 0 ? visibleRows : 1), static_cast(count))); +} + +template +inline void drawUiTabBar(freeink::ui::Screen& screen, freeink::ui::TabBarProps props, + const freeink::ui::Rect rect, const ThemeTabBarAppearance appearance) { + namespace fui = freeink::ui; + if (props.tabs == nullptr || props.count == 0) return; + if (fui::textStyleUnset(props.text)) props.text = screen.theme().smallText; + props.minTouchSize = screen.theme().minTouchSize; + + switch (appearance) { + case ThemeTabBarAppearance::Pill: + fui::tabBar(screen.frame(), rect, props); + return; + case ThemeTabBarAppearance::BorderedText: + break; + } + + props.tabInset = fui::Insets{}; + props.contentInset = fui::Insets{}; + props.tabStyles = fui::plainStyles(); + + const int16_t gap = props.gap > 0 ? props.gap : 0; + const int16_t slotWidth = static_cast((rect.width - gap * (props.count - 1)) / props.count); + const int16_t contentHeight = static_cast(rect.height - (props.divider ? 1 : 0)); + for (uint8_t i = 0; i < props.count; ++i) { + const int16_t slotX = static_cast(rect.x + i * (slotWidth + gap)); + const int16_t width = static_cast(i == props.count - 1 ? rect.right() - slotX : slotWidth); + fui::TabBarProps tab = props; + tab.tabs = &props.tabs[i]; + tab.count = 1; + tab.gap = 0; + tab.divider = false; + tab.text.bold = props.tabs[i].selected; + fui::tabBar(screen.frame(), fui::Rect{slotX, rect.y, width, contentHeight}, tab); + } + + screen.target().fill(fui::Rect{rect.x, rect.y, rect.width, 1}, props.dividerPaint); + if (props.divider) { + screen.target().fill(fui::Rect{rect.x, static_cast(rect.bottom() - 1), rect.width, 1}, props.dividerPaint); + } +} + +inline freeink::ui::InputSnapshot touchSnapshotFrom(const MappedInputManager& mappedInput) { + freeink::ui::InputSnapshot snap{}; + int tx = 0; + int ty = 0; + // Live contact position: only InputDrag-masked elements (sliders) react, so + // carrying it in every snapshot is free for ordinary screens. + if (mappedInput.isScreenTouchHeld(tx, ty)) { + snap.touchHeld = true; + snap.touchX = static_cast(tx); + snap.touchY = static_cast(ty); + } + if (mappedInput.wasScreenTouchDown(tx, ty)) { + snap.touchPressed = true; + snap.touchX = static_cast(tx); + snap.touchY = static_cast(ty); + } + unsigned long heldMs = 0; + if (mappedInput.wasScreenTapped(tx, ty, heldMs)) { + snap.touchReleased = true; + // A stationary press held past the threshold routes as a long-press so + // rows masked InputLongPress can offer a hold action (delete / forget). + snap.longPress = heldMs >= UI_TOUCH_LONG_PRESS_MS; + snap.touchX = static_cast(tx); + snap.touchY = static_cast(ty); + } else if (mappedInput.wasScreenTouchReleased()) { + snap.touchReleased = true; + snap.touchX = -1; + snap.touchY = -1; + } + // A swipe is also a cancelled row press. Some touch backends report the + // gesture before exposing their raw release edge, so clear it explicitly + // before list code repaints the new viewport. + if (!snap.touchReleased && mappedInput.wasSwipe() != MappedInputManager::SwipeDir::None) { + snap.touchReleased = true; + snap.touchX = -1; + snap.touchY = -1; + } + return snap; +} diff --git a/src/components/icons/book.h b/src/components/icons/book.h deleted file mode 100644 index 928db1303e..0000000000 --- a/src/components/icons/book.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include - -// size: 32x32 -static const uint8_t BookIcon[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, - 0x07, 0xC0, 0x00, 0x00, 0x03, 0xCF, 0xFF, 0xF8, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, - 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, - 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, - 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, - 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xC3, 0xFF, 0xFC, 0x23, 0xE0, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x1F, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/icons/chart.h b/src/components/icons/chart.h index 93bf3f55ee..4279f44de7 100644 --- a/src/components/icons/chart.h +++ b/src/components/icons/chart.h @@ -25,3 +25,17 @@ static const uint8_t ChartIcon[] = { // rows 27-31: all white 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + +// FreeInkUI draws bitmap rows directly in logical screen coordinates, while +// GfxRenderer::drawIcon rotates this source bitmap for the portrait display. +// Keep a clockwise-rotated copy so list rows show the same vertical chart used +// for Reading Stats in the Lyra themes. +static const uint8_t ChartListIcon[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, + 0xF8, 0x3F, 0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0xF8, 0x38, 0x3F, 0xFF, + 0xF8, 0x38, 0x3F, 0xFF, 0xF8, 0x38, 0x3F, 0xFF, 0xF8, 0x38, 0x3F, 0xFF, 0xF8, 0x38, 0x3F, 0xFF, 0xF8, 0x38, 0x38, + 0x3F, 0xF8, 0x38, 0x38, 0x3F, 0xF8, 0x38, 0x38, 0x3F, 0xF8, 0x38, 0x38, 0x3F, 0xF8, 0x38, 0x38, 0x3F, 0xF8, 0x38, + 0x38, 0x3F, 0xF8, 0x38, 0x38, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, +}; diff --git a/src/components/icons/file24.h b/src/components/icons/file24.h deleted file mode 100644 index 197097b337..0000000000 --- a/src/components/icons/file24.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -#include - -// size: 24x24 -static const uint8_t File24Icon[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x03, 0xF8, 0x00, 0x03, 0xF0, 0x7F, 0xF1, - 0xE2, 0x7F, 0xF9, 0xC6, 0x7F, 0xF9, 0x8E, 0x7F, 0xF9, 0x80, 0x7F, 0xF9, 0x80, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, - 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, - 0x8F, 0xFF, 0xF1, 0xC0, 0x00, 0x01, 0xC0, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/icons/folder.h b/src/components/icons/folder.h deleted file mode 100644 index b0997c45cc..0000000000 --- a/src/components/icons/folder.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include - -// size: 32x32 -static const uint8_t FolderIcon[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x1F, 0xFE, 0x00, 0x00, 0x1F, 0xFE, 0x3F, 0xFF, - 0x9F, 0xFE, 0x7F, 0xFF, 0xCF, 0xFE, 0x7F, 0xFF, 0xCF, 0xFE, 0x7F, 0xFF, 0xCF, 0xFE, 0x7F, 0xFF, 0xCF, 0xFE, 0x7F, - 0xFF, 0xCF, 0xFE, 0x7F, 0xFF, 0xCF, 0xFE, 0x7F, 0xFF, 0xCF, 0xFE, 0x7F, 0xFF, 0xCF, 0xFE, 0x7F, 0xFF, 0xCF, 0xFE, - 0x7F, 0xFF, 0xCF, 0xFE, 0x7F, 0xFF, 0xCF, 0xFE, 0x7F, 0xFF, 0xCF, 0xFC, 0x7F, 0xFF, 0xCF, 0xF0, 0xFF, 0xFF, 0xCF, - 0xE1, 0xFF, 0xFF, 0xCF, 0xE3, 0xFF, 0xFF, 0xCF, 0xE7, 0xFF, 0xFF, 0xCF, 0xE7, 0xFF, 0xFF, 0xCF, 0xE7, 0xFF, 0xFF, - 0xCF, 0xE7, 0xFF, 0xFF, 0xCF, 0xE7, 0xFF, 0xFF, 0xCF, 0xE7, 0xFF, 0xFF, 0x8F, 0xE7, 0xFF, 0xFF, 0x8F, 0xF0, 0x00, - 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/icons/folder24.h b/src/components/icons/folder24.h deleted file mode 100644 index 20f470077f..0000000000 --- a/src/components/icons/folder24.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -#include - -// size: 24x24 -static const uint8_t Folder24Icon[] = { - 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x1F, 0xFC, 0x00, 0x0F, 0xF9, 0xFF, 0xE7, 0xF9, 0xFF, 0xE7, 0xF9, 0xFF, 0xE7, - 0xF9, 0xFF, 0xE7, 0xF9, 0xFF, 0xE7, 0xF9, 0xFF, 0xE7, 0xF9, 0xFF, 0xE7, 0xF9, 0xFF, 0xE7, 0xF9, 0xFF, 0xE7, - 0xF9, 0xFF, 0xE7, 0xF3, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xCF, 0xFF, 0xE7, 0xCF, 0xFF, 0xE7, 0xCF, 0xFF, 0xE7, - 0xCF, 0xFF, 0xE7, 0xCF, 0xFF, 0xE7, 0xCF, 0xFF, 0xE7, 0xE0, 0x00, 0x0F, 0xF0, 0x00, 0x1F, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/icons/frontlightHeaderIcons.h b/src/components/icons/frontlightHeaderIcons.h new file mode 100644 index 0000000000..4483a41be2 --- /dev/null +++ b/src/components/icons/frontlightHeaderIcons.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Icon.h" + +// Generated by libs/assets/Icons/tools/gen_icons.py from Lucide SVGs. Do not edit. +// Sizes: 28px. Icons: 1. + +// sliders-horizontal (lucide: sliders-horizontal) +static const uint8_t icon_sliders_horizontal_28_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFE, 0x3F, + 0xFF, 0xE0, 0x06, 0x00, 0x7F, 0xE0, 0x0E, 0x00, 0x7F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xE0, 0x18, 0x00, 0x7F, 0xE0, + 0x18, 0x00, 0x7F, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xE0, 0x01, 0x80, 0x7F, 0xE0, 0x01, 0x80, 0x7F, 0xFF, 0xFF, 0xCF, + 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_sliders_horizontal_28 = {28, 28, 13, icon_sliders_horizontal_28_bits}; diff --git a/src/components/icons/frontlightHeaderIcons.manifest b/src/components/icons/frontlightHeaderIcons.manifest new file mode 100644 index 0000000000..785cd3c70e --- /dev/null +++ b/src/components/icons/frontlightHeaderIcons.manifest @@ -0,0 +1,6 @@ +# Frontlight header controls. Regenerate with: +# python3 freeink-sdk/libs/assets/Icons/tools/gen_icons.py \ +# --manifest src/components/icons/frontlightHeaderIcons.manifest \ +# --svgdir freeink-sdk/libs/assets/Icons/lucide/icons --sizes 28 \ +# --out src/components/icons/frontlightHeaderIcons.h +sliders-horizontal = sliders-horizontal diff --git a/src/components/icons/hotspot.h b/src/components/icons/hotspot.h deleted file mode 100644 index 8168332515..0000000000 --- a/src/components/icons/hotspot.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include - -// size: 32x32 -static const uint8_t HotspotIcon[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xFE, 0x0F, 0xF0, - 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0xFF, 0xFE, 0x3F, 0xFD, 0xF8, 0x1F, 0xBF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xC3, - 0xC3, 0xFF, 0xFF, 0xCF, 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, - 0xF9, 0x9F, 0xFF, 0xFF, 0xF3, 0xCF, 0xFF, 0xFF, 0xF3, 0xCF, 0xFF, 0xFF, 0xF9, 0x9F, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, - 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0xF3, 0xFF, 0xFF, 0xC3, 0xC3, 0xFF, 0xFF, 0xE0, 0x07, - 0xFF, 0xFD, 0xF8, 0x1F, 0xBF, 0xFC, 0x7F, 0xFF, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x0F, 0xF0, 0x7F, 0xFF, 0x80, - 0x01, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/icons/image24.h b/src/components/icons/image24.h deleted file mode 100644 index 783038041d..0000000000 --- a/src/components/icons/image24.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -#include - -// size: 24x24 -static const uint8_t Image24Icon[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x0F, 0xE0, 0x00, 0x07, 0xCF, 0xF1, 0xF3, 0xCF, 0xE3, 0xF3, - 0xCF, 0xE7, 0xF3, 0xCF, 0xEF, 0xF3, 0xCF, 0xE7, 0xF3, 0xCF, 0xE3, 0xF3, 0xCF, 0xF1, 0xF3, 0xCF, 0xF8, 0xF3, - 0xCF, 0x3C, 0x73, 0xCE, 0x1E, 0x33, 0xCC, 0xCF, 0x13, 0xCC, 0xCF, 0x83, 0xCE, 0x1F, 0xC3, 0xCF, 0x3F, 0xE3, - 0xCF, 0xFF, 0xF3, 0xCF, 0xFF, 0xF3, 0xE0, 0x00, 0x07, 0xF0, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/icons/library.h b/src/components/icons/library.h deleted file mode 100644 index ab79e61ccb..0000000000 --- a/src/components/icons/library.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include - -// size: 32x32 -static const uint8_t LibraryIcon[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, - 0x1F, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xC1, 0xCF, 0xFF, 0xFE, 0x07, 0xE7, 0xFF, 0xF0, 0x3F, 0xE7, 0xFF, 0x81, - 0xFF, 0x87, 0xFC, 0x0F, 0xFC, 0x0F, 0xF0, 0x3F, 0xF0, 0x3F, 0xE1, 0xFF, 0x81, 0xFF, 0xE7, 0xFC, 0x0F, 0xFF, 0xE7, - 0xE0, 0x7F, 0xFF, 0xF3, 0x83, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x07, 0xE7, 0xFF, 0xFF, 0xE7, - 0xE7, 0xFF, 0xFF, 0xE7, 0xE7, 0xFF, 0xFF, 0xE7, 0xE7, 0xFF, 0xFF, 0xE7, 0xE0, 0x00, 0x00, 0x07, 0xE0, 0x00, 0x00, - 0x07, 0xE7, 0xFF, 0xFF, 0xE7, 0xE7, 0xFF, 0xFF, 0xE7, 0xE7, 0xFF, 0xFF, 0xE7, 0xE0, 0x00, 0x00, 0x07, 0xF0, 0x00, - 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/icons/listIcons.h b/src/components/icons/listIcons.h new file mode 100644 index 0000000000..4519a3522a --- /dev/null +++ b/src/components/icons/listIcons.h @@ -0,0 +1,484 @@ +#pragma once + +#include "Icon.h" + +// Generated by libs/assets/Icons/tools/gen_icons.py from Lucide SVGs. Do not edit. +// Sizes: 24, 32px. Icons: 28. + +// folder (lucide: folder) +static const uint8_t icon_folder_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xFF, 0xC0, 0x3F, 0xFF, 0x9F, 0x9F, 0xFF, 0x9F, 0xC0, 0x07, + 0x9F, 0xE0, 0x03, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, + 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, + 0x9F, 0xFF, 0xF9, 0xC0, 0x00, 0x03, 0xE0, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_folder_24 = {24, 24, 12, icon_folder_24_bits}; +static const uint8_t icon_folder_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, + 0xFF, 0xC7, 0xE1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF8, 0x00, 0x07, 0x8F, 0xFC, 0x00, 0x03, 0x8F, 0xFF, + 0xFF, 0xE3, 0x8F, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF1, 0x8F, + 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF1, + 0x8F, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, + 0xF1, 0xCF, 0xFF, 0xFF, 0xF3, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x0F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_folder_32 = {32, 32, 16, icon_folder_32_bits}; + +// file-text (lucide: file-text) +static const uint8_t icon_file_text_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0xFF, 0xF0, 0x00, 0x7F, 0xE7, 0xF8, 0x3F, 0xE7, 0xF9, 0x1F, 0xE7, 0xF9, 0x8F, + 0xE7, 0xF9, 0xC7, 0xE7, 0xF8, 0x07, 0xE6, 0x1C, 0x07, 0xE6, 0x1F, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, + 0xE6, 0x00, 0x67, 0xE6, 0x00, 0x67, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE6, 0x00, 0x67, 0xE6, 0x00, 0x67, + 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xF0, 0x00, 0x0F, 0xF8, 0x00, 0x1F, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_file_text_24 = {24, 24, 12, icon_file_text_24_bits}; +static const uint8_t icon_file_text_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x0F, 0xFF, 0xF8, 0x00, 0x03, 0xFF, 0xF8, 0x00, 0x01, 0xFF, 0xF1, 0xFF, 0x80, + 0xFF, 0xF1, 0xFF, 0x88, 0x7F, 0xF1, 0xFF, 0x8C, 0x3F, 0xF1, 0xFF, 0x8E, 0x1F, 0xF1, 0xFF, 0x8F, 0x1F, 0xF1, 0xFF, + 0x80, 0x0F, 0xF1, 0xFF, 0xC0, 0x0F, 0xF1, 0xC3, 0xE0, 0x0F, 0xF1, 0xC3, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, + 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xC0, 0x03, 0x8F, 0xF1, 0x80, 0x01, 0x8F, 0xF1, 0xC0, 0x03, 0x8F, + 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xC0, 0x03, 0x8F, 0xF1, 0x80, 0x01, 0x8F, 0xF1, 0xC0, 0x03, + 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF8, 0x00, + 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_file_text_32 = {32, 32, 16, icon_file_text_32_bits}; + +// image (lucide: image) +static const uint8_t icon_image_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x0F, 0xE0, 0x00, 0x07, 0xCF, 0xFF, 0xF3, 0xCF, 0xFF, 0xF3, + 0xCF, 0x3F, 0xF3, 0xCE, 0x1F, 0xF3, 0xCC, 0xCF, 0xF3, 0xCC, 0xCF, 0xF3, 0xCE, 0x1F, 0x73, 0xCF, 0x3C, 0x13, + 0xCF, 0xF8, 0x83, 0xCF, 0xF1, 0xC3, 0xCF, 0xE3, 0xE3, 0xCF, 0xC7, 0xF3, 0xCF, 0x8F, 0xF3, 0xCF, 0x1F, 0xF3, + 0xCE, 0x3F, 0xF3, 0xCC, 0x7F, 0xF3, 0xE0, 0x00, 0x07, 0xF0, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_image_24 = {24, 24, 12, icon_image_24_bits}; +static const uint8_t icon_image_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, + 0x07, 0xE3, 0xFF, 0xFF, 0xC7, 0xE7, 0xFF, 0xFF, 0xE7, 0xE7, 0xFF, 0xFF, 0xE7, 0xE7, 0xC3, 0xFF, 0xE7, 0xE7, 0x81, + 0xFF, 0xE7, 0xE7, 0x00, 0xFF, 0xE7, 0xE7, 0x18, 0xFF, 0xE7, 0xE7, 0x18, 0xFF, 0xE7, 0xE7, 0x00, 0xFF, 0xE7, 0xE7, + 0x81, 0xF0, 0xE7, 0xE7, 0xC3, 0xE0, 0x67, 0xE7, 0xFF, 0xC0, 0x07, 0xE7, 0xFF, 0x8F, 0x07, 0xE7, 0xFF, 0x1F, 0x87, + 0xE7, 0xFE, 0x3F, 0xC7, 0xE7, 0xFC, 0x7F, 0xC7, 0xE7, 0xF8, 0xFF, 0xE7, 0xE7, 0xF1, 0xFF, 0xE7, 0xE7, 0xE3, 0xFF, + 0xE7, 0xE7, 0xC7, 0xFF, 0xE7, 0xE7, 0x8F, 0xFF, 0xE7, 0xE2, 0x0F, 0xFF, 0xC7, 0xE0, 0x00, 0x00, 0x07, 0xF0, 0x00, + 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_image_32 = {32, 32, 16, icon_image_32_bits}; + +// book-open (lucide: book-open) +static const uint8_t icon_book_open_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x3C, 0x03, 0x80, 0x18, 0x01, 0x9F, 0xC3, 0xF9, 0x9F, 0xE7, 0xF9, + 0x9F, 0xE7, 0xF9, 0x9F, 0xE7, 0xF9, 0x9F, 0xE7, 0xF9, 0x9F, 0xE7, 0xF9, 0x9F, 0xE7, 0xF9, 0x9F, 0xE7, 0xF9, + 0x9F, 0xE7, 0xF9, 0x9F, 0xE7, 0xF9, 0x9F, 0xE7, 0xF9, 0x9F, 0xE7, 0xF9, 0x9F, 0xE7, 0xF9, 0x80, 0x00, 0x01, + 0xC0, 0x00, 0x03, 0xFF, 0xC3, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_book_open_24 = {24, 24, 11, icon_book_open_24_bits}; +static const uint8_t icon_book_open_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x01, 0x80, + 0x03, 0x8F, 0xF0, 0x0F, 0xF1, 0x8F, 0xFC, 0x3F, 0xF1, 0x8F, 0xFC, 0x3F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, 0x8F, 0xFE, + 0x7F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, 0x8F, + 0xFE, 0x7F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, + 0x8F, 0xFE, 0x7F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, 0x8F, 0xFE, 0x7F, 0xF1, 0xC0, 0x00, 0x00, + 0x03, 0xC0, 0x00, 0x00, 0x03, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFE, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_book_open_32 = {32, 32, 14, icon_book_open_32_bits}; + +// book (lucide: book) +static const uint8_t icon_book_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x0F, 0xF0, 0x00, 0x07, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, + 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, + 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, + 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xF0, 0x00, 0x07, 0xF8, 0x00, 0x0F, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_book_24 = {24, 24, 12, icon_book_24_bits}; +static const uint8_t icon_book_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x0F, 0xF1, 0xFF, 0xFF, + 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, + 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, + 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, + 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, + 0x0F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF8, 0x00, + 0x00, 0x0F, 0xFC, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_book_32 = {32, 32, 17, icon_book_32_bits}; + +// book-marked (lucide: book-marked) +static const uint8_t icon_book_marked_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x0F, 0xF0, 0x00, 0x07, 0xE7, 0x9E, 0x67, 0xE7, 0x9E, 0x67, 0xE7, 0x9E, 0x67, + 0xE7, 0x92, 0x67, 0xE7, 0x80, 0x67, 0xE7, 0x80, 0x67, 0xE7, 0x8C, 0x67, 0xE7, 0x9E, 0x67, 0xE7, 0xFF, 0xE7, + 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, + 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xF0, 0x00, 0x07, 0xF8, 0x00, 0x0F, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_book_marked_24 = {24, 24, 11, icon_book_marked_24_bits}; +static const uint8_t icon_book_marked_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x0F, 0xF1, 0xF1, 0xF1, + 0x8F, 0xF1, 0xF1, 0xF1, 0x8F, 0xF1, 0xF1, 0xF1, 0x8F, 0xF1, 0xF1, 0xF1, 0x8F, 0xF1, 0xF1, 0x31, 0x8F, 0xF1, 0xF0, + 0x11, 0x8F, 0xF1, 0xF0, 0x01, 0x8F, 0xF1, 0xF0, 0x41, 0x8F, 0xF1, 0xF0, 0xE1, 0x8F, 0xF1, 0xF1, 0xF1, 0x8F, 0xF1, + 0xFB, 0xFB, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, + 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, + 0x0F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF8, 0x00, + 0x00, 0x0F, 0xFC, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_book_marked_32 = {32, 32, 15, icon_book_marked_32_bits}; + +// file (lucide: file) +static const uint8_t icon_file_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0xFF, 0xF0, 0x00, 0x7F, 0xE7, 0xF8, 0x3F, 0xE7, 0xF9, 0x1F, 0xE7, 0xF9, 0x8F, + 0xE7, 0xF9, 0xC7, 0xE7, 0xF8, 0x07, 0xE7, 0xFC, 0x07, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, + 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, + 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xF0, 0x00, 0x0F, 0xF8, 0x00, 0x1F, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_file_24 = {24, 24, 11, icon_file_24_bits}; +static const uint8_t icon_file_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x0F, 0xFF, 0xF8, 0x00, 0x03, 0xFF, 0xF8, 0x00, 0x01, 0xFF, 0xF1, 0xFF, 0x80, + 0xFF, 0xF1, 0xFF, 0x88, 0x7F, 0xF1, 0xFF, 0x8C, 0x3F, 0xF1, 0xFF, 0x8E, 0x1F, 0xF1, 0xFF, 0x8F, 0x1F, 0xF1, 0xFF, + 0x80, 0x0F, 0xF1, 0xFF, 0xC0, 0x0F, 0xF1, 0xFF, 0xE0, 0x0F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, + 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, + 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, + 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x8F, 0xF8, 0x00, + 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_file_32 = {32, 32, 15, icon_file_32_bits}; + +// wifi (lucide: wifi) +static const uint8_t icon_wifi_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFC, 0x00, 0x1F, + 0xF0, 0xFF, 0x0F, 0xC3, 0xFF, 0xC3, 0x8F, 0xFF, 0xF1, 0x9F, 0x81, 0xF9, 0xFE, 0x00, 0x7F, 0xF8, 0x7E, 0x1F, + 0xF1, 0xFF, 0x8F, 0xF3, 0xFF, 0xCF, 0xFF, 0xC3, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x3C, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_wifi_24 = {24, 24, 10, icon_wifi_24_bits}; +static const uint8_t icon_wifi_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xFE, 0x00, 0x00, 0x7F, 0xF8, 0x1F, 0xF8, 0x1F, 0xE0, 0x7F, + 0xFE, 0x07, 0xC1, 0xFF, 0xFF, 0x83, 0x87, 0xFF, 0xFF, 0xE1, 0xCF, 0xF8, 0x1F, 0xF3, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, + 0x00, 0x00, 0xFF, 0xFE, 0x1F, 0xF8, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xC3, 0xC3, 0xFF, 0xFF, 0xC7, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_wifi_32 = {32, 32, 13, icon_wifi_32_bits}; + +// library (lucide: library) +static const uint8_t icon_library_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xE6, 0x7F, + 0xE7, 0xE6, 0x7F, 0xE6, 0x66, 0x7F, 0xE6, 0x67, 0x3F, 0xE6, 0x67, 0x3F, 0xE6, 0x67, 0x3F, 0xE6, 0x67, 0x9F, + 0xE6, 0x67, 0x9F, 0xE6, 0x67, 0x9F, 0xE6, 0x67, 0x9F, 0xE6, 0x67, 0xCF, 0xE6, 0x67, 0xCF, 0xE6, 0x67, 0xCF, + 0xE6, 0x67, 0xE7, 0xE6, 0x67, 0xE7, 0xE6, 0x67, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_library_24 = {24, 24, 12, icon_library_24_bits}; +static const uint8_t icon_library_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, + 0xFF, 0xF1, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xFF, 0xF1, 0xFE, 0x73, 0xFF, 0xF1, 0xFE, 0x71, 0xFF, 0xF1, 0xDE, + 0x71, 0xFF, 0xF1, 0x8E, 0x79, 0xFF, 0xF1, 0x8E, 0x78, 0xFF, 0xF1, 0x8E, 0x78, 0xFF, 0xF1, 0x8E, 0x7C, 0xFF, 0xF1, + 0x8E, 0x7C, 0x7F, 0xF1, 0x8E, 0x7C, 0x7F, 0xF1, 0x8E, 0x7C, 0x7F, 0xF1, 0x8E, 0x7E, 0x7F, 0xF1, 0x8E, 0x7E, 0x3F, + 0xF1, 0x8E, 0x7E, 0x3F, 0xF1, 0x8E, 0x7F, 0x3F, 0xF1, 0x8E, 0x7F, 0x1F, 0xF1, 0x8E, 0x7F, 0x1F, 0xF1, 0x8E, 0x7F, + 0x1F, 0xF1, 0x8E, 0x7F, 0x9F, 0xF1, 0x8E, 0x7F, 0x8F, 0xF1, 0x8E, 0x7F, 0x8F, 0xF3, 0xCE, 0x7F, 0xCF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_library_32 = {32, 32, 17, icon_library_32_bits}; + +// lyra-library (lucide: library-big) +static const uint8_t icon_lyra_library_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x19, 0xFF, 0xC0, 0x00, 0xFF, 0xCC, 0xC4, 0xFF, 0xCC, 0xCE, 0x7F, + 0xCC, 0xC6, 0x7F, 0xCC, 0xC6, 0x7F, 0xCC, 0xC7, 0x3F, 0xCC, 0xC3, 0x3F, 0xCC, 0xC3, 0x3F, 0xCC, 0xC3, 0x9F, + 0xCC, 0xC9, 0x9F, 0xCC, 0xC9, 0x9F, 0xCC, 0xCD, 0xCF, 0xCC, 0xCC, 0xCF, 0xCC, 0xCC, 0xE7, 0xCC, 0xCE, 0x67, + 0xCC, 0xCE, 0x67, 0xCC, 0xCE, 0x67, 0xC0, 0x0F, 0x07, 0xE0, 0x1F, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_lyra_library_24 = {24, 24, 12, icon_lyra_library_24_bits}; +static const uint8_t icon_lyra_library_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x01, 0x0F, 0xFF, 0xE0, 0x00, 0x07, + 0xFF, 0xE7, 0x18, 0x07, 0xFF, 0xE7, 0x18, 0xE3, 0xFF, 0xE7, 0x18, 0xE3, 0xFF, 0xE7, 0x18, 0x71, 0xFF, 0xE7, 0x18, + 0x71, 0xFF, 0xE7, 0x18, 0x79, 0xFF, 0xE7, 0x18, 0x38, 0xFF, 0xE7, 0x18, 0x38, 0xFF, 0xE7, 0x18, 0x1C, 0xFF, 0xE7, + 0x18, 0x1C, 0x7F, 0xE7, 0x18, 0x9C, 0x7F, 0xE7, 0x18, 0x8E, 0x7F, 0xE7, 0x18, 0x8E, 0x3F, 0xE7, 0x18, 0xCE, 0x3F, + 0xE7, 0x18, 0xC7, 0x3F, 0xE7, 0x18, 0xC7, 0x1F, 0xE7, 0x18, 0xE7, 0x9F, 0xE7, 0x18, 0xE3, 0x8F, 0xE7, 0x18, 0xE3, + 0x8F, 0xE7, 0x18, 0xF1, 0xCF, 0xE7, 0x18, 0xF1, 0xCF, 0xE7, 0x18, 0xF8, 0x0F, 0xE0, 0x00, 0xF8, 0x0F, 0xF0, 0x01, + 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_lyra_library_32 = {32, 32, 16, icon_lyra_library_32_bits}; + +// radio-tower (lucide: radio-tower) +static const uint8_t icon_radio_tower_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xCF, 0xE3, 0xFF, 0xC7, 0xE7, 0xFF, 0xE7, 0xCE, 0x7E, 0x73, 0xCC, 0xFF, 0x33, + 0x9C, 0xE7, 0x39, 0x99, 0xC3, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xC3, 0x99, 0x9C, 0xC3, 0x39, + 0xCC, 0xC3, 0x33, 0xCF, 0xC3, 0xF3, 0xE7, 0xC3, 0xE7, 0xE3, 0x99, 0xC7, 0xF3, 0x99, 0xCF, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0xFF, 0x3C, 0xFF, 0xFF, 0x3C, 0xFF, 0xFE, 0x7E, 0x7F, 0xFE, 0x7E, 0x7F, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_radio_tower_24 = {24, 24, 11, icon_radio_tower_24_bits}; +static const uint8_t icon_radio_tower_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xBF, 0xF8, 0xFF, 0xFF, 0x1F, 0xF1, 0xFF, 0xFF, 0x8F, 0xE1, 0xFF, 0xFF, + 0x87, 0xE3, 0x9F, 0xFB, 0xC7, 0xC7, 0x8F, 0xF1, 0xE3, 0xC7, 0x1F, 0xF8, 0xE3, 0xCF, 0x3C, 0x3C, 0xF3, 0xCE, 0x38, + 0x1C, 0x73, 0xCE, 0x30, 0x0C, 0x73, 0x8E, 0x71, 0x8E, 0x71, 0x8E, 0x71, 0x8E, 0x71, 0xCE, 0x30, 0x0C, 0x73, 0xCE, + 0x38, 0x1C, 0x73, 0xCF, 0x3C, 0x3C, 0xE3, 0xC7, 0x1C, 0x38, 0xE3, 0xC7, 0xFC, 0x3D, 0xE7, 0xE3, 0xF8, 0x1F, 0xC7, + 0xE1, 0xF8, 0x1F, 0x8F, 0xF1, 0xF9, 0x9F, 0x8F, 0xF8, 0xF1, 0x8F, 0x1F, 0xFD, 0xF1, 0x8F, 0xBF, 0xFF, 0xE0, 0x07, + 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xE3, 0xC7, 0xFF, 0xFF, 0xC7, 0xE3, 0xFF, 0xFF, 0xC7, 0xE3, 0xFF, 0xFF, 0xCF, + 0xF3, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0xDF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_radio_tower_32 = {32, 32, 14, icon_radio_tower_32_bits}; + +// sun (lucide: sun) +static const uint8_t icon_sun_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xF3, 0xE7, 0xCF, 0xF1, 0xFF, 0x8F, + 0xF9, 0xFF, 0x9F, 0xFF, 0xC3, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x3C, 0xFF, 0xFE, 0x7E, 0x7F, 0x86, 0x7E, 0x61, + 0x86, 0x7E, 0x61, 0xFE, 0x7E, 0x7F, 0xFF, 0x3C, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xC3, 0xFF, 0xF9, 0xFF, 0x9F, + 0xF1, 0xFF, 0x8F, 0xF3, 0xE7, 0xCF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_sun_24 = {24, 24, 12, icon_sun_24_bits}; +static const uint8_t icon_sun_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFE, 0x7F, + 0xFF, 0xFD, 0xFE, 0x7F, 0xBF, 0xF8, 0xFF, 0xFF, 0x1F, 0xFC, 0x7F, 0xFE, 0x3F, 0xFE, 0x3F, 0xFC, 0x7F, 0xFF, 0x7E, + 0x7E, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xC3, 0xC3, 0xFF, 0xFF, 0xC7, 0xE3, 0xFF, 0xFF, + 0xCF, 0xF3, 0xFF, 0xC3, 0x8F, 0xF1, 0xC3, 0xC3, 0x8F, 0xF1, 0xC3, 0xFF, 0xCF, 0xF3, 0xFF, 0xFF, 0xC7, 0xE3, 0xFF, + 0xFF, 0xC3, 0xC3, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0x7E, 0x7E, 0xFF, 0xFE, 0x3F, 0xFC, + 0x7F, 0xFC, 0x7F, 0xFE, 0x3F, 0xF8, 0xFF, 0xFF, 0x1F, 0xFD, 0xFE, 0x7F, 0xBF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFE, + 0x7F, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_sun_32 = {32, 32, 16, icon_sun_32_bits}; + +// sun-dim (lucide: sun-dim) +static const uint8_t icon_sun_dim_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF9, 0xFF, 0x9F, 0xFF, 0xC3, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x3C, 0xFF, 0xFE, 0x7E, 0x7F, 0xE6, 0x7E, 0x67, + 0xE6, 0x7E, 0x67, 0xFE, 0x7E, 0x7F, 0xFF, 0x3C, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xC3, 0xFF, 0xF9, 0xFF, 0x9F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_sun_dim_24 = {24, 24, 12, icon_sun_dim_24_bits}; +static const uint8_t icon_sun_dim_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, + 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFE, 0xFF, 0xFE, 0x3F, 0xFC, 0x7F, 0xFF, 0x7E, + 0x7E, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xC3, 0xC3, 0xFF, 0xFF, 0xC7, 0xE3, 0xFF, 0xFF, + 0xCF, 0xF3, 0xFF, 0xF3, 0x8F, 0xF1, 0xCF, 0xF3, 0x8F, 0xF1, 0xCF, 0xFF, 0xCF, 0xF3, 0xFF, 0xFF, 0xC7, 0xE3, 0xFF, + 0xFF, 0xC3, 0xC3, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0x7E, 0x7E, 0xFF, 0xFE, 0x3F, 0xFC, + 0x7F, 0xFF, 0x7F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_sun_dim_32 = {32, 32, 16, icon_sun_dim_32_bits}; + +// history (lucide: history) +static const uint8_t icon_history_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0x81, 0xFF, 0xCE, 0x00, 0x7F, 0xC8, 0x7E, 0x3F, 0xC1, 0xFF, 0x9F, + 0xC3, 0xE7, 0xCF, 0xC0, 0x67, 0xE7, 0xC0, 0x67, 0xE7, 0xFF, 0xE7, 0xF3, 0xFF, 0xE7, 0xF3, 0xCF, 0xE7, 0xF3, + 0xCF, 0xE1, 0xF3, 0xCF, 0xF8, 0x73, 0xCF, 0xFE, 0x73, 0xE7, 0xFF, 0xE7, 0xE7, 0xFF, 0xE7, 0xF3, 0xFF, 0xCF, + 0xF9, 0xFF, 0x9F, 0xFC, 0x7E, 0x3F, 0xFE, 0x00, 0x7F, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_history_24 = {24, 24, 10, icon_history_24_bits}; +static const uint8_t icon_history_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xF0, 0x0F, 0xFF, 0xE7, 0x80, 0x01, + 0xFF, 0xE7, 0x03, 0xC0, 0xFF, 0xE4, 0x1F, 0xF8, 0x7F, 0xE0, 0x7F, 0xFC, 0x3F, 0xE0, 0xFE, 0x7F, 0x1F, 0xE0, 0x1E, + 0x7F, 0x0F, 0xE0, 0x0E, 0x7F, 0x8F, 0xE0, 0x0E, 0x7F, 0xCF, 0xFF, 0xFE, 0x7F, 0xC7, 0xFF, 0xFE, 0x7F, 0xC7, 0xFF, + 0xFE, 0x7F, 0xE7, 0xE7, 0xFE, 0x3F, 0xE7, 0xE7, 0xFE, 0x0F, 0xE7, 0xE7, 0xFF, 0x03, 0xE7, 0xE3, 0xFF, 0xC1, 0xC7, + 0xE3, 0xFF, 0xF3, 0xC7, 0xF3, 0xFF, 0xFF, 0xCF, 0xF1, 0xFF, 0xFF, 0x8F, 0xF0, 0xFF, 0xFF, 0x1F, 0xF8, 0xFF, 0xFF, + 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x1F, 0xF8, 0x7F, 0xFF, 0x03, 0xC0, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xF0, + 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_history_32 = {32, 32, 14, icon_history_32_bits}; + +// settings (lucide: settings) +static const uint8_t icon_settings_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x99, 0xFF, 0xFF, 0x99, 0xFF, 0xF0, 0x3C, 0x0F, + 0xE0, 0x7E, 0x07, 0xCF, 0xFF, 0xF3, 0xCF, 0xC3, 0xF3, 0xC7, 0x81, 0xE3, 0xE3, 0x18, 0xC7, 0xF3, 0x3C, 0xCF, + 0xF3, 0x3C, 0xCF, 0xE3, 0x18, 0xC7, 0xC7, 0x81, 0xE3, 0xCF, 0xC3, 0xF3, 0xCF, 0xFF, 0xF3, 0xE0, 0x7E, 0x07, + 0xF0, 0x3C, 0x0F, 0xFF, 0x99, 0xFF, 0xFF, 0x99, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_settings_24 = {24, 24, 11, icon_settings_24_bits}; +static const uint8_t icon_settings_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xF1, 0x8F, + 0xFF, 0xFF, 0xF3, 0xCF, 0xFF, 0xFF, 0xE3, 0xC7, 0xFF, 0xF0, 0x03, 0xC0, 0x0F, 0xF0, 0x07, 0xE0, 0x0F, 0xE3, 0x9F, + 0xF9, 0xC7, 0xE7, 0xFF, 0xFF, 0xE7, 0xE7, 0xF8, 0x1F, 0xE7, 0xE3, 0xF0, 0x0F, 0xC7, 0xF1, 0xE1, 0x87, 0x8F, 0xF8, + 0xE3, 0xC7, 0x1F, 0xF8, 0xE7, 0xE7, 0x1F, 0xF8, 0xE7, 0xE7, 0x1F, 0xF8, 0xE3, 0xC7, 0x1F, 0xF1, 0xE1, 0x87, 0x8F, + 0xE3, 0xF0, 0x0F, 0xC7, 0xE7, 0xF8, 0x1F, 0xE7, 0xE7, 0xFF, 0xFF, 0xE7, 0xE3, 0x9F, 0xF9, 0xC7, 0xF0, 0x07, 0xE0, + 0x0F, 0xF0, 0x03, 0xC0, 0x1F, 0xFF, 0xE3, 0xC7, 0xFF, 0xFF, 0xF3, 0xCF, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF0, + 0x0F, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_settings_32 = {32, 32, 15, icon_settings_32_bits}; + +// lyra-settings (lucide: settings-2) +static const uint8_t icon_lyra_settings_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xF0, 0x3F, 0xFF, 0xE3, 0x1F, 0xFF, + 0xE7, 0x80, 0x0F, 0xE7, 0x80, 0x0F, 0xE3, 0x1F, 0xFF, 0xF0, 0x3F, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xFC, 0x0F, 0xFF, 0xF8, 0xC7, 0xF0, 0x01, 0xE7, 0xF0, 0x01, 0xE7, + 0xFF, 0xF8, 0xC7, 0xFF, 0xFC, 0x0F, 0xFF, 0xFE, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_lyra_settings_24 = {24, 24, 12, icon_lyra_settings_24_bits}; +static const uint8_t icon_lyra_settings_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, + 0xFF, 0xFC, 0x07, 0xFF, 0xFF, 0xF8, 0x03, 0xFF, 0xFF, 0xF1, 0xE3, 0xFF, 0xFF, 0xF1, 0xF0, 0x00, 0x3F, 0xF1, 0xF0, + 0x00, 0x1F, 0xF1, 0xF0, 0x00, 0x3F, 0xF8, 0xE3, 0xFF, 0xFF, 0xF8, 0x07, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF, 0xFF, 0xFF, + 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, + 0xFF, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xC7, 0x1F, 0xFC, 0x00, 0x0F, 0x8F, 0xF8, 0x00, 0x0F, 0x8F, 0xFC, 0x00, 0x0F, + 0x8F, 0xFF, 0xFF, 0xC7, 0x8F, 0xFF, 0xFF, 0xC0, 0x1F, 0xFF, 0xFF, 0xE0, 0x3F, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_lyra_settings_32 = {32, 32, 16, icon_lyra_settings_32_bits}; + +// cog (lucide: cog) +static const uint8_t icon_cog_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFC, 0x81, 0x3F, 0xFE, 0x00, 0x7F, 0xFC, 0x3E, 0x3F, + 0xE8, 0x3F, 0x17, 0xE1, 0x3F, 0x87, 0xF3, 0x9F, 0xCF, 0xE7, 0x87, 0xE7, 0xE7, 0x83, 0xE7, 0x87, 0x98, 0x01, + 0x87, 0x98, 0x01, 0xE7, 0x83, 0xE7, 0xE7, 0x87, 0xE7, 0xF3, 0x9F, 0xCF, 0xE1, 0x3F, 0x87, 0xE8, 0x3F, 0x17, + 0xFC, 0x3E, 0x3F, 0xFE, 0x00, 0x7F, 0xFC, 0x81, 0x3F, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_cog_24 = {24, 24, 12, icon_cog_24_bits}; +static const uint8_t icon_cog_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xBE, 0x7D, 0xFF, 0xFF, 0x18, 0x18, + 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xF2, 0x07, 0xFC, 0x4F, 0xE0, 0x63, + 0xFE, 0x07, 0xF0, 0xE3, 0xFF, 0x0F, 0xF8, 0xF1, 0xFF, 0x1F, 0xF9, 0xF8, 0x3F, 0x9F, 0xF1, 0xF8, 0x1F, 0x8F, 0xF1, + 0xF0, 0x0F, 0x8F, 0xC1, 0xF1, 0x80, 0x03, 0xC1, 0xF1, 0x80, 0x03, 0xF1, 0xF0, 0x0F, 0x8F, 0xF1, 0xF8, 0x1F, 0x8F, + 0xF9, 0xF8, 0x3F, 0x9F, 0xF8, 0xF1, 0xFF, 0x1F, 0xF0, 0xE3, 0xFF, 0x0F, 0xE0, 0x63, 0xFE, 0x07, 0xF2, 0x07, 0xFC, + 0x4F, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x18, 0x18, 0xFF, 0xFF, 0xBE, + 0x7D, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_cog_32 = {32, 32, 16, icon_cog_32_bits}; + +// transfer (lucide: arrow-left-right) +static const uint8_t icon_transfer_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFC, 0x7F, 0xFF, 0xF8, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, + 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xF1, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFC, 0x7F, 0xFF, 0xFE, 0x7F, 0xFF, + 0xFF, 0xFE, 0x7F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0x8F, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, + 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_transfer_24 = {24, 24, 12, icon_transfer_24_bits}; +static const uint8_t icon_transfer_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, 0x0F, 0xFF, + 0xFF, 0xFE, 0x1F, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, + 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x1F, 0xFC, 0x7F, 0xFF, 0xFF, 0xFE, 0x3F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, + 0x8F, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, + 0xFF, 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xFE, 0x3F, 0xF8, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, + 0x0F, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, + 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_transfer_32 = {32, 32, 16, icon_transfer_32_bits}; + +// lyra-transfer (lucide: send-horizontal) +static const uint8_t icon_lyra_transfer_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xC0, 0x7F, 0xFF, 0xE6, 0x1F, 0xFF, + 0xE7, 0x87, 0xFF, 0xE7, 0xE1, 0xFF, 0xF3, 0xF8, 0x7F, 0xF3, 0xFE, 0x0F, 0xF9, 0xFF, 0x83, 0xF8, 0x00, 0x01, + 0xF8, 0x00, 0x01, 0xF9, 0xFF, 0x83, 0xF3, 0xFE, 0x0F, 0xF3, 0xF8, 0x7F, 0xE7, 0xE1, 0xFF, 0xE7, 0x87, 0xFF, + 0xE6, 0x1F, 0xFF, 0xC0, 0x7F, 0xFF, 0xC1, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_lyra_transfer_24 = {24, 24, 12, icon_lyra_transfer_24_bits}; +static const uint8_t icon_lyra_transfer_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE1, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xFF, + 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xE2, 0x07, 0xFF, 0xFF, 0xF3, 0x81, 0xFF, 0xFF, 0xF1, 0xE0, 0x7F, 0xFF, 0xF1, 0xFC, + 0x1F, 0xFF, 0xF9, 0xFF, 0x03, 0xFF, 0xF8, 0xFF, 0xC0, 0xFF, 0xFC, 0xFF, 0xF0, 0x3F, 0xFC, 0x7F, 0xFC, 0x0F, 0xFC, + 0x7F, 0xFF, 0x03, 0xFE, 0x00, 0x00, 0x01, 0xFE, 0x00, 0x00, 0x01, 0xFC, 0x7F, 0xFF, 0x03, 0xFC, 0x7F, 0xFC, 0x0F, + 0xFC, 0xFF, 0xF0, 0x3F, 0xF8, 0xFF, 0xC0, 0xFF, 0xF9, 0xFF, 0x03, 0xFF, 0xF1, 0xFC, 0x1F, 0xFF, 0xF1, 0xE0, 0x7F, + 0xFF, 0xF3, 0x81, 0xFF, 0xFF, 0xE2, 0x07, 0xFF, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xE0, 0x7F, 0xFF, 0xFF, 0xE1, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_lyra_transfer_32 = {32, 32, 16, icon_lyra_transfer_32_bits}; + +// chevrons-left-right-ellipsis (lucide: chevrons-left-right-ellipsis) +static const uint8_t icon_chevrons_left_right_ellipsis_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFC, 0xFF, 0x3F, 0xF8, 0xFF, 0x1F, 0xF1, 0xFF, 0x8F, 0xE3, 0xFF, 0xC7, 0xC7, 0xFF, 0xE3, 0x8E, 0x66, 0x71, + 0x8E, 0x66, 0x71, 0xC7, 0xFF, 0xE3, 0xE3, 0xFF, 0xC7, 0xF1, 0xFF, 0x8F, 0xF8, 0xFF, 0x1F, 0xFC, 0xFF, 0x3F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_chevrons_left_right_ellipsis_24 = {24, 24, 12, + icon_chevrons_left_right_ellipsis_24_bits}; +static const uint8_t icon_chevrons_left_right_ellipsis_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFC, 0xFF, 0xFE, 0x1F, + 0xF8, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x7F, 0xFE, 0x1F, 0xF0, 0xFF, 0xFF, 0x0F, 0xE1, 0xFF, 0xFF, 0x87, 0xC3, + 0xFF, 0xFF, 0xC3, 0xC7, 0xCE, 0x73, 0xE3, 0xC7, 0xCE, 0x73, 0xE3, 0xC3, 0xFF, 0xFF, 0xC3, 0xE1, 0xFF, 0xFF, 0x87, + 0xF0, 0xFF, 0xFF, 0x0F, 0xF8, 0x7F, 0xFE, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x1F, 0xF8, 0x7F, 0xFF, 0x3F, 0xFC, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_chevrons_left_right_ellipsis_32 = {32, 32, 16, + icon_chevrons_left_right_ellipsis_32_bits}; + +// menu (lucide: menu) +static const uint8_t icon_menu_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x07, + 0xE0, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xE0, 0x00, 0x07, 0xE0, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_menu_24 = {24, 24, 12, icon_menu_24_bits}; +static const uint8_t icon_menu_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF8, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF8, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_menu_32 = {32, 32, 16, icon_menu_32_bits}; + +// home (lucide: house) +static const uint8_t icon_home_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x18, 0xFF, 0xFE, 0x3C, 0x7F, 0xFC, 0x7E, 0x3F, + 0xF9, 0xFF, 0x9F, 0xE3, 0xFF, 0xC7, 0xE7, 0xFF, 0xE7, 0xCF, 0xFF, 0xF3, 0xCF, 0xFF, 0xF3, 0xCF, 0x81, 0xF3, + 0xCF, 0x00, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, + 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xE0, 0x00, 0x07, 0xF0, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_home_24 = {24, 24, 13, icon_home_24_bits}; +static const uint8_t icon_home_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xE1, 0x87, + 0xFF, 0xFF, 0xC3, 0xC3, 0xFF, 0xFF, 0x87, 0xF1, 0xFF, 0xFE, 0x1F, 0xF8, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x7F, + 0xFE, 0x1F, 0xF0, 0xFF, 0xFF, 0x0F, 0xE1, 0xFF, 0xFF, 0x87, 0xE3, 0xFF, 0xFF, 0xC7, 0xE7, 0xFF, 0xFF, 0xE7, 0xE7, + 0xFF, 0xFF, 0xE7, 0xE7, 0xF0, 0x0F, 0xE7, 0xE7, 0xE0, 0x07, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, + 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, + 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE3, 0xE7, 0xE7, 0xC7, 0xE0, 0x00, 0x00, 0x07, 0xF0, 0x00, + 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_home_32 = {32, 32, 17, icon_home_32_bits}; + +// search (lucide: search) +static const uint8_t icon_search_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0xFC, 0x00, 0xFF, 0xF8, 0xFC, 0x7F, 0xF1, 0xFE, 0x3F, + 0xE3, 0xFF, 0x1F, 0xE7, 0xFF, 0x9F, 0xCF, 0xFF, 0xCF, 0xCF, 0xFF, 0xCF, 0xCF, 0xFF, 0xCF, 0xCF, 0xFF, 0xCF, + 0xCF, 0xFF, 0xCF, 0xCF, 0xFF, 0xCF, 0xE7, 0xFF, 0x9F, 0xE3, 0xFF, 0x1F, 0xF1, 0xFE, 0x3F, 0xF8, 0xFC, 0x1F, + 0xFC, 0x00, 0x8F, 0xFF, 0x03, 0xC7, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_search_24 = {24, 24, 11, icon_search_24_bits}; +static const uint8_t icon_search_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0x00, 0x07, + 0xFF, 0xFE, 0x07, 0x83, 0xFF, 0xFC, 0x3F, 0xE1, 0xFF, 0xF8, 0x7F, 0xF8, 0xFF, 0xF0, 0xFF, 0xFC, 0x7F, 0xF1, 0xFF, + 0xFC, 0x7F, 0xF3, 0xFF, 0xFE, 0x3F, 0xE3, 0xFF, 0xFE, 0x3F, 0xE3, 0xFF, 0xFF, 0x3F, 0xE7, 0xFF, 0xFF, 0x3F, 0xE7, + 0xFF, 0xFF, 0x1F, 0xE7, 0xFF, 0xFF, 0x1F, 0xE7, 0xFF, 0xFF, 0x3F, 0xE3, 0xFF, 0xFF, 0x3F, 0xE3, 0xFF, 0xFE, 0x3F, + 0xF1, 0xFF, 0xFE, 0x3F, 0xF1, 0xFF, 0xFC, 0x7F, 0xF8, 0xFF, 0xF8, 0xFF, 0xFC, 0x3F, 0xF0, 0xFF, 0xFE, 0x0F, 0xC0, + 0x7F, 0xFF, 0x00, 0x06, 0x3F, 0xFF, 0xC0, 0x0F, 0x1F, 0xFF, 0xFC, 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, + 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_search_32 = {32, 32, 15, icon_search_32_bits}; + +// pointer (lucide: pointer) +static const uint8_t icon_pointer_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFC, 0x3F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, 0xF9, 0x9F, 0xFF, + 0xF9, 0x87, 0xFF, 0xF9, 0x80, 0x7F, 0xF9, 0x98, 0x07, 0xF9, 0x99, 0x83, 0xF9, 0x99, 0x99, 0xF9, 0xFF, 0x99, + 0xC1, 0xFF, 0xF9, 0x81, 0xFF, 0xF9, 0x98, 0xFF, 0xF9, 0x9C, 0xFF, 0xF9, 0xCF, 0xFF, 0xF1, 0xE7, 0xFF, 0xF3, + 0xF3, 0xFF, 0xE3, 0xF9, 0xFF, 0xC7, 0xFC, 0x7F, 0x0F, 0xFE, 0x00, 0x1F, 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_pointer_24 = {24, 24, 12, icon_pointer_24_bits}; +static const uint8_t icon_pointer_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xFE, 0x03, 0xFF, 0xFF, 0xFE, 0x33, 0xFF, + 0xFF, 0xFE, 0x71, 0xFF, 0xFF, 0xFE, 0x71, 0xFF, 0xFF, 0xFE, 0x71, 0xFF, 0xFF, 0xFE, 0x70, 0x3F, 0xFF, 0xFE, 0x70, + 0x1B, 0xFF, 0xFE, 0x70, 0x00, 0xFF, 0xFE, 0x71, 0x80, 0x07, 0xFE, 0x71, 0x8C, 0x03, 0xFE, 0x71, 0x8E, 0x23, 0xFE, + 0x7B, 0xDE, 0x31, 0xFE, 0x7F, 0xFE, 0x71, 0xE0, 0x7F, 0xFF, 0xF1, 0xC0, 0x7F, 0xFF, 0xF1, 0x8C, 0x3F, 0xFF, 0xF1, + 0x8E, 0x3F, 0xFF, 0xF1, 0x8F, 0x3F, 0xFF, 0xF1, 0xC7, 0xFF, 0xFF, 0xE3, 0xE3, 0xFF, 0xFF, 0xE3, 0xE1, 0xFF, 0xFF, + 0xC3, 0xF0, 0xFF, 0xFF, 0xC7, 0xF8, 0x7F, 0xFF, 0x87, 0xFC, 0x3F, 0xFE, 0x0F, 0xFF, 0x0F, 0xF8, 0x1F, 0xFF, 0x80, + 0x00, 0x3F, 0xFF, 0xE0, 0x00, 0xFF, 0xFF, 0xFE, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_pointer_32 = {32, 32, 17, icon_pointer_32_bits}; + +// pointer-off (lucide: pointer-off) +static const uint8_t icon_pointer_off_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0x9E, 0x7F, 0xFF, 0x8E, 0x3F, 0xFF, 0xC7, 0x9F, 0xFF, 0xE3, 0x9F, 0xFF, 0xF1, 0xFF, 0xFF, + 0xF8, 0xF7, 0xFF, 0xF8, 0x70, 0x7F, 0xF8, 0x38, 0x07, 0xF9, 0x1D, 0x83, 0xF9, 0x8F, 0x99, 0xF9, 0xC7, 0x99, + 0xC1, 0xE3, 0xF9, 0x81, 0xF1, 0xF9, 0x98, 0xF8, 0xF9, 0x9C, 0xFC, 0x79, 0xCF, 0xFE, 0x39, 0xC7, 0xFF, 0x1F, + 0xE3, 0xFF, 0x8F, 0xF1, 0xFF, 0xC7, 0xFC, 0x7F, 0x83, 0xFE, 0x00, 0x11, 0xFF, 0x80, 0x79, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_pointer_off_24 = {24, 24, 13, icon_pointer_off_24_bits}; +static const uint8_t icon_pointer_off_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xDF, 0xFF, 0xFF, 0x8F, 0x87, 0xFF, 0xFF, 0xC7, 0x83, 0xFF, 0xFF, 0xE3, 0xF3, 0xFF, + 0xFF, 0xF1, 0xF1, 0xFF, 0xFF, 0xF8, 0xF3, 0xFF, 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xFC, 0x3F, 0x3F, 0xFF, 0xFE, 0x1F, + 0x1B, 0xFF, 0xFE, 0x0F, 0x00, 0xFF, 0xFE, 0x07, 0x80, 0x07, 0xFE, 0x63, 0xCC, 0x03, 0xFE, 0x71, 0xFE, 0x23, 0xFE, + 0x78, 0xFE, 0x71, 0xFE, 0x7C, 0x7E, 0x71, 0xE0, 0x7E, 0x3F, 0xF1, 0xC0, 0x7F, 0x1F, 0xF1, 0x8C, 0x3F, 0x8F, 0xF1, + 0x8E, 0x3F, 0xC7, 0xF1, 0x8F, 0x3F, 0xE3, 0xF3, 0xC7, 0xFF, 0xF1, 0xF3, 0xE3, 0xFF, 0xF8, 0xF3, 0xE1, 0xFF, 0xFC, + 0x7F, 0xF0, 0xFF, 0xFE, 0x3F, 0xF8, 0x7F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFF, 0x0F, 0xFC, 0x07, 0xFF, 0x80, + 0x00, 0x63, 0xFF, 0xE0, 0x00, 0xF1, 0xFF, 0xFC, 0x0F, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_pointer_off_32 = {32, 32, 17, icon_pointer_off_32_bits}; + +// chevron-up (lucide: chevron-up) +static const uint8_t icon_chevron_up_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x18, 0xFF, + 0xFE, 0x3C, 0x7F, 0xFC, 0x7E, 0x3F, 0xF8, 0xFF, 0x1F, 0xF9, 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_chevron_up_24 = {24, 24, 12, icon_chevron_up_24_bits}; +static const uint8_t icon_chevron_up_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, + 0xF1, 0x8F, 0xFF, 0xFF, 0xE3, 0xC7, 0xFF, 0xFF, 0xC7, 0xE3, 0xFF, 0xFF, 0x8F, 0xF1, 0xFF, 0xFF, 0x1F, 0xF8, 0xFF, + 0xFE, 0x3F, 0xFC, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_chevron_up_32 = {32, 32, 16, icon_chevron_up_32_bits}; + +// chevron-down (lucide: chevron-down) +static const uint8_t icon_chevron_down_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0x9F, 0xF8, 0xFF, 0x1F, 0xFC, 0x7E, 0x3F, 0xFE, 0x3C, 0x7F, + 0xFF, 0x18, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_chevron_down_24 = {24, 24, 11, icon_chevron_down_24_bits}; +static const uint8_t icon_chevron_down_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x3F, 0xFC, 0x7F, 0xFF, 0x1F, 0xF8, 0xFF, 0xFF, + 0x8F, 0xF1, 0xFF, 0xFF, 0xC7, 0xE3, 0xFF, 0xFF, 0xE3, 0xC7, 0xFF, 0xFF, 0xF1, 0x8F, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, + 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_chevron_down_32 = {32, 32, 15, icon_chevron_down_32_bits}; + +// sliders-horizontal (lucide: sliders-horizontal) +static const uint8_t icon_sliders_horizontal_24_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xF9, 0xFF, 0xC0, 0x18, 0x03, 0xC0, 0x18, 0x03, + 0xFF, 0xF9, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFE, 0x7F, 0xFF, 0xC0, 0x60, 0x03, + 0xC0, 0x60, 0x03, 0xFE, 0x7F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFE, 0x7F, + 0xC0, 0x06, 0x03, 0xC0, 0x06, 0x03, 0xFF, 0xFE, 0x7F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_sliders_horizontal_24 = {24, 24, 12, icon_sliders_horizontal_24_bits}; +static const uint8_t icon_sliders_horizontal_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0x8F, + 0xFF, 0xF0, 0x03, 0x80, 0x0F, 0xE0, 0x01, 0x80, 0x07, 0xE0, 0x03, 0x80, 0x07, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, + 0x8F, 0xFF, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, + 0x8F, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0x07, 0xE0, 0x0E, 0x00, 0x07, 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, + 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xFF, 0xF1, + 0xFF, 0xE0, 0x00, 0x70, 0x07, 0xE0, 0x00, 0x70, 0x07, 0xF0, 0x00, 0xF0, 0x0F, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xFF, + 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_sliders_horizontal_32 = {32, 32, 15, icon_sliders_horizontal_32_bits}; diff --git a/src/components/icons/listIcons.manifest b/src/components/icons/listIcons.manifest new file mode 100644 index 0000000000..f92674b30c --- /dev/null +++ b/src/components/icons/listIcons.manifest @@ -0,0 +1,34 @@ +# Manifest for src/components/icons/listIcons.h — regenerate with: +# python3 /libs/assets/Icons/tools/gen_icons.py \ +# --manifest src/components/icons/listIcons.manifest \ +# --svgdir /libs/assets/Icons/lucide/icons \ +# --sizes 24,32 --out src/components/icons/listIcons.h +# then clang-format -i the output. +folder = folder +file-text = file-text +image = image +book-open = book-open +book = book +book-marked = book-marked +file = file +wifi = wifi +library = library +lyra-library = library-big +radio-tower = radio-tower +sun = sun +sun-dim = sun-dim +history = history +settings = settings +lyra-settings = settings-2 +cog = cog +transfer = arrow-left-right +lyra-transfer = send-horizontal +chevrons-left-right-ellipsis = chevrons-left-right-ellipsis +menu = menu +home = house +search = search +pointer = pointer +pointer-off = pointer-off +chevron-up = chevron-up +chevron-down = chevron-down +sliders-horizontal = sliders-horizontal diff --git a/src/components/icons/recent.h b/src/components/icons/recent.h deleted file mode 100644 index fd61b95b4c..0000000000 --- a/src/components/icons/recent.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include - -// size: 32x32 -static const uint8_t RecentIcon[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, - 0x07, 0xC0, 0x00, 0x00, 0x03, 0xCF, 0xFF, 0xF8, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, - 0xFC, 0xF3, 0xC0, 0x03, 0xFC, 0xF3, 0xC0, 0x03, 0xFC, 0xF3, 0xCF, 0xC7, 0xFC, 0xF3, 0xCF, 0x8F, 0xFC, 0xF3, 0xCF, - 0x1F, 0xFC, 0xF3, 0xCF, 0x8F, 0xFC, 0xF3, 0xCF, 0x87, 0xFC, 0xF3, 0xCF, 0xC3, 0xFC, 0xF3, 0xC0, 0x03, 0xFC, 0xF3, - 0xC0, 0x03, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xCF, 0xFF, 0xFC, - 0xF3, 0xCF, 0xFF, 0xFC, 0xF3, 0xC3, 0xFF, 0xFC, 0x23, 0xE0, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x1F, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/icons/settings2.h b/src/components/icons/settings2.h deleted file mode 100644 index 9db813dbeb..0000000000 --- a/src/components/icons/settings2.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once -#include - -// size: 24x24 -static const uint8_t Settings2Icon24[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFC, 0x81, 0x3F, 0xFE, 0x00, 0x7F, 0xFC, 0x26, 0x3F, - 0xE8, 0xE7, 0x17, 0xE1, 0xE7, 0x83, 0xF3, 0xE7, 0xCF, 0xF7, 0xC3, 0xCF, 0xE7, 0x81, 0xE7, 0x87, 0x99, 0xE1, - 0x87, 0x99, 0xE1, 0xE6, 0x00, 0x67, 0xF0, 0x00, 0x0F, 0xF0, 0xFF, 0x0F, 0xE1, 0xFF, 0x83, 0xE8, 0xFF, 0x17, - 0xFC, 0x3E, 0x3F, 0xFE, 0x00, 0x7F, 0xFC, 0x81, 0x3F, 0xFF, 0xE7, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF}; - -// size: 32x32 -static const uint8_t Settings2Icon[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, - 0xFF, 0xFF, 0xFF, 0xE0, 0x3F, 0xFF, 0x3F, 0xE3, 0x1F, 0xFF, 0x3F, 0xCF, 0x9F, 0xFF, 0x3F, 0xCF, 0xCF, 0xFF, 0x3F, - 0xCF, 0xCF, 0xFF, 0x3F, 0xCF, 0x8F, 0xFF, 0x3F, 0xC7, 0x9F, 0xFF, 0x3F, 0xE0, 0x1F, 0xFF, 0x3F, 0xF0, 0x7F, 0xFF, - 0x3F, 0xFC, 0xFF, 0xFF, 0x3F, 0xFC, 0xFF, 0xFF, 0x3F, 0xFC, 0xFF, 0xFF, 0x3F, 0xFC, 0xFF, 0xFE, 0x0F, 0xFC, 0xFF, - 0xF8, 0x07, 0xFC, 0xFF, 0xF9, 0xE3, 0xFC, 0xFF, 0xF1, 0xF3, 0xFC, 0xFF, 0xF3, 0xF3, 0xFC, 0xFF, 0xF3, 0xF3, 0xFC, - 0xFF, 0xF9, 0xF3, 0xFC, 0xFF, 0xF8, 0xC7, 0xFC, 0xFF, 0xFC, 0x07, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/icons/text24.h b/src/components/icons/text24.h deleted file mode 100644 index 9020ec05df..0000000000 --- a/src/components/icons/text24.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -#include - -// size: 24x24 -static const uint8_t Text24Icon[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x07, 0xF8, 0x00, 0x03, 0xF0, 0x7F, 0xF9, - 0xE2, 0x7F, 0xF9, 0xC6, 0x73, 0x39, 0xCE, 0x73, 0x39, 0x80, 0x73, 0x39, 0x80, 0xF3, 0x39, 0x9F, 0xF3, 0x39, - 0x9F, 0xF3, 0x39, 0x9F, 0x33, 0x39, 0x9F, 0x33, 0x39, 0x9F, 0x33, 0x39, 0x9F, 0x33, 0x39, 0x9F, 0xFF, 0xF9, - 0x9F, 0xFF, 0xF9, 0xC0, 0x00, 0x03, 0xE0, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/icons/touchHeaderIcons.h b/src/components/icons/touchHeaderIcons.h new file mode 100644 index 0000000000..1f3558f64b --- /dev/null +++ b/src/components/icons/touchHeaderIcons.h @@ -0,0 +1,17 @@ +#pragma once + +#include "Icon.h" + +// Generated by libs/assets/Icons/tools/gen_icons.py from Lucide SVGs. Do not edit. +// Sizes: 32px. Icons: 1. + +// back (lucide: chevron-left) +static const uint8_t icon_back_32_bits[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0xFF, + 0x8F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFE, 0x3F, 0xFF, 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, + 0xF1, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, + 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xFE, 0x3F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, 0xC7, + 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const freeink::Icon icon_back_32 = {32, 32, 16, icon_back_32_bits}; diff --git a/src/components/icons/touchHeaderIcons.manifest b/src/components/icons/touchHeaderIcons.manifest new file mode 100644 index 0000000000..4c86a92e4d --- /dev/null +++ b/src/components/icons/touchHeaderIcons.manifest @@ -0,0 +1,6 @@ +# Touch-only header controls. Regenerate with: +# python3 freeink-sdk/libs/assets/Icons/tools/gen_icons.py \ +# --manifest src/components/icons/touchHeaderIcons.manifest \ +# --svgdir freeink-sdk/libs/assets/Icons/lucide/icons --sizes 32 \ +# --out src/components/icons/touchHeaderIcons.h +back = chevron-left diff --git a/src/components/icons/transfer.h b/src/components/icons/transfer.h deleted file mode 100644 index fd28566dd1..0000000000 --- a/src/components/icons/transfer.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include - -// size: 32x32 -static const uint8_t TransferIcon[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x3F, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xF8, 0x3F, - 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xF0, 0x1F, 0xFF, 0xFF, 0xF0, 0x4F, 0xFF, 0xFF, 0xE2, 0x4F, 0xFF, 0xFF, 0xE2, - 0x47, 0xFF, 0xFF, 0xC6, 0x67, 0xFF, 0xFF, 0xC6, 0x63, 0xFF, 0xFF, 0xCE, 0x73, 0xFF, 0xFF, 0x8E, 0x71, 0xFF, 0xFF, - 0x9E, 0x79, 0xFF, 0xFF, 0x1E, 0x78, 0xFF, 0xFF, 0x3E, 0x7C, 0xFF, 0xFE, 0x3E, 0x7C, 0x7F, 0xFE, 0x3E, 0x7E, 0x7F, - 0xFC, 0x7E, 0x7E, 0x3F, 0xFC, 0x7E, 0x7F, 0x3F, 0xF8, 0xFE, 0x7F, 0x3F, 0xF8, 0xFE, 0x7F, 0x9F, 0xF1, 0xFC, 0x1F, - 0x9F, 0xF1, 0xE0, 0x07, 0xCF, 0xE3, 0x83, 0x80, 0xCF, 0xE0, 0x0F, 0xF0, 0x07, 0xE0, 0x7F, 0xFC, 0x07, 0xF3, 0xFF, - 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/icons/wifi.h b/src/components/icons/wifi.h deleted file mode 100644 index d84f5aedb2..0000000000 --- a/src/components/icons/wifi.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include - -// size: 32x32 -static const uint8_t WifiIcon[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0xFF, 0x8F, 0xFF, - 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0x1F, 0x3F, 0xFF, 0xFE, 0x3E, 0x3F, 0xFF, 0xFE, 0x3C, 0x7F, 0xFF, 0xFE, 0x7C, - 0x7F, 0xFF, 0xFC, 0x78, 0xFD, 0xFF, 0xFC, 0x78, 0xF1, 0xFF, 0xFC, 0xF9, 0xF1, 0xFF, 0xFC, 0xF1, 0xE3, 0xFF, 0xFC, - 0xF1, 0xE3, 0xFF, 0xFC, 0xF1, 0xE7, 0xDF, 0xFC, 0xF3, 0xE7, 0xDF, 0xFC, 0xF1, 0xE7, 0xFF, 0xFC, 0xF9, 0xE3, 0xFF, - 0xFC, 0xF9, 0xF3, 0xFF, 0xFC, 0xF9, 0xF1, 0xFF, 0xFC, 0x7C, 0xFB, 0xFF, 0xFE, 0x7C, 0xFF, 0xFF, 0xFE, 0x7C, 0x7F, - 0xFF, 0xFF, 0x3E, 0x3F, 0xFF, 0xFF, 0x3F, 0x3F, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, 0xCF, - 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index a8e603f586..20f7251732 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -1,7 +1,9 @@ #include "BaseTheme.h" +#include #include #include +#include #include #include #include @@ -9,19 +11,26 @@ #include #include #include +#include #include +#include "CrossPointSettings.h" +#include "DeviceCapabilities.h" #include "I18n.h" #include "RecentBooksStore.h" #include "activities/reader/BookReadingStats.h" +#include "components/TouchRegistry.h" +#include "components/UIScale.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" #include "fontIds.h" // Internal constants namespace { constexpr int homeMenuMargin = 20; constexpr int homeMarginTop = 30; -constexpr int subtitleY = 738; +constexpr int roundedRaffHeaderClockYOffset = 5; +constexpr int detachedHeaderBatteryTopInset = 5; } // namespace @@ -57,27 +66,20 @@ void BaseTheme::fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t const bool foregroundBlack) const { const bool charging = gpio.isUsbConnected(); - const int maxFillWidth = rect.width - 5; - const int fillHeight = rect.height - 4; - if (maxFillWidth <= 0 || fillHeight <= 0) { - return; - } - // +1 to round up so we always fill at least one pixel - int filledWidth = percentage * maxFillWidth / 100 + 1; - if (filledWidth > maxFillWidth) { - filledWidth = maxFillWidth; - } - - // When charging, ensure minimum fill so lightning bolt is fully visible - constexpr int minFillForBolt = 8; - if (charging && filledWidth < minFillForBolt) { - filledWidth = std::min(minFillForBolt, maxFillWidth); - } - - renderer.fillRect(rect.x + 2, rect.y + 2, filledWidth, fillHeight, foregroundBlack); - if (charging) { + // Solid fill when charging so lightning bolt is visible. + renderer.fillRect(rect.x + 2, rect.y + 2, rect.width - 5, rect.height - 4, foregroundBlack); drawBatteryLightningBolt(renderer, rect.x + 4, rect.y + 2, !foregroundBlack); + } else { + if (percentage > 10) { + renderer.fillRect(rect.x + 2, rect.y + 2, 3, rect.height - 4, foregroundBlack); + } + if (percentage > 40) { + renderer.fillRect(rect.x + 6, rect.y + 2, 3, rect.height - 4, foregroundBlack); + } + if (percentage > 70) { + renderer.fillRect(rect.x + 10, rect.y + 2, 3, rect.height - 4, foregroundBlack); + } } } @@ -124,6 +126,16 @@ int BaseTheme::homeHeaderClockTextYOffset(const GfxRenderer& renderer) { return homeHeaderTopInset - centeredClockY; } +Rect BaseTheme::buttonMenuTouchTarget(const Rect rowRect, const Rect menuRect, const bool isLastItem, + const int rowSpacing) { + if (!isLastItem) return rowRect; + + const int touchTop = std::max(menuRect.y, rowRect.y - rowSpacing / 2); + const int rowBottom = rowRect.y + rowRect.height; + const int menuBottom = menuRect.y + menuRect.height; + return Rect{rowRect.x, touchTop, rowRect.width, std::max(rowBottom, menuBottom) - touchTop}; +} + void BaseTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const size_t current, const size_t total) const { if (total == 0) { @@ -133,7 +145,6 @@ void BaseTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const si // Use 64-bit arithmetic to avoid overflow for large files const int percent = static_cast((static_cast(current) * 100) / total); - LOG_DBG("UI", "Drawing progress bar: current=%u, total=%u, percent=%d", current, total, percent); // Draw outline renderer.drawRect(rect.x, rect.y, rect.width, rect.height); @@ -150,6 +161,8 @@ void BaseTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const si void BaseTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, const char* btn4, const bool allowInvertedText) const { + if (gpio.hasTouch()) return; + const GfxRenderer::Orientation orig_orientation = renderer.getOrientation(); const bool invertText = allowInvertedText && orig_orientation == GfxRenderer::Orientation::PortraitInverted; renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -157,20 +170,26 @@ void BaseTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c const int pageHeight = renderer.getScreenHeight(); constexpr int buttonWidth = 106; constexpr int buttonHeight = BaseMetrics::values.buttonHintsHeight; - constexpr int buttonY = BaseMetrics::values.buttonHintsHeight; - constexpr int textYOffset = 7; // Distance from top of button to text baseline - // X3 has wider screen in portrait (528 vs 480), use more spacing - constexpr int x4ButtonPositions[] = {25, 130, 245, 350}; - constexpr int x3ButtonPositions[] = {38, 154, 268, 384}; - const int* buttonPositions = gpio.deviceIsX3() ? x3ButtonPositions : x4ButtonPositions; + constexpr int buttonY = BaseMetrics::values.buttonHintsHeight; // Distance from bottom + constexpr int textYOffset = 7; // Distance from top of button to text baseline + // Keyed to the portrait panel width: the 528-wide X3 gets more spacing than + // the 480-wide boards (X4, X4 Pro, and the other 800x480 panels). + constexpr int narrowButtonPositions[] = {25, 130, 245, 350}; + constexpr int wideButtonPositions[] = {38, 154, 268, 384}; + const int* buttonPositions = renderer.getScreenWidth() >= 528 ? wideButtonPositions : narrowButtonPositions; const char* labels[] = {btn1, btn2, btn3, btn4}; for (int i = 0; i < 4; i++) { - // Only draw if the label is non-empty + const int x = buttonPositions[i]; if (labels[i] != nullptr && labels[i][0] != '\0') { - const int x = buttonPositions[i]; + TouchRegistry::getInstance().add(Rect{x, pageHeight - buttonY, buttonWidth, buttonHeight}, i, + TouchRegistry::Button); renderer.fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false); renderer.drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight); + } else { + // Fast refreshes retain the previous hint pixels. Clear a label that was + // present on the last screen before leaving this button slot inactive. + renderer.fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false); } } @@ -190,13 +209,15 @@ void BaseTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c } void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const { + if (gpio.hasTouch()) return; + const int screenWidth = renderer.getScreenWidth(); constexpr int buttonWidth = BaseMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated) constexpr int buttonHeight = 80; // Height on screen (width when rotated) constexpr int buttonMargin = 4; - if (gpio.deviceIsX3()) { - // X3 layout: Up on left side, Down on right side, positioned higher + if (deviceHasEdgeSideButtons(gpio)) { + // Edge-button layout (X3, X4 Pro): Up on left side, Down on right side, positioned higher constexpr int x3ButtonY = 155; if (topBtn != nullptr && topBtn[0] != '\0') { @@ -254,16 +275,32 @@ void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* top } } +int BaseTheme::getListRowStep(bool hasSubtitle, const int rowHeightScale) const { + const int rowScale = std::max(1, rowHeightScale); + int rowHeight = + ((hasSubtitle) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight) * rowScale; + return rowHeight; +} + +int BaseTheme::getListPageItems(int contentHeight, bool hasSubtitle, const int rowHeightScale) const { + const int rowStep = getListRowStep(hasSubtitle, rowHeightScale); + if (rowStep <= 0) return 1; + return std::max(1, contentHeight / rowStep); +} + void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, const std::function& rowSubtitle, const std::function& rowIcon, const std::function& rowValue, bool highlightValue, const std::function& rowDimmed, - const std::function& isHeader) const { + const std::function& isHeader, const int rowHeightScale, + const bool showSelection) const { + const int rowScale = std::max(1, rowHeightScale); int rowHeight = - (rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight; - int pageItems = rect.height / rowHeight; + ((rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight) * + rowScale; + int pageItems = rowHeight > 0 ? std::max(1, rect.height / rowHeight) : 1; constexpr int sectionHeaderTopPadding = 15; const int totalPages = (itemCount + pageItems - 1) / pageItems; @@ -294,7 +331,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, // Draw selection (skip header rows) int contentWidth = rect.width - 5; - if (selectedIndex >= 0) { + if (showSelection && selectedIndex >= 0) { renderer.fillRect(rect.x, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight); } constexpr int maxValueWidth = 240; @@ -303,7 +340,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, // Draw all items const auto pageStartIndex = selectedIndex / pageItems * pageItems; const int rectBottom = rect.y + rect.height; - if (selectedIndex >= 0 && !(isHeader && isHeader(selectedIndex))) { + if (showSelection && selectedIndex >= 0 && !(isHeader && isHeader(selectedIndex))) { int selY = rect.y; for (int j = pageStartIndex; j < selectedIndex; j++) { selY += rowHeight; @@ -317,6 +354,9 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, // Draw all visible page items for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) { const int itemY = rect.y + (i % pageItems) * rowHeight; + if (!(isHeader && isHeader(i))) { + TouchRegistry::getInstance().add(Rect{rect.x, itemY - 2, rect.width, rowHeight}, i, TouchRegistry::Item); + } int rowTextWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2; std::string valueText; @@ -373,46 +413,87 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle, const bool readerContext) const { - // Hide last battery draw - constexpr int maxBatteryWidth = 80; - renderer.fillRect(rect.x + rect.width - maxBatteryWidth, rect.y + homeHeaderTopInset, maxBatteryWidth, - BaseMetrics::values.batteryHeight + 10, false); - + namespace fui = freeink::ui; + const auto spec = uiScaleSpec(); + fui::GfxRendererFrame<1> ui(renderer, spec.smallFontId, spec.bodyFontId, spec.titleFontId); + const fui::ThemeTokens tokens = uiThemeTokens(ui.target); + ui.target.setFont(fui::GfxRendererTarget::FONT_SMALL, SMALL_FONT_ID); + const ThemeMetrics& metrics = UITheme::getInstance().getMetrics(); + const fui::Rect band{static_cast(rect.x), static_cast(rect.y), static_cast(rect.width), + static_cast(rect.height)}; + + const bool showHeaderClock = halClock.isAvailable() && (readerContext ? SETTINGS.shouldShowClockInReader() + : SETTINGS.shouldShowClockOutsideReader()); const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; - // Position icon at right edge, drawBatteryRight will place text to the left - const int batteryX = rect.x + rect.width - 12 - BaseMetrics::values.batteryWidth; - const int batteryY = rect.y + homeHeaderTopInset; - drawBatteryRight(renderer, - Rect{batteryX, batteryY, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight}, - showBatteryPercentage); - - if (title) { - int padding = rect.width - batteryX + BaseMetrics::values.batteryWidth; - auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title, - rect.width - padding * 2 - BaseMetrics::values.contentSidePadding * 2, - EpdFontFamily::BOLD); - const bool showHeaderClock = halClock.isAvailable() && (readerContext ? SETTINGS.shouldShowClockInReader() - : SETTINGS.shouldShowClockOutsideReader()); - if (showHeaderClock) { - renderer.drawText(UI_12_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, rect.y + 5, - truncatedTitle.c_str(), true, EpdFontFamily::BOLD); + const uint16_t percentage = powerManager.getBatteryPercentage(); + char percentText[8]; + snprintf(percentText, sizeof(percentText), "%u%%", static_cast(percentage)); + constexpr int16_t batteryNubWidth = 2; + int16_t batteryReserve = static_cast(metrics.batteryWidth + batteryNubWidth); + if (showBatteryPercentage) { + batteryReserve = static_cast( + batteryReserve + batteryPercentSpacing + + ui.target.measureText(fui::GfxRendererTarget::FONT_SMALL, percentText, tokens.smallText).width); + } + + fui::HeaderProps props; + props.title = title; + props.rightLabel = subtitle; + props.borderEdges = fui::EdgeBottom; + props.titleText = tokens.titleText; + const bool hasVisibleTitle = title != nullptr && title[0] != '\0'; + props.titleText.align = showHeaderClock && hasVisibleTitle ? fui::TextAlign::Left : tokens.headerTitleAlign; + props.subtitleText = tokens.smallText; + props.styles = tokens.popup; + props.sidePadding = tokens.headerSidePadding; + const bool batteryLeft = metrics.headerBatterySide == 1; + const bool batteryDetached = metrics.headerBatteryDetached; + const bool roundedRaffCompactHeader = !readerContext && + SETTINGS.uiTheme == CrossPointSettings::UI_THEME::ROUNDEDRAFF && + rect.height != metrics.homeTopPadding; + if (batteryDetached) { + const int titleLineHeight = ui.target.lineHeight(fui::GfxRendererTarget::FONT_TITLE); + const int titleTop = static_cast(band.height) - tokens.headerUnderline - tokens.spaceMd - titleLineHeight; + props.titleOffsetY = static_cast(titleTop - (static_cast(band.height) - titleLineHeight) / 2); + } else { + const int16_t reserve = static_cast(batteryReserve + tokens.spaceMd); + if (batteryLeft) { + props.leftReserve = reserve; } else { - renderer.drawCenteredText(UI_12_FONT_ID, rect.y + 5, truncatedTitle.c_str(), true, EpdFontFamily::BOLD); + props.rightReserve = reserve; } } - - if (subtitle) { - auto truncatedSubtitle = renderer.truncatedText( - SMALL_FONT_ID, subtitle, rect.width - BaseMetrics::values.contentSidePadding * 2, EpdFontFamily::REGULAR); - int truncatedSubtitleWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedSubtitle.c_str()); - renderer.drawText(SMALL_FONT_ID, - rect.x + rect.width - BaseMetrics::values.contentSidePadding - truncatedSubtitleWidth, subtitleY, - truncatedSubtitle.c_str(), true); + if (title != nullptr && props.styles.normal.border.kind == fui::PaintKind::None && tokens.headerUnderline > 0) { + props.styles.normal.border = fui::Paint::solid(fui::Color::Black); + props.styles.normal.borderWidth = tokens.headerUnderline; + } + fui::header(ui.frame, band, props); + + const int16_t batteryEdgeInset = batteryDetached ? 12 : tokens.headerSidePadding; + const int16_t batteryX = batteryLeft ? static_cast(band.x + batteryEdgeInset) + : static_cast(band.right() - batteryEdgeInset - batteryReserve); + // RoundedRaff's Home header is taller than ordinary headers. Shift compact + // headers to its battery baseline; Home already has that extra height. + const int16_t batteryY = static_cast( + band.y + + (batteryDetached + ? detachedHeaderBatteryTopInset + : (roundedRaffCompactHeader ? std::max(0, (metrics.homeTopPadding - metrics.headerHeight) / 2) : 0))); + const int16_t batteryIconX = + batteryLeft ? batteryX : static_cast(batteryX + batteryReserve - metrics.batteryWidth - batteryNubWidth); + const Rect batteryRect{batteryIconX, batteryY, metrics.batteryWidth, metrics.batteryHeight}; + if (batteryLeft) { + drawBatteryLeft(renderer, batteryRect, showBatteryPercentage); + } else { + drawBatteryRight(renderer, batteryRect, showBatteryPercentage); } - drawTopStatusBarClock(renderer, rect.y, nullptr, readerContext, - title == nullptr && !readerContext ? homeHeaderClockTextYOffset(renderer) : 0); + const bool roundedRaffHeader = !readerContext && SETTINGS.uiTheme == CrossPointSettings::UI_THEME::ROUNDEDRAFF; + const int clockYOffset = roundedRaffHeader + ? roundedRaffHeaderClockYOffset + : (!hasVisibleTitle && !readerContext ? homeHeaderClockTextYOffset(renderer) : 0); + drawTopStatusBarClock(renderer, rect.y, nullptr, readerContext, clockYOffset); } void BaseTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const { @@ -445,9 +526,13 @@ void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const s int currentX = rect.x + BaseMetrics::values.contentSidePadding; - for (const auto& tab : tabs) { + for (size_t i = 0; i < tabs.size(); ++i) { + const auto& tab = tabs[i]; const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); + TouchRegistry::getInstance().add( + Rect{currentX - 3, rect.y, textWidth + BaseMetrics::values.tabSpacing, rect.height}, static_cast(i), + TouchRegistry::Tab); // Draw underline for selected tab if (tab.selected) { @@ -466,6 +551,29 @@ void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const s } } +bool BaseTheme::tabIndexFromPoint(const GfxRenderer& renderer, const Rect rect, const std::vector& tabs, + const int x, const int y, int& index) const { + if (tabs.empty() || y < rect.y || y >= rect.y + rect.height) { + return false; + } + + int currentX = rect.x + BaseMetrics::values.contentSidePadding; + for (size_t i = 0; i < tabs.size(); i++) { + const auto& tab = tabs[i]; + const int textWidth = + renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); + const int left = (i == 0) ? rect.x : currentX - BaseMetrics::values.tabSpacing / 2; + const int right = currentX + textWidth + BaseMetrics::values.tabSpacing / 2; + if (x >= left && x < right) { + index = static_cast(i); + return true; + } + currentX += textWidth + BaseMetrics::values.tabSpacing; + } + + return false; +} + // Draw the "Recent Book" cover card on the home screen // TODO: Refactor method to make it cleaner, split into smaller methods void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, @@ -523,6 +631,9 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: bookX = rect.x + (rect.width - bookWidth) / 2; const int bookY = rect.y; const int bookHeight = baseHeight; + if (hasContinueReading) { + TouchRegistry::getInstance().add(Rect{bookX, bookY, bookWidth, bookHeight}, 0, TouchRegistry::Cover); + } // Bookmark dimensions (used in multiple places) const int bookmarkWidth = bookWidth / 8; @@ -544,8 +655,6 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: if (Storage.openFileForRead("HOME", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { - LOG_DBG("THEME", "Rendering bmp"); - // Draw the cover image (bookWidth and bookHeight already match image aspect ratio) renderer.drawBitmap(bitmap, bookX, bookY, bookWidth, bookHeight); @@ -560,7 +669,6 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // First render: if selected, draw selection indicators now if (bookSelected) { - LOG_DBG("THEME", "Drawing selection"); renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2); renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4); } @@ -620,12 +728,17 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: } } +int BaseTheme::getMenuRowHeight(const GfxRenderer&) const { return UITheme::getInstance().getMetrics().menuRowHeight; } + void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, const std::function& buttonLabel, const std::function& rowIcon) const { (void)rowIcon; constexpr int maxVisibleItems = 7; - const int pageItems = maxVisibleItems; + const auto& metrics = UITheme::getInstance().getMetrics(); + const int rowStep = metrics.menuRowHeight + metrics.menuSpacing; + const int availableHeight = std::max(0, rect.height - metrics.verticalSpacing); + const int pageItems = std::clamp((availableHeight + metrics.menuSpacing) / rowStep, 1, maxVisibleItems); const int totalPages = (buttonCount + pageItems - 1) / pageItems; const int pageStartIndex = (selectedIndex / pageItems) * pageItems; @@ -636,9 +749,8 @@ void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount constexpr int margin = 15; // Offset from right edge const int centerX = rect.x + rect.width - indicatorWidth / 2 - margin; - const int menuHeight = maxVisibleItems * (BaseMetrics::values.menuRowHeight + BaseMetrics::values.menuSpacing) - - BaseMetrics::values.menuSpacing; - const int indicatorTop = rect.y + BaseMetrics::values.verticalSpacing; + const int menuHeight = pageItems * rowStep - metrics.menuSpacing; + const int indicatorTop = rect.y + metrics.verticalSpacing; const int indicatorBottom = indicatorTop + menuHeight - arrowSize; // Draw up arrow (^) only when there are items above the current page @@ -663,31 +775,29 @@ void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount for (int i = pageStartIndex; i < buttonCount && i < pageStartIndex + pageItems; ++i) { const int displayIndex = i - pageStartIndex; - const int tileY = - BaseMetrics::values.verticalSpacing + rect.y + - static_cast(displayIndex) * (BaseMetrics::values.menuRowHeight + BaseMetrics::values.menuSpacing); + const int tileY = metrics.verticalSpacing + rect.y + static_cast(displayIndex) * rowStep; const bool selected = selectedIndex == i; - int tileWidth = rect.width - BaseMetrics::values.contentSidePadding * 2; - if (totalPages > 1) { - tileWidth -= 30; // some margin for scroll arrows - } + constexpr int paginationGutter = 30; + const int tileSidePadding = metrics.contentSidePadding + (totalPages > 1 ? paginationGutter / 2 : 0); + const int tileWidth = rect.width - tileSidePadding * 2; + const Rect tileRect{rect.x + tileSidePadding, tileY, tileWidth, metrics.menuRowHeight}; + TouchRegistry::getInstance().add(buttonMenuTouchTarget(tileRect, rect, i == buttonCount - 1, metrics.menuSpacing), + i, TouchRegistry::Item); if (selected) { - renderer.fillRect(rect.x + BaseMetrics::values.contentSidePadding, tileY, tileWidth, - BaseMetrics::values.menuRowHeight); + renderer.fillRect(rect.x + tileSidePadding, tileY, tileWidth, metrics.menuRowHeight); } else { - renderer.drawRect(rect.x + BaseMetrics::values.contentSidePadding, tileY, tileWidth, - BaseMetrics::values.menuRowHeight); + renderer.drawRect(rect.x + tileSidePadding, tileY, tileWidth, metrics.menuRowHeight); } const char* label = buttonLabel != nullptr ? buttonLabel(i) : ""; if (!label) label = ""; const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, label); - const int textX = rect.x + BaseMetrics::values.contentSidePadding + (tileWidth - textWidth) / 2; + const int textX = rect.x + tileSidePadding + (tileWidth - textWidth) / 2; const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); const int textY = - tileY + (BaseMetrics::values.menuRowHeight - lineHeight) / 2; // vertically centered assuming y is top of text + tileY + (metrics.menuRowHeight - lineHeight) / 2; // vertically centered assuming y is top of text // Invert text when the tile is selected, to contrast with the filled background renderer.drawText(UI_10_FONT_ID, textX, textY, label, selectedIndex != i); } @@ -753,40 +863,44 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c const int pageCount, std::string title, const int paddingBottom, const int textYOffset, const bool isPageBookmarked, const char* timeLeftLabel, const bool darkMode, const float chapterProgressPercent, const int stableCurrentPage, - const int stablePageCount, const bool showProgress) const { + const int stablePageCount, const bool showProgress, const bool pageCountEstimated) const { const bool foregroundBlack = !darkMode; auto metrics = UITheme::getInstance().getMetrics(); int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, &orientedMarginLeft); + const auto statusBar = SETTINGS.statusBarSpec(); + const bool showStatusBarTextLane = statusBar.textLaneVisible(halClock.isAvailable()); // Draw Progress Text const auto screenHeight = renderer.getScreenHeight(); auto textY = screenHeight - UITheme::getInstance().getStatusBarHeight() - orientedMarginBottom - paddingBottom - 4; int progressTextWidth = 0; - const bool showStablePageNumbers = SETTINGS.stablePageNumbers && stableCurrentPage > 0 && stablePageCount > 0; - if (showProgress && - (SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarChapterPageCount || showStablePageNumbers)) { + const bool showStablePageNumbers = statusBar.showStablePageNumbers && stableCurrentPage > 0 && stablePageCount > 0; + if (showProgress && (statusBar.showBookProgressPercent || statusBar.showChapterPageCount || showStablePageNumbers)) { // Right aligned text for progress counter char progressStr[48]; - - if (SETTINGS.statusBarChapterPageCount && showStablePageNumbers && SETTINGS.statusBarBookProgressPercentage) { - snprintf(progressStr, sizeof(progressStr), "%d/%d %d/%d %.0f%%", currentPage, pageCount, stableCurrentPage, - stablePageCount, bookProgress); - } else if (SETTINGS.statusBarChapterPageCount && showStablePageNumbers) { - snprintf(progressStr, sizeof(progressStr), "%d/%d %d/%d", currentPage, pageCount, stableCurrentPage, - stablePageCount); - } else if (SETTINGS.statusBarChapterPageCount && SETTINGS.statusBarBookProgressPercentage) { - snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", currentPage, pageCount, bookProgress); - } else if (showStablePageNumbers && SETTINGS.statusBarBookProgressPercentage) { + // Prefix the section page count with "~" while a still-building spine only yields an estimated total. + const char* estimatePrefix = pageCountEstimated ? "~" : ""; + + if (statusBar.showChapterPageCount && showStablePageNumbers && statusBar.showBookProgressPercent) { + snprintf(progressStr, sizeof(progressStr), "%s%d/%d %d/%d %.0f%%", estimatePrefix, currentPage, pageCount, + stableCurrentPage, stablePageCount, bookProgress); + } else if (statusBar.showChapterPageCount && showStablePageNumbers) { + snprintf(progressStr, sizeof(progressStr), "%s%d/%d %d/%d", estimatePrefix, currentPage, pageCount, + stableCurrentPage, stablePageCount); + } else if (statusBar.showChapterPageCount && statusBar.showBookProgressPercent) { + snprintf(progressStr, sizeof(progressStr), "%s%d/%d %.0f%%", estimatePrefix, currentPage, pageCount, + bookProgress); + } else if (showStablePageNumbers && statusBar.showBookProgressPercent) { snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", stableCurrentPage, stablePageCount, bookProgress); - } else if (SETTINGS.statusBarBookProgressPercentage) { + } else if (statusBar.showBookProgressPercent) { snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress); } else if (showStablePageNumbers) { snprintf(progressStr, sizeof(progressStr), "%d/%d", stableCurrentPage, stablePageCount); } else { - snprintf(progressStr, sizeof(progressStr), "%d/%d", currentPage, pageCount); + snprintf(progressStr, sizeof(progressStr), "%s%d/%d", estimatePrefix, currentPage, pageCount); } progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr); @@ -797,12 +911,12 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c } // Draw Progress Bar - if (showProgress && SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS) { + if (showProgress && statusBar.showsProgressBar()) { const int progressBarMaxWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; - const int progressBarY = renderer.getScreenHeight() - orientedMarginBottom - - ((SETTINGS.statusBarProgressBarThickness + 1) * 2) - paddingBottom; + const int progressBarY = + renderer.getScreenHeight() - orientedMarginBottom - statusBar.progressBarHeightPx - paddingBottom; size_t progress; - if (SETTINGS.statusBarProgressBar == CrossPointSettings::STATUS_BAR_PROGRESS_BAR::BOOK_PROGRESS) { + if (statusBar.progressBarMode == CrossPointSettings::STATUS_BAR_PROGRESS_BAR::BOOK_PROGRESS) { progress = static_cast(bookProgress); } else if (chapterProgressPercent >= 0.0f) { progress = static_cast(std::clamp(chapterProgressPercent, 0.0f, 100.0f)); @@ -811,8 +925,7 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c progress = (pageCount > 0) ? (static_cast(currentPage) / pageCount) * 100 : 0; } const int barWidth = progressBarMaxWidth * progress / 100; - renderer.fillRect(orientedMarginLeft, progressBarY, barWidth, ((SETTINGS.statusBarProgressBarThickness + 1) * 2), - foregroundBlack); + renderer.fillRect(orientedMarginLeft, progressBarY, barWidth, statusBar.progressBarHeightPx, foregroundBlack); } // Bookmark icon: drawn at the far left of the status bar when the current page is bookmarked. @@ -823,9 +936,10 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c static constexpr int bmNotchDepth = 5; static constexpr int statusItemGap = 8; const int leftClusterX = metrics.statusBarHorizontalMargin + orientedMarginLeft + 1; - const int bmTotalWidth = isPageBookmarked ? (bmIconW + bmIconGap) : 0; + const bool showBookmark = showStatusBarTextLane && isPageBookmarked; + const int bmTotalWidth = showBookmark ? (bmIconW + bmIconGap) : 0; - if (isPageBookmarked) { + if (showBookmark) { const int bmX = leftClusterX; // +5 compensates for the battery nub drawn above the rect origin by drawBatteryLeft, // which shifts the battery body's visual center below the mathematical rect center. @@ -837,10 +951,9 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c } // Draw Battery - const bool showBatteryPercentage = - SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; + const bool showBatteryPercentage = statusBar.showBatteryPercent; int leftClusterWidth = bmTotalWidth; - if (SETTINGS.statusBarBattery) { + if (statusBar.showBattery) { GUI.drawBatteryLeft(renderer, Rect{leftClusterX + bmTotalWidth, textY, metrics.batteryWidth, metrics.batteryHeight}, showBatteryPercentage, foregroundBlack); int batteryWidth = metrics.batteryWidth; @@ -960,104 +1073,6 @@ void BaseTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int } } -void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, - const char* secondaryLabel, const KeyboardKeyType keyType, - const bool inactiveSelection) const { - const auto& metrics = UITheme::getInstance().getMetrics(); - const int cr = metrics.keyboardKeyCornerRadius; - const bool isSpecialKey = keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode || - keyType == KeyboardKeyType::Del || keyType == KeyboardKeyType::Space || - keyType == KeyboardKeyType::Ok || keyType == KeyboardKeyType::Disabled; - - if (isSelected) { - if (inactiveSelection) { - if (cr > 0) { - renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cr, Color::LightGray); - } else { - renderer.drawRect(rect.x, rect.y, rect.width, rect.height, 2, true); - } - } else if (keyType == KeyboardKeyType::Disabled) { - if (cr > 0) { - renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cr, Color::LightGray); - } else { - renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray); - } - } else { - if (cr > 0) { - renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cr, Color::Black); - } else { - renderer.fillRect(rect.x, rect.y, rect.width, rect.height, true); - } - } - } else { - if (metrics.keyboardFillUnselected) { - if (keyType == KeyboardKeyType::Disabled) { - if (cr > 0) { - renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cr, Color::LightGray); - } else { - renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray); - } - } else { - if (cr > 0) { - renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cr, Color::White); - } else { - renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false); - } - } - } - - const bool shouldDrawOutline = - (metrics.keyboardDrawSpecialOutlineWhenUnselected && isSpecialKey) || metrics.keyboardOutlineAllUnselected; - if (shouldDrawOutline) { - if (cr > 0) { - renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, cr, true); - } else { - renderer.drawRect(rect.x, rect.y, rect.width, rect.height); - } - } - } - - const bool invert = isSelected && !inactiveSelection; - - if (keyType == KeyboardKeyType::Space) { - const int lineHalfWidth = rect.width * 3 / 10; - const int centerX = rect.x + rect.width / 2; - const int lineY = rect.y + rect.height / 2 + 3; - renderer.drawLine(centerX - lineHalfWidth, lineY, centerX + lineHalfWidth, lineY, 3, !invert); - return; - } - - if (keyType == KeyboardKeyType::Del) { - const int centerX = rect.x + rect.width / 2; - const int centerY = rect.y + rect.height / 2; - const int arrowLen = rect.width / 4; - const int arrowHead = std::max(metrics.keyboardMinArrowHeadSize, arrowLen / 2); - renderer.drawLine(centerX - arrowLen / 2, centerY, centerX + arrowLen / 2, centerY, 3, !invert); - renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY - arrowHead, 3, - !invert); - renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY + arrowHead, 3, - !invert); - return; - } - - if (label == nullptr || label[0] == '\0') { - return; - } - - const bool hasSecondary = secondaryLabel != nullptr && secondaryLabel[0] != '\0'; - const int itemWidth = renderer.getTextWidth(UI_12_FONT_ID, label); - const int textX = rect.x + (rect.width - itemWidth) / 2; - const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2; - - renderer.drawText(UI_12_FONT_ID, textX, textY, label, !invert); - - if (hasSecondary) { - const int secWidth = renderer.getTextWidth(SMALL_FONT_ID, secondaryLabel); - renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - secWidth - metrics.keyboardSecondaryLabelRightPadding, - rect.y + metrics.keyboardSecondaryLabelTopPadding, secondaryLabel, !invert); - } -} - void BaseTheme::drawOptionPopup(const GfxRenderer& renderer, const char* title, const std::vector& options, int selectedIndex) const { const auto& metrics = UITheme::getInstance().getMetrics(); @@ -1090,8 +1105,21 @@ void BaseTheme::drawOptionPopup(const GfxRenderer& renderer, const char* title, const int maxDialogH = std::max(rowHeight + titleLineHeight + metrics.optionPopupTitleGap + innerPadding * 2, pageHeight - metrics.buttonHintsHeight - metrics.optionPopupDialogSideMargin * 2); + // Reserve the narrow scroll gutter up front. A wrapped title may reduce the + // number of visible options, so deciding this after title layout would make + // the draw and cached touch geometry disagree. + const int dialogW = std::min((maxTextWidth + innerPadding * 2 + selectionHPadding * 2 + metrics.scrollBarWidth + + metrics.scrollBarRightOffset + selectionHPadding) * + 12 / 10, + pageWidth - metrics.optionPopupDialogSideMargin * 2); + const int titleContentWidth = std::max(1, dialogW - innerPadding * 2); + const int maxTitleLines = + std::max(1, (maxDialogH - innerPadding * 2 - metrics.optionPopupTitleGap - rowHeight) / titleLineHeight); + const auto titleLines = + renderer.wrappedText(UI_12_FONT_ID, title, titleContentWidth, maxTitleLines, EpdFontFamily::BOLD); + const int titleHeight = static_cast(titleLines.size()) * titleLineHeight; const int maxListHeight = - std::max(rowHeight, maxDialogH - innerPadding * 2 - titleLineHeight - metrics.optionPopupTitleGap); + std::max(rowHeight, maxDialogH - innerPadding * 2 - titleHeight - metrics.optionPopupTitleGap); const int rowStep = rowHeight + itemSpacing; const int maxVisibleOptions = std::max(1, std::min(optionCount, (maxListHeight + itemSpacing) / rowStep)); const int safeSelectedIndex = std::clamp(selectedIndex, 0, optionCount - 1); @@ -1099,9 +1127,10 @@ void BaseTheme::drawOptionPopup(const GfxRenderer& renderer, const char* title, const int visibleEnd = visibleStart + maxVisibleOptions; const int visibleCount = visibleEnd - visibleStart; const int listHeight = rowHeight * visibleCount + itemSpacing * (visibleCount - 1); - const int dialogW = std::min((maxTextWidth + innerPadding * 2 + selectionHPadding * 2) * 12 / 10, - pageWidth - metrics.optionPopupDialogSideMargin * 2); - const int contentHeight = titleLineHeight + metrics.optionPopupTitleGap + listHeight; + const bool hasHiddenOptions = visibleCount < optionCount; + const int scrollBarGutter = + hasHiddenOptions ? metrics.scrollBarWidth + metrics.scrollBarRightOffset + selectionHPadding : 0; + const int contentHeight = titleHeight + metrics.optionPopupTitleGap + listHeight; const int dialogH = contentHeight + innerPadding * 2; const int dialogX = (pageWidth - dialogW) / 2; const int dialogY = (pageHeight - dialogH) / 2; @@ -1124,8 +1153,11 @@ void BaseTheme::drawOptionPopup(const GfxRenderer& renderer, const char* title, int y = dialogY + innerPadding; - renderer.drawCenteredText(UI_12_FONT_ID, y, title, true, EpdFontFamily::BOLD); - y += titleLineHeight; + for (const auto& line : titleLines) { + const int lineWidth = renderer.getTextWidth(UI_12_FONT_ID, line.c_str(), EpdFontFamily::BOLD); + renderer.drawText(UI_12_FONT_ID, dialogX + (dialogW - lineWidth) / 2, y, line.c_str(), true, EpdFontFamily::BOLD); + y += titleLineHeight; + } if (metrics.optionPopupTitleSeparator) { const int sepY = y + metrics.optionPopupTitleGap / 2; @@ -1135,9 +1167,19 @@ void BaseTheme::drawOptionPopup(const GfxRenderer& renderer, const char* title, y += metrics.optionPopupTitleGap; const int itemRectX = dialogX + innerPadding; - const int itemRectW = dialogW - innerPadding * 2; + const int itemRectW = std::max(1, dialogW - innerPadding * 2 - scrollBarGutter); const int selectionRadius = metrics.optionPopupSelectionRadius; + if (hasHiddenOptions) { + const int scrollBarX = dialogX + dialogW - innerPadding - metrics.scrollBarRightOffset; + const int scrollBarHeight = std::max(metrics.scrollBarWidth, (listHeight * visibleCount) / optionCount); + const int scrollRange = std::max(0, listHeight - scrollBarHeight); + const int scrollSteps = std::max(1, optionCount - visibleCount); + const int scrollBarY = y + (scrollRange * visibleStart) / scrollSteps; + renderer.drawLine(scrollBarX, y, scrollBarX, y + listHeight, true); + renderer.fillRect(scrollBarX - metrics.scrollBarWidth, scrollBarY, metrics.scrollBarWidth, scrollBarHeight, true); + } + for (int visibleIndex = 0; visibleIndex < visibleCount; visibleIndex++) { const int optionIndex = visibleStart + visibleIndex; const int itemY = y + visibleIndex * (rowHeight + itemSpacing); diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index 6c8129a31b..b1bd6d2f66 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -25,6 +25,11 @@ struct TabInfo { bool selected; }; +enum class ThemeTabBarAppearance : uint8_t { + Pill, + BorderedText, +}; + struct ThemeMetrics { int batteryWidth; int batteryHeight; @@ -40,11 +45,26 @@ struct ThemeMetrics { int contentSidePadding; int listRowHeight; int listWithSubtitleRowHeight; + // FreeInkUI screens take shape from the theme and size from UIScale. + int listRowGap; + int listRowRadius; + int listInset; + int listSidePadding; + int listSelectionStyle; + int listScrollWidth; + int listScrollSide; + bool listTitleBold; + int headerSidePadding; + int headerUnderlineSize; + int headerTitleAlign; + int headerBatterySide; + bool headerBatteryDetached; int menuRowHeight; int menuSpacing; int tabSpacing; int tabBarHeight; + ThemeTabBarAppearance tabBarAppearance; int scrollBarWidth; int scrollBarRightOffset; @@ -63,24 +83,12 @@ struct ThemeMetrics { int progressBarMarginTop; int statusBarHorizontalMargin; int statusBarVerticalMargin; - - int keyboardKeyWidth; int keyboardKeyHeight; int keyboardKeySpacing; - int keyboardBottomKeyHeight; - int keyboardBottomKeySpacing; - bool keyboardBottomAligned; bool keyboardCenteredText; int keyboardVerticalOffset; int keyboardTextFieldWidthPercent; int keyboardWidthPercent; - int keyboardKeyCornerRadius; - bool keyboardFillUnselected; - bool keyboardOutlineAllUnselected; - bool keyboardDrawSpecialOutlineWhenUnselected; - int keyboardSecondaryLabelRightPadding; - int keyboardSecondaryLabelTopPadding; - int keyboardMinArrowHeadSize; float popupTopOffsetRatio; int popupMarginX; @@ -131,8 +139,6 @@ enum UIIcon { Chart }; -enum class KeyboardKeyType { Normal, Shift, Mode, Space, Del, Ok, Disabled }; - // Default theme implementation (Classic Theme) // Additional themes can inherit from this and override methods as needed @@ -140,18 +146,32 @@ namespace BaseMetrics { constexpr ThemeMetrics values = {.batteryWidth = 15, .batteryHeight = 12, .topPadding = 5, - .batteryBarHeight = 20, - .headerHeight = 45, + .batteryBarHeight = 45, + .headerHeight = 70, .verticalSpacing = 10, .previewPadding = 12, .previewHeightPercent = 30, .contentSidePadding = 20, .listRowHeight = 30, .listWithSubtitleRowHeight = 50, + .listRowGap = 0, + .listRowRadius = 0, + .listInset = 0, + .listSidePadding = 20, + .listSelectionStyle = 0, + .listScrollWidth = 4, + .listScrollSide = 0, + .listTitleBold = false, + .headerSidePadding = 18, + .headerUnderlineSize = 0, + .headerTitleAlign = 1, + .headerBatterySide = 0, + .headerBatteryDetached = true, .menuRowHeight = 45, .menuSpacing = 8, .tabSpacing = 10, .tabBarHeight = 50, + .tabBarAppearance = ThemeTabBarAppearance::Pill, .scrollBarWidth = 4, .scrollBarRightOffset = 5, .homeTopPadding = 40, @@ -166,23 +186,12 @@ constexpr ThemeMetrics values = {.batteryWidth = 15, .progressBarMarginTop = 1, .statusBarHorizontalMargin = 5, .statusBarVerticalMargin = 19, - .keyboardKeyWidth = 22, - .keyboardKeyHeight = 40, + .keyboardKeyHeight = 48, .keyboardKeySpacing = 0, - .keyboardBottomKeyHeight = 35, - .keyboardBottomKeySpacing = 5, - .keyboardBottomAligned = true, .keyboardCenteredText = false, .keyboardVerticalOffset = -13, .keyboardTextFieldWidthPercent = 85, - .keyboardWidthPercent = 90, - .keyboardKeyCornerRadius = 0, - .keyboardFillUnselected = false, - .keyboardOutlineAllUnselected = false, - .keyboardDrawSpecialOutlineWhenUnselected = true, - .keyboardSecondaryLabelRightPadding = 1, - .keyboardSecondaryLabelTopPadding = 0, - .keyboardMinArrowHeadSize = 0, + .keyboardWidthPercent = 94, .popupTopOffsetRatio = 0.075f, .popupMarginX = 15, .popupMarginY = 15, @@ -229,19 +238,25 @@ class BaseTheme { virtual void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, const char* btn4, bool allowInvertedText = false) const; virtual void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const; + virtual int getMenuRowHeight(const GfxRenderer& renderer) const; + virtual int getListRowStep(bool hasSubtitle, int rowHeightScale = 1) const; + virtual int getListPageItems(int contentHeight, bool hasSubtitle, int rowHeightScale = 1) const; virtual void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, const std::function& rowSubtitle = nullptr, const std::function& rowIcon = nullptr, const std::function& rowValue = nullptr, bool highlightValue = false, const std::function& rowDimmed = nullptr, - const std::function& isHeader = nullptr) const; + const std::function& isHeader = nullptr, int rowHeightScale = 1, + bool showSelection = true) const; virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle = nullptr, bool readerContext = false) const; virtual void drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel = nullptr) const; virtual void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, bool selected) const; + virtual bool tabIndexFromPoint(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, int x, int y, + int& index) const; virtual void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, const std::function& storeCoverBuffer, @@ -260,15 +275,12 @@ class BaseTheme { const int textYOffset = 0, const bool isPageBookmarked = false, const char* timeLeftLabel = nullptr, bool darkMode = false, float chapterProgressPercent = -1.0f, int stableCurrentPage = 0, int stablePageCount = 0, - bool showProgress = true) const; + bool showProgress = true, bool pageCountEstimated = false) const; virtual void drawTopStatusBarClock(const GfxRenderer& renderer, int topY = -1, const char* previewTime = nullptr, bool readerContext = true, int textYOffset = 0, bool darkMode = false) const; virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const; virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false, int contentStartX = 0, int contentWidth = 0) const; - virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, - const char* secondaryLabel = nullptr, KeyboardKeyType keyType = KeyboardKeyType::Normal, - bool inactiveSelection = false) const; virtual bool showsFileIcons() const { return false; } virtual bool usesCompactFileBrowserRows() const { return false; } virtual int compactFileBrowserRowHeight(const GfxRenderer&) const { return BaseMetrics::values.listRowHeight; } @@ -279,6 +291,7 @@ class BaseTheme { static constexpr int batteryPercentSpacing = 4; static constexpr int homeHeaderTopInset = 2; static int homeHeaderClockTextYOffset(const GfxRenderer& renderer); + static Rect buttonMenuTouchTarget(Rect rowRect, Rect menuRect, bool isLastItem, int rowSpacing); static void drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, bool foregroundBlack = true); static void drawBatteryLightningBolt(const GfxRenderer& renderer, int boltX, int boltY, bool foregroundBlack = false); diff --git a/src/components/themes/dashboard/DashboardTheme.cpp b/src/components/themes/dashboard/DashboardTheme.cpp index 87bd9244b3..9fc4439d93 100644 --- a/src/components/themes/dashboard/DashboardTheme.cpp +++ b/src/components/themes/dashboard/DashboardTheme.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -19,10 +20,11 @@ #include "activities/reader/BookReadingStats.h" #include "activities/reader/GlobalReadingStats.h" #include "activities/reader/ReadingStatsUtils.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" +#include "components/UiAppHelpers.h" #include "components/icons/afternoon.h" #include "components/icons/book24.h" -#include "components/icons/cover.h" #include "components/icons/evening.h" #include "components/icons/morning.h" #include "components/icons/night.h" @@ -95,7 +97,7 @@ void drawMissingBookCover(const GfxRenderer& renderer, const Rect& coverRect, co renderer.drawRoundedRect(coverRect.x, coverRect.y, coverRect.width, coverRect.height, 1, kCoverCornerRadius, true); constexpr int iconSize = 32; - renderer.drawIcon(CoverIcon, coverRect.x + (coverRect.width - iconSize) / 2, coverRect.y + 36, iconSize, iconSize); + drawLucideIcon(renderer, icon_book_open_32, coverRect.x + (coverRect.width - iconSize) / 2, coverRect.y + 36); constexpr int textPadding = 14; const int textW = coverRect.width - textPadding * 2; @@ -248,7 +250,7 @@ void drawDashboardStats(const GfxRenderer& renderer, const Rect& coverRect, cons const float progressPercent, const bool black = true) { const int rightX = renderer.getScreenWidth() - contentInset(renderer) - (gpio.deviceIsX3() ? kPairInwardShiftX3 : 0); const int blockH = statsBlockHeight(renderer); - const bool showRtcStats = gpio.deviceIsX3(); + const bool showRtcStats = halClock.isAvailable(); const int rowCount = showRtcStats ? kStatsRowCount : kStatsRowCountX4; const BookReadingStats emptyStats{}; const BookReadingStats& bookStats = stats != nullptr ? *stats : emptyStats; @@ -277,7 +279,7 @@ void drawDashboardStats(const GfxRenderer& renderer, const Rect& coverRect, cons } else { snprintf(value, sizeof(value), "-"); } - drawStatsRow(renderer, rightX, rowY, value, tr(STR_TIME_LEFT), black); + drawStatsRow(renderer, rightX, rowY, value, tr(STR_TIME_LEFT_SHORT), black); rowY = statsBlockTop(coverRect, ++rowIndex, blockH, rowCount); if (progressPercent >= 0.0f) { @@ -477,10 +479,11 @@ void drawRightAnchoredFooterStat(const GfxRenderer& renderer, const int labelRig void drawFooterStats(const GfxRenderer& renderer, const Rect& coverRect, const GlobalReadingStats* globalStats, const bool inverted = false) { const int inset = contentInset(renderer); - const int footerY = renderer.getScreenHeight() - DashboardMetrics::values.buttonHintsHeight - kFooterBottomGap; + const int buttonHintReserve = gpio.hasTouch() ? 0 : DashboardMetrics::values.buttonHintsHeight; + const int footerY = renderer.getScreenHeight() - buttonHintReserve - kFooterBottomGap; const int centerY = std::max(coverRect.y + coverRect.height + 120, footerY); - if (gpio.deviceIsX4()) { + if (!halClock.isAvailable()) { char totalTime[40]; char booksRead[16]; const uint32_t totalReadingSeconds = globalStats != nullptr ? globalStats->totalReadingSeconds : 0; @@ -491,7 +494,7 @@ void drawFooterStats(const GfxRenderer& renderer, const Rect& coverRect, const G const int halfW = renderer.getScreenWidth() / 2; const int maxTextW = std::max(1, halfW - inset * 2); drawLeftAnchoredFooterStat(renderer, coverRect.x, centerY, maxTextW, totalTime, - tr(STR_STATS_TOTAL_READING_TIME_LBL), inverted); + tr(STR_STATS_TOTAL_READING_TIME_LBL_SHORT), inverted); const int rightX = renderer.getScreenWidth() - inset; drawRightAnchoredFooterStat(renderer, rightX, centerY, maxTextW, booksRead, tr(STR_STATS_COMPLETED_LBL), inverted); return; @@ -504,7 +507,7 @@ void drawFooterStats(const GfxRenderer& renderer, const Rect& coverRect, const G drawIconLabel(renderer, StreakIcon, coverRect.x, centerY, streakBuf, leftTextW, inverted); const char* readerLabel = readerTypeLabel(globalStats); - const int rightX = renderer.getScreenWidth() - inset - kPairInwardShiftX3; + const int rightX = renderer.getScreenWidth() - inset - (gpio.deviceIsX3() ? kPairInwardShiftX3 : 0); const int maxReaderTextW = std::max(1, renderer.getScreenWidth() / 2 - inset - kFooterIconSize - kFooterIconTextGap); drawRightAlignedIconLabel(renderer, readerTypeIcon(globalStats), rightX, centerY, readerLabel, maxReaderTextW, inverted); @@ -557,6 +560,7 @@ void DashboardTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const coverBufferStored = storeCoverBuffer(); coverRendered = coverBufferStored; } + TouchRegistry::getInstance().add(coverRect, 0, TouchRegistry::Cover); drawDashboardStats(renderer, coverRect, stats, progressPercent); drawBookText(renderer, coverRect, recentBooks[0], currentChapterTitle); diff --git a/src/components/themes/lyra/Lyra3CoversTheme.cpp b/src/components/themes/lyra/Lyra3CoversTheme.cpp index 4baa1a2a1a..a595b06a09 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.cpp +++ b/src/components/themes/lyra/Lyra3CoversTheme.cpp @@ -8,8 +8,9 @@ #include #include "RecentBooksStore.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" -#include "components/icons/cover.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" // Internal constants @@ -75,7 +76,8 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con tileY + hPaddingInSelection + (Lyra3CoversMetrics::values.homeCoverHeight / 3), tileWidth - 2 * hPaddingInSelection, 2 * Lyra3CoversMetrics::values.homeCoverHeight / 3, true); - renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32); + drawLucideIcon(renderer, icon_book_open_32, tileX + hPaddingInSelection + 24, + tileY + hPaddingInSelection + 24); } } @@ -88,6 +90,9 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con bool bookSelected = (selectorIndex == i); int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i; + const int targetHeight = + Lyra3CoversMetrics::values.homeCoverHeight + hPaddingInSelection + renderer.getLineHeight(SMALL_FONT_ID) * 3; + TouchRegistry::getInstance().add(Rect{tileX, tileY, tileWidth, targetHeight}, i, TouchRegistry::Cover); const int maxLineWidth = tileWidth - 2 * hPaddingInSelection; diff --git a/src/components/themes/lyra/LyraCarouselTheme.cpp b/src/components/themes/lyra/LyraCarouselTheme.cpp index e817c7dd4c..f243408abe 100644 --- a/src/components/themes/lyra/LyraCarouselTheme.cpp +++ b/src/components/themes/lyra/LyraCarouselTheme.cpp @@ -16,7 +16,10 @@ #include "RecentBooksStore.h" #include "activities/reader/BookReadingStats.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" +#include "components/UiAppHelpers.h" +#include "components/icons/chart.h" #include "components/icons/cover.h" #include "fontIds.h" @@ -172,6 +175,64 @@ Rect computeCenterCoverSlotRect(const GfxRenderer& renderer, Rect rect, const st return Rect{centerX, centerDrawY, kDisplayCenterW, kDisplayCenterH}; } +void registerCoverTarget(int bookIdx, Rect rect, int bookCount, int* registeredIds, int& registeredCount) { + if (bookIdx < 0 || bookIdx >= bookCount || rect.width <= 0 || rect.height <= 0) return; + for (int i = 0; i < registeredCount; ++i) { + if (registeredIds[i] == bookIdx) return; + } + if (registeredCount < LyraCarouselMetrics::values.homeRecentBooksCount) { + registeredIds[registeredCount++] = bookIdx; + } + TouchRegistry::getInstance().add(rect, bookIdx, TouchRegistry::Cover); +} + +void registerCarouselCoverTouchTargets(const GfxRenderer& renderer, Rect rect, + const std::vector& recentBooks, int centerIdx) { + const int bookCount = static_cast(recentBooks.size()); + if (bookCount <= 0 || centerIdx < 0 || centerIdx >= bookCount) return; + + const int screenW = renderer.getScreenWidth(); + const Rect centerCoverSlotRect = computeCenterCoverSlotRect(renderer, rect, recentBooks); + const int centerX = centerCoverSlotRect.x; + const int sideMaxHeight = std::max(kNearSideInnerH, kNearSideOuterH); + const int sideTileY = centerCoverSlotRect.y + (kDisplayCenterH - sideMaxHeight) / 2; + const int nearOverlap = 4; + const int farOverlap = 2; + constexpr int nearCoverInset = 10; + const int baseLeftNearX = centerX - kNearSideW + nearOverlap; + const int baseRightNearX = centerX + kDisplayCenterW - nearOverlap; + const int leftNearX = baseLeftNearX + nearCoverInset; + const int rightNearX = baseRightNearX - nearCoverInset; + const int leftFarX = std::max(0, baseLeftNearX - kFarSideW + farOverlap); + const int rightFarX = std::min(screenW - kFarSideW, baseRightNearX + kNearSideW - farOverlap); + int registeredIds[LyraCarouselMetrics::values.homeRecentBooksCount] = {-1, -1, -1}; + int registeredCount = 0; + + const int leftNearIdx = (centerIdx + bookCount - 1) % bookCount; + const int leftFarIdx = (centerIdx + bookCount - 2) % bookCount; + const int rightNearIdx = (centerIdx + 1) % bookCount; + const int rightFarIdx = (centerIdx + 2) % bookCount; + + if (bookCount >= 5) { + registerCoverTarget(leftFarIdx, Rect{leftFarX, sideTileY, kFarSideW, std::max(kFarSideInnerH, kFarSideOuterH)}, + bookCount, registeredIds, registeredCount); + } + if (bookCount >= 4) { + registerCoverTarget(rightFarIdx, Rect{rightFarX, sideTileY, kFarSideW, std::max(kFarSideOuterH, kFarSideInnerH)}, + bookCount, registeredIds, registeredCount); + } + if (bookCount >= 2) { + registerCoverTarget(leftNearIdx, Rect{leftNearX, sideTileY, kNearSideW, std::max(kNearSideInnerH, kNearSideOuterH)}, + bookCount, registeredIds, registeredCount); + } + if (bookCount >= 3) { + registerCoverTarget(rightNearIdx, + Rect{rightNearX, sideTileY, kNearSideW, std::max(kNearSideOuterH, kNearSideInnerH)}, bookCount, + registeredIds, registeredCount); + } + registerCoverTarget(centerIdx, shrinkCenterCoverRect(centerCoverSlotRect), bookCount, registeredIds, registeredCount); +} + void drawMenuBookmarkIcon(const GfxRenderer& renderer, int x, int y, bool selected) { constexpr int ribbonWidth = 16; constexpr int ribbonHeight = 22; @@ -241,6 +302,7 @@ void LyraCarouselTheme::setPreRenderIndex(int idx) { void LyraCarouselTheme::drawCarouselBorder(GfxRenderer& renderer, Rect coverRect, const std::vector& recentBooks, int centerIdx, bool inCarouselRow) const { + registerCarouselCoverTouchTargets(renderer, coverRect, recentBooks, centerIdx); if (!inCarouselRow) return; Rect borderRect = shrinkCenterCoverRect(computeCenterCoverSlotRect(renderer, coverRect, recentBooks)); renderer.drawRoundedRect(borderRect.x, borderRect.y, borderRect.width, borderRect.height, kSelectionLineW, @@ -498,6 +560,7 @@ void LyraCarouselTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, // Always outline the centre cover at its own edge (white ring sits outside the black line); // thicker when the carousel row is active + registerCarouselCoverTouchTargets(renderer, rect, recentBooks, centerIdx); const int outlineW = inCarouselRow ? kSelectionLineW : kThinOutlineW; renderer.drawRoundedRect(lastCenterCoverRect.x, lastCenterCoverRect.y, lastCenterCoverRect.width, lastCenterCoverRect.height, outlineW, kCornerRadius, true); @@ -513,6 +576,7 @@ void LyraCarouselTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int but // Rect is retained by the BaseTheme interface; this carousel menu anchors to the screen bottom. const MenuLayoutMetrics metrics = computeMenuLayout(renderer, buttonCount); + registerButtonMenuTouchTargets(renderer, buttonCount); for (int i = 0; i < buttonCount; ++i) { const int tileX = i * metrics.tileW; @@ -531,13 +595,18 @@ void LyraCarouselTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int but const UIIcon icon = rowIcon(i); if (icon == UIIcon::BookmarkIcon) { drawMenuBookmarkIcon(renderer, iconX, iconY, selected); + } else if (icon == UIIcon::Chart) { + if (selected) + renderer.drawIconInverted(ChartIcon, iconX, iconY, kMenuIconSize, kMenuIconSize); + else + renderer.drawIcon(ChartIcon, iconX, iconY, kMenuIconSize, kMenuIconSize); } else { - const uint8_t* bmp = iconForName(icon, kMenuIconSize); + const freeink::Icon* bmp = iconForName(icon, kMenuIconSize); if (bmp != nullptr) { if (selected) - renderer.drawIconInverted(bmp, iconX, iconY, kMenuIconSize, kMenuIconSize); + drawLucideIcon(renderer, *bmp, iconX, iconY, false); else - renderer.drawIcon(bmp, iconX, iconY, kMenuIconSize, kMenuIconSize); + drawLucideIcon(renderer, *bmp, iconX, iconY); } } } @@ -554,12 +623,23 @@ void LyraCarouselTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int but } } +void LyraCarouselTheme::registerButtonMenuTouchTargets(const GfxRenderer& renderer, int buttonCount) const { + if (buttonCount <= 0) return; + const MenuLayoutMetrics metrics = computeMenuLayout(renderer, buttonCount); + for (int i = 0; i < buttonCount; ++i) { + TouchRegistry::getInstance().add( + Rect{i * metrics.tileW, metrics.labelY, metrics.tileW, metrics.rowY + metrics.tileH - metrics.labelY}, i, + TouchRegistry::Item); + } +} + void LyraCarouselTheme::drawButtonMenuSelectionOverlay(const GfxRenderer& renderer, int buttonCount, int selectedIndex, const std::function& buttonLabel, const std::function& rowIcon) const { if (buttonCount <= 0 || selectedIndex < 0 || selectedIndex >= buttonCount) return; const MenuLayoutMetrics metrics = computeMenuLayout(renderer, buttonCount); + registerButtonMenuTouchTargets(renderer, buttonCount); const int tileX = selectedIndex * metrics.tileW; const int iconX = tileX + (metrics.tileW - kMenuIconSize) / 2; @@ -574,10 +654,12 @@ void LyraCarouselTheme::drawButtonMenuSelectionOverlay(const GfxRenderer& render const UIIcon icon = rowIcon(selectedIndex); if (icon == UIIcon::BookmarkIcon) { drawMenuBookmarkIcon(renderer, iconX, iconY, true); + } else if (icon == UIIcon::Chart) { + renderer.drawIconInverted(ChartIcon, iconX, iconY, kMenuIconSize, kMenuIconSize); } else { - const uint8_t* bmp = iconForName(icon, kMenuIconSize); + const freeink::Icon* bmp = iconForName(icon, kMenuIconSize); if (bmp != nullptr) { - renderer.drawIconInverted(bmp, iconX, iconY, kMenuIconSize, kMenuIconSize); + drawLucideIcon(renderer, *bmp, iconX, iconY, false); } } } @@ -602,7 +684,9 @@ void LyraCarouselTheme::drawList(const GfxRenderer& renderer, Rect rect, int ite const std::function& rowIcon, const std::function& rowValue, bool highlightValue, const std::function& rowDimmed, - const std::function& isHeader) const { + const std::function& isHeader, const int rowHeightScale, + const bool showSelection) const { drawListWithMetrics(renderer, rect, itemCount, selectedIndex, rowTitle, rowSubtitle, rowIcon, rowValue, - highlightValue, rowDimmed, isHeader, LyraCarouselMetrics::values, true); + highlightValue, rowDimmed, isHeader, LyraCarouselMetrics::values, true, rowHeightScale, + showSelection); } diff --git a/src/components/themes/lyra/LyraCarouselTheme.h b/src/components/themes/lyra/LyraCarouselTheme.h index efba4e2ac4..7236e1fd0c 100644 --- a/src/components/themes/lyra/LyraCarouselTheme.h +++ b/src/components/themes/lyra/LyraCarouselTheme.h @@ -53,6 +53,7 @@ class LyraCarouselTheme : public LyraTheme { void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, const std::function& buttonLabel, const std::function& rowIcon) const override; + void registerButtonMenuTouchTargets(const GfxRenderer& renderer, int buttonCount) const; // LyraTheme has no virtual overlay hook; this is a carousel-only helper. void drawButtonMenuSelectionOverlay(const GfxRenderer& renderer, int buttonCount, int selectedIndex, const std::function& buttonLabel, @@ -65,5 +66,6 @@ class LyraCarouselTheme : public LyraTheme { const std::function& rowIcon = {}, const std::function& rowValue = {}, bool highlightValue = false, const std::function& rowDimmed = {}, - const std::function& isHeader = {}) const override; + const std::function& isHeader = {}, int rowHeightScale = 1, + bool showSelection = true) const override; }; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 0907ccc998..3ae629c184 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -13,24 +13,13 @@ #include #include +#include "DeviceCapabilities.h" #include "RecentBooksStore.h" #include "activities/reader/BookReadingStats.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" -#include "components/icons/book.h" -#include "components/icons/book24.h" +#include "components/UiAppHelpers.h" #include "components/icons/chart.h" -#include "components/icons/cover.h" -#include "components/icons/file24.h" -#include "components/icons/folder.h" -#include "components/icons/folder24.h" -#include "components/icons/hotspot.h" -#include "components/icons/image24.h" -#include "components/icons/library.h" -#include "components/icons/recent.h" -#include "components/icons/settings2.h" -#include "components/icons/text24.h" -#include "components/icons/transfer.h" -#include "components/icons/wifi.h" #include "fontIds.h" // Internal constants @@ -69,42 +58,40 @@ int mainMenuIconYOffset(const UIIcon icon) { } // namespace -const uint8_t* LyraTheme::iconForName(UIIcon icon, uint32_t size) { +const freeink::Icon* LyraTheme::iconForName(UIIcon icon, uint32_t size) { if (size == 24) { switch (icon) { case UIIcon::Folder: - return Folder24Icon; + return &icon_folder_24; case UIIcon::Text: - return Text24Icon; + return &icon_file_text_24; case UIIcon::Image: - return Image24Icon; + return &icon_image_24; case UIIcon::Book: - return Book24Icon; + return &icon_book_marked_24; case UIIcon::File: - return File24Icon; + return &icon_file_24; default: return nullptr; } } else if (size == 32) { switch (icon) { case UIIcon::Folder: - return FolderIcon; + return &icon_folder_32; case UIIcon::Book: - return BookIcon; - case UIIcon::Chart: - return ChartIcon; + return &icon_book_marked_32; case UIIcon::Recent: - return RecentIcon; + return &icon_history_32; case UIIcon::Settings: - return Settings2Icon; + return &icon_lyra_settings_32; case UIIcon::Transfer: - return TransferIcon; + return &icon_lyra_transfer_32; case UIIcon::Library: - return LibraryIcon; + return &icon_lyra_library_32; case UIIcon::Wifi: - return WifiIcon; + return &icon_wifi_32; case UIIcon::Hotspot: - return HotspotIcon; + return &icon_radio_tower_32; default: return nullptr; } @@ -112,82 +99,6 @@ const uint8_t* LyraTheme::iconForName(UIIcon icon, uint32_t size) { return nullptr; } -void LyraTheme::fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t percentage, - const bool foregroundBlack) const { - const bool charging = gpio.isUsbConnected(); - - if (charging) { - // Solid fill when charging so lightning bolt is visible - renderer.fillRect(rect.x + 2, rect.y + 2, rect.width - 5, rect.height - 4, foregroundBlack); - drawBatteryLightningBolt(renderer, rect.x + 4, rect.y + 2, !foregroundBlack); - } else { - if (percentage > 10) { - renderer.fillRect(rect.x + 2, rect.y + 2, 3, rect.height - 4, foregroundBlack); - } - if (percentage > 40) { - renderer.fillRect(rect.x + 6, rect.y + 2, 3, rect.height - 4, foregroundBlack); - } - if (percentage > 70) { - renderer.fillRect(rect.x + 10, rect.y + 2, 3, rect.height - 4, foregroundBlack); - } - } -} - -void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle, - const bool readerContext) const { - renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false); - - const bool showBatteryPercentage = - SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; - // Position icon at right edge, drawBatteryRight will place text to the left - const int batteryX = rect.x + rect.width - 12 - LyraMetrics::values.batteryWidth; - const int batteryY = rect.y + homeHeaderTopInset; - drawBatteryRight(renderer, - Rect{batteryX, batteryY, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight}, - showBatteryPercentage); - - int maxTitleWidth = title != nullptr ? renderer.getTextWidth(UI_12_FONT_ID, title, EpdFontFamily::BOLD) : 0; - int maxSubtitleWidth = - subtitle != nullptr ? renderer.getTextWidth(SMALL_FONT_ID, subtitle, EpdFontFamily::REGULAR) : 0; - - // Available space is the distance between the side paddings, and a with side padding between title and subtitle. - const int availableSpace = rect.width - LyraMetrics::values.contentSidePadding * 3; - - if (maxTitleWidth + maxSubtitleWidth > availableSpace) { - if ((maxTitleWidth > availableSpace / 2) && (maxSubtitleWidth > availableSpace / 2)) { - // Both are wider then half the space, truncate both. - maxTitleWidth = availableSpace / 2; - maxSubtitleWidth = availableSpace / 2; - } else { - // Truncate the the longest one - if (maxTitleWidth > maxSubtitleWidth) { - maxTitleWidth = availableSpace - maxSubtitleWidth; - } else { - maxSubtitleWidth = availableSpace - maxTitleWidth; - } - } - } - - if (title) { - auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title, maxTitleWidth, EpdFontFamily::BOLD); - renderer.drawText(UI_12_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding, - rect.y + LyraMetrics::values.batteryBarHeight + 3, truncatedTitle.c_str(), true, - EpdFontFamily::BOLD); - renderer.drawLine(rect.x, rect.y + rect.height - 3, rect.x + rect.width - 1, rect.y + rect.height - 3, 3, true); - } - - if (subtitle) { - auto truncatedSubtitle = renderer.truncatedText(SMALL_FONT_ID, subtitle, maxSubtitleWidth, EpdFontFamily::REGULAR); - int truncatedSubtitleWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedSubtitle.c_str()); - renderer.drawText(SMALL_FONT_ID, - rect.x + rect.width - LyraMetrics::values.contentSidePadding - truncatedSubtitleWidth, - rect.y + 50, truncatedSubtitle.c_str(), true); - } - - drawTopStatusBarClock(renderer, rect.y, nullptr, readerContext, - title == nullptr && !readerContext ? homeHeaderClockTextYOffset(renderer) : 0); -} - void LyraTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const { int currentX = rect.x + LyraMetrics::values.contentSidePadding; int rightSpace = LyraMetrics::values.contentSidePadding; @@ -215,8 +126,14 @@ void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::ve renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray); } - for (const auto& tab : tabs) { + const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_10_FONT_ID)) / 2; + + for (size_t i = 0; i < tabs.size(); ++i) { + const auto& tab = tabs[i]; const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, tab.label, EpdFontFamily::REGULAR); + TouchRegistry::getInstance().add( + Rect{currentX, rect.y, textWidth + LyraMetrics::values.tabSpacing + 2 * hPaddingInSelection, rect.height}, + static_cast(i), TouchRegistry::Tab); if (tab.selected) { if (selected) { @@ -230,7 +147,7 @@ void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::ve } } - renderer.drawText(UI_10_FONT_ID, currentX + hPaddingInSelection, rect.y + 6, tab.label, !(tab.selected && selected), + renderer.drawText(UI_10_FONT_ID, currentX + hPaddingInSelection, textY, tab.label, !(tab.selected && selected), EpdFontFamily::REGULAR); currentX += textWidth + LyraMetrics::values.tabSpacing + 2 * hPaddingInSelection; @@ -239,15 +156,51 @@ void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::ve renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width - 1, rect.y + rect.height - 1, true); } +bool LyraTheme::tabIndexFromPoint(const GfxRenderer& renderer, const Rect rect, const std::vector& tabs, + const int x, const int y, int& index) const { + if (tabs.empty() || y < rect.y || y >= rect.y + rect.height) { + return false; + } + + int currentX = rect.x + LyraMetrics::values.contentSidePadding; + for (size_t i = 0; i < tabs.size(); i++) { + const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, tabs[i].label, EpdFontFamily::REGULAR); + const int tabWidth = textWidth + 2 * hPaddingInSelection; + const int left = (i == 0) ? rect.x : currentX - LyraMetrics::values.tabSpacing / 2; + const int right = currentX + tabWidth + LyraMetrics::values.tabSpacing / 2; + if (x >= left && x < right) { + index = static_cast(i); + return true; + } + currentX += tabWidth + LyraMetrics::values.tabSpacing; + } + + return false; +} + +int LyraTheme::getListRowStep(bool hasSubtitle, const int rowHeightScale) const { + const int rowScale = std::max(1, rowHeightScale); + int rowHeight = + ((hasSubtitle) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight) * rowScale; + return rowHeight; +} + +int LyraTheme::getListPageItems(int contentHeight, bool hasSubtitle, const int rowHeightScale) const { + const int rowStep = getListRowStep(hasSubtitle, rowHeightScale); + if (rowStep <= 0) return 1; + return std::max(1, contentHeight / rowStep); +} + void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, const std::function& rowSubtitle, const std::function& rowIcon, const std::function& rowValue, bool highlightValue, const std::function& rowDimmed, - const std::function& isHeader) const { + const std::function& isHeader, const int rowHeightScale, + const bool showSelection) const { drawListWithMetrics(renderer, rect, itemCount, selectedIndex, rowTitle, rowSubtitle, rowIcon, rowValue, - highlightValue, rowDimmed, isHeader, LyraMetrics::values, false); + highlightValue, rowDimmed, isHeader, LyraMetrics::values, false, rowHeightScale, showSelection); } void LyraTheme::drawListWithMetrics(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, @@ -257,8 +210,10 @@ void LyraTheme::drawListWithMetrics(const GfxRenderer& renderer, Rect rect, int const std::function& rowValue, bool highlightValue, const std::function& rowDimmed, const std::function& isHeader, const ThemeMetrics& metrics, - const bool invertSelectedRows) const { - int rowHeight = (rowSubtitle != nullptr) ? metrics.listWithSubtitleRowHeight : metrics.listRowHeight; + const bool invertSelectedRows, const int rowHeightScale, + const bool showSelection) const { + const int rowScale = std::max(1, rowHeightScale); + int rowHeight = ((rowSubtitle != nullptr) ? metrics.listWithSubtitleRowHeight : metrics.listRowHeight) * rowScale; if (itemCount <= 0) return; const auto isHeaderRow = [&isHeader](int index) { return isHeader != nullptr && isHeader(index); }; const int sectionHeaderTopPadding = halTiltSensor.isAvailable() ? 10 : 20; @@ -273,8 +228,7 @@ void LyraTheme::drawListWithMetrics(const GfxRenderer& renderer, Rect rect, int totalContentHeight += visualRowHeight(i); } const bool contentFits = totalContentHeight <= rect.height; - int pageItems = contentFits ? itemCount : rect.height / rowHeight; - if (pageItems <= 0) pageItems = 1; + int pageItems = contentFits ? itemCount : (rowHeight > 0 ? std::max(1, rect.height / rowHeight) : 1); const int totalPages = (itemCount + pageItems - 1) / pageItems; if (!contentFits && totalPages > 1) { @@ -292,7 +246,7 @@ void LyraTheme::drawListWithMetrics(const GfxRenderer& renderer, Rect rect, int // Draw selection (skip header rows) int contentWidth = rect.width - (totalPages > 1 ? (metrics.scrollBarWidth + metrics.scrollBarRightOffset) : 1); const auto pageStartIndex = selectedIndex / pageItems * pageItems; - if (selectedIndex >= 0 && !isHeaderRow(selectedIndex)) { + if (showSelection && selectedIndex >= 0 && !isHeaderRow(selectedIndex)) { int selY = rect.y; for (int j = pageStartIndex; j < selectedIndex; j++) { selY += visualRowHeight(j); @@ -320,7 +274,7 @@ void LyraTheme::drawListWithMetrics(const GfxRenderer& renderer, Rect rect, int const int itemY = currentY; const int currentRowHeight = visualRowHeight(i); currentY += currentRowHeight; - const bool selectedRow = i == selectedIndex; + const bool selectedRow = showSelection && i == selectedIndex; const bool foreground = !(invertSelectedRows && selectedRow); if (isHeaderRow(i)) { @@ -337,6 +291,7 @@ void LyraTheme::drawListWithMetrics(const GfxRenderer& renderer, Rect rect, int true); continue; } + TouchRegistry::getInstance().add(Rect{rect.x, itemY, contentWidth, currentRowHeight}, i, TouchRegistry::Item); int rowTextWidth = textWidth; @@ -367,15 +322,15 @@ void LyraTheme::drawListWithMetrics(const GfxRenderer& renderer, Rect rect, int if (rowIcon != nullptr) { UIIcon icon = rowIcon(i); - const uint8_t* iconBitmap = iconForName(icon, iconSize); + const freeink::Icon* iconBitmap = iconForName(icon, iconSize); if (iconBitmap != nullptr) { const int iconX = rect.x + metrics.contentSidePadding + hPaddingInSelection; const int iconY = rowSubtitle != nullptr ? itemY + 16 : centeredRowY(itemY, currentRowHeight, static_cast(iconSize)); if (invertSelectedRows && selectedRow) { - renderer.drawIconInverted(iconBitmap, iconX, iconY, iconSize, iconSize); + drawLucideIcon(renderer, *iconBitmap, iconX, iconY, false); } else { - renderer.drawIcon(iconBitmap, iconX, iconY, iconSize, iconSize); + drawLucideIcon(renderer, *iconBitmap, iconX, iconY); } } } @@ -404,6 +359,8 @@ void LyraTheme::drawListWithMetrics(const GfxRenderer& renderer, Rect rect, int void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, const char* btn4, const bool allowInvertedText) const { + if (gpio.hasTouch()) return; + const GfxRenderer::Orientation orig_orientation = renderer.getOrientation(); const bool invertText = allowInvertedText && orig_orientation == GfxRenderer::Orientation::PortraitInverted; renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -412,23 +369,28 @@ void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c constexpr int buttonWidth = 80; constexpr int smallButtonHeight = 15; constexpr int buttonHeight = LyraMetrics::values.buttonHintsHeight; - constexpr int buttonY = LyraMetrics::values.buttonHintsHeight; - constexpr int textYOffset = 7; // Distance from top of button to text baseline - // X3 has wider screen in portrait (528 vs 480), use more spacing - constexpr int x4ButtonPositions[] = {58, 146, 254, 342}; - constexpr int x3ButtonPositions[] = {65, 157, 291, 383}; - const int* buttonPositions = gpio.deviceIsX3() ? x3ButtonPositions : x4ButtonPositions; + constexpr int buttonY = LyraMetrics::values.buttonHintsHeight; // Distance from bottom + constexpr int textYOffset = 7; // Distance from top of button to text baseline + // Keyed to the portrait panel width: the 528-wide X3 gets more spacing than + // the 480-wide boards (X4, X4 Pro, and the other 800x480 panels). + constexpr int narrowButtonPositions[] = {58, 146, 254, 342}; + constexpr int wideButtonPositions[] = {65, 157, 291, 383}; + const int* buttonPositions = renderer.getScreenWidth() >= 528 ? wideButtonPositions : narrowButtonPositions; const char* labels[] = {btn1, btn2, btn3, btn4}; for (int i = 0; i < 4; i++) { const int x = buttonPositions[i]; if (labels[i] != nullptr && labels[i][0] != '\0') { + TouchRegistry::getInstance().add(Rect{x, pageHeight - buttonY, buttonWidth, buttonHeight}, i, + TouchRegistry::Button); // Draw the filled background and border for a FULL-sized button renderer.fillRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, cornerRadius, Color::White); renderer.drawRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, 1, cornerRadius, true, true, false, false, true); } else { - // Draw the filled background and border for a SMALL-sized button + // Clear the previous full-sized hint before drawing the inactive marker. + // Dictionary chaining can otherwise leave its old label visible. + renderer.fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false); const int smallButtonY = pageHeight - smallButtonHeight; renderer.fillRoundedRect(x, smallButtonY, buttonWidth, smallButtonHeight, cornerRadius, Color::White); renderer.drawRoundedRect(x, smallButtonY, buttonWidth, smallButtonHeight, 1, cornerRadius, true, true, false, @@ -452,13 +414,15 @@ void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c } void LyraTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const { + if (gpio.hasTouch()) return; + const int screenWidth = renderer.getScreenWidth(); constexpr int buttonWidth = LyraMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated) constexpr int buttonHeight = 78; // Height on screen (width when rotated) constexpr int buttonMargin = 0; - if (gpio.deviceIsX3()) { - // X3 layout: Up on left side, Down on right side, positioned higher + if (deviceHasEdgeSideButtons(gpio)) { + // Edge-button layout (X3, X4 Pro): Up on left side, Down on right side, positioned higher constexpr int x3ButtonY = 155; if (topBtn != nullptr && topBtn[0] != '\0') { @@ -519,6 +483,10 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // Draw cover image as background if available (inside the box) // Only load from SD on first render, then use stored buffer if (hasContinueReading) { + TouchRegistry::getInstance().add(Rect{LyraMetrics::values.contentSidePadding, tileY, tileWidth, + LyraMetrics::values.homeCoverHeight + 2 * hPaddingInSelection}, + 0, TouchRegistry::Cover); + RecentBook book = recentBooks[0]; if (!coverRendered) { std::string coverPath = book.coverBmpPath; @@ -553,7 +521,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: renderer.fillRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection + (LyraMetrics::values.homeCoverHeight / 3), coverWidth, 2 * LyraMetrics::values.homeCoverHeight / 3, true); - renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32); + drawLucideIcon(renderer, icon_book_open_32, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24); } coverBufferStored = storeCoverBuffer(); @@ -676,6 +644,8 @@ void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount Rect tileRect = Rect{rect.x + menuMetrics.contentSidePadding, rect.y + displayIndex * (menuMetrics.menuRowHeight + menuMetrics.menuSpacing), tileWidth, menuMetrics.menuRowHeight}; + TouchRegistry::getInstance().add( + buttonMenuTouchTarget(tileRect, rect, i == buttonCount - 1, menuMetrics.menuSpacing), i, TouchRegistry::Item); const bool selected = selectedIndex == i; @@ -706,11 +676,13 @@ void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount iconY + ribbonHeight}; renderer.fillPolygon(polyX, polyY, 5, true); textX += mainMenuIconSize + hPaddingInSelection + 2; + } else if (icon == UIIcon::Chart) { + renderer.drawIcon(ChartIcon, textX, textY + 3 + mainMenuIconYOffset(icon), mainMenuIconSize, mainMenuIconSize); + textX += mainMenuIconSize + hPaddingInSelection + 2; } else { - const uint8_t* iconBitmap = iconForName(icon, mainMenuIconSize); + const freeink::Icon* iconBitmap = iconForName(icon, mainMenuIconSize); if (iconBitmap != nullptr) { - renderer.drawIcon(iconBitmap, textX, textY + 3 + mainMenuIconYOffset(icon), mainMenuIconSize, - mainMenuIconSize); + drawLucideIcon(renderer, *iconBitmap, textX, textY + 3 + mainMenuIconYOffset(icon)); textX += mainMenuIconSize + hPaddingInSelection + 2; } } diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index f0b790c275..d68f5d2931 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include "components/themes/BaseTheme.h" @@ -19,10 +21,24 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .contentSidePadding = 20, .listRowHeight = 36, .listWithSubtitleRowHeight = 60, + .listRowGap = 0, + .listRowRadius = 6, + .listInset = 20, + .listSidePadding = 8, + .listSelectionStyle = 1, + .listScrollWidth = 4, + .listScrollSide = 0, + .listTitleBold = false, + .headerSidePadding = 18, + .headerUnderlineSize = 3, + .headerTitleAlign = 0, + .headerBatterySide = 0, + .headerBatteryDetached = true, .menuRowHeight = 56, .menuSpacing = 6, .tabSpacing = 8, .tabBarHeight = 40, + .tabBarAppearance = ThemeTabBarAppearance::Pill, .scrollBarWidth = 4, .scrollBarRightOffset = 5, .homeTopPadding = 56, @@ -37,23 +53,12 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .progressBarMarginTop = 1, .statusBarHorizontalMargin = 5, .statusBarVerticalMargin = 19, - .keyboardKeyWidth = 31, - .keyboardKeyHeight = 40, + .keyboardKeyHeight = 48, .keyboardKeySpacing = 0, - .keyboardBottomKeyHeight = 35, - .keyboardBottomKeySpacing = 5, - .keyboardBottomAligned = true, .keyboardCenteredText = false, .keyboardVerticalOffset = -7, .keyboardTextFieldWidthPercent = 85, - .keyboardWidthPercent = 90, - .keyboardKeyCornerRadius = 6, - .keyboardFillUnselected = false, - .keyboardOutlineAllUnselected = false, - .keyboardDrawSpecialOutlineWhenUnselected = true, - .keyboardSecondaryLabelRightPadding = 1, - .keyboardSecondaryLabelTopPadding = 0, - .keyboardMinArrowHeadSize = 0, + .keyboardWidthPercent = 94, .popupTopOffsetRatio = 0.165f, .popupMarginX = 16, .popupMarginY = 12, @@ -88,20 +93,21 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, class LyraTheme : public BaseTheme { public: // Component drawing methods - void fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t percentage, - bool foregroundBlack = true) const override; - void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle = nullptr, - bool readerContext = false) const override; void drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel = nullptr) const override; void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, bool selected) const override; + bool tabIndexFromPoint(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, int x, int y, + int& index) const override; + int getListRowStep(bool hasSubtitle, int rowHeightScale = 1) const override; + int getListPageItems(int contentHeight, bool hasSubtitle, int rowHeightScale = 1) const override; void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, const std::function& rowSubtitle, const std::function& rowIcon, const std::function& rowValue, bool highlightValue, const std::function& rowDimmed = nullptr, - const std::function& isHeader = nullptr) const override; + const std::function& isHeader = nullptr, int rowHeightScale = 1, + bool showSelection = true) const override; void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, const char* btn4, bool allowInvertedText = false) const override; void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const override; @@ -124,8 +130,8 @@ class LyraTheme : public BaseTheme { const std::function& rowValue, bool highlightValue, const std::function& rowDimmed, const std::function& isHeader, const ThemeMetrics& metrics, - bool invertSelectedRows) const; + bool invertSelectedRows, int rowHeightScale = 1, bool showSelection = true) const; // Returns nullptr when the icon or requested bitmap size is not available. - static const uint8_t* iconForName(UIIcon icon, uint32_t size); + static const freeink::Icon* iconForName(UIIcon icon, uint32_t size); }; diff --git a/src/components/themes/minimal/MinimalTheme.cpp b/src/components/themes/minimal/MinimalTheme.cpp index fd53f3ef07..979df7f162 100644 --- a/src/components/themes/minimal/MinimalTheme.cpp +++ b/src/components/themes/minimal/MinimalTheme.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -21,10 +22,13 @@ #include "activities/reader/BookReadingStats.h" #include "activities/reader/GlobalReadingStats.h" #include "activities/reader/ReadingStatsUtils.h" +#include "components/TouchRegistry.h" +#include "components/UIScale.h" #include "components/UITheme.h" +#include "components/UIThemeTokens.h" +#include "components/UiAppHelpers.h" #include "components/icons/afternoon.h" #include "components/icons/book24.h" -#include "components/icons/cover.h" #include "components/icons/evening.h" #include "components/icons/morning.h" #include "components/icons/night.h" @@ -37,6 +41,16 @@ struct MinimalQuote { const char* author; }; +bool tabSlotIndexFromPoint(const Rect rect, const int tabCount, const int x, const int y, int& index) { + if (tabCount <= 0 || rect.width <= 0 || y < rect.y || y >= rect.y + rect.height || x < rect.x || + x >= rect.x + rect.width) { + return false; + } + + index = std::min(tabCount - 1, ((x - rect.x) * tabCount) / rect.width); + return true; +} + constexpr MinimalQuote kQuotes[] = { {"\"A book is a dream you hold in your hands.\"", "Neil Gaiman"}, {"\"I have always imagined that Paradise will be a kind of library.\"", "Jorge Luis Borges"}, @@ -318,8 +332,8 @@ void drawMissingBookCover(const GfxRenderer& renderer, const Rect& coverRect, co renderer.drawLine(placeholderRect.x, dividerY, placeholderRect.x + placeholderRect.width - 1, dividerY, true); constexpr int iconSize = 32; - renderer.drawIcon(CoverIcon, placeholderRect.x + (placeholderRect.width - iconSize) / 2, - placeholderRect.y + (placeholderRect.height / 3 - iconSize) / 2, iconSize, iconSize); + drawLucideIcon(renderer, icon_book_open_32, placeholderRect.x + (placeholderRect.width - iconSize) / 2, + placeholderRect.y + (placeholderRect.height / 3 - iconSize) / 2); constexpr int textPadding = 16; constexpr int textVerticalPadding = 22; @@ -436,6 +450,7 @@ void MinimalTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std: const int nextSlotX = rect.x + ((i + 1) * rect.width) / tabCount; const int slotWidth = nextSlotX - slotX; const auto& tab = tabs[i]; + TouchRegistry::getInstance().add(Rect{slotX, rect.y, slotWidth, rect.height}, i, TouchRegistry::Tab); const auto fontStyle = tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR; const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, tab.label, fontStyle); const int textX = slotX + (slotWidth - textWidth) / 2; @@ -444,6 +459,12 @@ void MinimalTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std: } } +bool MinimalTheme::tabIndexFromPoint(const GfxRenderer& renderer, const Rect rect, const std::vector& tabs, + const int x, const int y, int& index) const { + (void)renderer; + return tabSlotIndexFromPoint(rect, static_cast(tabs.size()), x, y, index); +} + int MinimalTheme::compactFileBrowserRowHeightFor(const GfxRenderer& renderer) { const int textHeight = renderer.getLineHeight(UI_10_FONT_ID) * 2 + kFileBrowserRowVerticalPadding; return std::max(kFileBrowserIconSize + kFileBrowserRowVerticalPadding, textHeight); @@ -459,16 +480,17 @@ void MinimalTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCoun const std::function& rowIcon, const std::function& rowValue, bool highlightValue, const std::function& rowDimmed, - const std::function& isHeader) const { + const std::function& isHeader, const int rowHeightScale, + const bool showSelection) const { const bool compactFileRows = rowSubtitle != nullptr && rowIcon != nullptr && rowValue != nullptr; if (!compactFileRows) { LyraTheme::drawList(renderer, rect, itemCount, selectedIndex, rowTitle, rowSubtitle, rowIcon, rowValue, - highlightValue, rowDimmed, isHeader); + highlightValue, rowDimmed, isHeader, rowHeightScale, showSelection); return; } drawCompactFileBrowserList(renderer, rect, itemCount, selectedIndex, rowTitle, rowSubtitle, rowIcon, rowValue, - rowDimmed); + rowDimmed, showSelection); } void MinimalTheme::drawCompactFileBrowserList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, @@ -476,14 +498,17 @@ void MinimalTheme::drawCompactFileBrowserList(const GfxRenderer& renderer, Rect const std::function& rowSubtitle, const std::function& rowIcon, const std::function& rowValue, - const std::function& rowDimmed) { + const std::function& rowDimmed, const bool showSelection, + const bool uniformRowHeight, const bool allowTwoLineTitles) { if (itemCount <= 0) return; const int fileRowHeight = compactFileBrowserRowHeightFor(renderer); const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); const int folderRowHeight = MinimalMetrics::values.listRowHeight; const auto isFolderRow = [&](int index) { return rowSubtitle(index) == "folder"; }; - const auto rowHeightFor = [&](int index) { return isFolderRow(index) ? folderRowHeight : fileRowHeight; }; + const auto rowHeightFor = [&](int index) { + return !uniformRowHeight && isFolderRow(index) ? folderRowHeight : fileRowHeight; + }; const auto pageEndFor = [&](int startIndex) { int usedHeight = 0; int endIndex = startIndex; @@ -527,7 +552,7 @@ void MinimalTheme::drawCompactFileBrowserList(const GfxRenderer& renderer, Rect MinimalMetrics::values.scrollBarWidth, scrollBarHeight, true); } - if (selectedIndex >= 0) { + if (showSelection && selectedIndex >= 0) { int selectedY = rect.y; for (int i = pageStartIndex; i < selectedIndex; i++) { selectedY += rowHeightFor(i); @@ -545,7 +570,8 @@ void MinimalTheme::drawCompactFileBrowserList(const GfxRenderer& renderer, Rect for (int i = pageStartIndex; i < itemCount && i < pageEndIndex; i++) { const int rowHeight = rowHeightFor(i); const bool folderRow = isFolderRow(i); - const bool selectedRow = i == selectedIndex; + const bool selectedRow = showSelection && i == selectedIndex; + TouchRegistry::getInstance().add(Rect{rect.x, itemY, contentWidth, rowHeight}, i, TouchRegistry::Item); std::string valueText = rowValue(i); if (!valueText.empty()) { @@ -556,13 +582,13 @@ void MinimalTheme::drawCompactFileBrowserList(const GfxRenderer& renderer, Rect const int textWidth = std::max(1, contentWidth - textX - MinimalMetrics::values.contentSidePadding - valueWidth - valueGap); - const uint8_t* iconBitmap = iconForName(rowIcon(i), kFileBrowserIconSize); + const freeink::Icon* iconBitmap = iconForName(rowIcon(i), kFileBrowserIconSize); if (iconBitmap != nullptr) { const int iconY = centeredRowY(itemY, rowHeight, kFileBrowserIconSize); - renderer.drawIcon(iconBitmap, iconX, iconY, kFileBrowserIconSize, kFileBrowserIconSize); + drawLucideIcon(renderer, *iconBitmap, iconX, iconY); } - const int maxTitleLines = folderRow ? 1 : 2; + const int maxTitleLines = folderRow || !allowTwoLineTitles ? 1 : 2; auto lines = renderer.wrappedText(UI_10_FONT_ID, rowTitle(i).c_str(), textWidth, maxTitleLines); const int textBlockHeight = static_cast(lines.size()) * lineHeight; int textY = centeredRowY(itemY, rowHeight, textBlockHeight); @@ -591,8 +617,24 @@ void MinimalTheme::drawCompactFileBrowserList(const GfxRenderer& renderer, Rect void MinimalTheme::setHomeButtonHintSelection(const int selectedIndex) { homeButtonHintSelection = selectedIndex; } +Rect MinimalTheme::buttonMenuPanelRect(const GfxRenderer& renderer, const int buttonCount) { + if (buttonCount <= 0) { + return Rect{0, 0, 0, 0}; + } + + const int panelW = std::min(kMenuPanelWidth, renderer.getScreenWidth() - 80); + const int panelH = buttonCount * kMenuRowHeight + 2; + const int panelX = (renderer.getScreenWidth() - panelW) / 2; + return Rect{panelX, kMenuPanelTop, panelW, panelH}; +} + void MinimalTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, const char* btn4, const bool allowInvertedText) const { + if (gpio.hasTouch()) { + homeButtonHintSelection = -1; + return; + } + const GfxRenderer::Orientation origOrientation = renderer.getOrientation(); const bool invertText = allowInvertedText && origOrientation == GfxRenderer::Orientation::PortraitInverted; renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -615,11 +657,15 @@ void MinimalTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, cons const int x = buttonPositions[i]; const bool hasLabel = labels[i] != nullptr && labels[i][0] != '\0'; if (hasLabel) { + TouchRegistry::getInstance().add(Rect{x, pageHeight - buttonY, buttonWidth, buttonHeight}, i, + TouchRegistry::Button); const Color background = i == selectedIndex ? Color::LightGray : Color::White; renderer.fillRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, kButtonCornerRadius, background); renderer.drawRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, 1, kButtonCornerRadius, true, true, false, false, true); } else { + // Clear the previous full-sized hint before drawing the inactive marker. + renderer.fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false); const int smallButtonY = pageHeight - smallButtonHeight; renderer.fillRoundedRect(x, smallButtonY, buttonWidth, smallButtonHeight, kButtonCornerRadius, Color::White); renderer.drawRoundedRect(x, smallButtonY, buttonWidth, smallButtonHeight, 1, kButtonCornerRadius, true, true, @@ -680,6 +726,7 @@ void MinimalTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const s coverBufferStored = storeCoverBuffer(); coverRendered = coverBufferStored; } + TouchRegistry::getInstance().add(coverRect, 0, TouchRegistry::Cover); drawProgressBlock(renderer, coverRect, stats, progressPercent, false); } @@ -717,14 +764,21 @@ void MinimalTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCo return; } - const int panelW = std::min(kMenuPanelWidth, renderer.getScreenWidth() - 80); - const int panelH = buttonCount * kMenuRowHeight + 2; - const int panelX = (renderer.getScreenWidth() - panelW) / 2; - const int panelY = kMenuPanelTop; + const Rect panel = buttonMenuPanelRect(renderer, buttonCount); + const int panelX = panel.x; + const int panelY = panel.y; + const int panelW = panel.width; + const int panelH = panel.height; + const auto scale = uiScaleSpec(); + freeink::ui::GfxRendererFrame<1> ui(renderer, scale.smallFontId, scale.bodyFontId, scale.titleFontId); + freeink::ui::TextStyle labelText = uiThemeTokens(ui.target).bodyText; + labelText.align = freeink::ui::TextAlign::Center; + labelText.maxLines = 1; renderer.drawRoundedRect(panelX, panelY, panelW, panelH, 1, kMenuPanelRadius, true); for (int i = 0; i < buttonCount; ++i) { const int rowY = panelY + 1 + i * kMenuRowHeight; + TouchRegistry::getInstance().add(Rect{panelX, rowY, panelW, kMenuRowHeight}, i, TouchRegistry::Item); if (i == selectedIndex) { const int triangleX = panelX + kMenuSelectionTriangleInset; const int triangleCenterY = rowY + kMenuRowHeight / 2; @@ -737,8 +791,8 @@ void MinimalTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCo const char* label = buttonLabel != nullptr ? buttonLabel(i) : ""; if (!label) label = ""; - const int labelW = renderer.getTextWidth(UI_12_FONT_ID, label); - const int labelY = rowY + (kMenuRowHeight - renderer.getLineHeight(UI_12_FONT_ID)) / 2; - renderer.drawText(UI_12_FONT_ID, panelX + (panelW - labelW) / 2, labelY, label); + ui.target.text(freeink::ui::Rect{static_cast(panelX), static_cast(rowY), + static_cast(panelW), static_cast(kMenuRowHeight)}, + label, labelText); } } diff --git a/src/components/themes/minimal/MinimalTheme.h b/src/components/themes/minimal/MinimalTheme.h index dda1ae2390..76a84fa35b 100644 --- a/src/components/themes/minimal/MinimalTheme.h +++ b/src/components/themes/minimal/MinimalTheme.h @@ -17,6 +17,7 @@ constexpr ThemeMetrics makeValues() { v.homeRecentBooksCount = 1; v.homeContinueReadingInMenu = false; v.homeMenuTopOffset = 0; + v.tabBarAppearance = ThemeTabBarAppearance::BorderedText; return v; } @@ -31,24 +32,30 @@ struct GlobalReadingStats; class MinimalTheme : public LyraTheme { public: static void setHomeButtonHintSelection(int selectedIndex); + static Rect buttonMenuPanelRect(const GfxRenderer& renderer, int buttonCount); static int compactFileBrowserRowHeightFor(const GfxRenderer& renderer); static void drawCompactFileBrowserList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, const std::function& rowSubtitle, const std::function& rowIcon, const std::function& rowValue, - const std::function& rowDimmed = nullptr); + const std::function& rowDimmed = nullptr, + bool showSelection = true, bool uniformRowHeight = false, + bool allowTwoLineTitles = true); void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle = nullptr, bool readerContext = false) const override; void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, bool selected) const override; + bool tabIndexFromPoint(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, int x, int y, + int& index) const override; void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, const std::function& rowSubtitle, const std::function& rowIcon, const std::function& rowValue, bool highlightValue, const std::function& rowDimmed = nullptr, - const std::function& isHeader = nullptr) const override; + const std::function& isHeader = nullptr, int rowHeightScale = 1, + bool showSelection = true) const override; void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, const char* btn4, bool allowInvertedText = false) const override; void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, diff --git a/src/components/themes/roundedraff/RoundedRaffTheme.cpp b/src/components/themes/roundedraff/RoundedRaffTheme.cpp index 25320764f4..a9027a817b 100644 --- a/src/components/themes/roundedraff/RoundedRaffTheme.cpp +++ b/src/components/themes/roundedraff/RoundedRaffTheme.cpp @@ -1,6 +1,7 @@ #include "RoundedRaffTheme.h" #include +#include #include #include #include @@ -11,8 +12,9 @@ #include #include "RecentBooksStore.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" -#include "components/icons/cover.h" +#include "components/UiAppHelpers.h" #include "fontIds.h" namespace { @@ -28,10 +30,6 @@ constexpr int kTabHorizontalInset = 2; constexpr int kTitleFontId = UI_12_FONT_ID; // Requested main title size: 12px constexpr int kSubtitleFontId = SMALL_FONT_ID; // Requested subtitle size: 8px constexpr int kGuideFontId = SMALL_FONT_ID; // Closest available to requested 6px -constexpr int kHeaderTitleYOffset = 14; -constexpr int kHomeHeaderTitleYOffset = kHeaderTitleYOffset + 7; -constexpr int kHeaderBatteryYOffset = 5; -constexpr int kHomeHeaderBatteryYOffset = 23; constexpr int kHeaderClockYOffset = 3; void drawScrollBar(const GfxRenderer& renderer, Rect rect, int itemCount, int pageStartIndex, int pageItems) { @@ -58,44 +56,13 @@ int coverWidth = 0; void RoundedRaffTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle, const bool readerContext) const { - (void)subtitle; // Home screen header is custom-rendered in drawRecentBookCover. if (title == nullptr) { - const int clockYOffset = readerContext ? 0 : homeHeaderClockTextYOffset(renderer) + kHeaderClockYOffset; + const int clockYOffset = readerContext ? 0 : kHeaderClockYOffset; drawTopStatusBarClock(renderer, rect.y, nullptr, readerContext, clockYOffset); return; } - const int sidePadding = RoundedRaffMetrics::values.contentSidePadding; - const int titleX = rect.x + sidePadding; - const bool isHomeHeader = rect.height == RoundedRaffMetrics::values.homeTopPadding; - const int titleY = rect.y + (isHomeHeader ? kHomeHeaderTitleYOffset : kHeaderTitleYOffset); - - const bool showBatteryPercentage = - SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; - const int batteryIconX = rect.x + rect.width - sidePadding - RoundedRaffMetrics::values.batteryWidth; - const int batteryYOffset = isHomeHeader ? kHomeHeaderBatteryYOffset : kHeaderBatteryYOffset; - const int batteryY = rect.y + homeHeaderTopInset + batteryYOffset; - - // Reserve space for the widest possible percentage text to avoid title/battery overlap - int batteryGroupLeftX = batteryIconX; - if (showBatteryPercentage) { - // Clear a fixed-width area for the battery percentage to avoid ghosting when digit count changes (e.g. 100% -> 99%) - const int maxTextWidth = renderer.getTextWidth(SMALL_FONT_ID, "100%"); - batteryGroupLeftX -= maxTextWidth + batteryPercentSpacing; - - const int clearW = maxTextWidth + batteryPercentSpacing + RoundedRaffMetrics::values.batteryWidth; - const int clearH = std::max(renderer.getTextHeight(SMALL_FONT_ID), RoundedRaffMetrics::values.batteryHeight + 8); - renderer.fillRect(batteryIconX - maxTextWidth - batteryPercentSpacing, batteryY, clearW, clearH, false); - } - - const int maxTitleWidth = std::max(0, batteryGroupLeftX - 20 - titleX); - auto headerTitle = renderer.truncatedText(kTitleFontId, title, maxTitleWidth, EpdFontFamily::BOLD); - renderer.drawText(kTitleFontId, titleX, titleY, headerTitle.c_str(), true, EpdFontFamily::BOLD); - drawBatteryRight( - renderer, - Rect{batteryIconX, batteryY, RoundedRaffMetrics::values.batteryWidth, RoundedRaffMetrics::values.batteryHeight}, - showBatteryPercentage); - drawTopStatusBarClock(renderer, rect.y, nullptr, readerContext, readerContext ? 0 : kHeaderClockYOffset); + BaseTheme::drawHeader(renderer, rect, title, subtitle, readerContext); } void RoundedRaffTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, @@ -116,6 +83,7 @@ void RoundedRaffTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const const int tabX = slotX + tabInsetX; const int tabWidth = std::max(0, slotWidth - tabInsetX * 2); const auto& tab = tabs[i]; + TouchRegistry::getInstance().add(Rect{slotX, rect.y, slotWidth, rect.height}, i, TouchRegistry::Tab); if (tab.selected) { renderer.fillRoundedRect(tabX, tabY, tabWidth, tabHeight, 18, selected ? Color::Black : Color::DarkGray); @@ -131,6 +99,18 @@ void RoundedRaffTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width - 1, rect.y + rect.height - 1, true); } +bool RoundedRaffTheme::tabIndexFromPoint(const GfxRenderer& renderer, const Rect rect, const std::vector& tabs, + const int x, const int y, int& index) const { + (void)renderer; + if (tabs.empty() || y < rect.y || y >= rect.y + rect.height || x < rect.x || x >= rect.x + rect.width) { + return false; + } + + const int slotWidth = std::max(1, rect.width / static_cast(tabs.size())); + index = std::min(static_cast(tabs.size()) - 1, (x - rect.x) / slotWidth); + return true; +} + void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, const std::function& storeCoverBuffer, @@ -152,11 +132,11 @@ void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con } const int imgY = tileY + (tileHeight - RoundedRaffMetrics::values.homeCoverHeight) / 2; const int tileX = RoundedRaffMetrics::values.contentSidePadding; - // Draw book card regardless, fill with message based on `hasContinueReading` // Draw cover image as background if available (inside the box) // Only load from SD on first render, then use stored buffer if (hasContinueReading) { + TouchRegistry::getInstance().add(Rect{tileX, tileY, tileWidth, tileHeight}, 0, TouchRegistry::Cover); RecentBook book = recentBooks[0]; if (!coverRendered) { std::string coverPath = book.coverBmpPath; @@ -193,7 +173,7 @@ void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con // Render empty cover renderer.fillRect(tileX + (tileWidth - coverWidth) / 2, imgY + (RoundedRaffMetrics::values.homeCoverHeight / 3), coverWidth, 2 * RoundedRaffMetrics::values.homeCoverHeight / 3, true); - renderer.drawIcon(CoverIcon, tileX + (tileWidth - coverWidth) / 2 + 24, imgY + 24, 32); + drawLucideIcon(renderer, icon_book_open_32, tileX + (tileWidth - coverWidth) / 2 + 24, imgY + 24); renderer.maskRoundedRectOutsideCorners(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth, RoundedRaffMetrics::values.homeCoverHeight, kCoverRadius, Color::LightGray); @@ -219,13 +199,17 @@ void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con } } +int RoundedRaffTheme::getMenuRowHeight(const GfxRenderer& renderer) const { + return renderer.getLineHeight(kTitleFontId) + 15; +} + void RoundedRaffTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, const std::function& buttonLabel, const std::function& rowIcon) const { (void)rowIcon; const int sidePadding = kHomeMenuSidePadding; const int rowX = rect.x + sidePadding; - const int rowHeight = renderer.getLineHeight(kTitleFontId) + 15; // 10px top + 10px bottom + const int rowHeight = getMenuRowHeight(renderer); const int rowGap = kSelectableRowGap; const int rowStep = rowHeight + rowGap; const int pageItems = std::max(1, rect.height / rowStep); @@ -245,6 +229,9 @@ void RoundedRaffTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int butt const int rowWidth = std::min( menuMaxWidth, renderer.getTextWidth(kTitleFontId, truncatedLabel.c_str(), EpdFontFamily::BOLD) + kRowPaddingX); const bool isSelected = selectedIndex == i; + const Rect rowRect{rowX, rowY, menuMaxWidth, rowHeight}; + TouchRegistry::getInstance().add(buttonMenuTouchTarget(rowRect, rect, i == buttonCount - 1, rowGap), i, + TouchRegistry::Item); renderer.fillRoundedRect(rowX, rowY, rowWidth, rowHeight, kMenuRadius, isSelected ? Color::Black : Color::White); const int textY = rowY + (rowHeight - textLineHeight) / 2; const int textX = rowX + kInteractiveInsetX; @@ -276,61 +263,16 @@ void RoundedRaffTheme::drawTextField(const GfxRenderer& renderer, Rect rect, con renderer.drawLine(lineStart, lineY, lineStart + lineW - 1, lineY, thickness, true); } -void RoundedRaffTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, - const char* secondaryLabel, const KeyboardKeyType keyType, - const bool inactiveSelection) const { - constexpr int keyRadius = 6; - const bool disabled = keyType == KeyboardKeyType::Disabled; - const bool invert = isSelected && !inactiveSelection; - const bool hasSecondary = secondaryLabel != nullptr && secondaryLabel[0] != '\0'; - - if (isSelected) { - const Color fillColor = (inactiveSelection || disabled) ? Color::LightGray : Color::Black; - renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, fillColor); - } else { - if (disabled) { - renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, Color::LightGray); - } else { - renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, Color::White); - } - renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, keyRadius, true); - } - - if (keyType == KeyboardKeyType::Space) { - const int lineHalfWidth = rect.width * 3 / 10; - const int centerX = rect.x + rect.width / 2; - const int lineY = rect.y + rect.height / 2 + 3; - renderer.drawLine(centerX - lineHalfWidth, lineY, centerX + lineHalfWidth, lineY, 3, !invert); - return; - } - - if (keyType == KeyboardKeyType::Del) { - const int centerX = rect.x + rect.width / 2; - const int centerY = rect.y + rect.height / 2; - const int arrowLen = rect.width / 4; - const int arrowHead = std::max(1, arrowLen / 2); - renderer.drawLine(centerX - arrowLen / 2, centerY, centerX + arrowLen / 2, centerY, 3, !invert); - renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY - arrowHead, 3, - !invert); - renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY + arrowHead, 3, - !invert); - return; - } - - if (label != nullptr && label[0] != '\0') { - const int primaryFontId = hasSecondary ? UI_10_FONT_ID : UI_12_FONT_ID; - const int itemWidth = renderer.getTextWidth(primaryFontId, label); - const int centeredTextX = rect.x + (rect.width - itemWidth) / 2; - const int textX = hasSecondary ? std::max(rect.x + 4, centeredTextX - 6) : centeredTextX; - const int lineHeight = renderer.getLineHeight(primaryFontId); - const int textY = hasSecondary ? rect.y + rect.height - lineHeight - 2 : rect.y + (rect.height - lineHeight) / 2; - renderer.drawText(primaryFontId, textX, textY, label, !invert); - } +int RoundedRaffTheme::getListRowStep(bool hasSubtitle, const int rowHeightScale) const { + const int rowScale = std::max(1, rowHeightScale); + const int rowHeight = + (hasSubtitle ? RoundedRaffMetrics::values.listWithSubtitleRowHeight : RoundedRaffMetrics::values.listRowHeight) * + rowScale; + return rowHeight + kSelectableRowGap; +} - if (hasSecondary) { - const int secWidth = renderer.getTextWidth(SMALL_FONT_ID, secondaryLabel); - renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - secWidth - 3, rect.y + 2, secondaryLabel, !invert); - } +int RoundedRaffTheme::getListPageItems(int contentHeight, bool hasSubtitle, const int rowHeightScale) const { + return std::max(1, contentHeight / getListRowStep(hasSubtitle, rowHeightScale)); } void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, @@ -339,10 +281,12 @@ void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int item const std::function& rowIcon, const std::function& rowValue, bool highlightValue, const std::function& rowDimmed, - const std::function& isHeader) const { + const std::function& isHeader, const int rowHeightScale, + const bool showSelection) const { (void)rowIcon; (void)highlightValue; (void)rowDimmed; + const int rowScale = std::max(1, rowHeightScale); const bool hasSubtitle = static_cast(rowSubtitle); const int titleLineHeight = renderer.getLineHeight(kTitleFontId); const int subtitleLineHeight = renderer.getLineHeight(kSubtitleFontId); @@ -351,7 +295,7 @@ void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int item constexpr int subtitleInterLineGap = 4; const int subtitleRowHeight = subtitleTopPadding + titleLineHeight + subtitleInterLineGap + subtitleLineHeight + subtitleBottomPadding; - const int rowHeight = hasSubtitle ? subtitleRowHeight : RoundedRaffMetrics::values.listRowHeight; + const int rowHeight = (hasSubtitle ? subtitleRowHeight : RoundedRaffMetrics::values.listRowHeight) * rowScale; const auto isHeaderRow = [&isHeader](int index) { return isHeader != nullptr && isHeader(index); }; bool hasHeaderRows = false; for (int i = 0; i < itemCount; ++i) { @@ -388,7 +332,7 @@ void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int item rowY += visualRowHeight(j) + selectableRowGap; if (isHeaderRow(j + 1)) rowY += sectionHeaderTopPadding; } - const bool isSelected = i == selectedIndex; + const bool isSelected = showSelection && i == selectedIndex; const int currentRowHeight = visualRowHeight(i); if (isHeaderRow(i)) { @@ -403,6 +347,7 @@ void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int item renderer.drawLine(rowX, rowY + currentRowHeight - 1, rowX + rowWidth, rowY + currentRowHeight - 1, true); continue; } + TouchRegistry::getInstance().add(Rect{rowX, rowY, rowWidth, currentRowHeight}, i, TouchRegistry::Item); renderer.fillRoundedRect(rowX, rowY, rowWidth, currentRowHeight, kRowRadius, isSelected ? Color::Black : Color::White); @@ -458,6 +403,8 @@ void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int item void RoundedRaffTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, const char* btn4, const bool allowInvertedText) const { + if (gpio.hasTouch()) return; + const GfxRenderer::Orientation origOrientation = renderer.getOrientation(); const bool invertText = allowInvertedText && origOrientation == GfxRenderer::Orientation::PortraitInverted; renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -479,11 +426,29 @@ void RoundedRaffTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const bool backDisabled = (leftOuterLabel == nullptr || leftOuterLabel[0] == '\0'); const int leftGroupX = sidePadding; const int rightGroupX = leftGroupX + groupWidth + groupGap; + if (!backDisabled) { + TouchRegistry::getInstance().add(Rect{leftGroupX, outlineY, groupWidth / 2, hintHeight}, HalGPIO::BTN_BACK, + TouchRegistry::Button); + } const std::string backLabel = backDisabled ? "" : std::string(leftOuterLabel); // Callers should provide the button labels. If a label is not specified, it should render empty. const std::string selectText = (leftInnerLabel && leftInnerLabel[0] != '\0') ? std::string(leftInnerLabel) : ""; const std::string upText = (rightInnerLabel && rightInnerLabel[0] != '\0') ? std::string(rightInnerLabel) : ""; const std::string downText = (rightOuterLabel && rightOuterLabel[0] != '\0') ? std::string(rightOuterLabel) : ""; + if (!selectText.empty()) { + TouchRegistry::getInstance().add( + Rect{leftGroupX + groupWidth / 2, outlineY, groupWidth - groupWidth / 2, hintHeight}, HalGPIO::BTN_CONFIRM, + TouchRegistry::Button); + } + if (!upText.empty()) { + TouchRegistry::getInstance().add(Rect{rightGroupX, outlineY, groupWidth / 2, hintHeight}, HalGPIO::BTN_LEFT, + TouchRegistry::Button); + } + if (!downText.empty()) { + TouchRegistry::getInstance().add( + Rect{rightGroupX + groupWidth / 2, outlineY, groupWidth - groupWidth / 2, hintHeight}, HalGPIO::BTN_RIGHT, + TouchRegistry::Button); + } // Ensure button hints always "win" visually even if other elements accidentally render into this area. renderer.fillRect(leftGroupX, outlineY, groupWidth, hintHeight, false); diff --git a/src/components/themes/roundedraff/RoundedRaffTheme.h b/src/components/themes/roundedraff/RoundedRaffTheme.h index 8691b14383..3319c6cb67 100644 --- a/src/components/themes/roundedraff/RoundedRaffTheme.h +++ b/src/components/themes/roundedraff/RoundedRaffTheme.h @@ -8,21 +8,35 @@ namespace RoundedRaffMetrics { constexpr ThemeMetrics values = {.batteryWidth = 15, .batteryHeight = 12, .topPadding = 0, - .batteryBarHeight = 20, - .headerHeight = 45, + .batteryBarHeight = 45, + .headerHeight = 70, .verticalSpacing = 10, .previewPadding = 12, .previewHeightPercent = 30, .contentSidePadding = 15, .listRowHeight = 42, .listWithSubtitleRowHeight = 69, + .listRowGap = 6, + .listRowRadius = 20, + .listInset = 20, + .listSidePadding = 20, + .listSelectionStyle = 0, + .listScrollWidth = 4, + .listScrollSide = 0, + .listTitleBold = true, + .headerSidePadding = 18, + .headerUnderlineSize = 0, + .headerTitleAlign = 0, + .headerBatterySide = 0, + .headerBatteryDetached = true, .menuRowHeight = 42, .menuSpacing = 6, .tabSpacing = 10, .tabBarHeight = 50, + .tabBarAppearance = ThemeTabBarAppearance::Pill, .scrollBarWidth = 4, .scrollBarRightOffset = 5, - .homeTopPadding = 55, + .homeTopPadding = 75, // Compact cover tile so the full roundedraff home menu fits above button hints. .homeCoverHeight = 260, .homeCoverTileHeight = 300, @@ -35,23 +49,12 @@ constexpr ThemeMetrics values = {.batteryWidth = 15, .progressBarMarginTop = 1, .statusBarHorizontalMargin = 5, .statusBarVerticalMargin = 19, - .keyboardKeyWidth = 22, - .keyboardKeyHeight = 38, + .keyboardKeyHeight = 36, .keyboardKeySpacing = 10, - .keyboardBottomKeyHeight = 34, - .keyboardBottomKeySpacing = 5, - .keyboardBottomAligned = true, .keyboardCenteredText = false, .keyboardVerticalOffset = 0, .keyboardTextFieldWidthPercent = 85, - .keyboardWidthPercent = 90, - .keyboardKeyCornerRadius = 10, - .keyboardFillUnselected = true, - .keyboardOutlineAllUnselected = true, - .keyboardDrawSpecialOutlineWhenUnselected = true, - .keyboardSecondaryLabelRightPadding = 3, - .keyboardSecondaryLabelTopPadding = 1, - .keyboardMinArrowHeadSize = 1, + .keyboardWidthPercent = 94, .popupTopOffsetRatio = 0.12f, .popupMarginX = 20, .popupMarginY = 14, @@ -89,26 +92,29 @@ class RoundedRaffTheme : public BaseTheme { bool readerContext = false) const override; void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, bool selected) const override; + bool tabIndexFromPoint(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, int x, int y, + int& index) const override; void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, const std::function& storeCoverBuffer, const BookReadingStats* stats = nullptr, float progressPercent = -1.0f, const GlobalReadingStats* globalStats = nullptr, const char* currentChapterTitle = nullptr) const override; + int getMenuRowHeight(const GfxRenderer& renderer) const override; void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, const std::function& buttonLabel, const std::function& rowIcon) const override; void drawTextField(const GfxRenderer& renderer, Rect rect, int textWidth, bool cursorMode = false, int contentStartX = 0, int contentWidth = 0) const override; - void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, bool isSelected, - const char* secondaryLabel = nullptr, KeyboardKeyType keyType = KeyboardKeyType::Normal, - bool inactiveSelection = false) const override; + int getListRowStep(bool hasSubtitle, int rowHeightScale = 1) const override; + int getListPageItems(int contentHeight, bool hasSubtitle, int rowHeightScale = 1) const override; void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, const std::function& rowSubtitle = nullptr, const std::function& rowIcon = nullptr, const std::function& rowValue = nullptr, bool highlightValue = false, const std::function& rowDimmed = nullptr, - const std::function& isHeader = nullptr) const override; + const std::function& isHeader = nullptr, int rowHeightScale = 1, + bool showSelection = true) const override; void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, const char* btn4, bool allowInvertedText = false) const override; bool homeMenuShowsContinueReading() const { return true; } diff --git a/src/main.cpp b/src/main.cpp index a70f9c3e57..071ccc21af 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include -#include +#include +#include #include #include #include @@ -13,9 +14,13 @@ #include #include #include +#include #include -#include +#if !defined(SIMULATOR) && !FREEINK_MCU_C3 +#include +#endif #include +#include #ifdef SIMULATOR using esp_reset_reason_t = int; @@ -62,6 +67,11 @@ inline esp_sleep_wakeup_cause_t esp_sleep_get_wakeup_cause() { return ESP_SLEEP_ #include #include +#include + +#ifndef SIMULATOR +#include +#endif #include "AppVersion.h" #include "CrossPointSettings.h" @@ -72,154 +82,99 @@ inline esp_sleep_wakeup_cause_t esp_sleep_get_wakeup_cause() { return ESP_SLEEP_ #include "OpdsServerStore.h" #include "RecentBooksStore.h" #include "SdCardFontSystem.h" +#include "SilentRestart.h" #include "activities/Activity.h" #include "activities/ActivityManager.h" -#include "activities/reader/EpubReaderUtils.h" #include "activities/reader/KOReaderSyncActivity.h" #include "activities/reader/ReadingStatsUtils.h" #include "activities/reader/StatsBackup.h" +#include "activities/settings/FontDownloadActivity.h" +#include "activities/settings/KOReaderAuthActivity.h" #include "activities/settings/KOReaderSettingsActivity.h" +#include "activities/settings/OtaUpdateActivity.h" #include "activities/settings/SdFirmwareUpdateActivity.h" #include "components/UITheme.h" #include "fontIds.h" #include "network/UsbSerialFileTransfer.h" #ifdef SIMULATOR +#include + +#include "simulator/SimulatorHomeKeyInput.h" #include "simulator/SimulatorSmokeTest.h" #endif #include "images/LoadingIcon.h" #include "util/ButtonNavigator.h" +#include "util/Dictionary.h" +#include "util/DictionaryRegistry.h" #include "util/ScreenshotUtil.h" -MappedInputManager mappedInputManager(gpio); GfxRenderer renderer(display); +MappedInputManager mappedInputManager(gpio, renderer); ActivityManager activityManager(renderer, mappedInputManager); FontDecompressor fontDecompressor; SdCardFontSystem sdFontSystem; +DictionaryRegistry dictionaryRegistry; FontCacheManager fontCacheManager(renderer.getFontMap(), renderer.getSdCardFonts()); static unsigned long allowSleepAt = 0; +static unsigned long lastX4ProPowerClickAt = 0; +// A held power button can span deep-sleep wake and the first main-loop frame. +// Do not treat that wake gesture as an in-session shortcut until it has been released. +static bool powerButtonReleasedSinceWake = false; + +namespace { +constexpr unsigned long X4PRO_POWER_DOUBLE_CLICK_MS = 500; +constexpr unsigned long X4PRO_POWER_CLICK_MAX_HOLD_MS = 400; +} // namespace + +static void logBootHeap(const char* stage) { + LOG_DBG("BOOTMEM", "%s: free=%u maxAlloc=%u", stage, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); +} // Fonts -#ifndef OMIT_MEDIUM_FONT -EpdFont lexenddeca14RegularFont(&lexenddeca_14_regular); -EpdFont lexenddeca14BoldFont(&lexenddeca_14_bold); -EpdFont lexenddeca14ItalicFont(&lexenddeca_14_italic); -EpdFont lexenddeca14BoldItalicFont(&lexenddeca_14_bolditalic); -EpdFontFamily lexenddeca14FontFamily(&lexenddeca14RegularFont, &lexenddeca14BoldFont, &lexenddeca14ItalicFont, - &lexenddeca14BoldItalicFont); -#endif -#ifndef OMIT_TEENSY_FONT -EpdFont lexenddeca8RegularFont(&lexenddeca_8_regular); -EpdFont lexenddeca8BoldFont(&lexenddeca_8_bold); -EpdFont lexenddeca8ItalicFont(&lexenddeca_8_italic); -EpdFont lexenddeca8BoldItalicFont(&lexenddeca_8_bolditalic); -EpdFontFamily lexenddeca8FontFamily(&lexenddeca8RegularFont, &lexenddeca8BoldFont, &lexenddeca8ItalicFont, - &lexenddeca8BoldItalicFont); -#endif -#ifndef OMIT_ITTY_BITTY_FONT -EpdFont lexenddeca9RegularFont(&lexenddeca_9_regular); -EpdFont lexenddeca9BoldFont(&lexenddeca_9_bold); -EpdFont lexenddeca9ItalicFont(&lexenddeca_9_italic); -EpdFont lexenddeca9BoldItalicFont(&lexenddeca_9_bolditalic); -EpdFontFamily lexenddeca9FontFamily(&lexenddeca9RegularFont, &lexenddeca9BoldFont, &lexenddeca9ItalicFont, - &lexenddeca9BoldItalicFont); -#endif -#ifndef OMIT_TINY_FONT EpdFont lexenddeca10RegularFont(&lexenddeca_10_regular); EpdFont lexenddeca10BoldFont(&lexenddeca_10_bold); EpdFont lexenddeca10ItalicFont(&lexenddeca_10_italic); EpdFont lexenddeca10BoldItalicFont(&lexenddeca_10_bolditalic); EpdFontFamily lexenddeca10FontFamily(&lexenddeca10RegularFont, &lexenddeca10BoldFont, &lexenddeca10ItalicFont, &lexenddeca10BoldItalicFont); -#endif -#ifndef OMIT_SMALL_FONT EpdFont lexenddeca12RegularFont(&lexenddeca_12_regular); EpdFont lexenddeca12BoldFont(&lexenddeca_12_bold); EpdFont lexenddeca12ItalicFont(&lexenddeca_12_italic); EpdFont lexenddeca12BoldItalicFont(&lexenddeca_12_bolditalic); EpdFontFamily lexenddeca12FontFamily(&lexenddeca12RegularFont, &lexenddeca12BoldFont, &lexenddeca12ItalicFont, &lexenddeca12BoldItalicFont); -#endif -#ifndef OMIT_LARGE_FONT +EpdFont lexenddeca14RegularFont(&lexenddeca_14_regular); +EpdFont lexenddeca14BoldFont(&lexenddeca_14_bold); +EpdFont lexenddeca14ItalicFont(&lexenddeca_14_italic); +EpdFont lexenddeca14BoldItalicFont(&lexenddeca_14_bolditalic); +EpdFontFamily lexenddeca14FontFamily(&lexenddeca14RegularFont, &lexenddeca14BoldFont, &lexenddeca14ItalicFont, + &lexenddeca14BoldItalicFont); EpdFont lexenddeca16RegularFont(&lexenddeca_16_regular); EpdFont lexenddeca16BoldFont(&lexenddeca_16_bold); EpdFont lexenddeca16ItalicFont(&lexenddeca_16_italic); EpdFont lexenddeca16BoldItalicFont(&lexenddeca_16_bolditalic); EpdFontFamily lexenddeca16FontFamily(&lexenddeca16RegularFont, &lexenddeca16BoldFont, &lexenddeca16ItalicFont, &lexenddeca16BoldItalicFont); -#endif -#ifndef OMIT_XLARGE_FONT -EpdFont lexenddeca18RegularFont(&lexenddeca_18_regular); -EpdFont lexenddeca18BoldFont(&lexenddeca_18_bold); -EpdFont lexenddeca18ItalicFont(&lexenddeca_18_italic); -EpdFont lexenddeca18BoldItalicFont(&lexenddeca_18_bolditalic); -EpdFontFamily lexenddeca18FontFamily(&lexenddeca18RegularFont, &lexenddeca18BoldFont, &lexenddeca18ItalicFont, - &lexenddeca18BoldItalicFont); -#endif -#ifndef OMIT_HUGE_FONT -EpdFont lexenddeca20RegularFont(&lexenddeca_20_regular); -EpdFont lexenddeca20BoldFont(&lexenddeca_20_bold); -EpdFont lexenddeca20ItalicFont(&lexenddeca_20_italic); -EpdFont lexenddeca20BoldItalicFont(&lexenddeca_20_bolditalic); -EpdFontFamily lexenddeca20FontFamily(&lexenddeca20RegularFont, &lexenddeca20BoldFont, &lexenddeca20ItalicFont, - &lexenddeca20BoldItalicFont); -#endif - -#ifndef OMIT_TEENSY_FONT -EpdFont bitter8RegularFont(&bitter_8_regular); -EpdFont bitter8BoldFont(&bitter_8_bold); -EpdFont bitter8ItalicFont(&bitter_8_italic); -EpdFont bitter8BoldItalicFont(&bitter_8_bolditalic); -EpdFontFamily bitter8FontFamily(&bitter8RegularFont, &bitter8BoldFont, &bitter8ItalicFont, &bitter8BoldItalicFont); -#endif -#ifndef OMIT_ITTY_BITTY_FONT -EpdFont bitter9RegularFont(&bitter_9_regular); -EpdFont bitter9BoldFont(&bitter_9_bold); -EpdFont bitter9ItalicFont(&bitter_9_italic); -EpdFont bitter9BoldItalicFont(&bitter_9_bolditalic); -EpdFontFamily bitter9FontFamily(&bitter9RegularFont, &bitter9BoldFont, &bitter9ItalicFont, &bitter9BoldItalicFont); -#endif -#ifndef OMIT_TINY_FONT EpdFont bitter10RegularFont(&bitter_10_regular); EpdFont bitter10BoldFont(&bitter_10_bold); EpdFont bitter10ItalicFont(&bitter_10_italic); EpdFont bitter10BoldItalicFont(&bitter_10_bolditalic); EpdFontFamily bitter10FontFamily(&bitter10RegularFont, &bitter10BoldFont, &bitter10ItalicFont, &bitter10BoldItalicFont); -#endif -#ifndef OMIT_SMALL_FONT EpdFont bitter12RegularFont(&bitter_12_regular); EpdFont bitter12BoldFont(&bitter_12_bold); EpdFont bitter12ItalicFont(&bitter_12_italic); EpdFont bitter12BoldItalicFont(&bitter_12_bolditalic); EpdFontFamily bitter12FontFamily(&bitter12RegularFont, &bitter12BoldFont, &bitter12ItalicFont, &bitter12BoldItalicFont); -#endif -#ifndef OMIT_MEDIUM_FONT EpdFont bitter14RegularFont(&bitter_14_regular); EpdFont bitter14BoldFont(&bitter_14_bold); EpdFont bitter14ItalicFont(&bitter_14_italic); EpdFont bitter14BoldItalicFont(&bitter_14_bolditalic); EpdFontFamily bitter14FontFamily(&bitter14RegularFont, &bitter14BoldFont, &bitter14ItalicFont, &bitter14BoldItalicFont); -#endif -#ifndef OMIT_LARGE_FONT EpdFont bitter16RegularFont(&bitter_16_regular); EpdFont bitter16BoldFont(&bitter_16_bold); EpdFont bitter16ItalicFont(&bitter_16_italic); EpdFont bitter16BoldItalicFont(&bitter_16_bolditalic); EpdFontFamily bitter16FontFamily(&bitter16RegularFont, &bitter16BoldFont, &bitter16ItalicFont, &bitter16BoldItalicFont); -#endif -#ifndef OMIT_XLARGE_FONT -EpdFont bitter18RegularFont(&bitter_18_regular); -EpdFont bitter18BoldFont(&bitter_18_bold); -EpdFont bitter18ItalicFont(&bitter_18_italic); -EpdFont bitter18BoldItalicFont(&bitter_18_bolditalic); -EpdFontFamily bitter18FontFamily(&bitter18RegularFont, &bitter18BoldFont, &bitter18ItalicFont, &bitter18BoldItalicFont); -#endif -#ifndef OMIT_HUGE_FONT -EpdFont bitter20RegularFont(&bitter_20_regular); -EpdFont bitter20BoldFont(&bitter_20_bold); -EpdFont bitter20ItalicFont(&bitter_20_italic); -EpdFont bitter20BoldItalicFont(&bitter_20_bolditalic); -EpdFontFamily bitter20FontFamily(&bitter20RegularFont, &bitter20BoldFont, &bitter20ItalicFont, &bitter20BoldItalicFont); -#endif EpdFont smallFont(&inter_8_regular); EpdFontFamily smallFontFamily(&smallFont); @@ -325,12 +280,32 @@ const char* wakeupRouteName(const HalGPIO::WakeupReason reason) { } } +void logMemoryStats(const char* phase) { +#if defined(BOARD_HAS_PSRAM) + LOG_INF("MEM", "%s: heap free=%u total=%u min=%u maxAlloc=%u psram free=%u total=%u min=%u maxAlloc=%u", phase, + ESP.getFreeHeap(), ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap(), ESP.getFreePsram(), + ESP.getPsramSize(), ESP.getMinFreePsram(), ESP.getMaxAllocPsram()); +#else + LOG_INF("MEM", "%s: heap free=%u total=%u min=%u maxAlloc=%u", phase, ESP.getFreeHeap(), ESP.getHeapSize(), + ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); +#endif +} + // Definitions for SilentRestart.h. RTC_NOINIT survives ESP.restart() but not power loss. RTC_NOINIT_ATTR uint32_t silentRebootMagic; RTC_NOINIT_ATTR uint32_t silentRebootTarget; +RTC_NOINIT_ATTR uint32_t silentRebootPayload; +RTC_NOINIT_ATTR uint32_t silentReaderPageBuildMagic; +RTC_NOINIT_ATTR uint32_t silentReaderPageBuildBookHash; +RTC_NOINIT_ATTR uint32_t silentReaderPageBuildPackedTarget; +RTC_NOINIT_ATTR uint32_t silentReaderPageBuildFlags; constexpr uint32_t SILENT_REBOOT_MAGIC = 0xC1EAB007; constexpr uint32_t SILENT_REBOOT_TARGET_HOME = 0; constexpr uint32_t SILENT_REBOOT_TARGET_READER = 1; +constexpr uint32_t SILENT_READER_PAGE_BUILD_MAGIC = 0xC1EAB017; +constexpr uint32_t SILENT_READER_PAGE_BUILD_AUTO_TURN = 1U << 0; +constexpr uint32_t NETWORK_RENDER_TASK_STACK_BYTES = 8192; +constexpr uint32_t READER_RENDER_TASK_STACK_BYTES = 16384; // How the device is coming back to life, resolved once at boot. Both resume // flows suppress the splash and leave the panel holding its pre-boot frame; a @@ -338,6 +313,7 @@ constexpr uint32_t SILENT_REBOOT_TARGET_READER = 1; enum class BootResume : uint8_t { Splash, // cold boot, flash, panic, or plain reboot Silent, // heap-defrag ESP.restart() (RTC flag; lost on power loss) + Network, // minimal boot directly into a memory-intensive network activity QuickResume, // wake from a quick-resume deep sleep (SD flag; survives power loss) }; @@ -349,9 +325,29 @@ enum class BootResume : uint8_t { // startDeepSleep() does not return, so a set latch only ends at the wakeup reset. static bool deepSleepInProgress = false; +static void restartWithSilentToken() { +#ifdef SIMULATOR + SimulatorLifecycle::setSilentRebootToken(silentRebootMagic, silentRebootTarget, silentRebootPayload); +#endif + ESP.restart(); +} + +static uint32_t silentRestartBookHash(const std::string& bookPath) { + return uzlib_crc32(bookPath.data(), static_cast(bookPath.size()), 0); +} + +static void clearSilentRestartReaderPageBuild() { + silentReaderPageBuildMagic = 0; + silentReaderPageBuildBookHash = 0; + silentReaderPageBuildPackedTarget = 0; + silentReaderPageBuildFlags = 0; +} + void silentRestart() { if (deepSleepInProgress) return; // sleeping supersedes the heap-defrag reboot + clearSilentRestartReaderPageBuild(); silentRebootTarget = SILENT_REBOOT_TARGET_HOME; + silentRebootPayload = 0; silentRebootMagic = SILENT_REBOOT_MAGIC; LOG_DBG("MAIN", "Silent restart (target=home)"); // E-ink retains the previous frame until Home's first paint lands (~2-3s). @@ -360,19 +356,61 @@ void silentRestart() { // book, looking like a trampoline back to the reader they just exited. GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); delay(50); - ESP.restart(); + restartWithSilentToken(); +} + +void armSilentRestartReaderPageBuild(const std::string& bookPath, const uint16_t spineIndex, const uint16_t targetPage, + const bool autoPageTurnActive) { + silentReaderPageBuildBookHash = silentRestartBookHash(bookPath); + silentReaderPageBuildPackedTarget = (static_cast(spineIndex) << 16) | targetPage; + silentReaderPageBuildFlags = autoPageTurnActive ? SILENT_READER_PAGE_BUILD_AUTO_TURN : 0; + silentReaderPageBuildMagic = SILENT_READER_PAGE_BUILD_MAGIC; +} + +bool consumeSilentRestartReaderPageBuild(const std::string& bookPath, uint16_t& spineIndex, uint16_t& targetPage, + bool& autoPageTurnActive) { + const bool matches = silentReaderPageBuildMagic == SILENT_READER_PAGE_BUILD_MAGIC && + silentReaderPageBuildBookHash == silentRestartBookHash(bookPath); + const uint32_t packedTarget = silentReaderPageBuildPackedTarget; + const uint32_t flags = silentReaderPageBuildFlags; + clearSilentRestartReaderPageBuild(); + if (!matches) { + autoPageTurnActive = false; + return false; + } + + spineIndex = static_cast(packedTarget >> 16); + targetPage = static_cast(packedTarget & 0xFFFFU); + autoPageTurnActive = (flags & SILENT_READER_PAGE_BUILD_AUTO_TURN) != 0; + return true; } void silentRestartToReader() { if (deepSleepInProgress) return; // sleeping supersedes the heap-defrag reboot silentRebootTarget = SILENT_REBOOT_TARGET_READER; + silentRebootPayload = 0; silentRebootMagic = SILENT_REBOOT_MAGIC; LOG_DBG("MAIN", "Silent restart (target=reader)"); GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); delay(50); - ESP.restart(); + restartWithSilentToken(); +} + +void silentRestartToNetwork(const NetworkBootTarget target, const uint32_t payload) { + if (deepSleepInProgress) return; + clearSilentRestartReaderPageBuild(); + silentRebootTarget = static_cast(target); + silentRebootPayload = payload; + silentRebootMagic = SILENT_REBOOT_MAGIC; + LOG_DBG("MAIN", "Silent restart (target=network/%lu payload=%lu)", static_cast(silentRebootTarget), + static_cast(payload)); + GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); + delay(50); + restartWithSilentToken(); } +void silentRestartToManageFonts() { silentRestartToNetwork(NetworkBootTarget::MANAGE_FONTS); } + void waitForPowerRelease() { gpio.update(); while (gpio.isPressed(HalGPIO::BTN_POWER)) { @@ -385,58 +423,53 @@ bool isGlobalPowerButtonAction(const CrossPointSettings::SHORT_PWRBTN action) { return isPowerButtonActionAvailableOutsideReader(action); } -bool startGlobalSyncProgress() { +bool startGlobalSyncProgress(const bool networkBootReady = false) { + if (activityManager.hasActivityNamed(KOReaderSyncActivity::NAME)) { + LOG_DBG("MAIN", "Ignoring KOReader sync shortcut while sync is already active"); + return true; + } + if (!KOREADER_STORE.hasCredentials()) { + if (networkBootReady) return false; activityManager.pushActivity(std::make_unique(renderer, mappedInputManager)); return true; } - const std::string epubPath = APP_STATE.openEpubPath; + std::string epubPath = APP_STATE.openEpubPath; if (epubPath.empty() || !FsHelpers::hasEpubExtension(epubPath) || !Storage.exists(epubPath.c_str())) { + if (networkBootReady) return false; LOG_DBG("MAIN", "No syncable EPUB open, opening KOReader settings instead"); activityManager.pushActivity(std::make_unique(renderer, mappedInputManager)); return true; } - auto epub = std::make_shared(epubPath, "/.crosspoint"); - if (!epub->load(true, SETTINGS.embeddedStyle == 0)) { - LOG_ERR("MAIN", "Failed to load EPUB for global sync: %s", epubPath.c_str()); - activityManager.pushActivity(std::make_unique(renderer, mappedInputManager)); + if (!networkBootReady) { + silentRestartToNetwork(NetworkBootTarget::KOREADER_SYNC); return true; } - epub->setupCacheDir(); - - int spineIndex = 0; - int pageNumber = 0; - int totalPagesInSpine = 1; - EpubReaderUtils::Progress progress; - if (EpubReaderUtils::loadProgress(*epub, progress, "MAIN")) { - spineIndex = progress.spineIndex; - pageNumber = progress.pageNumber; - if (progress.hasPageCount) { - totalPagesInSpine = std::max(1, progress.pageCount); - } - } - - if (spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) { - spineIndex = 0; + const DocumentMatchMethod matchMethod = KOREADER_STORE.getMatchMethod(); + auto syncActivity = + makeUniqueNoThrow(renderer, mappedInputManager, std::move(epubPath), matchMethod); + if (!syncActivity) { + LOG_ERR("MAIN", "OOM: KOReader sync activity (free=%u maxAlloc=%u)", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + return false; } - - CrossPointPosition localPos = {spineIndex, pageNumber, totalPagesInSpine}; - KOReaderPosition localKoPos = ProgressMapper::toKOReader(epub, localPos); - const int tocIdx = epub->getTocIndexForSpineIndex(spineIndex); - std::string localChapterName = (tocIdx >= 0) ? epub->getTocItem(tocIdx).title : ""; - - activityManager.pushActivity( - std::make_unique(renderer, mappedInputManager, epubPath, spineIndex, pageNumber, - totalPagesInSpine, std::move(localKoPos), std::move(localChapterName))); + activityManager.replaceActivity(std::move(syncActivity)); return true; } CrossPointSettings::SHORT_PWRBTN getPowerButtonAction() { static bool longPowerButtonHandled = false; + if (activityManager.readerPowerButtonOpensSettings()) { + if (mappedInputManager.wasReleased(MappedInputManager::Button::Power)) { + longPowerButtonHandled = false; + screenshotComboHandled = false; + } + return CrossPointSettings::SHORT_PWRBTN::IGNORE; + } + if (mappedInputManager.wasReleased(MappedInputManager::Button::Power)) { if (longPowerButtonHandled) { longPowerButtonHandled = false; @@ -474,7 +507,9 @@ bool handleGlobalPowerButtonAction(const CrossPointSettings::SHORT_PWRBTN action enterDeepSleep(); return true; case CrossPointSettings::SHORT_PWRBTN::FORCE_REFRESH: { - LOG_DBG("MAIN", "Manual screen refresh triggered"); + if (SETTINGS.textAntiAliasing && activityManager.requestManualReaderRefresh()) { + return true; + } RenderLock lock; renderer.displayBuffer(HalDisplay::HALF_REFRESH); return true; @@ -539,6 +574,35 @@ void putTiltSensorToSleepForDeepSleep() { } LOG_ERR("MAIN", "Tilt sensor did not confirm sleep before deep sleep"); } + +bool handleX4ProFrontlightDoubleClick() { +#ifdef SIMULATOR + return false; +#else + if (!BoardConfig::isX4Pro() || !gpio.wasReleased(HalGPIO::BTN_POWER)) { + return false; + } + + const unsigned long now = millis(); + if (gpio.getPowerButtonHeldTime() > X4PRO_POWER_CLICK_MAX_HOLD_MS) { + lastX4ProPowerClickAt = 0; + return false; + } + + if (lastX4ProPowerClickAt == 0 || now - lastX4ProPowerClickAt > X4PRO_POWER_DOUBLE_CLICK_MS) { + lastX4ProPowerClickAt = now; + return false; + } + + lastX4ProPowerClickAt = 0; + const bool lightOn = !Frontlight.isOn(); + Frontlight.setOn(lightOn); + SETTINGS.frontlightOn = lightOn ? 1 : 0; + SETTINGS.saveToFile(); + LOG_INF("LIGHT", "Frontlight toggled %s by power-button double-click", lightOn ? "on" : "off"); + return true; +#endif +} } // namespace constexpr char SLEEP_FRAME_FILE[] = "/.crosspoint/sleep_frame.bin"; @@ -564,6 +628,42 @@ static bool loadSleepFrameBuffer() { return true; } +// The wake-hold verification runs before the SD card is mounted (see setup()), +// so the one setting it needs — "short press = sleep", which makes any tap a +// valid wake — is mirrored into NVS. Written at sleep entry (the value that +// matters is the one in force when the device went down) and re-synced after +// each settings load in case the device lost power without a clean sleep. +constexpr char WAKE_NVS_NAMESPACE[] = "crosspoint"; +constexpr char WAKE_SHORT_PRESS_KEY[] = "wakeShortPr"; + +bool readWakeShortPressFromNvs() { +#ifdef SIMULATOR + return false; +#else + nvs_handle_t h; + if (nvs_open(WAKE_NVS_NAMESPACE, NVS_READONLY, &h) != ESP_OK) return false; + uint8_t v = 0; + const esp_err_t e = nvs_get_u8(h, WAKE_SHORT_PRESS_KEY, &v); + nvs_close(h); + return e == ESP_OK && v != 0; +#endif +} + +void mirrorWakeShortPressToNvs() { +#ifndef SIMULATOR + const uint8_t want = (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) ? 1 : 0; + nvs_handle_t h; + if (nvs_open(WAKE_NVS_NAMESPACE, NVS_READWRITE, &h) != ESP_OK) return; + uint8_t cur = 0; + const bool have = nvs_get_u8(h, WAKE_SHORT_PRESS_KEY, &cur) == ESP_OK; + if (!have || cur != want) { // skip the flash write when unchanged + nvs_set_u8(h, WAKE_SHORT_PRESS_KEY, want); + nvs_commit(h); + } + nvs_close(h); +#endif +} + // Enter deep sleep mode void enterDeepSleep(bool fromTimeout) { HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation @@ -588,7 +688,7 @@ void enterDeepSleep(bool fromTimeout) { delay(POST_SLEEP_SCREEN_SETTLE_MS); } - if (gpio.deviceIsX3() && SETTINGS.autoBackupStats != 0) { + if (halClock.isAvailable() && SETTINGS.autoBackupStats != 0) { ReadingStatsDateTime now; if (getCurrentLocalReadingStatsDateTime(now) && !backupGlobalStats(false)) { LOG_ERR("MAIN", "Automatic reading-stats backup failed before deep sleep"); @@ -597,12 +697,26 @@ void enterDeepSleep(bool fromTimeout) { putTiltSensorToSleepForDeepSleep(); display.deepSleep(); + mirrorWakeShortPressToNvs(); // next boot's wake-hold check reads this pre-SD LOG_DBG("MAIN", "Entering deep sleep"); powerManager.startDeepSleep(gpio); } -void setupDisplayAndFonts(bool seamless = false) { +void setupDisplayAndFonts(const bool seamless = false, const bool loadReaderResources = true) { +#if !defined(SIMULATOR) && !FREEINK_MCU_C3 + // C3 X3/X4 detection already runs in HalGPIO::begin() before SPI owns the + // panel pins. S3 boards initialize display SPI inside display.begin(), so an + // X4 Pro must resolve a UC8179 replacement panel here, before driver selection. + static bool controllerResolved = false; + if (!controllerResolved) { + controllerResolved = true; + if (freeink::applyXteinkDisplayController()) { + LOG_DBG("MAIN", "Panel controller: UltraChip UC81xx variant detected"); + } + } +#endif + #ifdef SIMULATOR (void)seamless; display.begin(); @@ -610,11 +724,10 @@ void setupDisplayAndFonts(bool seamless = false) { display.begin(seamless); #endif renderer.begin(); - if (!ScratchWorkspace::initialize()) { - LOG_ERR("MAIN", "Scratch workspace init failed"); - } - activityManager.begin(); - LOG_DBG("MAIN", "Display initialized"); + // FreeInkUI headers need more than 4 KB once the render loop and nested + // screen builders share the task stack. Every lightweight network target + // uses this shared 8 KB budget; reader rendering retains its 16 KB budget. + activityManager.begin(loadReaderResources ? READER_RENDER_TASK_STACK_BYTES : NETWORK_RENDER_TASK_STACK_BYTES); // Initialize font decompressor for compressed reader fonts if (!fontDecompressor.init()) { @@ -623,66 +736,31 @@ void setupDisplayAndFonts(bool seamless = false) { fontCacheManager.setFontDecompressor(&fontDecompressor); renderer.setFontCacheManager(&fontCacheManager); -#ifndef OMIT_TEENSY_FONT - renderer.insertFont(LEXENDDECA_8_FONT_ID, lexenddeca8FontFamily); -#endif -#ifndef OMIT_ITTY_BITTY_FONT - renderer.insertFont(LEXENDDECA_9_FONT_ID, lexenddeca9FontFamily); -#endif -#ifndef OMIT_TINY_FONT renderer.insertFont(LEXENDDECA_10_FONT_ID, lexenddeca10FontFamily); -#endif -#ifndef OMIT_SMALL_FONT renderer.insertFont(LEXENDDECA_12_FONT_ID, lexenddeca12FontFamily); -#endif -#ifndef OMIT_MEDIUM_FONT renderer.insertFont(LEXENDDECA_14_FONT_ID, lexenddeca14FontFamily); -#endif -#ifndef OMIT_LARGE_FONT renderer.insertFont(LEXENDDECA_16_FONT_ID, lexenddeca16FontFamily); -#endif -#ifndef OMIT_XLARGE_FONT - renderer.insertFont(LEXENDDECA_18_FONT_ID, lexenddeca18FontFamily); -#endif -#ifndef OMIT_HUGE_FONT - renderer.insertFont(LEXENDDECA_20_FONT_ID, lexenddeca20FontFamily); -#endif - -#ifndef OMIT_TEENSY_FONT - renderer.insertFont(BITTER_8_FONT_ID, bitter8FontFamily); -#endif -#ifndef OMIT_ITTY_BITTY_FONT - renderer.insertFont(BITTER_9_FONT_ID, bitter9FontFamily); -#endif -#ifndef OMIT_TINY_FONT renderer.insertFont(BITTER_10_FONT_ID, bitter10FontFamily); -#endif -#ifndef OMIT_SMALL_FONT renderer.insertFont(BITTER_12_FONT_ID, bitter12FontFamily); -#endif -#ifndef OMIT_MEDIUM_FONT renderer.insertFont(BITTER_14_FONT_ID, bitter14FontFamily); -#endif -#ifndef OMIT_LARGE_FONT renderer.insertFont(BITTER_16_FONT_ID, bitter16FontFamily); -#endif -#ifndef OMIT_XLARGE_FONT - renderer.insertFont(BITTER_18_FONT_ID, bitter18FontFamily); -#endif -#ifndef OMIT_HUGE_FONT - renderer.insertFont(BITTER_20_FONT_ID, bitter20FontFamily); -#endif renderer.insertFont(UI_10_FONT_ID, ui10FontFamily); renderer.insertFont(UI_12_FONT_ID, ui12FontFamily); renderer.insertFont(SMALL_FONT_ID, smallFontFamily); - // Discover and load SD card fonts - sdFontSystem.begin(renderer); - - LOG_DBG("MAIN", "Fonts setup"); + if (loadReaderResources) { + sdFontSystem.begin(renderer); + } else { + LOG_DBG("MAIN", "Skipping EPUB scratch workspace and SD fonts for minimal network boot"); + } } void setup() { +#ifdef SIMULATOR + SimulatorLifecycle::restoreSilentRebootToken(silentRebootMagic, silentRebootTarget, silentRebootPayload); +#endif + BoardConfig::holdPowerRails(); + t1 = millis(); const esp_reset_reason_t rawResetReason = esp_reset_reason(); @@ -701,7 +779,7 @@ void setup() { logSerial.setRxBufferSize(1024); logSerial.setTxBufferSize(1024); Serial.begin(115200); -#ifndef SIMULATOR +#if !defined(SIMULATOR) && LOG_SERIAL_HAS_TX_TIMEOUT logSerial.setTxTimeoutMs(1); // This is a load-bearing 1. Do not modify. #endif #endif @@ -711,55 +789,64 @@ void setup() { resetReasonName(rawResetReason), static_cast(rawWakeupCause), wakeupCauseName(rawWakeupCause)); // Read-and-clear so a panic later in setup() doesn't loop into silent reboot. - // Bound the target range too — RTC_NOINIT memory is uninitialized on cold boot. + // Validate the target too — RTC_NOINIT memory is uninitialized on cold boot. const bool isSilentReboot = (silentRebootMagic == SILENT_REBOOT_MAGIC); - const uint32_t snapshotTarget = - (isSilentReboot && silentRebootTarget <= SILENT_REBOOT_TARGET_READER) ? silentRebootTarget : 0; + const bool isValidSilentTarget = + silentRebootTarget <= SILENT_REBOOT_TARGET_READER || isNetworkBootTargetValue(silentRebootTarget); + const uint32_t snapshotTarget = (isSilentReboot && isValidSilentTarget) ? silentRebootTarget : 0; + const uint32_t snapshotPayload = isSilentReboot ? silentRebootPayload : 0; + const bool isNetworkResume = snapshotTarget >= static_cast(NetworkBootTarget::OTA); silentRebootMagic = 0; silentRebootTarget = 0; + silentRebootPayload = 0; + if (!isSilentReboot || snapshotTarget != SILENT_REBOOT_TARGET_READER) { + clearSilentRestartReaderPageBuild(); + } gpio.begin(); + // Sticky shares Confirm and Power on one GPIO. Emit Power first so the + // configured shortcut wins; MappedInputManager mirrors it back to Confirm + // only on screens that explicitly allow the fallback. + gpio.setSharedConfirmPowerShortPressEmitsPower(true); powerManager.begin(); halTiltSensor.begin(); halClock.begin(); +#if FREEINK_DEVICE_X4 || FREEINK_DEVICE_X3 LOG_INF("MAIN", "Hardware detect: %s", gpio.deviceIsX3() ? "X3" : "X4"); LOG_INF("BOOT", "Post-GPIO diagnostic: device=%s usb=%d silentReboot=%d silentTarget=%lu", gpio.deviceIsX3() ? "X3" : "X4", gpio.isUsbConnected() ? 1 : 0, isSilentReboot ? 1 : 0, static_cast(snapshotTarget)); +#else +#ifdef SIMULATOR + LOG_INF("MAIN", "Device: Simulator"); +#else + LOG_INF("MAIN", "Device: %s", BoardConfig::ACTIVE.name); +#endif +#endif - // SD Card Initialization - // We need 6 open files concurrently when parsing a new chapter - if (!Storage.begin()) { - LOG_ERR("MAIN", "SD card initialization failed"); - setupDisplayAndFonts(isSilentReboot); - activityManager.goToFullScreenMessage("SD card error", EpdFontFamily::BOLD); - return; - } - - HalSystem::checkPanic(); - - SETTINGS.loadFromFile(); - Storage.installDateTimeCallback(&SETTINGS.clockUtcOffsetQ); - APP_STATE.loadFromFile(); - RECENT_BOOKS.loadFromFile(); - I18N.setLanguage(static_cast(SETTINGS.language)); - KOREADER_STORE.loadFromFile(); - OPDS_STORE.loadFromFile(); - UITheme::getInstance().reload(); - ButtonNavigator::setMappedInputManager(mappedInputManager); - - // Check wake duration before the remaining file loads so the user does not - // have to hold the power button across all of the SD reads below. + // Verify the wake reason BEFORE the SD mount and settings loads. The power + // button must still be held when the check runs (released = back to sleep), + // so everything ahead of it extends the real-world hold requirement — and + // the SD mount (per-attempt power-cycle retries on some boards) is the + // slowest, most variable stage of boot. Verifying here needs only gpio + + // powerManager; the one setting involved ("short press = sleep" makes any + // tap a valid wake) comes from its NVS mirror since SETTINGS lives on the + // not-yet-mounted SD card. const auto wakeupReason = gpio.getWakeupReason(); LOG_INF("BOOT", "Wake route: %s", wakeupRouteName(wakeupReason)); switch (wakeupReason) { - case HalGPIO::WakeupReason::PowerButton: - LOG_INF("BOOT", "Power-button wake: verifying duration required=%u shortAllowed=%d", - SETTINGS.getPowerButtonWakeDuration(), SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP); - gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonWakeDuration(), - SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP); + case HalGPIO::WakeupReason::PowerButton: { + const bool shortPressWakes = readWakeShortPressFromNvs(); + const uint16_t requiredDuration = shortPressWakes ? CrossPointSettings::POWER_BUTTON_WAKE_SHORT_MS + : CrossPointSettings::POWER_BUTTON_WAKE_LONG_MS; + LOG_INF("BOOT", "Power-button wake: verifying duration required=%u shortAllowed=%d", requiredDuration, + shortPressWakes); + if (!gpio.verifyPowerButtonWakeup(requiredDuration, shortPressWakes)) { + powerManager.startDeepSleep(gpio); + } break; + } case HalGPIO::WakeupReason::AfterUSBPower: // TEMP: continue booting while diagnosing post-flash/reset behavior. // Normal behavior is to go back to sleep when USB power causes a cold boot. @@ -775,9 +862,53 @@ void setup() { break; } - // Recovery firmware mode: hold left side button (BTN_UP) together with the power button at - // boot to skip directly to the SD-card firmware update screen. Useful on devices where USB - // flashing has been locked down (e.g. recent X3 firmware). + // SD Card Initialization + // We need 6 open files concurrently when parsing a new chapter + if (!Storage.begin()) { + LOG_ERR("MAIN", "SD card initialization failed"); + setupDisplayAndFonts(isSilentReboot, !isNetworkResume); + activityManager.goToFullScreenMessage("SD card error", EpdFontFamily::BOLD); + return; + } + logBootHeap("storage ready"); + + HalSystem::checkPanic(); + + SETTINGS.loadFromFile(); + Storage.installDateTimeCallback(&SETTINGS.clockUtcOffsetQ); + APP_STATE.loadFromFile(); + I18N.setLanguage(static_cast(SETTINGS.language)); + if (!isNetworkResume) { + RECENT_BOOKS.loadFromFile(); + logBootHeap("settings and recent books loaded"); + KOREADER_STORE.loadFromFile(); + logBootHeap("sync credentials loaded"); + Dictionary::isValidDictionary(); + } else if (snapshotTarget == static_cast(NetworkBootTarget::KOREADER_SYNC) || + snapshotTarget == static_cast(NetworkBootTarget::KOREADER_AUTH) || + snapshotTarget == static_cast(NetworkBootTarget::FILE_TRANSFER)) { + KOREADER_STORE.loadFromFile(); + } + UITheme::getInstance().reload(); + ButtonNavigator::setMappedInputManager(mappedInputManager); + logBootHeap("boot state ready"); + // Frontlight PWM up (no-op on boards without one). Brightness + warmth are always + // restored from persisted settings. The on/off state defaults to OFF at wake/boot — + // so the user isn't greeted by a surprise glow (or a silent battery drain) — unless + // "Restore Light on Wake" is enabled, which brings back the pre-sleep on/off state too. + // Silent/network restarts are automated transitions rather than deliberate + // sleep, so preserve the light state across them regardless of that setting. + const bool restoreLightOn = SETTINGS.frontlightOn != 0 && (SETTINGS.frontlightRestoreOnWake != 0 || isSilentReboot); + Frontlight.begin(SETTINGS.frontlightBrightness, SETTINGS.frontlightWarmth, restoreLightOn); + + // Re-sync the wake-hold NVS mirror with the freshly-loaded settings, covering + // a setting change followed by power loss without a clean sleep. (The wake + // verification itself already ran, pre-SD, further up.) + mirrorWakeShortPressToNvs(); + + // Recovery firmware mode: hold a side button together with Power to open the + // SD-card firmware update screen. X4 Pro uses BTN_DOWN because BTN_UP is GPIO0, + // an S3 strap pin that can read low during boot and false-trigger recovery. bool recoveryFirmwareMode = false; if (wakeupReason == HalGPIO::WakeupReason::PowerButton) { // Refresh the cached button state a few times — isPressed() needs ~half a second to settle @@ -788,30 +919,40 @@ void setup() { gpio.update(); delay(10); } - if (gpio.isPressed(HalGPIO::BTN_UP)) { + const bool recoveryButtonHeld = + BoardConfig::isX4Pro() ? gpio.isPressed(HalGPIO::BTN_DOWN) : gpio.isPressed(HalGPIO::BTN_UP); + if (recoveryButtonHeld) { recoveryFirmwareMode = true; - LOG_INF("MAIN", "Recovery firmware mode (UP + POWER held at boot)"); + LOG_INF("MAIN", "Recovery firmware mode (%s + POWER held at boot)", BoardConfig::isX4Pro() ? "DOWN" : "UP"); } } // First serial output only here to avoid timing inconsistencies for power button press duration verification LOG_DBG("MAIN", "Starting CrossInk version " CROSSINK_VERSION); + logMemoryStats("Boot"); // Resolve the single boot-presentation decision. Skipping the splash also // skips the panel-clearing pass and the X3 initial-full-sync arming (see // HalDisplay::begin), so the first paint is FAST_REFRESH (~500ms) over the // retained frame and input dispatches against a visible UI. - const BootResume resume = isSilentReboot ? BootResume::Silent + const BootResume resume = isNetworkResume ? BootResume::Network + : isSilentReboot ? BootResume::Silent : !APP_STATE.showBootScreen ? BootResume::QuickResume : BootResume::Splash; + bool allowFastInitialReaderRefresh = false; - setupDisplayAndFonts(resume != BootResume::Splash); + setupDisplayAndFonts(resume != BootResume::Splash, resume != BootResume::Network); + logBootHeap("display and selected fonts ready"); switch (resume) { case BootResume::Silent: // Splash skipped: the routing block below picks the target activity; the // panel keeps showing the pre-reboot popup until that first paint lands. break; + case BootResume::Network: + LOG_INF("BOOT", "Minimal network boot ready: target=%lu free=%u maxAlloc=%u", + static_cast(snapshotTarget), ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + break; case BootResume::QuickResume: // One-shot flag: re-arm the splash for the next non-quick-resume boot. Save // before any painting so a hang in the blocking paint path can't strand @@ -819,7 +960,13 @@ void setup() { APP_STATE.showBootScreen = true; APP_STATE.saveToFile(); if (loadSleepFrameBuffer()) { - // Frame restored: swap the sleep moon for the loading icon. + const bool useDifferentialRefresh = gpio.deviceIsX3(); + if (useDifferentialRefresh) { + // begin() clears the X3 controller RAM, so restore the saved frame as + // the baseline before replacing the moon with the loading icon. + renderer.cleanupGrayscaleWithFrameBuffer(); + } + const auto pageHeight = renderer.getScreenHeight(); if (SETTINGS.readerDarkMode != 0) { renderer.drawImageInverted(LoadingIcon, 0, pageHeight - LOADINGICON_HEIGHT, LOADINGICON_WIDTH, @@ -827,7 +974,12 @@ void setup() { } else { renderer.drawImage(LoadingIcon, 0, pageHeight - LOADINGICON_HEIGHT, LOADINGICON_WIDTH, LOADINGICON_HEIGHT); } - renderer.displayBuffer(HalDisplay::HALF_REFRESH); + if (useDifferentialRefresh) { + renderer.displayGrayscaleBase(HalDisplay::FAST_REFRESH); + allowFastInitialReaderRefresh = true; + } else { + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + } } else { activityManager.goToBoot(); // frame file missing, fall back to the splash } @@ -844,6 +996,58 @@ void setup() { } else if (HalSystem::isRebootFromPanic()) { // If we rebooted from a panic, go to crash report screen to show the panic info activityManager.goToCrashReport(); + } else if (resume == BootResume::Network) { + bool launched = false; + switch (static_cast(snapshotTarget)) { + case NetworkBootTarget::OTA: { + auto otaActivity = makeUniqueNoThrow(renderer, mappedInputManager); + if (otaActivity) { + activityManager.replaceActivity(std::move(otaActivity)); + launched = true; + } else { + LOG_ERR("MAIN", "OOM: OTA activity after minimal boot (free=%u maxAlloc=%u)", ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + } + break; + } + case NetworkBootTarget::OPDS: + launched = activityManager.goToOpdsServer(snapshotPayload, true); + break; + case NetworkBootTarget::KOREADER_SYNC: + launched = startGlobalSyncProgress(true); + break; + case NetworkBootTarget::KOREADER_AUTH: { + const auto mode = + snapshotPayload == 1 ? KOReaderAuthActivity::Mode::SIGN_UP : KOReaderAuthActivity::Mode::AUTHENTICATE; + auto authActivity = makeUniqueNoThrow(renderer, mappedInputManager, mode); + if (authActivity) { + activityManager.replaceActivity(std::move(authActivity)); + launched = true; + } else { + LOG_ERR("MAIN", "OOM: KOReader auth activity after minimal boot (free=%u maxAlloc=%u)", ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + } + break; + } + case NetworkBootTarget::FILE_TRANSFER: + launched = activityManager.resumeFileTransferFromNetworkBoot(snapshotPayload); + break; + case NetworkBootTarget::MANAGE_FONTS: { + auto fontsActivity = makeUniqueNoThrow(renderer, mappedInputManager); + if (fontsActivity) { + activityManager.replaceActivity(std::move(fontsActivity)); + launched = true; + } else { + LOG_ERR("MAIN", "OOM: Manage Fonts activity after minimal boot (free=%u maxAlloc=%u)", ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + } + break; + } + } + if (!launched) { + LOG_ERR("MAIN", "Minimal network boot target failed; returning home"); + silentRestart(); + } } else if (resume == BootResume::Silent && snapshotTarget == SILENT_REBOOT_TARGET_READER && !APP_STATE.openEpubPath.empty()) { activityManager.goToReader(APP_STATE.openEpubPath); @@ -851,7 +1055,7 @@ void setup() { // target == home (or reader with no open book): land on home — don't fall // through to the sleep-wake "resume reader" logic, which fires on stale // openEpubPath + lastSleepFromReader from a prior session. - activityManager.goHome(); + activityManager.goHome(HomeMenuItem::NONE, true); } else if (APP_STATE.openEpubPath.empty() || !APP_STATE.lastSleepFromReader || mappedInputManager.isPressed(MappedInputManager::Button::Back) || APP_STATE.readerActivityLoadCount > 0) { // Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity @@ -863,10 +1067,10 @@ void setup() { APP_STATE.openEpubPath = ""; APP_STATE.readerActivityLoadCount++; APP_STATE.saveToFile(); - activityManager.goToReader(path); + activityManager.goToReader(path, false, allowFastInitialReaderRefresh); } - if (resume == BootResume::Silent) { + if (resume == BootResume::Silent || resume == BootResume::Network) { // Block until the first paint physically completes. refreshDisplay() // waits on the panel BUSY pin so when this returns the user can see the // new activity. Without the wait, an edge captured by gpio.update() @@ -894,14 +1098,16 @@ void loop() { static unsigned long lastMemPrint = 0; gpio.update(); +#ifdef SIMULATOR + simulatorHomeKeyInput.update(); +#endif halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.tiltPageTurnDirection, SETTINGS.orientation, activityManager.isReaderActivity()); renderer.setFadingFix(SETTINGS.fadingFix); if (Serial && millis() - lastMemPrint >= 10000) { - LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(), - ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); + logMemoryStats("Periodic"); lastMemPrint = millis(); } @@ -916,15 +1122,19 @@ void loop() { // Check for any user activity (button press or release) or active background work static unsigned long lastActivityTime = millis(); - if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() || - activityManager.preventAutoSleep()) { + if (gpio.wasAnyPressed() || gpio.wasAnyReleased() +#if CROSSINK_APP_CAP_TOUCH + || gpio.wasTouchActivity() +#endif + || halTiltSensor.hadActivity() || activityManager.preventAutoSleep()) { lastActivityTime = millis(); // Reset inactivity timer powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity } static bool screenshotButtonsReleased = true; static bool screenshotComboActive = false; - if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.isPressed(HalGPIO::BTN_DOWN)) { + if (!activityManager.readerPowerButtonOpensSettings() && gpio.isPressed(HalGPIO::BTN_POWER) && + gpio.isPressed(HalGPIO::BTN_DOWN)) { screenshotComboActive = true; if (screenshotButtonsReleased) { screenshotButtonsReleased = false; @@ -955,6 +1165,11 @@ void loop() { return; } #endif + // X4 Pro-only frontlight shortcut. Consume the second release so a configured + // short-power action does not also run for the click that toggled the light. + if (handleX4ProFrontlightDoubleClick()) { + return; + } const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs(); if (sleepTimeoutMs > 0 && millis() - lastActivityTime >= sleepTimeoutMs) { @@ -967,7 +1182,14 @@ void loop() { return; } - if (millis() >= allowSleepAt && handleGlobalPowerButtonAction(getPowerButtonAction())) { + // Do not feed the wake gesture into getPowerButtonAction(). In particular, + // the release edge can otherwise run the configured short/long Power action + // in the same loop that arms the post-wake guard. + if (!powerButtonReleasedSinceWake) { + if (!gpio.isPressed(HalGPIO::BTN_POWER)) { + powerButtonReleasedSinceWake = true; + } + } else if (millis() >= allowSleepAt && handleGlobalPowerButtonAction(getPowerButtonAction())) { lastActivityTime = millis(); return; } @@ -978,6 +1200,23 @@ void loop() { activityManager.requestUpdate(); } + // While on external power the percent climbs with no user interaction to + // repaint it (gauge boards like the X4 Pro report SoC continuously), so poll + // for a change once a minute. Off-charger the percent moves too slowly to + // justify unsolicited e-ink refreshes. + if (gpio.isUsbConnected()) { + static unsigned long lastBatteryPollTime = 0UL; + static uint16_t lastBatteryPercent = 0xFFFF; + if (millis() - lastBatteryPollTime >= 60000UL) { + lastBatteryPollTime = millis(); + const uint16_t percent = powerManager.getBatteryPercentage(); + if (lastBatteryPercent != 0xFFFF && percent != lastBatteryPercent) { + activityManager.requestUpdate(); + } + lastBatteryPercent = percent; + } + } + const unsigned long activityStartTime = millis(); activityManager.loop(); const unsigned long activityDuration = millis() - activityStartTime; diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 7437af53ca..078f88af8c 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -1,6 +1,7 @@ #include "CrossPointWebServer.h" #include +#include #ifdef SIMULATOR #include #endif @@ -9,15 +10,17 @@ #include #include #include +#include #include #include #include -#include #include #include +#include #include +#include "AppCapabilities.h" #include "AppVersion.h" #include "CrossPointSettings.h" #include "FontInstaller.h" @@ -68,6 +71,116 @@ uint8_t enumRawValueForDisplayIndex(const SettingInfo& setting, uint8_t displayI return setting.enumRawValues[displayIndex]; } +bool isWebSettingAvailable(const SettingInfo& setting) { +#if !CROSSINK_APP_CAP_TOUCH + if (setting.nameId == StrId::STR_TOUCH_READER_CONTROLS || setting.nameId == StrId::STR_DISABLE_TOUCHSCREEN) { + return false; + } +#endif + +#if !FREEINK_CAP_FRONTLIGHT + if (setting.nameId == StrId::STR_BRIGHTNESS || setting.nameId == StrId::STR_WARMTH || + setting.nameId == StrId::STR_FRONTLIGHT) { + return false; + } +#endif + + if (!halClock.isAvailable()) { + switch (setting.nameId) { + case StrId::STR_HIDE_CLOCK: + case StrId::STR_AUTO_BACKUP_STATS: + case StrId::STR_CLOCK_UTC_OFFSET: + case StrId::STR_CLOCK_FORMAT: + case StrId::STR_DATE_FORMAT: + case StrId::STR_DATE_SEPARATOR: + case StrId::STR_CLOCK_SYNCED: + return false; + default: + break; + } + } + + return true; +} + +// Streams a font-catalog JSON response in bounded pieces. This avoids holding +// both an ArduinoJson document and its serialized String in the fragmented +// network heap, and gives WiFi a chance to drain each piece before the next. +class FontListJsonWriter { + public: + explicit FontListJsonWriter(WebServer& server) : server_(server) {} + + void append(const char* text) { append(text, strlen(text)); } + + void append(const char* text, size_t textLength) { + while (textLength > 0) { + if (length_ == sizeof(buffer_)) flush(); + const size_t copyLength = std::min(textLength, sizeof(buffer_) - length_); + memcpy(buffer_ + length_, text, copyLength); + length_ += copyLength; + text += copyLength; + textLength -= copyLength; + } + } + + void appendUnsigned(unsigned long value) { + char number[16]; + const int length = snprintf(number, sizeof(number), "%lu", value); + append(number, static_cast(length)); + } + + void appendJsonString(const char* value) { + append("\""); + for (const unsigned char* p = reinterpret_cast(value); *p; ++p) { + switch (*p) { + case '\"': + append("\\\""); + break; + case '\\': + append("\\\\"); + break; + case '\b': + append("\\b"); + break; + case '\f': + append("\\f"); + break; + case '\n': + append("\\n"); + break; + case '\r': + append("\\r"); + break; + case '\t': + append("\\t"); + break; + default: + if (*p < 0x20) { + static constexpr char kHexDigits[] = "0123456789ABCDEF"; + const char escaped[] = {'\\', 'u', '0', '0', kHexDigits[*p >> 4], kHexDigits[*p & 0x0F]}; + append(escaped, sizeof(escaped)); + } else { + append(reinterpret_cast(p), 1); + } + break; + } + } + append("\""); + } + + void flush() { + if (length_ == 0) return; + server_.sendContent(buffer_, length_); + length_ = 0; + } + + private: + static constexpr size_t BUFFER_SIZE = 192; + WebServer& server_; + char buffer_[BUFFER_SIZE]; + size_t length_ = 0; +}; + // WebSocket upload state HalFile wsUploadFile; String wsUploadFileName; @@ -101,8 +214,11 @@ String normalizeWebPath(const String& inputPath) { } bool isProtectedPath(const String& path) { - // Check every segment of the path, not just the last one. - // This prevents access to e.g. /.hidden/somefile or /System Volume Information/foo + // Hidden/system items stay out of the default file-manager view. Enabling + // Show Hidden Files intentionally makes them fully manageable. + if (SETTINGS.showHiddenFiles) return false; + + // Check every segment so a hidden parent also protects its descendants. int start = 0; while (start < (int)path.length()) { if (path.charAt(start) == '/') { @@ -114,7 +230,7 @@ bool isProtectedPath(const String& path) { String segment = path.substring(start, end); - if (!SETTINGS.showHiddenFiles && segment.startsWith(".")) return true; + if (segment.startsWith(".")) return true; for (const auto* item : HIDDEN_ITEMS) { if (segment.equals(item)) return true; @@ -170,15 +286,17 @@ void CrossPointWebServer::begin() { // Note: WebServer class doesn't have setNoDelay() in the standard ESP32 library. // We rely on disabling WiFi sleep for responsiveness. - LOG_DBG("WEB", "[MEM] Free heap after WebServer allocation: %d bytes", ESP.getFreeHeap()); - if (!server) { LOG_ERR("WEB", "Failed to create WebServer!"); return; } + // Add Access-Control-Allow-* headers to every response so web-based clients + // and PWAs on other origins can use the HTTP API. Preflight OPTIONS requests + // are answered in handleNotFound(). + server->enableCORS(true); + // Setup routes - LOG_DBG("WEB", "Setting up routes..."); server->on("/", HTTP_GET, [this] { handleRoot(); }); server->on("/files", HTTP_GET, [this] { handleFileList(); }); server->on("/js/jszip.min.js", HTTP_GET, [this] { handleJszip(); }); @@ -226,34 +344,32 @@ void CrossPointWebServer::begin() { server->on("/api/wifi/delete", HTTP_POST, [this] { handleDeleteWifiNetwork(); }); server->onNotFound([this] { handleNotFound(); }); - LOG_DBG("WEB", "[MEM] Free heap after route setup: %d bytes", ESP.getFreeHeap()); // Collect WebDAV headers and register handler const char* davHeaders[] = {"Depth", "Destination", "Overwrite", "If", "Lock-Token", "Timeout"}; server->collectHeaders(davHeaders, 6); server->addHandler(new WebDAVHandler()); // Note: WebDAVHandler will be deleted by WebServer when server is stopped - LOG_DBG("WEB", "WebDAV handler initialized"); server->begin(); // Start WebSocket server for fast binary uploads - LOG_DBG("WEB", "Starting WebSocket server on port %d...", wsPort); wsServer.reset(new WebSocketsServer(wsPort)); wsInstance = const_cast(this); wsServer->begin(); wsServer->onEvent(wsEventCallback); - LOG_DBG("WEB", "WebSocket server started"); udpActive = udp.begin(LOCAL_UDP_PORT); LOG_DBG("WEB", "Discovery UDP %s on port %d", udpActive ? "enabled" : "failed", LOCAL_UDP_PORT); + // Do not subscribe the serving task to the task watchdog. Arduino WebServer + // permits five-second client and ACK waits, which can consume the entire + // default watchdog window on a weak connection even while the CPU idle task + // is healthy. The system idle-task watchdog still detects real CPU stalls. + running = true; - LOG_DBG("WEB", "Web server started on port %d", port); // Show the correct IP based on network mode const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString(); - LOG_DBG("WEB", "Access at http://%s/", ipAddr.c_str()); - LOG_DBG("WEB", "WebSocket at ws://%s:%d/", ipAddr.c_str(), wsPort); LOG_DBG("WEB", "[MEM] Free heap after server.begin(): %d bytes", ESP.getFreeHeap()); } @@ -279,7 +395,6 @@ void CrossPointWebServer::stop() { return; } - LOG_DBG("WEB", "STOP INITIATED - setting running=false first"); running = false; // Set this FIRST to prevent handleClient from using server LOG_DBG("WEB", "[MEM] Free heap before stop: %d bytes", ESP.getFreeHeap()); @@ -291,11 +406,9 @@ void CrossPointWebServer::stop() { // Stop WebSocket server if (wsServer) { - LOG_DBG("WEB", "Stopping WebSocket server..."); wsServer->close(); wsServer.reset(); wsInstance = nullptr; - LOG_DBG("WEB", "WebSocket server stopped"); } if (udpActive) { @@ -307,14 +420,11 @@ void CrossPointWebServer::stop() { delay(20); server->stop(); - LOG_DBG("WEB", "[MEM] Free heap after server->stop(): %d bytes", ESP.getFreeHeap()); // Brief delay before deletion delay(10); server.reset(); - LOG_DBG("WEB", "Web server stopped and deleted"); - LOG_DBG("WEB", "[MEM] Free heap after delete server: %d bytes", ESP.getFreeHeap()); // Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared // later in the file and will be cleared when they go out of scope or on next upload @@ -388,15 +498,11 @@ static void sendHtmlContent(WebServer* server, const char* data, size_t len) { server->send_P(200, "text/html", data, len); } -void CrossPointWebServer::handleRoot() const { - sendHtmlContent(server.get(), HomePageHtml, sizeof(HomePageHtml)); - LOG_DBG("WEB", "Served root page"); -} +void CrossPointWebServer::handleRoot() const { sendHtmlContent(server.get(), HomePageHtml, sizeof(HomePageHtml)); } void CrossPointWebServer::handleJszip() const { server->sendHeader("Content-Encoding", "gzip"); server->send_P(200, "application/javascript", jszip_minJs, jszip_minJsCompressedSize); - LOG_DBG("WEB", "Served jszip.min.js"); } // Shared stylesheet and logo are referenced with a content-hashed ?v= query, @@ -405,17 +511,31 @@ void CrossPointWebServer::handleStyleCss() const { server->sendHeader("Content-Encoding", "gzip"); server->sendHeader("Cache-Control", "public, max-age=31536000, immutable"); server->send_P(200, "text/css", StyleCss, StyleCssCompressedSize); - LOG_DBG("WEB", "Served style.css"); } void CrossPointWebServer::handleLogo() const { // Raw PNG (already compressed); no Content-Encoding. server->sendHeader("Cache-Control", "public, max-age=31536000, immutable"); server->send_P(200, "image/png", LogoPng, LogoPngSize); - LOG_DBG("WEB", "Served logo.png"); } void CrossPointWebServer::handleNotFound() const { + // CORS preflight: routes are registered per-method, so OPTIONS requests land + // here. The Access-Control-Allow-* headers are added by enableCORS(). + if (server->method() == HTTP_OPTIONS) { + server->send(204, "text/plain", ""); + return; + } + + // in AP mode, redirect unmatched browser/captive-portal requests to "/" so the OS auto-opens the browser + // API requests (/api/*) still return 404 so XHR errors surface correctly + // see https://en.wikipedia.org/wiki/Captive_portal#Detection + if (apMode && !server->uri().startsWith("/api/")) { + server->sendHeader("Location", "/", true); + server->send(302, "text/plain", ""); + return; + } + String message = "404 Not Found\n\n"; message += "URI: " + server->uri() + "\n"; server->send(404, "text/plain", message); @@ -432,10 +552,20 @@ void CrossPointWebServer::handleStatus() const { doc["rssi"] = apMode ? 0 : WiFi.RSSI(); doc["freeHeap"] = ESP.getFreeHeap(); doc["uptime"] = millis() / 1000; +#if FREEINK_DEVICE_X4 || FREEINK_DEVICE_X3 doc["device"] = gpio.deviceIsX3() ? "X3" : "X4"; +#else +#ifdef SIMULATOR + doc["device"] = "Simulator"; +#else + doc["device"] = BoardConfig::ACTIVE.name; +#endif +#endif char snBuf[33] = {0}; bool valid = false; +#if !CONFIG_IDF_TARGET_ESP32 + // Classic ESP32's efuse table has no USER_DATA block (C3/S3 only) if (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA, snBuf, 256) == ESP_OK) { valid = snBuf[0] != '\0' && snBuf[0] != (char)0xFF; for (int i = 0; i < 32 && snBuf[i] != '\0'; i++) { @@ -445,6 +575,7 @@ void CrossPointWebServer::handleStatus() const { } } } +#endif if (valid) { doc["serial"] = snBuf; @@ -457,7 +588,7 @@ void CrossPointWebServer::handleStatus() const { server->send(200, "application/json", response); } -void CrossPointWebServer::scanFiles(const char* path, const std::function& callback) const { +void CrossPointWebServer::scanFiles(const char* path, const FileVisitor visitor, void* context) const { HalFile root = Storage.open(path); if (!root) { LOG_DBG("WEB", "Failed to open directory: %s", path); @@ -470,8 +601,6 @@ void CrossPointWebServer::scanFiles(const char* path, const std::function