Compare commits

...
31 Commits
Author SHA1 Message Date
Dave Allie 5d68c8b305 Cut release 0.6.0
Compile Release / build-release (push) Canceled after 0s
2025-12-16 23:15:47 +11:00
Jonas Diemer def7abbd60 Improve indent (#28)
* add horizontal indent in first line of paragraph in case Extra Paragraph Spacing is OFF

* Treat tabs as whitespace (so they are properly stripped)

* Changed size of indent to 1 em.

* Fixed calculation of space when indenting (avoiding squeezed text).

* Source code formatting
2025-12-16 23:02:32 +11:00
Jonas Diemer 9ad8111ce7 Wrap-around navigation in Settings. (#31) 2025-12-16 22:54:16 +11:00
Arthur Tazhitdinov 57d1939be7 Add Cyrillic range to fonts (#27)
* Enhance TOC parsing and chapter selection logic

- Update .gitignore to include additional paths
- Refactor Epub::parseContentOpf to improve NCX item retrieval
- Modify ContentOpfParser to store media type in ManifestItem
- Implement rebuildVisibleSpineIndices in EpubReaderChapterSelectionScreen for better chapter navigation
- Adjust rendering logic to handle empty chapter lists gracefully

* Refactor TOC parsing logic to streamline cover image and NCX item retrieval

* add cyrillic ranges

* revert

* clang format fix
2025-12-16 22:52:49 +11:00
Jonas Diemer 012992f904 Feature/auto poweroff (#32)
* Automatically deep sleep after 10 minutes of inactivity.

* formatting.
2025-12-16 22:49:31 +11:00
Dave Allie c262f222de Parse cover image path from content.opf file (#24) 2025-12-16 03:15:54 +11:00
Dave Allie 449b3ca161 Fixed light gray text rendering 2025-12-16 02:16:38 +11:00
Dave Allie 6989035ef8 Run CI action on PR as well as push 2025-12-15 23:17:35 +11:00
Dave Allie 108cf57202 Fix formatting 2025-12-15 23:17:23 +11:00
Jonas Diemer a640fbecf8 Settings Screen and first 2 settings (#18)
* white sleep screen

* quicker pwr button

* no extra spacing between paragraphs

* Added settings class with de/serialization and whiteSleepScreen setting to control inverting the sleep screen

* Added Settings screen for real, made settings a global singleton

* Added setting for extra paragraph spacing.

* fixed typo

* Rework after feedback

* Fixed type from bool to uint8
2025-12-15 23:16:46 +11:00
Dave Allie 7a5719b46d Upgrade open-x4-sdk to fix white streaks on sleep screen (#21)
https://github.com/open-x4-epaper/community-sdk/pull/6 fixes a power down issue with the display which was causing streaks to appear on the sleep screen
2025-12-15 22:27:27 +11:00
Dave Allie 8c3576e397 Add Github Action to build release firmware on tag (#20) 2025-12-15 20:00:34 +11:00
Dave Allie fdb5634ea6 Add cppcheck and formatter to CI (#19)
* Add cppcheck and formatter to CI

* Checkout submodules

* Install matching clang-format version in CI
2025-12-15 19:46:52 +11:00
Dave Allie 5cabba7712 Add Github templates
Added Github templates for PRs and bugs
Also added one for funding, please do not feel obligated to
donate to the project, this is not a for-profit endevour, I
just had someone ask where they could send a few dollars so
I have set it up
2025-12-15 08:16:59 +11:00
Dave Allie a86d405fb0 Add comparison images 2025-12-14 13:46:15 +11:00
Dave Allie e4b5dc0e6a Update contribution instructions 2025-12-14 13:46:03 +11:00
Dave Allie dfc74f94c2 Cut release 0.5.1 2025-12-13 21:52:48 +11:00
Dave Allie 3518cbb56d Add user guide 2025-12-13 21:52:03 +11:00
Dave Allie 8994953254 Add chapter selection screen 2025-12-13 21:17:34 +11:00
Dave Allie ead39fd04b Return -1 from getTocIndexForSpineIndex if TOC item does not exist 2025-12-13 21:17:22 +11:00
Dave Allie 5a7381a0eb Cut release 0.5.0 2025-12-13 20:16:12 +11:00
Dave Allie f69fc90b5c Show end of book screen when navigating past last page 2025-12-13 20:10:38 +11:00
Dave Allie 5bae283838 Process lines into pages as they are built 2025-12-13 20:10:16 +11:00
Dave Allie c7a32fe41f Remove tinyxml2 dependency replace with expat parsers (#9) 2025-12-13 19:36:01 +11:00
Dave Allie d450f362d1 Cut release 0.4.0 2025-12-13 17:15:06 +11:00
Dave Allie 6ddcf9b592 Show clearer indexing string 2025-12-13 16:02:27 +11:00
Dave Allie 492c6fd23e Bump page file version 2025-12-13 00:42:17 +11:00
Dave Allie 7c852cf7d1 Swap out babyblue font for pixelarial14 2025-12-13 00:16:10 +11:00
Dave Allie 69f357998e Move to smart pointers and split out ParsedText class (#6)
* Move to smart pointers and split out ParsedText class

* Cleanup ParsedText

* Fix clearCache functions and clear section cache if page load fails

* Bump Page and Section file versions

* Combine removeDir implementations in Epub

* Adjust screen margins
2025-12-12 22:13:34 +11:00
Dave Allie 09f68a3d03 Restructure readme 2025-12-09 00:07:38 +11:00
Dave Allie 7ec7efcb47 Add section in readme on flashing via xteink.dve.al 2025-12-09 00:04:55 +11:00
64 changed files with 21194 additions and 11706 deletions
+2
View File
@@ -0,0 +1,2 @@
CompileFlags:
Add: [-std=c++2a]
+2
View File
@@ -0,0 +1,2 @@
github: [daveallie]
ko_fi: daveallie
+54
View File
@@ -0,0 +1,54 @@
name: Bug Report
description: Report an issue or unexpected behavior
title: "Short, descriptive title of the issue"
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report this bug! Please fill out the details below.
- type: input
id: version
attributes:
label: Affected Version
description: What version of the project/library are you using? (e.g., v1.2.3, master branch commit SHA)
placeholder: Ex. v1.2.3
validations:
required: true
- type: textarea
id: bug-description
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is.
placeholder:
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
description: Clearly list the steps necessary to reproduce the unexpected behavior.
placeholder: |
1. Go to '...'
2. Select '...'
3. Crash
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Log Output/Screenshots
description: If applicable, error messages, or log output to help explain your problem. You can drag and drop images here.
render: shell
+9
View File
@@ -0,0 +1,9 @@
## Summary
* **What is the goal of this PR?** (e.g., Fixes a bug in the user authentication module, Implements the new feature for
file uploading.)
* **What changes are included?**
## Additional Context
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on).
+43
View File
@@ -0,0 +1,43 @@
name: CI
'on':
push:
branches: [master]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/cache@v5
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install PlatformIO Core
run: pip install --upgrade platformio
- name: Install clang-format-21
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 21
sudo apt-get update
sudo apt-get install -y clang-format-21
- name: Run cppcheck
run: pio check --fail-on-defect medium --fail-on-defect high
- name: Run clang-format
run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
- name: Build CrossPoint
run: pio run
+40
View File
@@ -0,0 +1,40 @@
name: Compile Release
on:
push:
tags:
- '*'
jobs:
build-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/cache@v5
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install PlatformIO Core
run: pip install --upgrade platformio
- name: Build CrossPoint
run: pio run -e gh_release
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: CrossPoint-${{ github.ref_name }}
path: |
.pio/build/gh_release/bootloader.bin
.pio/build/gh_release/firmware.bin
.pio/build/gh_release/firmware.elf
.pio/build/gh_release/firmware.map
.pio/build/gh_release/partitions.bin
+2
View File
@@ -1,3 +1,5 @@
.pio
.idea
.DS_Store
.vscode
lib/EpdFont/fontsrc
+32 -14
View File
@@ -6,7 +6,7 @@ Built using **PlatformIO** and targeting the **ESP32-C3** microcontroller.
CrossPoint Reader is a purpose-built firmware designed to be a drop-in, fully open-source replacement for the official
Xteink firmware. It aims to match or improve upon the standard EPUB reading experience.
![](./docs/cover.jpg)
![](./docs/images/cover.jpg)
## Motivation
@@ -36,7 +36,34 @@ This project is **not affiliated with Xteink**; it's built as a community projec
- [ ] WiFi connectivity
- [ ] BLE connectivity
## Getting Started
## Installing
### Web (latest firmware)
1. Connect your Xteink X4 to your computer via USB-C
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
### Web (specific firmware version)
1. Connect your Xteink X4 to your computer via USB-C
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
### Manual
See [Development](#development) below.
## Usage
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
## Development
### Prerequisites
@@ -58,24 +85,12 @@ git submodule update --init --recursive
### Flashing your device
#### Command line
Connect your Xteink X4 to your computer via USB-C and run the following command.
```sh
pio run --target upload
```
#### Web
1. Connect your Xteink X4 to your computer via USB-C
2. Download the `firmware.bin` file from the latest release via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
4. Press the reset button on the Xteink X4 to restart the device
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
## Internals
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only
@@ -116,6 +131,9 @@ EPUB file will reset the reading progress.
Contributions are very welcome!
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/daveallie/crosspoint-reader/discussions/categories/ideas).
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
### To submit a contribution:
1. Fork the repo
+78
View File
@@ -0,0 +1,78 @@
# CrossPoint User Guide
Welcome to the **CrossPoint** firmware. This guide outlines the hardware controls, navigation, and reading features of
the device.
## 1. Hardware Overview
The device utilises the standard buttons on the Xtink X4 in the same layout:
### Button Layout
| Location | Buttons |
|-----------------|--------------------------------------------|
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
| **Right Side** | **Power**, **Volume Up**, **Volume Down** |
---
## 2. Power & Startup
### Power On / Off
To turn the device on or off, **press and hold the Power button for 1 full second**.
### First Launch
Upon turning the device on for the first time, you will be placed on the **Book Selection Screen** (File Browser).
> **Note:** On subsequent restarts, the firmware will automatically reopen the last book you were reading.
---
## 3. Book Selection
The Home Screen acts as a folder and file browser.
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up
and down through folders and books.
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
---
## 4. Reading Mode
Once you have opened a book, the button layout changes to facilitate reading.
### Page Turning
| Action | Buttons |
|-------------------|--------------------------------------|
| **Previous Page** | Press **Left** _or_ **Volume Up** |
| **Next Page** | Press **Right** _or_ **Volume Down** |
### Chapter Navigation
* **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.
### System Navigation
* **Return to Home:** Press **Back** to close the book and return to the Book Selection screen.
* **Chapter Menu:** Press **Confirm** to open the Table of Contents/Chapter Selection screen.
---
## 5. Chapter Selection Screen
Accessible by pressing **Confirm** while inside a book.
1. Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to highlight the desired chapter.
2. Press **Confirm** to jump to that chapter.
3. *Alternatively, press **Back** to cancel and return to your current page.*
---
## 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:
* **Images:** Embedded images in e-books will not render.
* **Text Formatting:** There are currently no settings to adjust font type, size, line spacing, or margins.
+19
View File
@@ -0,0 +1,19 @@
# CrossPoint vs XTOS
Below is like for like comparison of CrossPoint (version 0.5.1) and XTOS (version 3.1.1). CrossPoint is on the left,
XTOS is on the right. CrossPoint does not currently support all features of XTOS, so this comparison is just of key
features which both firmwares support.
## EPUB reading
![](./images/comparison/reading-1.jpg)
![](./images/comparison/reading-2.jpg)
![](./images/comparison/reading-3.jpg)
## Menus
![](./images/comparison/menu.jpg)
![](./images/comparison/chapter-menu.jpg)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+589 -73
View File
@@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t ubuntu_bold_10Bitmaps[8017] = {
static const uint8_t ubuntu_bold_10Bitmaps[13720] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0xEF, 0xEF, 0xE7, 0x1E, 0xF0, 0xF7, 0x87, 0x3C,
0x39, 0xCF, 0xFF, 0xFF, 0xFC, 0xF7, 0x87, 0xBC, 0x39, 0xE7, 0xFF, 0xFF, 0xFE, 0xF7, 0x87, 0xBC, 0x39, 0xE1, 0xCE,
0x00, 0x0F, 0x01, 0xE0, 0xFF, 0xBF, 0xF7, 0xFC, 0xFB, 0x9E, 0x03, 0xF8, 0x3F, 0xC3, 0xFC, 0x1F, 0x80, 0xF7, 0xBE,
@@ -400,36 +400,337 @@ static const uint8_t ubuntu_bold_10Bitmaps[8017] = {
0x80, 0xF8, 0x07, 0x80, 0x7C, 0x07, 0xC0, 0x3C, 0x03, 0xE0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x33, 0x0F, 0xE3,
0xF0, 0x7C, 0x0E, 0x00, 0x07, 0xFD, 0xFF, 0x7F, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1F, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0,
0x3F, 0x3F, 0xBF, 0xDF, 0x6F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0x7E,
0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFE, 0x3B, 0xDE, 0xEF, 0x78, 0x7B, 0xDF, 0xEF, 0x70, 0x7B, 0xDF, 0xEF, 0x70, 0x3B, 0xDE, 0xE7,
0xF9, 0xDE, 0xF7, 0xBD, 0xE0, 0x7F, 0xBF, 0xDD, 0xFE, 0xFF, 0x77, 0x38, 0x7F, 0xBF, 0xDD, 0xFE, 0xFF, 0x77, 0x38,
0x1E, 0x07, 0x81, 0xE0, 0x78, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x03, 0x80, 0xE0,
0x38, 0x0E, 0x03, 0x80, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x1E, 0x07, 0x8F, 0xFF, 0xFF,
0xFF, 0xC7, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0x7C, 0xFF, 0xFF, 0xF7, 0xEF, 0x80, 0xF0, 0xE1, 0xFF, 0x3E, 0x7F,
0xE7, 0xCF, 0xF8, 0x70, 0xF0, 0x3E, 0x1E, 0x00, 0x0F, 0xE3, 0xC0, 0x01, 0xDC, 0xF0, 0x00, 0x7B, 0xDE, 0x00, 0x0F,
0x7F, 0x80, 0x00, 0xEE, 0xF0, 0x00, 0x1F, 0xFC, 0x00, 0x01, 0xF7, 0xFE, 0x7E, 0x01, 0xFF, 0xCF, 0xE0, 0x3B, 0xFF,
0xFC, 0x0F, 0x73, 0xF3, 0x83, 0xCE, 0x7E, 0x70, 0x79, 0xFF, 0xFE, 0x1E, 0x3F, 0x9F, 0xC3, 0xC3, 0xF3, 0xF0, 0x00,
0x38, 0xF3, 0xE7, 0x9F, 0x1E, 0x3E, 0x3C, 0x38, 0x00, 0x00, 0xE3, 0xE3, 0xE3, 0xC7, 0xCF, 0x3E, 0xF8, 0xE0, 0x00,
0x01, 0xE0, 0x3C, 0x0F, 0x01, 0xE0, 0x78, 0x0F, 0x03, 0xC0, 0x78, 0x1E, 0x03, 0xC0, 0xF0, 0x1E, 0x07, 0x80, 0xF0,
0x3C, 0x00, 0x07, 0xF0, 0xFF, 0x1F, 0xF1, 0xF6, 0x3C, 0x07, 0xFE, 0x7F, 0xC3, 0xC0, 0x7F, 0xC7, 0xFC, 0x3C, 0x03,
0xF7, 0x1F, 0xF0, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0x0F, 0xC0, 0xFC, 0x1F, 0xC3, 0xF0, 0x3F,
0xC3, 0xFC, 0x1F, 0xC3, 0xF0, 0x3F, 0x03, 0xF0, 0x0F, 0x00, 0x3F, 0x87, 0xFC, 0x3F, 0xE3, 0xBE, 0xFF, 0xFF, 0xFF,
0x1F, 0x83, 0xF0, 0xFF, 0xFF, 0xFF, 0x78, 0x07, 0xDE, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0xFF, 0xFF, 0xFF, 0xC1, 0xF0,
0x07, 0xCF, 0xFF, 0x7F, 0xF8, 0x0F, 0x01, 0xF8, 0xFF, 0x87, 0xF8, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F,
0x00, 0x3F, 0x83, 0xFC, 0x3F, 0xE3, 0xBF, 0x00, 0xF1, 0xFF, 0x3F, 0xF7, 0xFF, 0x7D, 0xFF, 0x0F, 0xF0, 0xFF, 0xBE,
0x7F, 0xE7, 0xFC, 0x3F, 0x80, 0x07, 0x80, 0x0F, 0x80, 0x3F, 0x00, 0x7F, 0x01, 0xFE, 0x03, 0xDC, 0x07, 0xBC, 0x1E,
0x78, 0x3C, 0x78, 0x78, 0xF1, 0xE1, 0xE3, 0xC1, 0xE7, 0xFF, 0xDF, 0xFF, 0xBF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F,
0xC1, 0xFE, 0x0F, 0xF0, 0x78, 0xFF, 0xFF, 0xFF, 0xFF, 0xBC, 0x07, 0xC0, 0x7C, 0x07, 0x80, 0xF8, 0x0F, 0x83, 0xE0,
0x78, 0x1E, 0x07, 0xC0, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFC, 0x01, 0xE0, 0x3C, 0x0F, 0x01,
0xE0, 0x78, 0x0F, 0x03, 0xC0, 0x78, 0x1E, 0x03, 0xC0, 0xF0, 0x1E, 0x07, 0x80, 0xF0, 0x3C, 0x00, 0x77, 0xFE, 0xE0,
0x00, 0x3C, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x03, 0xC0, 0x0F, 0x0F, 0x38, 0xFD, 0xE3, 0xF7, 0x87, 0xFC,
0x07, 0xF0, 0x0F, 0xC0, 0x3E, 0x00, 0x78, 0x00, 0x7D, 0xF3, 0xFF, 0xFF, 0xFF, 0xEE, 0x7F, 0xFF, 0xBF, 0xFD, 0xF7,
0xC0, 0x00, 0x07, 0xC7, 0xE7, 0xF3, 0xE1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78,
0x7C, 0x7E, 0x3F, 0x1F, 0x00, 0x00, 0x3C, 0x77, 0xFF, 0x7F, 0xEF, 0xFE, 0xE3, 0xC0, 0x00, 0x3C, 0x77, 0xFF, 0x7F,
0xEF, 0xFE, 0xE3, 0xC0, 0x03, 0x80, 0xE0, 0x7B, 0xFF, 0xFF, 0xFF, 0xF0, 0xE0, 0x70, 0xFF, 0xFF, 0xFF, 0xFD, 0xC0,
0x70, 0x1C, 0x00, 0x07, 0xDF, 0xFF, 0xFF, 0xFE, 0xF8, 0x3F, 0xEF, 0xFD, 0xFF, 0x07, 0xC0, 0x0F, 0xFF, 0xFF, 0xFF,
0xC0, 0xF8, 0x3F, 0xCF, 0xFD, 0xFF, 0x0F, 0xDF, 0xFF, 0xFF, 0xFE, 0xF8, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0,
0x7F, 0xFF, 0x0C, 0x03, 0xC0, 0x78, 0x07, 0x80, 0x70, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F,
0xFD, 0xFF, 0xBF, 0xF7, 0x80, 0xF0, 0x1E, 0x03, 0xFF, 0xFF, 0xFF, 0xFE, 0x7B, 0xCF, 0x79, 0xEF, 0x00, 0x00, 0x01,
0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x7F, 0xEF, 0xFD, 0xFF, 0xBC, 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF,
0xFF, 0xF0, 0xFF, 0xF8, 0x7F, 0xFC, 0x3F, 0xFE, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x7F, 0xE0, 0x3F, 0xF8, 0x1F, 0xFE,
0x0F, 0x9F, 0x07, 0x87, 0xC3, 0xC1, 0xE1, 0xE3, 0xE0, 0xF7, 0xF0, 0x7B, 0xF8, 0x3D, 0xF8, 0x03, 0x00, 0xF0, 0x3E,
0x0F, 0x01, 0xC0, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0,
0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x00, 0x1F, 0xF1, 0xFF, 0xDF, 0xFD, 0xFD, 0xEF, 0x80, 0x78, 0x03, 0xFF, 0x1F, 0xF8,
0xFF, 0xC7, 0x80, 0x3E, 0x01, 0xFD, 0xE7, 0xFF, 0x1F, 0xFC, 0x7F, 0xC0, 0x1F, 0xE3, 0xFE, 0x7F, 0xE7, 0xDC, 0x78,
0x07, 0xE0, 0x7F, 0x83, 0xFE, 0x0F, 0xE0, 0x3F, 0x01, 0xF7, 0xBF, 0x7F, 0xFF, 0xFE, 0x7F, 0xC0, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF7, 0x7B, 0xFD, 0xC0, 0x03, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83,
0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x00, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03,
0xC0, 0xF0, 0x3D, 0x9F, 0xFF, 0xFF, 0xEF, 0xF0, 0x07, 0xFE, 0x00, 0x1F, 0xFC, 0x00, 0x3F, 0xF8, 0x00, 0x78, 0xF0,
0x00, 0xF1, 0xE0, 0x01, 0xE3, 0xFF, 0x03, 0xC7, 0xFF, 0x07, 0x8F, 0xFF, 0x0F, 0x1E, 0x7E, 0x3E, 0x3C, 0x3C, 0x78,
0x78, 0x7B, 0xF0, 0xF3, 0xFF, 0xE1, 0xFF, 0xFF, 0x83, 0xFF, 0xBC, 0x07, 0xFE, 0x00, 0xF0, 0x78, 0x07, 0x83, 0xC0,
0x3C, 0x1E, 0x01, 0xE0, 0xF0, 0x0F, 0x07, 0x80, 0x7F, 0xFF, 0xF3, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xF0, 0x79, 0xFF,
0x83, 0xC3, 0xFC, 0x1E, 0x1F, 0xE0, 0xF3, 0xFF, 0x07, 0xFF, 0xF8, 0x3F, 0xFB, 0xC1, 0xFF, 0x00, 0xFF, 0xF8, 0xFF,
0xF8, 0xFF, 0xF8, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0xFC, 0x0F, 0xFE, 0x0F, 0xFE, 0x0F, 0x3F, 0x0F, 0x0F,
0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x01, 0x80, 0x07, 0x00, 0x3C, 0x01, 0xE0, 0x07, 0x00, 0x00, 0x0F,
0x0F, 0xBC, 0x7C, 0xF3, 0xE3, 0xDF, 0x0F, 0xF8, 0x3F, 0xC0, 0xFE, 0x03, 0xFC, 0x0F, 0xF8, 0x3D, 0xF0, 0xF7, 0xC3,
0xCF, 0x8F, 0x1F, 0x3C, 0x3E, 0xF0, 0xF8, 0x06, 0x00, 0x3C, 0x00, 0xF8, 0x01, 0xE0, 0x01, 0x80, 0x00, 0x0F, 0x07,
0xFC, 0x1F, 0xF0, 0xFF, 0xC7, 0xFF, 0x3F, 0xFC, 0xFF, 0xF7, 0xBF, 0xFE, 0xFF, 0xF3, 0xFF, 0x8F, 0xFE, 0x3F, 0xF0,
0xFF, 0x83, 0xFE, 0x0F, 0xF0, 0x3C, 0x0C, 0xE0, 0x7B, 0x80, 0xFE, 0x01, 0xF0, 0x00, 0x03, 0xE1, 0xF7, 0x87, 0x9F,
0x1E, 0x3C, 0xF8, 0xFB, 0xC1, 0xEF, 0x07, 0xFC, 0x0F, 0xE0, 0x3F, 0x80, 0x7C, 0x01, 0xF0, 0x6F, 0x81, 0xFE, 0x07,
0xF0, 0x1F, 0x80, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83,
0xFC, 0x1F, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x07, 0xC0, 0x0F, 0x80,
0x3F, 0x80, 0x7F, 0x00, 0xFF, 0x03, 0xDE, 0x07, 0xBC, 0x1F, 0x3C, 0x3C, 0x78, 0x7F, 0xF1, 0xFF, 0xF3, 0xFF, 0xE7,
0x83, 0xFF, 0x07, 0xFC, 0x07, 0x80, 0xFF, 0xEF, 0xFE, 0xFF, 0xEF, 0x00, 0xF0, 0x0F, 0xFC, 0xFF, 0xEF, 0xFF, 0xF3,
0xFF, 0x0F, 0xF0, 0xFF, 0x3F, 0xFF, 0xFF, 0xFE, 0xFF, 0xC0, 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xE3, 0xEF, 0x1F, 0x7F,
0xF3, 0xFF, 0x9F, 0xFE, 0xF1, 0xF7, 0x87, 0xFC, 0x3F, 0xE7, 0xEF, 0xFF, 0x7F, 0xF3, 0xFF, 0x00, 0xFF, 0xFF, 0xFF,
0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x00, 0x0F,
0xFE, 0x07, 0xFF, 0x03, 0xFF, 0x81, 0xE3, 0xC0, 0xF1, 0xE0, 0x78, 0xF0, 0x3C, 0x78, 0x1E, 0x3C, 0x1F, 0x1E, 0x0F,
0x0F, 0x07, 0x87, 0x87, 0xC3, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0xF0, 0x07, 0xF8, 0x03, 0xFC,
0x01, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3F, 0xF7, 0xFE, 0xFF, 0xDE, 0x03, 0xC0, 0x78, 0x0F,
0xFF, 0xFF, 0xFF, 0xF8, 0xF8, 0xF0, 0xFB, 0xE7, 0x8F, 0x9F, 0xBC, 0xF8, 0x7D, 0xEF, 0x81, 0xFF, 0x7C, 0x07, 0xFF,
0xC0, 0x1F, 0xFC, 0x00, 0xFF, 0xC0, 0x0F, 0xFF, 0x00, 0xFF, 0xFC, 0x07, 0xFD, 0xF0, 0x7D, 0xE7, 0xC7, 0xCF, 0x3E,
0x3C, 0x78, 0xFB, 0xE3, 0xC3, 0xE0, 0x7F, 0xC7, 0xFE, 0x7F, 0xE7, 0xBF, 0x01, 0xF0, 0x3E, 0x1F, 0xE1, 0xFE, 0x1F,
0xF0, 0x3F, 0x00, 0xF7, 0xBF, 0x7F, 0xFF, 0xFE, 0x7F, 0xC0, 0xF0, 0x7F, 0xC1, 0xFF, 0x0F, 0xFC, 0x7F, 0xF3, 0xFF,
0xCF, 0xFF, 0x7B, 0xFF, 0xEF, 0xFF, 0x3F, 0xF8, 0xFF, 0xE3, 0xFF, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x03, 0xC0, 0x0C,
0xE0, 0x7F, 0x80, 0xFE, 0x03, 0xF0, 0x00, 0x03, 0xC1, 0xFF, 0x07, 0xFC, 0x3F, 0xF1, 0xFF, 0xCF, 0xFF, 0x3F, 0xFD,
0xEF, 0xFF, 0xBF, 0xFC, 0xFF, 0xE3, 0xFF, 0x8F, 0xFC, 0x3F, 0xE0, 0xFF, 0x83, 0xFC, 0x0F, 0xF0, 0xFB, 0xC7, 0xCF,
0x3E, 0x3D, 0xF0, 0xFF, 0x83, 0xFC, 0x0F, 0xE0, 0x3F, 0xC0, 0xFF, 0x83, 0xDF, 0x0F, 0x7C, 0x3C, 0xF8, 0xF1, 0xF3,
0xC3, 0xEF, 0x0F, 0x80, 0x07, 0xFE, 0x1F, 0xFC, 0x3F, 0xF8, 0x78, 0xF0, 0xF1, 0xE1, 0xE3, 0xC3, 0xC7, 0x87, 0x8F,
0x0F, 0x1E, 0x3E, 0x3C, 0x78, 0x7B, 0xF0, 0xFF, 0xE1, 0xFF, 0x83, 0xFC, 0x07, 0x80, 0x78, 0x0F, 0x3E, 0x0F, 0x9F,
0x07, 0xCF, 0xC7, 0xEF, 0xE3, 0xF7, 0xFB, 0xFB, 0xFD, 0xFF, 0xEE, 0xEF, 0xF7, 0xF7, 0xFB, 0xF3, 0xFC, 0xF9, 0xFE,
0x7C, 0xFF, 0x1C, 0x7F, 0x80, 0x3F, 0xC0, 0x1E, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xE0, 0x1F, 0xE0, 0x7F, 0xF1, 0xFF,
0xF3, 0xF7, 0xEF, 0x83, 0xDE, 0x07, 0xFC, 0x07, 0xF8, 0x0F, 0xF0, 0x1F, 0xE0, 0x7F, 0xE0, 0xFF, 0xF7, 0xE7, 0xFF,
0xC7, 0xFF, 0x07, 0xF8, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0,
0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xE0, 0xFF, 0xCF, 0xFE, 0xFF, 0xFF, 0x3F, 0xF0, 0xFF,
0x0F, 0xF3, 0xFF, 0xFF, 0xFF, 0xEF, 0xFC, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x00, 0x1F, 0xF1, 0xFF, 0x9F,
0xFC, 0xFD, 0xEF, 0x80, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3E, 0x01, 0xFC, 0xE7, 0xFF, 0x1F, 0xF8,
0x7F, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03,
0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x00, 0xF8, 0x7D, 0xE1, 0xE7, 0xC7, 0x8F, 0x3E, 0x3E, 0xF0, 0x7B, 0xC1,
0xFF, 0x03, 0xF8, 0x0F, 0xE0, 0x1F, 0x00, 0x7C, 0x1B, 0xE0, 0x7F, 0x81, 0xFC, 0x07, 0xE0, 0x00, 0x01, 0xE0, 0x00,
0x78, 0x00, 0xFF, 0xC0, 0x7F, 0xFC, 0x3F, 0xFF, 0x9F, 0xFF, 0xE7, 0x9E, 0x7D, 0xE7, 0x8F, 0x79, 0xE3, 0xDE, 0x79,
0xF7, 0xFF, 0xF8, 0xFF, 0xFE, 0x3F, 0xFF, 0x03, 0xFF, 0x00, 0x1E, 0x00, 0x07, 0x80, 0xF8, 0x7D, 0xF3, 0xE7, 0xEF,
0x8F, 0xFC, 0x1F, 0xE0, 0x7F, 0x80, 0xFC, 0x01, 0xF0, 0x0F, 0xC0, 0x7F, 0x83, 0xFF, 0x0F, 0xFC, 0x7C, 0xF9, 0xE1,
0xFF, 0x87, 0xC0, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78,
0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x0F, 0x00, 0x0F, 0x00,
0x0F, 0x00, 0x0F, 0xF8, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3D, 0xF3, 0xEF, 0xFF, 0x7F, 0xF8, 0xFF,
0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0xF0, 0xF1, 0xFE, 0x1E, 0x3F, 0xC3, 0xC7, 0xF8, 0x78, 0xFF,
0x0F, 0x1F, 0xE1, 0xE3, 0xFC, 0x3C, 0x7F, 0x87, 0x8F, 0xF0, 0xF1, 0xFE, 0x1E, 0x3F, 0xC3, 0xC7, 0xF8, 0x78, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0xF0, 0xF1, 0xE3, 0xC3, 0xC7, 0x8F, 0x0F, 0x1E, 0x3C, 0x3C, 0x78, 0xF0,
0xF1, 0xE3, 0xC3, 0xC7, 0x8F, 0x0F, 0x1E, 0x3C, 0x3C, 0x78, 0xF0, 0xF1, 0xE3, 0xC3, 0xC7, 0x8F, 0x0F, 0x1E, 0x3C,
0x3C, 0x78, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0,
0x00, 0x03, 0xC0, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0xF8, 0x0F, 0xFE, 0x0F, 0xFE,
0x0F, 0x7F, 0x0F, 0x1F, 0x0F, 0x1F, 0x0F, 0x7F, 0x0F, 0xFE, 0x0F, 0xFC, 0x0F, 0xF8, 0xF0, 0x07, 0xF8, 0x03, 0xFC,
0x01, 0xFE, 0x00, 0xFF, 0x00, 0x7F, 0xFE, 0x3F, 0xFF, 0x9F, 0xFF, 0xEF, 0xF3, 0xF7, 0xF8, 0x7B, 0xFC, 0x3D, 0xFE,
0x7E, 0xFF, 0xFF, 0x7F, 0xFF, 0x3F, 0xFE, 0x1E, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0xFC, 0xFF, 0xEF,
0xFF, 0xF3, 0xFF, 0x0F, 0xF0, 0xFF, 0x3F, 0xFF, 0xFF, 0xFE, 0xFF, 0xC0, 0x7F, 0xC1, 0xFF, 0x87, 0xFF, 0x1E, 0xFE,
0x00, 0x78, 0x01, 0xE1, 0xFF, 0xC7, 0xFF, 0x1F, 0xFC, 0x01, 0xF0, 0x07, 0x9E, 0xFE, 0x7F, 0xF1, 0xFF, 0x87, 0xFC,
0x00, 0xF0, 0x3F, 0x87, 0x83, 0xFF, 0x3C, 0x3F, 0xFD, 0xE3, 0xF7, 0xEF, 0x1E, 0x0F, 0xF9, 0xF0, 0x7F, 0xFF, 0x01,
0xFF, 0xF8, 0x0F, 0xFF, 0xC0, 0x7F, 0x9F, 0x07, 0xFC, 0xF8, 0x3F, 0xE3, 0xF7, 0xEF, 0x1F, 0xFF, 0x78, 0x7F, 0xF3,
0xC0, 0xFE, 0x00, 0x0F, 0xF8, 0xFF, 0xCF, 0xFE, 0xFC, 0xF7, 0xC7, 0xBE, 0x3D, 0xF9, 0xE7, 0xFF, 0x3F, 0xF8, 0xFF,
0xC7, 0xDE, 0x7C, 0xF3, 0xC7, 0xBE, 0x3D, 0xE1, 0xE0, 0x7F, 0x8F, 0xF8, 0xFF, 0x19, 0xF3, 0xFE, 0xFF, 0xFE, 0x7F,
0xCF, 0xFF, 0xEF, 0xFC, 0xFF, 0x80, 0x03, 0xE0, 0xFE, 0x1F, 0xE3, 0xFE, 0x7C, 0x07, 0xFC, 0x7F, 0xE7, 0xFF, 0x7D,
0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x7D, 0xF7, 0xFE, 0x3F, 0xE1, 0xFC, 0xFF, 0x9F, 0xFB, 0xFF, 0xF9, 0xEF, 0xFD, 0xFF,
0xBC, 0xFF, 0x9F, 0xFF, 0xFF, 0xFB, 0xFE, 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78,
0x3C, 0x00, 0x1F, 0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0xBC, 0x1E, 0xF0, 0xF3, 0xC3, 0xCF, 0x0F, 0x3C, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFC, 0x07, 0xF0, 0x1F, 0xC0, 0x70, 0x1F, 0xC3, 0xFE, 0x7F, 0xE7, 0xDF, 0x7F, 0xF7, 0xFF, 0x78, 0x07,
0xEE, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0xF9, 0xE7, 0xDF, 0x7B, 0xE3, 0xFF, 0xF0, 0xFF, 0xF8, 0x1F, 0xFE, 0x03, 0xFF,
0x01, 0xFF, 0xE0, 0xFF, 0xFC, 0x7D, 0xEF, 0x1E, 0x7B, 0xEF, 0x9E, 0x7C, 0x7F, 0x1F, 0xE7, 0xFD, 0xDF, 0x3F, 0x8F,
0xE0, 0x7D, 0xDF, 0x7F, 0xFF, 0xFF, 0xF8, 0xF1, 0xFE, 0x7F, 0xDF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xF9,
0xFF, 0x3F, 0xC7, 0x80, 0x39, 0x87, 0x78, 0xFE, 0x0F, 0x80, 0x00, 0x00, 0x3C, 0x7F, 0x9F, 0xF7, 0xFE, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFB, 0xFE, 0x7F, 0xCF, 0xF1, 0xE0, 0xF3, 0xEF, 0x7C, 0xFF, 0x8F, 0xF0, 0xFE, 0x0F, 0xE0, 0xFF,
0x0F, 0xF8, 0xF7, 0xCF, 0x3E, 0xF1, 0xE0, 0x1F, 0xF1, 0xFF, 0x1F, 0xF1, 0xEF, 0x1E, 0xF1, 0xEF, 0x3E, 0xFF, 0xCF,
0xFC, 0xFF, 0x8F, 0xF0, 0xF0, 0x78, 0x79, 0xE1, 0xEF, 0xCF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xF7, 0xBF, 0xDE, 0xFF, 0x03, 0xC0, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0xF3, 0xFC, 0xFF,
0x3C, 0x1F, 0xC3, 0xFE, 0x7F, 0xF7, 0xDF, 0x78, 0xF7, 0x8F, 0x78, 0xF7, 0xDF, 0x7F, 0xF3, 0xFE, 0x1F, 0xC0, 0xFF,
0xFF, 0xFF, 0xFF, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3C, 0xFF, 0x9F, 0xFB, 0xFF, 0x7B, 0xFF,
0x1F, 0xE3, 0xFC, 0x7F, 0xDF, 0xFF, 0xFF, 0xFB, 0xFE, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x00, 0x1F, 0xCF, 0xF7, 0xFD,
0xF6, 0x78, 0x1E, 0x07, 0x81, 0xF3, 0x7F, 0xCF, 0xF1, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0x80, 0xF0, 0x1E, 0x03,
0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x00, 0xF1, 0xFF, 0x1E, 0x79, 0xE7, 0x9E, 0x7B, 0xC3, 0xFC, 0x3F, 0xC3, 0xF8,
0x1F, 0x81, 0xF0, 0x0F, 0x0D, 0xF0, 0xFE, 0x0F, 0xC0, 0xF8, 0x00, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0,
0x03, 0xC0, 0x1F, 0xF8, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFF, 0x7B, 0xCF, 0x7B, 0xCF, 0x7B, 0xCF, 0x7F, 0xFF, 0x7F,
0xFE, 0x3F, 0xFE, 0x1F, 0xF8, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0xF9, 0xE7, 0xBE, 0x7F, 0xC3, 0xF8,
0x1F, 0x81, 0xF0, 0x1F, 0x83, 0xFC, 0x7F, 0xC7, 0x9E, 0xF1, 0xF0, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF,
0x3C, 0xF3, 0xCF, 0x3C, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x00, 0x70, 0x07, 0xF9, 0xFF, 0x3F, 0xE7, 0xBC, 0xF7,
0x9E, 0xFF, 0xDF, 0xF9, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3,
0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C,
0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC,
0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xF0, 0xFE, 0x07, 0xF0, 0x3F, 0x80, 0x3C, 0x01, 0xFF, 0x0F, 0xFC, 0x7F, 0xE3,
0xDF, 0x1F, 0xF8, 0xFF, 0xC7, 0xFC, 0xF0, 0x1F, 0xE0, 0x3F, 0xC0, 0x7F, 0x80, 0xFF, 0xF9, 0xFF, 0xFB, 0xFF, 0xF7,
0xF9, 0xFF, 0xFF, 0xDF, 0xFF, 0xBF, 0xFE, 0x78, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0xF9, 0xFF, 0xBF, 0xF7, 0x9F,
0xFF, 0xDF, 0xFB, 0xFE, 0x00, 0x7F, 0x0F, 0xF1, 0xFF, 0x3B, 0xE3, 0xFE, 0x7F, 0xC0, 0xFB, 0xBE, 0x7F, 0xDF, 0xF3,
0xFC, 0x00, 0xF0, 0xFC, 0x78, 0xFF, 0x3C, 0xFF, 0xDE, 0x7F, 0xEF, 0xFC, 0x7F, 0xFC, 0x3F, 0xFF, 0x1F, 0xE7, 0xFE,
0xF3, 0xFF, 0x78, 0xFF, 0x3C, 0x3F, 0x00, 0x1F, 0xEF, 0xFD, 0xFF, 0xBE, 0xF7, 0xDE, 0xFF, 0xDF, 0xF9, 0xFF, 0x3D,
0xEF, 0x3F, 0xE7, 0x80, 0x0C, 0x01, 0xE0, 0x1F, 0x00, 0x78, 0x03, 0x80, 0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xE7, 0xDF,
0x7F, 0xF7, 0xFF, 0x78, 0x07, 0xEE, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0x3D, 0xC3, 0xDE, 0x3D, 0xC0, 0x00, 0x00, 0x01,
0xFC, 0x3F, 0xE7, 0xFE, 0x7D, 0xF7, 0xFF, 0x7F, 0xF7, 0x80, 0x7E, 0xE7, 0xFE, 0x3F, 0xE1, 0xFE, 0x78, 0x07, 0x80,
0xFF, 0x0F, 0xF0, 0x78, 0x07, 0xFC, 0x7F, 0xE7, 0xFE, 0x7D, 0xE7, 0x8F, 0x78, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x78,
0xF7, 0x8F, 0x01, 0xF0, 0x7E, 0x07, 0xE0, 0x7C, 0x0C, 0x0F, 0x07, 0x87, 0x83, 0x80, 0x03, 0xFF, 0xFF, 0xFF, 0xF8,
0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x00, 0x1F, 0xC7, 0xF9, 0xFF, 0x3E, 0xEF, 0xF9, 0xFF, 0x3E, 0x03,
0xEE, 0x7F, 0xC7, 0xFC, 0x7F, 0x80, 0x3F, 0x9F, 0xE7, 0xF9, 0xEE, 0x7F, 0x9F, 0xE3, 0xFD, 0xDF, 0x7F, 0xDF, 0xE7,
0xF0, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x73, 0xBF, 0xDC, 0xE0, 0x00, 0x00, 0xF0, 0x78, 0x3C, 0x1E,
0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x3C, 0x78, 0xF0, 0x03, 0xC7, 0x8F, 0x1E, 0x3C, 0x78, 0xF1,
0xE3, 0xC7, 0x8F, 0x3F, 0xFF, 0xF7, 0xE0, 0x1F, 0xF0, 0x03, 0xFE, 0x00, 0x7F, 0xC0, 0x0F, 0x78, 0x01, 0xEF, 0xF8,
0x3D, 0xFF, 0x8F, 0xBF, 0xFF, 0xE7, 0x9F, 0xFC, 0xFF, 0xDF, 0x1F, 0xFB, 0xC3, 0xFE, 0x00, 0xF3, 0xC0, 0x79, 0xE0,
0x3C, 0xF0, 0x1E, 0x78, 0x0F, 0xFF, 0xE7, 0xFF, 0xFB, 0xFF, 0xFF, 0xE7, 0x9F, 0xF3, 0xFF, 0xF9, 0xFF, 0xBC, 0x7F,
0x80, 0x78, 0x07, 0x80, 0xFF, 0x0F, 0xF0, 0x78, 0x07, 0xFC, 0x7F, 0xE7, 0xFE, 0x7D, 0xE7, 0x8F, 0x78, 0xF7, 0x8F,
0x78, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x03, 0x00, 0x78, 0x0F, 0x81, 0xE0, 0x1C, 0x00, 0x00, 0xF3, 0xEF, 0x7C, 0xFF,
0x8F, 0xF0, 0xFE, 0x0F, 0xE0, 0xFF, 0x0F, 0xF8, 0xF7, 0xCF, 0x3E, 0xF1, 0xE0, 0x18, 0x07, 0x80, 0xF0, 0x0F, 0x00,
0xE0, 0x00, 0x3C, 0x7F, 0x9F, 0xF7, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFE, 0x7F, 0xCF, 0xF1, 0xE0, 0x39,
0x83, 0xFC, 0x3F, 0x81, 0xF0, 0x00, 0x00, 0x00, 0xF1, 0xFF, 0x1E, 0x79, 0xE7, 0x9E, 0x7B, 0xC3, 0xFC, 0x3F, 0xC3,
0xF8, 0x1F, 0x81, 0xF0, 0x0F, 0x0D, 0xF0, 0xFE, 0x0F, 0xC0, 0xF8, 0x00, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC,
0xFF, 0x3F, 0xCF, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x1E, 0x07, 0x80, 0x1E, 0x00, 0x3C, 0x03, 0xFF, 0x87, 0xFF, 0x0F,
0xFE, 0x03, 0xC0, 0x07, 0xFE, 0x0F, 0xFE, 0x1F, 0xFE, 0x3C, 0xFC, 0x78, 0x78, 0xF0, 0xF1, 0xE7, 0xE3, 0xFF, 0xC7,
0xFF, 0x0F, 0xF8, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x7F, 0xE3, 0xFF, 0x1F, 0xF8, 0x3C, 0x01, 0xE0,
0x0F, 0xF8, 0x7F, 0xE3, 0xCF, 0x9F, 0xFC, 0xFF, 0xC7, 0xFE, 0x1F, 0xE0, 0x7F, 0xF1, 0xFF, 0xF3, 0xF7, 0xEF, 0x83,
0xDF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xE0, 0x7F, 0xE0, 0xFF, 0xF7, 0xE7, 0xFF, 0xC7, 0xFF, 0x07, 0xF8,
0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xF7, 0xDF, 0x7F, 0xF7, 0xFF, 0x7F, 0xF7, 0xDF, 0x7F, 0xF3, 0xFE, 0x1F, 0xC0, 0xF0,
0x1F, 0xBE, 0x07, 0xE7, 0x83, 0xF9, 0xE0, 0xF4, 0x7C, 0x7C, 0x0F, 0x1E, 0x03, 0xC7, 0x80, 0xFB, 0xC0, 0x1E, 0xF0,
0x07, 0xFC, 0x00, 0xFE, 0x00, 0x3F, 0x80, 0x0F, 0xE0, 0x01, 0xF0, 0x00, 0x7C, 0x00, 0xF0, 0xFF, 0x8F, 0xDE, 0x7E,
0xF3, 0x87, 0xBC, 0x1F, 0xE0, 0xFE, 0x07, 0xF0, 0x1F, 0x00, 0xF8, 0x03, 0xC0, 0x1F, 0xE0, 0x1F, 0xE0, 0x0F, 0xC0,
0x00, 0x00, 0xF0, 0x7C, 0xF0, 0x7C, 0xF0, 0xFC, 0xF1, 0xFC, 0xF3, 0xFC, 0xF3, 0xFC, 0xF7, 0xBC, 0xFF, 0xBC, 0xFF,
0x3C, 0xFE, 0x3C, 0xFE, 0x3C, 0xFC, 0x3C, 0xF8, 0x3F, 0xF8, 0x3F, 0xF0, 0x3F, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x3C,
0x00, 0x1C, 0x7F, 0x83, 0xFC, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x79, 0xF3, 0xCF, 0x9E, 0xFC, 0xFF, 0xE7, 0xFF, 0x3F,
0xF9, 0xFB, 0xCF, 0xDE, 0x7C, 0xFB, 0xC7, 0xDE, 0x3E, 0x00, 0xF0, 0x0F, 0x00, 0x30, 0x78, 0x03, 0xC0, 0x3F, 0xC1,
0xFE, 0x07, 0x80, 0x3F, 0xF1, 0xFF, 0xCF, 0xFF, 0x79, 0xFB, 0xC3, 0xDE, 0x1E, 0xF3, 0xF7, 0xFF, 0xBF, 0xF8, 0xFF,
0x80, 0x78, 0x0F, 0xF0, 0xFF, 0x07, 0x80, 0x7F, 0xC7, 0xFE, 0x79, 0xE7, 0x9F, 0x7F, 0xE7, 0xFE, 0x7F, 0xC0, 0xFF,
0xCF, 0xFE, 0xFF, 0xFF, 0x3F, 0xF2, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xEF, 0xFC, 0xF1, 0xEF, 0x1E, 0xF0, 0x8F,
0x00, 0xF0, 0x00, 0xFF, 0x9F, 0xFB, 0xFF, 0xF9, 0xFF, 0x1F, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0x79,
0xEF, 0x1D, 0xE1, 0x3C, 0x00, 0x01, 0xE0, 0x3C, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0x80, 0xF0, 0x1E, 0x03,
0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x00, 0x07, 0x83, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1,
0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x80, 0x3F, 0xF3, 0xFF, 0x3F, 0xF3, 0xC0, 0x3C, 0x03, 0xC0, 0xFF, 0x8F,
0xF8, 0xFF, 0x83, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x00, 0x7F, 0xDF, 0xF7, 0xFD, 0xE0, 0x78, 0x3F,
0xCF, 0xF1, 0xE0, 0x78, 0x1E, 0x07, 0x80, 0xFF, 0xE7, 0xFF, 0x3F, 0xF9, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0x1F,
0xFC, 0xFF, 0xF7, 0x9F, 0xBC, 0x3D, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC3, 0xC0, 0x7E, 0x07, 0xF0, 0x7F, 0x03, 0xF0,
0xFF, 0x9F, 0xF3, 0xFE, 0x78, 0x0F, 0x01, 0xFF, 0x3F, 0xF7, 0xFE, 0xF7, 0xDE, 0x3F, 0xC7, 0x81, 0xF0, 0xFC, 0x1F,
0x83, 0xE0, 0xF8, 0xF0, 0xF9, 0xF3, 0xC7, 0xC7, 0xEF, 0x3E, 0x0F, 0xBD, 0xF0, 0x1F, 0xF7, 0xC0, 0x3F, 0xFE, 0x00,
0x7F, 0xF0, 0x01, 0xFF, 0x80, 0x0F, 0xFF, 0x00, 0x7F, 0xFE, 0x01, 0xFF, 0xFC, 0x0F, 0xBD, 0xF8, 0x7C, 0xF3, 0xF9,
0xE3, 0xC7, 0xEF, 0x8F, 0x0F, 0x80, 0x00, 0x1E, 0x00, 0x00, 0x78, 0x00, 0x01, 0xE0, 0x00, 0x07, 0x80, 0xF9, 0xE7,
0xDF, 0x7B, 0xE3, 0xFF, 0xF0, 0xFF, 0xF8, 0x1F, 0xFE, 0x03, 0xFF, 0x01, 0xFF, 0xE0, 0xFF, 0xFC, 0x7D, 0xFF, 0xDE,
0x7B, 0xFF, 0x9E, 0x7C, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x70, 0x7F, 0xC7, 0xFE, 0x7F, 0xE7, 0xBF, 0x01, 0xF0,
0x3E, 0x1F, 0xE1, 0xFE, 0x1F, 0xF0, 0x3F, 0x00, 0xF7, 0xBF, 0x7F, 0xFF, 0xFE, 0x7F, 0xC0, 0xF0, 0x0F, 0x00, 0xF8,
0x1F, 0x01, 0xF0, 0x7F, 0x1F, 0xE7, 0xFD, 0xDF, 0x3F, 0x8F, 0xE0, 0x7D, 0xDF, 0x7F, 0xFF, 0xFF, 0xF8, 0x78, 0x1E,
0x07, 0xC3, 0xE0, 0xF8, 0xF0, 0xF9, 0xE3, 0xE3, 0xCF, 0x87, 0xBE, 0x0F, 0xF8, 0x1F, 0xE0, 0x3F, 0x80, 0x7F, 0x80,
0xFF, 0x81, 0xFF, 0x83, 0xDF, 0x87, 0x9F, 0x0F, 0x1F, 0xFE, 0x1F, 0xFC, 0x3F, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C,
0x00, 0xF8, 0xF3, 0xEF, 0x7C, 0xF7, 0x8F, 0xF8, 0xFF, 0x0F, 0xE0, 0xFF, 0x0F, 0xF8, 0xF7, 0xFF, 0x3F, 0xF1, 0xF0,
0x0F, 0x00, 0xF0, 0x0F, 0xF0, 0x3E, 0xF0, 0x3E, 0xF6, 0x7C, 0xF6, 0xF8, 0xF7, 0xF0, 0xFF, 0xE0, 0xFF, 0xE0, 0xFF,
0xF0, 0xF7, 0xF0, 0xF6, 0xF8, 0xF6, 0xF8, 0xF0, 0x7C, 0xF0, 0x3E, 0xF0, 0x3E, 0xF0, 0x1F, 0xF0, 0xFB, 0xFF, 0xCF,
0xFE, 0x3F, 0xF0, 0xFF, 0x83, 0xFF, 0x0F, 0xFE, 0x3F, 0xF8, 0xF1, 0xF3, 0xC3, 0xEF, 0x07, 0x80, 0x7C, 0x00, 0xF8,
0x7F, 0xF9, 0xF7, 0xF7, 0xC7, 0xDF, 0x0F, 0xFE, 0x1F, 0xF8, 0x3F, 0xE0, 0x7F, 0x80, 0xFF, 0x81, 0xFF, 0x83, 0xFF,
0x87, 0xDF, 0x0F, 0x9F, 0x1F, 0x1F, 0x3E, 0x1F, 0x78, 0x07, 0xF7, 0xFF, 0xFD, 0xFF, 0xC7, 0xFC, 0x3F, 0xC1, 0xFC,
0x0F, 0xF0, 0x7F, 0xC3, 0xDF, 0x1E, 0x7C, 0xF1, 0xE0, 0xFF, 0x1F, 0x7F, 0x9F, 0x3F, 0xDF, 0x81, 0xFF, 0x80, 0xFF,
0x80, 0x7F, 0x80, 0x3F, 0x80, 0x1F, 0xE0, 0x0F, 0xF8, 0x07, 0xFC, 0x03, 0xDF, 0x01, 0xE7, 0xC0, 0xF1, 0xF0, 0x78,
0xF8, 0x3C, 0x3E, 0xFE, 0x7F, 0xFB, 0xEF, 0xFF, 0x07, 0xF8, 0x1F, 0xC0, 0x7F, 0x01, 0xFE, 0x07, 0xFC, 0x1E, 0xF8,
0x79, 0xE1, 0xE7, 0xC0, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xFF, 0xF8, 0xFF,
0xF8, 0xFF, 0xF8, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x7F, 0xF0, 0x7F, 0xF0, 0x7F, 0x00, 0x0F, 0x00, 0x0F,
0x00, 0x0F, 0x00, 0x0F, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xFF, 0xCF, 0xFC, 0xFF, 0xCF, 0x3C, 0xF3, 0xFF, 0x3F,
0xF3, 0xF0, 0x07, 0x00, 0x70, 0x07, 0xF0, 0x7F, 0xF8, 0x3F, 0xFC, 0x1F, 0xFE, 0x0F, 0x0F, 0x07, 0x87, 0x83, 0xC3,
0xFF, 0xE1, 0xFF, 0xF0, 0xFF, 0xF8, 0x78, 0x3C, 0x3C, 0x1E, 0x1E, 0x0F, 0x0F, 0x07, 0x87, 0x83, 0xC3, 0xC1, 0xE0,
0xF3, 0xFF, 0x9F, 0xFC, 0xFF, 0xE7, 0x8F, 0xFC, 0x7F, 0xE3, 0xFF, 0x1E, 0x78, 0xF3, 0xC7, 0x9E, 0x3C, 0xF0, 0xFF,
0xF8, 0x03, 0xFF, 0xE0, 0x0F, 0xFF, 0x80, 0x3C, 0x1E, 0x00, 0xF0, 0x78, 0x03, 0xC1, 0xE0, 0x0F, 0x07, 0xFE, 0x3C,
0x1F, 0xFC, 0xF0, 0x7F, 0xFB, 0xC1, 0xE7, 0xEF, 0x07, 0x87, 0xFC, 0x1E, 0x0F, 0xF0, 0x78, 0x3F, 0xC1, 0xE0, 0xFF,
0x07, 0x87, 0xC0, 0x00, 0x3E, 0x00, 0x03, 0xF8, 0x00, 0x0F, 0xC0, 0x00, 0x3E, 0x00, 0xFF, 0xC0, 0x7F, 0xE0, 0x3F,
0xF0, 0x1E, 0x78, 0x0F, 0x3F, 0xE7, 0x9F, 0xFB, 0xCF, 0xFD, 0xE7, 0x9F, 0xF3, 0xC7, 0xF9, 0xE3, 0xFC, 0xF1, 0xE0,
0x01, 0xF0, 0x03, 0xF8, 0x01, 0xF8, 0x00, 0xF8, 0x0F, 0xE0, 0x3F, 0xE0, 0x7F, 0xE0, 0x7E, 0x60, 0xF8, 0x00, 0xF1,
0xFC, 0xF3, 0xFE, 0xF3, 0xFE, 0xF7, 0xDE, 0xF7, 0x9E, 0xFF, 0x9E, 0xFF, 0xFE, 0x7F, 0xFE, 0x3F, 0xFC, 0x1F, 0xF0,
0x01, 0xFC, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x3E, 0x1F, 0xC1, 0xFE, 0x1F, 0xE0, 0xFF, 0xE7, 0x3F, 0xBB, 0xFD, 0xDC,
0xEF, 0xEF, 0x7F, 0xF9, 0xFF, 0x87, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7C, 0x1F, 0xF1, 0xFF, 0x9F, 0xFC, 0xFD, 0xEF,
0x80, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3E, 0x01, 0xFC, 0xE7, 0xFF, 0x1F, 0xF8, 0x7F, 0xC0, 0xF0,
0x07, 0x80, 0x5C, 0x03, 0xE0, 0x1F, 0x00, 0x1F, 0xCF, 0xF7, 0xFD, 0xF6, 0x78, 0x1E, 0x07, 0x81, 0xF3, 0x7F, 0xCF,
0xF0, 0xFC, 0x3C, 0x0F, 0x07, 0xC1, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01,
0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xFE, 0x07, 0xF0, 0x3F, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78,
0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1F, 0x83, 0xF0, 0x7E, 0x01, 0xC0, 0x38, 0x07,
0x00, 0xF8, 0x7F, 0xE1, 0xE7, 0xCF, 0x9F, 0x3C, 0x3F, 0xF0, 0x7F, 0x81, 0xFE, 0x03, 0xF0, 0x0F, 0xC0, 0x1E, 0x00,
0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x00, 0xF1, 0xFF, 0x1E, 0x79, 0xE7, 0xBE, 0x7B, 0xC3, 0xFC, 0x3F,
0x81, 0xF8, 0x1F, 0x81, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF8, 0x7F, 0xE1, 0xF7, 0xCF, 0x9F,
0x3C, 0x3F, 0xF0, 0xFF, 0x81, 0xFE, 0x03, 0xF0, 0x0F, 0xC0, 0xFF, 0xC3, 0xFF, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00,
0x78, 0x00, 0xF1, 0xFF, 0x1E, 0x79, 0xE7, 0xBE, 0x7B, 0xC3, 0xFC, 0x3F, 0x81, 0xF8, 0x1F, 0x87, 0xFC, 0x7F, 0xC0,
0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF8, 0x7C, 0xF9, 0xF1, 0xFB, 0xE1, 0xFF, 0x81, 0xFE, 0x03, 0xFC, 0x03, 0xF0,
0x03, 0xE0, 0x0F, 0xE0, 0x3F, 0xC0, 0xFF, 0xC1, 0xFF, 0xC7, 0xCF, 0xEF, 0x0F, 0xFE, 0x1F, 0x80, 0x0F, 0x00, 0x1E,
0x00, 0x3C, 0x00, 0x78, 0xF9, 0xE7, 0xBE, 0x7F, 0xC3, 0xF8, 0x1F, 0x81, 0xF0, 0x1F, 0x83, 0xFC, 0x7F, 0xF7, 0x9F,
0xF1, 0xF0, 0x07, 0x00, 0x70, 0x07, 0xFF, 0xF7, 0x8F, 0xFF, 0x78, 0xFF, 0xF7, 0x80, 0xF0, 0x78, 0x0F, 0x07, 0x80,
0xF0, 0x78, 0x0F, 0x07, 0x80, 0xF0, 0x78, 0x0F, 0x07, 0x80, 0xF0, 0x78, 0x0F, 0x07, 0x80, 0xF0, 0x78, 0x0F, 0xFF,
0xF0, 0xFF, 0xFF, 0x0F, 0xFF, 0xF0, 0x00, 0x1F, 0x00, 0x01, 0xF0, 0x00, 0x1F, 0x00, 0x01, 0xF0, 0xFF, 0x79, 0xFE,
0xF3, 0xFD, 0xE1, 0xE3, 0xC3, 0xC7, 0x87, 0x8F, 0x0F, 0x1E, 0x1E, 0x3C, 0x3F, 0xFE, 0x7F, 0xFC, 0xFF, 0xF8, 0x00,
0xF0, 0x01, 0xE0, 0x03, 0xC0, 0xF0, 0xF3, 0xC3, 0xCF, 0x0F, 0x3C, 0x3C, 0xF0, 0xF3, 0xC3, 0xCF, 0xCF, 0x3F, 0xFC,
0x7F, 0xF0, 0xFF, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xFC, 0x03, 0xF0, 0x0F, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03,
0xC0, 0xF1, 0xE7, 0x8F, 0x3C, 0x79, 0xE3, 0xCF, 0x9E, 0x7F, 0xF1, 0xFF, 0x87, 0xFC, 0x01, 0xF8, 0x0F, 0xC0, 0x7E,
0x00, 0xF0, 0x07, 0x80, 0x3C, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x6F, 0xFE, 0xFF, 0xFF, 0x7F, 0xF3,
0xFF, 0x06, 0xF0, 0x6F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0xF1, 0xFE, 0x3F, 0xC7, 0xFB, 0xFF, 0xFF, 0xFF, 0xDF, 0xF9,
0xFF, 0x07, 0xE0, 0xFC, 0x07, 0x80, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x7F, 0xF3, 0xFF, 0xDF, 0xFE,
0xF9, 0xFF, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3F,
0xEF, 0xFF, 0xFF, 0xF7, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0x01, 0xFE, 0x00, 0xFF, 0xCE, 0x7F,
0xFB, 0xBF, 0x3E, 0xFF, 0x07, 0xFF, 0xFF, 0xF7, 0xFF, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xF8, 0x00,
0x1F, 0x9C, 0x07, 0xFF, 0x80, 0xFF, 0xE0, 0x0F, 0xF0, 0xC7, 0xF3, 0xBF, 0xEE, 0xFF, 0xFF, 0xDF, 0xFF, 0xFD, 0xFF,
0xF1, 0xE0, 0x07, 0xEE, 0x0F, 0xF8, 0x1F, 0xF0, 0x3F, 0xC0, 0x01, 0xFE, 0x00, 0xFF, 0xC0, 0x7F, 0xFB, 0x9F, 0xFE,
0xEF, 0x87, 0xBF, 0xC1, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0x3F, 0xFF, 0xC3, 0xC0, 0x00, 0xF8, 0x00, 0x3F, 0x9C, 0x07,
0xFF, 0x80, 0xFF, 0xE0, 0x1F, 0xF0, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x80, 0x03, 0xF3, 0x1F, 0xEE,
0xFF, 0xFB, 0xFF, 0xFE, 0x3F, 0xFF, 0xF7, 0xFF, 0xC7, 0xEE, 0x0F, 0xF8, 0x3F, 0xF0, 0x7F, 0xC0, 0x78, 0x01, 0xE0,
0x07, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x01, 0x9C, 0x00, 0x1E, 0xE0, 0x00, 0x7F, 0x00, 0x01,
0xF0, 0x00, 0x00, 0x00, 0x7C, 0x78, 0x7D, 0xF3, 0xC7, 0xCF, 0xDE, 0x7C, 0x3E, 0xF7, 0xC0, 0xFF, 0xBE, 0x03, 0xFF,
0xE0, 0x0F, 0xFE, 0x00, 0x7F, 0xE0, 0x07, 0xFF, 0x80, 0x7F, 0xFE, 0x03, 0xFE, 0xF8, 0x3E, 0xF3, 0xE3, 0xE7, 0x9F,
0x1E, 0x3C, 0x7D, 0xF1, 0xE1, 0xF0, 0x07, 0x30, 0x01, 0xFE, 0x00, 0x7F, 0x00, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x00,
0x0F, 0x9E, 0x7D, 0xF7, 0xBE, 0x3F, 0xFF, 0x0F, 0xFF, 0x81, 0xFF, 0xE0, 0x3F, 0xF0, 0x1F, 0xFE, 0x0F, 0xFF, 0xC7,
0xDE, 0xF1, 0xE7, 0xBE, 0xF9, 0xE7, 0xC0, 0xF0, 0xFF, 0x8F, 0xBC, 0xF9, 0xEF, 0x8F, 0x78, 0x7F, 0xC3, 0xFF, 0x1F,
0xFC, 0xFF, 0xF7, 0x9F, 0xBC, 0x3F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC3, 0xE0, 0x3E, 0x07, 0xF0, 0x3F, 0x01, 0xF0,
0xF3, 0xFE, 0xFB, 0xDE, 0x7F, 0x8F, 0xF1, 0xFF, 0x3F, 0xF7, 0xBE, 0xF1, 0xFE, 0x3F, 0xC7, 0x80, 0xF0, 0x3C, 0x1F,
0x83, 0xE0, 0x0F, 0xFE, 0x03, 0xFF, 0x80, 0xFF, 0xE0, 0x3C, 0x78, 0x0F, 0x1E, 0x03, 0xC7, 0x80, 0xF1, 0xE0, 0x3C,
0x78, 0x0F, 0x1E, 0x07, 0xC7, 0x81, 0xE1, 0xE3, 0xF8, 0x78, 0xFC, 0x1F, 0xBF, 0x07, 0xEF, 0x01, 0xF8, 0x00, 0x1E,
0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0x60, 0x1F, 0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xC3, 0xEF,
0x3F, 0x3C, 0xFC, 0xFF, 0xE3, 0xFF, 0x0F, 0xC0, 0x0E, 0x00, 0x78, 0x01, 0xC0, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0,
0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xE0,
0x0F, 0x01, 0xF8, 0x0F, 0xC0, 0xFC, 0xF1, 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0xF1, 0xFE,
0x3F, 0xC7, 0x81, 0xF0, 0xFC, 0x1F, 0x83, 0xE0, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0,
0x78, 0xFF, 0xF8, 0xFF, 0xF8, 0xFF, 0xF8, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x7E, 0xF0, 0x7E, 0xF0, 0x7E,
0x00, 0x1E, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x18, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xFF, 0xCF, 0xFC, 0xFF, 0xCF,
0x3C, 0xF3, 0xFF, 0x3F, 0xF3, 0xF0, 0x0E, 0x01, 0xE0, 0x1C, 0xF8, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC,
0x3D, 0xF3, 0xEF, 0xFF, 0x7F, 0xF8, 0xFF, 0xC0, 0x1E, 0x00, 0xF0, 0x3F, 0x81, 0xFC, 0x0F, 0xE0, 0x78, 0x03, 0xC0,
0x1E, 0x00, 0xF0, 0xF9, 0xFF, 0x3F, 0xE7, 0xBC, 0xF7, 0xFE, 0xFF, 0xCF, 0xF8, 0x0F, 0x01, 0xE0, 0xFC, 0x1F, 0x83,
0x80, 0x70, 0x0E, 0x00, 0x78, 0x0F, 0x0F, 0x83, 0xE1, 0xF0, 0x7C, 0x3F, 0x1F, 0x8F, 0xE3, 0xF1, 0xFE, 0xFE, 0x3F,
0xDF, 0xE7, 0xBB, 0xBC, 0xF7, 0xF7, 0x9E, 0xFC, 0xF3, 0xCF, 0x9E, 0x79, 0xF3, 0xCF, 0x1C, 0x7F, 0xE0, 0x0F, 0xFC,
0x01, 0xF8, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0x78, 0x00, 0x0E, 0x00, 0xF0, 0x78, 0xF8, 0x78, 0xF8, 0xF8, 0xFC,
0xF8, 0xFD, 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0xFF, 0xBC, 0xF7, 0xBF, 0xF7, 0xBF, 0xF0, 0x3E, 0x00, 0x1E, 0x00, 0x1E,
0x00, 0x1C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x0C, 0xE0, 0x3D, 0xC0, 0x3F, 0x80, 0x3E, 0x00, 0x00,
0x00, 0xF8, 0x01, 0xF0, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0xE0, 0x7B, 0xC0, 0xF7, 0x83, 0xE7, 0x87, 0x8F, 0x0F, 0xFE,
0x3F, 0xFE, 0x7F, 0xFC, 0xF0, 0x7F, 0xE0, 0xFF, 0x80, 0xF0, 0x39, 0x87, 0xF8, 0xFE, 0x0F, 0xC0, 0x00, 0x00, 0x1F,
0xE3, 0xFE, 0x3F, 0xC6, 0x7C, 0xFF, 0xBF, 0xFF, 0x9F, 0xF3, 0xFF, 0xFB, 0xFF, 0x3F, 0xE0, 0x1E, 0xF0, 0x3D, 0xE0,
0x7B, 0xC0, 0x00, 0x00, 0x7C, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xF0, 0x0F, 0xF0, 0x3D, 0xE0, 0x7B, 0xC1, 0xF3, 0xC3,
0xC7, 0x87, 0xFF, 0x1F, 0xFF, 0x3F, 0xFE, 0x78, 0x3F, 0xF0, 0x7F, 0xC0, 0x78, 0x7B, 0xCF, 0x79, 0xEF, 0x00, 0x00,
0x00, 0xFF, 0x1F, 0xF1, 0xFE, 0x33, 0xE7, 0xFD, 0xFF, 0xFC, 0xFF, 0x9F, 0xFF, 0xDF, 0xF9, 0xFF, 0x00, 0xFF, 0xF0,
0x07, 0xFF, 0x80, 0x7F, 0xFC, 0x07, 0xF8, 0x00, 0x3F, 0xC0, 0x03, 0xDE, 0x00, 0x1E, 0xFF, 0x81, 0xE7, 0xFC, 0x1F,
0x3F, 0xE0, 0xFF, 0xE0, 0x0F, 0xFF, 0x00, 0x7F, 0xF8, 0x07, 0xC3, 0xFF, 0xBC, 0x1F, 0xFF, 0xE0, 0xFF, 0xE0, 0x7F,
0xFF, 0x1F, 0xFF, 0xE3, 0xFF, 0xF8, 0xCF, 0xDF, 0x3F, 0xFF, 0xDF, 0xFF, 0xFF, 0x9E, 0x03, 0xE7, 0xEE, 0xFF, 0xFF,
0x9F, 0xFF, 0xE3, 0xFF, 0xF8, 0x39, 0x87, 0x78, 0xFE, 0x0F, 0x80, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1E, 0x03,
0xC0, 0x7F, 0xEF, 0xFD, 0xFF, 0xBC, 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF, 0xFF, 0xF0, 0x19, 0xC3, 0xDC, 0x1F, 0xC0,
0xF8, 0x00, 0x00, 0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xE7, 0xDF, 0x7F, 0xF7, 0xFF, 0x78, 0x07, 0xEE, 0x7F, 0xE3, 0xFE,
0x1F, 0xE0, 0x7F, 0xC1, 0xFF, 0x87, 0xFF, 0x1E, 0x7E, 0x00, 0x78, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1,
0xFF, 0x07, 0xBF, 0x7E, 0x7F, 0xF0, 0xFF, 0xC1, 0xFC, 0x00, 0x7F, 0x87, 0xFC, 0x7F, 0xE3, 0x3E, 0x01, 0xF7, 0xFF,
0x7F, 0xF7, 0x9E, 0x7F, 0xE7, 0xFC, 0x1F, 0x80, 0x3D, 0xC0, 0xF7, 0x83, 0xDC, 0x00, 0x00, 0x7F, 0xC1, 0xFF, 0x87,
0xFF, 0x1E, 0x7E, 0x00, 0x78, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xFF, 0x07, 0xBF, 0x7E, 0x7F, 0xF0,
0xFF, 0xC1, 0xFC, 0x00, 0x39, 0xC3, 0xFC, 0x39, 0xC0, 0x00, 0x00, 0x07, 0xF8, 0x7F, 0xC7, 0xFE, 0x33, 0xE0, 0x1F,
0x7F, 0xF7, 0xFF, 0x79, 0xE7, 0xFE, 0x7F, 0xC1, 0xF8, 0x03, 0xDC, 0x00, 0x1E, 0xF0, 0x00, 0xF7, 0x00, 0x00, 0x00,
0x0F, 0x8F, 0x0F, 0xBE, 0x78, 0xF9, 0xFB, 0xCF, 0x87, 0xDE, 0xF8, 0x1F, 0xF7, 0xC0, 0x7F, 0xFC, 0x01, 0xFF, 0xC0,
0x0F, 0xFC, 0x00, 0xFF, 0xF0, 0x0F, 0xFF, 0xC0, 0x7F, 0xDF, 0x07, 0xDE, 0x7C, 0x7C, 0xF3, 0xE3, 0xC7, 0x8F, 0xBE,
0x3C, 0x3E, 0x07, 0x78, 0x01, 0xFE, 0x00, 0x77, 0x80, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x79, 0xF7, 0xDE, 0xF8, 0xFF,
0xFC, 0x3F, 0xFE, 0x07, 0xFF, 0x80, 0xFF, 0xC0, 0x7F, 0xF8, 0x3F, 0xFF, 0x1F, 0x7B, 0xC7, 0x9E, 0xFB, 0xE7, 0x9F,
0x3B, 0xC3, 0xFC, 0x3B, 0xC0, 0x00, 0x00, 0x07, 0xFC, 0x7F, 0xE7, 0xFE, 0x7B, 0xF0, 0x1F, 0x03, 0xE1, 0xFE, 0x1F,
0xE1, 0xFF, 0x03, 0xF0, 0x0F, 0x7B, 0xF7, 0xFF, 0xFF, 0xE7, 0xFC, 0x77, 0xBD, 0xE7, 0x78, 0x00, 0x00, 0x1F, 0xC7,
0xF9, 0xFF, 0x77, 0xCF, 0xE3, 0xF8, 0x1F, 0x77, 0xDF, 0xFF, 0xFF, 0xFE, 0x7F, 0xF7, 0xFF, 0x7F, 0xF0, 0x3E, 0x07,
0xC0, 0xF8, 0x1F, 0xC1, 0xFE, 0x03, 0xF0, 0x0F, 0x00, 0xF7, 0xBF, 0xFF, 0xFF, 0xFE, 0x7F, 0xC0, 0xFF, 0xFF, 0xFF,
0xFC, 0x1E, 0x0F, 0x07, 0x83, 0xF0, 0xFE, 0x0F, 0xC0, 0xF0, 0x3F, 0x9F, 0xFF, 0xFF, 0xEF, 0xF0, 0x1F, 0xE0, 0x7F,
0x80, 0x00, 0x3C, 0x1F, 0xF0, 0x7F, 0xC3, 0xFF, 0x1F, 0xFC, 0xFF, 0xF3, 0xFF, 0xDE, 0xFF, 0xFB, 0xFF, 0xCF, 0xFE,
0x3F, 0xF8, 0xFF, 0xC3, 0xFE, 0x0F, 0xF8, 0x3F, 0xC0, 0xF0, 0x3F, 0x87, 0xF0, 0x00, 0x00, 0x0F, 0x1F, 0xE7, 0xFD,
0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0x9F, 0xF3, 0xFC, 0x78, 0x1E, 0xE0, 0x7B, 0xC1, 0xEE, 0x00, 0x00,
0xF0, 0x7F, 0xC1, 0xFF, 0x0F, 0xFC, 0x7F, 0xF3, 0xFF, 0xCF, 0xFF, 0x7B, 0xFF, 0xEF, 0xFF, 0x3F, 0xF8, 0xFF, 0xE3,
0xFF, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x03, 0xC0, 0x3B, 0xCF, 0x78, 0xEF, 0x00, 0x00, 0x01, 0xE3, 0xFC, 0xFF, 0xBF,
0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xF3, 0xFE, 0x7F, 0x8F, 0x1E, 0xE0, 0x3D, 0xE0, 0x7B, 0x80, 0x00, 0x01,
0xFE, 0x07, 0xFF, 0x1F, 0xFF, 0x3F, 0x7E, 0xF8, 0x3D, 0xE0, 0x7F, 0xC0, 0x7F, 0x80, 0xFF, 0x01, 0xFE, 0x07, 0xFE,
0x0F, 0xFF, 0x7E, 0x7F, 0xFC, 0x7F, 0xF0, 0x7F, 0x80, 0x3D, 0xE3, 0xDE, 0x3D, 0xE0, 0x00, 0x00, 0x01, 0xFC, 0x3F,
0xE7, 0xFF, 0x7D, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x7D, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x1F, 0xE0, 0x7F, 0xF1, 0xFF,
0xF3, 0xF7, 0xEF, 0x83, 0xDE, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xE0, 0x7F, 0xE0, 0xFF, 0xF7, 0xE7, 0xFF,
0xC7, 0xFF, 0x07, 0xF8, 0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xF7, 0xDF, 0x7F, 0xF7, 0xFF, 0x78, 0xF7, 0xDF, 0x7F, 0xF3,
0xFE, 0x1F, 0xC0, 0x1E, 0xE0, 0x3D, 0xE0, 0x7B, 0x80, 0x00, 0x01, 0xFE, 0x07, 0xFF, 0x1F, 0xFF, 0x3F, 0x7E, 0xF8,
0x3D, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xFE, 0x07, 0xFE, 0x0F, 0xFF, 0x7E, 0x7F, 0xFC, 0x7F, 0xF0, 0x7F,
0x80, 0x3D, 0xC3, 0xDE, 0x3D, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x3F, 0xE7, 0xFF, 0x7D, 0xF7, 0xFF, 0x7F, 0xF7, 0x8F,
0x7D, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x3D, 0xE0, 0xF7, 0x83, 0xDE, 0x00, 0x00, 0x7F, 0xC1, 0xFF, 0x87, 0xFF, 0x1E,
0xFE, 0x00, 0x78, 0x01, 0xE1, 0xFF, 0xC7, 0xFF, 0x1F, 0xFC, 0x01, 0xF0, 0x07, 0x9E, 0xFE, 0x7F, 0xF1, 0xFF, 0x87,
0xFC, 0x00, 0x7B, 0x8F, 0x79, 0xEE, 0x00, 0x00, 0x00, 0xFE, 0x1F, 0xE3, 0xFE, 0x77, 0xC7, 0xFC, 0xFF, 0x81, 0xF7,
0x7C, 0xFF, 0xBF, 0xE7, 0xF8, 0x1F, 0xE0, 0x7F, 0x80, 0x00, 0x3E, 0x1F, 0x78, 0x79, 0xF1, 0xE3, 0xCF, 0x8F, 0xBC,
0x1E, 0xF0, 0x7F, 0xC0, 0xFE, 0x03, 0xF8, 0x07, 0xC0, 0x1F, 0x06, 0xF8, 0x1F, 0xE0, 0x7F, 0x01, 0xF8, 0x00, 0x3F,
0xC3, 0xFC, 0x00, 0x00, 0x00, 0xF1, 0xFF, 0x1E, 0x79, 0xE7, 0x9E, 0x7B, 0xC3, 0xFC, 0x3F, 0xC3, 0xF8, 0x1F, 0x81,
0xF0, 0x0F, 0x0D, 0xF0, 0xFE, 0x0F, 0xC0, 0xF8, 0x00, 0x1E, 0xE0, 0x7B, 0xC1, 0xEE, 0x00, 0x00, 0xF8, 0x7D, 0xE1,
0xE7, 0xC7, 0x8F, 0x3E, 0x3E, 0xF0, 0x7B, 0xC1, 0xFF, 0x03, 0xF8, 0x0F, 0xE0, 0x1F, 0x00, 0x7C, 0x1B, 0xE0, 0x7F,
0x81, 0xFC, 0x07, 0xE0, 0x00, 0x3B, 0xC7, 0xBC, 0x3B, 0xC0, 0x00, 0x00, 0x0F, 0x1F, 0xF1, 0xE7, 0x9E, 0x79, 0xE7,
0xBC, 0x3F, 0xC3, 0xFC, 0x3F, 0x81, 0xF8, 0x1F, 0x00, 0xF0, 0xDF, 0x0F, 0xE0, 0xFC, 0x0F, 0x80, 0x07, 0x70, 0x3D,
0xC0, 0xEF, 0x07, 0xF8, 0x1C, 0xC0, 0x00, 0x0F, 0x87, 0xDE, 0x1E, 0x7C, 0x78, 0xF3, 0xE3, 0xEF, 0x07, 0xBC, 0x1F,
0xF0, 0x3F, 0x80, 0xFE, 0x01, 0xF0, 0x07, 0xC1, 0xBE, 0x07, 0xF8, 0x1F, 0xC0, 0x7E, 0x00, 0x0C, 0xE1, 0xFE, 0x3D,
0xC3, 0xB8, 0x33, 0x80, 0x00, 0xF1, 0xFF, 0x1E, 0x79, 0xE7, 0x9E, 0x7B, 0xC3, 0xFC, 0x3F, 0xC3, 0xF8, 0x1F, 0x81,
0xF0, 0x0F, 0x0D, 0xF0, 0xFE, 0x0F, 0xC0, 0xF8, 0x00, 0x1D, 0xE1, 0xEF, 0x07, 0x78, 0x00, 0x0F, 0x87, 0xFC, 0x3F,
0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0xDF, 0x3E, 0xFF, 0xF7, 0xFF, 0x8F, 0xFC, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03,
0xC0, 0x1E, 0x3B, 0xCF, 0x78, 0xEF, 0x00, 0x00, 0x01, 0xF3, 0xFE, 0x7F, 0xCF, 0x79, 0xEF, 0x3D, 0xFF, 0xBF, 0xF3,
0xFE, 0x03, 0xC0, 0x78, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03,
0xC0, 0x78, 0x0F, 0xE1, 0xFC, 0x3F, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0x07,
0x83, 0xC1, 0xE0, 0xFC, 0x7E, 0x3F, 0x03, 0x81, 0xC0, 0xE0, 0x07, 0x78, 0x03, 0xFC, 0x01, 0xDE, 0x00, 0x00, 0x0F,
0x00, 0x7F, 0x80, 0x3F, 0xC0, 0x1F, 0xE0, 0x0F, 0xF0, 0x07, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFE, 0xFF, 0x3F, 0x7F,
0x87, 0xBF, 0xC3, 0xDF, 0xE7, 0xEF, 0xFF, 0xF7, 0xFF, 0xF3, 0xFF, 0xE1, 0xE0, 0x0E, 0xF0, 0x3D, 0xE0, 0x3B, 0xC0,
0x00, 0x00, 0x00, 0x1E, 0x03, 0xFC, 0x07, 0xF8, 0x0F, 0xF0, 0x1F, 0xFF, 0x3F, 0xFF, 0x7F, 0xFE, 0xFF, 0x3F, 0xFF,
0xFB, 0xFF, 0xF7, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x3B, 0xDE, 0xEF, 0x78, 0x7B, 0xDF, 0xEF, 0x70, 0x7B, 0xDF, 0xEF, 0x70,
0x3B, 0xDE, 0xE7, 0xF9, 0xDE, 0xF7, 0xBD, 0xE0, 0x7F, 0xBF, 0xDD, 0xFE, 0xFF, 0x77, 0x38, 0x7F, 0xBF, 0xDD, 0xFE,
0xFF, 0x77, 0x38, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E,
0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x1E, 0x07,
0x8F, 0xFF, 0xFF, 0xFF, 0xC7, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0x7C, 0xFF, 0xFF, 0xF7, 0xEF, 0x80, 0xF0, 0xE1,
0xFF, 0x3E, 0x7F, 0xE7, 0xCF, 0xF8, 0x70, 0xF0, 0x3E, 0x1E, 0x00, 0x0F, 0xE3, 0xC0, 0x01, 0xDC, 0xF0, 0x00, 0x7B,
0xDE, 0x00, 0x0F, 0x7F, 0x80, 0x00, 0xEE, 0xF0, 0x00, 0x1F, 0xFC, 0x00, 0x01, 0xF7, 0xFE, 0x7E, 0x01, 0xFF, 0xCF,
0xE0, 0x3B, 0xFF, 0xFC, 0x0F, 0x73, 0xF3, 0x83, 0xCE, 0x7E, 0x70, 0x79, 0xFF, 0xFE, 0x1E, 0x3F, 0x9F, 0xC3, 0xC3,
0xF3, 0xF0, 0x00, 0x38, 0xF3, 0xE7, 0x9F, 0x1E, 0x3E, 0x3C, 0x38, 0x00, 0x00, 0xE3, 0xE3, 0xE3, 0xC7, 0xCF, 0x3E,
0xF8, 0xE0, 0x00, 0x01, 0xE0, 0x3C, 0x0F, 0x01, 0xE0, 0x78, 0x0F, 0x03, 0xC0, 0x78, 0x1E, 0x03, 0xC0, 0xF0, 0x1E,
0x07, 0x80, 0xF0, 0x3C, 0x00, 0x07, 0xF0, 0xFF, 0x1F, 0xF1, 0xF6, 0x3C, 0x07, 0xFE, 0x7F, 0xC3, 0xC0, 0x7F, 0xC7,
0xFC, 0x3C, 0x03, 0xF7, 0x1F, 0xF0, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0x0F, 0xC0, 0xFC, 0x1F,
0xC3, 0xF0, 0x3F, 0xC3, 0xFC, 0x1F, 0xC3, 0xF0, 0x3F, 0x03, 0xF0, 0x0F, 0x00, 0x3F, 0x87, 0xFC, 0x3F, 0xE3, 0xBE,
0xFF, 0xFF, 0xFF, 0x1F, 0x83, 0xF0, 0xFF, 0xFF, 0xFF, 0x78, 0x07, 0xDE, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0xFF, 0xFF,
0xFF, 0xC1, 0xF0, 0x07, 0xCF, 0xFF, 0x7F, 0xF8, 0x0F, 0x01, 0xF8, 0xFF, 0x87, 0xF8, 0x1F, 0x00, 0x7C, 0x01, 0xF0,
0x07, 0xC0, 0x1F, 0x00, 0x3F, 0x83, 0xFC, 0x3F, 0xE3, 0xBF, 0x00, 0xF1, 0xFF, 0x3F, 0xF7, 0xFF, 0x7D, 0xFF, 0x0F,
0xF0, 0xFF, 0xBE, 0x7F, 0xE7, 0xFC, 0x3F, 0x80, 0x07, 0x80, 0x0F, 0x80, 0x3F, 0x00, 0x7F, 0x01, 0xFE, 0x03, 0xDC,
0x07, 0xBC, 0x1E, 0x78, 0x3C, 0x78, 0x78, 0xF1, 0xE1, 0xE3, 0xC1, 0xE7, 0xFF, 0xDF, 0xFF, 0xBF, 0xFF, 0x80, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF,
0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x78, 0xFF, 0xFF, 0xFF, 0xFF, 0xBC, 0x07, 0xC0, 0x7C, 0x07, 0x80, 0xF8,
0x0F, 0x83, 0xE0, 0x78, 0x1E, 0x07, 0xC0, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFC, 0x01, 0xE0,
0x3C, 0x0F, 0x01, 0xE0, 0x78, 0x0F, 0x03, 0xC0, 0x78, 0x1E, 0x03, 0xC0, 0xF0, 0x1E, 0x07, 0x80, 0xF0, 0x3C, 0x00,
0x77, 0xFE, 0xE0, 0x00, 0x3C, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x03, 0xC0, 0x0F, 0x0F, 0x38, 0xFD, 0xE3,
0xF7, 0x87, 0xFC, 0x07, 0xF0, 0x0F, 0xC0, 0x3E, 0x00, 0x78, 0x00, 0x7D, 0xF3, 0xFF, 0xFF, 0xFF, 0xEE, 0x7F, 0xFF,
0xBF, 0xFD, 0xF7, 0xC0, 0x00, 0x07, 0xC7, 0xE7, 0xF3, 0xE1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1,
0xE0, 0xF0, 0x78, 0x7C, 0x7E, 0x3F, 0x1F, 0x00, 0x00, 0x3C, 0x77, 0xFF, 0x7F, 0xEF, 0xFE, 0xE3, 0xC0, 0x00, 0x3C,
0x77, 0xFF, 0x7F, 0xEF, 0xFE, 0xE3, 0xC0, 0x03, 0x80, 0xE0, 0x7B, 0xFF, 0xFF, 0xFF, 0xF0, 0xE0, 0x70, 0xFF, 0xFF,
0xFF, 0xFD, 0xC0, 0x70, 0x1C, 0x00, 0x07, 0xDF, 0xFF, 0xFF, 0xFE, 0xF8, 0x3F, 0xEF, 0xFD, 0xFF, 0x07, 0xC0, 0x0F,
0xFF, 0xFF, 0xFF, 0xC0, 0xF8, 0x3F, 0xCF, 0xFD, 0xFF, 0x0F, 0xDF, 0xFF, 0xFF, 0xFE, 0xF8, 0x00, 0x0F, 0xFF, 0xFF,
0xFF, 0xC0,
};
static const EpdGlyph ubuntu_bold_10Glyphs[] = {
@@ -758,54 +1059,269 @@ static const EpdGlyph ubuntu_bold_10Glyphs[] = {
{10, 17, 11, 0, 17, 22, 7407}, // ž
{9, 16, 7, 1, 16, 18, 7429}, // ſ
{8, 3, 8, 0, 15, 3, 7447}, // ̑
{11, 3, 11, 0, 8, 5, 7450}, //
{21, 3, 21, 0, 8, 8, 7455}, //
{21, 3, 21, 0, 8, 8, 7463}, //
{5, 6, 5, 0, 16, 4, 7471}, //
{5, 6, 5, 0, 16, 4, 7475}, //
{5, 6, 5, 0, 3, 4, 7479}, //
{10, 6, 10, 0, 16, 8, 7483}, //
{9, 6, 10, 0, 16, 7, 7491}, //
{9, 6, 10, 0, 3, 7, 7498}, //
{10, 18, 10, 0, 15, 23, 7505}, //
{10, 18, 10, 0, 15, 23, 7528}, //
{7, 6, 8, 0, 10, 6, 7551}, //
{19, 4, 21, 1, 4, 10, 7557}, //
{27, 15, 28, 0, 15, 51, 7567}, //
{7, 11, 7, 0, 11, 10, 7618}, //
{7, 11, 7, 0, 11, 10, 7628}, //
{11, 15, 3, -4, 15, 21, 7638}, //
{12, 15, 12, 0, 15, 23, 7659}, //
{12, 15, 12, 0, 15, 23, 7682}, //
{12, 15, 12, 0, 15, 23, 7705}, //
{13, 15, 12, 0, 15, 25, 7728}, //
{12, 15, 13, 0, 15, 23, 7753}, //
{15, 15, 15, 0, 15, 29, 7776}, //
{13, 17, 15, 1, 15, 28, 7805}, //
{11, 17, 11, 0, 15, 24, 7833}, //
{10, 3, 12, 1, 8, 4, 7857}, //
{11, 15, 3, -4, 15, 21, 7861}, //
{5, 4, 5, 0, 9, 3, 7882}, //
{14, 15, 13, 0, 15, 27, 7885}, //
{13, 7, 13, 0, 10, 12, 7912}, //
{9, 21, 8, 0, 16, 24, 7924}, //
{12, 11, 12, 0, 12, 17, 7948}, //
{10, 14, 12, 1, 14, 18, 7965}, //
{10, 13, 12, 1, 13, 17, 7983}, //
{10, 13, 12, 1, 13, 17, 8000}, //
{11, 21, 13, 1, 21, 29, 7450}, // Ѐ
{11, 20, 13, 1, 20, 28, 7479}, // Ё
{17, 15, 17, 0, 15, 32, 7507}, // Ђ
{11, 21, 12, 1, 21, 29, 7539}, // Ѓ
{13, 15, 14, 1, 15, 25, 7568}, // Є
{12, 15, 12, 0, 15, 23, 7593}, // Ѕ
{4, 15, 7, 1, 15, 8, 7616}, // І
{9, 19, 7, -1, 19, 22, 7624}, // Ї
{10, 15, 11, 0, 15, 19, 7646}, // Ј
{23, 15, 24, 0, 15, 44, 7665}, // Љ
{21, 15, 23, 1, 15, 40, 7709}, // Њ
{16, 15, 16, 0, 15, 30, 7749}, // Ћ
{14, 21, 14, 1, 21, 37, 7779}, // Ќ
{14, 21, 16, 1, 21, 37, 7816}, // Ѝ
{14, 20, 14, 0, 20, 35, 7853}, // Ў
{13, 19, 15, 1, 15, 31, 7888}, // Џ
{15, 15, 15, 0, 15, 29, 7919}, // А
{12, 15, 14, 1, 15, 23, 7948}, // Б
{13, 15, 14, 1, 15, 25, 7971}, // В
{11, 15, 12, 1, 15, 21, 7996}, // Г
{17, 19, 17, 0, 15, 41, 8017}, // Д
{11, 15, 13, 1, 15, 21, 8058}, // Е
{21, 15, 21, 0, 15, 40, 8079}, // Ж
{12, 15, 13, 0, 15, 23, 8119}, // З
{14, 15, 16, 1, 15, 27, 8142}, // И
{14, 20, 16, 1, 20, 35, 8169}, // Й
{14, 15, 14, 1, 15, 27, 8204}, // К
{15, 15, 17, 0, 15, 29, 8231}, // Л
{17, 15, 19, 1, 15, 32, 8260}, // М
{13, 15, 15, 1, 15, 25, 8292}, // Н
{15, 15, 17, 1, 15, 29, 8317}, // О
{13, 15, 15, 1, 15, 25, 8346}, // П
{12, 15, 14, 1, 15, 23, 8371}, // Р
{13, 15, 14, 1, 15, 25, 8394}, // С
{13, 15, 13, 0, 15, 25, 8419}, // Т
{14, 15, 14, 0, 15, 27, 8444}, // У
{18, 16, 18, 0, 16, 36, 8471}, // Ф
{14, 15, 14, 0, 15, 27, 8507}, // Х
{16, 19, 17, 1, 15, 38, 8534}, // Ц
{13, 15, 14, 0, 15, 25, 8572}, // Ч
{19, 15, 21, 1, 15, 36, 8597}, // Ш
{22, 19, 23, 1, 15, 53, 8633}, // Щ
{16, 15, 16, 0, 15, 30, 8686}, // Ъ
{17, 15, 19, 1, 15, 32, 8716}, // Ы
{12, 15, 14, 1, 15, 23, 8748}, // Ь
{14, 15, 14, 0, 15, 27, 8771}, // Э
{21, 15, 23, 1, 15, 40, 8798}, // Ю
{13, 15, 14, 0, 15, 25, 8838}, // Я
{11, 11, 12, 0, 11, 16, 8863}, // а
{12, 16, 13, 0, 16, 24, 8879}, // б
{11, 11, 13, 1, 11, 16, 8903}, // в
{9, 11, 10, 1, 11, 13, 8919}, // г
{14, 14, 14, 0, 11, 25, 8932}, // д
{12, 11, 12, 0, 11, 17, 8957}, // е
{18, 11, 18, 0, 11, 25, 8974}, // ж
{10, 11, 10, 0, 11, 14, 8999}, // з
{11, 11, 13, 1, 11, 16, 9013}, // и
{11, 17, 13, 1, 17, 24, 9029}, // й
{12, 11, 12, 1, 11, 17, 9053}, // к
{12, 11, 13, 0, 11, 17, 9070}, // л
{14, 11, 16, 1, 11, 20, 9087}, // м
{10, 11, 12, 1, 11, 14, 9107}, // н
{12, 11, 13, 0, 11, 17, 9121}, // о
{10, 11, 12, 1, 11, 14, 9138}, // п
{11, 15, 13, 1, 11, 21, 9152}, // р
{10, 11, 11, 0, 11, 14, 9173}, // с
{11, 11, 11, 0, 11, 16, 9187}, // т
{12, 15, 11, 0, 11, 23, 9203}, // у
{16, 20, 17, 0, 16, 40, 9226}, // ф
{12, 11, 12, 0, 11, 17, 9266}, // х
{12, 14, 13, 1, 11, 21, 9283}, // ц
{11, 11, 12, 0, 11, 16, 9304}, // ч
{16, 11, 18, 1, 11, 22, 9320}, // ш
{18, 14, 19, 1, 11, 32, 9342}, // щ
{13, 11, 13, 0, 11, 18, 9374}, // ъ
{15, 11, 17, 1, 11, 21, 9392}, // ы
{11, 11, 12, 1, 11, 16, 9413}, // ь
{11, 11, 11, 0, 11, 16, 9429}, // э
{17, 11, 18, 1, 11, 24, 9445}, // ю
{11, 11, 12, 0, 11, 16, 9469}, // я
{12, 17, 12, 0, 17, 26, 9485}, // ѐ
{12, 16, 12, 0, 16, 24, 9511}, // ё
{12, 20, 13, 0, 16, 30, 9535}, // ђ
{9, 17, 10, 1, 17, 20, 9565}, // ѓ
{11, 11, 11, 0, 11, 16, 9585}, // є
{10, 11, 10, 0, 11, 14, 9601}, // ѕ
{4, 16, 6, 1, 16, 8, 9615}, // і
{9, 16, 6, -2, 16, 18, 9623}, // ї
{7, 20, 6, -2, 16, 18, 9641}, // ј
{19, 11, 19, 0, 11, 27, 9659}, // љ
{17, 11, 18, 1, 11, 24, 9686}, // њ
{12, 16, 13, 0, 16, 24, 9710}, // ћ
{12, 17, 12, 1, 17, 26, 9734}, // ќ
{11, 17, 13, 1, 17, 24, 9760}, // ѝ
{12, 21, 11, 0, 17, 32, 9784}, // ў
{10, 14, 12, 1, 11, 18, 9816}, // џ
{15, 16, 16, 0, 16, 30, 9834}, // Ѣ
{13, 16, 13, 0, 16, 26, 9864}, // ѣ
{15, 15, 17, 1, 15, 29, 9890}, // Ѳ
{12, 11, 13, 0, 11, 17, 9919}, // ѳ
{18, 15, 17, 0, 15, 34, 9936}, // Ѵ
{13, 11, 13, 0, 11, 18, 9970}, // ѵ
{16, 23, 17, 1, 19, 46, 9988}, // Ҋ
{13, 19, 13, 1, 16, 31, 10034}, // ҋ
{13, 15, 14, 0, 15, 25, 10065}, // Ҍ
{12, 11, 12, 0, 11, 17, 10090}, // ҍ
{12, 15, 14, 1, 15, 23, 10107}, // Ҏ
{11, 15, 13, 1, 11, 21, 10130}, // ҏ
{11, 18, 12, 1, 18, 25, 10151}, // Ґ
{9, 14, 10, 1, 14, 16, 10176}, // ґ
{12, 15, 13, 1, 15, 23, 10192}, // Ғ
{10, 11, 11, 1, 11, 14, 10215}, // ғ
{13, 19, 14, 1, 15, 31, 10229}, // Ҕ
{11, 15, 12, 1, 11, 21, 10260}, // ҕ
{22, 19, 21, 0, 15, 53, 10281}, // Җ
{18, 14, 18, 0, 11, 32, 10334}, // җ
{12, 20, 13, 0, 15, 30, 10366}, // Ҙ
{10, 16, 10, 0, 11, 20, 10396}, // ҙ
{15, 19, 15, 1, 15, 36, 10416}, // Қ
{12, 14, 13, 1, 11, 21, 10452}, // қ
{16, 15, 16, 1, 15, 30, 10473}, // Ҝ
{14, 11, 14, 1, 11, 20, 10503}, // ҝ
{15, 16, 15, 0, 16, 30, 10523}, // Ҟ
{13, 12, 13, 0, 12, 20, 10553}, // ҟ
{17, 15, 17, 0, 15, 32, 10573}, // Ҡ
{14, 11, 14, 0, 11, 20, 10605}, // ҡ
{16, 19, 17, 1, 15, 38, 10625}, // Ң
{12, 14, 13, 1, 11, 21, 10663}, // ң
{17, 15, 18, 1, 15, 32, 10684}, // Ҥ
{13, 11, 14, 1, 11, 18, 10716}, // ҥ
{22, 19, 23, 1, 15, 53, 10734}, // Ҧ
{17, 15, 18, 1, 11, 32, 10787}, // ҧ
{16, 19, 17, 1, 15, 38, 10819}, // Ҩ
{13, 14, 13, 0, 11, 23, 10857}, // ҩ
{13, 20, 14, 1, 15, 33, 10880}, // Ҫ
{10, 15, 11, 0, 11, 19, 10913}, // ҫ
{13, 19, 13, 0, 15, 31, 10932}, // Ҭ
{11, 14, 11, 0, 11, 20, 10963}, // ҭ
{14, 15, 14, 0, 15, 27, 10983}, // Ү
{12, 15, 12, 0, 11, 23, 11010}, // ү
{14, 15, 14, 0, 15, 27, 11033}, // Ұ
{12, 15, 12, 0, 11, 23, 11060}, // ұ
{15, 19, 15, 0, 15, 36, 11083}, // Ҳ
{12, 14, 12, 0, 11, 21, 11119}, // ҳ
{20, 19, 20, 0, 15, 48, 11140}, // Ҵ
{15, 14, 15, 0, 11, 27, 11188}, // ҵ
{14, 19, 15, 1, 15, 34, 11215}, // Ҷ
{13, 14, 12, 0, 11, 23, 11249}, // ҷ
{12, 15, 14, 1, 15, 23, 11272}, // Ҹ
{11, 11, 12, 0, 11, 16, 11295}, // ҹ
{13, 15, 15, 1, 15, 25, 11311}, // Һ
{10, 16, 12, 1, 16, 20, 11336}, // һ
{18, 15, 18, 0, 15, 34, 11356}, // Ҽ
{14, 11, 15, 0, 11, 20, 11390}, // ҽ
{18, 19, 18, 0, 15, 43, 11410}, // Ҿ
{14, 14, 15, 0, 11, 25, 11453}, // ҿ
{4, 15, 7, 1, 15, 8, 11478}, // Ӏ
{21, 20, 21, 0, 20, 53, 11486}, // Ӂ
{18, 17, 18, 0, 17, 39, 11539}, // ӂ
{13, 19, 15, 1, 15, 31, 11578}, // Ӄ
{11, 15, 12, 1, 11, 21, 11609}, // ӄ
{18, 19, 17, 0, 15, 43, 11630}, // Ӆ
{14, 14, 14, 0, 11, 25, 11673}, // ӆ
{13, 19, 15, 1, 15, 31, 11698}, // Ӈ
{11, 15, 13, 1, 11, 21, 11729}, // ӈ
{16, 19, 17, 1, 15, 38, 11750}, // Ӊ
{12, 14, 13, 1, 11, 21, 11788}, // ӊ
{13, 19, 14, 0, 15, 31, 11809}, // Ӌ
{11, 14, 12, 0, 11, 20, 11840}, // ӌ
{19, 19, 20, 1, 15, 46, 11860}, // Ӎ
{16, 14, 17, 1, 11, 28, 11906}, // ӎ
{4, 15, 6, 1, 15, 8, 11934}, // ӏ
{15, 20, 15, 0, 20, 38, 11942}, // Ӑ
{11, 17, 12, 0, 17, 24, 11980}, // ӑ
{15, 19, 15, 0, 19, 36, 12004}, // Ӓ
{11, 16, 12, 0, 16, 22, 12040}, // ӓ
{21, 15, 21, 0, 15, 40, 12062}, // Ӕ
{18, 11, 18, 0, 11, 25, 12102}, // ӕ
{11, 20, 13, 1, 20, 28, 12127}, // Ӗ
{12, 17, 12, 0, 17, 26, 12155}, // ӗ
{14, 15, 15, 1, 15, 27, 12181}, // Ә
{12, 11, 12, 0, 11, 17, 12208}, // ә
{14, 19, 15, 1, 19, 34, 12225}, // Ӛ
{12, 16, 12, 0, 16, 24, 12259}, // ӛ
{21, 19, 21, 0, 19, 50, 12283}, // Ӝ
{18, 16, 18, 0, 16, 36, 12333}, // ӝ
{12, 20, 13, 0, 20, 30, 12369}, // Ӟ
{10, 16, 10, 0, 16, 20, 12399}, // ӟ
{12, 15, 13, 0, 15, 23, 12419}, // Ӡ
{10, 15, 11, 0, 11, 19, 12442}, // ӡ
{14, 18, 16, 1, 18, 32, 12461}, // Ӣ
{11, 15, 13, 1, 15, 21, 12493}, // ӣ
{14, 19, 16, 1, 19, 34, 12514}, // Ӥ
{11, 16, 13, 1, 16, 22, 12548}, // ӥ
{15, 19, 17, 1, 19, 36, 12570}, // Ӧ
{12, 16, 13, 0, 16, 24, 12606}, // ӧ
{15, 15, 17, 1, 15, 29, 12630}, // Ө
{12, 11, 13, 0, 11, 17, 12659}, // ө
{15, 19, 17, 1, 19, 36, 12676}, // Ӫ
{12, 16, 13, 0, 16, 24, 12712}, // ӫ
{14, 19, 14, 0, 19, 34, 12736}, // Ӭ
{11, 16, 11, 0, 16, 22, 12770}, // ӭ
{14, 18, 14, 0, 18, 32, 12792}, // Ӯ
{12, 19, 11, 0, 15, 29, 12824}, // ӯ
{14, 19, 14, 0, 19, 34, 12853}, // Ӱ
{12, 20, 11, 0, 16, 30, 12887}, // ӱ
{14, 21, 14, 0, 21, 37, 12917}, // Ӳ
{12, 21, 11, 0, 17, 32, 12954}, // ӳ
{13, 19, 14, 0, 19, 31, 12986}, // Ӵ
{11, 16, 12, 0, 16, 22, 13017}, // ӵ
{11, 19, 12, 1, 15, 27, 13039}, // Ӷ
{9, 14, 10, 1, 11, 16, 13066}, // ӷ
{17, 19, 19, 1, 19, 41, 13082}, // Ӹ
{15, 16, 17, 1, 16, 30, 13123}, // ӹ
{11, 3, 11, 0, 8, 5, 13153}, //
{21, 3, 21, 0, 8, 8, 13158}, // —
{21, 3, 21, 0, 8, 8, 13166}, // ―
{5, 6, 5, 0, 16, 4, 13174}, //
{5, 6, 5, 0, 16, 4, 13178}, //
{5, 6, 5, 0, 3, 4, 13182}, //
{10, 6, 10, 0, 16, 8, 13186}, // “
{9, 6, 10, 0, 16, 7, 13194}, // ”
{9, 6, 10, 0, 3, 7, 13201}, // „
{10, 18, 10, 0, 15, 23, 13208}, // †
{10, 18, 10, 0, 15, 23, 13231}, // ‡
{7, 6, 8, 0, 10, 6, 13254}, // •
{19, 4, 21, 1, 4, 10, 13260}, // …
{27, 15, 28, 0, 15, 51, 13270}, // ‰
{7, 11, 7, 0, 11, 10, 13321}, //
{7, 11, 7, 0, 11, 10, 13331}, //
{11, 15, 3, -4, 15, 21, 13341}, //
{12, 15, 12, 0, 15, 23, 13362}, // €
{12, 15, 12, 0, 15, 23, 13385}, // ₮
{12, 15, 12, 0, 15, 23, 13408}, // ₴
{13, 15, 12, 0, 15, 25, 13431}, // ₹
{12, 15, 13, 0, 15, 23, 13456}, // ∂
{15, 15, 15, 0, 15, 29, 13479}, // ∆
{13, 17, 15, 1, 15, 28, 13508}, // ∏
{11, 17, 11, 0, 15, 24, 13536}, // ∑
{10, 3, 12, 1, 8, 4, 13560}, //
{11, 15, 3, -4, 15, 21, 13564}, //
{5, 4, 5, 0, 9, 3, 13585}, // ∙
{14, 15, 13, 0, 15, 27, 13588}, // √
{13, 7, 13, 0, 10, 12, 13615}, // ∞
{9, 21, 8, 0, 16, 24, 13627}, // ∫
{12, 11, 12, 0, 12, 17, 13651}, // ≈
{10, 14, 12, 1, 14, 18, 13668}, // ≠
{10, 13, 12, 1, 13, 17, 13686}, // ≤
{10, 13, 12, 1, 13, 17, 13703}, // ≥
};
static const EpdUnicodeInterval ubuntu_bold_10Intervals[] = {
{0x0, 0x0, 0x0}, {0x8, 0x9, 0x1}, {0xD, 0xD, 0x3}, {0x1D, 0x1D, 0x4},
{0x20, 0x7E, 0x5}, {0xA0, 0xFF, 0x64}, {0x100, 0x17F, 0xC4}, {0x311, 0x311, 0x144},
{0x2013, 0x2015, 0x145}, {0x2018, 0x201A, 0x148}, {0x201C, 0x201E, 0x14B}, {0x2020, 0x2022, 0x14E},
{0x2026, 0x2026, 0x151}, {0x2030, 0x2030, 0x152}, {0x2039, 0x203A, 0x153}, {0x2044, 0x2044, 0x155},
{0x20AC, 0x20AC, 0x156}, {0x20AE, 0x20AE, 0x157}, {0x20B4, 0x20B4, 0x158}, {0x20B9, 0x20B9, 0x159},
{0x2202, 0x2202, 0x15A}, {0x2206, 0x2206, 0x15B}, {0x220F, 0x220F, 0x15C}, {0x2211, 0x2212, 0x15D},
{0x2215, 0x2215, 0x15F}, {0x2219, 0x221A, 0x160}, {0x221E, 0x221E, 0x162}, {0x222B, 0x222B, 0x163},
{0x2248, 0x2248, 0x164}, {0x2260, 0x2260, 0x165}, {0x2264, 0x2265, 0x166},
{0x400, 0x45F, 0x145}, {0x462, 0x463, 0x1A5}, {0x472, 0x475, 0x1A7}, {0x48A, 0x4F9, 0x1AB},
{0x2013, 0x2015, 0x21B}, {0x2018, 0x201A, 0x21E}, {0x201C, 0x201E, 0x221}, {0x2020, 0x2022, 0x224},
{0x2026, 0x2026, 0x227}, {0x2030, 0x2030, 0x228}, {0x2039, 0x203A, 0x229}, {0x2044, 0x2044, 0x22B},
{0x20AC, 0x20AC, 0x22C}, {0x20AE, 0x20AE, 0x22D}, {0x20B4, 0x20B4, 0x22E}, {0x20B9, 0x20B9, 0x22F},
{0x2202, 0x2202, 0x230}, {0x2206, 0x2206, 0x231}, {0x220F, 0x220F, 0x232}, {0x2211, 0x2212, 0x233},
{0x2215, 0x2215, 0x235}, {0x2219, 0x221A, 0x236}, {0x221E, 0x221E, 0x238}, {0x222B, 0x222B, 0x239},
{0x2248, 0x2248, 0x23A}, {0x2260, 0x2260, 0x23B}, {0x2264, 0x2265, 0x23C},
};
static const EpdFontData ubuntu_bold_10 = {
ubuntu_bold_10Bitmaps, ubuntu_bold_10Glyphs, ubuntu_bold_10Intervals, 31, 24, 20, -4, false,
ubuntu_bold_10Bitmaps, ubuntu_bold_10Glyphs, ubuntu_bold_10Intervals, 35, 24, 20, -4, false,
};
+1 -1
View File
@@ -54,7 +54,7 @@ intervals = [
# (0x0370, 0x03FF),
### Cyrillic ###
# Russian, Ukrainian, Bulgarian, etc.
# (0x0400, 0x04FF),
(0x0400, 0x04FF),
### Math Symbols (common subset) ###
# General math operators
(0x2200, 0x22FF),
+117 -192
View File
@@ -6,250 +6,169 @@
#include <map>
bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) {
// open up the meta data to find where the content.opf file lives
size_t s;
const auto metaInfo = reinterpret_cast<char*>(zip.readFileToMemory("META-INF/container.xml", &s, true));
if (!metaInfo) {
Serial.printf("[%lu] [EBP] Could not find META-INF/container.xml\n", millis());
#include "Epub/FsHelpers.h"
#include "Epub/parsers/ContainerParser.h"
#include "Epub/parsers/ContentOpfParser.h"
#include "Epub/parsers/TocNcxParser.h"
bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
const auto containerPath = "META-INF/container.xml";
size_t containerSize;
// Get file size without loading it all into heap
if (!getItemSize(containerPath, &containerSize)) {
Serial.printf("[%lu] [EBP] Could not find or size META-INF/container.xml\n", millis());
return false;
}
// parse the meta data
tinyxml2::XMLDocument metaDataDoc;
const auto result = metaDataDoc.Parse(metaInfo);
free(metaInfo);
ContainerParser containerParser(containerSize);
if (result != tinyxml2::XML_SUCCESS) {
Serial.printf("[%lu] [EBP] Could not parse META-INF/container.xml. Error: %d\n", millis(), result);
if (!containerParser.setup()) {
return false;
}
const auto container = metaDataDoc.FirstChildElement("container");
if (!container) {
Serial.printf("[%lu] [EBP] Could not find container element in META-INF/container.xml\n", millis());
// Stream read (reusing your existing stream logic)
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
containerParser.teardown();
return false;
}
const auto rootfiles = container->FirstChildElement("rootfiles");
if (!rootfiles) {
Serial.printf("[%lu] [EBP] Could not find rootfiles element in META-INF/container.xml\n", millis());
// Extract the result
if (containerParser.fullPath.empty()) {
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
containerParser.teardown();
return false;
}
// find the root file that has the media-type="application/oebps-package+xml"
auto rootfile = rootfiles->FirstChildElement("rootfile");
while (rootfile) {
const char* mediaType = rootfile->Attribute("media-type");
if (mediaType && strcmp(mediaType, "application/oebps-package+xml") == 0) {
const char* full_path = rootfile->Attribute("full-path");
if (full_path) {
contentOpfFile = full_path;
return true;
}
}
rootfile = rootfile->NextSiblingElement("rootfile");
}
*contentOpfFile = std::move(containerParser.fullPath);
Serial.printf("[%lu] [EBP] Could not get path to content.opf file\n", millis());
return false;
}
bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
// read in the content.opf file and parse it
auto contents = reinterpret_cast<char*>(zip.readFileToMemory(content_opf_file.c_str(), nullptr, true));
// parse the contents
tinyxml2::XMLDocument doc;
auto result = doc.Parse(contents);
free(contents);
if (result != tinyxml2::XML_SUCCESS) {
Serial.printf("[%lu] [EBP] Error parsing content.opf - %s\n", millis(),
tinyxml2::XMLDocument::ErrorIDToName(result));
return false;
}
auto package = doc.FirstChildElement("package");
if (!package) package = doc.FirstChildElement("opf:package");
if (!package) {
Serial.printf("[%lu] [EBP] Could not find package element in content.opf\n", millis());
return false;
}
// get the metadata - title and cover image
auto metadata = package->FirstChildElement("metadata");
if (!metadata) metadata = package->FirstChildElement("opf:metadata");
if (!metadata) {
Serial.printf("[%lu] [EBP] Missing metadata\n", millis());
return false;
}
auto titleEl = metadata->FirstChildElement("dc:title");
if (!titleEl) {
Serial.printf("[%lu] [EBP] Missing title\n", millis());
return false;
}
this->title = titleEl->GetText();
auto cover = metadata->FirstChildElement("meta");
if (!cover) cover = metadata->FirstChildElement("opf:meta");
while (cover && cover->Attribute("name") && strcmp(cover->Attribute("name"), "cover") != 0) {
cover = cover->NextSiblingElement("meta");
}
if (!cover) {
Serial.printf("[%lu] [EBP] Missing cover\n", millis());
}
auto coverItem = cover ? cover->Attribute("content") : nullptr;
// read the manifest and spine
// the manifest gives us the names of the files
// the spine gives us the order of the files
// we can then read the files in the order they are in the spine
auto manifest = package->FirstChildElement("manifest");
if (!manifest) manifest = package->FirstChildElement("opf:manifest");
if (!manifest) {
Serial.printf("[%lu] [EBP] Missing manifest\n", millis());
return false;
}
// create a mapping from id to file name
auto item = manifest->FirstChildElement("item");
if (!item) item = manifest->FirstChildElement("opf:item");
std::map<std::string, std::string> items;
while (item) {
std::string itemId = item->Attribute("id");
std::string href = contentBasePath + item->Attribute("href");
// grab the cover image
if (coverItem && itemId == coverItem) {
coverImageItem = href;
}
// grab the ncx file
if (itemId == "ncx" || itemId == "ncxtoc") {
tocNcxItem = href;
}
items[itemId] = href;
auto nextItem = item->NextSiblingElement("item");
if (!nextItem) nextItem = item->NextSiblingElement("opf:item");
item = nextItem;
}
// find the spine
auto spineEl = package->FirstChildElement("spine");
if (!spineEl) spineEl = package->FirstChildElement("opf:spine");
if (!spineEl) {
Serial.printf("[%lu] [EBP] Missing spine\n", millis());
return false;
}
// read the spine
auto itemref = spineEl->FirstChildElement("itemref");
if (!itemref) itemref = spineEl->FirstChildElement("opf:itemref");
while (itemref) {
auto id = itemref->Attribute("idref");
if (items.find(id) != items.end()) {
spine.emplace_back(id, items[id]);
}
auto nextItemRef = itemref->NextSiblingElement("itemref");
if (!nextItemRef) nextItemRef = itemref->NextSiblingElement("opf:itemref");
itemref = nextItemRef;
}
containerParser.teardown();
return true;
}
bool Epub::parseTocNcxFile(const ZipFile& zip) {
bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
size_t contentOpfSize;
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
return false;
}
ContentOpfParser opfParser(getBasePath(), contentOpfSize);
if (!opfParser.setup()) {
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
return false;
}
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
opfParser.teardown();
return false;
}
// Grab data from opfParser into epub
title = opfParser.title;
if (!opfParser.coverItemId.empty() && opfParser.items.count(opfParser.coverItemId) > 0) {
coverImageItem = opfParser.items.at(opfParser.coverItemId);
}
if (opfParser.items.count("ncx")) {
tocNcxItem = opfParser.items.at("ncx");
} else if (opfParser.items.count("ncxtoc")) {
tocNcxItem = opfParser.items.at("ncxtoc");
}
for (auto& spineRef : opfParser.spineRefs) {
if (opfParser.items.count(spineRef)) {
spine.emplace_back(spineRef, opfParser.items.at(spineRef));
}
}
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
opfParser.teardown();
return true;
}
bool Epub::parseTocNcxFile() {
// the ncx file should have been specified in the content.opf file
if (tocNcxItem.empty()) {
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
return false;
}
const auto ncxData = reinterpret_cast<char*>(zip.readFileToMemory(tocNcxItem.c_str(), nullptr, true));
if (!ncxData) {
Serial.printf("[%lu] [EBP] Could not find %s\n", millis(), tocNcxItem.c_str());
size_t tocSize;
if (!getItemSize(tocNcxItem, &tocSize)) {
Serial.printf("[%lu] [EBP] Could not get size of toc ncx\n", millis());
return false;
}
// Parse the Toc contents
tinyxml2::XMLDocument doc;
const auto result = doc.Parse(ncxData);
free(ncxData);
TocNcxParser ncxParser(contentBasePath, tocSize);
if (result != tinyxml2::XML_SUCCESS) {
Serial.printf("[%lu] [EBP] Error parsing toc %s\n", millis(), tinyxml2::XMLDocument::ErrorIDToName(result));
if (!ncxParser.setup()) {
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
return false;
}
const auto ncx = doc.FirstChildElement("ncx");
if (!ncx) {
Serial.printf("[%lu] [EBP] Could not find first child ncx in toc\n", millis());
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
Serial.printf("[%lu] [EBP] Could not read toc ncx stream\n", millis());
ncxParser.teardown();
return false;
}
const auto navMap = ncx->FirstChildElement("navMap");
if (!navMap) {
Serial.printf("[%lu] [EBP] Could not find navMap child in ncx\n", millis());
return false;
}
this->toc = std::move(ncxParser.toc);
recursivelyParseNavMap(navMap->FirstChildElement("navPoint"));
Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size());
ncxParser.teardown();
return true;
}
void Epub::recursivelyParseNavMap(tinyxml2::XMLElement* element) {
// Fills toc map
while (element) {
std::string navTitle = element->FirstChildElement("navLabel")->FirstChildElement("text")->FirstChild()->Value();
const auto content = element->FirstChildElement("content");
std::string href = contentBasePath + content->Attribute("src");
// split the href on the # to get the href and the anchor
const size_t pos = href.find('#');
std::string anchor;
if (pos != std::string::npos) {
anchor = href.substr(pos + 1);
href = href.substr(0, pos);
}
toc.emplace_back(navTitle, href, anchor, 0);
tinyxml2::XMLElement* nestedNavPoint = element->FirstChildElement("navPoint");
if (nestedNavPoint) {
recursivelyParseNavMap(nestedNavPoint);
}
element = element->NextSiblingElement("navPoint");
}
}
// load in the meta data for the epub file
bool Epub::load() {
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
ZipFile zip("/sd" + filepath);
std::string contentOpfFile;
if (!findContentOpfFile(zip, contentOpfFile)) {
Serial.printf("[%lu] [EBP] Could not open ePub\n", millis());
std::string contentOpfFilePath;
if (!findContentOpfFile(&contentOpfFilePath)) {
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
return false;
}
contentBasePath = contentOpfFile.substr(0, contentOpfFile.find_last_of('/') + 1);
Serial.printf("[%lu] [EBP] Found content.opf at: %s\n", millis(), contentOpfFilePath.c_str());
if (!parseContentOpf(zip, contentOpfFile)) {
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
if (!parseContentOpf(contentOpfFilePath)) {
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
return false;
}
if (!parseTocNcxFile(zip)) {
if (!parseTocNcxFile()) {
Serial.printf("[%lu] [EBP] Could not parse toc\n", millis());
return false;
}
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
return true;
}
void Epub::clearCache() const { SD.rmdir(cachePath.c_str()); }
bool Epub::clearCache() const {
if (!SD.exists(cachePath.c_str())) {
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
return true;
}
if (!FsHelpers::removeDir(cachePath.c_str())) {
Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis());
return false;
}
Serial.printf("[%lu] [EPB] Cache cleared successfully\n", millis());
return true;
}
void Epub::setupCacheDir() const {
if (SD.exists(cachePath.c_str())) {
@@ -329,6 +248,13 @@ bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, con
return zip.readFileToStream(path.c_str(), out, chunkSize);
}
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
const ZipFile zip("/sd" + filepath);
const std::string path = normalisePath(itemHref);
return zip.getInflatedFileSize(path.c_str(), size);
}
int Epub::getSpineItemsCount() const { return spine.size(); }
std::string& Epub::getSpineItem(const int spineIndex) {
@@ -376,6 +302,5 @@ int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
}
Serial.printf("[%lu] [EBP] TOC item not found\n", millis());
// not found - default to first item
return 0;
return -1;
}
+7 -17
View File
@@ -1,22 +1,13 @@
#pragma once
#include <Print.h>
#include <tinyxml2.h>
#include <string>
#include <unordered_map>
#include <vector>
class ZipFile;
#include "Epub/EpubTocEntry.h"
class EpubTocEntry {
public:
std::string title;
std::string href;
std::string anchor;
int level;
EpubTocEntry(std::string title, std::string href, std::string anchor, const int level)
: title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {}
};
class ZipFile;
class Epub {
// the title read from the EPUB meta data
@@ -36,11 +27,9 @@ class Epub {
// Uniq cache key based on filepath
std::string cachePath;
// find the path for the content.opf file
static bool findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile);
bool parseContentOpf(ZipFile& zip, std::string& content_opf_file);
bool parseTocNcxFile(const ZipFile& zip);
void recursivelyParseNavMap(tinyxml2::XMLElement* element);
bool findContentOpfFile(std::string* contentOpfFile) const;
bool parseContentOpf(const std::string& contentOpfFilePath);
bool parseTocNcxFile();
public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
@@ -50,7 +39,7 @@ class Epub {
~Epub() = default;
std::string& getBasePath() { return contentBasePath; }
bool load();
void clearCache() const;
bool clearCache() const;
void setupCacheDir() const;
const std::string& getCachePath() const;
const std::string& getPath() const;
@@ -59,6 +48,7 @@ class Epub {
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 getItemSize(const std::string& itemHref, size_t* size) const;
std::string& getSpineItem(int spineIndex);
int getSpineItemsCount() const;
EpubTocEntry& getTocItem(int tocTndex);
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <string>
class EpubTocEntry {
public:
std::string title;
std::string href;
std::string anchor;
int level;
EpubTocEntry(std::string title, std::string href, std::string anchor, const int level)
: title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {}
};
+36
View File
@@ -0,0 +1,36 @@
#include "FsHelpers.h"
#include <SD.h>
bool FsHelpers::removeDir(const char* path) {
// 1. Open the directory
File dir = SD.open(path);
if (!dir) {
return false;
}
if (!dir.isDirectory()) {
return false;
}
File file = dir.openNextFile();
while (file) {
String filePath = path;
if (!filePath.endsWith("/")) {
filePath += "/";
}
filePath += file.name();
if (file.isDirectory()) {
if (!removeDir(filePath.c_str())) {
return false;
}
} else {
if (!SD.remove(filePath.c_str())) {
return false;
}
}
file = dir.openNextFile();
}
return SD.rmdir(path);
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
class FsHelpers {
public:
static bool removeDir(const char* path);
};
+15 -14
View File
@@ -3,7 +3,7 @@
#include <HardwareSerial.h>
#include <Serialization.h>
constexpr uint8_t PAGE_FILE_VERSION = 1;
constexpr uint8_t PAGE_FILE_VERSION = 3;
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }
@@ -15,18 +15,18 @@ void PageLine::serialize(std::ostream& os) {
block->serialize(os);
}
PageLine* PageLine::deserialize(std::istream& is) {
int32_t xPos;
int32_t yPos;
std::unique_ptr<PageLine> PageLine::deserialize(std::istream& is) {
int16_t xPos;
int16_t yPos;
serialization::readPod(is, xPos);
serialization::readPod(is, yPos);
const auto tb = TextBlock::deserialize(is);
return new PageLine(tb, xPos, yPos);
auto tb = TextBlock::deserialize(is);
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
}
void Page::render(GfxRenderer& renderer, const int fontId) const {
for (const auto element : elements) {
for (auto& element : elements) {
element->render(renderer, fontId);
}
}
@@ -37,14 +37,14 @@ void Page::serialize(std::ostream& os) const {
const uint32_t count = elements.size();
serialization::writePod(os, count);
for (auto* el : elements) {
for (const auto& el : elements) {
// Only PageLine exists currently
serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine));
static_cast<PageLine*>(el)->serialize(os);
el->serialize(os);
}
}
Page* Page::deserialize(std::istream& is) {
std::unique_ptr<Page> Page::deserialize(std::istream& is) {
uint8_t version;
serialization::readPod(is, version);
if (version != PAGE_FILE_VERSION) {
@@ -52,7 +52,7 @@ Page* Page::deserialize(std::istream& is) {
return nullptr;
}
auto* page = new Page();
auto page = std::unique_ptr<Page>(new Page());
uint32_t count;
serialization::readPod(is, count);
@@ -62,10 +62,11 @@ Page* Page::deserialize(std::istream& is) {
serialization::readPod(is, tag);
if (tag == TAG_PageLine) {
auto* pl = PageLine::deserialize(is);
page->elements.push_back(pl);
auto pl = PageLine::deserialize(is);
page->elements.push_back(std::move(pl));
} else {
throw std::runtime_error("Unknown PageElement tag");
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
return nullptr;
}
}
+12 -15
View File
@@ -1,4 +1,7 @@
#pragma once
#include <utility>
#include <vector>
#include "blocks/TextBlock.h"
enum PageElementTag : uint8_t {
@@ -8,9 +11,9 @@ enum PageElementTag : uint8_t {
// represents something that has been added to a page
class PageElement {
public:
int xPos;
int yPos;
explicit PageElement(const int xPos, const int yPos) : xPos(xPos), yPos(yPos) {}
int16_t xPos;
int16_t yPos;
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
virtual ~PageElement() = default;
virtual void render(GfxRenderer& renderer, int fontId) = 0;
virtual void serialize(std::ostream& os) = 0;
@@ -18,27 +21,21 @@ class PageElement {
// a line from a block element
class PageLine final : public PageElement {
const TextBlock* block;
std::shared_ptr<TextBlock> block;
public:
PageLine(const TextBlock* block, const int xPos, const int yPos) : PageElement(xPos, yPos), block(block) {}
~PageLine() override { delete block; }
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), block(std::move(block)) {}
void render(GfxRenderer& renderer, int fontId) override;
void serialize(std::ostream& os) override;
static PageLine* deserialize(std::istream& is);
static std::unique_ptr<PageLine> deserialize(std::istream& is);
};
class Page {
public:
~Page() {
for (const auto element : elements) {
delete element;
}
}
// the list of block index and line numbers on this page
std::vector<PageElement*> elements;
std::vector<std::shared_ptr<PageElement>> elements;
void render(GfxRenderer& renderer, int fontId) const;
void serialize(std::ostream& os) const;
static Page* deserialize(std::istream& is);
static std::unique_ptr<Page> deserialize(std::istream& is);
};
+171
View File
@@ -0,0 +1,171 @@
#include "ParsedText.h"
#include <GfxRenderer.h>
#include <algorithm>
#include <cmath>
#include <functional>
#include <limits>
#include <vector>
constexpr int MAX_COST = std::numeric_limits<int>::max();
void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) {
if (word.empty()) return;
words.push_back(std::move(word));
wordStyles.push_back(fontStyle);
}
// Consumes data to minimize memory usage
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const int horizontalMargin,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
if (words.empty()) {
return;
}
const size_t totalWordCount = words.size();
const int pageWidth = renderer.getScreenWidth() - horizontalMargin;
const int spaceWidth = renderer.getSpaceWidth(fontId);
// width of 1em to indent first line of paragraph if Extra Spacing is enabled
const int indentWidth = (!extraParagraphSpacing) ? 1 * renderer.getTextWidth(fontId, "m", REGULAR) : 0;
std::vector<uint16_t> wordWidths;
wordWidths.reserve(totalWordCount);
auto wordsIt = words.begin();
auto wordStylesIt = wordStyles.begin();
while (wordsIt != words.end()) {
wordWidths.push_back(renderer.getTextWidth(fontId, wordsIt->c_str(), *wordStylesIt));
std::advance(wordsIt, 1);
std::advance(wordStylesIt, 1);
}
// DP table to store the minimum badness (cost) of lines starting at index i
std::vector<int> dp(totalWordCount);
// 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i'
std::vector<size_t> ans(totalWordCount);
// Base Case
dp[totalWordCount - 1] = 0;
ans[totalWordCount - 1] = totalWordCount - 1;
for (int i = totalWordCount - 2; i >= 0; --i) {
int currlen = -spaceWidth + indentWidth;
dp[i] = MAX_COST;
for (size_t j = i; j < totalWordCount; ++j) {
// Current line length: previous width + space + current word width
currlen += wordWidths[j] + spaceWidth;
if (currlen > pageWidth) {
break;
}
int cost;
if (j == totalWordCount - 1) {
cost = 0; // Last line
} else {
const int remainingSpace = pageWidth - currlen;
// Use long long for the square to prevent overflow
const long long cost_ll = static_cast<long long>(remainingSpace) * remainingSpace + dp[j + 1];
if (cost_ll > MAX_COST) {
cost = MAX_COST;
} else {
cost = static_cast<int>(cost_ll);
}
}
if (cost < dp[i]) {
dp[i] = cost;
ans[i] = j; // j is the index of the last word in this optimal line
}
}
}
// Stores the index of the word that starts the next line (last_word_index + 1)
std::vector<size_t> lineBreakIndices;
size_t currentWordIndex = 0;
constexpr size_t MAX_LINES = 1000;
while (currentWordIndex < totalWordCount) {
if (lineBreakIndices.size() >= MAX_LINES) {
break;
}
size_t nextBreakIndex = ans[currentWordIndex] + 1;
lineBreakIndices.push_back(nextBreakIndex);
currentWordIndex = nextBreakIndex;
}
// Initialize iterators for consumption
auto wordStartIt = words.begin();
auto wordStyleStartIt = wordStyles.begin();
size_t wordWidthIndex = 0;
size_t lastBreakAt = 0;
for (const size_t lineBreak : lineBreakIndices) {
const size_t lineWordCount = lineBreak - lastBreakAt;
// Calculate end iterators for the range to splice
auto wordEndIt = wordStartIt;
auto wordStyleEndIt = wordStyleStartIt;
std::advance(wordEndIt, lineWordCount);
std::advance(wordStyleEndIt, lineWordCount);
// Calculate total word width for this line
int lineWordWidthSum = 0;
for (size_t i = 0; i < lineWordCount; ++i) {
lineWordWidthSum += wordWidths[wordWidthIndex + i];
}
// Calculate spacing
int spareSpace = pageWidth - lineWordWidthSum;
if (wordWidthIndex == 0) {
spareSpace -= indentWidth;
}
int spacing = spaceWidth;
const bool isLastLine = lineBreak == totalWordCount;
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
spacing = spareSpace / (lineWordCount - 1);
}
// Calculate initial x position
uint16_t xpos = (wordWidthIndex == 0) ? indentWidth : 0;
if (style == TextBlock::RIGHT_ALIGN) {
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
} else if (style == TextBlock::CENTER_ALIGN) {
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
}
// Pre-calculate X positions for words
std::list<uint16_t> lineXPos;
for (size_t i = 0; i < lineWordCount; ++i) {
const uint16_t currentWordWidth = wordWidths[wordWidthIndex + i];
lineXPos.push_back(xpos);
xpos += currentWordWidth + spacing;
}
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
std::list<std::string> lineWords;
lineWords.splice(lineWords.begin(), words, wordStartIt, wordEndIt);
std::list<EpdFontStyle> lineWordStyles;
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyleStartIt, wordStyleEndIt);
processLine(
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
// Update pointers/indices for the next line
wordStartIt = wordEndIt;
wordStyleStartIt = wordStyleEndIt;
wordWidthIndex += lineWordCount;
lastBreakAt = lineBreak;
}
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <EpdFontFamily.h>
#include <cstdint>
#include <functional>
#include <list>
#include <memory>
#include <string>
#include "blocks/TextBlock.h"
class GfxRenderer;
class ParsedText {
std::list<std::string> words;
std::list<EpdFontStyle> wordStyles;
TextBlock::BLOCK_STYLE style;
bool extraParagraphSpacing;
public:
explicit ParsedText(const TextBlock::BLOCK_STYLE style, const bool extraParagraphSpacing)
: style(style), extraParagraphSpacing(extraParagraphSpacing) {}
~ParsedText() = default;
void addWord(std::string word, EpdFontStyle fontStyle);
void setStyle(const TextBlock::BLOCK_STYLE style) { this->style = style; }
TextBlock::BLOCK_STYLE getStyle() const { return style; }
bool isEmpty() const { return words.empty(); }
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, int horizontalMargin,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
};
+40 -19
View File
@@ -1,17 +1,17 @@
#include "Section.h"
#include <GfxRenderer.h>
#include <SD.h>
#include <Serialization.h>
#include <fstream>
#include "EpubHtmlParserSlim.h"
#include "FsHelpers.h"
#include "Page.h"
#include "Serialization.h"
#include "parsers/ChapterHtmlSlimParser.h"
constexpr uint8_t SECTION_FILE_VERSION = 3;
constexpr uint8_t SECTION_FILE_VERSION = 5;
void Section::onPageComplete(const Page* page) {
void Section::onPageComplete(std::unique_ptr<Page> page) {
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
std::ofstream outputFile("/sd" + filePath);
@@ -21,11 +21,11 @@ void Section::onPageComplete(const Page* page) {
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
pageCount++;
delete page;
}
void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
const int marginRight, const int marginBottom, const int marginLeft) const {
const int marginRight, const int marginBottom, const int marginLeft,
const bool extraParagraphSpacing) const {
std::ofstream outputFile(("/sd" + cachePath + "/section.bin").c_str());
serialization::writePod(outputFile, SECTION_FILE_VERSION);
serialization::writePod(outputFile, fontId);
@@ -34,12 +34,14 @@ void Section::writeCacheMetadata(const int fontId, const float lineCompression,
serialization::writePod(outputFile, marginRight);
serialization::writePod(outputFile, marginBottom);
serialization::writePod(outputFile, marginLeft);
serialization::writePod(outputFile, extraParagraphSpacing);
serialization::writePod(outputFile, pageCount);
outputFile.close();
}
bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
const int marginRight, const int marginBottom, const int marginLeft) {
const int marginRight, const int marginBottom, const int marginLeft,
const bool extraParagraphSpacing) {
if (!SD.exists(cachePath.c_str())) {
return false;
}
@@ -57,25 +59,28 @@ bool Section::loadCacheMetadata(const int fontId, const float lineCompression, c
serialization::readPod(inputFile, version);
if (version != SECTION_FILE_VERSION) {
inputFile.close();
clearCache();
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
clearCache();
return false;
}
int fileFontId, fileMarginTop, fileMarginRight, fileMarginBottom, fileMarginLeft;
float fileLineCompression;
bool fileExtraParagraphSpacing;
serialization::readPod(inputFile, fileFontId);
serialization::readPod(inputFile, fileLineCompression);
serialization::readPod(inputFile, fileMarginTop);
serialization::readPod(inputFile, fileMarginRight);
serialization::readPod(inputFile, fileMarginBottom);
serialization::readPod(inputFile, fileMarginLeft);
serialization::readPod(inputFile, fileExtraParagraphSpacing);
if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop ||
marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft) {
marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft ||
extraParagraphSpacing != fileExtraParagraphSpacing) {
inputFile.close();
clearCache();
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
clearCache();
return false;
}
}
@@ -91,10 +96,25 @@ void Section::setupCacheDir() const {
SD.mkdir(cachePath.c_str());
}
void Section::clearCache() const { SD.rmdir(cachePath.c_str()); }
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
bool Section::clearCache() const {
if (!SD.exists(cachePath.c_str())) {
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
return true;
}
if (!FsHelpers::removeDir(cachePath.c_str())) {
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis());
return false;
}
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis());
return true;
}
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
const int marginRight, const int marginBottom, const int marginLeft) {
const int marginRight, const int marginBottom, const int marginLeft,
const bool extraParagraphSpacing) {
const auto localPath = epub->getSpineItem(spineIndex);
// TODO: Should we get rid of this file all together?
@@ -114,8 +134,9 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
auto visitor = EpubHtmlParserSlim(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
marginBottom, marginLeft, [this](const Page* page) { this->onPageComplete(page); });
ChapterHtmlSlimParser visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
marginBottom, marginLeft, extraParagraphSpacing,
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); });
success = visitor.parseAndBuildPages();
SD.remove(tmpHtmlPath.c_str());
@@ -124,12 +145,12 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
return false;
}
writeCacheMetadata(fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft);
writeCacheMetadata(fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft, extraParagraphSpacing);
return true;
}
Page* Section::loadPageFromSD() const {
std::unique_ptr<Page> Section::loadPageFromSD() const {
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
if (!SD.exists(filePath.c_str() + 3)) {
Serial.printf("[%lu] [SCT] Page file does not exist: %s\n", millis(), filePath.c_str());
@@ -137,7 +158,7 @@ Page* Section::loadPageFromSD() const {
}
std::ifstream inputFile(filePath);
Page* p = Page::deserialize(inputFile);
auto page = Page::deserialize(inputFile);
inputFile.close();
return p;
return page;
}
+10 -8
View File
@@ -1,33 +1,35 @@
#pragma once
#include <memory>
#include "Epub.h"
class Page;
class GfxRenderer;
class Section {
Epub* epub;
std::shared_ptr<Epub> epub;
const int spineIndex;
GfxRenderer& renderer;
std::string cachePath;
void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft) const;
void onPageComplete(const Page* page);
int marginLeft, bool extraParagraphSpacing) const;
void onPageComplete(std::unique_ptr<Page> page);
public:
int pageCount = 0;
int currentPage = 0;
explicit Section(Epub* epub, const int spineIndex, GfxRenderer& renderer)
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
: epub(epub), spineIndex(spineIndex), renderer(renderer) {
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
}
~Section() = default;
bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft);
int marginLeft, bool extraParagraphSpacing);
void setupCacheDir() const;
void clearCache() const;
bool clearCache() const;
bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft);
Page* loadPageFromSD() const;
int marginLeft, bool extraParagraphSpacing);
std::unique_ptr<Page> loadPageFromSD() const;
};
+14 -167
View File
@@ -3,170 +3,17 @@
#include <GfxRenderer.h>
#include <Serialization.h>
void TextBlock::addWord(const std::string& word, const bool is_bold, const bool is_italic) {
if (word.length() == 0) return;
words.push_back(word);
wordStyles.push_back((is_bold ? BOLD_SPAN : 0) | (is_italic ? ITALIC_SPAN : 0));
}
std::list<TextBlock*> TextBlock::splitIntoLines(const GfxRenderer& renderer, const int fontId,
const int horizontalMargin) {
const int totalWordCount = words.size();
const int pageWidth = GfxRenderer::getScreenWidth() - horizontalMargin;
const int spaceWidth = renderer.getSpaceWidth(fontId);
words.shrink_to_fit();
wordStyles.shrink_to_fit();
wordXpos.reserve(totalWordCount);
// measure each word
uint16_t wordWidths[totalWordCount];
for (int i = 0; i < totalWordCount; i++) {
// measure the word
EpdFontStyle fontStyle = REGULAR;
if (wordStyles[i] & BOLD_SPAN) {
if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = BOLD_ITALIC;
} else {
fontStyle = BOLD;
}
} else if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = ITALIC;
}
const int width = renderer.getTextWidth(fontId, words[i].c_str(), fontStyle);
wordWidths[i] = width;
}
// now apply the dynamic programming algorithm to find the best line breaks
// DP table in which dp[i] represents cost of line starting with word words[i]
int dp[totalWordCount];
// Array in which ans[i] store index of last word in line starting with word
// word[i]
size_t ans[totalWordCount];
// If only one word is present then only one line is required. Cost of last
// line is zero. Hence cost of this line is zero. Ending point is also n-1 as
// single word is present
dp[totalWordCount - 1] = 0;
ans[totalWordCount - 1] = totalWordCount - 1;
// Make each word first word of line by iterating over each index in arr.
for (int i = totalWordCount - 2; i >= 0; i--) {
int currlen = -1;
dp[i] = INT_MAX;
// Variable to store possible minimum cost of line.
int cost;
// Keep on adding words in current line by iterating from starting word upto
// last word in arr.
for (int j = i; j < totalWordCount; j++) {
// Update the width of the words in current line + the space between two
// words.
currlen += wordWidths[j] + spaceWidth;
// If we're bigger than the current pagewidth then we can't add more words
if (currlen > pageWidth) break;
// if we've run out of words then this is last line and the cost should be
// 0 Otherwise the cost is the sqaure of the left over space + the costs
// of all the previous lines
if (j == totalWordCount - 1)
cost = 0;
else
cost = (pageWidth - currlen) * (pageWidth - currlen) + dp[j + 1];
// Check if this arrangement gives minimum cost for line starting with
// word words[i].
if (cost < dp[i]) {
dp[i] = cost;
ans[i] = j;
}
}
}
// We can now iterate through the answer to find the line break positions
std::list<uint16_t> lineBreaks;
for (size_t i = 0; i < totalWordCount;) {
i = ans[i] + 1;
if (i > totalWordCount) {
break;
}
lineBreaks.push_back(i);
// Text too big, just exit
if (lineBreaks.size() > 1000) {
break;
}
}
std::list<TextBlock*> lines;
// With the line breaks calculated we can now position the words along the
// line
int startWord = 0;
for (const auto lineBreak : lineBreaks) {
const int lineWordCount = lineBreak - startWord;
int lineWordWidthSum = 0;
for (int i = startWord; i < lineBreak; i++) {
lineWordWidthSum += wordWidths[i];
}
// Calculate spacing between words
const uint16_t spareSpace = pageWidth - lineWordWidthSum;
uint16_t spacing = spaceWidth;
// evenly space words if using justified style, not the last line, and at
// least 2 words
if (style == JUSTIFIED && lineBreak != lineBreaks.back() && lineWordCount >= 2) {
spacing = spareSpace / (lineWordCount - 1);
}
uint16_t xpos = 0;
if (style == RIGHT_ALIGN) {
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
} else if (style == CENTER_ALIGN) {
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
}
for (int i = startWord; i < lineBreak; i++) {
wordXpos[i] = xpos;
xpos += wordWidths[i] + spacing;
}
std::vector<std::string> lineWords;
std::vector<uint16_t> lineXPos;
std::vector<uint8_t> lineWordStyles;
lineWords.reserve(lineWordCount);
lineXPos.reserve(lineWordCount);
lineWordStyles.reserve(lineWordCount);
for (int i = startWord; i < lineBreak; i++) {
lineWords.push_back(words[i]);
lineXPos.push_back(wordXpos[i]);
lineWordStyles.push_back(wordStyles[i]);
}
const auto textLine = new TextBlock(lineWords, lineXPos, lineWordStyles, style);
lines.push_back(textLine);
startWord = lineBreak;
}
return lines;
}
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
auto wordIt = words.begin();
auto wordStylesIt = wordStyles.begin();
auto wordXposIt = wordXpos.begin();
for (int i = 0; i < words.size(); i++) {
// render the word
EpdFontStyle fontStyle = REGULAR;
if (wordStyles[i] & BOLD_SPAN && wordStyles[i] & ITALIC_SPAN) {
fontStyle = BOLD_ITALIC;
} else if (wordStyles[i] & BOLD_SPAN) {
fontStyle = BOLD;
} else if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = ITALIC;
}
renderer.drawText(fontId, x + wordXpos[i], y, words[i].c_str(), true, fontStyle);
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
std::advance(wordIt, 1);
std::advance(wordStylesIt, 1);
std::advance(wordXposIt, 1);
}
}
@@ -190,11 +37,11 @@ void TextBlock::serialize(std::ostream& os) const {
serialization::writePod(os, style);
}
TextBlock* TextBlock::deserialize(std::istream& is) {
std::unique_ptr<TextBlock> TextBlock::deserialize(std::istream& is) {
uint32_t wc, xc, sc;
std::vector<std::string> words;
std::vector<uint16_t> wordXpos;
std::vector<uint8_t> wordStyles;
std::list<std::string> words;
std::list<uint16_t> wordXpos;
std::list<EpdFontStyle> wordStyles;
BLOCK_STYLE style;
// words
@@ -215,5 +62,5 @@ TextBlock* TextBlock::deserialize(std::istream& is) {
// style
serialization::readPod(is, style);
return new TextBlock(words, wordXpos, wordStyles, style);
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style));
}
+18 -28
View File
@@ -1,50 +1,40 @@
#pragma once
#include <EpdFontFamily.h>
#include <list>
#include <memory>
#include <string>
#include <vector>
#include "Block.h"
enum SPAN_STYLE : uint8_t {
BOLD_SPAN = 1,
ITALIC_SPAN = 2,
};
enum BLOCK_STYLE : uint8_t {
JUSTIFIED = 0,
LEFT_ALIGN = 1,
CENTER_ALIGN = 2,
RIGHT_ALIGN = 3,
};
// represents a block of words in the html document
class TextBlock final : public Block {
// pointer to each word
std::vector<std::string> words;
// x position of each word
std::vector<uint16_t> wordXpos;
// the styles of each word
std::vector<uint8_t> wordStyles;
public:
enum BLOCK_STYLE : uint8_t {
JUSTIFIED = 0,
LEFT_ALIGN = 1,
CENTER_ALIGN = 2,
RIGHT_ALIGN = 3,
};
// the style of the block - left, center, right aligned
private:
std::list<std::string> words;
std::list<uint16_t> wordXpos;
std::list<EpdFontStyle> wordStyles;
BLOCK_STYLE style;
public:
explicit TextBlock(const BLOCK_STYLE style) : style(style) {}
explicit TextBlock(const std::vector<std::string>& words, const std::vector<uint16_t>& word_xpos,
// the styles of each word
const std::vector<uint8_t>& word_styles, const BLOCK_STYLE style)
: words(words), wordXpos(word_xpos), wordStyles(word_styles), style(style) {}
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos, std::list<EpdFontStyle> word_styles,
const BLOCK_STYLE style)
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
~TextBlock() override = default;
void addWord(const std::string& word, bool is_bold, bool is_italic);
void setStyle(const BLOCK_STYLE style) { this->style = style; }
BLOCK_STYLE getStyle() const { return style; }
bool isEmpty() override { return words.empty(); }
void layout(GfxRenderer& renderer) override {};
// given a renderer works out where to break the words into lines
std::list<TextBlock*> splitIntoLines(const GfxRenderer& renderer, int fontId, int horizontalMargin);
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
BlockType getType() override { return TEXT_BLOCK; }
void serialize(std::ostream& os) const;
static TextBlock* deserialize(std::istream& is);
static std::unique_ptr<TextBlock> deserialize(std::istream& is);
};
@@ -1,11 +1,11 @@
#include "EpubHtmlParserSlim.h"
#include "ChapterHtmlSlimParser.h"
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include <expat.h>
#include "Page.h"
#include "htmlEntities.h"
#include "../Page.h"
#include "../htmlEntities.h"
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
@@ -25,7 +25,7 @@ constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
const char* SKIP_TAGS[] = {"head", "table"};
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n'; }
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
// given the start and end of a tag, check to see if it matches a known tag
bool matches(const char* tag_name, const char* possible_tags[], const int possible_tag_count) {
@@ -38,7 +38,7 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
}
// start a new text block if needed
void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) {
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::BLOCK_STYLE style) {
if (currentTextBlock) {
// already have a text block running and it is empty - just reuse it
if (currentTextBlock->isEmpty()) {
@@ -46,15 +46,13 @@ void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) {
return;
}
currentTextBlock->finish();
makePages();
delete currentTextBlock;
}
currentTextBlock = new TextBlock(style);
currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing));
}
void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
(void)atts;
// Middle of skip
@@ -64,23 +62,7 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na
}
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
// const char* src = element.Attribute("src");
// if (src) {
// // don't leave an empty text block in the list
// // const BLOCK_STYLE style = currentTextBlock->get_style();
// if (currentTextBlock->isEmpty()) {
// delete currentTextBlock;
// currentTextBlock = nullptr;
// }
// // TODO: Fix this
// // blocks.push_back(new ImageBlock(m_base_path + src));
// // start a new text block - with the same style as before
// // startNewTextBlock(style);
// } else {
// // ESP_LOGE(TAG, "Could not find src attribute");
// }
// start skip
// TODO: Start processing image tags
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
@@ -94,13 +76,13 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na
}
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
self->startNewTextBlock(CENTER_ALIGN);
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
if (strcmp(name, "br") == 0) {
self->startNewTextBlock(self->currentTextBlock->getStyle());
} else {
self->startNewTextBlock(JUSTIFIED);
self->startNewTextBlock(TextBlock::JUSTIFIED);
}
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
@@ -111,21 +93,29 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na
self->depth += 1;
}
void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
// Middle of skip
if (self->skipUntilDepth < self->depth) {
return;
}
EpdFontStyle fontStyle = REGULAR;
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
fontStyle = BOLD_ITALIC;
} else if (self->boldUntilDepth < self->depth) {
fontStyle = BOLD;
} else if (self->italicUntilDepth < self->depth) {
fontStyle = ITALIC;
}
for (int i = 0; i < len; i++) {
if (isWhitespace(s[i])) {
// Currently looking at whitespace, if there's anything in the partWordBuffer, flush it
if (self->partWordBufferIndex > 0) {
self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
self->italicUntilDepth < self->depth);
self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
self->partWordBufferIndex = 0;
}
// Skip the whitespace char
@@ -135,8 +125,7 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s
// If we're about to run out of space, then cut the word off and start a new one
if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
self->italicUntilDepth < self->depth);
self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
self->partWordBufferIndex = 0;
}
@@ -144,8 +133,8 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s
}
}
void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
(void)name;
if (self->partWordBufferIndex > 0) {
@@ -158,9 +147,17 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1;
if (shouldBreakText) {
EpdFontStyle fontStyle = REGULAR;
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
fontStyle = BOLD_ITALIC;
} else if (self->boldUntilDepth < self->depth) {
fontStyle = BOLD;
} else if (self->italicUntilDepth < self->depth) {
fontStyle = ITALIC;
}
self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
self->italicUntilDepth < self->depth);
self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
self->partWordBufferIndex = 0;
}
}
@@ -183,8 +180,8 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name
}
}
bool EpubHtmlParserSlim::parseAndBuildPages() {
startNewTextBlock(JUSTIFIED);
bool ChapterHtmlSlimParser::parseAndBuildPages() {
startNewTextBlock(TextBlock::JUSTIFIED);
const XML_Parser parser = XML_ParserCreate(nullptr);
int done;
@@ -240,56 +237,45 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
// Process last page if there is still text
if (currentTextBlock) {
makePages();
completePageFn(currentPage);
currentPage = nullptr;
delete currentTextBlock;
currentTextBlock = nullptr;
completePageFn(std::move(currentPage));
currentPage.reset();
currentTextBlock.reset();
}
return true;
}
void EpubHtmlParserSlim::makePages() {
void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
const int pageHeight = GfxRenderer::getScreenHeight() - marginTop - marginBottom;
if (currentPageNextY + lineHeight > pageHeight) {
completePageFn(std::move(currentPage));
currentPage.reset(new Page());
currentPageNextY = marginTop;
}
currentPage->elements.push_back(std::make_shared<PageLine>(line, marginLeft, currentPageNextY));
currentPageNextY += lineHeight;
}
void ChapterHtmlSlimParser::makePages() {
if (!currentTextBlock) {
Serial.printf("[%lu] [EHP] !! No text block to make pages for !!\n", millis());
return;
}
if (!currentPage) {
currentPage = new Page();
currentPage.reset(new Page());
currentPageNextY = marginTop;
}
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
const int pageHeight = GfxRenderer::getScreenHeight() - marginTop - marginBottom;
// Long running task, make sure to let other things happen
vTaskDelay(1);
if (currentTextBlock->getType() == TEXT_BLOCK) {
const auto lines = currentTextBlock->splitIntoLines(renderer, fontId, marginLeft + marginRight);
for (const auto line : lines) {
if (currentPageNextY + lineHeight > pageHeight) {
completePageFn(currentPage);
currentPage = new Page();
currentPageNextY = marginTop;
}
currentPage->elements.push_back(new PageLine(line, marginLeft, currentPageNextY));
currentPageNextY += lineHeight;
}
// add some extra line between blocks
currentTextBlock->layoutAndExtractLines(
renderer, fontId, marginLeft + marginRight,
[this](const std::shared_ptr<TextBlock>& textBlock) { addLineToPage(textBlock); });
// Extra paragraph spacing if enabled
if (extraParagraphSpacing) {
currentPageNextY += lineHeight / 2;
}
// TODO: Image block support
// if (block->getType() == BlockType::IMAGE_BLOCK) {
// ImageBlock *imageBlock = (ImageBlock *)block;
// if (y + imageBlock->height > page_height) {
// pages.push_back(new Page());
// y = 0;
// }
// pages.back()->elements.push_back(new PageImage(imageBlock, y));
// y += imageBlock->height;
// }
}
@@ -4,18 +4,20 @@
#include <climits>
#include <functional>
#include <memory>
#include "blocks/TextBlock.h"
#include "../ParsedText.h"
#include "../blocks/TextBlock.h"
class Page;
class GfxRenderer;
#define MAX_WORD_SIZE 200
class EpubHtmlParserSlim {
class ChapterHtmlSlimParser {
const char* filepath;
GfxRenderer& renderer;
std::function<void(Page*)> completePageFn;
std::function<void(std::unique_ptr<Page>)> completePageFn;
int depth = 0;
int skipUntilDepth = INT_MAX;
int boldUntilDepth = INT_MAX;
@@ -24,17 +26,18 @@ class EpubHtmlParserSlim {
// leave one char at end for null pointer
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
int partWordBufferIndex = 0;
TextBlock* currentTextBlock = nullptr;
Page* currentPage = nullptr;
int currentPageNextY = 0;
std::unique_ptr<ParsedText> currentTextBlock = nullptr;
std::unique_ptr<Page> currentPage = nullptr;
int16_t currentPageNextY = 0;
int fontId;
float lineCompression;
int marginTop;
int marginRight;
int marginBottom;
int marginLeft;
bool extraParagraphSpacing;
void startNewTextBlock(BLOCK_STYLE style);
void startNewTextBlock(TextBlock::BLOCK_STYLE style);
void makePages();
// XML callbacks
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
@@ -42,10 +45,10 @@ class EpubHtmlParserSlim {
static void XMLCALL endElement(void* userData, const XML_Char* name);
public:
explicit EpubHtmlParserSlim(const char* filepath, GfxRenderer& renderer, const int fontId,
const float lineCompression, const int marginTop, const int marginRight,
const int marginBottom, const int marginLeft,
const std::function<void(Page*)>& completePageFn)
explicit ChapterHtmlSlimParser(const char* filepath, GfxRenderer& renderer, const int fontId,
const float lineCompression, const int marginTop, const int marginRight,
const int marginBottom, const int marginLeft, const bool extraParagraphSpacing,
const std::function<void(std::unique_ptr<Page>)>& completePageFn)
: filepath(filepath),
renderer(renderer),
fontId(fontId),
@@ -54,7 +57,9 @@ class EpubHtmlParserSlim {
marginRight(marginRight),
marginBottom(marginBottom),
marginLeft(marginLeft),
extraParagraphSpacing(extraParagraphSpacing),
completePageFn(completePageFn) {}
~EpubHtmlParserSlim() = default;
~ChapterHtmlSlimParser() = default;
bool parseAndBuildPages();
void addLineToPage(std::shared_ptr<TextBlock> line);
};
+96
View File
@@ -0,0 +1,96 @@
#include "ContainerParser.h"
#include <HardwareSerial.h>
bool ContainerParser::setup() {
parser = XML_ParserCreate(nullptr);
if (!parser) {
Serial.printf("[%lu] [CTR] Couldn't allocate memory for parser\n", millis());
return false;
}
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
return true;
}
bool ContainerParser::teardown() {
if (parser) {
XML_ParserFree(parser);
parser = nullptr;
}
return true;
}
size_t ContainerParser::write(const uint8_t data) { return write(&data, 1); }
size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
if (!parser) return 0;
const uint8_t* currentBufferPos = buffer;
auto remainingInBuffer = size;
while (remainingInBuffer > 0) {
void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) {
Serial.printf("[%lu] [CTR] Couldn't allocate buffer\n", millis());
return 0;
}
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
memcpy(buf, currentBufferPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [CTR] Parse error: %s\n", millis(), XML_ErrorString(XML_GetErrorCode(parser)));
return 0;
}
currentBufferPos += toRead;
remainingInBuffer -= toRead;
remainingSize -= toRead;
}
return size;
}
void XMLCALL ContainerParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<ContainerParser*>(userData);
// Simple state tracking to ensure we are looking at the valid schema structure
if (self->state == START && strcmp(name, "container") == 0) {
self->state = IN_CONTAINER;
return;
}
if (self->state == IN_CONTAINER && strcmp(name, "rootfiles") == 0) {
self->state = IN_ROOTFILES;
return;
}
if (self->state == IN_ROOTFILES && strcmp(name, "rootfile") == 0) {
const char* mediaType = nullptr;
const char* path = nullptr;
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "media-type") == 0) {
mediaType = atts[i + 1];
} else if (strcmp(atts[i], "full-path") == 0) {
path = atts[i + 1];
}
}
// Check if this is the standard OEBPS package
if (mediaType && path && strcmp(mediaType, "application/oebps-package+xml") == 0) {
self->fullPath = path;
}
}
}
void XMLCALL ContainerParser::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<ContainerParser*>(userData);
if (self->state == IN_ROOTFILES && strcmp(name, "rootfiles") == 0) {
self->state = IN_CONTAINER;
} else if (self->state == IN_CONTAINER && strcmp(name, "container") == 0) {
self->state = START;
}
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <Print.h>
#include <string>
#include "expat.h"
class ContainerParser final : public Print {
enum ParserState {
START,
IN_CONTAINER,
IN_ROOTFILES,
};
size_t remainingSize;
XML_Parser parser = nullptr;
ParserState state = START;
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void endElement(void* userData, const XML_Char* name);
public:
std::string fullPath;
explicit ContainerParser(const size_t xmlSize) : remainingSize(xmlSize) {}
bool setup();
bool teardown();
size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override;
};
+175
View File
@@ -0,0 +1,175 @@
#include "ContentOpfParser.h"
#include <HardwareSerial.h>
#include <ZipFile.h>
bool ContentOpfParser::setup() {
parser = XML_ParserCreate(nullptr);
if (!parser) {
Serial.printf("[%lu] [COF] Couldn't allocate memory for parser\n", millis());
return false;
}
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
return true;
}
bool ContentOpfParser::teardown() {
if (parser) {
XML_ParserFree(parser);
parser = nullptr;
}
return true;
}
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
if (!parser) return 0;
const uint8_t* currentBufferPos = buffer;
auto remainingInBuffer = size;
while (remainingInBuffer > 0) {
void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) {
Serial.printf("[%lu] [COF] Couldn't allocate memory for buffer\n", millis());
XML_ParserFree(parser);
parser = nullptr;
return 0;
}
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
memcpy(buf, currentBufferPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [COF] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
XML_ParserFree(parser);
parser = nullptr;
return 0;
}
currentBufferPos += toRead;
remainingInBuffer -= toRead;
remainingSize -= toRead;
}
return size;
}
void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<ContentOpfParser*>(userData);
(void)atts;
if (self->state == START && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) {
self->state = IN_PACKAGE;
return;
}
if (self->state == IN_PACKAGE && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
self->state = IN_METADATA;
return;
}
if (self->state == IN_METADATA && strcmp(name, "dc:title") == 0) {
self->state = IN_BOOK_TITLE;
return;
}
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
self->state = IN_MANIFEST;
return;
}
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
self->state = IN_SPINE;
return;
}
if (self->state == IN_METADATA && (strcmp(name, "meta") == 0 || strcmp(name, "opf:meta") == 0)) {
bool isCover = false;
std::string coverItemId;
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "name") == 0 && strcmp(atts[i + 1], "cover") == 0) {
isCover = true;
} else if (strcmp(atts[i], "content") == 0) {
coverItemId = atts[i + 1];
}
}
if (isCover) {
self->coverItemId = coverItemId;
}
return;
}
if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) {
std::string itemId;
std::string href;
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "id") == 0) {
itemId = atts[i + 1];
} else if (strcmp(atts[i], "href") == 0) {
href = self->baseContentPath + atts[i + 1];
}
}
self->items[itemId] = href;
return;
}
if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) {
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "idref") == 0) {
self->spineRefs.emplace_back(atts[i + 1]);
break;
}
}
return;
}
}
void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<ContentOpfParser*>(userData);
if (self->state == IN_BOOK_TITLE) {
self->title.append(s, len);
return;
}
}
void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<ContentOpfParser*>(userData);
(void)name;
if (self->state == IN_SPINE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
self->state = IN_PACKAGE;
return;
}
if (self->state == IN_MANIFEST && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
self->state = IN_PACKAGE;
return;
}
if (self->state == IN_BOOK_TITLE && strcmp(name, "dc:title") == 0) {
self->state = IN_METADATA;
return;
}
if (self->state == IN_METADATA && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
self->state = IN_PACKAGE;
return;
}
if (self->state == IN_PACKAGE && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) {
self->state = START;
return;
}
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <Print.h>
#include <map>
#include "Epub.h"
#include "expat.h"
class ContentOpfParser final : public Print {
enum ParserState {
START,
IN_PACKAGE,
IN_METADATA,
IN_BOOK_TITLE,
IN_MANIFEST,
IN_SPINE,
};
const std::string& baseContentPath;
size_t remainingSize;
XML_Parser parser = nullptr;
ParserState state = START;
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void characterData(void* userData, const XML_Char* s, int len);
static void endElement(void* userData, const XML_Char* name);
public:
std::string title;
std::string tocNcxPath;
std::string coverItemId;
std::map<std::string, std::string> items;
std::vector<std::string> spineRefs;
explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize)
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
bool setup();
bool teardown();
size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override;
};
+165
View File
@@ -0,0 +1,165 @@
#include "TocNcxParser.h"
#include <HardwareSerial.h>
bool TocNcxParser::setup() {
parser = XML_ParserCreate(nullptr);
if (!parser) {
Serial.printf("[%lu] [TOC] Couldn't allocate memory for parser\n", millis());
return false;
}
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
return true;
}
bool TocNcxParser::teardown() {
if (parser) {
XML_ParserFree(parser);
parser = nullptr;
}
return true;
}
size_t TocNcxParser::write(const uint8_t data) { return write(&data, 1); }
size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
if (!parser) return 0;
const uint8_t* currentBufferPos = buffer;
auto remainingInBuffer = size;
while (remainingInBuffer > 0) {
void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) {
Serial.printf("[%lu] [TOC] Couldn't allocate memory for buffer\n", millis());
return 0;
}
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
memcpy(buf, currentBufferPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [TOC] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
return 0;
}
currentBufferPos += toRead;
remainingInBuffer -= toRead;
remainingSize -= toRead;
}
return size;
}
void XMLCALL TocNcxParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
// NOTE: We rely on navPoint label and content coming before any nested navPoints, this will be fine:
// <navPoint>
// <navLabel><text>Chapter 1</text></navLabel>
// <content src="ch1.html"/>
// <navPoint> ...nested... </navPoint>
// </navPoint>
//
// This will NOT:
// <navPoint>
// <navPoint> ...nested... </navPoint>
// <navLabel><text>Chapter 1</text></navLabel>
// <content src="ch1.html"/>
// </navPoint>
auto* self = static_cast<TocNcxParser*>(userData);
if (self->state == START && strcmp(name, "ncx") == 0) {
self->state = IN_NCX;
return;
}
if (self->state == IN_NCX && strcmp(name, "navMap") == 0) {
self->state = IN_NAV_MAP;
return;
}
// Handles both top-level and nested navPoints
if ((self->state == IN_NAV_MAP || self->state == IN_NAV_POINT) && strcmp(name, "navPoint") == 0) {
self->state = IN_NAV_POINT;
self->currentDepth++;
self->currentLabel.clear();
self->currentSrc.clear();
return;
}
if (self->state == IN_NAV_POINT && strcmp(name, "navLabel") == 0) {
self->state = IN_NAV_LABEL;
return;
}
if (self->state == IN_NAV_LABEL && strcmp(name, "text") == 0) {
self->state = IN_NAV_LABEL_TEXT;
return;
}
if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) {
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "src") == 0) {
self->currentSrc = atts[i + 1];
break;
}
}
return;
}
}
void XMLCALL TocNcxParser::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<TocNcxParser*>(userData);
if (self->state == IN_NAV_LABEL_TEXT) {
self->currentLabel.append(s, len);
}
}
void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<TocNcxParser*>(userData);
if (self->state == IN_NAV_LABEL_TEXT && strcmp(name, "text") == 0) {
self->state = IN_NAV_LABEL;
return;
}
if (self->state == IN_NAV_LABEL && strcmp(name, "navLabel") == 0) {
self->state = IN_NAV_POINT;
return;
}
if (self->state == IN_NAV_POINT && strcmp(name, "navPoint") == 0) {
self->currentDepth--;
if (self->currentDepth == 0) {
self->state = IN_NAV_MAP;
}
return;
}
if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) {
// At this point (end of content tag), we likely have both Label (from previous tags) and Src.
// This is the safest place to push the data, assuming <navLabel> always comes before <content>.
// NCX spec says navLabel comes before content.
if (!self->currentLabel.empty() && !self->currentSrc.empty()) {
std::string href = self->baseContentPath + self->currentSrc;
std::string anchor;
const size_t pos = href.find('#');
if (pos != std::string::npos) {
anchor = href.substr(pos + 1);
href = href.substr(0, pos);
}
// Push to vector
self->toc.emplace_back(self->currentLabel, href, anchor, self->currentDepth);
// Clear them so we don't re-add them if there are weird XML structures
self->currentLabel.clear();
self->currentSrc.clear();
}
}
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <Print.h>
#include <string>
#include <vector>
#include "Epub/EpubTocEntry.h"
#include "expat.h"
class TocNcxParser final : public Print {
enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT };
const std::string& baseContentPath;
size_t remainingSize;
XML_Parser parser = nullptr;
ParserState state = START;
std::string currentLabel;
std::string currentSrc;
size_t currentDepth = 0;
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void characterData(void* userData, const XML_Char* s, int len);
static void endElement(void* userData, const XML_Char* name);
public:
std::vector<EpubTocEntry> toc;
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize)
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
bool setup();
bool teardown();
size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override;
};
+15 -5
View File
@@ -162,8 +162,12 @@ int GfxRenderer::getLineHeight(const int fontId) const {
return fontMap.at(fontId).getData(REGULAR)->advanceY;
}
uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
void GfxRenderer::swapBuffers() const { einkDisplay.swapBuffers(); }
void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); }
void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }
void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); }
@@ -203,14 +207,20 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
if (is2Bit) {
const uint8_t byte = bitmap[pixelPosition / 4];
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
// the direct bit from the font is 0 -> white, 1 -> light gray, 2 -> dark gray, 3 -> black
// we swap this to better match the way images and screen think about colors:
// 0 -> black, 1 -> dark grey, 2 -> light grey, 3 -> white
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
const uint8_t val = (byte >> bit_index) & 0x3;
if (fontRenderMode == BW && val > 0) {
if (renderMode == BW && bmpVal < 3) {
// Black (also paints over the grays in BW mode)
drawPixel(screenX, screenY, pixelState);
} else if (fontRenderMode == GRAYSCALE_MSB && val == 1) {
// TODO: Not sure how this anti-aliasing goes on black backgrounds
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
// Light gray (also mark the MSB if it's going to be a dark gray too)
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
drawPixel(screenX, screenY, false);
} else if (fontRenderMode == GRAYSCALE_LSB && val == 2) {
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
// Dark gray
drawPixel(screenX, screenY, false);
}
} else {
+11 -8
View File
@@ -1,24 +1,23 @@
#pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <map>
#include "EpdFontFamily.h"
class GfxRenderer {
public:
enum FontRenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
private:
EInkDisplay& einkDisplay;
FontRenderMode fontRenderMode;
RenderMode renderMode;
std::map<int, EpdFontFamily> fontMap;
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
EpdFontStyle style) const;
public:
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), fontRenderMode(BW) {}
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW) {}
~GfxRenderer() = default;
// Setup
@@ -42,13 +41,17 @@ class GfxRenderer {
int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const;
void drawCenteredText(int fontId, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
void drawText(int fontId, int x, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
void setFontRenderMode(const FontRenderMode mode) { this->fontRenderMode = mode; }
int getSpaceWidth(int fontId) const;
int getLineHeight(int fontId) const;
// Low level functions
void swapBuffers() const;
// Grayscale functions
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const;
// Low level functions
uint8_t* getFrameBuffer() const;
void swapBuffers() const;
void grayscaleRevert() const;
};
+19 -2
View File
@@ -40,7 +40,7 @@ bool ZipFile::loadFileStat(const char* filename, mz_zip_archive_file_stat* fileS
// find the file
mz_uint32 fileIndex = 0;
if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) {
Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis, filename);
Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis(), filename);
mz_zip_reader_end(&zipArchive);
return false;
}
@@ -82,6 +82,16 @@ long ZipFile::getDataOffset(const mz_zip_archive_file_stat& fileStat) const {
return fileOffset + localHeaderSize + filenameLength + extraOffset;
}
bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) const {
mz_zip_archive_file_stat fileStat;
if (!loadFileStat(filename, &fileStat)) {
return false;
}
*size = static_cast<size_t>(fileStat.m_uncomp_size);
return true;
}
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) const {
mz_zip_archive_file_stat fileStat;
if (!loadFileStat(filename, &fileStat)) {
@@ -268,7 +278,14 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
// Write output chunk
if (outBytes > 0) {
processedOutputBytes += outBytes;
out.write(outputBuffer + outputCursor, outBytes);
if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) {
Serial.printf("[%lu] [ZIP] Failed to write all output bytes to stream\n", millis());
fclose(file);
free(outputBuffer);
free(fileReadBuffer);
free(inflator);
return false;
}
// Update output position in buffer (with wraparound)
outputCursor = (outputCursor + outBytes) & (TINFL_LZ_DICT_SIZE - 1);
}
+1
View File
@@ -14,6 +14,7 @@ class ZipFile {
public:
explicit ZipFile(std::string filePath) : filePath(std::move(filePath)) {}
~ZipFile() = default;
bool getInflatedFileSize(const char* filename, size_t* size) const;
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false) const;
bool readFileToStream(const char* filename, Print& out, size_t chunkSize) const;
};
+5 -2
View File
@@ -1,5 +1,5 @@
[platformio]
crosspoint_version = 0.3.0
crosspoint_version = 0.6.0
default_envs = default
[base]
@@ -8,6 +8,9 @@ board = esp32-c3-devkitm-1
framework = arduino
monitor_speed = 115200
upload_speed = 921600
check_tool = cppcheck
check_skip_packages = yes
check_severity = medium, high
board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216
@@ -20,6 +23,7 @@ build_flags =
# https://libexpat.github.io/doc/api/latest/#XML_GE
-DXML_GE=0
-DXML_CONTEXT_BYTES=1024
-std=c++2a
; Board configuration
board_build.flash_mode = dio
@@ -28,7 +32,6 @@ board_build.partitions = partitions.csv
; Libraries
lib_deps =
https://github.com/leethomason/tinyxml2.git#11.0.0
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
+65
View File
@@ -0,0 +1,65 @@
#include "CrossPointSettings.h"
#include <HardwareSerial.h>
#include <SD.h>
#include <Serialization.h>
#include <cstdint>
#include <fstream>
// Initialize the static instance
CrossPointSettings CrossPointSettings::instance;
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
constexpr uint8_t SETTINGS_COUNT = 2;
constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin";
bool CrossPointSettings::saveToFile() const {
// Make sure the directory exists
SD.mkdir("/.crosspoint");
std::ofstream outputFile(SETTINGS_FILE);
serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
serialization::writePod(outputFile, SETTINGS_COUNT);
serialization::writePod(outputFile, whiteSleepScreen);
serialization::writePod(outputFile, extraParagraphSpacing);
outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
return true;
}
bool CrossPointSettings::loadFromFile() {
if (!SD.exists(SETTINGS_FILE + 3)) { // +3 to skip "/sd" prefix
Serial.printf("[%lu] [CPS] Settings file does not exist, using defaults\n", millis());
return false;
}
std::ifstream inputFile(SETTINGS_FILE);
uint8_t version;
serialization::readPod(inputFile, version);
if (version != SETTINGS_FILE_VERSION) {
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
inputFile.close();
return false;
}
uint8_t fileSettingsCount = 0;
serialization::readPod(inputFile, fileSettingsCount);
// load settings that exist
switch (fileSettingsCount) {
case 1:
serialization::readPod(inputFile, whiteSleepScreen);
break;
case 2:
serialization::readPod(inputFile, whiteSleepScreen);
serialization::readPod(inputFile, extraParagraphSpacing);
break;
}
inputFile.close();
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
return true;
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <cstdint>
#include <iosfwd>
class CrossPointSettings {
private:
// Private constructor for singleton
CrossPointSettings() = default;
// Static instance
static CrossPointSettings instance;
public:
// Delete copy constructor and assignment
CrossPointSettings(const CrossPointSettings&) = delete;
CrossPointSettings& operator=(const CrossPointSettings&) = delete;
// Sleep screen settings
uint8_t whiteSleepScreen = 0;
// Text rendering settings
uint8_t extraParagraphSpacing = 1;
~CrossPointSettings() = default;
// Get singleton instance
static CrossPointSettings& getInstance() { return instance; }
bool saveToFile() const;
bool loadFromFile();
};
// Helper macro to access settings
#define SETTINGS CrossPointSettings::getInstance()
+4 -4
View File
@@ -9,7 +9,7 @@
* "./lib/EpdFont/builtinFonts/bookerly_italic_2b.h",
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
*/
#define READER_FONT_ID 1747632454
#define READER_FONT_ID 828106571
/**
* Generated with:
@@ -18,12 +18,12 @@
* "./lib/EpdFont/builtinFonts/ubuntu_bold_10.h",
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
*/
#define UI_FONT_ID 225955604
#define UI_FONT_ID -56235187
/**
* Generated with:
* ruby -rdigest -e 'puts [
* "./lib/EpdFont/builtinFonts/babyblue.h",
* "./lib/EpdFont/builtinFonts/pixelarial14.h",
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
*/
#define SMALL_FONT_ID 141891058
#define SMALL_FONT_ID -1952330053
+43 -24
View File
@@ -5,21 +5,23 @@
#include <InputManager.h>
#include <SD.h>
#include <SPI.h>
#include <builtinFonts/bookerly_2b.h>
#include <builtinFonts/bookerly_bold_2b.h>
#include <builtinFonts/bookerly_bold_italic_2b.h>
#include <builtinFonts/bookerly_italic_2b.h>
#include <builtinFonts/pixelarial14.h>
#include <builtinFonts/ubuntu_10.h>
#include <builtinFonts/ubuntu_bold_10.h>
#include "Battery.h"
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "builtinFonts/babyblue.h"
#include "builtinFonts/bookerly_2b.h"
#include "builtinFonts/bookerly_bold_2b.h"
#include "builtinFonts/bookerly_bold_italic_2b.h"
#include "builtinFonts/bookerly_italic_2b.h"
#include "builtinFonts/ubuntu_10.h"
#include "builtinFonts/ubuntu_bold_10.h"
#include "config.h"
#include "screens/BootLogoScreen.h"
#include "screens/EpubReaderScreen.h"
#include "screens/FileSelectionScreen.h"
#include "screens/FullScreenMessageScreen.h"
#include "screens/SettingsScreen.h"
#include "screens/SleepScreen.h"
#define SPI_FQ 40000000
@@ -49,7 +51,7 @@ EpdFont bookerlyItalicFont(&bookerly_italic_2b);
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b);
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
EpdFont smallFont(&babyblue);
EpdFont smallFont(&pixelarial14);
EpdFontFamily smallFontFamily(&smallFont);
EpdFont ubuntu10Font(&ubuntu_10);
@@ -58,23 +60,24 @@ EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ubuntuBold10Font);
// Power button timing
// Time required to confirm boot from sleep
constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000;
constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 500;
// Time required to enter sleep mode
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000;
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 500;
// Auto-sleep timeout (10 minutes of inactivity)
constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000;
Epub* loadEpub(const std::string& path) {
std::unique_ptr<Epub> loadEpub(const std::string& path) {
if (!SD.exists(path.c_str())) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
return nullptr;
}
const auto epub = new Epub(path, "/.crosspoint");
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
if (epub->load()) {
return epub;
}
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
delete epub;
return nullptr;
}
@@ -151,12 +154,12 @@ void onSelectEpubFile(const std::string& path) {
exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
Epub* epub = loadEpub(path);
auto epub = loadEpub(path);
if (epub) {
appState.openEpubPath = path;
appState.saveToFile();
exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
} else {
exitScreen();
enterNewScreen(
@@ -166,17 +169,18 @@ void onSelectEpubFile(const std::string& path) {
}
}
void onGoToSettings() {
exitScreen();
enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome));
}
void onGoHome() {
exitScreen();
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile));
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings));
}
void setup() {
// Begin serial only if USB connected
pinMode(UART0_RXD, INPUT);
if (digitalRead(UART0_RXD) == HIGH) {
Serial.begin(115200);
}
Serial.begin(115200);
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
@@ -204,12 +208,13 @@ void setup() {
// SD Card Initialization
SD.begin(SD_SPI_CS, SPI, SPI_FQ);
SETTINGS.loadFromFile();
appState.loadFromFile();
if (!appState.openEpubPath.empty()) {
Epub* epub = loadEpub(appState.openEpubPath);
auto epub = loadEpub(appState.openEpubPath);
if (epub) {
exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
// Ensure we're not still holding the power button before leaving setup
waitForPowerRelease();
return;
@@ -217,7 +222,7 @@ void setup() {
}
exitScreen();
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile));
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings));
// Ensure we're not still holding the power button before leaving setup
waitForPowerRelease();
@@ -234,6 +239,20 @@ void loop() {
}
inputManager.update();
// Check for any user activity (button press or release)
static unsigned long lastActivityTime = millis();
if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased()) {
lastActivityTime = millis(); // Reset inactivity timer
}
if (millis() - lastActivityTime >= AUTO_SLEEP_TIMEOUT_MS) {
Serial.printf("[%lu] [SLP] Auto-sleep triggered after %lu ms of inactivity\n", millis(), AUTO_SLEEP_TIMEOUT_MS);
enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
return;
}
if (inputManager.wasReleased(InputManager::BTN_POWER) && inputManager.getHeldTime() > POWER_BUTTON_WAKEUP_MS) {
enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
@@ -0,0 +1,107 @@
#include "EpubReaderChapterSelectionScreen.h"
#include <GfxRenderer.h>
#include <SD.h>
#include "config.h"
constexpr int PAGE_ITEMS = 24;
constexpr int SKIP_PAGE_MS = 700;
void EpubReaderChapterSelectionScreen::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderChapterSelectionScreen*>(param);
self->displayTaskLoop();
}
void EpubReaderChapterSelectionScreen::onEnter() {
if (!epub) {
return;
}
renderingMutex = xSemaphoreCreateMutex();
selectorIndex = currentSpineIndex;
// Trigger first update
updateRequired = true;
xTaskCreate(&EpubReaderChapterSelectionScreen::taskTrampoline, "EpubReaderChapterSelectionScreenTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void EpubReaderChapterSelectionScreen::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void EpubReaderChapterSelectionScreen::handleInput() {
const bool prevReleased =
inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased =
inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS;
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
onSelectSpineIndex(selectorIndex);
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoBack();
} else if (prevReleased) {
if (skipPage) {
selectorIndex =
((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + epub->getSpineItemsCount()) % epub->getSpineItemsCount();
} else {
selectorIndex = (selectorIndex + epub->getSpineItemsCount() - 1) % epub->getSpineItemsCount();
}
updateRequired = true;
} else if (nextReleased) {
if (skipPage) {
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % epub->getSpineItemsCount();
} else {
selectorIndex = (selectorIndex + 1) % epub->getSpineItemsCount();
}
updateRequired = true;
}
}
void EpubReaderChapterSelectionScreen::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void EpubReaderChapterSelectionScreen::renderScreen() {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
renderer.drawCenteredText(READER_FONT_ID, 10, "Select Chapter", true, BOLD);
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 + 2, pageWidth - 1, 30);
for (int i = pageStartIndex; i < epub->getSpineItemsCount() && i < pageStartIndex + PAGE_ITEMS; i++) {
const int tocIndex = epub->getTocIndexForSpineIndex(i);
if (tocIndex == -1) {
renderer.drawText(UI_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, "Unnamed", i != selectorIndex);
} else {
auto item = epub->getTocItem(tocIndex);
renderer.drawText(UI_FONT_ID, 20 + (item.level - 1) * 15, 60 + (i % PAGE_ITEMS) * 30, item.title.c_str(),
i != selectorIndex);
}
}
renderer.displayBuffer();
}
@@ -0,0 +1,38 @@
#pragma once
#include <Epub.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <memory>
#include "Screen.h"
class EpubReaderChapterSelectionScreen final : public Screen {
std::shared_ptr<Epub> epub;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int currentSpineIndex = 0;
int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onGoBack;
const std::function<void(int newSpineIndex)> onSelectSpineIndex;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
public:
explicit EpubReaderChapterSelectionScreen(GfxRenderer& renderer, InputManager& inputManager,
const std::shared_ptr<Epub>& epub, const int currentSpineIndex,
const std::function<void()>& onGoBack,
const std::function<void(int newSpineIndex)>& onSelectSpineIndex)
: Screen(renderer, inputManager),
epub(epub),
currentSpineIndex(currentSpineIndex),
onGoBack(onGoBack),
onSelectSpineIndex(onSelectSpineIndex) {}
void onEnter() override;
void onExit() override;
void handleInput() override;
};
+109 -41
View File
@@ -5,14 +5,16 @@
#include <SD.h>
#include "Battery.h"
#include "CrossPointSettings.h"
#include "EpubReaderChapterSelectionScreen.h"
#include "config.h"
constexpr int PAGES_PER_REFRESH = 15;
constexpr unsigned long SKIP_CHAPTER_MS = 700;
constexpr float lineCompression = 0.95f;
constexpr int marginTop = 11;
constexpr int marginTop = 8;
constexpr int marginRight = 10;
constexpr int marginBottom = 30;
constexpr int marginBottom = 22;
constexpr int marginLeft = 10;
void EpubReaderScreen::taskTrampoline(void* param) {
@@ -60,13 +62,42 @@ void EpubReaderScreen::onExit() {
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
delete section;
section = nullptr;
delete epub;
epub = nullptr;
section.reset();
epub.reset();
}
void EpubReaderScreen::handleInput() {
// Pass input responsibility to sub screen if exists
if (subScreen) {
subScreen->handleInput();
return;
}
// Enter chapter selection screen
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
// Don't start screen transition while rendering
xSemaphoreTake(renderingMutex, portMAX_DELAY);
subScreen.reset(new EpubReaderChapterSelectionScreen(
this->renderer, this->inputManager, epub, currentSpineIndex,
[this] {
subScreen->onExit();
subScreen.reset();
updateRequired = true;
},
[this](const int newSpineIndex) {
if (currentSpineIndex != newSpineIndex) {
currentSpineIndex = newSpineIndex;
nextPageNumber = 0;
section.reset();
}
subScreen->onExit();
subScreen.reset();
updateRequired = true;
}));
subScreen->onEnter();
xSemaphoreGive(renderingMutex);
}
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoHome();
return;
@@ -81,6 +112,14 @@ void EpubReaderScreen::handleInput() {
return;
}
// any botton press when at end of the book goes back to the last page
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
currentSpineIndex = epub->getSpineItemsCount() - 1;
nextPageNumber = UINT16_MAX;
updateRequired = true;
return;
}
const bool skipChapter = inputManager.getHeldTime() > SKIP_CHAPTER_MS;
if (skipChapter) {
@@ -88,8 +127,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0;
currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1;
delete section;
section = nullptr;
section.reset();
xSemaphoreGive(renderingMutex);
updateRequired = true;
return;
@@ -109,8 +147,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = UINT16_MAX;
currentSpineIndex--;
delete section;
section = nullptr;
section.reset();
xSemaphoreGive(renderingMutex);
}
updateRequired = true;
@@ -122,8 +159,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0;
currentSpineIndex++;
delete section;
section = nullptr;
section.reset();
xSemaphoreGive(renderingMutex);
}
updateRequired = true;
@@ -148,16 +184,29 @@ void EpubReaderScreen::renderScreen() {
return;
}
if (currentSpineIndex >= epub->getSpineItemsCount() || currentSpineIndex < 0) {
// edge case handling for sub-zero spine index
if (currentSpineIndex < 0) {
currentSpineIndex = 0;
}
// based bounds of book, show end of book screen
if (currentSpineIndex > epub->getSpineItemsCount()) {
currentSpineIndex = epub->getSpineItemsCount();
}
// Show end of book screen
if (currentSpineIndex == epub->getSpineItemsCount()) {
renderer.clearScreen();
renderer.drawCenteredText(READER_FONT_ID, 300, "End of book", true, BOLD);
renderer.displayBuffer();
return;
}
if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex);
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
section = new Section(epub, currentSpineIndex, renderer);
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
marginLeft)) {
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft,
SETTINGS.extraParagraphSpacing)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
{
@@ -167,7 +216,10 @@ void EpubReaderScreen::renderScreen() {
constexpr int y = 50;
const int w = textWidth + margin * 2;
const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2;
renderer.grayscaleRevert();
uint8_t* fb1 = renderer.getFrameBuffer();
renderer.swapBuffers();
memcpy(fb1, renderer.getFrameBuffer(), EInkDisplay::BUFFER_SIZE);
renderer.fillRect(x, y, w, h, 0);
renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing...");
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
@@ -177,10 +229,9 @@ void EpubReaderScreen::renderScreen() {
section->setupCacheDir();
if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
marginLeft)) {
marginLeft, SETTINGS.extraParagraphSpacing)) {
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
delete section;
section = nullptr;
section.reset();
return;
}
} else {
@@ -212,11 +263,18 @@ void EpubReaderScreen::renderScreen() {
return;
}
const Page* p = section->loadPageFromSD();
const auto start = millis();
renderContents(p);
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
delete p;
{
auto p = section->loadPageFromSD();
if (!p) {
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
section->clearCache();
section.reset();
return renderScreen();
}
const auto start = millis();
renderContents(std::move(p));
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
}
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
uint8_t data[4];
@@ -228,8 +286,8 @@ void EpubReaderScreen::renderScreen() {
f.close();
}
void EpubReaderScreen::renderContents(const Page* p) {
p->render(renderer, READER_FONT_ID);
void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
page->render(renderer, READER_FONT_ID);
renderStatusBar();
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
@@ -243,34 +301,35 @@ void EpubReaderScreen::renderContents(const Page* p) {
// TODO: Only do this if font supports it
{
renderer.clearScreen(0x00);
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_LSB);
p->render(renderer, READER_FONT_ID);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
page->render(renderer, READER_FONT_ID);
renderer.copyGrayscaleLsbBuffers();
// Render and copy to MSB buffer
renderer.clearScreen(0x00);
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_MSB);
p->render(renderer, READER_FONT_ID);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
page->render(renderer, READER_FONT_ID);
renderer.copyGrayscaleMsbBuffers();
// display grayscale part
renderer.displayGrayBuffer();
renderer.setFontRenderMode(GfxRenderer::BW);
renderer.setRenderMode(GfxRenderer::BW);
}
}
void EpubReaderScreen::renderStatusBar() const {
constexpr auto textY = 776;
// Right aligned text for progress counter
const std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount);
const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str());
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, 776,
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY,
progress.c_str());
// Left aligned battery icon and percentage
const uint16_t percentage = battery.readPercentage();
const auto percentageText = std::to_string(percentage) + "%";
const auto percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
renderer.drawText(SMALL_FONT_ID, 20 + marginLeft, 776, percentageText.c_str());
renderer.drawText(SMALL_FONT_ID, 20 + marginLeft, textY, percentageText.c_str());
// 1 column on left, 2 columns on right, 5 columns of battery body
constexpr int batteryWidth = 15;
@@ -286,8 +345,8 @@ void EpubReaderScreen::renderStatusBar() const {
renderer.drawLine(x, y, x, y + batteryHeight - 1);
// Battery end
renderer.drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1);
renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 3, y + batteryHeight - 3);
renderer.drawLine(x + batteryWidth - 2, y + 2, x + batteryWidth - 2, y + batteryHeight - 3);
renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 1, y + 2);
renderer.drawLine(x + batteryWidth - 3, y + batteryHeight - 3, x + batteryWidth - 1, y + batteryHeight - 3);
renderer.drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3);
// The +1 is to round up, so that we always fill at least one pixel
@@ -302,13 +361,22 @@ void EpubReaderScreen::renderStatusBar() const {
const int titleMarginLeft = 20 + percentageTextWidth + 30 + marginLeft;
const int titleMarginRight = progressTextWidth + 30 + marginRight;
const int availableTextWidth = GfxRenderer::getScreenWidth() - titleMarginLeft - titleMarginRight;
const auto tocItem = epub->getTocItem(epub->getTocIndexForSpineIndex(currentSpineIndex));
auto title = tocItem.title;
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
while (titleWidth > availableTextWidth) {
title = title.substr(0, title.length() - 8) + "...";
const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
std::string title;
int titleWidth;
if (tocIndex == -1) {
title = "Unnamed";
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, "Unnamed");
} else {
const auto tocItem = epub->getTocItem(tocIndex);
title = tocItem.title;
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
while (titleWidth > availableTextWidth) {
title = title.substr(0, title.length() - 8) + "...";
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
}
}
renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, 777, title.c_str());
renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, textY, title.c_str());
}
+6 -5
View File
@@ -8,10 +8,11 @@
#include "Screen.h"
class EpubReaderScreen final : public Screen {
Epub* epub;
Section* section = nullptr;
std::shared_ptr<Epub> epub;
std::unique_ptr<Section> section = nullptr;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
std::unique_ptr<Screen> subScreen = nullptr;
int currentSpineIndex = 0;
int nextPageNumber = 0;
int pagesUntilFullRefresh = 0;
@@ -21,13 +22,13 @@ class EpubReaderScreen final : public Screen {
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
void renderContents(const Page* p);
void renderContents(std::unique_ptr<Page> p);
void renderStatusBar() const;
public:
explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, Epub* epub,
explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
const std::function<void()>& onGoHome)
: Screen(renderer, inputManager), epub(epub), onGoHome(onGoHome) {}
: Screen(renderer, inputManager), epub(std::move(epub)), onGoHome(onGoHome) {}
void onEnter() override;
void onExit() override;
void handleInput() override;
+13 -5
View File
@@ -91,11 +91,16 @@ void FileSelectionScreen::handleInput() {
} else {
onSelect(basepath + files[selectorIndex]);
}
} else if (inputManager.wasPressed(InputManager::BTN_BACK) && basepath != "/") {
basepath = basepath.substr(0, basepath.rfind('/'));
if (basepath.empty()) basepath = "/";
loadFiles();
updateRequired = true;
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (basepath != "/") {
basepath = basepath.substr(0, basepath.rfind('/'));
if (basepath.empty()) basepath = "/";
loadFiles();
updateRequired = true;
} else {
// At root level, go to settings
onSettingsOpen();
}
} else if (prevPressed) {
selectorIndex = (selectorIndex + files.size() - 1) % files.size();
updateRequired = true;
@@ -123,6 +128,9 @@ void FileSelectionScreen::render() const {
const auto pageWidth = GfxRenderer::getScreenWidth();
renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD);
// Help text
renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Settings");
if (files.empty()) {
renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found");
} else {
+4 -2
View File
@@ -17,6 +17,7 @@ class FileSelectionScreen final : public Screen {
int selectorIndex = 0;
bool updateRequired = false;
const std::function<void(const std::string&)> onSelect;
const std::function<void()> onSettingsOpen;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
@@ -25,8 +26,9 @@ class FileSelectionScreen final : public Screen {
public:
explicit FileSelectionScreen(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(const std::string&)>& onSelect)
: Screen(renderer, inputManager), onSelect(onSelect) {}
const std::function<void(const std::string&)>& onSelect,
const std::function<void()>& onSettingsOpen)
: Screen(renderer, inputManager), onSelect(onSelect), onSettingsOpen(onSettingsOpen) {}
void onEnter() override;
void onExit() override;
void handleInput() override;
+130
View File
@@ -0,0 +1,130 @@
#include "SettingsScreen.h"
#include <GfxRenderer.h>
#include "CrossPointSettings.h"
#include "config.h"
// Define the static settings list
const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = {
{"White Sleep Screen", &CrossPointSettings::whiteSleepScreen},
{"Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing}};
void SettingsScreen::taskTrampoline(void* param) {
auto* self = static_cast<SettingsScreen*>(param);
self->displayTaskLoop();
}
void SettingsScreen::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
// Reset selection to first item
selectedSettingIndex = 0;
// Trigger first update
updateRequired = true;
xTaskCreate(&SettingsScreen::taskTrampoline, "SettingsScreenTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void SettingsScreen::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void SettingsScreen::handleInput() {
// Handle actions with early return
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
toggleCurrentSetting();
updateRequired = true;
return;
}
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
SETTINGS.saveToFile();
onGoHome();
return;
}
// Handle navigation
if (inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT)) {
// Move selection up (with wrap-around)
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
updateRequired = true;
} else if (inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT)) {
// Move selection down (with wrap-around)
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
updateRequired = true;
}
}
void SettingsScreen::toggleCurrentSetting() {
// Validate index
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
return;
}
// Toggle the boolean value using the member pointer
bool currentValue = SETTINGS.*(settingsList[selectedSettingIndex].valuePtr);
SETTINGS.*(settingsList[selectedSettingIndex].valuePtr) = !currentValue;
// Save settings when they change
SETTINGS.saveToFile();
}
void SettingsScreen::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void SettingsScreen::render() const {
renderer.clearScreen();
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
// Draw header
renderer.drawCenteredText(READER_FONT_ID, 10, "Settings", true, BOLD);
// We always have at least one setting
// Draw all settings
for (int i = 0; i < settingsCount; i++) {
const int settingY = 60 + i * 30; // 30 pixels between settings
// Draw selection indicator for the selected setting
if (i == selectedSettingIndex) {
renderer.drawText(UI_FONT_ID, 5, settingY, ">");
}
// Draw setting name and value
renderer.drawText(UI_FONT_ID, 20, settingY, settingsList[i].name);
bool value = SETTINGS.*(settingsList[i].valuePtr);
renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF");
}
// Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "Press OK to toggle, BACK to save & exit");
// Always use standard refresh for settings screen
renderer.displayBuffer();
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <cstdint>
#include <string>
#include <vector>
#include "Screen.h"
class CrossPointSettings;
// Structure to hold setting information
struct SettingInfo {
const char* name; // Display name of the setting
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings
};
class SettingsScreen final : public Screen {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
int selectedSettingIndex = 0; // Currently selected setting
const std::function<void()> onGoHome;
// Static settings list
static constexpr int settingsCount = 2; // Number of settings
static const SettingInfo settingsList[settingsCount];
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void toggleCurrentSetting();
public:
explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
: Screen(renderer, inputManager), onGoHome(onGoHome) {}
void onEnter() override;
void onExit() override;
void handleInput() override;
};
+7 -1
View File
@@ -2,6 +2,7 @@
#include <GfxRenderer.h>
#include "CrossPointSettings.h"
#include "config.h"
#include "images/CrossLarge.h"
@@ -13,6 +14,11 @@ void SleepScreen::onEnter() {
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
renderer.invertScreen();
// Apply white screen if enabled in settings
if (!SETTINGS.whiteSleepScreen) {
renderer.invertScreen();
}
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
}