Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
567fa6e1e2 | ||
|
|
da4d3b5ea5 | ||
|
|
172916afd4 | ||
|
|
ebcd813ff6 | ||
|
|
712c566664 | ||
|
|
5894ae5afe | ||
|
|
8c1c80787a | ||
|
|
140fcb9db5 | ||
|
|
e0b6b9b28a | ||
|
|
83315b6179 | ||
|
|
8e0d2bece2 | ||
|
|
4848a77e1b | ||
|
|
49190cca6d | ||
|
|
e9c2fe1c87 | ||
|
|
dd1741bf0b | ||
|
|
51c5c3c0aa | ||
|
|
5e24895f6d | ||
|
|
e2ca0e94ca | ||
|
|
a4b9a43ca1 | ||
|
|
c73fca26f5 | ||
|
|
dfd7b615dc | ||
|
|
aca6dceaa8 | ||
|
|
6ca75c4653 | ||
|
|
1b9c8ab545 | ||
|
|
bf6cf83577 | ||
|
|
3a761b18af | ||
|
|
13f0ebed96 | ||
|
|
0bc0baa966 | ||
|
|
5d369df6be | ||
|
|
b8ebcf5867 | ||
|
|
e858ebbe88 | ||
|
|
9224bc3f8c | ||
|
|
67a679ab41 | ||
|
|
7a53342f9d | ||
|
|
3ce11f14ce | ||
|
|
47ef92e8fd | ||
|
|
e3d6e32609 | ||
|
|
d399afb53d | ||
|
|
838993259d | ||
|
|
cc74039cab | ||
|
|
87d6c032a5 | ||
|
|
c9b5462370 | ||
|
|
e548bfc0e1 | ||
|
|
73c30748d8 | ||
|
|
6d68466891 | ||
|
|
8824c87490 | ||
|
|
5fef99c641 | ||
|
|
7a792a5384 | ||
|
|
f69cddf2cc | ||
|
|
7185e5d287 | ||
|
|
12940cc546 | ||
|
|
21277e03eb | ||
|
|
4eef2b5793 | ||
|
|
5a55fa1c6e | ||
|
|
c98ba142e8 | ||
|
|
c1c94c0112 | ||
|
|
eb84bcee7c | ||
|
|
d45f355e87 | ||
|
|
56ec3dfb6d | ||
|
|
e517945aaa | ||
|
|
489220832f | ||
|
|
3ee10b31ab | ||
|
|
a946c83a07 | ||
|
|
847786e342 | ||
|
|
c2fb8ce55d | ||
|
|
ed05554d74 | ||
|
|
9a9dc044ce | ||
|
|
1c027ce2cd | ||
|
|
49f97b69ca | ||
|
|
14643d0225 | ||
|
|
fecd1849b9 | ||
|
|
2040e088e7 | ||
|
|
65d23910a3 | ||
|
|
52995fa722 | ||
|
|
d4f8eda154 | ||
|
|
33b8fa0e19 | ||
|
|
16c760b2d2 | ||
|
|
8f3df7e10e | ||
|
|
0165fab581 | ||
|
|
66b100c6ca | ||
|
|
41bda43899 | ||
|
|
82f21f3c1d | ||
|
|
a9242fe61f | ||
|
|
88d0d90471 | ||
|
|
97c4871316 | ||
|
|
66811bf50b | ||
|
|
87287012ba | ||
|
|
d4ae108d9b | ||
|
|
7240cd52a9 | ||
|
|
0bae3bbf64 | ||
|
|
2b12a65011 | ||
|
|
46fa186b82 | ||
|
|
0cc2c64df2 | ||
|
|
1f956e972b | ||
|
|
9c573e6f7f | ||
|
|
0edb2baced | ||
|
|
b792b792bf | ||
|
|
afe9672156 | ||
|
|
9f95b31de5 | ||
|
|
c76507c937 | ||
|
|
881aa2e005 | ||
|
|
14972b34cb | ||
|
|
c8f4870d7c | ||
|
|
5fdf23f1d2 | ||
|
|
2fb417ee90 | ||
|
|
c8f6160fbc | ||
|
|
8e4484cd22 | ||
|
|
0332e1103a | ||
|
|
5790d6f5dc | ||
|
|
062d69dc2a | ||
|
|
5e9626eb2a | ||
|
|
00e83af4e8 | ||
|
|
39080c0e51 | ||
|
|
9e59a5106b | ||
|
|
a922e553ed | ||
|
|
04ad4e5aa4 | ||
|
|
6e9ba1006a | ||
|
|
40f9ed485c | ||
|
|
b82e044ac3 | ||
|
|
026733a4fe | ||
|
|
57b075ec97 | ||
|
|
648c688642 | ||
|
|
06065dfd8b | ||
|
|
93226c9fbb | ||
|
|
941643cf97 | ||
|
|
9bba41ed96 | ||
|
|
34cf5f0636 | ||
|
|
f2ca65d752 | ||
|
|
6a8971fc20 | ||
|
|
e2cba5be83 | ||
|
|
52a0b5bbe9 | ||
|
|
3abcd0d05d | ||
|
|
03f0ce04cc | ||
|
|
be1b5bad21 | ||
|
|
3dd52f30fa | ||
|
|
e43fec79be | ||
|
|
bf7bffd506 | ||
|
|
9f31f80c80 | ||
|
|
fb5fc32c5d | ||
|
|
d4bd119950 | ||
|
|
85d76da967 | ||
|
|
e4ac90f5c1 | ||
|
|
278b056bd0 | ||
|
|
b01eb50325 | ||
|
|
1bfe694807 | ||
|
|
7b32a87596 | ||
|
|
071ccb9d1b | ||
|
|
d7f4bd54f5 | ||
|
|
2437943c94 | ||
|
|
140d8749a6 | ||
|
|
534504cf7a | ||
|
|
b1763821b5 | ||
|
|
c0b83b626e | ||
|
|
f8c0b1acea | ||
|
|
f9b604f04e | ||
|
|
3dc5f6fec4 | ||
|
|
41c93e4eba | ||
|
|
1c33162368 | ||
|
|
27d42fbef3 | ||
|
|
dd280bdc97 | ||
|
|
bf031fd999 | ||
|
|
02350c6a9f | ||
|
|
9023b262a1 | ||
|
|
eabd149371 | ||
|
|
838246d147 | ||
|
|
f96b6ab29c | ||
|
|
e3d0201365 | ||
|
|
286b47f489 | ||
|
|
aff4dc6628 | ||
|
|
98a39374e8 | ||
|
|
e8c0fb42d4 | ||
|
|
b77af16caa | ||
|
|
e3c1e28b8f | ||
|
|
dc7544d944 | ||
|
|
504c7b307d | ||
|
|
b6bc1f7ed3 | ||
|
|
ea0abaf351 | ||
|
|
2771579007 | ||
|
|
27035b2b91 | ||
|
|
1107590b56 | ||
|
|
66ddb52103 | ||
|
|
9f4f71fabe | ||
|
|
d23020e268 | ||
|
|
f4491875ab | ||
|
|
6fe28da41b | ||
|
|
689b539c6b | ||
|
|
ce37c80c2d | ||
|
|
b39ce22e54 | ||
|
|
77c655fcf5 | ||
|
|
246afae6ef | ||
|
|
fcfa10bb1f | ||
|
|
febf79a98a | ||
|
|
424104f8ff | ||
|
|
955c78de64 | ||
|
|
958508eb6b | ||
|
|
6aa5d41a42 | ||
|
|
2a27c6d068 | ||
|
|
b73ae7fe74 | ||
|
|
f264efdb12 | ||
|
|
0d32d21d75 | ||
|
|
9b4dfbd180 | ||
|
|
926c786705 | ||
|
|
299623927e | ||
|
|
9a3bb81337 | ||
|
|
73d1839ddd | ||
|
|
cc86533e86 | ||
|
|
bf3f270067 | ||
|
|
cfe838e03b | ||
|
|
7484fe478c | ||
|
|
d41d539435 | ||
|
|
cf6fec78dc | ||
|
|
10d76dde12 | ||
|
|
7b5a63d220 | ||
|
|
c1d5f5d562 | ||
|
|
adfeee063f | ||
|
|
2d3928ed81 | ||
|
|
48249fbd1e | ||
|
|
1a53dccebd | ||
|
|
3e28724b62 | ||
|
|
d86b3fe134 | ||
|
|
1a3d6b125d | ||
|
|
b2020f5512 | ||
|
|
70dc0f018e | ||
|
|
424594488f | ||
|
|
57fdb1c0fb | ||
|
|
5e1694748c | ||
|
|
063a1df851 | ||
|
|
d429966dd4 | ||
|
|
c78f2a9840 | ||
|
|
11f01d3a41 | ||
|
|
973d372521 | ||
|
|
67da8139b3 | ||
|
|
c287aa03a4 |
@@ -1,9 +1,18 @@
|
|||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
* **What is the goal of this PR?** (e.g., Fixes a bug in the user authentication module, Implements the new feature for
|
* **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.)
|
||||||
file uploading.)
|
|
||||||
* **What changes are included?**
|
* **What changes are included?**
|
||||||
|
|
||||||
## Additional Context
|
## Additional Context
|
||||||
|
|
||||||
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on).
|
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks,
|
||||||
|
specific areas to focus on).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### AI Usage
|
||||||
|
|
||||||
|
While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
|
||||||
|
helps set the right context for reviewers.
|
||||||
|
|
||||||
|
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO >**_
|
||||||
|
|||||||
@@ -7,17 +7,11 @@ name: CI
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- uses: actions/cache@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/pip
|
|
||||||
~/.platformio/.cache
|
|
||||||
key: ${{ runner.os }}-pio
|
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
@@ -34,7 +28,7 @@ jobs:
|
|||||||
sudo apt-get install -y clang-format-21
|
sudo apt-get install -y clang-format-21
|
||||||
|
|
||||||
- name: Run cppcheck
|
- name: Run cppcheck
|
||||||
run: pio check --fail-on-defect medium --fail-on-defect high
|
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||||
|
|
||||||
- name: Run clang-format
|
- 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)
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: "PR Formatting"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- edited
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
statuses: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
title-check:
|
||||||
|
name: Title Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Check PR Title
|
||||||
|
uses: amannn/action-semantic-pull-request@v6
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -7,17 +7,18 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-release:
|
build-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- uses: actions/cache@v5
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/pip
|
~/.cache/pip
|
||||||
~/.platformio/.cache
|
~/.platformio/.cache
|
||||||
key: ${{ runner.os }}-pio
|
key: ${{ runner.os }}-pio
|
||||||
|
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
|
|||||||
@@ -3,3 +3,9 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
lib/EpdFont/fontsrc
|
lib/EpdFont/fontsrc
|
||||||
|
*.generated.h
|
||||||
|
.vs
|
||||||
|
build
|
||||||
|
**/__pycache__/
|
||||||
|
/compile_commands.json
|
||||||
|
/.cache
|
||||||
|
|||||||
@@ -23,18 +23,27 @@ CrossPoint Reader aims to:
|
|||||||
|
|
||||||
This project is **not affiliated with Xteink**; it's built as a community project.
|
This project is **not affiliated with Xteink**; it's built as a community project.
|
||||||
|
|
||||||
## Features
|
## Features & Usage
|
||||||
|
|
||||||
- [x] EPUB parsing and rendering
|
- [x] EPUB parsing and rendering (EPUB 2 and EPUB 3)
|
||||||
|
- [ ] Image support within EPUB
|
||||||
- [x] Saved reading position
|
- [x] Saved reading position
|
||||||
- [ ] File explorer with file picker
|
- [x] File explorer with file picker
|
||||||
- [x] Basic EPUB picker from root directory
|
- [x] Basic EPUB picker from root directory
|
||||||
- [x] Support nested folders
|
- [x] Support nested folders
|
||||||
- [ ] EPUB picker with cover art
|
- [ ] EPUB picker with cover art
|
||||||
- [ ] Image support within EPUB
|
- [x] Custom sleep screen
|
||||||
- [ ] Configurable font, layout, and display options
|
- [x] Cover sleep screen
|
||||||
- [ ] WiFi connectivity
|
- [x] Wifi book upload
|
||||||
- [ ] BLE connectivity
|
- [x] Wifi OTA updates
|
||||||
|
- [x] Configurable font, layout, and display options
|
||||||
|
- [ ] User provided fonts
|
||||||
|
- [ ] Full UTF support
|
||||||
|
- [x] Screen rotation
|
||||||
|
|
||||||
|
Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages).
|
||||||
|
|
||||||
|
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
@@ -59,10 +68,6 @@ back to the other partition using the "Swap boot partition" button here https://
|
|||||||
|
|
||||||
See [Development](#development) below.
|
See [Development](#development) below.
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
@@ -97,9 +102,9 @@ CrossPoint Reader is pretty aggressive about caching data down to the SD card to
|
|||||||
has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based
|
has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based
|
||||||
on this constraint.
|
on this constraint.
|
||||||
|
|
||||||
### EPUB caching
|
### Data caching
|
||||||
|
|
||||||
The first time chapters of an EPUB are loaded, they are cached to the SD card. Subsequent loads are served from the
|
The first time chapters of a book are loaded, they are cached to the SD card. Subsequent loads are served from the
|
||||||
cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows:
|
cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows:
|
||||||
|
|
||||||
|
|
||||||
@@ -107,25 +112,22 @@ cache. This cache directory exists at `.crosspoint` on the SD card. The structur
|
|||||||
.crosspoint/
|
.crosspoint/
|
||||||
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
|
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
|
||||||
│ ├── progress.bin # Stores reading progress (chapter, page, etc.)
|
│ ├── progress.bin # Stores reading progress (chapter, page, etc.)
|
||||||
│ ├── 0/ # Each chapter is stored in a subdirectory named by its index (based on the spine order)
|
│ ├── cover.bmp # Book cover image (once generated)
|
||||||
│ │ ├── section.bin # Section metadata (page count)
|
│ ├── book.bin # Book metadata (title, author, spine, table of contents, etc.)
|
||||||
│ │ ├── page_0.bin # Each page is stored in a separate file, it
|
│ └── sections/ # All chapter data is stored in the sections subdirectory
|
||||||
│ │ ├── page_1.bin # contains the position (x, y) and text for each word
|
│ ├── 0.bin # Chapter data (screen count, all text layout info, etc.)
|
||||||
│ │ └── ...
|
│ ├── 1.bin # files are named by their index in the spine
|
||||||
│ ├── 1/
|
│ └── ...
|
||||||
│ │ ├── section.bin
|
|
||||||
│ │ ├── page_0.bin
|
|
||||||
│ │ ├── page_1.bin
|
|
||||||
│ │ └── ...
|
|
||||||
│ └── ...
|
|
||||||
│
|
│
|
||||||
└── epub_189013891/
|
└── epub_189013891/
|
||||||
```
|
```
|
||||||
|
|
||||||
Deleting the `.crosspoint` directory will clear the cache.
|
Deleting the `.crosspoint` directory will clear the entire cache.
|
||||||
|
|
||||||
Due the way it's currently implemented, the cache is not automatically cleared when the EPUB is deleted and moving an
|
Due the way it's currently implemented, the cache is not automatically cleared when a book is deleted and moving a book
|
||||||
EPUB file will reset the reading progress.
|
file will use a new cache directory, resetting the reading progress.
|
||||||
|
|
||||||
|
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,39 @@
|
|||||||
# CrossPoint User Guide
|
# CrossPoint User Guide
|
||||||
|
|
||||||
Welcome to the **CrossPoint** firmware. This guide outlines the hardware controls, navigation, and reading features of
|
Welcome to the **CrossPoint** firmware. This guide outlines the hardware controls, navigation, and reading features of the device.
|
||||||
the device.
|
|
||||||
|
- [CrossPoint User Guide](#crosspoint-user-guide)
|
||||||
|
- [1. Hardware Overview](#1-hardware-overview)
|
||||||
|
- [Button Layout](#button-layout)
|
||||||
|
- [2. Power \& Startup](#2-power--startup)
|
||||||
|
- [Power On / Off](#power-on--off)
|
||||||
|
- [First Launch](#first-launch)
|
||||||
|
- [3. Screens](#3-screens)
|
||||||
|
- [3.1 Home Screen](#31-home-screen)
|
||||||
|
- [3.2 Book Selection](#32-book-selection)
|
||||||
|
- [3.3 Reading Mode](#33-reading-mode)
|
||||||
|
- [3.4 File Upload Screen](#34-file-upload-screen)
|
||||||
|
- [3.5 Settings](#35-settings)
|
||||||
|
- [3.6 Sleep Screen](#36-sleep-screen)
|
||||||
|
- [4. Reading Mode](#4-reading-mode)
|
||||||
|
- [Page Turning](#page-turning)
|
||||||
|
- [Chapter Navigation](#chapter-navigation)
|
||||||
|
- [System Navigation](#system-navigation)
|
||||||
|
- [5. Chapter Selection Screen](#5-chapter-selection-screen)
|
||||||
|
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
|
||||||
|
|
||||||
|
|
||||||
## 1. Hardware Overview
|
## 1. Hardware Overview
|
||||||
|
|
||||||
The device utilises the standard buttons on the Xtink X4 in the same layout:
|
The device utilises the standard buttons on the Xtink X4 (in the same layout as the manufacturer firmware, by default):
|
||||||
|
|
||||||
### Button Layout
|
### Button Layout
|
||||||
| Location | Buttons |
|
| Location | Buttons |
|
||||||
|-----------------|--------------------------------------------|
|
| --------------- | ---------------------------------------------------- |
|
||||||
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
|
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
|
||||||
| **Right Side** | **Power**, **Volume Up**, **Volume Down** |
|
| **Right Side** | **Power**, **Volume Up**, **Volume Down**, **Reset** |
|
||||||
|
|
||||||
|
Button layout can be customized in **[Settings](#35-settings)**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -19,24 +41,131 @@ The device utilises the standard buttons on the Xtink X4 in the same layout:
|
|||||||
|
|
||||||
### Power On / Off
|
### Power On / Off
|
||||||
|
|
||||||
To turn the device on or off, **press and hold the Power button for 1 full second**.
|
To turn the device on or off, **press and hold the Power button for approximately half a second**.
|
||||||
|
In **[Settings](#35-settings)** you can configure the power button to turn the device off with a short press instead of a long one.
|
||||||
|
|
||||||
|
To reboot the device (for example if it's frozen, or after a firmware update), press and release the Reset button, and then quickly press and hold the Power button for a few seconds.
|
||||||
|
|
||||||
### First Launch
|
### First Launch
|
||||||
|
|
||||||
Upon turning the device on for the first time, you will be placed on the **Book Selection Screen** (File Browser).
|
Upon turning the device on for the first time, you will be placed on the **[Home](#31-home-screen)** screen.
|
||||||
|
|
||||||
> **Note:** On subsequent restarts, the firmware will automatically reopen the last book you were reading.
|
> [!NOTE]
|
||||||
|
> On subsequent restarts, the firmware will automatically reopen the last book you were reading.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Book Selection
|
## 3. Screens
|
||||||
|
|
||||||
The Home Screen acts as a folder and file browser.
|
### 3.1 Home Screen
|
||||||
|
|
||||||
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up
|
The Home Screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, **[Book Selection](#32-book-selection)**, **[Settings](#35-settings)**, or the **[File Upload](#34-file-upload-screen)** screen.
|
||||||
and down through folders and books.
|
|
||||||
|
### 3.2 Book Selection
|
||||||
|
|
||||||
|
The Book Selection 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. You can also long-press these buttons to scroll a full page up or down.
|
||||||
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
|
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
|
||||||
|
|
||||||
|
### 3.3 Reading Mode
|
||||||
|
|
||||||
|
See [Reading Mode](#4-reading-mode) below for more information.
|
||||||
|
|
||||||
|
### 3.4 File Upload Screen
|
||||||
|
|
||||||
|
The File Upload screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with a WiFi selection dialog and then your X4 will start hosting a web server.
|
||||||
|
|
||||||
|
See the [webserver docs](./docs/webserver.md) for more information on how to connect to the web server and upload files.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details.
|
||||||
|
|
||||||
|
### 3.4.1 Calibre Wireless Transfers
|
||||||
|
|
||||||
|
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
|
||||||
|
|
||||||
|
1. Install the plugin in Calibre:
|
||||||
|
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
|
||||||
|
- Download the zip file.
|
||||||
|
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
|
||||||
|
2. On the device: File Transfer → Connect to Calibre → Join a network.
|
||||||
|
3. Make sure your computer is on the same WiFi network.
|
||||||
|
4. In Calibre, click "Send to device" to transfer books.
|
||||||
|
|
||||||
|
### 3.5 Settings
|
||||||
|
|
||||||
|
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
|
||||||
|
- **Sleep Screen**: Which sleep screen to display when the device sleeps:
|
||||||
|
- "Dark" (default) - The default dark Crosspoint logo sleep screen
|
||||||
|
- "Light" - The same default sleep screen, on a white background
|
||||||
|
- "Custom" - Custom images from the SD card; see [Sleep Screen](#36-sleep-screen) below for more information
|
||||||
|
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
||||||
|
- "None" - A blank screen
|
||||||
|
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
|
||||||
|
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
|
||||||
|
- "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected)
|
||||||
|
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected
|
||||||
|
- "None" (default) - The cover image will be converted to a grayscale image and displayed as it is
|
||||||
|
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
|
||||||
|
- "Inverted" - The image will be inverted as in white&black and will be displayed without grayscale conversion
|
||||||
|
- **Status Bar**: Configure the status bar displayed while reading:
|
||||||
|
- "None" - No status bar
|
||||||
|
- "No Progress" - Show status bar without reading progress
|
||||||
|
- "Full" - Show status bar with reading progress
|
||||||
|
- **Hide Battery %**: Configure where to suppress the battery pecentage display in the status bar; the battery icon will still be shown:
|
||||||
|
- "Never" - Always show battery percentage (default)
|
||||||
|
- "In Reader" - Show battery percentage everywhere except in reading mode
|
||||||
|
- "Always" - Always hide battery percentage
|
||||||
|
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book. If disabled, paragraphs will not have vertical space between them, but will have first-line indentation.
|
||||||
|
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
|
||||||
|
- **Short Power Button Click**: Controls the effect of a short click of the power button:
|
||||||
|
- "Ignore" - Require a long press to turn off the device
|
||||||
|
- "Sleep" - A short press powers the device off
|
||||||
|
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
|
||||||
|
- **Reading Orientation**: Set the screen orientation for reading EPUB files:
|
||||||
|
- "Portrait" (default) - Standard portrait orientation
|
||||||
|
- "Landscape CW" - Landscape, rotated clockwise
|
||||||
|
- "Inverted" - Portrait, upside down
|
||||||
|
- "Landscape CCW" - Landscape, rotated counter-clockwise
|
||||||
|
- **Front Button Layout**: Configure the order of the bottom edge buttons:
|
||||||
|
- Back, Confirm, Left, Right (default)
|
||||||
|
- Left, Right, Back, Confirm
|
||||||
|
- Left, Back, Confirm, Right
|
||||||
|
- Back, Confirm, Right, Left
|
||||||
|
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
|
||||||
|
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
|
||||||
|
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
|
||||||
|
- "Page Scroll" - Long-pressing scrolls a page up/down
|
||||||
|
- Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
|
||||||
|
- **Reader Font Family**: Choose the font used for reading:
|
||||||
|
- "Bookerly" (default) - Amazon's reading font
|
||||||
|
- "Noto Sans" - Google's sans-serif font
|
||||||
|
- "Open Dyslexic" - Font designed for readers with dyslexia
|
||||||
|
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium", "Large", or "X Large".
|
||||||
|
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal", or "Wide".
|
||||||
|
- **Reader Screen Margin**: Controls the screen margins in reader mode between 5 and 40 pixels in 5 pixel increments.
|
||||||
|
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
|
||||||
|
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
|
||||||
|
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
|
||||||
|
- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication.
|
||||||
|
- **Check for updates**: Check for firmware updates over WiFi.
|
||||||
|
|
||||||
|
### 3.6 Sleep Screen
|
||||||
|
|
||||||
|
You can customize the sleep screen by placing custom images in specific locations on the SD card:
|
||||||
|
|
||||||
|
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
|
||||||
|
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images inside. If images are found in this directory, they will take priority over the `sleep.bmp` file, and one will be randomly selected each time the device sleeps.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> For best results:
|
||||||
|
> - Use uncompressed BMP files with 24-bit color depth
|
||||||
|
> - Use a resolution of 480x800 pixels to match the device's screen resolution.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Reading Mode
|
## 4. Reading Mode
|
||||||
@@ -45,17 +174,34 @@ Once you have opened a book, the button layout changes to facilitate reading.
|
|||||||
|
|
||||||
### Page Turning
|
### Page Turning
|
||||||
| Action | Buttons |
|
| Action | Buttons |
|
||||||
|-------------------|--------------------------------------|
|
| ----------------- | ------------------------------------ |
|
||||||
| **Previous Page** | Press **Left** _or_ **Volume Up** |
|
| **Previous Page** | Press **Left** _or_ **Volume Up** |
|
||||||
| **Next Page** | Press **Right** _or_ **Volume Down** |
|
| **Next Page** | Press **Right** _or_ **Volume Down** |
|
||||||
|
|
||||||
|
The role of the volume (side) buttons can be swapped in **[Settings](#35-settings)**.
|
||||||
|
|
||||||
|
If the **Short Power Button Click** setting is set to "Page Turn", you can also turn to the next page by briefly pressing the Power button.
|
||||||
|
|
||||||
### Chapter Navigation
|
### Chapter Navigation
|
||||||
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
|
* **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.
|
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
|
||||||
|
|
||||||
|
This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake.
|
||||||
|
|
||||||
|
|
||||||
### System Navigation
|
### System Navigation
|
||||||
* **Return to Home:** Press **Back** to close the book and return to the Book Selection screen.
|
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
|
||||||
* **Chapter Menu:** Press **Confirm** to open the Table of Contents/Chapter Selection screen.
|
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
||||||
|
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)**.
|
||||||
|
|
||||||
|
### Supported Languages
|
||||||
|
|
||||||
|
CrossPoint renders text using the following Unicode character blocks, enabling support for a wide range of languages:
|
||||||
|
|
||||||
|
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
|
||||||
|
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
|
||||||
|
|
||||||
|
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -71,8 +217,6 @@ Accessible by pressing **Confirm** while inside a book.
|
|||||||
|
|
||||||
## 6. Current Limitations & Roadmap
|
## 6. Current Limitations & Roadmap
|
||||||
|
|
||||||
Please note that this firmware is currently in active development. The following features are **not yet supported** but
|
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
|
||||||
are planned for future updates:
|
|
||||||
|
|
||||||
* **Images:** Embedded images in e-books will not render.
|
* **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.
|
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
find src lib \( -name "*.c" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) -exec clang-format -style=file -i {} +
|
GIT_LS_FILES_FLAGS=""
|
||||||
|
if [[ "$1" == "-g" ]]; then
|
||||||
|
GIT_LS_FILES_FLAGS="--modified"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Main Logic ---
|
||||||
|
|
||||||
|
# Format all files (or only modified files if -g is passed)
|
||||||
|
|
||||||
|
# Use 'git ls-files' to get a list of all files tracked by git:
|
||||||
|
# --modified: files tracked by git that have been modified (staged or unstaged)
|
||||||
|
# --exclude-standard: ignores files in .gitignore
|
||||||
|
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
|
||||||
|
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
||||||
|
| grep -E '\.(c|cpp|h|hpp)$' \
|
||||||
|
| grep -v -E '^lib/CrossPointFont/builtinFonts/' \
|
||||||
|
| grep -v -E '^lib/CrossPointFont/Group5' \
|
||||||
|
| xargs -r clang-format -style=file -i
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
# File Formats
|
||||||
|
|
||||||
|
## `book.bin`
|
||||||
|
|
||||||
|
### Version 3
|
||||||
|
|
||||||
|
ImHex Pattern:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
import std.mem;
|
||||||
|
import std.string;
|
||||||
|
import std.core;
|
||||||
|
|
||||||
|
// === Configuration ===
|
||||||
|
#define EXPECTED_VERSION 3
|
||||||
|
#define MAX_STRING_LENGTH 65535
|
||||||
|
|
||||||
|
// === String Structure ===
|
||||||
|
|
||||||
|
struct String {
|
||||||
|
u32 length [[hidden, comment("String byte length")]];
|
||||||
|
if (length > MAX_STRING_LENGTH) {
|
||||||
|
std::warning(std::format("Unusually large string length: {} bytes", length));
|
||||||
|
}
|
||||||
|
char data[length] [[comment("UTF-8 string data")]];
|
||||||
|
} [[sealed, format("format_string"), comment("Length-prefixed UTF-8 string")]];
|
||||||
|
|
||||||
|
fn format_string(String s) {
|
||||||
|
return s.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Metadata Structure ===
|
||||||
|
|
||||||
|
struct Metadata {
|
||||||
|
String title [[comment("Book title")]];
|
||||||
|
String author [[comment("Book author")]];
|
||||||
|
String coverItemHref [[comment("Path to cover image")]];
|
||||||
|
String textReferenceHref [[comment("Path to guided first text reference")]];
|
||||||
|
} [[comment("Book metadata information")]];
|
||||||
|
|
||||||
|
// === Spine Entry Structure ===
|
||||||
|
|
||||||
|
struct SpineEntry {
|
||||||
|
String href [[comment("Resource path")]];
|
||||||
|
u32 cumulativeSize [[comment("Cumulative size in bytes"), color("FF6B6B")]];
|
||||||
|
s16 tocIndex [[comment("Index into TOC (-1 if none)"), color("4ECDC4")]];
|
||||||
|
} [[comment("Spine entry defining reading order")]];
|
||||||
|
|
||||||
|
// === TOC Entry Structure ===
|
||||||
|
|
||||||
|
struct TocEntry {
|
||||||
|
String title [[comment("Chapter/section title")]];
|
||||||
|
String href [[comment("Resource path")]];
|
||||||
|
String anchor [[comment("Fragment identifier")]];
|
||||||
|
u8 level [[comment("Nesting level (0-255)"), color("95E1D3")]];
|
||||||
|
s16 spineIndex [[comment("Index into spine (-1 if none)"), color("F38181")]];
|
||||||
|
} [[comment("Table of contents entry")]];
|
||||||
|
|
||||||
|
// === Book Bin Structure ===
|
||||||
|
|
||||||
|
struct BookBin {
|
||||||
|
// Header
|
||||||
|
u8 version [[comment("Format version"), color("FFD93D")]];
|
||||||
|
|
||||||
|
// Version validation
|
||||||
|
if (version != EXPECTED_VERSION) {
|
||||||
|
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
|
||||||
|
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
|
||||||
|
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
|
||||||
|
|
||||||
|
// Metadata section
|
||||||
|
Metadata metadata [[comment("Book metadata")]];
|
||||||
|
|
||||||
|
// Validate LUT offset alignment
|
||||||
|
u32 currentOffset = $;
|
||||||
|
if (currentOffset != lutOffset) {
|
||||||
|
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup Tables
|
||||||
|
u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]];
|
||||||
|
u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]];
|
||||||
|
|
||||||
|
// Data Entries
|
||||||
|
SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]];
|
||||||
|
TocEntry toc[tocCount] [[comment("Table of contents entries")]];
|
||||||
|
};
|
||||||
|
|
||||||
|
// === File Parsing ===
|
||||||
|
|
||||||
|
BookBin book @ 0x00;
|
||||||
|
|
||||||
|
// Validate we've consumed the entire file
|
||||||
|
u32 fileSize = std::mem::size();
|
||||||
|
u32 parsedSize = $;
|
||||||
|
|
||||||
|
if (parsedSize != fileSize) {
|
||||||
|
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `section.bin`
|
||||||
|
|
||||||
|
### Version 8
|
||||||
|
|
||||||
|
ImHex Pattern:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
import std.mem;
|
||||||
|
import std.string;
|
||||||
|
import std.core;
|
||||||
|
|
||||||
|
// === Configuration ===
|
||||||
|
#define EXPECTED_VERSION 8
|
||||||
|
#define MAX_STRING_LENGTH 65535
|
||||||
|
|
||||||
|
// === String Structure ===
|
||||||
|
|
||||||
|
struct String {
|
||||||
|
u32 length [[hidden, comment("String byte length")]];
|
||||||
|
if (length > MAX_STRING_LENGTH) {
|
||||||
|
std::warning(std::format("Unusually large string length: {} bytes", length));
|
||||||
|
}
|
||||||
|
char data[length] [[comment("UTF-8 string data")]];
|
||||||
|
} [[sealed, format("format_string"), comment("Length-prefixed UTF-8 string")]];
|
||||||
|
|
||||||
|
fn format_string(String s) {
|
||||||
|
return s.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Page Structure ===
|
||||||
|
|
||||||
|
enum StorageType : u8 {
|
||||||
|
PageLine = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
enum WordStyle : u8 {
|
||||||
|
REGULAR = 0,
|
||||||
|
BOLD = 1,
|
||||||
|
ITALIC = 2,
|
||||||
|
BOLD_ITALIC = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
enum BlockStyle : u8 {
|
||||||
|
JUSTIFIED = 0,
|
||||||
|
LEFT_ALIGN = 1,
|
||||||
|
CENTER_ALIGN = 2,
|
||||||
|
RIGHT_ALIGN = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PageLine {
|
||||||
|
s16 xPos;
|
||||||
|
s16 yPos;
|
||||||
|
u16 wordCount;
|
||||||
|
String words[wordCount];
|
||||||
|
u16 wordXPos[wordCount];
|
||||||
|
WordStyle wordStyle[wordCount];
|
||||||
|
BlockStyle blockStyle;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PageElement {
|
||||||
|
u8 pageElementType;
|
||||||
|
if (pageElementType == 1) {
|
||||||
|
PageLine pageLine [[inline]];
|
||||||
|
} else {
|
||||||
|
std::error(std::format("Unknown page element type: {}", pageElementType));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Page {
|
||||||
|
u16 elementCount;
|
||||||
|
PageElement elements[elementCount] [[inline]];
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Section Bin Structure ===
|
||||||
|
|
||||||
|
struct SectionBin {
|
||||||
|
// Header
|
||||||
|
u8 version [[comment("Format version"), color("FFD93D")]];
|
||||||
|
|
||||||
|
// Version validation
|
||||||
|
if (version != EXPECTED_VERSION) {
|
||||||
|
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache busting parameters
|
||||||
|
s32 fontId;
|
||||||
|
float lineCompression;
|
||||||
|
bool extraParagraphSpacing;
|
||||||
|
u16 viewportWidth;
|
||||||
|
u16 vieportHeight;
|
||||||
|
u16 pageCount;
|
||||||
|
u32 lutOffset;
|
||||||
|
|
||||||
|
Page page[pageCount];
|
||||||
|
|
||||||
|
// Validate LUT offset alignment
|
||||||
|
u32 currentOffset = $;
|
||||||
|
if (currentOffset != lutOffset) {
|
||||||
|
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup Tables
|
||||||
|
u32 lut[pageCount];
|
||||||
|
};
|
||||||
|
|
||||||
|
// === File Parsing ===
|
||||||
|
|
||||||
|
SectionBin book @ 0x00;
|
||||||
|
|
||||||
|
// Validate we've consumed the entire file
|
||||||
|
u32 fileSize = std::mem::size();
|
||||||
|
u32 parsedSize = $;
|
||||||
|
|
||||||
|
if (parsedSize != fileSize) {
|
||||||
|
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Hypher Binary Tries
|
||||||
|
|
||||||
|
CrossPoint embeds the exact binary automata produced by
|
||||||
|
[Typst's `hypher`](https://github.com/typst/hypher).
|
||||||
|
|
||||||
|
## File layout
|
||||||
|
|
||||||
|
Each `.bin` blob is a single self-contained automaton:
|
||||||
|
|
||||||
|
```
|
||||||
|
uint32_t root_addr_be; // big-endian offset of the root node
|
||||||
|
uint8_t levels[]; // shared "levels" tape (dist/score pairs)
|
||||||
|
uint8_t nodes[]; // node records packed back-to-back
|
||||||
|
```
|
||||||
|
|
||||||
|
The size of the `levels` tape is implicit. Individual nodes reference slices
|
||||||
|
inside that tape via 12-bit offsets, so no additional pointers are required.
|
||||||
|
|
||||||
|
### Node encoding
|
||||||
|
|
||||||
|
Every node starts with a single control byte:
|
||||||
|
|
||||||
|
- Bit 7 – set when the node stores scores (`levels`).
|
||||||
|
- Bits 5-6 – stride of the target deltas (1, 2, or 3 bytes, big-endian).
|
||||||
|
- Bits 0-4 – transition count (values ≥ 31 spill into an extra byte).
|
||||||
|
|
||||||
|
If the `levels` flag is set, two more bytes follow. Together they encode a
|
||||||
|
12-bit offset into the global `levels` tape and a 4-bit length. Each byte in the
|
||||||
|
levels tape packs a distance/score pair as `dist * 10 + score`, where `dist`
|
||||||
|
counts how many UTF-8 bytes we advanced since the previous digit.
|
||||||
|
|
||||||
|
After the optional levels header come the transition labels (one byte per edge)
|
||||||
|
followed by the signed target deltas. Targets are stored as relative offsets
|
||||||
|
from the current node address. Deltas up to ±128 fit in a single byte, larger
|
||||||
|
distances grow to 2 or 3 bytes. The runtime walks the transitions with a simple
|
||||||
|
linear scan and materializes the absolute address by adding the decoded delta
|
||||||
|
to the current node’s base.
|
||||||
|
|
||||||
|
## Embedding blobs into the firmware
|
||||||
|
|
||||||
|
The helper script `scripts/generate_hyphenation_trie.py` acts as a thin
|
||||||
|
wrapper: it reads the hypher-generated `.bin` files, formats them as `constexpr`
|
||||||
|
byte arrays, and emits headers under
|
||||||
|
`lib/Epub/Epub/hyphenation/generated/`. Each header defines the raw data plus a
|
||||||
|
`SerializedHyphenationPatterns` descriptor so the reader can keep the automaton
|
||||||
|
in flash.
|
||||||
|
|
||||||
|
To refresh the firmware assets after updating the `.bin` files, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
./scripts/generate_hyphenation_trie.py \
|
||||||
|
--input lib/Epub/Epub/hyphenation/tries/en.bin \
|
||||||
|
--output lib/Epub/Epub/hyphenation/generated/hyph-en.trie.h
|
||||||
|
|
||||||
|
./scripts/generate_hyphenation_trie.py \
|
||||||
|
--input lib/Epub/Epub/hyphenation/tries/fr.bin \
|
||||||
|
--output lib/Epub/Epub/hyphenation/generated/hyph-fr.trie.h
|
||||||
|
|
||||||
|
./scripts/generate_hyphenation_trie.py \
|
||||||
|
--input lib/Epub/Epub/hyphenation/tries/de.bin \
|
||||||
|
--output lib/Epub/Epub/hyphenation/generated/hyph-de.trie.h
|
||||||
|
|
||||||
|
./scripts/generate_hyphenation_trie.py \
|
||||||
|
--input lib/Epub/Epub/hyphenation/tries/ru.bin \
|
||||||
|
--output lib/Epub/Epub/hyphenation/generated/hyph-ru.trie.h
|
||||||
|
```
|
||||||
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 899 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
@@ -0,0 +1,57 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
This document show most common issues and possible solutions while using the device features.
|
||||||
|
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Cannot See the Device on the Network](#cannot-see-the-device-on-the-network)
|
||||||
|
- [Connection Drops or Times Out](#connection-drops-or-times-out)
|
||||||
|
- [Upload Fails](#upload-fails)
|
||||||
|
- [Saved Password Not Working](#saved-password-not-working)
|
||||||
|
|
||||||
|
### Cannot See the Device on the Network
|
||||||
|
|
||||||
|
**Problem:** Browser shows "Cannot connect" or "Site can't be reached"
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
|
||||||
|
1. Verify both devices are on the **same WiFi network**
|
||||||
|
- Check your computer/phone WiFi settings
|
||||||
|
- Confirm the CrossPoint Reader shows "Connected" status
|
||||||
|
2. Double-check the IP address
|
||||||
|
- Make sure you typed it correctly
|
||||||
|
- Include `http://` at the beginning
|
||||||
|
3. Try disabling VPN if you're using one
|
||||||
|
4. Some networks have "client isolation" enabled - check with your network administrator
|
||||||
|
|
||||||
|
### Connection Drops or Times Out
|
||||||
|
|
||||||
|
**Problem:** WiFi connection is unstable
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
|
||||||
|
1. Move closer to the WiFi router
|
||||||
|
2. Check signal strength on the device (should be at least `||` or better)
|
||||||
|
3. Avoid interference from other devices
|
||||||
|
4. Try a different WiFi network if available
|
||||||
|
|
||||||
|
### Upload Fails
|
||||||
|
|
||||||
|
**Problem:** File upload doesn't complete or shows an error
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
|
||||||
|
1. Ensure the file is a valid `.epub` file
|
||||||
|
2. Check that the SD card has enough free space
|
||||||
|
3. Try uploading a smaller file first to test
|
||||||
|
4. Refresh the browser page and try again
|
||||||
|
|
||||||
|
### Saved Password Not Working
|
||||||
|
|
||||||
|
**Problem:** Device fails to connect with saved credentials
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
|
||||||
|
1. When connection fails, you'll be prompted to "Forget Network"
|
||||||
|
2. Select **Yes** to remove the saved password
|
||||||
|
3. Reconnect and enter the password again
|
||||||
|
4. Choose to save the new password
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
# Webserver Endpoints
|
||||||
|
|
||||||
|
This document describes all HTTP and WebSocket endpoints available on the CrossPoint Reader webserver.
|
||||||
|
|
||||||
|
- [Webserver Endpoints](#webserver-endpoints)
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [HTTP Endpoints](#http-endpoints)
|
||||||
|
- [GET `/` - Home Page](#get----home-page)
|
||||||
|
- [GET `/files` - File Browser Page](#get-files---file-browser-page)
|
||||||
|
- [GET `/api/status` - Device Status](#get-apistatus---device-status)
|
||||||
|
- [GET `/api/files` - List Files](#get-apifiles---list-files)
|
||||||
|
- [POST `/upload` - Upload File](#post-upload---upload-file)
|
||||||
|
- [POST `/mkdir` - Create Folder](#post-mkdir---create-folder)
|
||||||
|
- [POST `/delete` - Delete File or Folder](#post-delete---delete-file-or-folder)
|
||||||
|
- [WebSocket Endpoint](#websocket-endpoint)
|
||||||
|
- [Port 81 - Fast Binary Upload](#port-81---fast-binary-upload)
|
||||||
|
- [Network Modes](#network-modes)
|
||||||
|
- [Station Mode (STA)](#station-mode-sta)
|
||||||
|
- [Access Point Mode (AP)](#access-point-mode-ap)
|
||||||
|
- [Notes](#notes)
|
||||||
|
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The CrossPoint Reader exposes a webserver for file management and device monitoring:
|
||||||
|
|
||||||
|
- **HTTP Server**: Port 80
|
||||||
|
- **WebSocket Server**: Port 81 (for fast binary uploads)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HTTP Endpoints
|
||||||
|
|
||||||
|
### GET `/` - Home Page
|
||||||
|
|
||||||
|
Serves the home page HTML interface.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl http://crosspoint.local/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** HTML page (200 OK)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET `/files` - File Browser Page
|
||||||
|
|
||||||
|
Serves the file browser HTML interface.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl http://crosspoint.local/files
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** HTML page (200 OK)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET `/api/status` - Device Status
|
||||||
|
|
||||||
|
Returns JSON with device status information.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl http://crosspoint.local/api/status
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"ip": "192.168.1.100",
|
||||||
|
"mode": "STA",
|
||||||
|
"rssi": -45,
|
||||||
|
"freeHeap": 123456,
|
||||||
|
"uptime": 3600
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| ---------- | ------ | --------------------------------------------------------- |
|
||||||
|
| `version` | string | CrossPoint firmware version |
|
||||||
|
| `ip` | string | Device IP address |
|
||||||
|
| `mode` | string | `"STA"` (connected to WiFi) or `"AP"` (access point mode) |
|
||||||
|
| `rssi` | number | WiFi signal strength in dBm (0 in AP mode) |
|
||||||
|
| `freeHeap` | number | Free heap memory in bytes |
|
||||||
|
| `uptime` | number | Seconds since device boot |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET `/api/files` - List Files
|
||||||
|
|
||||||
|
Returns a JSON array of files and folders in the specified directory.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
# List root directory
|
||||||
|
curl http://crosspoint.local/api/files
|
||||||
|
|
||||||
|
# List specific directory
|
||||||
|
curl "http://crosspoint.local/api/files?path=/Books"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Required | Default | Description |
|
||||||
|
| --------- | -------- | ------- | ---------------------- |
|
||||||
|
| `path` | No | `/` | Directory path to list |
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"name": "MyBook.epub", "size": 1234567, "isDirectory": false, "isEpub": true},
|
||||||
|
{"name": "Notes", "size": 0, "isDirectory": true, "isEpub": false},
|
||||||
|
{"name": "document.pdf", "size": 54321, "isDirectory": false, "isEpub": false}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| ------------- | ------- | ---------------------------------------- |
|
||||||
|
| `name` | string | File or folder name |
|
||||||
|
| `size` | number | Size in bytes (0 for directories) |
|
||||||
|
| `isDirectory` | boolean | `true` if the item is a folder |
|
||||||
|
| `isEpub` | boolean | `true` if the file has `.epub` extension |
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- Hidden files (starting with `.`) are automatically filtered out
|
||||||
|
- System folders (`System Volume Information`, `XTCache`) are hidden
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST `/upload` - Upload File
|
||||||
|
|
||||||
|
Uploads a file to the SD card via multipart form data.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
# Upload to root directory
|
||||||
|
curl -X POST -F "file=@mybook.epub" http://crosspoint.local/upload
|
||||||
|
|
||||||
|
# Upload to specific directory
|
||||||
|
curl -X POST -F "file=@mybook.epub" "http://crosspoint.local/upload?path=/Books"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Required | Default | Description |
|
||||||
|
| --------- | -------- | ------- | ------------------------------- |
|
||||||
|
| `path` | No | `/` | Target directory for the upload |
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```
|
||||||
|
File uploaded successfully: mybook.epub
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
|
||||||
|
| Status | Body | Cause |
|
||||||
|
| ------ | ----------------------------------------------- | --------------------------- |
|
||||||
|
| 400 | `Failed to create file on SD card` | Cannot create file |
|
||||||
|
| 400 | `Failed to write to SD card - disk may be full` | Write error during upload |
|
||||||
|
| 400 | `Failed to write final data to SD card` | Error flushing final buffer |
|
||||||
|
| 400 | `Upload aborted` | Client aborted the upload |
|
||||||
|
| 400 | `Unknown error during upload` | Unspecified error |
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- Existing files with the same name will be overwritten
|
||||||
|
- Uses a 4KB buffer for efficient SD card writes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST `/mkdir` - Create Folder
|
||||||
|
|
||||||
|
Creates a new folder on the SD card.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST -d "name=NewFolder&path=/" http://crosspoint.local/mkdir
|
||||||
|
```
|
||||||
|
|
||||||
|
**Form Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Required | Default | Description |
|
||||||
|
| --------- | -------- | ------- | ---------------------------- |
|
||||||
|
| `name` | Yes | - | Name of the folder to create |
|
||||||
|
| `path` | No | `/` | Parent directory path |
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```
|
||||||
|
Folder created: NewFolder
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
|
||||||
|
| Status | Body | Cause |
|
||||||
|
| ------ | ----------------------------- | ----------------------------- |
|
||||||
|
| 400 | `Missing folder name` | `name` parameter not provided |
|
||||||
|
| 400 | `Folder name cannot be empty` | Empty folder name |
|
||||||
|
| 400 | `Folder already exists` | Folder with same name exists |
|
||||||
|
| 500 | `Failed to create folder` | SD card error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST `/delete` - Delete File or Folder
|
||||||
|
|
||||||
|
Deletes a file or folder from the SD card.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```bash
|
||||||
|
# Delete a file
|
||||||
|
curl -X POST -d "path=/Books/mybook.epub&type=file" http://crosspoint.local/delete
|
||||||
|
|
||||||
|
# Delete an empty folder
|
||||||
|
curl -X POST -d "path=/OldFolder&type=folder" http://crosspoint.local/delete
|
||||||
|
```
|
||||||
|
|
||||||
|
**Form Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Required | Default | Description |
|
||||||
|
| --------- | -------- | ------- | -------------------------------- |
|
||||||
|
| `path` | Yes | - | Path to the item to delete |
|
||||||
|
| `type` | No | `file` | Type of item: `file` or `folder` |
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```
|
||||||
|
Deleted successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
|
||||||
|
| Status | Body | Cause |
|
||||||
|
| ------ | --------------------------------------------- | ----------------------------- |
|
||||||
|
| 400 | `Missing path` | `path` parameter not provided |
|
||||||
|
| 400 | `Cannot delete root directory` | Attempted to delete `/` |
|
||||||
|
| 400 | `Folder is not empty. Delete contents first.` | Non-empty folder |
|
||||||
|
| 403 | `Cannot delete system files` | Hidden file (starts with `.`) |
|
||||||
|
| 403 | `Cannot delete protected items` | Protected system folder |
|
||||||
|
| 404 | `Item not found` | Path does not exist |
|
||||||
|
| 500 | `Failed to delete item` | SD card error |
|
||||||
|
|
||||||
|
**Protected Items:**
|
||||||
|
- Files/folders starting with `.`
|
||||||
|
- `System Volume Information`
|
||||||
|
- `XTCache`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WebSocket Endpoint
|
||||||
|
|
||||||
|
### Port 81 - Fast Binary Upload
|
||||||
|
|
||||||
|
A WebSocket endpoint for high-speed binary file uploads. More efficient than HTTP multipart for large files.
|
||||||
|
|
||||||
|
**Connection:**
|
||||||
|
```
|
||||||
|
ws://crosspoint.local:81/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Protocol:**
|
||||||
|
|
||||||
|
1. **Client** sends TEXT message: `START:<filename>:<size>:<path>`
|
||||||
|
2. **Server** responds with TEXT: `READY`
|
||||||
|
3. **Client** sends BINARY messages with file data chunks
|
||||||
|
4. **Server** sends TEXT progress updates: `PROGRESS:<received>:<total>`
|
||||||
|
5. **Server** sends TEXT when complete: `DONE` or `ERROR:<message>`
|
||||||
|
|
||||||
|
**Example Session:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Client -> "START:mybook.epub:1234567:/Books"
|
||||||
|
Server -> "READY"
|
||||||
|
Client -> [binary chunk 1]
|
||||||
|
Client -> [binary chunk 2]
|
||||||
|
Server -> "PROGRESS:65536:1234567"
|
||||||
|
Client -> [binary chunk 3]
|
||||||
|
...
|
||||||
|
Server -> "PROGRESS:1234567:1234567"
|
||||||
|
Server -> "DONE"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Messages:**
|
||||||
|
|
||||||
|
| Message | Cause |
|
||||||
|
| --------------------------------- | ---------------------------------- |
|
||||||
|
| `ERROR:Failed to create file` | Cannot create file on SD card |
|
||||||
|
| `ERROR:Invalid START format` | Malformed START message |
|
||||||
|
| `ERROR:No upload in progress` | Binary data received without START |
|
||||||
|
| `ERROR:Write failed - disk full?` | SD card write error |
|
||||||
|
|
||||||
|
**Example with `websocat`:**
|
||||||
|
```bash
|
||||||
|
# Interactive session
|
||||||
|
websocat ws://crosspoint.local:81
|
||||||
|
|
||||||
|
# Then type:
|
||||||
|
START:mybook.epub:1234567:/Books
|
||||||
|
# Wait for READY, then send binary data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- Progress updates are sent every 64KB or at completion
|
||||||
|
- Disconnection during upload will delete the incomplete file
|
||||||
|
- Existing files with the same name will be overwritten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Network Modes
|
||||||
|
|
||||||
|
The device can operate in two network modes:
|
||||||
|
|
||||||
|
### Station Mode (STA)
|
||||||
|
- Device connects to an existing WiFi network
|
||||||
|
- IP address assigned by router/DHCP
|
||||||
|
- `mode` field in `/api/status` returns `"STA"`
|
||||||
|
- `rssi` field shows signal strength
|
||||||
|
|
||||||
|
### Access Point Mode (AP)
|
||||||
|
- Device creates its own WiFi hotspot
|
||||||
|
- Default IP is typically `192.168.4.1`
|
||||||
|
- `mode` field in `/api/status` returns `"AP"`
|
||||||
|
- `rssi` field returns `0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- These examples use `crosspoint.local`. If your network does not support mDNS or the address does not resolve, replace it with the specific **IP Address** displayed on your device screen (e.g., `http://192.168.1.102/`).
|
||||||
|
- All paths on the SD card start with `/`
|
||||||
|
- Trailing slashes are automatically stripped (except for root `/`)
|
||||||
|
- The webserver uses chunked transfer encoding for file listings
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
# Web Server Guide
|
||||||
|
|
||||||
|
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload EPUB files from your computer or phone.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
CrossPoint Reader includes a built-in web server that allows you to:
|
||||||
|
|
||||||
|
- Upload EPUB files wirelessly from any device on the same WiFi network
|
||||||
|
- Browse and manage files on your device's SD card
|
||||||
|
- Create folders to organize your ebooks
|
||||||
|
- Delete files and folders
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Your CrossPoint Reader device
|
||||||
|
- A WiFi network
|
||||||
|
- A computer, phone, or tablet connected to the **same WiFi network**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Accessing the WiFi Screen
|
||||||
|
|
||||||
|
1. From the main menu or file browser, navigate to the **Settings** screen
|
||||||
|
2. Select the **WiFi** option
|
||||||
|
3. The device will automatically start scanning for available networks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Connecting to WiFi
|
||||||
|
|
||||||
|
### Viewing Available Networks
|
||||||
|
|
||||||
|
Once the scan completes, you'll see a list of available WiFi networks with the following indicators:
|
||||||
|
|
||||||
|
- **Signal strength bars** (`||||`, `|||`, `||`, `|`) - Shows connection quality
|
||||||
|
- **`*` symbol** - Indicates the network is password-protected (encrypted)
|
||||||
|
- **`+` symbol** - Indicates you have previously saved credentials for this network
|
||||||
|
|
||||||
|
<img src="./images/wifi/wifi_networks.jpeg" height="500">
|
||||||
|
|
||||||
|
### Selecting a Network
|
||||||
|
|
||||||
|
1. Use the **Left/Right** (or **Volume Up/Down**) buttons to navigate through the network list
|
||||||
|
2. Press **Confirm** to select the highlighted network
|
||||||
|
|
||||||
|
### Entering Password (for encrypted networks)
|
||||||
|
|
||||||
|
If the network requires a password:
|
||||||
|
|
||||||
|
1. An on-screen keyboard will appear
|
||||||
|
2. Use the navigation buttons to select characters
|
||||||
|
3. Press **Confirm** to enter each character
|
||||||
|
4. When complete, select the **Done** option on the keyboard
|
||||||
|
|
||||||
|
<img src="./images/wifi/wifi_password.jpeg" height="500">
|
||||||
|
|
||||||
|
**Note:** If you've previously connected to this network, the saved password will be used automatically.
|
||||||
|
|
||||||
|
### Connection Process
|
||||||
|
|
||||||
|
The device will display "Connecting..." while establishing the connection. This typically takes 5-10 seconds.
|
||||||
|
|
||||||
|
### Saving Credentials
|
||||||
|
|
||||||
|
If this is a new network, you'll be prompted to save the password:
|
||||||
|
|
||||||
|
- Select **Yes** to save credentials for automatic connection next time (NOTE: These are stored in plaintext on the device's SD card. Do not use this for sensitive networks.)
|
||||||
|
- Select **No** to connect without saving
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Connection Success
|
||||||
|
|
||||||
|
Once connected, the screen will display:
|
||||||
|
|
||||||
|
- **Network name** (SSID)
|
||||||
|
- **IP Address** (e.g., `192.168.1.102`)
|
||||||
|
- **Web server URL** (e.g., `http://192.168.1.102/`)
|
||||||
|
|
||||||
|
<img src="./images/wifi/wifi_connected.jpeg" height="500">
|
||||||
|
|
||||||
|
**Important:** Make note of the IP address - you'll need this to access the web interface from your computer or phone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Accessing the Web Interface
|
||||||
|
|
||||||
|
### From a Computer
|
||||||
|
|
||||||
|
1. Ensure your computer is connected to the **same WiFi network** as your CrossPoint Reader
|
||||||
|
2. Open any web browser (Chrome is recommended)
|
||||||
|
3. Type the IP address shown on your device into the browser's address bar
|
||||||
|
- Example: `http://192.168.1.102/`
|
||||||
|
4. Press Enter
|
||||||
|
|
||||||
|
### From a Phone or Tablet
|
||||||
|
|
||||||
|
1. Ensure your phone/tablet is connected to the **same WiFi network** as your CrossPoint Reader
|
||||||
|
2. Open your mobile browser (Safari, Chrome, etc.)
|
||||||
|
3. Type the IP address into the address bar
|
||||||
|
- Example: `http://192.168.1.102/`
|
||||||
|
4. Tap Go
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Using the Web Interface
|
||||||
|
|
||||||
|
### Home Page
|
||||||
|
|
||||||
|
The home page displays:
|
||||||
|
|
||||||
|
- Device status and version information
|
||||||
|
- WiFi connection status
|
||||||
|
- Current IP address
|
||||||
|
- Available memory
|
||||||
|
|
||||||
|
Navigation links:
|
||||||
|
|
||||||
|
- **Home** - Returns to the status page
|
||||||
|
- **File Manager** - Access file management features
|
||||||
|
|
||||||
|
<img src="./images/wifi/webserver_homepage.png" width="600">
|
||||||
|
|
||||||
|
### File Manager
|
||||||
|
|
||||||
|
Click **File Manager** to access file management features.
|
||||||
|
|
||||||
|
#### Browsing Files
|
||||||
|
|
||||||
|
- The file manager displays all files and folders on your SD card
|
||||||
|
- **Folders** are highlighted in yellow with a 📁 icon
|
||||||
|
- **EPUB files** are highlighted in green with a 📗 icon
|
||||||
|
- Click on a folder name to navigate into it
|
||||||
|
- Use the breadcrumb navigation at the top to go back to parent folders
|
||||||
|
|
||||||
|
<img src="./images/wifi/webserver_files.png" width="600">
|
||||||
|
|
||||||
|
#### Uploading EPUB Files
|
||||||
|
|
||||||
|
1. Click the **+ Add** button in the top-right corner
|
||||||
|
2. Select **Upload eBook** from the dropdown menu
|
||||||
|
3. Click **Choose File** and select an `.epub` file from your device
|
||||||
|
4. Click **Upload**
|
||||||
|
5. A progress bar will show the upload status
|
||||||
|
6. The page will automatically refresh when the upload is complete
|
||||||
|
|
||||||
|
**Note:** Only `.epub` files are accepted. Other file types will be rejected.
|
||||||
|
|
||||||
|
<img src="./images/wifi/webserver_upload.png" width="600">
|
||||||
|
|
||||||
|
#### Creating Folders
|
||||||
|
|
||||||
|
1. Click the **+ Add** button in the top-right corner
|
||||||
|
2. Select **New Folder** from the dropdown menu
|
||||||
|
3. Enter a folder name (letters, numbers, underscores, and hyphens only)
|
||||||
|
4. Click **Create Folder**
|
||||||
|
|
||||||
|
This is useful for organizing your ebooks by genre, author, or series.
|
||||||
|
|
||||||
|
#### Deleting Files and Folders
|
||||||
|
|
||||||
|
1. Click the **🗑️** (trash) icon next to any file or folder
|
||||||
|
2. Confirm the deletion in the popup dialog
|
||||||
|
3. Click **Delete** to permanently remove the item
|
||||||
|
|
||||||
|
**Warning:** Deletion is permanent and cannot be undone!
|
||||||
|
|
||||||
|
**Note:** Folders must be empty before they can be deleted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Command Line File Management
|
||||||
|
|
||||||
|
For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode a detailed documentation can be found [here](./webserver-endpoints.md).
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- The web server runs on port 80 (standard HTTP)
|
||||||
|
- **No authentication is required** - anyone on the same network can access the interface
|
||||||
|
- The web server is only accessible while the WiFi screen shows "Connected"
|
||||||
|
- The web server automatically stops when you exit the WiFi screen
|
||||||
|
- For security, only use on trusted private networks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
- **Supported WiFi:** 2.4GHz networks (802.11 b/g/n)
|
||||||
|
- **Web Server Port:** 80 (HTTP)
|
||||||
|
- **Maximum Upload Size:** Limited by available SD card space
|
||||||
|
- **Supported File Format:** `.epub` only
|
||||||
|
- **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tips and Best Practices
|
||||||
|
|
||||||
|
1. **Organize with folders** - Create folders before uploading to keep your library organized
|
||||||
|
2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads
|
||||||
|
3. **Upload multiple files** - You can upload files one at a time; the page refreshes after each upload
|
||||||
|
4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction")
|
||||||
|
5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future
|
||||||
|
6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exiting WiFi Mode
|
||||||
|
|
||||||
|
When you're finished uploading files:
|
||||||
|
|
||||||
|
1. Press the **Back** button on your CrossPoint Reader
|
||||||
|
2. The web server will automatically stop
|
||||||
|
3. WiFi will disconnect to conserve battery
|
||||||
|
4. You'll return to the previous screen
|
||||||
|
|
||||||
|
Your uploaded files will be immediately available in the file browser!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [User Guide](../USER_GUIDE.md) - General device operation
|
||||||
|
- [Troubleshooting](./troubleshooting.md) - Troubleshooting
|
||||||
|
- [README](../README.md) - Project overview and features
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
#include "CrossPointFont.h"
|
||||||
|
|
||||||
|
#include <Utf8.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#define FONT_SCALE 2
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Number of set bits from 0->15
|
||||||
|
uint8_t bitCount[] = {
|
||||||
|
0, // 0b0000,
|
||||||
|
1, // 0b0001,
|
||||||
|
1, // 0b0010,
|
||||||
|
2, // 0b0011,
|
||||||
|
1, // 0b0100,
|
||||||
|
2, // 0b0101,
|
||||||
|
2, // 0b0110,
|
||||||
|
3, // 0b0111,
|
||||||
|
1, // 0b1000,
|
||||||
|
2, // 0b1001,
|
||||||
|
2, // 0b1010,
|
||||||
|
3, // 0b1011,
|
||||||
|
2, // 0b1100,
|
||||||
|
3, // 0b1101,
|
||||||
|
3, // 0b1110,
|
||||||
|
4, // 0b1111,
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void CrossPointFont::getTextBounds(const char* string, const Style style, const int startX, const int startY, int* minX,
|
||||||
|
int* minY, int* maxX, int* maxY) const {
|
||||||
|
*minX = startX;
|
||||||
|
*minY = startY;
|
||||||
|
*maxX = startX;
|
||||||
|
*maxY = startY;
|
||||||
|
|
||||||
|
if (*string == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cursorX = startX;
|
||||||
|
const int cursorY = startY;
|
||||||
|
uint32_t cp;
|
||||||
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||||
|
const CrossPointFontGlyph* glyph = getGlyph(cp, style);
|
||||||
|
|
||||||
|
if (!glyph) {
|
||||||
|
glyph = getGlyph(REPLACEMENT_GLYPH, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!glyph) {
|
||||||
|
// TODO: Better handle this?
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
*minX = std::min(*minX, cursorX + glyph->xOffset / FONT_SCALE);
|
||||||
|
*maxX = std::max(*maxX, cursorX + (glyph->xOffset + glyph->width) / FONT_SCALE);
|
||||||
|
*minY = std::min(*minY, cursorY + (glyph->yOffset - glyph->height) / FONT_SCALE);
|
||||||
|
*maxY = std::max(*maxY, cursorY + glyph->yOffset / FONT_SCALE);
|
||||||
|
cursorX += glyph->xAdvance / FONT_SCALE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossPointFont::getTextDimensions(const char* string, const Style style, int* w, int* h) const {
|
||||||
|
int minX = 0, minY = 0, maxX = 0, maxY = 0;
|
||||||
|
|
||||||
|
getTextBounds(string, style, 0, 0, &minX, &minY, &maxX, &maxY);
|
||||||
|
|
||||||
|
*w = maxX - minX;
|
||||||
|
*h = maxY - minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CrossPointFont::styleGroup(const Style style) const {
|
||||||
|
if (style == REGULAR) return 0;
|
||||||
|
|
||||||
|
if (data.header.styles == 0b0001) {
|
||||||
|
// Only have regular font, show regular
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (data.header.styles == 0b0011) {
|
||||||
|
// Only have bold and regular font
|
||||||
|
// Show bold if style is bold or bold_italic
|
||||||
|
return style == BOLD || style == BOLD_ITALIC ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (data.header.styles == 0b0101) {
|
||||||
|
// Only have italic and regular font
|
||||||
|
// Show italic if style is italic or bold_italic
|
||||||
|
return style == ITALIC || style == BOLD_ITALIC ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (data.header.styles == 0b1001) {
|
||||||
|
// Only have bold_italic and regular font
|
||||||
|
// Show bold_italic if style is any non-regular
|
||||||
|
return style == BOLD_ITALIC || style == BOLD || style == ITALIC ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (data.header.styles == 0b0111) {
|
||||||
|
// Have all but bold_italic
|
||||||
|
// Show bold if style is bold_italic, otherwise show the requested style
|
||||||
|
return style == BOLD_ITALIC ? 1 : style;
|
||||||
|
}
|
||||||
|
if (data.header.styles == 0b1011) {
|
||||||
|
// Have all but italic
|
||||||
|
// Show bold_italic if style is italic, otherwise show the requested style
|
||||||
|
return style == ITALIC ? 2 : style;
|
||||||
|
}
|
||||||
|
if (data.header.styles == 0b1101) {
|
||||||
|
// Have all but bold
|
||||||
|
// Show bold_italic if style is bold, otherwise show the requested style
|
||||||
|
return style == BOLD ? 2 : style;
|
||||||
|
}
|
||||||
|
if (data.header.styles == 0b1111) {
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CrossPointFontGlyph* CrossPointFont::getGlyph(const uint32_t cp, const Style style) const {
|
||||||
|
const CrossPointFontUnicodeInterval* intervals = data.intervals;
|
||||||
|
const int count = data.header.intervalCount;
|
||||||
|
|
||||||
|
if (count == 0) return nullptr;
|
||||||
|
|
||||||
|
// Binary search for O(log n) lookup instead of O(n)
|
||||||
|
// Critical for Korean fonts with many unicode intervals
|
||||||
|
int left = 0;
|
||||||
|
int right = count - 1;
|
||||||
|
|
||||||
|
while (left <= right) {
|
||||||
|
const int mid = left + (right - left) / 2;
|
||||||
|
const CrossPointFontUnicodeInterval* interval = &intervals[mid];
|
||||||
|
|
||||||
|
if (cp < interval->first) {
|
||||||
|
right = mid - 1;
|
||||||
|
} else if (cp > interval->last) {
|
||||||
|
left = mid + 1;
|
||||||
|
} else {
|
||||||
|
// Found: cp >= interval->first && cp <= interval->last
|
||||||
|
const uint32_t index =
|
||||||
|
interval->offset + (cp - interval->first) * bitCount[data.header.styles] + styleGroup(style);
|
||||||
|
return &data.glyphs[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "CrossPointFontFormat.h"
|
||||||
|
#include "Group5/Group5.h"
|
||||||
|
|
||||||
|
class CrossPointFont {
|
||||||
|
public:
|
||||||
|
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
|
||||||
|
|
||||||
|
CrossPointFontData data;
|
||||||
|
explicit CrossPointFont(void* rawData) {
|
||||||
|
data.header = *static_cast<CrossPointFontHeader*>(rawData);
|
||||||
|
data.intervals = (CrossPointFontUnicodeInterval*)(static_cast<uint8_t*>(rawData) + sizeof(CrossPointFontHeader));
|
||||||
|
data.glyphs = (CrossPointFontGlyph*)((uint8_t*)data.intervals +
|
||||||
|
sizeof(CrossPointFontUnicodeInterval) * data.header.intervalCount);
|
||||||
|
data.bitmap = (uint8_t*)data.glyphs + sizeof(CrossPointFontGlyph) * data.header.glyphCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
~CrossPointFont() = default;
|
||||||
|
void getTextDimensions(const char* string, Style style, int* w, int* h) const;
|
||||||
|
const CrossPointFontGlyph* getGlyph(uint32_t cp, Style style) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void getTextBounds(const char* string, Style style, int startX, int startY, int* minX, int* minY, int* maxX,
|
||||||
|
int* maxY) const;
|
||||||
|
uint8_t styleGroup(Style style) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: CrossPointFontSmall
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// 16-bit marker at the start of a CrossPoint font file
|
||||||
|
// (CrossPoint Font Format)
|
||||||
|
#define CPF_FONT_MARKER 0xCFF1
|
||||||
|
#define CPF_FONT_MARKER_SMALL 0xCFF2
|
||||||
|
|
||||||
|
// Font info per large character (glyph)
|
||||||
|
typedef struct {
|
||||||
|
uint32_t bitmapOffset; /// Offset to compressed bitmap data for this glyph
|
||||||
|
uint16_t width; /// bitmap width in pixels
|
||||||
|
uint16_t height; /// bitmap height in pixels
|
||||||
|
uint16_t xAdvance; /// total width in pixels (bitmap + padding)
|
||||||
|
int16_t xOffset; /// left padding to upper left corner
|
||||||
|
int16_t yOffset; /// top padding to upper left corner
|
||||||
|
} CrossPointFontGlyph;
|
||||||
|
|
||||||
|
// Font info per small character (glyph)
|
||||||
|
typedef struct {
|
||||||
|
uint32_t bitmapOffset; /// Offset to compressed bitmap data for this glyph
|
||||||
|
uint8_t width; /// bitmap width in pixels
|
||||||
|
uint8_t height; /// bitmap height in pixels
|
||||||
|
uint8_t xAdvance; /// total width in pixels (bitmap + padding)
|
||||||
|
int8_t xOffset; /// left padding to upper left corner
|
||||||
|
int16_t yOffset; /// top padding to upper left corner
|
||||||
|
} CrossPointFontSmallGlyph;
|
||||||
|
|
||||||
|
/// Glyph interval structure
|
||||||
|
typedef struct {
|
||||||
|
uint32_t first; /// The first unicode code point of the interval
|
||||||
|
uint32_t last; /// The last unicode code point of the interval
|
||||||
|
uint32_t offset; /// Index of the first code point into the glyph array
|
||||||
|
} CrossPointFontUnicodeInterval;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t u16Marker; /// CPF_FONT_MARKER / CPF_FONT_MARKER_SMALL
|
||||||
|
uint16_t height; /// Newline distance (y axis)
|
||||||
|
uint16_t ascender; /// Maximal height of a glyph above the base line
|
||||||
|
uint8_t styles; /// Regular = 0x01, Bold = 0x02, Italic = 0x04, BoldItalic = 0x08, can be OR'd together
|
||||||
|
uint16_t intervalCount; /// Number of unicode intervals.
|
||||||
|
uint32_t glyphCount; /// Number of total glyphs across all styles
|
||||||
|
} CrossPointFontHeader;
|
||||||
|
|
||||||
|
/// Data stored for FONT AS A WHOLE
|
||||||
|
typedef struct {
|
||||||
|
CrossPointFontHeader header;
|
||||||
|
CrossPointFontUnicodeInterval* intervals; /// Valid unicode intervals for this font
|
||||||
|
CrossPointFontGlyph* glyphs; /// Glyph array
|
||||||
|
uint8_t* bitmap; /// Glyph bitmaps, concatenated
|
||||||
|
} CrossPointFontData;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
CrossPointFontHeader header;
|
||||||
|
CrossPointFontUnicodeInterval* intervals; /// Valid unicode intervals for this font
|
||||||
|
CrossPointFontSmallGlyph* glyphs; /// Glyph array
|
||||||
|
uint8_t* bitmap; /// Glyph bitmaps, concatenated
|
||||||
|
} CrossPointFontDataSmall;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#include "g5enc.inl"
|
||||||
|
#include "g5dec.inl"
|
||||||
|
//
|
||||||
|
// Group5 1-bit image compression library
|
||||||
|
// Written by Larry Bank (bitbank@pobox.com)
|
||||||
|
// Decoder C++ wrapper functions
|
||||||
|
//
|
||||||
|
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
int G5DECODER::init(int iWidth, int iHeight, uint8_t *pData, int iDataSize)
|
||||||
|
{
|
||||||
|
return g5_decode_init(&_g5dec, iWidth, iHeight, pData, iDataSize);
|
||||||
|
} /* init() */
|
||||||
|
|
||||||
|
int G5DECODER::decodeLine(uint8_t *pOut)
|
||||||
|
{
|
||||||
|
return g5_decode_line(&_g5dec, pOut);
|
||||||
|
} /* decodeLine() */
|
||||||
|
|
||||||
|
//
|
||||||
|
// Encoder C++ wrapper functions
|
||||||
|
//
|
||||||
|
int G5ENCODER::init(int iWidth, int iHeight, uint8_t *pOut, int iOutSize)
|
||||||
|
{
|
||||||
|
return g5_encode_init(&_g5enc, iWidth, iHeight, pOut, iOutSize);
|
||||||
|
} /* init() */
|
||||||
|
|
||||||
|
int G5ENCODER::encodeLine(uint8_t *pPixels)
|
||||||
|
{
|
||||||
|
return g5_encode_encodeLine(&_g5enc, pPixels);
|
||||||
|
} /* encodeLine() */
|
||||||
|
|
||||||
|
int G5ENCODER::size()
|
||||||
|
{
|
||||||
|
return g5_encode_getOutSize(&_g5enc);
|
||||||
|
} /* size() */
|
||||||
|
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
#ifndef __GROUP5__
|
||||||
|
#define __GROUP5__
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#ifdef __AVR__
|
||||||
|
#include <avr/pgmspace.h>
|
||||||
|
#endif
|
||||||
|
//
|
||||||
|
// Group5 1-bit image compression library
|
||||||
|
// Written by Larry Bank (bitbank@pobox.com)
|
||||||
|
//
|
||||||
|
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
// The name "Group5" is derived from the CCITT Group4 standard
|
||||||
|
// This code is based on a lot of the good ideas from CCITT T.6
|
||||||
|
// for FAX image compression, but modified to work in a very
|
||||||
|
// constrained environment. The Huffman tables for horizontal
|
||||||
|
// mode have been replaced with a simple 2-bit flag followed by
|
||||||
|
// short or long counts of a fixed length. The short codes are
|
||||||
|
// always 3 bits (run lengths 0-7) and the long codes are the
|
||||||
|
// number of bits needed to encode the width of the image.
|
||||||
|
// For example, if a 320 pixel wide image is being compressed,
|
||||||
|
// the longest horizontal run needed is 320, which requires 9
|
||||||
|
// bits to encode. The 2 prefix bits have the following meaning:
|
||||||
|
// 00 = short, short (3+3 bits)
|
||||||
|
// 01 = short, long (3+N bits)
|
||||||
|
// 10 = long, short (N+3 bits)
|
||||||
|
// 11 = long, long (N+N bits)
|
||||||
|
// The rest of the code works identically to Group4 2D FAX
|
||||||
|
//
|
||||||
|
// Caution - this is the maximum number of color changes per line
|
||||||
|
// The default value is set low to work embedded systems with little RAM
|
||||||
|
// for font compression, this is plenty since each line of a character should have
|
||||||
|
// a maximum of 7 color changes
|
||||||
|
// You can define this in your compiler macros to override the default vlaue
|
||||||
|
//
|
||||||
|
#ifndef MAX_IMAGE_FLIPS
|
||||||
|
#ifdef __AVR__
|
||||||
|
#define MAX_IMAGE_FLIPS 32
|
||||||
|
#else
|
||||||
|
#define MAX_IMAGE_FLIPS 512
|
||||||
|
#endif // __AVR__
|
||||||
|
#endif
|
||||||
|
// Horizontal prefix bits
|
||||||
|
enum {
|
||||||
|
HORIZ_SHORT_SHORT=0,
|
||||||
|
HORIZ_SHORT_LONG,
|
||||||
|
HORIZ_LONG_SHORT,
|
||||||
|
HORIZ_LONG_LONG
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return code for encoder and decoder
|
||||||
|
enum {
|
||||||
|
G5_SUCCESS = 0,
|
||||||
|
G5_INVALID_PARAMETER,
|
||||||
|
G5_DECODE_ERROR,
|
||||||
|
G5_UNSUPPORTED_FEATURE,
|
||||||
|
G5_ENCODE_COMPLETE,
|
||||||
|
G5_DECODE_COMPLETE,
|
||||||
|
G5_NOT_INITIALIZED,
|
||||||
|
G5_DATA_OVERFLOW,
|
||||||
|
G5_MAX_FLIPS_EXCEEDED
|
||||||
|
};
|
||||||
|
//
|
||||||
|
// Decoder state
|
||||||
|
//
|
||||||
|
typedef struct g5_dec_image_tag
|
||||||
|
{
|
||||||
|
int iWidth, iHeight; // image size
|
||||||
|
int iError;
|
||||||
|
int y; // last y value drawn
|
||||||
|
int iVLCSize;
|
||||||
|
int iHLen; // length of 'long' horizontal codes for this image
|
||||||
|
int iPitch; // width in bytes of output buffer
|
||||||
|
uint32_t u32Accum; // fractional scaling accumulator
|
||||||
|
uint32_t ulBitOff, ulBits; // vlc decode variables
|
||||||
|
uint8_t *pSrc, *pBuf; // starting & current buffer pointer
|
||||||
|
int16_t *pCur, *pRef; // current state of current vs reference flips
|
||||||
|
int16_t CurFlips[MAX_IMAGE_FLIPS];
|
||||||
|
int16_t RefFlips[MAX_IMAGE_FLIPS];
|
||||||
|
} G5DECIMAGE;
|
||||||
|
|
||||||
|
// Due to unaligned memory causing an exception, we have to do these macros the slow way
|
||||||
|
#ifdef __AVR__
|
||||||
|
// assume PROGMEM as the source of data
|
||||||
|
inline uint32_t TIFFMOTOLONG(uint8_t *p)
|
||||||
|
{
|
||||||
|
uint32_t u32 = pgm_read_dword(p);
|
||||||
|
return __builtin_bswap32(u32);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
#define TIFFMOTOLONG(p) (((uint32_t)(*p)<<24UL) + ((uint32_t)(*(p+1))<<16UL) + ((uint32_t)(*(p+2))<<8UL) + (uint32_t)(*(p+3)))
|
||||||
|
#endif // __AVR__
|
||||||
|
|
||||||
|
#define TOP_BIT 0x80000000
|
||||||
|
#define MAX_VALUE 0xffffffff
|
||||||
|
// Must be a 32-bit target processor
|
||||||
|
#define REGISTER_WIDTH 32
|
||||||
|
#define BIGUINT uint32_t
|
||||||
|
|
||||||
|
//
|
||||||
|
// G5 Encoder
|
||||||
|
//
|
||||||
|
|
||||||
|
typedef struct g5_buffered_bits
|
||||||
|
{
|
||||||
|
unsigned char *pBuf; // buffer pointer
|
||||||
|
uint32_t ulBits; // buffered bits
|
||||||
|
uint32_t ulBitOff; // current bit offset
|
||||||
|
uint32_t ulDataSize; // available data
|
||||||
|
} G5_BUFFERED_BITS;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Encoder state
|
||||||
|
//
|
||||||
|
typedef struct g5_enc_image_tag
|
||||||
|
{
|
||||||
|
int iWidth, iHeight; // image size
|
||||||
|
int iError;
|
||||||
|
int y; // last y encoded
|
||||||
|
int iOutSize;
|
||||||
|
int iDataSize; // generated output size
|
||||||
|
uint8_t *pOutBuf;
|
||||||
|
int16_t *pCur, *pRef; // pointers to swap current and reference lines
|
||||||
|
G5_BUFFERED_BITS bb;
|
||||||
|
int16_t CurFlips[MAX_IMAGE_FLIPS];
|
||||||
|
int16_t RefFlips[MAX_IMAGE_FLIPS];
|
||||||
|
} G5ENCIMAGE;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
//
|
||||||
|
// The G5 classes wrap portable C code which does the actual work
|
||||||
|
//
|
||||||
|
class G5ENCODER
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int init(int iWidth, int iHeight, uint8_t *pOut, int iOutSize);
|
||||||
|
int encodeLine(uint8_t *pPixels);
|
||||||
|
int size();
|
||||||
|
|
||||||
|
private:
|
||||||
|
G5ENCIMAGE _g5enc;
|
||||||
|
};
|
||||||
|
class G5DECODER
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int init(int iWidth, int iHeight, uint8_t *pData, int iDataSize);
|
||||||
|
int decodeLine(uint8_t *pOut);
|
||||||
|
|
||||||
|
private:
|
||||||
|
G5DECIMAGE _g5dec;
|
||||||
|
};
|
||||||
|
#endif // __cplusplus
|
||||||
|
|
||||||
|
#endif // __GROUP5__
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
//
|
||||||
|
// Group5
|
||||||
|
// A 1-bpp image decoder
|
||||||
|
//
|
||||||
|
// Written by Larry Bank (bitbank@pobox.com)
|
||||||
|
//
|
||||||
|
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "Group5.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
The code tree that follows has: bit_length, decode routine
|
||||||
|
These codes are for Group 4 (MMR) decoding
|
||||||
|
|
||||||
|
01 = vertneg1, 11h = vert1, 20h = horiz, 30h = pass, 12h = vert2
|
||||||
|
02 = vertneg2, 13h = vert3, 03 = vertneg3, 90h = trash
|
||||||
|
*/
|
||||||
|
|
||||||
|
static const uint8_t code_table[128] =
|
||||||
|
{0x90, 0, 0x40, 0, /* trash, uncompr mode - codes 0 and 1 */
|
||||||
|
3, 7, /* V(-3) pos = 2 */
|
||||||
|
0x13, 7, /* V(3) pos = 3 */
|
||||||
|
2, 6, 2, 6, /* V(-2) pos = 4,5 */
|
||||||
|
0x12, 6, 0x12, 6, /* V(2) pos = 6,7 */
|
||||||
|
0x30, 4, 0x30, 4, 0x30, 4, 0x30, 4, /* pass pos = 8->F */
|
||||||
|
0x30, 4, 0x30, 4, 0x30, 4, 0x30, 4,
|
||||||
|
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3, /* horiz pos = 10->1F */
|
||||||
|
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3,
|
||||||
|
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3,
|
||||||
|
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3,
|
||||||
|
/* V(-1) pos = 20->2F */
|
||||||
|
1, 3, 1, 3, 1, 3, 1, 3,
|
||||||
|
1, 3, 1, 3, 1, 3, 1, 3,
|
||||||
|
1, 3, 1, 3, 1, 3, 1, 3,
|
||||||
|
1, 3, 1, 3, 1, 3, 1, 3,
|
||||||
|
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3, /* V(1) pos = 30->3F */
|
||||||
|
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3,
|
||||||
|
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3,
|
||||||
|
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3};
|
||||||
|
|
||||||
|
static int g5_decode_init(G5DECIMAGE *pImage, int iWidth, int iHeight, uint8_t *pData, int iDataSize)
|
||||||
|
{
|
||||||
|
if (pImage == NULL || iWidth < 1 || iHeight < 1 || pData == NULL || iDataSize < 1)
|
||||||
|
return G5_INVALID_PARAMETER;
|
||||||
|
|
||||||
|
pImage->iVLCSize = iDataSize;
|
||||||
|
pImage->pSrc = pData;
|
||||||
|
pImage->ulBitOff = 0;
|
||||||
|
pImage->y = 0;
|
||||||
|
pImage->ulBits = TIFFMOTOLONG(pData); // preload the first 32 bits of data
|
||||||
|
pImage->iWidth = iWidth;
|
||||||
|
pImage->iHeight = iHeight;
|
||||||
|
return G5_SUCCESS;
|
||||||
|
|
||||||
|
} /* g5_decode_init() */
|
||||||
|
|
||||||
|
static void G5DrawLine(G5DECIMAGE *pPage, int16_t *pCurFlips, uint8_t *pOut)
|
||||||
|
{
|
||||||
|
int x, len, run;
|
||||||
|
uint8_t lBit, rBit, *p;
|
||||||
|
int iStart = 0, xright = pPage->iWidth;
|
||||||
|
uint8_t *pDest;
|
||||||
|
|
||||||
|
iStart = 0;
|
||||||
|
|
||||||
|
pDest = pOut;
|
||||||
|
len = (xright+7)>>3; // number of bytes to generate
|
||||||
|
for (x=0; x<len; x++) {
|
||||||
|
pOut[x] = 0xff; // start with white and only draw the black runs
|
||||||
|
}
|
||||||
|
x = 0;
|
||||||
|
while (x < xright) { // while the scaled x is within the window bounds
|
||||||
|
x = *pCurFlips++; // black starting point
|
||||||
|
run = *pCurFlips++ - x; // get the black run
|
||||||
|
x -= iStart;
|
||||||
|
if (x >= xright || run == 0)
|
||||||
|
break;
|
||||||
|
if ((x + run) > 0) { /* If the run is visible, draw it */
|
||||||
|
if (x < 0) {
|
||||||
|
run += x; /* draw only visible part of run */
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
if ((x + run) > xright) { /* Don't let it go off right edge */
|
||||||
|
run = xright - x;
|
||||||
|
}
|
||||||
|
/* Draw this run */
|
||||||
|
lBit = 0xff << (8 - (x & 7));
|
||||||
|
rBit = 0xff >> ((x + run) & 7);
|
||||||
|
len = ((x+run)>>3) - (x >> 3);
|
||||||
|
p = &pDest[x >> 3];
|
||||||
|
if (len == 0) {
|
||||||
|
lBit |= rBit;
|
||||||
|
*p &= lBit;
|
||||||
|
} else {
|
||||||
|
*p++ &= lBit;
|
||||||
|
while (len > 1) {
|
||||||
|
*p++ = 0;
|
||||||
|
len--;
|
||||||
|
}
|
||||||
|
*p = rBit;
|
||||||
|
}
|
||||||
|
} // visible run
|
||||||
|
} /* while drawing line */
|
||||||
|
} /* G5DrawLine() */
|
||||||
|
//
|
||||||
|
// Initialize internal structures to decode the image
|
||||||
|
//
|
||||||
|
static void Decode_Begin(G5DECIMAGE *pPage)
|
||||||
|
{
|
||||||
|
int i, xsize;
|
||||||
|
int16_t *CurFlips, *RefFlips;
|
||||||
|
|
||||||
|
xsize = pPage->iWidth;
|
||||||
|
|
||||||
|
RefFlips = pPage->RefFlips;
|
||||||
|
CurFlips = pPage->CurFlips;
|
||||||
|
|
||||||
|
/* Seed the current and reference line with XSIZE for V(0) codes */
|
||||||
|
for (i=0; i<MAX_IMAGE_FLIPS-2; i++) {
|
||||||
|
RefFlips[i] = xsize;
|
||||||
|
CurFlips[i] = xsize;
|
||||||
|
}
|
||||||
|
/* Prefill both current and reference lines with 7fff to prevent it from
|
||||||
|
walking off the end if the data gets bunged and the current X is > XSIZE
|
||||||
|
3-16-94 */
|
||||||
|
CurFlips[i] = RefFlips[i] = 0x7fff;
|
||||||
|
CurFlips[i+1] = RefFlips[i+1] = 0x7fff;
|
||||||
|
|
||||||
|
pPage->pCur = CurFlips;
|
||||||
|
pPage->pRef = RefFlips;
|
||||||
|
pPage->pBuf = pPage->pSrc;
|
||||||
|
pPage->ulBits = TIFFMOTOLONG(pPage->pSrc); // load 32 bits to start
|
||||||
|
pPage->ulBitOff = 0;
|
||||||
|
// Calculate the number of bits needed for a long horizontal code
|
||||||
|
#ifdef __AVR__
|
||||||
|
pPage->iHLen = 16 - __builtin_clz(pPage->iWidth);
|
||||||
|
#else
|
||||||
|
pPage->iHLen = 32 - __builtin_clz(pPage->iWidth);
|
||||||
|
#endif
|
||||||
|
} /* Decode_Begin() */
|
||||||
|
//
|
||||||
|
// Decode a single line of G5 data (private function)
|
||||||
|
//
|
||||||
|
static int DecodeLine(G5DECIMAGE *pPage)
|
||||||
|
{
|
||||||
|
signed int a0, a0_p, b1;
|
||||||
|
int16_t *pCur, *pRef, *RefFlips, *CurFlips;
|
||||||
|
int xsize, tot_run=0, tot_run1 = 0;
|
||||||
|
int32_t sCode;
|
||||||
|
uint32_t lBits;
|
||||||
|
uint32_t ulBits, ulBitOff;
|
||||||
|
uint8_t *pBuf/*, *pBufEnd*/;
|
||||||
|
uint32_t u32HMask, u32HLen; // horizontal code mask and length
|
||||||
|
|
||||||
|
pCur = CurFlips = pPage->pCur;
|
||||||
|
pRef = RefFlips = pPage->pRef;
|
||||||
|
ulBits = pPage->ulBits;
|
||||||
|
ulBitOff = pPage->ulBitOff;
|
||||||
|
pBuf = pPage->pBuf;
|
||||||
|
// pBufEnd = &pPage->pSrc[pPage->iVLCSize];
|
||||||
|
u32HLen = pPage->iHLen;
|
||||||
|
u32HMask = (1 << u32HLen) - 1;
|
||||||
|
a0 = -1;
|
||||||
|
xsize = pPage->iWidth;
|
||||||
|
|
||||||
|
while (a0 < xsize) { /* Decode this line */
|
||||||
|
if (ulBitOff > (REGISTER_WIDTH-8)) { // need at least 7 unused bits
|
||||||
|
pBuf += (ulBitOff >> 3);
|
||||||
|
ulBitOff &= 7;
|
||||||
|
ulBits = TIFFMOTOLONG(pBuf);
|
||||||
|
}
|
||||||
|
if ((int32_t)(ulBits << ulBitOff) < 0) { /* V(0) code is the most frequent case (1 bit) */
|
||||||
|
a0 = *pRef++;
|
||||||
|
ulBitOff++; // length = 1 bit
|
||||||
|
*pCur++ = a0;
|
||||||
|
} else { /* Slower method for the less frequence codes */
|
||||||
|
lBits = (ulBits >> ((REGISTER_WIDTH - 8) - ulBitOff)) & 0xfe; /* Only the first 7 bits are useful */
|
||||||
|
sCode = code_table[lBits]; /* Get the code type as an 8-bit value */
|
||||||
|
ulBitOff += code_table[lBits+1]; /* Get the code length */
|
||||||
|
switch (sCode) {
|
||||||
|
case 1: /* V(-1) */
|
||||||
|
case 2: /* V(-2) */
|
||||||
|
case 3: /* V(-3) */
|
||||||
|
a0 = *pRef - sCode; /* A0 = B1 - x */
|
||||||
|
*pCur++ = a0;
|
||||||
|
if (pRef == RefFlips) {
|
||||||
|
pRef += 2;
|
||||||
|
}
|
||||||
|
pRef--;
|
||||||
|
while (a0 >= *pRef) {
|
||||||
|
pRef += 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0x11: /* V(1) */
|
||||||
|
case 0x12: /* V(2) */
|
||||||
|
case 0x13: /* V(3) */
|
||||||
|
a0 = *pRef++; /* A0 = B1 */
|
||||||
|
b1 = a0;
|
||||||
|
a0 += sCode & 7; /* A0 = B1 + x */
|
||||||
|
if (b1 != xsize && a0 < xsize) {
|
||||||
|
while (a0 >= *pRef) {
|
||||||
|
pRef += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (a0 > xsize) {
|
||||||
|
a0 = xsize;
|
||||||
|
}
|
||||||
|
*pCur++ = a0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0x20: /* Horizontal codes */
|
||||||
|
if (ulBitOff > (REGISTER_WIDTH-16)) { // need at least 16 unused bits
|
||||||
|
pBuf += (ulBitOff >> 3);
|
||||||
|
ulBitOff &= 7;
|
||||||
|
ulBits = TIFFMOTOLONG(pBuf);
|
||||||
|
}
|
||||||
|
a0_p = a0;
|
||||||
|
if (a0 < 0) {
|
||||||
|
a0_p = 0;
|
||||||
|
}
|
||||||
|
lBits = (ulBits >> ((REGISTER_WIDTH - 2) - ulBitOff)) & 0x3; // get 2-bit prefix for code type
|
||||||
|
// There are 4 possible horizontal cases: short/short, short/long, long/short, long/long
|
||||||
|
// These are encoded in a 2-bit prefix code, followed by 3 bits for short or N bits for long code
|
||||||
|
// N is the log base 2 of the image width (e.g. 320 pixels requires 9 bits)
|
||||||
|
ulBitOff += 2;
|
||||||
|
switch (lBits) {
|
||||||
|
case HORIZ_SHORT_SHORT:
|
||||||
|
tot_run = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
|
||||||
|
ulBitOff += 3;
|
||||||
|
tot_run1 = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
|
||||||
|
ulBitOff += 3;
|
||||||
|
break;
|
||||||
|
case HORIZ_SHORT_LONG:
|
||||||
|
tot_run = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
|
||||||
|
ulBitOff += 3;
|
||||||
|
tot_run1 = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
|
||||||
|
ulBitOff += u32HLen;
|
||||||
|
break;
|
||||||
|
case HORIZ_LONG_SHORT:
|
||||||
|
tot_run = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
|
||||||
|
ulBitOff += u32HLen;
|
||||||
|
tot_run1 = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
|
||||||
|
ulBitOff += 3;
|
||||||
|
break;
|
||||||
|
case HORIZ_LONG_LONG:
|
||||||
|
tot_run = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
|
||||||
|
ulBitOff += u32HLen;
|
||||||
|
if (ulBitOff > (REGISTER_WIDTH-16)) { // need at least 16 unused bits
|
||||||
|
pBuf += (ulBitOff >> 3);
|
||||||
|
ulBitOff &= 7;
|
||||||
|
ulBits = TIFFMOTOLONG(pBuf);
|
||||||
|
}
|
||||||
|
tot_run1 = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
|
||||||
|
ulBitOff += u32HLen;
|
||||||
|
break;
|
||||||
|
} // switch on lBits
|
||||||
|
a0 = a0_p + tot_run;
|
||||||
|
*pCur++ = a0;
|
||||||
|
a0 += tot_run1;
|
||||||
|
if (a0 < xsize) {
|
||||||
|
while (a0 >= *pRef) {
|
||||||
|
pRef += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*pCur++ = a0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0x30: /* Pass code */
|
||||||
|
pRef++; /* A0 = B2, iRef+=2 */
|
||||||
|
a0 = *pRef++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: /* ERROR */
|
||||||
|
pPage->iError = G5_DECODE_ERROR;
|
||||||
|
goto pilreadg5z;
|
||||||
|
} /* switch */
|
||||||
|
} /* Slow climb */
|
||||||
|
}
|
||||||
|
/*--- Convert flips data into run lengths ---*/
|
||||||
|
*pCur++ = xsize; /* Terminate the line properly */
|
||||||
|
*pCur++ = xsize;
|
||||||
|
pilreadg5z:
|
||||||
|
// Save the current VLC decoder state
|
||||||
|
pPage->ulBits = ulBits;
|
||||||
|
pPage->ulBitOff = ulBitOff;
|
||||||
|
pPage->pBuf = pBuf;
|
||||||
|
return pPage->iError;
|
||||||
|
} /* DecodeLine() */
|
||||||
|
//
|
||||||
|
// Decompress the VLC data
|
||||||
|
//
|
||||||
|
static int g5_decode_line(G5DECIMAGE *pPage, uint8_t *pOut)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
uint8_t *pBufEnd;
|
||||||
|
int16_t *t1;
|
||||||
|
|
||||||
|
if (pPage == NULL || pOut == NULL)
|
||||||
|
return G5_INVALID_PARAMETER;
|
||||||
|
if (pPage->y >= pPage->iHeight)
|
||||||
|
return G5_DECODE_COMPLETE;
|
||||||
|
|
||||||
|
pPage->iError = G5_SUCCESS;
|
||||||
|
|
||||||
|
if (pPage->y == 0) { // first time through
|
||||||
|
Decode_Begin(pPage);
|
||||||
|
}
|
||||||
|
pBufEnd = &pPage->pSrc[pPage->iVLCSize];
|
||||||
|
|
||||||
|
if (pPage->pBuf >= pBufEnd) { // read past the end, error
|
||||||
|
pPage->iError = G5_DECODE_ERROR;
|
||||||
|
return G5_DECODE_ERROR;
|
||||||
|
}
|
||||||
|
rc = DecodeLine(pPage);
|
||||||
|
if (rc == G5_SUCCESS) {
|
||||||
|
// Draw the current line
|
||||||
|
G5DrawLine(pPage, pPage->pCur, pOut);
|
||||||
|
/*--- Swap current and reference lines ---*/
|
||||||
|
t1 = pPage->pRef;
|
||||||
|
pPage->pRef = pPage->pCur;
|
||||||
|
pPage->pCur = t1;
|
||||||
|
pPage->y++;
|
||||||
|
if (pPage->y >= pPage->iHeight) {
|
||||||
|
pPage->iError = G5_DECODE_COMPLETE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pPage->iError = rc;
|
||||||
|
}
|
||||||
|
return pPage->iError;
|
||||||
|
} /* Decode() */
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
//
|
||||||
|
// G5 Encoder
|
||||||
|
// A 1-bpp image encoding library
|
||||||
|
//
|
||||||
|
// Written by Larry Bank (bitbank@pobox.com)
|
||||||
|
//
|
||||||
|
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "Group5.h"
|
||||||
|
|
||||||
|
/* Number of consecutive 1 bits in a byte from MSB to LSB */
|
||||||
|
static uint8_t bitcount[256] =
|
||||||
|
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 0-15 */
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 16-31 */
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 32-47 */
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 48-63 */
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 64-79 */
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 80-95 */
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 96-111 */
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 112-127 */
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 128-143 */
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 144-159 */
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 160-175 */
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 176-191 */
|
||||||
|
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, /* 192-207 */
|
||||||
|
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, /* 208-223 */
|
||||||
|
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, /* 224-239 */
|
||||||
|
4,4,4,4,4,4,4,4,5,5,5,5,6,6,7,8}; /* 240-255 */
|
||||||
|
|
||||||
|
/* Table of vertical codes for G5 encoding */
|
||||||
|
/* code followed by length, starting with v(-3) */
|
||||||
|
static const uint8_t vtable[14] =
|
||||||
|
{3,7, /* V(-3) = 0000011 */
|
||||||
|
3,6, /* V(-2) = 000011 */
|
||||||
|
3,3, /* V(-1) = 011 */
|
||||||
|
1,1, /* V(0) = 1 */
|
||||||
|
2,3, /* V(1) = 010 */
|
||||||
|
2,6, /* V(2) = 000010 */
|
||||||
|
2,7}; /* V(3) = 0000010 */
|
||||||
|
|
||||||
|
|
||||||
|
static void G5ENCInsertCode(G5_BUFFERED_BITS *bb, BIGUINT ulCode, int iLen)
|
||||||
|
{
|
||||||
|
if ((bb->ulBitOff + iLen) > REGISTER_WIDTH) { // need to write data
|
||||||
|
bb->ulBits |= (ulCode >> (bb->ulBitOff + iLen - REGISTER_WIDTH)); // partial bits on first word
|
||||||
|
*(BIGUINT *)bb->pBuf = __builtin_bswap32(bb->ulBits);
|
||||||
|
bb->pBuf += sizeof(BIGUINT);
|
||||||
|
bb->ulBits = ulCode << ((REGISTER_WIDTH*2) - (bb->ulBitOff + iLen));
|
||||||
|
bb->ulBitOff += iLen - REGISTER_WIDTH;
|
||||||
|
} else {
|
||||||
|
bb->ulBits |= (ulCode << (REGISTER_WIDTH - bb->ulBitOff - iLen));
|
||||||
|
bb->ulBitOff += iLen;
|
||||||
|
}
|
||||||
|
} /* G5ENCInsertCode() */
|
||||||
|
//
|
||||||
|
// Flush any buffered bits to the output
|
||||||
|
//
|
||||||
|
static void G5ENCFlushBits(G5_BUFFERED_BITS *bb)
|
||||||
|
{
|
||||||
|
while (bb->ulBitOff >= 8)
|
||||||
|
{
|
||||||
|
*bb->pBuf++ = (unsigned char) (bb->ulBits >> (REGISTER_WIDTH - 8));
|
||||||
|
bb->ulBits <<= 8;
|
||||||
|
bb->ulBitOff -= 8;
|
||||||
|
}
|
||||||
|
if (bb->ulBitOff) { // partial byte?
|
||||||
|
*bb->pBuf++ = (unsigned char) (bb->ulBits >> (REGISTER_WIDTH - 8));
|
||||||
|
}
|
||||||
|
bb->ulBitOff = 0;
|
||||||
|
bb->ulBits = 0;
|
||||||
|
} /* G5ENCFlushBits() */
|
||||||
|
//
|
||||||
|
// Initialize the compressor
|
||||||
|
// This must be called before adding data to the output
|
||||||
|
//
|
||||||
|
static int g5_encode_init(G5ENCIMAGE *pImage, int iWidth, int iHeight, uint8_t *pOut, int iOutSize)
|
||||||
|
{
|
||||||
|
int iError = G5_SUCCESS;
|
||||||
|
|
||||||
|
if (pImage == NULL || iHeight <= 0)
|
||||||
|
return G5_INVALID_PARAMETER;
|
||||||
|
pImage->iWidth = iWidth; // image size
|
||||||
|
pImage->iHeight = iHeight;
|
||||||
|
pImage->pCur = pImage->CurFlips;
|
||||||
|
pImage->pRef = pImage->RefFlips;
|
||||||
|
pImage->pOutBuf = pOut; // optional output buffer
|
||||||
|
pImage->iOutSize = iOutSize; // output buffer pre-allocated size
|
||||||
|
pImage->iDataSize = 0; // no data yet
|
||||||
|
pImage->y = 0;
|
||||||
|
for (int i=0; i<MAX_IMAGE_FLIPS; i++) {
|
||||||
|
pImage->RefFlips[i] = iWidth;
|
||||||
|
pImage->CurFlips[i] = iWidth;
|
||||||
|
}
|
||||||
|
pImage->bb.pBuf = pImage->pOutBuf;
|
||||||
|
pImage->bb.ulBits = 0;
|
||||||
|
pImage->bb.ulBitOff = 0;
|
||||||
|
pImage->iError = iError;
|
||||||
|
return iError;
|
||||||
|
} /* g5_encode_init() */
|
||||||
|
//
|
||||||
|
// Internal function to convert uncompressed 1-bit per pixel data
|
||||||
|
// into the run-end data needed to feed the G5 encoder
|
||||||
|
//
|
||||||
|
static int G5ENCEncodeLine(unsigned char *buf, int xsize, int16_t *pDest)
|
||||||
|
{
|
||||||
|
int iCount, xborder;
|
||||||
|
uint8_t i, c;
|
||||||
|
int8_t cBits;
|
||||||
|
int iLen;
|
||||||
|
int16_t x;
|
||||||
|
int16_t *pLimit = pDest + (MAX_IMAGE_FLIPS-4);
|
||||||
|
|
||||||
|
xborder = xsize;
|
||||||
|
iCount = (xsize + 7) >> 3; /* Number of bytes per line */
|
||||||
|
cBits = 8;
|
||||||
|
iLen = 0; /* Current run length */
|
||||||
|
x = 0;
|
||||||
|
|
||||||
|
c = *buf++; /* Get the first byte to start */
|
||||||
|
iCount--;
|
||||||
|
while (iCount >=0) {
|
||||||
|
if (pDest >= pLimit) return G5_MAX_FLIPS_EXCEEDED;
|
||||||
|
i = bitcount[c]; /* Get the number of consecutive bits */
|
||||||
|
iLen += i; /* Add this length to total run length */
|
||||||
|
c <<= i;
|
||||||
|
cBits -= i; /* Minus the number in a byte */
|
||||||
|
if (cBits <= 0)
|
||||||
|
{
|
||||||
|
iLen += cBits; /* Adjust length */
|
||||||
|
cBits = 8;
|
||||||
|
c = *buf++; /* Get another data byte */
|
||||||
|
iCount--;
|
||||||
|
continue; /* Keep doing white until color change */
|
||||||
|
}
|
||||||
|
c = ~c; /* flip color to count black pixels */
|
||||||
|
/* Store the white run length */
|
||||||
|
xborder -= iLen;
|
||||||
|
if (xborder < 0)
|
||||||
|
{
|
||||||
|
iLen += xborder; /* Make sure run length is not past end */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
x += iLen;
|
||||||
|
*pDest++ = x;
|
||||||
|
iLen = 0;
|
||||||
|
doblack:
|
||||||
|
i = bitcount[c]; /* Get consecutive bits */
|
||||||
|
iLen += i; /* Add to total run length */
|
||||||
|
c <<= i;
|
||||||
|
cBits -= i;
|
||||||
|
if (cBits <= 0)
|
||||||
|
{
|
||||||
|
iLen += cBits; /* Adjust length */
|
||||||
|
cBits = 8;
|
||||||
|
c = *buf++; /* Get another data byte */
|
||||||
|
c = ~c; /* Flip color to find black */
|
||||||
|
iCount--;
|
||||||
|
if (iCount < 0)
|
||||||
|
break;
|
||||||
|
goto doblack;
|
||||||
|
}
|
||||||
|
/* Store the black run length */
|
||||||
|
c = ~c; /* Flip color again to find white pixels */
|
||||||
|
xborder -= iLen;
|
||||||
|
if (xborder < 0)
|
||||||
|
{
|
||||||
|
iLen += xborder; /* Make sure run length is not past end */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
x += iLen;
|
||||||
|
*pDest++ = x;
|
||||||
|
iLen = 0;
|
||||||
|
} /* while */
|
||||||
|
|
||||||
|
if (pDest >= pLimit) return G5_MAX_FLIPS_EXCEEDED;
|
||||||
|
*pDest++ = xsize;
|
||||||
|
*pDest++ = xsize; // Store a few more XSIZE to end the line
|
||||||
|
*pDest++ = xsize; // so that the compressor doesn't go past
|
||||||
|
*pDest++ = xsize; // the end of the line
|
||||||
|
return G5_SUCCESS;
|
||||||
|
} /* G5ENCEncodeLine() */
|
||||||
|
//
|
||||||
|
// Compress a line of pixels and add it to the output
|
||||||
|
// the input format is expected to be MSB (most significant bit) first
|
||||||
|
// for example, pixel 0 is in byte 0 at bit 7 (0x80)
|
||||||
|
// Returns G5ENC_SUCCESS for each line if all is well and G5ENC_IMAGE_COMPLETE
|
||||||
|
// for the last line
|
||||||
|
//
|
||||||
|
static int g5_encode_encodeLine(G5ENCIMAGE *pImage, uint8_t *pPixels)
|
||||||
|
{
|
||||||
|
int16_t a0, a0_c, b2, a1;
|
||||||
|
int dx, run1, run2;
|
||||||
|
int xsize, iErr, iHighWater;
|
||||||
|
int iCur, iRef, iLen;
|
||||||
|
int iHLen; // number of bits for long horizontal codes
|
||||||
|
int16_t *CurFlips, *RefFlips;
|
||||||
|
G5_BUFFERED_BITS bb;
|
||||||
|
|
||||||
|
if (pImage == NULL || pPixels == NULL)
|
||||||
|
return G5_INVALID_PARAMETER;
|
||||||
|
iHighWater = pImage->iOutSize - 32;
|
||||||
|
iHLen = 32 - __builtin_clz(pImage->iWidth);
|
||||||
|
memcpy(&bb, &pImage->bb, sizeof(G5_BUFFERED_BITS)); // keep local copy
|
||||||
|
CurFlips = pImage->pCur;
|
||||||
|
RefFlips = pImage->pRef;
|
||||||
|
xsize = pImage->iWidth; /* For performance reasons */
|
||||||
|
|
||||||
|
// Convert the incoming line of pixels into run-end data
|
||||||
|
iErr = G5ENCEncodeLine(pPixels, pImage->iWidth, CurFlips);
|
||||||
|
if (iErr != G5_SUCCESS) return iErr; // exceeded the maximum number of color changes
|
||||||
|
/* Encode this line as G5 */
|
||||||
|
a0 = a0_c = 0;
|
||||||
|
iCur = iRef = 0;
|
||||||
|
while (a0 < xsize) {
|
||||||
|
b2 = RefFlips[iRef+1];
|
||||||
|
a1 = CurFlips[iCur];
|
||||||
|
if (b2 < a1) { /* Is b2 to the left of a1? */
|
||||||
|
/* yes, do pass mode */
|
||||||
|
a0 = b2;
|
||||||
|
iRef += 2;
|
||||||
|
G5ENCInsertCode(&bb, 1, 4); /* Pass code = 0001 */
|
||||||
|
} else { /* Try vertical and horizontal mode */
|
||||||
|
dx = RefFlips[iRef] - a1; /* b1 - a1 */
|
||||||
|
if (dx > 3 || dx < -3) { /* Horizontal mode */
|
||||||
|
G5ENCInsertCode(&bb, 1, 3); /* Horizontal code = 001 */
|
||||||
|
run1 = CurFlips[iCur] - a0;
|
||||||
|
run2 = CurFlips[iCur+1] - CurFlips[iCur];
|
||||||
|
if (run1 < 8) {
|
||||||
|
if (run2 < 8) { // short, short
|
||||||
|
G5ENCInsertCode(&bb, HORIZ_SHORT_SHORT, 2); /* short, short = 00 */
|
||||||
|
G5ENCInsertCode(&bb, run1, 3);
|
||||||
|
G5ENCInsertCode(&bb, run2, 3);
|
||||||
|
} else { // short, long
|
||||||
|
G5ENCInsertCode(&bb, HORIZ_SHORT_LONG, 2); /* short, long = 01 */
|
||||||
|
G5ENCInsertCode(&bb, run1, 3);
|
||||||
|
G5ENCInsertCode(&bb, run2, iHLen);
|
||||||
|
}
|
||||||
|
} else { // first run is long
|
||||||
|
if (run2 < 8) { // long, short
|
||||||
|
G5ENCInsertCode(&bb, HORIZ_LONG_SHORT, 2); /* long, short = 10 */
|
||||||
|
G5ENCInsertCode(&bb, run1, iHLen);
|
||||||
|
G5ENCInsertCode(&bb, run2, 3);
|
||||||
|
} else { // long, long
|
||||||
|
G5ENCInsertCode(&bb, HORIZ_LONG_LONG, 2); /* long, long = 11 */
|
||||||
|
G5ENCInsertCode(&bb, run1, iHLen);
|
||||||
|
G5ENCInsertCode(&bb, run2, iHLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a0 = CurFlips[iCur+1]; /* a0 = a2 */
|
||||||
|
if (a0 != xsize) {
|
||||||
|
iCur += 2; /* Skip two color flips */
|
||||||
|
while (RefFlips[iRef] != xsize && RefFlips[iRef] <= a0) {
|
||||||
|
iRef += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { /* Vertical mode */
|
||||||
|
dx = (dx + 3) * 2; /* Convert to index table */
|
||||||
|
G5ENCInsertCode(&bb, vtable[dx], vtable[dx+1]);
|
||||||
|
a0 = a1;
|
||||||
|
a0_c = 1-a0_c;
|
||||||
|
if (a0 != xsize) {
|
||||||
|
if (iRef != 0) {
|
||||||
|
iRef -= 2;
|
||||||
|
}
|
||||||
|
iRef++; /* Skip a color change in cur and ref */
|
||||||
|
iCur++;
|
||||||
|
while (RefFlips[iRef] <= a0 && RefFlips[iRef] != xsize) {
|
||||||
|
iRef += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* vertical mode */
|
||||||
|
} /* horiz/vert mode */
|
||||||
|
} /* while x < xsize */
|
||||||
|
iLen = (int)(bb.pBuf-pImage->pOutBuf);
|
||||||
|
if (iLen >= iHighWater) { // not enough space
|
||||||
|
pImage->iError = iErr = G5_DATA_OVERFLOW; // we don't have a better error
|
||||||
|
return iErr;
|
||||||
|
}
|
||||||
|
if (pImage->y == pImage->iHeight-1) { // last line of image
|
||||||
|
G5ENCFlushBits(&bb); // output the final buffered bits
|
||||||
|
// wrap up final output
|
||||||
|
pImage->iDataSize = 1 + (int)(bb.pBuf-pImage->pOutBuf);
|
||||||
|
iErr = G5_ENCODE_COMPLETE;
|
||||||
|
}
|
||||||
|
pImage->pCur = RefFlips; // swap current and reference lines
|
||||||
|
pImage->pRef = CurFlips;
|
||||||
|
pImage->y++;
|
||||||
|
memcpy(&pImage->bb, &bb, sizeof(bb));
|
||||||
|
return iErr;
|
||||||
|
} /* g5_encode_encodeLine() */
|
||||||
|
//
|
||||||
|
// Returns the number of bytes of G5 created by the encoder
|
||||||
|
//
|
||||||
|
static int g5_encode_getOutSize(G5ENCIMAGE *pImage)
|
||||||
|
{
|
||||||
|
int iSize = 0;
|
||||||
|
if (pImage != NULL)
|
||||||
|
iSize = pImage->iDataSize;
|
||||||
|
return iSize;
|
||||||
|
} /* g5_encode_getOutSize() */
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <builtinFonts/bookerly_12.h>
|
||||||
|
#include <builtinFonts/bookerly_14.h>
|
||||||
|
#include <builtinFonts/bookerly_16.h>
|
||||||
|
#include <builtinFonts/bookerly_18.h>
|
||||||
|
#include <builtinFonts/notosans_8.h>
|
||||||
|
#include <builtinFonts/notosans_12.h>
|
||||||
|
#include <builtinFonts/notosans_14.h>
|
||||||
|
#include <builtinFonts/notosans_16.h>
|
||||||
|
#include <builtinFonts/notosans_18.h>
|
||||||
|
#include <builtinFonts/opendyslexic_8.h>
|
||||||
|
#include <builtinFonts/opendyslexic_10.h>
|
||||||
|
#include <builtinFonts/opendyslexic_12.h>
|
||||||
|
#include <builtinFonts/opendyslexic_14.h>
|
||||||
|
#include <builtinFonts/ubuntu_10.h>
|
||||||
|
#include <builtinFonts/ubuntu_12.h>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo "// The contents of this file are generated by ./lib/CrossPointFont/builtinFonts/build-font-ids.sh"
|
||||||
|
echo "#pragma once"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "#define BOOKERLY_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define BOOKERLY_14_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_14.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define BOOKERLY_16_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_16.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define BOOKERLY_18_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_18.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define NOTOSANS_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define NOTOSANS_14_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_14.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define NOTOSANS_16_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_16.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define NOTOSANS_18_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_18.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define OPENDYSLEXIC_8_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_8.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define OPENDYSLEXIC_10_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_10.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define OPENDYSLEXIC_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define OPENDYSLEXIC_14_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_14.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define UI_10_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./ubuntu_10.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define UI_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./ubuntu_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
|
echo "#define SMALL_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_8.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
BOOKERLY_FONT_SIZES=(12 14 16 18)
|
||||||
|
NOTOSANS_FONT_SIZES=(12 14 16 18)
|
||||||
|
OPENDYSLEXIC_FONT_SIZES=(8 10 12 14)
|
||||||
|
|
||||||
|
for size in ${BOOKERLY_FONT_SIZES[@]}; do
|
||||||
|
font_name="bookerly_${size}"
|
||||||
|
font_path_prefix="./source/Bookerly/Bookerly-"
|
||||||
|
output_path="./${font_name}.h"
|
||||||
|
../fontconvert/fontconvert "${font_path_prefix}Regular.ttf" -b "${font_path_prefix}Bold.ttf" -i "${font_path_prefix}Italic.ttf" -bi "${font_path_prefix}BoldItalic.ttf" -o $output_path -p $size
|
||||||
|
done
|
||||||
|
|
||||||
|
for size in ${NOTOSANS_FONT_SIZES[@]}; do
|
||||||
|
font_name="notosans_${size}"
|
||||||
|
font_path_prefix="./source/NotoSans/NotoSans-"
|
||||||
|
output_path="./${font_name}.h"
|
||||||
|
../fontconvert/fontconvert "${font_path_prefix}Regular.ttf" -b "${font_path_prefix}Bold.ttf" -i "${font_path_prefix}Italic.ttf" -bi "${font_path_prefix}BoldItalic.ttf" -o $output_path -p $size
|
||||||
|
done
|
||||||
|
|
||||||
|
for size in ${OPENDYSLEXIC_FONT_SIZES[@]}; do
|
||||||
|
font_name="opendyslexic_${size}"
|
||||||
|
font_path_prefix="./source/OpenDyslexic/OpenDyslexic-"
|
||||||
|
output_path="./${font_name}.h"
|
||||||
|
../fontconvert/fontconvert "${font_path_prefix}Regular.otf" -b "${font_path_prefix}Bold.otf" -i "${font_path_prefix}Italic.otf" -bi "${font_path_prefix}BoldItalic.otf" -o $output_path -p $size
|
||||||
|
done
|
||||||
|
|
||||||
|
UI_FONT_SIZES=(10 12)
|
||||||
|
UI_FONT_STYLES=("Regular" "Bold")
|
||||||
|
|
||||||
|
for size in ${UI_FONT_SIZES[@]}; do
|
||||||
|
font_name="ubuntu_${size}"
|
||||||
|
font_path_prefix="./source/Ubuntu/Ubuntu-"
|
||||||
|
output_path="./${font_name}.h"
|
||||||
|
../fontconvert/fontconvert "${font_path_prefix}Regular.ttf" -b "${font_path_prefix}Bold.ttf" -o $output_path -p $size
|
||||||
|
done
|
||||||
|
|
||||||
|
../fontconvert/fontconvert ./source/NotoSans/NotoSans-Regular.ttf -o ./notosans_8.h -p 8
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
Copyright (c) 2019-07-29, Abbie Gonzalez (https://abbiecod.es|support@abbiecod.es),
|
||||||
|
with Reserved Font Name OpenDyslexic.
|
||||||
|
Copyright (c) 12/2012 - 2019
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
-------------------------------
|
||||||
|
UBUNTU FONT LICENCE Version 1.0
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
This licence allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely. The fonts, including any derivative works, can be
|
||||||
|
bundled, embedded, and redistributed provided the terms of this licence
|
||||||
|
are met. The fonts and derivatives, however, cannot be released under
|
||||||
|
any other licence. The requirement for fonts to remain under this
|
||||||
|
licence does not require any document created using the fonts or their
|
||||||
|
derivatives to be published under this licence, as long as the primary
|
||||||
|
purpose of the document is not to be a vehicle for the distribution of
|
||||||
|
the fonts.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this licence and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components
|
||||||
|
as received under this licence.
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to
|
||||||
|
a new environment.
|
||||||
|
|
||||||
|
"Copyright Holder(s)" refers to all individuals and companies who have a
|
||||||
|
copyright ownership of the Font Software.
|
||||||
|
|
||||||
|
"Substantially Changed" refers to Modified Versions which can be easily
|
||||||
|
identified as dissimilar to the Font Software by users of the Font
|
||||||
|
Software comparing the Original Version with the Modified Version.
|
||||||
|
|
||||||
|
To "Propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification and with or without charging
|
||||||
|
a redistribution fee), making available to the public, and in some
|
||||||
|
countries other activities as well.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
This licence does not grant any rights under trademark law and all such
|
||||||
|
rights are reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of the Font Software, to propagate the Font Software, subject to
|
||||||
|
the below conditions:
|
||||||
|
|
||||||
|
1) Each copy of the Font Software must contain the above copyright
|
||||||
|
notice and this licence. These can be included either as stand-alone
|
||||||
|
text files, human-readable headers or in the appropriate machine-
|
||||||
|
readable metadata fields within text or binary files as long as those
|
||||||
|
fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
2) The font name complies with the following:
|
||||||
|
(a) The Original Version must retain its name, unmodified.
|
||||||
|
(b) Modified Versions which are Substantially Changed must be renamed to
|
||||||
|
avoid use of the name of the Original Version or similar names entirely.
|
||||||
|
(c) Modified Versions which are not Substantially Changed must be
|
||||||
|
renamed to both (i) retain the name of the Original Version and (ii) add
|
||||||
|
additional naming elements to distinguish the Modified Version from the
|
||||||
|
Original Version. The name of such Modified Versions must be the name of
|
||||||
|
the Original Version, with "derivative X" where X represents the name of
|
||||||
|
the new work, appended to that name.
|
||||||
|
|
||||||
|
3) The name(s) of the Copyright Holder(s) and any contributor to the
|
||||||
|
Font Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except (i) as required by this licence, (ii) to
|
||||||
|
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
|
||||||
|
their explicit written permission.
|
||||||
|
|
||||||
|
4) The Font Software, modified or unmodified, in part or in whole, must
|
||||||
|
be distributed entirely under this licence, and must not be distributed
|
||||||
|
under any other licence. The requirement for fonts to remain under this
|
||||||
|
licence does not affect any document created using the Font Software,
|
||||||
|
except any version of the Font Software extracted from a document
|
||||||
|
created using the Font Software may only be distributed under this
|
||||||
|
licence.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This licence becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||||
|
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||||
|
DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
fontconvert
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
all: fontconvert
|
||||||
|
|
||||||
|
CC = gcc
|
||||||
|
CFLAGS = -Wall -I/usr/local/include/freetype2 -I/usr/include/freetype2 -I/usr/include -I/opt/homebrew/include/freetype2
|
||||||
|
LIBS = -L/opt/homebrew/lib -lfreetype
|
||||||
|
|
||||||
|
fontconvert: main.c ../Group5/Group5.h
|
||||||
|
$(CC) $(CFLAGS) main.c $(LIBS) -o fontconvert
|
||||||
|
strip fontconvert
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f fontconvert
|
||||||
@@ -0,0 +1,553 @@
|
|||||||
|
//
|
||||||
|
// TrueType to CrossPoint font converter
|
||||||
|
// Copyright (c) 2024 BitBank Software, inc.
|
||||||
|
// Written by Larry Bank, adapted by Dave Allie for CrossPoint
|
||||||
|
// August 31, 2024
|
||||||
|
// The CrossPoint font format is a losslessly compressed bitmap font of a single point size with multiple variants
|
||||||
|
// This was built entirely on the back of Larry Bank's bb_font format.
|
||||||
|
// The data is compressed with a compression scheme based on CCITT T.6
|
||||||
|
// The font structure includes overall size, per-character glyph info and then the
|
||||||
|
// compressed image data at the end.
|
||||||
|
// The font file format is designed to allow both dynamic loading of font data from
|
||||||
|
// external memory/disk or compiling the data as const into a progarm.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
// ./fontconvert <regular.ttf> [-b <bold.ttf>] [-i <italic.ttf>] [-bi <bold-italic.ttf>] -p <pt size> -o <out.cpf>
|
||||||
|
// ./fontconvert <regular.ttf> [-b <bold.ttf>] [-i <italic.ttf>] [-bi <bold-italic.ttf>] -p <pt size> -o <out.h>
|
||||||
|
//
|
||||||
|
// This code requires the freetype library
|
||||||
|
// found here: www.freetype.org
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef ARDUINO
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <ft2build.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "../CrossPointFontFormat.h"
|
||||||
|
|
||||||
|
#include FT_GLYPH_H
|
||||||
|
#include FT_MODULE_H
|
||||||
|
#include FT_TRUETYPE_DRIVER_H
|
||||||
|
#include "../Group5/g5enc.inl" // Group5 image compression library
|
||||||
|
G5ENCIMAGE g5enc; // Group5 encoder state
|
||||||
|
|
||||||
|
#define DPI 150 // Approximate resolution of common displays
|
||||||
|
#define OUTBUF_SIZE 1048576 // 1MB
|
||||||
|
#define MAX_INTERVALS 65536
|
||||||
|
#define FONT_SCALE_FACTOR 2
|
||||||
|
// TODO: Re-enable small font
|
||||||
|
// Disabled small font generation to get this working, but want to re-enable
|
||||||
|
#define SMALL_FONT_ENABLED 0
|
||||||
|
|
||||||
|
uint32_t raw_intervals[][2] = {
|
||||||
|
/* Basic Latin */
|
||||||
|
// ASCII letters, digits, punctuation, control characters
|
||||||
|
{0x0000, 0x007F},
|
||||||
|
/* Latin-1 Supplement */
|
||||||
|
// Accented characters for Western European languages
|
||||||
|
{0x0080, 0x00FF},
|
||||||
|
/* Latin Extended-A */
|
||||||
|
// Eastern European and Baltic languages
|
||||||
|
{0x0100, 0x017F},
|
||||||
|
/* General Punctuation (core subset) */
|
||||||
|
// Smart quotes, en dash, em dash, ellipsis, NO-BREAK SPACE
|
||||||
|
{0x2000, 0x206F},
|
||||||
|
/* Basic Symbols From "Latin-1 + Misc" */
|
||||||
|
// dashes, quotes, prime marks
|
||||||
|
{0x2010, 0x203A},
|
||||||
|
// misc punctuation
|
||||||
|
{0x2040, 0x205F},
|
||||||
|
// common currency symbols
|
||||||
|
{0x20A0, 0x20CF},
|
||||||
|
/* Combining Diacritical Marks (minimal subset) */
|
||||||
|
// Needed for proper rendering of many extended Latin languages
|
||||||
|
{0x0300, 0x036F},
|
||||||
|
/* Greek & Coptic */
|
||||||
|
// Used in science, maths, philosophy, some academic texts
|
||||||
|
// {0x0370, 0x03FF},
|
||||||
|
/* Cyrillic */
|
||||||
|
// Russian, Ukrainian, Bulgarian, etc.
|
||||||
|
{0x0400, 0x04FF},
|
||||||
|
/* Math Symbols (common subset) */
|
||||||
|
// Superscripts and Subscripts
|
||||||
|
{0x2070, 0x209F},
|
||||||
|
// General math operators
|
||||||
|
{0x2200, 0x22FF},
|
||||||
|
// Arrows
|
||||||
|
{0x2190, 0x21FF},
|
||||||
|
/* CJK */
|
||||||
|
// Core Unified Ideographs
|
||||||
|
// {0x4E00, 0x9FFF},
|
||||||
|
// Extension A
|
||||||
|
// {0x3400, 0x4DBF},
|
||||||
|
// Extension B
|
||||||
|
// {0x20000, 0x2A6DF},
|
||||||
|
// Extension C–F
|
||||||
|
// {0x2A700, 0x2EBEF},
|
||||||
|
// Extension G
|
||||||
|
// {0x30000, 0x3134F},
|
||||||
|
// Hiragana
|
||||||
|
// {0x3040, 0x309F},
|
||||||
|
// Katakana
|
||||||
|
// {0x30A0, 0x30FF},
|
||||||
|
// Katakana Phonetic Extensions
|
||||||
|
// {0x31F0, 0x31FF},
|
||||||
|
// Halfwidth Katakana
|
||||||
|
// {0xFF60, 0xFF9F},
|
||||||
|
// Hangul Syllables
|
||||||
|
// {0xAC00, 0xD7AF},
|
||||||
|
// Hangul Jamo
|
||||||
|
// {0x1100, 0x11FF},
|
||||||
|
// Hangul Compatibility Jamo
|
||||||
|
// {0x3130, 0x318F},
|
||||||
|
// Hangul Jamo Extended-A
|
||||||
|
// {0xA960, 0xA97F},
|
||||||
|
// Hangul Jamo Extended-B
|
||||||
|
// {0xD7B0, 0xD7FF},
|
||||||
|
// CJK Radicals Supplement
|
||||||
|
// {0x2E80, 0x2EFF},
|
||||||
|
// Kangxi Radicals
|
||||||
|
// {0x2F00, 0x2FDF},
|
||||||
|
// CJK Symbols and Punctuation
|
||||||
|
// {0x3000, 0x303F},
|
||||||
|
// CJK Compatibility Forms
|
||||||
|
// {0xFE30, 0xFE4F},
|
||||||
|
// CJK Compatibility Ideographs
|
||||||
|
// {0xF900, 0xFAFF},
|
||||||
|
/* Specials */
|
||||||
|
// Replacement Character
|
||||||
|
{0xFFFD, 0xFFFD},
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Comparison function for qsort
|
||||||
|
//
|
||||||
|
int compareIntervals(const void* a, const void* b) {
|
||||||
|
const uint32_t* ia = (uint32_t*)a;
|
||||||
|
const uint32_t* ib = (uint32_t*)b;
|
||||||
|
if (ia[0] < ib[0]) return -1;
|
||||||
|
if (ia[0] > ib[0]) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Sort and merge adjacent intervals
|
||||||
|
// Returns the number of intervals after merging
|
||||||
|
//
|
||||||
|
int sortAndMergeIntervals(uint32_t intervals[][2], const int count) {
|
||||||
|
int merged_count = 0;
|
||||||
|
|
||||||
|
// Sort intervals by start value
|
||||||
|
qsort(intervals, count, sizeof(uint32_t) * 2, compareIntervals);
|
||||||
|
|
||||||
|
// Merge overlapping/adjacent intervals
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
if (merged_count > 0 && intervals[i][0] <= intervals[merged_count - 1][1] + 1) {
|
||||||
|
// Merge with previous interval
|
||||||
|
if (intervals[i][1] > intervals[merged_count - 1][1]) {
|
||||||
|
intervals[merged_count - 1][1] = intervals[i][1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add as new interval
|
||||||
|
if (merged_count != i) {
|
||||||
|
intervals[merged_count][0] = intervals[i][0];
|
||||||
|
intervals[merged_count][1] = intervals[i][1];
|
||||||
|
}
|
||||||
|
merged_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create the comments and const array boilerplate for the hex data bytes
|
||||||
|
//
|
||||||
|
void StartHexFile(FILE* f, int iLen, const char* fname, int size) {
|
||||||
|
int i, j;
|
||||||
|
char szTemp[256];
|
||||||
|
fprintf(f, "#pragma once\n\n");
|
||||||
|
fprintf(f, "//\n// Created with fontconvert, written by Larry Bank, updated for CrossPoint by Dave Allie\n");
|
||||||
|
fprintf(f, "// Point size: %d (scaled %dx)\n", size, FONT_SCALE_FACTOR);
|
||||||
|
fprintf(f, "// compressed font data size = %d bytes\n//\n", iLen);
|
||||||
|
|
||||||
|
strcpy(szTemp, fname);
|
||||||
|
i = strlen(szTemp);
|
||||||
|
if (szTemp[i - 2] == '.') szTemp[i - 2] = 0; // get the leaf name for the data
|
||||||
|
j = i;
|
||||||
|
// go backwards to get rid trim off just the leaf name
|
||||||
|
while (j > 0 && szTemp[j] != '/') {
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
if (szTemp[j] == '/') j++;
|
||||||
|
fprintf(f, "static const uint8_t %s[] = {\n", &szTemp[j]);
|
||||||
|
} /* StartHexFile() */
|
||||||
|
|
||||||
|
//
|
||||||
|
// Add N bytes of hex data to the output
|
||||||
|
// The data will be arranged in rows of 16 bytes each
|
||||||
|
//
|
||||||
|
void AddHexBytes(FILE* f, void* pData, int iLen, int bLast) {
|
||||||
|
static int iCount = 0; // number of bytes processed so far
|
||||||
|
int i;
|
||||||
|
uint8_t* s = (uint8_t*)pData;
|
||||||
|
for (i = 0; i < iLen; i++) { // process the given data
|
||||||
|
fprintf(f, "0x%02x", *s++);
|
||||||
|
iCount++;
|
||||||
|
if (i < iLen - 1 || !bLast) fprintf(f, ",");
|
||||||
|
if ((iCount & 15) == 0) fprintf(f, "\n"); // next row of 16
|
||||||
|
}
|
||||||
|
if (bLast) {
|
||||||
|
fprintf(f, "};\n");
|
||||||
|
}
|
||||||
|
} /* AddHexBytes() */
|
||||||
|
|
||||||
|
int loadCodePoint(FT_Face face, uint32_t code_point, CrossPointFontGlyph* pGlyphs, uint8_t* pBitmap,
|
||||||
|
uint32_t* glyph_index, uint32_t* iOffset) {
|
||||||
|
uint8_t* s;
|
||||||
|
int iPitch, err;
|
||||||
|
FT_Glyph glyph;
|
||||||
|
|
||||||
|
// MONO renderer provides clean image with perfect crop
|
||||||
|
// (no wasted pixels) via bitmap struct.
|
||||||
|
if ((err = FT_Load_Char(face, code_point, FT_LOAD_TARGET_MONO))) {
|
||||||
|
printf("Error %d loading char U+%04X\n", err, code_point);
|
||||||
|
(*glyph_index)++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((err = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_MONO))) {
|
||||||
|
printf("Error %d rendering char U+%04X\n", err, code_point);
|
||||||
|
(*glyph_index)++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((err = FT_Get_Glyph(face->glyph, &glyph))) {
|
||||||
|
printf("Error %d getting glyph U+%04X\n", err, code_point);
|
||||||
|
(*glyph_index)++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Bitmap* bitmap = &face->glyph->bitmap;
|
||||||
|
FT_BitmapGlyphRec* g = (FT_BitmapGlyphRec*)glyph;
|
||||||
|
|
||||||
|
// TODO: Restore small font
|
||||||
|
if (0 /* bSmallFont */) {
|
||||||
|
#if SMALL_FONT_ENABLED == 1
|
||||||
|
printf("Stubbed\n");
|
||||||
|
return 1;
|
||||||
|
#else
|
||||||
|
printf("Small font has been disabled\n");
|
||||||
|
return 1;
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
pGlyphs[*glyph_index].bitmapOffset = *iOffset;
|
||||||
|
pGlyphs[*glyph_index].width = bitmap->width;
|
||||||
|
pGlyphs[*glyph_index].height = bitmap->rows;
|
||||||
|
pGlyphs[*glyph_index].xAdvance = (face->glyph->advance.x >> 6);
|
||||||
|
pGlyphs[*glyph_index].xOffset = g->left;
|
||||||
|
pGlyphs[*glyph_index].yOffset = g->top;
|
||||||
|
}
|
||||||
|
s = bitmap->buffer;
|
||||||
|
iPitch = bitmap->pitch;
|
||||||
|
|
||||||
|
g5_encode_init(&g5enc, bitmap->width, bitmap->rows, &pBitmap[*iOffset], OUTBUF_SIZE - *iOffset);
|
||||||
|
for (int y = 0; y < bitmap->rows; y++) {
|
||||||
|
g5_encode_encodeLine(&g5enc, &s[y * iPitch]);
|
||||||
|
} // for y
|
||||||
|
int iLen = g5_encode_getOutSize(&g5enc);
|
||||||
|
*iOffset += iLen;
|
||||||
|
|
||||||
|
FT_Done_Glyph(glyph);
|
||||||
|
(*glyph_index)++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
int i, err, size = 0;
|
||||||
|
uint32_t iLen, iOffset = 0;
|
||||||
|
FILE* fOut;
|
||||||
|
// TrueType library structures
|
||||||
|
FT_Library library;
|
||||||
|
char* regularFaceFile;
|
||||||
|
char* boldFaceFile = NULL;
|
||||||
|
char* italicFaceFile = NULL;
|
||||||
|
char* boldItalicFaceFile = NULL;
|
||||||
|
const char* outputFile = NULL;
|
||||||
|
FT_Face faceRegular;
|
||||||
|
FT_Face faceBold;
|
||||||
|
FT_Face faceItalic;
|
||||||
|
FT_Face faceBoldItalic;
|
||||||
|
|
||||||
|
int bSmallFont = 0; // indicates if we're creating a normal or small font file
|
||||||
|
CrossPointFontUnicodeInterval* pIntervals;
|
||||||
|
CrossPointFontGlyph* pGlyphs;
|
||||||
|
#if SMALL_FONT_ENABLED == 1
|
||||||
|
CrossPointFontSmallGlyph* pSmallGlyphs;
|
||||||
|
#endif
|
||||||
|
uint8_t* pBitmap;
|
||||||
|
CrossPointFontHeader epdFontHeader;
|
||||||
|
|
||||||
|
int bHFile; // flag indicating if the output will be a .H file of hex data
|
||||||
|
|
||||||
|
// Process intervals
|
||||||
|
uint32_t intervals[MAX_INTERVALS][2];
|
||||||
|
int intervalCount = sizeof(raw_intervals) / sizeof(raw_intervals[0]);
|
||||||
|
uint32_t totalGlyphs = 0;
|
||||||
|
|
||||||
|
if (argc < 6 || argc % 2 == 1) {
|
||||||
|
printf(
|
||||||
|
"Usage: %s <regular.ttf> [-b <bold.ttf>] [-i <italic.ttf>] [-bi <bold-italic.ttf>] -p point_size -o <out.cpf "
|
||||||
|
"or out.h>\n",
|
||||||
|
argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
regularFaceFile = argv[1];
|
||||||
|
for (int i = 2; i < argc; i += 2) {
|
||||||
|
if (strcmp(argv[i], "-b") == 0) {
|
||||||
|
// Bold font
|
||||||
|
boldFaceFile = argv[i + 1];
|
||||||
|
} else if (strcmp(argv[i], "-i") == 0) {
|
||||||
|
// Italic font
|
||||||
|
italicFaceFile = argv[i + 1];
|
||||||
|
} else if (strcmp(argv[i], "-bi") == 0) {
|
||||||
|
// Bold-Italic font
|
||||||
|
boldItalicFaceFile = argv[i + 1];
|
||||||
|
} else if (strcmp(argv[i], "-p") == 0) {
|
||||||
|
// Point size
|
||||||
|
size = atoi(argv[i + 1]);
|
||||||
|
} else if (strcmp(argv[i], "-o") == 0) {
|
||||||
|
// Output file
|
||||||
|
outputFile = argv[i + 1];
|
||||||
|
// output an H file?
|
||||||
|
bHFile = outputFile[strlen(outputFile) - 1] == 'H' || outputFile[strlen(outputFile) - 1] == 'h';
|
||||||
|
} else {
|
||||||
|
printf("Unknown argument: %s\n", argv[i]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputFile) {
|
||||||
|
printf("No output file specified\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size <= 0) {
|
||||||
|
printf("Invalid point size: %d\n", size);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size = size * FONT_SCALE_FACTOR;
|
||||||
|
bSmallFont = (size < 60) && SMALL_FONT_ENABLED == 1; // Glyph info can fit in signed 8-bit values
|
||||||
|
int fontVariants = 1; // Always at least one variant we treat as regular
|
||||||
|
if (boldFaceFile) fontVariants += 1;
|
||||||
|
if (italicFaceFile) fontVariants += 1;
|
||||||
|
if (boldItalicFaceFile) fontVariants += 1;
|
||||||
|
|
||||||
|
// Copy and sort/merge intervals
|
||||||
|
if (intervalCount > MAX_INTERVALS) {
|
||||||
|
printf("Error: too many intervals (max %d)\n", MAX_INTERVALS);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (i = 0; i < intervalCount; i++) {
|
||||||
|
intervals[i][0] = raw_intervals[i][0];
|
||||||
|
intervals[i][1] = raw_intervals[i][1];
|
||||||
|
}
|
||||||
|
intervalCount = sortAndMergeIntervals(intervals, intervalCount);
|
||||||
|
|
||||||
|
// Calculate total number of glyphs
|
||||||
|
for (i = 0; i < intervalCount; i++) {
|
||||||
|
totalGlyphs += intervals[i][1] - intervals[i][0] + 1;
|
||||||
|
}
|
||||||
|
totalGlyphs *= fontVariants;
|
||||||
|
|
||||||
|
printf("Processed intervals: %d, total glyphs: %u\n", intervalCount, totalGlyphs);
|
||||||
|
|
||||||
|
// Allocate memory for intervals
|
||||||
|
pIntervals = (CrossPointFontUnicodeInterval*)malloc(intervalCount * sizeof(CrossPointFontUnicodeInterval));
|
||||||
|
if (!pIntervals) {
|
||||||
|
printf("Error allocating memory for interval data\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate memory for glyphs
|
||||||
|
if (bSmallFont) {
|
||||||
|
#if SMALL_FONT_ENABLED == 1
|
||||||
|
pSmallGlyphs = (CrossPointFontSmallGlyph*)malloc(totalGlyphs * sizeof(CrossPointFontSmallGlyph));
|
||||||
|
if (!pSmallGlyphs) {
|
||||||
|
printf("Error allocating memory for glyph data\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
printf("Small font has been disabled\n");
|
||||||
|
return 1;
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
pGlyphs = (CrossPointFontGlyph*)malloc(totalGlyphs * sizeof(CrossPointFontGlyph));
|
||||||
|
if (!pGlyphs) {
|
||||||
|
printf("Error allocating memory for glyph data\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pBitmap = (uint8_t*)malloc(OUTBUF_SIZE); // Enough to hold the output
|
||||||
|
if (!pBitmap) {
|
||||||
|
printf("Error allocating memory for bitmap data\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init FreeType lib, load font
|
||||||
|
if ((err = FT_Init_FreeType(&library))) {
|
||||||
|
printf("FreeType init error: %d", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use TrueType engine version 35, without subpixel rendering.
|
||||||
|
// This improves clarity of fonts since this library does not
|
||||||
|
// support rendering multiple levels of gray in a glyph.
|
||||||
|
// See https://github.com/adafruit/Adafruit-GFX-Library/issues/103
|
||||||
|
FT_UInt interpreter_version = TT_INTERPRETER_VERSION_35;
|
||||||
|
FT_Property_Set(library, "truetype", "interpreter-version", &interpreter_version);
|
||||||
|
|
||||||
|
if ((err = FT_New_Face(library, regularFaceFile, 0, &faceRegular))) {
|
||||||
|
printf("Font load error: %d\n", err);
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (italicFaceFile && (err = FT_New_Face(library, italicFaceFile, 0, &faceItalic))) {
|
||||||
|
printf("Font load error: %d\n", err);
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boldFaceFile && (err = FT_New_Face(library, boldFaceFile, 0, &faceBold))) {
|
||||||
|
printf("Font load error: %d\n", err);
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boldItalicFaceFile && (err = FT_New_Face(library, boldItalicFaceFile, 0, &faceBoldItalic))) {
|
||||||
|
printf("Font load error: %d\n", err);
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift the size left by 6 because the library uses '26dot6' fixed-point format
|
||||||
|
FT_Set_Char_Size(faceRegular, size << 6, 0, DPI, 0);
|
||||||
|
if (boldFaceFile) FT_Set_Char_Size(faceBold, size << 6, 0, DPI, 0);
|
||||||
|
if (italicFaceFile) FT_Set_Char_Size(faceItalic, size << 6, 0, DPI, 0);
|
||||||
|
if (boldItalicFaceFile) FT_Set_Char_Size(faceBoldItalic, size << 6, 0, DPI, 0);
|
||||||
|
|
||||||
|
// Build intervals with offsets and process glyphs
|
||||||
|
uint32_t glyph_index = 0;
|
||||||
|
for (int iInterval = 0; iInterval < intervalCount; iInterval++) {
|
||||||
|
const uint32_t intervalStart = intervals[iInterval][0];
|
||||||
|
const uint32_t intervalEnd = intervals[iInterval][1];
|
||||||
|
|
||||||
|
// Store interval with offset
|
||||||
|
pIntervals[iInterval].first = intervalStart;
|
||||||
|
pIntervals[iInterval].last = intervalEnd;
|
||||||
|
pIntervals[iInterval].offset = glyph_index;
|
||||||
|
|
||||||
|
// Process each glyph in this interval
|
||||||
|
// Load the codepoint for each style variant
|
||||||
|
for (uint32_t codePoint = intervalStart; codePoint <= intervalEnd; codePoint++) {
|
||||||
|
loadCodePoint(faceRegular, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
|
||||||
|
if (boldFaceFile) loadCodePoint(faceBold, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
|
||||||
|
if (italicFaceFile) loadCodePoint(faceItalic, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
|
||||||
|
if (boldItalicFaceFile) loadCodePoint(faceBoldItalic, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
|
||||||
|
} // for each code point in interval
|
||||||
|
} // for each interval
|
||||||
|
|
||||||
|
// Try to create the output file
|
||||||
|
fOut = fopen(outputFile, "w+b");
|
||||||
|
if (!fOut) {
|
||||||
|
printf("Error creating output file: %s\n", outputFile);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
epdFontHeader.height = faceRegular->size->metrics.height >> 6;
|
||||||
|
epdFontHeader.ascender = faceRegular->size->metrics.ascender >> 6;
|
||||||
|
epdFontHeader.styles = 0b0001;
|
||||||
|
if (boldFaceFile) epdFontHeader.styles |= 0b0010;
|
||||||
|
if (italicFaceFile) epdFontHeader.styles |= 0b0100;
|
||||||
|
if (boldItalicFaceFile) epdFontHeader.styles |= 0b1000;
|
||||||
|
epdFontHeader.intervalCount = intervalCount;
|
||||||
|
epdFontHeader.glyphCount = totalGlyphs;
|
||||||
|
|
||||||
|
// Write the file header
|
||||||
|
if (bSmallFont) {
|
||||||
|
#if SMALL_FONT_ENABLED == 1
|
||||||
|
epdFontHeader.u16Marker = CPF_FONT_MARKER_SMALL;
|
||||||
|
if (faceRegular->size->metrics.height == 0) {
|
||||||
|
// No face height info, assume fixed width and get from a glyph.
|
||||||
|
epdFontHeader.height = pSmallGlyphs[0].height;
|
||||||
|
}
|
||||||
|
|
||||||
|
iLen = sizeof(CrossPointFontHeader) + intervalCount * sizeof(CrossPointFontUnicodeInterval) +
|
||||||
|
totalGlyphs * sizeof(CrossPointFontSmallGlyph) + iOffset;
|
||||||
|
if (bHFile) { // create an H file of hex values
|
||||||
|
StartHexFile(fOut, iLen, outputFile, size);
|
||||||
|
AddHexBytes(fOut, &epdFontHeader, sizeof(CrossPointFontHeader), 0);
|
||||||
|
// Write the intervals
|
||||||
|
AddHexBytes(fOut, pIntervals, sizeof(CrossPointFontUnicodeInterval) * intervalCount, 0);
|
||||||
|
// Write the glyph table
|
||||||
|
AddHexBytes(fOut, pSmallGlyphs, sizeof(CrossPointFontSmallGlyph) * totalGlyphs, 0);
|
||||||
|
// Write the compressed bitmap data
|
||||||
|
AddHexBytes(fOut, pBitmap, iOffset, 1);
|
||||||
|
} else {
|
||||||
|
fwrite(&epdFontHeader, 1, sizeof(CrossPointFontHeader), fOut);
|
||||||
|
// Write the intervals
|
||||||
|
fwrite(pIntervals, 1, intervalCount * sizeof(CrossPointFontUnicodeInterval), fOut);
|
||||||
|
// Write the glyph table
|
||||||
|
fwrite(pSmallGlyphs, 1, totalGlyphs * sizeof(CrossPointFontSmallGlyph), fOut);
|
||||||
|
// Write the compressed bitmap data
|
||||||
|
fwrite(pBitmap, 1, iOffset, fOut);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
printf("Small font has been disabled\n");
|
||||||
|
return 1;
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
epdFontHeader.u16Marker = CPF_FONT_MARKER;
|
||||||
|
if (faceRegular->size->metrics.height == 0) {
|
||||||
|
// No face height info, assume fixed width and get from a glyph.
|
||||||
|
epdFontHeader.height = pGlyphs[0].height;
|
||||||
|
}
|
||||||
|
|
||||||
|
iLen = sizeof(CrossPointFontHeader) + intervalCount * sizeof(CrossPointFontUnicodeInterval) +
|
||||||
|
totalGlyphs * sizeof(CrossPointFontGlyph) + iOffset;
|
||||||
|
if (bHFile) { // create an H file of hex values
|
||||||
|
StartHexFile(fOut, iLen, outputFile, size);
|
||||||
|
AddHexBytes(fOut, &epdFontHeader, sizeof(CrossPointFontHeader), 0);
|
||||||
|
// Write the intervals
|
||||||
|
AddHexBytes(fOut, pIntervals, sizeof(CrossPointFontUnicodeInterval) * intervalCount, 0);
|
||||||
|
// Write the glyph table
|
||||||
|
AddHexBytes(fOut, pGlyphs, sizeof(CrossPointFontGlyph) * totalGlyphs, 0);
|
||||||
|
// Write the compressed bitmap data
|
||||||
|
AddHexBytes(fOut, pBitmap, iOffset, 1);
|
||||||
|
} else {
|
||||||
|
fwrite(&epdFontHeader, 1, sizeof(CrossPointFontHeader), fOut);
|
||||||
|
// Write the intervals
|
||||||
|
fwrite(pIntervals, 1, intervalCount * sizeof(CrossPointFontUnicodeInterval), fOut);
|
||||||
|
// Write the glyph table
|
||||||
|
fwrite(pGlyphs, 1, totalGlyphs * sizeof(CrossPointFontGlyph), fOut);
|
||||||
|
// Write the compressed bitmap data
|
||||||
|
fwrite(pBitmap, 1, iOffset, fOut);
|
||||||
|
}
|
||||||
|
} // large fonts
|
||||||
|
fflush(fOut);
|
||||||
|
fclose(fOut); // done!
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
printf("Success!\nFont file size: %d bytes (%d glyphs)\n", iLen, totalGlyphs);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} /* main() */
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#include "EpdFont.h"
|
|
||||||
|
|
||||||
#include <Utf8.h>
|
|
||||||
|
|
||||||
inline int min(const int a, const int b) { return a < b ? a : b; }
|
|
||||||
inline int max(const int a, const int b) { return a < b ? b : a; }
|
|
||||||
|
|
||||||
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX,
|
|
||||||
int* maxY) const {
|
|
||||||
*minX = startX;
|
|
||||||
*minY = startY;
|
|
||||||
*maxX = startX;
|
|
||||||
*maxY = startY;
|
|
||||||
|
|
||||||
if (*string == '\0') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int cursorX = startX;
|
|
||||||
const int cursorY = startY;
|
|
||||||
uint32_t cp;
|
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
|
||||||
const EpdGlyph* glyph = getGlyph(cp);
|
|
||||||
|
|
||||||
if (!glyph) {
|
|
||||||
// TODO: Replace with fallback glyph property?
|
|
||||||
glyph = getGlyph('?');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!glyph) {
|
|
||||||
// TODO: Better handle this?
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
*minX = min(*minX, cursorX + glyph->left);
|
|
||||||
*maxX = max(*maxX, cursorX + glyph->left + glyph->width);
|
|
||||||
*minY = min(*minY, cursorY + glyph->top - glyph->height);
|
|
||||||
*maxY = max(*maxY, cursorY + glyph->top);
|
|
||||||
cursorX += glyph->advanceX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdFont::getTextDimensions(const char* string, int* w, int* h) const {
|
|
||||||
int minX = 0, minY = 0, maxX = 0, maxY = 0;
|
|
||||||
|
|
||||||
getTextBounds(string, 0, 0, &minX, &minY, &maxX, &maxY);
|
|
||||||
|
|
||||||
*w = maxX - minX;
|
|
||||||
*h = maxY - minY;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EpdFont::hasPrintableChars(const char* string) const {
|
|
||||||
int w = 0, h = 0;
|
|
||||||
|
|
||||||
getTextDimensions(string, &w, &h);
|
|
||||||
|
|
||||||
return w > 0 || h > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
|
||||||
const EpdUnicodeInterval* intervals = data->intervals;
|
|
||||||
for (int i = 0; i < data->intervalCount; i++) {
|
|
||||||
const EpdUnicodeInterval* interval = &intervals[i];
|
|
||||||
if (cp >= interval->first && cp <= interval->last) {
|
|
||||||
return &data->glyph[interval->offset + (cp - interval->first)];
|
|
||||||
}
|
|
||||||
if (cp < interval->first) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "EpdFontData.h"
|
|
||||||
|
|
||||||
class EpdFont {
|
|
||||||
void getTextBounds(const char* string, int startX, int startY, int* minX, int* minY, int* maxX, int* maxY) const;
|
|
||||||
|
|
||||||
public:
|
|
||||||
const EpdFontData* data;
|
|
||||||
explicit EpdFont(const EpdFontData* data) : data(data) {}
|
|
||||||
~EpdFont() = default;
|
|
||||||
void getTextDimensions(const char* string, int* w, int* h) const;
|
|
||||||
bool hasPrintableChars(const char* string) const;
|
|
||||||
|
|
||||||
const EpdGlyph* getGlyph(uint32_t cp) const;
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// From
|
|
||||||
// https://github.com/vroland/epdiy/blob/c61e9e923ce2418150d54f88cea5d196cdc40c54/src/epd_internals.h
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
/// Font data stored PER GLYPH
|
|
||||||
typedef struct {
|
|
||||||
uint8_t width; ///< Bitmap dimensions in pixels
|
|
||||||
uint8_t height; ///< Bitmap dimensions in pixels
|
|
||||||
uint8_t advanceX; ///< Distance to advance cursor (x axis)
|
|
||||||
int16_t left; ///< X dist from cursor pos to UL corner
|
|
||||||
int16_t top; ///< Y dist from cursor pos to UL corner
|
|
||||||
uint16_t dataLength; ///< Size of the font data.
|
|
||||||
uint32_t dataOffset; ///< Pointer into EpdFont->bitmap
|
|
||||||
} EpdGlyph;
|
|
||||||
|
|
||||||
/// Glyph interval structure
|
|
||||||
typedef struct {
|
|
||||||
uint32_t first; ///< The first unicode code point of the interval
|
|
||||||
uint32_t last; ///< The last unicode code point of the interval
|
|
||||||
uint32_t offset; ///< Index of the first code point into the glyph array
|
|
||||||
} EpdUnicodeInterval;
|
|
||||||
|
|
||||||
/// Data stored for FONT AS A WHOLE
|
|
||||||
typedef struct {
|
|
||||||
const uint8_t* bitmap; ///< Glyph bitmaps, concatenated
|
|
||||||
const EpdGlyph* glyph; ///< Glyph array
|
|
||||||
const EpdUnicodeInterval* intervals; ///< Valid unicode intervals for this font
|
|
||||||
uint32_t intervalCount; ///< Number of unicode intervals.
|
|
||||||
uint8_t advanceY; ///< Newline distance (y axis)
|
|
||||||
int ascender; ///< Maximal height of a glyph above the base line
|
|
||||||
int descender; ///< Maximal height of a glyph below the base line
|
|
||||||
bool is2Bit;
|
|
||||||
} EpdFontData;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#include "EpdFontFamily.h"
|
|
||||||
|
|
||||||
const EpdFont* EpdFontFamily::getFont(const EpdFontStyle style) const {
|
|
||||||
if (style == BOLD && bold) {
|
|
||||||
return bold;
|
|
||||||
}
|
|
||||||
if (style == ITALIC && italic) {
|
|
||||||
return italic;
|
|
||||||
}
|
|
||||||
if (style == BOLD_ITALIC) {
|
|
||||||
if (boldItalic) {
|
|
||||||
return boldItalic;
|
|
||||||
}
|
|
||||||
if (bold) {
|
|
||||||
return bold;
|
|
||||||
}
|
|
||||||
if (italic) {
|
|
||||||
return italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return regular;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const EpdFontStyle style) const {
|
|
||||||
getFont(style)->getTextDimensions(string, w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EpdFontFamily::hasPrintableChars(const char* string, const EpdFontStyle style) const {
|
|
||||||
return getFont(style)->hasPrintableChars(string);
|
|
||||||
}
|
|
||||||
|
|
||||||
const EpdFontData* EpdFontFamily::getData(const EpdFontStyle style) const { return getFont(style)->data; }
|
|
||||||
|
|
||||||
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const EpdFontStyle style) const {
|
|
||||||
return getFont(style)->getGlyph(cp);
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "EpdFont.h"
|
|
||||||
|
|
||||||
enum EpdFontStyle { REGULAR, BOLD, ITALIC, BOLD_ITALIC };
|
|
||||||
|
|
||||||
class EpdFontFamily {
|
|
||||||
const EpdFont* regular;
|
|
||||||
const EpdFont* bold;
|
|
||||||
const EpdFont* italic;
|
|
||||||
const EpdFont* boldItalic;
|
|
||||||
|
|
||||||
const EpdFont* getFont(EpdFontStyle style) const;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
|
||||||
const EpdFont* boldItalic = nullptr)
|
|
||||||
: regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {}
|
|
||||||
~EpdFontFamily() = default;
|
|
||||||
void getTextDimensions(const char* string, int* w, int* h, EpdFontStyle style = REGULAR) const;
|
|
||||||
bool hasPrintableChars(const char* string, EpdFontStyle style = REGULAR) const;
|
|
||||||
|
|
||||||
const EpdFontData* getData(EpdFontStyle style = REGULAR) const;
|
|
||||||
const EpdGlyph* getGlyph(uint32_t cp, EpdFontStyle style = REGULAR) const;
|
|
||||||
};
|
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
/**
|
|
||||||
* generated by fontconvert.py
|
|
||||||
* name: babyblue
|
|
||||||
* size: 8
|
|
||||||
* mode: 1-bit
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
#include "EpdFontData.h"
|
|
||||||
|
|
||||||
static const uint8_t babyblueBitmaps[3140] = {
|
|
||||||
0xFF, 0xFF, 0x30, 0xFF, 0xFF, 0xF0, 0x36, 0x1B, 0x0D, 0x9F, 0xFF, 0xF9, 0xB3, 0xFF, 0xFF, 0x36, 0x1B, 0x0D, 0x80,
|
|
||||||
0x18, 0x18, 0x7E, 0xFF, 0xDB, 0xD8, 0xFE, 0x7F, 0x9B, 0xDB, 0xFF, 0x7E, 0x18, 0x00, 0x71, 0x9F, 0x73, 0x6C, 0x6F,
|
|
||||||
0x8F, 0xE0, 0xFF, 0x83, 0xF8, 0xFB, 0x1B, 0x63, 0x7C, 0xC7, 0x00, 0x38, 0x3E, 0x1B, 0x0D, 0x87, 0xC3, 0xEF, 0xBF,
|
|
||||||
0x8E, 0xCF, 0x7F, 0x1E, 0xC0, 0xFF, 0x37, 0xEC, 0xCC, 0xCC, 0xCC, 0xCC, 0x67, 0x20, 0xCE, 0x73, 0x33, 0x33, 0x33,
|
|
||||||
0x33, 0x6C, 0x80, 0x32, 0xFF, 0xDE, 0xFE, 0x30, 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0xBF, 0x00, 0xFF,
|
|
||||||
0xC0, 0x30, 0x0C, 0x31, 0xC6, 0x18, 0xE3, 0x1C, 0x63, 0x8C, 0x00, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3,
|
|
||||||
0xC7, 0x7E, 0x3C, 0x19, 0xDF, 0xFD, 0x8C, 0x63, 0x18, 0xC6, 0x3C, 0x7E, 0xE7, 0x83, 0x07, 0x0E, 0x1C, 0x38, 0x70,
|
|
||||||
0xFE, 0xFF, 0x7E, 0xFF, 0xC3, 0x07, 0x3E, 0x3E, 0x07, 0x03, 0x83, 0xFF, 0x7E, 0x0E, 0x1E, 0x3E, 0x76, 0xE6, 0xFF,
|
|
||||||
0xFF, 0x06, 0x06, 0x06, 0x06, 0x7F, 0xFF, 0x06, 0x0F, 0xDF, 0xC1, 0x83, 0x87, 0xFD, 0xF0, 0x3E, 0x7F, 0xE3, 0xC0,
|
|
||||||
0xFC, 0xFE, 0xE7, 0xC3, 0xC7, 0x7E, 0x3C, 0xFF, 0xFF, 0xC0, 0xE0, 0xE0, 0x60, 0x70, 0x30, 0x18, 0x1C, 0x0C, 0x06,
|
|
||||||
0x00, 0x3C, 0x7E, 0x66, 0x66, 0x7E, 0x7E, 0xE7, 0xC3, 0xC7, 0x7E, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7F,
|
|
||||||
0x3F, 0x07, 0x3E, 0x7C, 0xB0, 0x03, 0xB0, 0x03, 0xF8, 0x06, 0x3D, 0xF7, 0x8F, 0x0F, 0x87, 0x03, 0xFF, 0xFF, 0x00,
|
|
||||||
0xFF, 0xFF, 0x81, 0xE1, 0xF0, 0xF1, 0xEF, 0xBC, 0x40, 0x38, 0xFB, 0xBE, 0x30, 0xE3, 0x8E, 0x18, 0x30, 0x00, 0xC0,
|
|
||||||
0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x9D, 0xE7, 0xEF, 0xCF, 0x73, 0x3D, 0x8C, 0xF6, 0x33, 0xD8, 0xDB, 0x3F, 0xC6, 0x7E,
|
|
||||||
0x0C, 0x03, 0x18, 0x18, 0x7F, 0xE0, 0xFF, 0x00, 0x38, 0xFB, 0xBE, 0x3C, 0x7F, 0xFF, 0xE3, 0xC7, 0x8C, 0xFC, 0xFE,
|
|
||||||
0xC7, 0xC7, 0xFE, 0xFE, 0xC7, 0xC7, 0xFE, 0xFC, 0x3F, 0x3F, 0xF8, 0x78, 0x0C, 0x06, 0x03, 0x01, 0x83, 0x7F, 0x9F,
|
|
||||||
0x80, 0xFC, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0xFE, 0xFC, 0xFF, 0xFF, 0x06, 0x0F, 0xDF, 0xB0, 0x60, 0xFD,
|
|
||||||
0xFC, 0xFF, 0xFF, 0x06, 0x0F, 0xDF, 0xB0, 0x60, 0xC1, 0x80, 0x3F, 0x3F, 0xF8, 0x78, 0x0C, 0x7E, 0x3F, 0x07, 0x83,
|
|
||||||
0x7F, 0x9F, 0x80, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xF0, 0x0C, 0x30, 0xC3,
|
|
||||||
0x0C, 0x38, 0xF3, 0xFD, 0xE0, 0xC3, 0xC7, 0xCE, 0xDC, 0xF8, 0xF8, 0xDC, 0xCC, 0xC6, 0xC3, 0xC1, 0x83, 0x06, 0x0C,
|
|
||||||
0x18, 0x30, 0x60, 0xFD, 0xFC, 0xE1, 0xF8, 0x7F, 0x3F, 0xCF, 0xFF, 0xF7, 0xBD, 0xEF, 0x7B, 0xCC, 0xF3, 0x30, 0xC3,
|
|
||||||
0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x3E, 0x3F, 0xB8, 0xF8, 0x3C, 0x1E, 0x0F, 0x07, 0x87, 0x7F,
|
|
||||||
0x1F, 0x00, 0xFE, 0xFF, 0xC3, 0xC3, 0xFF, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0x3E, 0x3F, 0xB8, 0xF8, 0x3C, 0x1E, 0x0F,
|
|
||||||
0x37, 0x9F, 0x7F, 0x1F, 0xC0, 0xFE, 0xFF, 0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0x7D, 0xFF, 0x1F, 0x87,
|
|
||||||
0xC3, 0xC1, 0xC3, 0xFE, 0xF8, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xC3, 0xC3, 0xC3, 0xC3,
|
|
||||||
0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0xC3, 0xC3, 0xC3, 0xE7, 0x66, 0x7E, 0x3C, 0x3C, 0x3C, 0x18, 0x83, 0x0F, 0x0C,
|
|
||||||
0x3C, 0x30, 0xF1, 0xE7, 0x67, 0x99, 0xBF, 0xE3, 0xCF, 0x0F, 0x3C, 0x3C, 0xF0, 0x61, 0x80, 0x80, 0xF8, 0x77, 0x38,
|
|
||||||
0xFC, 0x1E, 0x07, 0x83, 0xF1, 0xCE, 0xE1, 0xB0, 0x30, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18,
|
|
||||||
0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xFE, 0xFF, 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xFE, 0x81, 0xC1,
|
|
||||||
0x83, 0x83, 0x83, 0x06, 0x06, 0x0C, 0x0C, 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0xFE, 0x18, 0x3C, 0x7E, 0x66, 0xE7,
|
|
||||||
0xC3, 0x83, 0xFF, 0xFF, 0x9D, 0x80, 0x7D, 0xFE, 0x1B, 0xFF, 0xF8, 0xFF, 0xBF, 0xC1, 0x83, 0xE7, 0xEC, 0xF8, 0xF1,
|
|
||||||
0xE7, 0xFD, 0xF0, 0x3C, 0xFF, 0x9E, 0x0C, 0x18, 0x9F, 0x9E, 0x06, 0x0C, 0xFB, 0xFE, 0x78, 0xF1, 0xE3, 0x7E, 0x7C,
|
|
||||||
0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x3B, 0xD9, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0x3E, 0xFF, 0x9E, 0x3C,
|
|
||||||
0x78, 0xDF, 0x9F, 0x06, 0x0D, 0xF3, 0xC0, 0xC3, 0x0F, 0xBF, 0xEF, 0x3C, 0xF3, 0xCF, 0x30, 0xFB, 0xFF, 0xF0, 0x6D,
|
|
||||||
0x36, 0xDB, 0x6D, 0xBD, 0x00, 0xC3, 0x0C, 0xF7, 0xFB, 0xCE, 0x3C, 0xDB, 0x30, 0xFF, 0xFF, 0xF0, 0x7F, 0xBF, 0xFC,
|
|
||||||
0xCF, 0x33, 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0x7B, 0xFC, 0xF3, 0xCF, 0x3C, 0xF3, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7,
|
|
||||||
0x7E, 0x3C, 0x79, 0xFB, 0x3E, 0x3C, 0x79, 0xFF, 0x7C, 0xC1, 0x82, 0x00, 0x3C, 0xFF, 0x9E, 0x3C, 0x78, 0xDF, 0x9F,
|
|
||||||
0x06, 0x0C, 0x10, 0x77, 0xF7, 0x8C, 0x63, 0x18, 0x7D, 0xFF, 0x1F, 0xE7, 0xF0, 0xFF, 0xBE, 0x63, 0x3D, 0xE6, 0x31,
|
|
||||||
0x8C, 0x71, 0xC0, 0x8F, 0x3C, 0xF3, 0xCF, 0x3F, 0xDE, 0x83, 0xC3, 0xC7, 0x66, 0x66, 0x6E, 0x3C, 0x18, 0x80, 0xF3,
|
|
||||||
0x3D, 0xFD, 0xFE, 0x7F, 0x9F, 0xE3, 0x30, 0xCC, 0x83, 0xC7, 0x6E, 0x3C, 0x38, 0x7C, 0xE6, 0xC3, 0x83, 0xC7, 0x66,
|
|
||||||
0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0xFF, 0xFC, 0x71, 0xC7, 0x1C, 0x3F, 0x7F, 0x19, 0xCC, 0x63, 0x3B,
|
|
||||||
0x98, 0x61, 0x8C, 0x63, 0x1C, 0x40, 0xFF, 0xFF, 0xFF, 0xF8, 0x83, 0x87, 0x0C, 0x30, 0xE1, 0xC7, 0x38, 0xC3, 0x0C,
|
|
||||||
0x63, 0x08, 0x00, 0x79, 0xFF, 0xE3, 0xC0, 0xF2, 0xFF, 0xFE, 0x18, 0x30, 0xF3, 0xFF, 0xFB, 0x36, 0x6E, 0x7E, 0x78,
|
|
||||||
0x60, 0xC1, 0x00, 0x3C, 0x7E, 0x66, 0x60, 0xFC, 0xFC, 0x30, 0x72, 0xFF, 0xFE, 0x83, 0xFF, 0x7E, 0x66, 0x66, 0x7E,
|
|
||||||
0xFE, 0x83, 0x83, 0xE7, 0x7E, 0x3C, 0xFF, 0xFF, 0xFF, 0xFF, 0x18, 0x18, 0xFF, 0xFC, 0xBF, 0xF8, 0x3C, 0x7E, 0x66,
|
|
||||||
0x7E, 0x7C, 0xEE, 0xC7, 0xC3, 0x77, 0x3E, 0x0C, 0x66, 0x66, 0x7C, 0x38, 0x9E, 0xE6, 0x3F, 0x1F, 0xEF, 0xFF, 0xFF,
|
|
||||||
0xF3, 0xFC, 0x3F, 0x3F, 0xFF, 0xDF, 0xDF, 0xE3, 0xF0, 0x77, 0xFF, 0xFF, 0xBC, 0x36, 0xFF, 0xF6, 0xCD, 0xCD, 0x8D,
|
|
||||||
0x80, 0xFF, 0xFF, 0x03, 0x03, 0x03, 0x3F, 0x1F, 0xEF, 0xFF, 0xFB, 0xF6, 0xFF, 0xBF, 0xEF, 0xDB, 0xF7, 0xDF, 0xE3,
|
|
||||||
0xF0, 0xFF, 0xFF, 0x6F, 0xFF, 0x60, 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0xFE, 0xFF, 0x77, 0xE6, 0x77, 0x73,
|
|
||||||
0xFF, 0x77, 0xEF, 0x7F, 0xB8, 0x7F, 0x00, 0xCF, 0x3C, 0xF3, 0xCF, 0x3F, 0xFE, 0xC3, 0x0C, 0x20, 0x7F, 0xFF, 0xFE,
|
|
||||||
0xFE, 0xFE, 0x7E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x16, 0xB0, 0x63, 0xEC, 0x37, 0xFB, 0x33, 0x30, 0x7B,
|
|
||||||
0xFC, 0xF3, 0xFD, 0xE0, 0x99, 0xF9, 0xF9, 0xB7, 0xFF, 0xB6, 0x00, 0x30, 0x07, 0x03, 0xF0, 0x7B, 0x0E, 0x31, 0xC3,
|
|
||||||
0x38, 0x37, 0x60, 0xEE, 0x1D, 0xE3, 0xBF, 0x33, 0xF6, 0x06, 0x30, 0x67, 0x0E, 0xF1, 0xCB, 0x38, 0x37, 0x03, 0xEF,
|
|
||||||
0x1D, 0xF3, 0x9F, 0x70, 0xEE, 0x0E, 0xC1, 0xF0, 0x70, 0x6F, 0x8E, 0xB9, 0xCB, 0xB8, 0xFF, 0x07, 0xE6, 0x1C, 0xE3,
|
|
||||||
0x9E, 0x73, 0xFE, 0x3F, 0xC0, 0x60, 0x30, 0x60, 0xC1, 0x87, 0x1C, 0x30, 0x60, 0xC6, 0xD9, 0xE1, 0x80, 0x30, 0x70,
|
|
||||||
0x61, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x18, 0x70, 0xC1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF,
|
|
||||||
0x1E, 0x3C, 0x60, 0x38, 0xF9, 0xB1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x3C, 0xF9, 0xE1, 0xC7,
|
|
||||||
0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x6C, 0xD8, 0xE3, 0xEE, 0xF8, 0xF1, 0xFF, 0xFF, 0x8F, 0x1E, 0x30,
|
|
||||||
0x38, 0xF9, 0xF1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x0F, 0xF8, 0x7F, 0xC7, 0xC0, 0x36, 0x03,
|
|
||||||
0xB0, 0x19, 0xF9, 0xFF, 0xCF, 0xE0, 0xE3, 0x06, 0x1F, 0xB0, 0xFE, 0x3F, 0x3F, 0xF8, 0x78, 0x0C, 0x06, 0x03, 0x01,
|
|
||||||
0x83, 0x7F, 0x9F, 0x86, 0x01, 0x83, 0x81, 0x80, 0x30, 0x70, 0x67, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF,
|
|
||||||
0xE0, 0x0C, 0x38, 0x67, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0, 0x18, 0x78, 0xF7, 0xFF, 0xF8, 0x30,
|
|
||||||
0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0, 0x3C, 0x7B, 0xFF, 0xFC, 0x18, 0x3F, 0x7E, 0xC1, 0x83, 0xF7, 0xF0, 0x9D, 0x36,
|
|
||||||
0xDB, 0x6D, 0xB6, 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, 0x6F, 0xB6, 0x66, 0x66, 0x66, 0x66, 0x60, 0xBB, 0x66, 0x66, 0x66,
|
|
||||||
0x66, 0x66, 0x7E, 0x3F, 0x98, 0xEC, 0x3F, 0xDF, 0xED, 0x86, 0xC7, 0x7F, 0x3F, 0x00, 0x1E, 0x3E, 0x3C, 0xC3, 0xE3,
|
|
||||||
0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x18, 0x0E, 0x03, 0x07, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0,
|
|
||||||
0xF0, 0xEF, 0xE3, 0xE0, 0x0C, 0x0E, 0x06, 0x07, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0,
|
|
||||||
0x1C, 0x1F, 0x0D, 0x87, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x1E, 0x1F, 0x0F, 0x07,
|
|
||||||
0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x36, 0x1B, 0x0F, 0x8F, 0xEE, 0x3E, 0x0F, 0x07,
|
|
||||||
0x83, 0xC1, 0xE1, 0xDF, 0xC7, 0xC0, 0x8F, 0xF7, 0x9E, 0xFF, 0x30, 0x1F, 0xCF, 0xF7, 0x3D, 0x9F, 0x6E, 0xDF, 0x37,
|
|
||||||
0x8D, 0xC7, 0xFF, 0xB7, 0xC0, 0x30, 0x38, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x0C,
|
|
||||||
0x1C, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3,
|
|
||||||
0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x0C,
|
|
||||||
0x1C, 0x18, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0xC0, 0xFC, 0xFE, 0xC7, 0xC3, 0xC7, 0xFE,
|
|
||||||
0xFC, 0xC0, 0xC0, 0x38, 0xFB, 0xB6, 0x6D, 0xDB, 0x37, 0x67, 0xE7, 0xFF, 0x70, 0x30, 0x70, 0x63, 0xEF, 0xF0, 0xDF,
|
|
||||||
0xFF, 0xC7, 0xFD, 0xF8, 0x0C, 0x38, 0x63, 0xEF, 0xF0, 0xDF, 0xFF, 0xC7, 0xFD, 0xF8, 0x18, 0x78, 0xF3, 0xEF, 0xF0,
|
|
||||||
0xDF, 0xFF, 0xC7, 0xFD, 0xF8, 0x3C, 0xF9, 0xE3, 0xEF, 0xF0, 0xDF, 0xFF, 0xC7, 0xFD, 0xF8, 0x3C, 0x79, 0xF7, 0xF8,
|
|
||||||
0x6F, 0xFF, 0xE3, 0xFE, 0xFC, 0x18, 0x78, 0xF0, 0xC7, 0xDF, 0xE1, 0xBF, 0xFF, 0x8F, 0xFB, 0xF0, 0x7F, 0xEF, 0xFF,
|
|
||||||
0x86, 0x37, 0xFF, 0xFF, 0xFC, 0x61, 0xFF, 0xF7, 0xFE, 0x3C, 0xFF, 0x9E, 0x0C, 0x18, 0x9F, 0x9E, 0x18, 0x18, 0xE1,
|
|
||||||
0x80, 0x30, 0x38, 0x18, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x0C, 0x1C, 0x18, 0x3C, 0x7E, 0xE7, 0xFF,
|
|
||||||
0xFF, 0xC0, 0x7E, 0x3F, 0x18, 0x3C, 0x3C, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x3C, 0x3C, 0x3C, 0x7E,
|
|
||||||
0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x9D, 0xA6, 0xDB, 0x6D, 0x80, 0x7F, 0x4D, 0xB6, 0xDB, 0x00, 0x6F, 0xB4, 0x66,
|
|
||||||
0x66, 0x66, 0x60, 0xBB, 0x46, 0x66, 0x66, 0x66, 0x3E, 0x3E, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C,
|
|
||||||
0x3D, 0xF7, 0x9E, 0xFF, 0x3C, 0xF3, 0xCF, 0x3C, 0xC0, 0x30, 0x38, 0x18, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E,
|
|
||||||
0x3C, 0x0C, 0x1C, 0x18, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x3C, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3,
|
|
||||||
0xC3, 0xC7, 0x7E, 0x3C, 0x1E, 0x3E, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3C, 0x3C, 0x3C, 0x7E,
|
|
||||||
0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x18, 0xFF, 0xFF, 0x10, 0x18, 0x3F, 0x7F, 0xEF, 0xDF, 0xFB, 0xF7, 0xFE,
|
|
||||||
0xFC, 0x61, 0xC3, 0x23, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x18, 0xE3, 0x23, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80,
|
|
||||||
0x31, 0xE7, 0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x79, 0xE8, 0xF3, 0xCF, 0x3C, 0xF3, 0xFD, 0xE0, 0x0C, 0x1C,
|
|
||||||
0x18, 0x83, 0xC7, 0x66, 0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0xC1, 0x83, 0xE7, 0xEE, 0xF8, 0xF1, 0xE7,
|
|
||||||
0xFD, 0xF3, 0x06, 0x08, 0x00, 0x3C, 0x3C, 0x83, 0xC7, 0x66, 0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0x7C,
|
|
||||||
0xF8, 0xE3, 0xEE, 0xF8, 0xF1, 0xFF, 0xFF, 0x8F, 0x1E, 0x30, 0x7C, 0xF9, 0xF7, 0xF8, 0x6F, 0xFF, 0xE3, 0xFE, 0xFC,
|
|
||||||
0x6C, 0xF8, 0xE1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x6C, 0xF8, 0xE3, 0xEF, 0xF0, 0xDF, 0xFF,
|
|
||||||
0xC7, 0xFD, 0xF8, 0x38, 0x7C, 0xEE, 0xC6, 0xC6, 0xFE, 0xFE, 0xC6, 0xC6, 0xC6, 0x0C, 0x0C, 0x0F, 0x07, 0x7C, 0xFE,
|
|
||||||
0x86, 0x7E, 0xFE, 0xC6, 0xFE, 0x7E, 0x06, 0x0C, 0x0F, 0x0F, 0x0C, 0x0E, 0x06, 0x07, 0xE7, 0xFF, 0x0F, 0x01, 0x80,
|
|
||||||
0xC0, 0x60, 0x30, 0x6F, 0xF3, 0xF0, 0x0C, 0x38, 0x61, 0xE7, 0xFC, 0xF0, 0x60, 0xC4, 0xFC, 0xF0, 0x0C, 0x0F, 0x07,
|
|
||||||
0x87, 0xE7, 0xFF, 0x0F, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x6F, 0xF3, 0xF0, 0x18, 0x78, 0xF1, 0xE7, 0xFC, 0xF0, 0x60,
|
|
||||||
0xC4, 0xFC, 0xF0, 0x0C, 0x06, 0x0F, 0xCF, 0xFE, 0x1E, 0x03, 0x01, 0x80, 0xC0, 0x60, 0xDF, 0xE7, 0xE0, 0x18, 0x30,
|
|
||||||
0xF3, 0xFE, 0x78, 0x30, 0x62, 0x7E, 0x78, 0x1E, 0x0F, 0x03, 0x07, 0xE7, 0xFF, 0x0F, 0x01, 0x80, 0xC0, 0x60, 0x30,
|
|
||||||
0x6F, 0xF3, 0xF0, 0x3C, 0x78, 0x61, 0xE7, 0xFC, 0xF0, 0x60, 0xC4, 0xFC, 0xF0, 0x78, 0x78, 0x30, 0xFC, 0xFE, 0xC7,
|
|
||||||
0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0xFE, 0xFC, 0x01, 0x83, 0xC1, 0xE7, 0xC7, 0xE7, 0x33, 0x19, 0x8C, 0xC6, 0x3F, 0x0F,
|
|
||||||
0x80, 0x7E, 0x3F, 0x98, 0xEC, 0x3F, 0xDF, 0xED, 0x86, 0xC7, 0x7F, 0x3F, 0x00, 0x06, 0x1F, 0x1F, 0x3E, 0x7E, 0xE6,
|
|
||||||
0xC6, 0xC6, 0xC6, 0x7E, 0x3E, 0x3C, 0x7B, 0xFF, 0xFC, 0x18, 0x3F, 0x7E, 0xC1, 0x83, 0xF7, 0xF0, 0x3C, 0x3C, 0x3C,
|
|
||||||
0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x66, 0xFC, 0xF7, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0,
|
|
||||||
0x66, 0x7E, 0x3C, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x18, 0x33, 0xFF, 0xFC, 0x18, 0x3F, 0x7E, 0xC1,
|
|
||||||
0x83, 0xF7, 0xF0, 0x18, 0x18, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0xFE, 0xFE, 0xC0, 0xC0, 0xFC, 0xFC,
|
|
||||||
0xC0, 0xC0, 0xFC, 0xFE, 0x06, 0x0C, 0x0F, 0x0F, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x06, 0x0C, 0x0F,
|
|
||||||
0x07, 0x3C, 0x78, 0x67, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0, 0x3C, 0x3C, 0x18, 0x3C, 0x7E, 0xE7,
|
|
||||||
0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x0C, 0x0F, 0x07, 0x87, 0xE7, 0xFF, 0x0F, 0x01, 0x8F, 0xC7, 0xE0, 0xF0, 0x6F, 0xF3,
|
|
||||||
0xF0, 0x18, 0x78, 0xF1, 0xF7, 0xFC, 0xF1, 0xE3, 0xC6, 0xFC, 0xF8, 0x30, 0x6F, 0x9E, 0x00, 0x36, 0x1F, 0x07, 0x07,
|
|
||||||
0xE7, 0xFF, 0x0F, 0x01, 0x8F, 0xC7, 0xE0, 0xF0, 0x6F, 0xF3, 0xF0, 0x36, 0x7C, 0x71, 0xF7, 0xFC, 0xF1, 0xE3, 0xC6,
|
|
||||||
0xFC, 0xF8, 0x30, 0x6F, 0x9E, 0x00, 0x0C, 0x06, 0x0F, 0xCF, 0xFE, 0x1E, 0x03, 0x1F, 0x8F, 0xC1, 0xE0, 0xDF, 0xE7,
|
|
||||||
0xE0, 0x18, 0x30, 0xFB, 0xFE, 0x78, 0xF1, 0xE3, 0x7E, 0x7C, 0x18, 0x37, 0xCF, 0x00, 0x3F, 0x3F, 0xF8, 0x78, 0x0C,
|
|
||||||
0x7E, 0x3F, 0x07, 0x83, 0x7F, 0x9F, 0x83, 0x00, 0xC1, 0xC0, 0xE0, 0x18, 0x30, 0x61, 0xF7, 0xFC, 0xF1, 0xE3, 0xC6,
|
|
||||||
0xFC, 0xF8, 0x30, 0x6F, 0x9E, 0x00, 0x18, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3,
|
|
||||||
0x31, 0xE7, 0xB0, 0xC3, 0xEF, 0xFB, 0xCF, 0x3C, 0xF3, 0xCC, 0x61, 0xBF, 0xFF, 0xFD, 0x86, 0x7F, 0x9F, 0xE6, 0x19,
|
|
||||||
0x86, 0x61, 0x98, 0x60, 0x61, 0xF3, 0xE3, 0xE7, 0xEE, 0xD9, 0xB3, 0x66, 0xCD, 0x98, 0x7F, 0xEC, 0xC6, 0x31, 0x8C,
|
|
||||||
0x63, 0x18, 0xC6, 0x00, 0x7F, 0xEC, 0x86, 0x31, 0x8C, 0x63, 0x18, 0xFF, 0x66, 0x66, 0x66, 0x66, 0x66, 0xFF, 0x46,
|
|
||||||
0x66, 0x66, 0x66, 0x9F, 0xDC, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, 0x9F, 0xDC, 0x86, 0x31, 0x8C, 0x63, 0x18,
|
|
||||||
0x66, 0x66, 0x66, 0x66, 0x66, 0x6C, 0xF6, 0x66, 0x46, 0x66, 0x66, 0x66, 0x6C, 0xF6, 0xEF, 0xFF, 0xFF, 0xBF, 0xFF,
|
|
||||||
0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE3, 0xF3, 0xFF, 0xDE, 0xDE, 0xE7, 0xBD, 0xEF, 0x7B, 0xDE, 0xC6, 0x33, 0x10,
|
|
||||||
0x0C, 0x3C, 0x78, 0x60, 0xC1, 0x83, 0x06, 0x0D, 0x1B, 0x37, 0xE7, 0x80, 0x6F, 0xB4, 0x66, 0x66, 0x66, 0x66, 0x6C,
|
|
||||||
0x80, 0xC3, 0xC7, 0xCE, 0xDC, 0xF8, 0xF8, 0xDC, 0xCC, 0xC6, 0xC3, 0x38, 0x1C, 0x38, 0x30, 0xC3, 0x0C, 0xF7, 0xFB,
|
|
||||||
0xCE, 0x3C, 0xDB, 0x37, 0x0E, 0x71, 0x80, 0x8F, 0x7F, 0xBC, 0xE3, 0xCD, 0xB3, 0x30, 0xE1, 0x86, 0x0C, 0x18, 0x30,
|
|
||||||
0x60, 0xC1, 0x83, 0x07, 0xEF, 0xE0, 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xFD,
|
|
||||||
0xFD, 0xC1, 0xC7, 0x0C, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0xEF, 0xEC, 0x0D, 0x9B, 0x36, 0x6C, 0x18, 0x30, 0x60,
|
|
||||||
0xC1, 0xFB, 0xF8, 0x3F, 0xFF, 0xCC, 0xCC, 0xCC, 0xC0, 0xC1, 0x83, 0x06, 0x0C, 0xD9, 0xB0, 0x60, 0xFD, 0xFC, 0xCC,
|
|
||||||
0xCC, 0xFF, 0xCC, 0xCC, 0x60, 0x60, 0x78, 0x78, 0xF0, 0xE0, 0x60, 0x60, 0x7E, 0x7F, 0x66, 0x67, 0xFE, 0x66, 0x66,
|
|
||||||
0x0C, 0x1C, 0x18, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x18, 0x63, 0x8C, 0x7B, 0xFC, 0xF3,
|
|
||||||
0xCF, 0x3C, 0xF3, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x38, 0x1C, 0x38, 0x30, 0x7B, 0xFC,
|
|
||||||
0xF3, 0xCF, 0x3C, 0xF3, 0x70, 0xE7, 0x18, 0x3C, 0x3C, 0x18, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7,
|
|
||||||
0xC3, 0x79, 0xE3, 0x1E, 0xFF, 0x3C, 0xF3, 0xCF, 0x3C, 0xC0, 0x81, 0x83, 0x05, 0xE7, 0xEC, 0xD9, 0xB3, 0x66, 0xCD,
|
|
||||||
0x98, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x03, 0x03, 0x03, 0x7B, 0xFC, 0xF3, 0xCF, 0x3C,
|
|
||||||
0xF3, 0x0C, 0x31, 0x84, 0x3E, 0x1F, 0x0F, 0x8F, 0xEE, 0x3E, 0x0F, 0x07, 0x83, 0xC1, 0xE1, 0xDF, 0xC7, 0xC0, 0x3C,
|
|
||||||
0x3C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x36, 0x1F, 0x07, 0x07, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1,
|
|
||||||
0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x36, 0x3E, 0x1C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x1B, 0x1F, 0x8D,
|
|
||||||
0x87, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x1E, 0x3E, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3,
|
|
||||||
0xC3, 0xC7, 0x7E, 0x3C, 0x3F, 0xFB, 0xFF, 0xF9, 0xC1, 0x86, 0x0C, 0x3F, 0x61, 0xFB, 0x0C, 0x18, 0x60, 0xC7, 0x03,
|
|
||||||
0xFF, 0x0F, 0xFC, 0x3D, 0xE3, 0xFF, 0xB9, 0xCF, 0x87, 0xFC, 0x3F, 0xE3, 0x85, 0xFF, 0xE7, 0xBE, 0x18, 0x38, 0x30,
|
|
||||||
0xFE, 0xFF, 0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0x33, 0x98, 0xEF, 0xEF, 0x18, 0xC6, 0x30, 0xFE, 0xFF,
|
|
||||||
0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0x38, 0x1C, 0x38, 0x30, 0x77, 0xF7, 0x8C, 0x63, 0x18, 0xE3, 0xB9,
|
|
||||||
0x80, 0x3C, 0x3C, 0x18, 0xFE, 0xFF, 0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0xF7, 0x98, 0xEF, 0xEF, 0x18,
|
|
||||||
0xC6, 0x30, 0x18, 0x70, 0xC3, 0xEF, 0xF8, 0xFC, 0x3E, 0x1E, 0x0E, 0x1F, 0xF7, 0xC0, 0x18, 0x70, 0xC3, 0xEF, 0xF8,
|
|
||||||
0xFF, 0x3F, 0x87, 0xFD, 0xF0, 0x18, 0x78, 0xF3, 0xEF, 0xF8, 0xFC, 0x3E, 0x1E, 0x0E, 0x1F, 0xF7, 0xC0, 0x18, 0x78,
|
|
||||||
0xF3, 0xEF, 0xF8, 0xFF, 0x3F, 0x87, 0xFD, 0xF0, 0x7D, 0xFF, 0x1F, 0x87, 0xC3, 0xC1, 0xC3, 0xFE, 0xF8, 0xC0, 0xC7,
|
|
||||||
0x0C, 0x00, 0x7D, 0xFF, 0x1F, 0xE7, 0xF0, 0xFF, 0xBE, 0x18, 0x18, 0xE1, 0x80, 0x3C, 0x78, 0x63, 0xEF, 0xF8, 0xFC,
|
|
||||||
0x3E, 0x1E, 0x0E, 0x1F, 0xF7, 0xC0, 0x3C, 0x78, 0x63, 0xEF, 0xF8, 0xFF, 0x3F, 0x87, 0xFD, 0xF0, 0xFF, 0xFF, 0xC6,
|
|
||||||
0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x01, 0x83, 0x81, 0x80, 0x63, 0x3D, 0xE6, 0x31, 0x8C, 0x71,
|
|
||||||
0xCC, 0x37, 0x30, 0x3C, 0x3C, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x0D, 0xB6, 0xFF,
|
|
||||||
0xF1, 0x86, 0x18, 0x61, 0xC3, 0x80, 0xFF, 0xFF, 0x18, 0x18, 0x7E, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x63, 0x3D, 0xE6,
|
|
||||||
0x73, 0xCC, 0x71, 0xC0, 0x1E, 0x3E, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3D, 0xF7,
|
|
||||||
0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C,
|
|
||||||
0x79, 0xE8, 0xF3, 0xCF, 0x3C, 0xF3, 0xFD, 0xE0, 0x36, 0x3E, 0x1C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7,
|
|
||||||
0x7E, 0x3C, 0x6D, 0xF3, 0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x18, 0x3C, 0x3C, 0xDB, 0xC3, 0xC3, 0xC3, 0xC3,
|
|
||||||
0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x31, 0xE7, 0xAF, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x1E, 0x3E, 0x3C, 0xC3, 0xC3,
|
|
||||||
0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3D, 0xF7, 0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0xC3, 0xC3,
|
|
||||||
0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x30, 0x3C, 0x1C, 0x8D, 0x9B, 0x36, 0x6C, 0xD9, 0xBF, 0x3E,
|
|
||||||
0x0C, 0x30, 0x78, 0x70, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x20, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x79, 0xD9, 0xE6, 0x6F,
|
|
||||||
0xF8, 0xF3, 0xC3, 0xCF, 0x0F, 0x3C, 0x18, 0x60, 0x0C, 0x07, 0x81, 0xE2, 0x03, 0xCC, 0xF7, 0xF7, 0xF9, 0xFE, 0x7F,
|
|
||||||
0x8C, 0xC3, 0x30, 0x18, 0x3C, 0x3C, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x3C,
|
|
||||||
0x83, 0xC7, 0x66, 0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0x3C, 0x3C, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18,
|
|
||||||
0x18, 0x18, 0x18, 0x18, 0x0C, 0x1C, 0x18, 0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xFE, 0xFF, 0x0C, 0x38,
|
|
||||||
0x67, 0xFF, 0xE3, 0x8E, 0x38, 0xE1, 0xFB, 0xF8, 0x0C, 0x0C, 0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xFE,
|
|
||||||
0xFF, 0x18, 0x33, 0xFF, 0xF1, 0xC7, 0x1C, 0x70, 0xFD, 0xFC, 0x3C, 0x3C, 0x18, 0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38,
|
|
||||||
0x70, 0xE0, 0xFE, 0xFF, 0x3C, 0x78, 0x67, 0xFF, 0xE3, 0x8E, 0x38, 0xE1, 0xFB, 0xF8, 0x1E, 0x3F, 0x73, 0xFE, 0xFE,
|
|
||||||
0xFE, 0xFE, 0x62, 0x3F, 0x1E,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const EpdGlyph babyblueGlyphs[] = {
|
|
||||||
{0, 0, 5, 0, 0, 0, 0}, //
|
|
||||||
{2, 10, 3, 1, 10, 3, 0}, // !
|
|
||||||
{4, 5, 5, 1, 11, 3, 3}, // "
|
|
||||||
{9, 11, 10, 0, 11, 13, 6}, // #
|
|
||||||
{8, 14, 9, 0, 12, 14, 19}, // $
|
|
||||||
{11, 11, 13, 1, 11, 16, 33}, // %
|
|
||||||
{9, 11, 11, 1, 11, 13, 49}, // &
|
|
||||||
{2, 4, 3, 1, 11, 1, 62}, // '
|
|
||||||
{4, 15, 5, 1, 11, 8, 63}, // (
|
|
||||||
{4, 15, 5, 1, 11, 8, 71}, // )
|
|
||||||
{6, 6, 6, 0, 11, 5, 79}, // *
|
|
||||||
{8, 8, 9, 0, 9, 8, 84}, // +
|
|
||||||
{2, 5, 3, 1, 3, 2, 92}, // ,
|
|
||||||
{5, 2, 5, 0, 5, 2, 94}, // -
|
|
||||||
{2, 2, 3, 1, 2, 1, 96}, // .
|
|
||||||
{6, 11, 6, 0, 11, 9, 97}, // /
|
|
||||||
{8, 11, 9, 0, 11, 11, 106}, // 0
|
|
||||||
{5, 11, 6, 1, 11, 7, 117}, // 1
|
|
||||||
{8, 11, 9, 0, 11, 11, 124}, // 2
|
|
||||||
{8, 11, 10, 1, 11, 11, 135}, // 3
|
|
||||||
{8, 11, 9, 0, 11, 11, 146}, // 4
|
|
||||||
{7, 11, 9, 1, 11, 10, 157}, // 5
|
|
||||||
{8, 11, 9, 0, 11, 11, 167}, // 6
|
|
||||||
{9, 11, 10, 0, 11, 13, 178}, // 7
|
|
||||||
{8, 11, 9, 0, 11, 11, 191}, // 8
|
|
||||||
{8, 11, 9, 0, 11, 11, 202}, // 9
|
|
||||||
{2, 8, 3, 1, 8, 2, 213}, // :
|
|
||||||
{2, 11, 3, 1, 8, 3, 215}, // ;
|
|
||||||
{7, 8, 9, 1, 9, 7, 218}, // <
|
|
||||||
{8, 5, 9, 0, 8, 5, 225}, // =
|
|
||||||
{7, 8, 7, 0, 9, 7, 230}, // >
|
|
||||||
{7, 11, 9, 1, 11, 10, 237}, // ?
|
|
||||||
{14, 15, 15, 0, 11, 27, 247}, // @
|
|
||||||
{7, 10, 9, 1, 10, 9, 274}, // A
|
|
||||||
{8, 10, 10, 1, 10, 10, 283}, // B
|
|
||||||
{9, 10, 11, 1, 10, 12, 293}, // C
|
|
||||||
{8, 10, 10, 1, 10, 10, 305}, // D
|
|
||||||
{7, 10, 9, 1, 10, 9, 315}, // E
|
|
||||||
{7, 10, 9, 1, 10, 9, 324}, // F
|
|
||||||
{9, 10, 11, 1, 10, 12, 333}, // G
|
|
||||||
{8, 10, 10, 1, 10, 10, 345}, // H
|
|
||||||
{2, 10, 3, 1, 10, 3, 355}, // I
|
|
||||||
{6, 10, 6, 0, 10, 8, 358}, // J
|
|
||||||
{8, 10, 10, 1, 10, 10, 366}, // K
|
|
||||||
{7, 10, 9, 1, 10, 9, 376}, // L
|
|
||||||
{10, 10, 12, 1, 10, 13, 385}, // M
|
|
||||||
{8, 10, 10, 1, 10, 10, 398}, // N
|
|
||||||
{9, 10, 11, 1, 10, 12, 408}, // O
|
|
||||||
{8, 10, 10, 1, 10, 10, 420}, // P
|
|
||||||
{9, 10, 11, 1, 10, 12, 430}, // Q
|
|
||||||
{8, 10, 10, 1, 10, 10, 442}, // R
|
|
||||||
{7, 10, 9, 1, 10, 9, 452}, // S
|
|
||||||
{8, 10, 9, 0, 10, 10, 461}, // T
|
|
||||||
{8, 10, 10, 1, 10, 10, 471}, // U
|
|
||||||
{8, 10, 10, 1, 10, 10, 481}, // V
|
|
||||||
{14, 10, 15, 0, 10, 18, 491}, // W
|
|
||||||
{10, 10, 11, 0, 10, 13, 509}, // X
|
|
||||||
{8, 10, 10, 1, 10, 10, 522}, // Y
|
|
||||||
{8, 10, 9, 0, 10, 10, 532}, // Z
|
|
||||||
{4, 14, 5, 1, 10, 7, 542}, // [
|
|
||||||
{7, 10, 7, 0, 10, 9, 549}, // <backslash>
|
|
||||||
{4, 14, 4, 0, 10, 7, 558}, // ]
|
|
||||||
{8, 7, 9, 0, 11, 7, 565}, // ^
|
|
||||||
{8, 2, 9, 0, -2, 2, 572}, // _
|
|
||||||
{3, 3, 3, 0, 11, 2, 574}, // `
|
|
||||||
{7, 8, 7, 0, 8, 7, 576}, // a
|
|
||||||
{7, 10, 9, 1, 10, 9, 583}, // b
|
|
||||||
{7, 8, 7, 0, 8, 7, 592}, // c
|
|
||||||
{7, 10, 7, 0, 10, 9, 599}, // d
|
|
||||||
{8, 8, 9, 0, 8, 8, 608}, // e
|
|
||||||
{5, 11, 5, 0, 11, 7, 616}, // f
|
|
||||||
{7, 12, 7, 0, 8, 11, 623}, // g
|
|
||||||
{6, 10, 7, 1, 10, 8, 634}, // h
|
|
||||||
{2, 10, 3, 1, 10, 3, 642}, // i
|
|
||||||
{3, 14, 3, 0, 10, 6, 645}, // j
|
|
||||||
{6, 10, 7, 1, 10, 8, 651}, // k
|
|
||||||
{2, 10, 3, 1, 10, 3, 659}, // l
|
|
||||||
{10, 8, 12, 1, 8, 10, 662}, // m
|
|
||||||
{6, 8, 7, 1, 8, 6, 672}, // n
|
|
||||||
{8, 8, 9, 0, 8, 8, 678}, // o
|
|
||||||
{7, 11, 9, 1, 8, 10, 686}, // p
|
|
||||||
{7, 11, 7, 0, 8, 10, 696}, // q
|
|
||||||
{5, 8, 6, 1, 8, 5, 706}, // r
|
|
||||||
{7, 8, 7, 0, 8, 7, 711}, // s
|
|
||||||
{5, 10, 5, 0, 10, 7, 718}, // t
|
|
||||||
{6, 8, 7, 1, 8, 6, 725}, // u
|
|
||||||
{8, 8, 9, 0, 8, 8, 731}, // v
|
|
||||||
{10, 8, 11, 0, 8, 10, 739}, // w
|
|
||||||
{8, 8, 9, 0, 8, 8, 749}, // x
|
|
||||||
{8, 11, 9, 0, 8, 11, 757}, // y
|
|
||||||
{7, 8, 7, 0, 8, 7, 768}, // z
|
|
||||||
{5, 15, 5, 0, 11, 10, 775}, // {
|
|
||||||
{2, 15, 3, 1, 11, 4, 785}, // |
|
|
||||||
{6, 15, 6, 0, 11, 12, 789}, // }
|
|
||||||
{9, 3, 10, 0, 7, 4, 801}, // ~
|
|
||||||
{2, 12, 4, 2, 8, 3, 805}, // ¡
|
|
||||||
{7, 13, 7, 0, 10, 12, 808}, // ¢
|
|
||||||
{8, 10, 9, 0, 10, 10, 820}, // £
|
|
||||||
{8, 8, 9, 0, 9, 8, 830}, // ¤
|
|
||||||
{8, 10, 9, 0, 10, 10, 838}, // ¥
|
|
||||||
{2, 15, 3, 1, 11, 4, 848}, // ¦
|
|
||||||
{8, 15, 9, 0, 11, 15, 852}, // §
|
|
||||||
{5, 3, 5, 0, 11, 2, 867}, // ¨
|
|
||||||
{10, 11, 11, 0, 11, 14, 869}, // ©
|
|
||||||
{5, 6, 5, 0, 11, 4, 883}, // ª
|
|
||||||
{7, 7, 9, 1, 8, 7, 887}, // «
|
|
||||||
{8, 5, 9, 0, 8, 5, 894}, // ¬
|
|
||||||
{10, 11, 11, 0, 11, 14, 899}, // ®
|
|
||||||
{8, 2, 9, 0, 12, 2, 913}, // ¯
|
|
||||||
{4, 5, 5, 1, 11, 3, 915}, // °
|
|
||||||
{8, 9, 9, 0, 9, 9, 918}, // ±
|
|
||||||
{5, 8, 5, 0, 11, 5, 927}, // ²
|
|
||||||
{5, 6, 5, 0, 11, 4, 932}, // ³
|
|
||||||
{3, 3, 4, 1, 11, 2, 936}, // ´
|
|
||||||
{6, 12, 9, 2, 8, 9, 938}, // µ
|
|
||||||
{8, 14, 9, 0, 10, 14, 947}, // ¶
|
|
||||||
{2, 2, 3, 1, 6, 1, 961}, // ·
|
|
||||||
{4, 4, 4, 0, 0, 2, 962}, // ¸
|
|
||||||
{4, 7, 4, 0, 12, 4, 964}, // ¹
|
|
||||||
{6, 6, 6, 0, 11, 5, 968}, // º
|
|
||||||
{7, 7, 9, 1, 8, 7, 973}, // »
|
|
||||||
{12, 12, 13, 0, 12, 18, 980}, // ¼
|
|
||||||
{12, 11, 13, 0, 11, 17, 998}, // ½
|
|
||||||
{12, 11, 13, 0, 11, 17, 1015}, // ¾
|
|
||||||
{7, 12, 9, 1, 8, 11, 1032}, // ¿
|
|
||||||
{7, 13, 9, 1, 13, 12, 1043}, // À
|
|
||||||
{7, 13, 9, 1, 13, 12, 1055}, // Á
|
|
||||||
{7, 13, 9, 1, 13, 12, 1067}, // Â
|
|
||||||
{7, 13, 9, 1, 13, 12, 1079}, // Ã
|
|
||||||
{7, 12, 9, 1, 12, 11, 1091}, // Ä
|
|
||||||
{7, 13, 9, 1, 13, 12, 1102}, // Å
|
|
||||||
{13, 11, 14, 0, 11, 18, 1114}, // Æ
|
|
||||||
{9, 14, 11, 1, 10, 16, 1132}, // Ç
|
|
||||||
{7, 13, 9, 1, 13, 12, 1148}, // È
|
|
||||||
{7, 13, 9, 1, 13, 12, 1160}, // É
|
|
||||||
{7, 13, 9, 1, 13, 12, 1172}, // Ê
|
|
||||||
{7, 12, 9, 1, 12, 11, 1184}, // Ë
|
|
||||||
{3, 13, 3, 0, 13, 5, 1195}, // Ì
|
|
||||||
{3, 13, 4, 1, 13, 5, 1200}, // Í
|
|
||||||
{4, 13, 4, 0, 13, 7, 1205}, // Î
|
|
||||||
{4, 12, 4, 0, 12, 6, 1212}, // Ï
|
|
||||||
{9, 10, 10, 0, 10, 12, 1218}, // Ð
|
|
||||||
{8, 13, 10, 1, 13, 13, 1230}, // Ñ
|
|
||||||
{9, 13, 11, 1, 13, 15, 1243}, // Ò
|
|
||||||
{9, 13, 11, 1, 13, 15, 1258}, // Ó
|
|
||||||
{9, 13, 11, 1, 13, 15, 1273}, // Ô
|
|
||||||
{9, 13, 11, 1, 13, 15, 1288}, // Õ
|
|
||||||
{9, 12, 11, 1, 12, 14, 1303}, // Ö
|
|
||||||
{6, 6, 7, 1, 8, 5, 1317}, // ×
|
|
||||||
{10, 10, 11, 0, 10, 13, 1322}, // Ø
|
|
||||||
{8, 13, 10, 1, 13, 13, 1335}, // Ù
|
|
||||||
{8, 13, 10, 1, 13, 13, 1348}, // Ú
|
|
||||||
{8, 13, 10, 1, 13, 13, 1361}, // Û
|
|
||||||
{8, 12, 10, 1, 12, 12, 1374}, // Ü
|
|
||||||
{8, 13, 10, 1, 13, 13, 1386}, // Ý
|
|
||||||
{8, 10, 10, 1, 10, 10, 1399}, // Þ
|
|
||||||
{7, 11, 9, 1, 11, 10, 1409}, // ß
|
|
||||||
{7, 11, 7, 0, 11, 10, 1419}, // à
|
|
||||||
{7, 11, 7, 0, 11, 10, 1429}, // á
|
|
||||||
{7, 11, 7, 0, 11, 10, 1439}, // â
|
|
||||||
{7, 11, 7, 0, 11, 10, 1449}, // ã
|
|
||||||
{7, 10, 7, 0, 10, 9, 1459}, // ä
|
|
||||||
{7, 12, 7, 0, 12, 11, 1468}, // å
|
|
||||||
{12, 8, 13, 0, 8, 12, 1479}, // æ
|
|
||||||
{7, 12, 7, 0, 8, 11, 1491}, // ç
|
|
||||||
{8, 11, 9, 0, 11, 11, 1502}, // è
|
|
||||||
{8, 11, 9, 0, 11, 11, 1513}, // é
|
|
||||||
{8, 11, 9, 0, 11, 11, 1524}, // ê
|
|
||||||
{8, 10, 9, 0, 10, 10, 1535}, // ë
|
|
||||||
{3, 11, 3, 0, 11, 5, 1545}, // ì
|
|
||||||
{3, 11, 4, 1, 11, 5, 1550}, // í
|
|
||||||
{4, 11, 4, 0, 11, 6, 1555}, // î
|
|
||||||
{4, 10, 4, 0, 10, 5, 1561}, // ï
|
|
||||||
{8, 11, 9, 0, 11, 11, 1566}, // ð
|
|
||||||
{6, 11, 7, 1, 11, 9, 1577}, // ñ
|
|
||||||
{8, 11, 9, 0, 11, 11, 1586}, // ò
|
|
||||||
{8, 11, 9, 0, 11, 11, 1597}, // ó
|
|
||||||
{8, 11, 9, 0, 11, 11, 1608}, // ô
|
|
||||||
{8, 11, 9, 0, 11, 11, 1619}, // õ
|
|
||||||
{8, 10, 9, 0, 10, 10, 1630}, // ö
|
|
||||||
{8, 6, 9, 0, 8, 6, 1640}, // ÷
|
|
||||||
{8, 8, 9, 0, 8, 8, 1646}, // ø
|
|
||||||
{6, 11, 7, 1, 11, 9, 1654}, // ù
|
|
||||||
{6, 11, 7, 1, 11, 9, 1663}, // ú
|
|
||||||
{6, 11, 7, 1, 11, 9, 1672}, // û
|
|
||||||
{6, 10, 7, 1, 10, 8, 1681}, // ü
|
|
||||||
{8, 14, 9, 0, 11, 14, 1689}, // ý
|
|
||||||
{7, 13, 9, 1, 10, 12, 1703}, // þ
|
|
||||||
{8, 13, 9, 0, 10, 13, 1715}, // ÿ
|
|
||||||
{7, 12, 9, 1, 12, 11, 1728}, // Ā
|
|
||||||
{7, 10, 7, 0, 10, 9, 1739}, // ā
|
|
||||||
{7, 13, 9, 1, 13, 12, 1748}, // Ă
|
|
||||||
{7, 11, 7, 0, 11, 10, 1760}, // ă
|
|
||||||
{8, 14, 10, 1, 10, 14, 1770}, // Ą
|
|
||||||
{8, 12, 9, 0, 8, 12, 1784}, // ą
|
|
||||||
{9, 13, 11, 1, 13, 15, 1796}, // Ć
|
|
||||||
{7, 11, 7, 0, 11, 10, 1811}, // ć
|
|
||||||
{9, 13, 11, 1, 13, 15, 1821}, // Ĉ
|
|
||||||
{7, 11, 7, 0, 11, 10, 1836}, // ĉ
|
|
||||||
{9, 12, 11, 1, 12, 14, 1846}, // Ċ
|
|
||||||
{7, 10, 7, 0, 10, 9, 1860}, // ċ
|
|
||||||
{9, 13, 11, 1, 13, 15, 1869}, // Č
|
|
||||||
{7, 11, 7, 0, 11, 10, 1884}, // č
|
|
||||||
{8, 13, 10, 1, 13, 13, 1894}, // Ď
|
|
||||||
{9, 11, 10, 0, 11, 13, 1907}, // ď
|
|
||||||
{9, 10, 10, 0, 10, 12, 1920}, // Đ
|
|
||||||
{8, 11, 9, 0, 11, 11, 1932}, // đ
|
|
||||||
{7, 12, 9, 1, 12, 11, 1943}, // Ē
|
|
||||||
{8, 10, 9, 0, 10, 10, 1954}, // ē
|
|
||||||
{7, 13, 9, 1, 13, 12, 1964}, // Ĕ
|
|
||||||
{8, 11, 9, 0, 11, 11, 1976}, // ĕ
|
|
||||||
{7, 12, 9, 1, 12, 11, 1987}, // Ė
|
|
||||||
{8, 10, 9, 0, 10, 10, 1998}, // ė
|
|
||||||
{8, 14, 10, 1, 10, 14, 2008}, // Ę
|
|
||||||
{8, 12, 9, 0, 8, 12, 2022}, // ę
|
|
||||||
{7, 13, 9, 1, 13, 12, 2034}, // Ě
|
|
||||||
{8, 11, 9, 0, 11, 11, 2046}, // ě
|
|
||||||
{9, 13, 11, 1, 13, 15, 2057}, // Ĝ
|
|
||||||
{7, 15, 7, 0, 11, 14, 2072}, // ĝ
|
|
||||||
{9, 13, 11, 1, 13, 15, 2086}, // Ğ
|
|
||||||
{7, 15, 7, 0, 11, 14, 2101}, // ğ
|
|
||||||
{9, 12, 11, 1, 12, 14, 2115}, // Ġ
|
|
||||||
{7, 14, 7, 0, 10, 13, 2129}, // ġ
|
|
||||||
{9, 14, 11, 1, 10, 16, 2142}, // Ģ
|
|
||||||
{7, 15, 7, 0, 11, 14, 2158}, // ģ
|
|
||||||
{8, 13, 10, 1, 13, 13, 2172}, // Ĥ
|
|
||||||
{6, 13, 7, 1, 13, 10, 2185}, // ĥ
|
|
||||||
{10, 10, 11, 0, 10, 13, 2195}, // Ħ
|
|
||||||
{7, 11, 7, 0, 11, 10, 2208}, // ħ
|
|
||||||
{5, 13, 5, 0, 13, 9, 2218}, // Ĩ
|
|
||||||
{5, 11, 5, 0, 11, 7, 2227}, // ĩ
|
|
||||||
{4, 12, 4, 0, 12, 6, 2234}, // Ī
|
|
||||||
{4, 10, 4, 0, 10, 5, 2240}, // ī
|
|
||||||
{5, 13, 5, 0, 13, 9, 2245}, // Ĭ
|
|
||||||
{5, 11, 5, 0, 11, 7, 2254}, // ĭ
|
|
||||||
{4, 14, 4, 0, 10, 7, 2261}, // Į
|
|
||||||
{4, 14, 4, 0, 10, 7, 2268}, // į
|
|
||||||
{2, 12, 3, 1, 12, 3, 2275}, // İ
|
|
||||||
{2, 8, 3, 1, 8, 2, 2278}, // ı
|
|
||||||
{8, 10, 10, 1, 10, 10, 2280}, // IJ
|
|
||||||
{5, 14, 6, 1, 10, 9, 2290}, // ij
|
|
||||||
{7, 13, 7, 0, 13, 12, 2299}, // Ĵ
|
|
||||||
{4, 15, 4, 0, 11, 8, 2311}, // ĵ
|
|
||||||
{8, 14, 10, 1, 10, 14, 2319}, // Ķ
|
|
||||||
{6, 14, 7, 1, 10, 11, 2333}, // ķ
|
|
||||||
{6, 8, 7, 1, 8, 6, 2344}, // ĸ
|
|
||||||
{7, 13, 9, 1, 13, 12, 2350}, // Ĺ
|
|
||||||
{3, 13, 4, 1, 13, 5, 2362}, // ĺ
|
|
||||||
{7, 14, 9, 1, 10, 13, 2367}, // Ļ
|
|
||||||
{4, 14, 4, 0, 10, 7, 2380}, // ļ
|
|
||||||
{7, 11, 9, 1, 11, 10, 2387}, // Ľ
|
|
||||||
{4, 11, 5, 1, 11, 6, 2397}, // ľ
|
|
||||||
{7, 10, 9, 1, 10, 9, 2403}, // Ŀ
|
|
||||||
{4, 10, 5, 1, 10, 5, 2412}, // ŀ
|
|
||||||
{8, 10, 9, 0, 10, 10, 2417}, // Ł
|
|
||||||
{4, 10, 4, 0, 10, 5, 2427}, // ł
|
|
||||||
{8, 13, 10, 1, 13, 13, 2432}, // Ń
|
|
||||||
{6, 12, 7, 1, 12, 9, 2445}, // ń
|
|
||||||
{8, 14, 10, 1, 10, 14, 2454}, // Ņ
|
|
||||||
{6, 12, 7, 1, 8, 9, 2468}, // ņ
|
|
||||||
{8, 13, 10, 1, 13, 13, 2477}, // Ň
|
|
||||||
{6, 11, 7, 1, 11, 9, 2490}, // ň
|
|
||||||
{7, 11, 7, 0, 11, 10, 2499}, // ʼn
|
|
||||||
{8, 13, 10, 1, 10, 13, 2509}, // Ŋ
|
|
||||||
{6, 12, 7, 1, 8, 9, 2522}, // ŋ
|
|
||||||
{9, 12, 11, 1, 12, 14, 2531}, // Ō
|
|
||||||
{8, 10, 9, 0, 10, 10, 2545}, // ō
|
|
||||||
{9, 13, 11, 1, 13, 15, 2555}, // Ŏ
|
|
||||||
{8, 11, 9, 0, 11, 11, 2570}, // ŏ
|
|
||||||
{9, 13, 11, 1, 13, 15, 2581}, // Ő
|
|
||||||
{8, 11, 9, 0, 11, 11, 2596}, // ő
|
|
||||||
{13, 11, 15, 1, 11, 18, 2607}, // Œ
|
|
||||||
{13, 8, 14, 0, 8, 13, 2625}, // œ
|
|
||||||
{8, 13, 10, 1, 13, 13, 2638}, // Ŕ
|
|
||||||
{5, 11, 6, 1, 11, 7, 2651}, // ŕ
|
|
||||||
{8, 14, 10, 1, 10, 14, 2658}, // Ŗ
|
|
||||||
{5, 12, 6, 1, 8, 8, 2672}, // ŗ
|
|
||||||
{8, 13, 10, 1, 13, 13, 2680}, // Ř
|
|
||||||
{5, 11, 6, 1, 11, 7, 2693}, // ř
|
|
||||||
{7, 13, 9, 1, 13, 12, 2700}, // Ś
|
|
||||||
{7, 11, 7, 0, 11, 10, 2712}, // ś
|
|
||||||
{7, 13, 9, 1, 13, 12, 2722}, // Ŝ
|
|
||||||
{7, 11, 7, 0, 11, 10, 2734}, // ŝ
|
|
||||||
{7, 14, 9, 1, 10, 13, 2744}, // Ş
|
|
||||||
{7, 12, 7, 0, 8, 11, 2757}, // ş
|
|
||||||
{7, 13, 9, 1, 13, 12, 2768}, // Š
|
|
||||||
{7, 11, 7, 0, 11, 10, 2780}, // š
|
|
||||||
{9, 14, 10, 0, 10, 16, 2790}, // Ţ
|
|
||||||
{5, 14, 5, 0, 10, 9, 2806}, // ţ
|
|
||||||
{8, 13, 9, 0, 13, 13, 2815}, // Ť
|
|
||||||
{6, 11, 6, 0, 11, 9, 2828}, // ť
|
|
||||||
{8, 10, 9, 0, 10, 10, 2837}, // Ŧ
|
|
||||||
{5, 10, 5, 0, 10, 7, 2847}, // ŧ
|
|
||||||
{8, 13, 10, 1, 13, 13, 2854}, // Ũ
|
|
||||||
{6, 11, 7, 1, 11, 9, 2867}, // ũ
|
|
||||||
{8, 12, 10, 1, 12, 12, 2876}, // Ū
|
|
||||||
{6, 10, 7, 1, 10, 8, 2888}, // ū
|
|
||||||
{8, 13, 10, 1, 13, 13, 2896}, // Ŭ
|
|
||||||
{6, 11, 7, 1, 11, 9, 2909}, // ŭ
|
|
||||||
{8, 13, 10, 1, 13, 13, 2918}, // Ů
|
|
||||||
{6, 11, 7, 1, 11, 9, 2931}, // ů
|
|
||||||
{8, 13, 10, 1, 13, 13, 2940}, // Ű
|
|
||||||
{6, 11, 7, 1, 11, 9, 2953}, // ű
|
|
||||||
{8, 14, 10, 1, 10, 14, 2962}, // Ų
|
|
||||||
{7, 12, 9, 1, 8, 11, 2976}, // ų
|
|
||||||
{14, 13, 15, 0, 13, 23, 2987}, // Ŵ
|
|
||||||
{10, 11, 11, 0, 11, 14, 3010}, // ŵ
|
|
||||||
{8, 13, 10, 1, 13, 13, 3024}, // Ŷ
|
|
||||||
{8, 14, 9, 0, 11, 14, 3037}, // ŷ
|
|
||||||
{8, 12, 10, 1, 12, 12, 3051}, // Ÿ
|
|
||||||
{8, 13, 9, 0, 13, 13, 3063}, // Ź
|
|
||||||
{7, 11, 7, 0, 11, 10, 3076}, // ź
|
|
||||||
{8, 12, 9, 0, 12, 12, 3086}, // Ż
|
|
||||||
{7, 10, 7, 0, 10, 9, 3098}, // ż
|
|
||||||
{8, 13, 9, 0, 13, 13, 3107}, // Ž
|
|
||||||
{7, 11, 7, 0, 11, 10, 3120}, // ž
|
|
||||||
{8, 10, 9, 0, 10, 10, 3130}, // €
|
|
||||||
};
|
|
||||||
|
|
||||||
static const EpdUnicodeInterval babyblueIntervals[] = {
|
|
||||||
{0x20, 0x7E, 0x0}, {0xA1, 0xAC, 0x5F}, {0xAE, 0xFF, 0x6B}, {0x100, 0x17E, 0xBD}, {0x20AC, 0x20AC, 0x13C},
|
|
||||||
};
|
|
||||||
|
|
||||||
static const EpdFontData babyblue = {
|
|
||||||
babyblueBitmaps, babyblueGlyphs, babyblueIntervals, 5, 17, 13, -4, false,
|
|
||||||
};
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
#!python3
|
|
||||||
import freetype
|
|
||||||
import zlib
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
import math
|
|
||||||
import argparse
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
# Originally from https://github.com/vroland/epdiy
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Generate a header file from a font to be used with epdiy.")
|
|
||||||
parser.add_argument("name", action="store", help="name of the font.")
|
|
||||||
parser.add_argument("size", type=int, help="font size to use.")
|
|
||||||
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.")
|
|
||||||
parser.add_argument("--2bit", dest="is2Bit", action="store_true", help="generate 2-bit greyscale bitmap instead of 1-bit black and white.")
|
|
||||||
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
|
|
||||||
|
|
||||||
font_stack = [freetype.Face(f) for f in args.fontstack]
|
|
||||||
is2Bit = args.is2Bit
|
|
||||||
size = args.size
|
|
||||||
font_name = args.name
|
|
||||||
|
|
||||||
# inclusive unicode code point intervals
|
|
||||||
# must not overlap and be in ascending order
|
|
||||||
intervals = [
|
|
||||||
### Basic Latin ###
|
|
||||||
# ASCII letters, digits, punctuation, control characters
|
|
||||||
(0x0000, 0x007F),
|
|
||||||
### Latin-1 Supplement ###
|
|
||||||
# Accented characters for Western European languages
|
|
||||||
(0x0080, 0x00FF),
|
|
||||||
### Latin Extended-A ###
|
|
||||||
# Eastern European and Baltic languages
|
|
||||||
(0x0100, 0x017F),
|
|
||||||
### General Punctuation (core subset) ###
|
|
||||||
# Smart quotes, en dash, em dash, ellipsis, NO-BREAK SPACE
|
|
||||||
(0x2000, 0x206F),
|
|
||||||
### Basic Symbols From "Latin-1 + Misc" ###
|
|
||||||
# dashes, quotes, prime marks
|
|
||||||
(0x2010, 0x203A),
|
|
||||||
# misc punctuation
|
|
||||||
(0x2040, 0x205F),
|
|
||||||
# common currency symbols
|
|
||||||
(0x20A0, 0x20CF),
|
|
||||||
### Combining Diacritical Marks (minimal subset) ###
|
|
||||||
# Needed for proper rendering of many extended Latin languages
|
|
||||||
(0x0300, 0x036F),
|
|
||||||
### Greek & Coptic ###
|
|
||||||
# Used in science, maths, philosophy, some academic texts
|
|
||||||
# (0x0370, 0x03FF),
|
|
||||||
### Cyrillic ###
|
|
||||||
# Russian, Ukrainian, Bulgarian, etc.
|
|
||||||
(0x0400, 0x04FF),
|
|
||||||
### Math Symbols (common subset) ###
|
|
||||||
# General math operators
|
|
||||||
(0x2200, 0x22FF),
|
|
||||||
# Arrows
|
|
||||||
(0x2190, 0x21FF),
|
|
||||||
### CJK ###
|
|
||||||
# Core Unified Ideographs
|
|
||||||
# (0x4E00, 0x9FFF),
|
|
||||||
# # Extension A
|
|
||||||
# (0x3400, 0x4DBF),
|
|
||||||
# # Extension B
|
|
||||||
# (0x20000, 0x2A6DF),
|
|
||||||
# # Extension C–F
|
|
||||||
# (0x2A700, 0x2EBEF),
|
|
||||||
# # Extension G
|
|
||||||
# (0x30000, 0x3134F),
|
|
||||||
# # Hiragana
|
|
||||||
# (0x3040, 0x309F),
|
|
||||||
# # Katakana
|
|
||||||
# (0x30A0, 0x30FF),
|
|
||||||
# # Katakana Phonetic Extensions
|
|
||||||
# (0x31F0, 0x31FF),
|
|
||||||
# # Halfwidth Katakana
|
|
||||||
# (0xFF60, 0xFF9F),
|
|
||||||
# # Hangul Syllables
|
|
||||||
# (0xAC00, 0xD7AF),
|
|
||||||
# # Hangul Jamo
|
|
||||||
# (0x1100, 0x11FF),
|
|
||||||
# # Hangul Compatibility Jamo
|
|
||||||
# (0x3130, 0x318F),
|
|
||||||
# # Hangul Jamo Extended-A
|
|
||||||
# (0xA960, 0xA97F),
|
|
||||||
# # Hangul Jamo Extended-B
|
|
||||||
# (0xD7B0, 0xD7FF),
|
|
||||||
# # CJK Radicals Supplement
|
|
||||||
# (0x2E80, 0x2EFF),
|
|
||||||
# # Kangxi Radicals
|
|
||||||
# (0x2F00, 0x2FDF),
|
|
||||||
# # CJK Symbols and Punctuation
|
|
||||||
# (0x3000, 0x303F),
|
|
||||||
# # CJK Compatibility Forms
|
|
||||||
# (0xFE30, 0xFE4F),
|
|
||||||
# # CJK Compatibility Ideographs
|
|
||||||
# (0xF900, 0xFAFF),
|
|
||||||
]
|
|
||||||
|
|
||||||
add_ints = []
|
|
||||||
if args.additional_intervals:
|
|
||||||
add_ints = [tuple([int(n, base=0) for n in i.split(",")]) for i in args.additional_intervals]
|
|
||||||
|
|
||||||
def norm_floor(val):
|
|
||||||
return int(math.floor(val / (1 << 6)))
|
|
||||||
|
|
||||||
def norm_ceil(val):
|
|
||||||
return int(math.ceil(val / (1 << 6)))
|
|
||||||
|
|
||||||
def chunks(l, n):
|
|
||||||
for i in range(0, len(l), n):
|
|
||||||
yield l[i:i + n]
|
|
||||||
|
|
||||||
def load_glyph(code_point):
|
|
||||||
face_index = 0
|
|
||||||
while face_index < len(font_stack):
|
|
||||||
face = font_stack[face_index]
|
|
||||||
glyph_index = face.get_char_index(code_point)
|
|
||||||
if glyph_index > 0:
|
|
||||||
face.load_glyph(glyph_index, freetype.FT_LOAD_RENDER)
|
|
||||||
return face
|
|
||||||
face_index += 1
|
|
||||||
print(f"code point {code_point} ({hex(code_point)}) not found in font stack!", file=sys.stderr)
|
|
||||||
return None
|
|
||||||
|
|
||||||
unmerged_intervals = sorted(intervals + add_ints)
|
|
||||||
intervals = []
|
|
||||||
unvalidated_intervals = []
|
|
||||||
for i_start, i_end in unmerged_intervals:
|
|
||||||
if len(unvalidated_intervals) > 0 and i_start + 1 <= unvalidated_intervals[-1][1]:
|
|
||||||
unvalidated_intervals[-1] = (unvalidated_intervals[-1][0], max(unvalidated_intervals[-1][1], i_end))
|
|
||||||
continue
|
|
||||||
unvalidated_intervals.append((i_start, i_end))
|
|
||||||
|
|
||||||
for i_start, i_end in unvalidated_intervals:
|
|
||||||
start = i_start
|
|
||||||
for code_point in range(i_start, i_end + 1):
|
|
||||||
face = load_glyph(code_point)
|
|
||||||
if face is None:
|
|
||||||
if start < code_point:
|
|
||||||
intervals.append((start, code_point - 1))
|
|
||||||
start = code_point + 1
|
|
||||||
if start != i_end + 1:
|
|
||||||
intervals.append((start, i_end))
|
|
||||||
|
|
||||||
for face in font_stack:
|
|
||||||
face.set_char_size(size << 6, size << 6, 150, 150)
|
|
||||||
|
|
||||||
total_size = 0
|
|
||||||
all_glyphs = []
|
|
||||||
|
|
||||||
for i_start, i_end in intervals:
|
|
||||||
for code_point in range(i_start, i_end + 1):
|
|
||||||
face = load_glyph(code_point)
|
|
||||||
bitmap = face.glyph.bitmap
|
|
||||||
|
|
||||||
# Build out 4-bit greyscale bitmap
|
|
||||||
pixels4g = []
|
|
||||||
px = 0
|
|
||||||
for i, v in enumerate(bitmap.buffer):
|
|
||||||
y = i / bitmap.width
|
|
||||||
x = i % bitmap.width
|
|
||||||
if x % 2 == 0:
|
|
||||||
px = (v >> 4)
|
|
||||||
else:
|
|
||||||
px = px | (v & 0xF0)
|
|
||||||
pixels4g.append(px);
|
|
||||||
px = 0
|
|
||||||
# eol
|
|
||||||
if x == bitmap.width - 1 and bitmap.width % 2 > 0:
|
|
||||||
pixels4g.append(px)
|
|
||||||
px = 0
|
|
||||||
|
|
||||||
if is2Bit:
|
|
||||||
# 0 = white, 15 black, 8+ dark grey, 7- light grey
|
|
||||||
# Downsample to 2-bit bitmap
|
|
||||||
pixels2b = []
|
|
||||||
px = 0
|
|
||||||
pitch = (bitmap.width // 2) + (bitmap.width % 2)
|
|
||||||
for y in range(bitmap.rows):
|
|
||||||
for x in range(bitmap.width):
|
|
||||||
px = px << 2
|
|
||||||
bm = pixels4g[y * pitch + (x // 2)]
|
|
||||||
bm = (bm >> ((x % 2) * 4)) & 0xF
|
|
||||||
|
|
||||||
if bm == 15:
|
|
||||||
px += 3
|
|
||||||
elif bm >= 8:
|
|
||||||
px += 2
|
|
||||||
elif bm > 0:
|
|
||||||
px += 1
|
|
||||||
|
|
||||||
if (y * bitmap.width + x) % 4 == 3:
|
|
||||||
pixels2b.append(px)
|
|
||||||
px = 0
|
|
||||||
if (bitmap.width * bitmap.rows) % 4 != 0:
|
|
||||||
px = px << (4 - (bitmap.width * bitmap.rows) % 4) * 2
|
|
||||||
pixels2b.append(px)
|
|
||||||
|
|
||||||
# for y in range(bitmap.rows):
|
|
||||||
# line = ''
|
|
||||||
# for x in range(bitmap.width):
|
|
||||||
# pixelPosition = y * bitmap.width + x
|
|
||||||
# byte = pixels2b[pixelPosition // 4]
|
|
||||||
# bit_index = (3 - (pixelPosition % 4)) * 2
|
|
||||||
# line += '#' if ((byte >> bit_index) & 3) > 0 else '.'
|
|
||||||
# print(line)
|
|
||||||
# print('')
|
|
||||||
else:
|
|
||||||
# Downsample to 1-bit bitmap - treat any non-zero as black
|
|
||||||
pixelsbw = []
|
|
||||||
px = 0
|
|
||||||
pitch = (bitmap.width // 2) + (bitmap.width % 2)
|
|
||||||
for y in range(bitmap.rows):
|
|
||||||
for x in range(bitmap.width):
|
|
||||||
px = px << 1
|
|
||||||
bm = pixels4g[y * pitch + (x // 2)]
|
|
||||||
px += 1 if ((x & 1) == 0 and bm & 0xF > 0) or ((x & 1) == 1 and bm & 0xF0 > 0) else 0
|
|
||||||
|
|
||||||
if (y * bitmap.width + x) % 8 == 7:
|
|
||||||
pixelsbw.append(px)
|
|
||||||
px = 0
|
|
||||||
if (bitmap.width * bitmap.rows) % 8 != 0:
|
|
||||||
px = px << (8 - (bitmap.width * bitmap.rows) % 8)
|
|
||||||
pixelsbw.append(px)
|
|
||||||
|
|
||||||
# for y in range(bitmap.rows):
|
|
||||||
# line = ''
|
|
||||||
# for x in range(bitmap.width):
|
|
||||||
# pixelPosition = y * bitmap.width + x
|
|
||||||
# byte = pixelsbw[pixelPosition // 8]
|
|
||||||
# bit_index = 7 - (pixelPosition % 8)
|
|
||||||
# line += '#' if (byte >> bit_index) & 1 else '.'
|
|
||||||
# print(line)
|
|
||||||
# print('')
|
|
||||||
|
|
||||||
pixels = pixels2b if is2Bit else pixelsbw
|
|
||||||
|
|
||||||
# Build output data
|
|
||||||
packed = bytes(pixels)
|
|
||||||
glyph = GlyphProps(
|
|
||||||
width = bitmap.width,
|
|
||||||
height = bitmap.rows,
|
|
||||||
advance_x = norm_floor(face.glyph.advance.x),
|
|
||||||
left = face.glyph.bitmap_left,
|
|
||||||
top = face.glyph.bitmap_top,
|
|
||||||
data_length = len(packed),
|
|
||||||
data_offset = total_size,
|
|
||||||
code_point = code_point,
|
|
||||||
)
|
|
||||||
total_size += len(packed)
|
|
||||||
all_glyphs.append((glyph, packed))
|
|
||||||
|
|
||||||
# pipe seems to be a good heuristic for the "real" descender
|
|
||||||
face = load_glyph(ord('|'))
|
|
||||||
|
|
||||||
glyph_data = []
|
|
||||||
glyph_props = []
|
|
||||||
for index, glyph in enumerate(all_glyphs):
|
|
||||||
props, packed = glyph
|
|
||||||
glyph_data.extend([b for b in packed])
|
|
||||||
glyph_props.append(props)
|
|
||||||
|
|
||||||
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * mode: {'2-bit' if is2Bit else '1-bit'}\n */")
|
|
||||||
print("#pragma once")
|
|
||||||
print("#include \"EpdFontData.h\"\n")
|
|
||||||
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
|
|
||||||
for c in chunks(glyph_data, 16):
|
|
||||||
print (" " + " ".join(f"0x{b:02X}," for b in c))
|
|
||||||
print ("};\n");
|
|
||||||
|
|
||||||
print(f"static const EpdGlyph {font_name}Glyphs[] = {{")
|
|
||||||
for i, g in enumerate(glyph_props):
|
|
||||||
print (" { " + ", ".join([f"{a}" for a in list(g[:-1])]),"},", f"// {chr(g.code_point) if g.code_point != 92 else '<backslash>'}")
|
|
||||||
print ("};\n");
|
|
||||||
|
|
||||||
print(f"static const EpdUnicodeInterval {font_name}Intervals[] = {{")
|
|
||||||
offset = 0
|
|
||||||
for i_start, i_end in intervals:
|
|
||||||
print (f" {{ 0x{i_start:X}, 0x{i_end:X}, 0x{offset:X} }},")
|
|
||||||
offset += i_end - i_start + 1
|
|
||||||
print ("};\n");
|
|
||||||
|
|
||||||
print(f"static const EpdFontData {font_name} = {{")
|
|
||||||
print(f" {font_name}Bitmaps,")
|
|
||||||
print(f" {font_name}Glyphs,")
|
|
||||||
print(f" {font_name}Intervals,")
|
|
||||||
print(f" {len(intervals)},")
|
|
||||||
print(f" {norm_ceil(face.size.height)},")
|
|
||||||
print(f" {norm_ceil(face.size.ascender)},")
|
|
||||||
print(f" {norm_floor(face.size.descender)},")
|
|
||||||
print(f" {'true' if is2Bit else 'false'},")
|
|
||||||
print("};")
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
freetype-py==2.5.1
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SD.h>
|
#include <JpegToBmpConverter.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
#include "Epub/FsHelpers.h"
|
|
||||||
#include "Epub/parsers/ContainerParser.h"
|
#include "Epub/parsers/ContainerParser.h"
|
||||||
#include "Epub/parsers/ContentOpfParser.h"
|
#include "Epub/parsers/ContentOpfParser.h"
|
||||||
|
#include "Epub/parsers/TocNavParser.h"
|
||||||
#include "Epub/parsers/TocNcxParser.h"
|
#include "Epub/parsers/TocNcxParser.h"
|
||||||
|
|
||||||
bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
||||||
@@ -30,32 +30,37 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
|||||||
// Stream read (reusing your existing stream logic)
|
// Stream read (reusing your existing stream logic)
|
||||||
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
|
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
|
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
|
||||||
containerParser.teardown();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the result
|
// Extract the result
|
||||||
if (containerParser.fullPath.empty()) {
|
if (containerParser.fullPath.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
|
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
|
||||||
containerParser.teardown();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
*contentOpfFile = std::move(containerParser.fullPath);
|
*contentOpfFile = std::move(containerParser.fullPath);
|
||||||
|
|
||||||
containerParser.teardown();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
|
bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||||
|
std::string contentOpfFilePath;
|
||||||
|
if (!findContentOpfFile(&contentOpfFilePath)) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str());
|
||||||
|
|
||||||
size_t contentOpfSize;
|
size_t contentOpfSize;
|
||||||
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
|
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
|
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentOpfParser opfParser(getBasePath(), contentOpfSize);
|
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get());
|
||||||
|
|
||||||
if (!opfParser.setup()) {
|
if (!opfParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
||||||
return false;
|
return false;
|
||||||
@@ -63,105 +68,258 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
|
|||||||
|
|
||||||
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
|
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
|
||||||
opfParser.teardown();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab data from opfParser into epub
|
// Grab data from opfParser into epub
|
||||||
title = opfParser.title;
|
bookMetadata.title = opfParser.title;
|
||||||
if (!opfParser.coverItemId.empty() && opfParser.items.count(opfParser.coverItemId) > 0) {
|
bookMetadata.author = opfParser.author;
|
||||||
coverImageItem = opfParser.items.at(opfParser.coverItemId);
|
bookMetadata.language = opfParser.language;
|
||||||
|
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
||||||
|
bookMetadata.textReferenceHref = opfParser.textReferenceHref;
|
||||||
|
|
||||||
|
if (!opfParser.tocNcxPath.empty()) {
|
||||||
|
tocNcxItem = opfParser.tocNcxPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opfParser.items.count("ncx")) {
|
if (!opfParser.tocNavPath.empty()) {
|
||||||
tocNcxItem = opfParser.items.at("ncx");
|
tocNavItem = opfParser.tocNavPath;
|
||||||
} 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());
|
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
|
||||||
|
|
||||||
opfParser.teardown();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::parseTocNcxFile() {
|
bool Epub::parseTocNcxFile() const {
|
||||||
// the ncx file should have been specified in the content.opf file
|
// the ncx file should have been specified in the content.opf file
|
||||||
if (tocNcxItem.empty()) {
|
if (tocNcxItem.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
|
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t tocSize;
|
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
|
||||||
if (!getItemSize(tocNcxItem, &tocSize)) {
|
|
||||||
Serial.printf("[%lu] [EBP] Could not get size of toc ncx\n", millis());
|
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
||||||
|
FsFile tempNcxFile;
|
||||||
|
if (!SdMan.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
||||||
|
tempNcxFile.close();
|
||||||
|
if (!SdMan.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto ncxSize = tempNcxFile.size();
|
||||||
|
|
||||||
TocNcxParser ncxParser(contentBasePath, tocSize);
|
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||||
|
|
||||||
if (!ncxParser.setup()) {
|
if (!ncxParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
||||||
|
tempNcxFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
|
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||||
Serial.printf("[%lu] [EBP] Could not read toc ncx stream\n", millis());
|
if (!ncxBuffer) {
|
||||||
ncxParser.teardown();
|
Serial.printf("[%lu] [EBP] Could not allocate memory for toc ncx parser\n", millis());
|
||||||
|
tempNcxFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this->toc = std::move(ncxParser.toc);
|
while (tempNcxFile.available()) {
|
||||||
|
const auto readSize = tempNcxFile.read(ncxBuffer, 1024);
|
||||||
|
if (readSize == 0) break;
|
||||||
|
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size());
|
if (processedSize != readSize) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not process all toc ncx data\n", millis());
|
||||||
|
free(ncxBuffer);
|
||||||
|
tempNcxFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ncxParser.teardown();
|
free(ncxBuffer);
|
||||||
|
tempNcxFile.close();
|
||||||
|
SdMan.remove(tmpNcxPath.c_str());
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [EBP] Parsed TOC items\n", millis());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::parseTocNavFile() const {
|
||||||
|
// the nav file should have been specified in the content.opf file (EPUB 3)
|
||||||
|
if (tocNavItem.empty()) {
|
||||||
|
Serial.printf("[%lu] [EBP] No nav file specified\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [EBP] Parsing toc nav file: %s\n", millis(), tocNavItem.c_str());
|
||||||
|
|
||||||
|
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
||||||
|
FsFile tempNavFile;
|
||||||
|
if (!SdMan.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
||||||
|
tempNavFile.close();
|
||||||
|
if (!SdMan.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto navSize = tempNavFile.size();
|
||||||
|
|
||||||
|
// Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf
|
||||||
|
// and the HTMLX nav file will have hrefs relative to itself
|
||||||
|
const std::string navContentBasePath = tocNavItem.substr(0, tocNavItem.find_last_of('/') + 1);
|
||||||
|
TocNavParser navParser(navContentBasePath, navSize, bookMetadataCache.get());
|
||||||
|
|
||||||
|
if (!navParser.setup()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not setup toc nav parser\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||||
|
if (!navBuffer) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not allocate memory for toc nav parser\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (tempNavFile.available()) {
|
||||||
|
const auto readSize = tempNavFile.read(navBuffer, 1024);
|
||||||
|
const auto processedSize = navParser.write(navBuffer, readSize);
|
||||||
|
|
||||||
|
if (processedSize != readSize) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not process all toc nav data\n", millis());
|
||||||
|
free(navBuffer);
|
||||||
|
tempNavFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(navBuffer);
|
||||||
|
tempNavFile.close();
|
||||||
|
SdMan.remove(tmpNavPath.c_str());
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [EBP] Parsed TOC nav items\n", millis());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// load in the meta data for the epub file
|
// load in the meta data for the epub file
|
||||||
bool Epub::load() {
|
bool Epub::load(const bool buildIfMissing) {
|
||||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
||||||
ZipFile zip("/sd" + filepath);
|
|
||||||
|
|
||||||
std::string contentOpfFilePath;
|
// Initialize spine/TOC cache
|
||||||
if (!findContentOpfFile(&contentOpfFilePath)) {
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
|
|
||||||
|
// Try to load existing cache first
|
||||||
|
if (bookMetadataCache->load()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't load from cache above and we aren't allowed to build, fail now
|
||||||
|
if (!buildIfMissing) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Found content.opf at: %s\n", millis(), contentOpfFilePath.c_str());
|
// Cache doesn't exist or is invalid, build it
|
||||||
|
Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis());
|
||||||
|
setupCacheDir();
|
||||||
|
|
||||||
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
const uint32_t indexingStart = millis();
|
||||||
|
|
||||||
if (!parseContentOpf(contentOpfFilePath)) {
|
// Begin building cache - stream entries to disk immediately
|
||||||
|
if (!bookMetadataCache->beginWrite()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPF Pass
|
||||||
|
const uint32_t opfStart = millis();
|
||||||
|
BookMetadataCache::BookMetadata bookMetadata;
|
||||||
|
if (!bookMetadataCache->beginContentOpfPass()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not begin writing content.opf pass\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseContentOpf(bookMetadata)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!bookMetadataCache->endContentOpfPass()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Serial.printf("[%lu] [EBP] OPF pass completed in %lu ms\n", millis(), millis() - opfStart);
|
||||||
|
|
||||||
if (!parseTocNcxFile()) {
|
// TOC Pass - try EPUB 3 nav first, fall back to NCX
|
||||||
Serial.printf("[%lu] [EBP] Could not parse toc\n", millis());
|
const uint32_t tocStart = millis();
|
||||||
|
if (!bookMetadataCache->beginTocPass()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tocParsed = false;
|
||||||
|
|
||||||
|
// Try EPUB 3 nav document first (preferred)
|
||||||
|
if (!tocNavItem.empty()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Attempting to parse EPUB 3 nav document\n", millis());
|
||||||
|
tocParsed = parseTocNavFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to NCX if nav parsing failed or wasn't available
|
||||||
|
if (!tocParsed && !tocNcxItem.empty()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Falling back to NCX TOC\n", millis());
|
||||||
|
tocParsed = parseTocNcxFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tocParsed) {
|
||||||
|
Serial.printf("[%lu] [EBP] Warning: Could not parse any TOC format\n", millis());
|
||||||
|
// Continue anyway - book will work without TOC
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookMetadataCache->endTocPass()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Serial.printf("[%lu] [EBP] TOC pass completed in %lu ms\n", millis(), millis() - tocStart);
|
||||||
|
|
||||||
|
// Close the cache files
|
||||||
|
if (!bookMetadataCache->endWrite()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not end writing cache\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build final book.bin
|
||||||
|
const uint32_t buildStart = millis();
|
||||||
|
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Serial.printf("[%lu] [EBP] buildBookBin completed in %lu ms\n", millis(), millis() - buildStart);
|
||||||
|
Serial.printf("[%lu] [EBP] Total indexing completed in %lu ms\n", millis(), millis() - indexingStart);
|
||||||
|
|
||||||
|
if (!bookMetadataCache->cleanupTmpFiles()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the cache from disk so it's in the correct state
|
||||||
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
|
if (!bookMetadataCache->load()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Failed to reload cache after writing\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::clearCache() const {
|
bool Epub::clearCache() const {
|
||||||
if (!SD.exists(cachePath.c_str())) {
|
if (!SdMan.exists(cachePath.c_str())) {
|
||||||
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
|
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!FsHelpers::removeDir(cachePath.c_str())) {
|
if (!SdMan.removeDir(cachePath.c_str())) {
|
||||||
Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis());
|
Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -171,68 +329,178 @@ bool Epub::clearCache() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Epub::setupCacheDir() const {
|
void Epub::setupCacheDir() const {
|
||||||
if (SD.exists(cachePath.c_str())) {
|
if (SdMan.exists(cachePath.c_str())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop over each segment of the cache path and create directories as needed
|
SdMan.mkdir(cachePath.c_str());
|
||||||
for (size_t i = 1; i < cachePath.length(); i++) {
|
|
||||||
if (cachePath[i] == '/') {
|
|
||||||
SD.mkdir(cachePath.substr(0, i).c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SD.mkdir(cachePath.c_str());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string& Epub::getCachePath() const { return cachePath; }
|
const std::string& Epub::getCachePath() const { return cachePath; }
|
||||||
|
|
||||||
const std::string& Epub::getPath() const { return filepath; }
|
const std::string& Epub::getPath() const { return filepath; }
|
||||||
|
|
||||||
const std::string& Epub::getTitle() const { return title; }
|
const std::string& Epub::getTitle() const {
|
||||||
|
static std::string blank;
|
||||||
const std::string& Epub::getCoverImageItem() const { return coverImageItem; }
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return blank;
|
||||||
std::string normalisePath(const std::string& path) {
|
|
||||||
std::vector<std::string> components;
|
|
||||||
std::string component;
|
|
||||||
|
|
||||||
for (const auto c : path) {
|
|
||||||
if (c == '/') {
|
|
||||||
if (!component.empty()) {
|
|
||||||
if (component == "..") {
|
|
||||||
if (!components.empty()) {
|
|
||||||
components.pop_back();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
components.push_back(component);
|
|
||||||
}
|
|
||||||
component.clear();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
component += c;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!component.empty()) {
|
return bookMetadataCache->coreMetadata.title;
|
||||||
components.push_back(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string result;
|
|
||||||
for (const auto& c : components) {
|
|
||||||
if (!result.empty()) {
|
|
||||||
result += "/";
|
|
||||||
}
|
|
||||||
result += c;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, bool trailingNullByte) const {
|
const std::string& Epub::getAuthor() const {
|
||||||
const ZipFile zip("/sd" + filepath);
|
static std::string blank;
|
||||||
const std::string path = normalisePath(itemHref);
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return blank;
|
||||||
|
}
|
||||||
|
|
||||||
const auto content = zip.readFileToMemory(path.c_str(), size, trailingNullByte);
|
return bookMetadataCache->coreMetadata.author;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& Epub::getLanguage() const {
|
||||||
|
static std::string blank;
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return blank;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->coreMetadata.language;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Epub::getCoverBmpPath(bool cropped) const {
|
||||||
|
const auto coverFileName = std::string("cover") + (cropped ? "_crop" : "");
|
||||||
|
return cachePath + "/" + coverFileName + ".bmp";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::generateCoverBmp(bool cropped) const {
|
||||||
|
// Already generated, return true
|
||||||
|
if (SdMan.exists(getCoverBmpPath(cropped).c_str())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Cannot generate cover BMP, cache not loaded\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||||
|
if (coverImageHref.empty()) {
|
||||||
|
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||||
|
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||||
|
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image (%s mode)\n", millis(), cropped ? "cropped" : "fit");
|
||||||
|
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||||
|
|
||||||
|
FsFile coverJpg;
|
||||||
|
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||||
|
coverJpg.close();
|
||||||
|
|
||||||
|
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile coverBmp;
|
||||||
|
if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||||
|
coverJpg.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
|
||||||
|
coverJpg.close();
|
||||||
|
coverBmp.close();
|
||||||
|
SdMan.remove(coverJpgTempPath.c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis());
|
||||||
|
SdMan.remove(getCoverBmpPath(cropped).c_str());
|
||||||
|
}
|
||||||
|
Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no");
|
||||||
|
return success;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping\n", millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
|
||||||
|
|
||||||
|
bool Epub::generateThumbBmp() const {
|
||||||
|
// Already generated, return true
|
||||||
|
if (SdMan.exists(getThumbBmpPath().c_str())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Cannot generate thumb BMP, cache not loaded\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||||
|
if (coverImageHref.empty()) {
|
||||||
|
Serial.printf("[%lu] [EBP] No known cover image for thumbnail\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||||
|
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||||
|
Serial.printf("[%lu] [EBP] Generating thumb BMP from JPG cover image\n", millis());
|
||||||
|
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||||
|
|
||||||
|
FsFile coverJpg;
|
||||||
|
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||||
|
coverJpg.close();
|
||||||
|
|
||||||
|
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile thumbBmp;
|
||||||
|
if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) {
|
||||||
|
coverJpg.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Use smaller target size for Continue Reading card (half of screen: 240x400)
|
||||||
|
// Generate 1-bit BMP for fast home screen rendering (no gray passes needed)
|
||||||
|
constexpr int THUMB_TARGET_WIDTH = 240;
|
||||||
|
constexpr int THUMB_TARGET_HEIGHT = 400;
|
||||||
|
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
|
||||||
|
THUMB_TARGET_HEIGHT);
|
||||||
|
coverJpg.close();
|
||||||
|
thumbBmp.close();
|
||||||
|
SdMan.remove(coverJpgTempPath.c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis());
|
||||||
|
SdMan.remove(getThumbBmpPath().c_str());
|
||||||
|
}
|
||||||
|
Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(),
|
||||||
|
success ? "yes" : "no");
|
||||||
|
return success;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping thumbnail\n", millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||||
|
if (itemHref.empty()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string path = FsHelpers::normalisePath(itemHref);
|
||||||
|
|
||||||
|
const auto content = ZipFile(filepath).readFileToMemory(path.c_str(), size, trailingNullByte);
|
||||||
if (!content) {
|
if (!content) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to read item %s\n", millis(), path.c_str());
|
Serial.printf("[%lu] [EBP] Failed to read item %s\n", millis(), path.c_str());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -242,65 +510,133 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const {
|
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const {
|
||||||
const ZipFile zip("/sd" + filepath);
|
if (itemHref.empty()) {
|
||||||
const std::string path = normalisePath(itemHref);
|
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return zip.readFileToStream(path.c_str(), out, chunkSize);
|
const std::string path = FsHelpers::normalisePath(itemHref);
|
||||||
|
return ZipFile(filepath).readFileToStream(path.c_str(), out, chunkSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
|
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
|
||||||
const ZipFile zip("/sd" + filepath);
|
const std::string path = FsHelpers::normalisePath(itemHref);
|
||||||
const std::string path = normalisePath(itemHref);
|
return ZipFile(filepath).getInflatedFileSize(path.c_str(), size);
|
||||||
|
|
||||||
return zip.getInflatedFileSize(path.c_str(), size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int Epub::getSpineItemsCount() const { return spine.size(); }
|
int Epub::getSpineItemsCount() const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return bookMetadataCache->getSpineCount();
|
||||||
|
}
|
||||||
|
|
||||||
std::string& Epub::getSpineItem(const int spineIndex) {
|
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; }
|
||||||
if (spineIndex < 0 || spineIndex >= spine.size()) {
|
|
||||||
|
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
Serial.printf("[%lu] [EBP] getSpineItem called but cache not loaded\n", millis());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
||||||
return spine.at(0).second;
|
return bookMetadataCache->getSpineEntry(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return spine.at(spineIndex).second;
|
return bookMetadataCache->getSpineEntry(spineIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
EpubTocEntry& Epub::getTocItem(const int tocTndex) {
|
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
||||||
if (tocTndex < 0 || tocTndex >= toc.size()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocTndex);
|
Serial.printf("[%lu] [EBP] getTocItem called but cache not loaded\n", millis());
|
||||||
return toc.at(0);
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return toc.at(tocTndex);
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
|
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->getTocEntry(tocIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Epub::getTocItemsCount() const { return toc.size(); }
|
int Epub::getTocItemsCount() const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->getTocCount();
|
||||||
|
}
|
||||||
|
|
||||||
// work out the section index for a toc index
|
// work out the section index for a toc index
|
||||||
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||||
// the toc entry should have an href that matches the spine item
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
// so we can find the spine index by looking for the href
|
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex called but cache not loaded\n", millis());
|
||||||
for (int i = 0; i < spine.size(); i++) {
|
return 0;
|
||||||
if (spine[i].second == toc[tocIndex].href) {
|
}
|
||||||
|
|
||||||
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
|
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int spineIndex = bookMetadataCache->getTocEntry(tocIndex).spineIndex;
|
||||||
|
if (spineIndex < 0) {
|
||||||
|
Serial.printf("[%lu] [EBP] Section not found for TOC index %d\n", millis(), tocIndex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return spineIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Epub::getTocIndexForSpineIndex(const int spineIndex) const { return getSpineItem(spineIndex).tocIndex; }
|
||||||
|
|
||||||
|
size_t Epub::getBookSize() const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded() || bookMetadataCache->getSpineCount() == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Epub::getSpineIndexForTextReference() const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
Serial.printf("[%lu] [EBP] getSpineIndexForTextReference called but cache not loaded\n", millis());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Serial.printf("[%lu] [ERS] Core Metadata: cover(%d)=%s, textReference(%d)=%s\n", millis(),
|
||||||
|
bookMetadataCache->coreMetadata.coverItemHref.size(),
|
||||||
|
bookMetadataCache->coreMetadata.coverItemHref.c_str(),
|
||||||
|
bookMetadataCache->coreMetadata.textReferenceHref.size(),
|
||||||
|
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
|
||||||
|
|
||||||
|
if (bookMetadataCache->coreMetadata.textReferenceHref.empty()) {
|
||||||
|
// there was no textReference in epub, so we return 0 (the first chapter)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through spine items to get the correct index matching the text href
|
||||||
|
for (size_t i = 0; i < getSpineItemsCount(); i++) {
|
||||||
|
if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) {
|
||||||
|
Serial.printf("[%lu] [ERS] Text reference %s found at index %d\n", millis(),
|
||||||
|
bookMetadataCache->coreMetadata.textReferenceHref.c_str(), i);
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// This should not happen, as we checked for empty textReferenceHref earlier
|
||||||
Serial.printf("[%lu] [EBP] Section not found\n", millis());
|
Serial.printf("[%lu] [EBP] Section not found for text reference\n", millis());
|
||||||
// not found - default to the start of the book
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
|
// Calculate progress in book (returns 0.0-1.0)
|
||||||
// the toc entry should have an href that matches the spine item
|
float Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
|
||||||
// so we can find the toc index by looking for the href
|
const size_t bookSize = getBookSize();
|
||||||
for (int i = 0; i < toc.size(); i++) {
|
if (bookSize == 0) {
|
||||||
if (toc[i].href == spine[spineIndex].second) {
|
return 0.0f;
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
|
||||||
Serial.printf("[%lu] [EBP] TOC item not found\n", millis());
|
const size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
|
||||||
return -1;
|
const float sectionProgSize = currentSpineRead * static_cast<float>(curChapterSize);
|
||||||
|
const float totalProgress = static_cast<float>(prevChapterSize) + sectionProgSize;
|
||||||
|
return totalProgress / static_cast<float>(bookSize);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Print.h>
|
#include <Print.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "Epub/EpubTocEntry.h"
|
#include "Epub/BookMetadataCache.h"
|
||||||
|
|
||||||
class ZipFile;
|
class ZipFile;
|
||||||
|
|
||||||
class Epub {
|
class Epub {
|
||||||
// the title read from the EPUB meta data
|
// the ncx file (EPUB 2)
|
||||||
std::string title;
|
|
||||||
// the cover image
|
|
||||||
std::string coverImageItem;
|
|
||||||
// the ncx file
|
|
||||||
std::string tocNcxItem;
|
std::string tocNcxItem;
|
||||||
|
// the nav file (EPUB 3)
|
||||||
|
std::string tocNavItem;
|
||||||
// where is the EPUBfile?
|
// where is the EPUBfile?
|
||||||
std::string filepath;
|
std::string filepath;
|
||||||
// the spine of the EPUB file
|
|
||||||
std::vector<std::pair<std::string, std::string>> spine;
|
|
||||||
// the toc of the EPUB file
|
|
||||||
std::vector<EpubTocEntry> toc;
|
|
||||||
// the base path for items in the EPUB file
|
// the base path for items in the EPUB file
|
||||||
std::string contentBasePath;
|
std::string contentBasePath;
|
||||||
// Uniq cache key based on filepath
|
// Uniq cache key based on filepath
|
||||||
std::string cachePath;
|
std::string cachePath;
|
||||||
|
// Spine and TOC cache
|
||||||
|
std::unique_ptr<BookMetadataCache> bookMetadataCache;
|
||||||
|
|
||||||
bool findContentOpfFile(std::string* contentOpfFile) const;
|
bool findContentOpfFile(std::string* contentOpfFile) const;
|
||||||
bool parseContentOpf(const std::string& contentOpfFilePath);
|
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
|
||||||
bool parseTocNcxFile();
|
bool parseTocNcxFile() const;
|
||||||
|
bool parseTocNavFile() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||||
@@ -38,21 +37,31 @@ class Epub {
|
|||||||
}
|
}
|
||||||
~Epub() = default;
|
~Epub() = default;
|
||||||
std::string& getBasePath() { return contentBasePath; }
|
std::string& getBasePath() { return contentBasePath; }
|
||||||
bool load();
|
bool load(bool buildIfMissing = true);
|
||||||
bool clearCache() const;
|
bool clearCache() const;
|
||||||
void setupCacheDir() const;
|
void setupCacheDir() const;
|
||||||
const std::string& getCachePath() const;
|
const std::string& getCachePath() const;
|
||||||
const std::string& getPath() const;
|
const std::string& getPath() const;
|
||||||
const std::string& getTitle() const;
|
const std::string& getTitle() const;
|
||||||
const std::string& getCoverImageItem() const;
|
const std::string& getAuthor() const;
|
||||||
|
const std::string& getLanguage() const;
|
||||||
|
std::string getCoverBmpPath(bool cropped = false) const;
|
||||||
|
bool generateCoverBmp(bool cropped = false) const;
|
||||||
|
std::string getThumbBmpPath() const;
|
||||||
|
bool generateThumbBmp() const;
|
||||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||||
bool trailingNullByte = false) const;
|
bool trailingNullByte = false) const;
|
||||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||||
bool getItemSize(const std::string& itemHref, size_t* size) const;
|
bool getItemSize(const std::string& itemHref, size_t* size) const;
|
||||||
std::string& getSpineItem(int spineIndex);
|
BookMetadataCache::SpineEntry getSpineItem(int spineIndex) const;
|
||||||
|
BookMetadataCache::TocEntry getTocItem(int tocIndex) const;
|
||||||
int getSpineItemsCount() const;
|
int getSpineItemsCount() const;
|
||||||
EpubTocEntry& getTocItem(int tocTndex);
|
|
||||||
int getTocItemsCount() const;
|
int getTocItemsCount() const;
|
||||||
int getSpineIndexForTocIndex(int tocIndex) const;
|
int getSpineIndexForTocIndex(int tocIndex) const;
|
||||||
int getTocIndexForSpineIndex(int spineIndex) const;
|
int getTocIndexForSpineIndex(int spineIndex) const;
|
||||||
|
size_t getCumulativeSpineItemSize(int spineIndex) const;
|
||||||
|
int getSpineIndexForTextReference() const;
|
||||||
|
|
||||||
|
size_t getBookSize() const;
|
||||||
|
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,448 @@
|
|||||||
|
#include "BookMetadataCache.h"
|
||||||
|
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
#include <ZipFile.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "FsHelpers.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint8_t BOOK_CACHE_VERSION = 5;
|
||||||
|
constexpr char bookBinFile[] = "/book.bin";
|
||||||
|
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||||
|
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginWrite() {
|
||||||
|
buildMode = true;
|
||||||
|
spineCount = 0;
|
||||||
|
tocCount = 0;
|
||||||
|
Serial.printf("[%lu] [BMC] Entering write mode\n", millis());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginContentOpfPass() {
|
||||||
|
Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis());
|
||||||
|
|
||||||
|
// Open spine file for writing
|
||||||
|
return SdMan.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endContentOpfPass() {
|
||||||
|
spineFile.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginTocPass() {
|
||||||
|
Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis());
|
||||||
|
|
||||||
|
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!SdMan.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
|
spineFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||||
|
spineHrefIndex.clear();
|
||||||
|
spineHrefIndex.reserve(spineCount);
|
||||||
|
spineFile.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto entry = readSpineEntry(spineFile);
|
||||||
|
SpineHrefIndexEntry idx;
|
||||||
|
idx.hrefHash = fnvHash64(entry.href);
|
||||||
|
idx.hrefLen = static_cast<uint16_t>(entry.href.size());
|
||||||
|
idx.spineIndex = static_cast<int16_t>(i);
|
||||||
|
spineHrefIndex.push_back(idx);
|
||||||
|
}
|
||||||
|
std::sort(spineHrefIndex.begin(), spineHrefIndex.end(),
|
||||||
|
[](const SpineHrefIndexEntry& a, const SpineHrefIndexEntry& b) {
|
||||||
|
return a.hrefHash < b.hrefHash || (a.hrefHash == b.hrefHash && a.hrefLen < b.hrefLen);
|
||||||
|
});
|
||||||
|
spineFile.seek(0);
|
||||||
|
useSpineHrefIndex = true;
|
||||||
|
Serial.printf("[%lu] [BMC] Using fast index for %d spine items\n", millis(), spineCount);
|
||||||
|
} else {
|
||||||
|
useSpineHrefIndex = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endTocPass() {
|
||||||
|
tocFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
|
||||||
|
spineHrefIndex.clear();
|
||||||
|
spineHrefIndex.shrink_to_fit();
|
||||||
|
useSpineHrefIndex = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endWrite() {
|
||||||
|
if (!buildMode) {
|
||||||
|
Serial.printf("[%lu] [BMC] endWrite called but not in build mode\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildMode = false;
|
||||||
|
Serial.printf("[%lu] [BMC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
|
||||||
|
// Open all three files, writing to meta, reading from spine and toc
|
||||||
|
if (!SdMan.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
|
bookFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SdMan.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
|
bookFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint32_t headerASize =
|
||||||
|
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||||
|
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
||||||
|
metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
|
||||||
|
sizeof(uint32_t) * 5;
|
||||||
|
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
|
||||||
|
const uint32_t lutOffset = headerASize + metadataSize;
|
||||||
|
|
||||||
|
// Header A
|
||||||
|
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
|
||||||
|
serialization::writePod(bookFile, lutOffset);
|
||||||
|
serialization::writePod(bookFile, spineCount);
|
||||||
|
serialization::writePod(bookFile, tocCount);
|
||||||
|
// Metadata
|
||||||
|
serialization::writeString(bookFile, metadata.title);
|
||||||
|
serialization::writeString(bookFile, metadata.author);
|
||||||
|
serialization::writeString(bookFile, metadata.language);
|
||||||
|
serialization::writeString(bookFile, metadata.coverItemHref);
|
||||||
|
serialization::writeString(bookFile, metadata.textReferenceHref);
|
||||||
|
|
||||||
|
// Loop through spine entries, writing LUT positions
|
||||||
|
spineFile.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
uint32_t pos = spineFile.position();
|
||||||
|
auto spineEntry = readSpineEntry(spineFile);
|
||||||
|
serialization::writePod(bookFile, pos + lutOffset + lutSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through toc entries, writing LUT positions
|
||||||
|
tocFile.seek(0);
|
||||||
|
for (int i = 0; i < tocCount; i++) {
|
||||||
|
uint32_t pos = tocFile.position();
|
||||||
|
auto tocEntry = readTocEntry(tocFile);
|
||||||
|
serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast<uint32_t>(spineFile.position()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// LUTs complete
|
||||||
|
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin
|
||||||
|
|
||||||
|
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
|
||||||
|
std::vector<int16_t> spineToTocIndex(spineCount, -1);
|
||||||
|
tocFile.seek(0);
|
||||||
|
for (int j = 0; j < tocCount; j++) {
|
||||||
|
auto tocEntry = readTocEntry(tocFile);
|
||||||
|
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
||||||
|
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
||||||
|
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ZipFile zip(epubPath);
|
||||||
|
// Pre-open zip file to speed up size calculations
|
||||||
|
if (!zip.open()) {
|
||||||
|
Serial.printf("[%lu] [BMC] Could not open EPUB zip for size calculations\n", millis());
|
||||||
|
bookFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
tocFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// NOTE: We intentionally skip calling loadAllFileStatSlims() here.
|
||||||
|
// For large EPUBs (2000+ chapters), pre-loading all ZIP central directory entries
|
||||||
|
// into memory causes OOM crashes on ESP32-C3's limited ~380KB RAM.
|
||||||
|
// Instead, for large books we use a one-pass batch lookup that scans the ZIP
|
||||||
|
// central directory once and matches against spine targets using hash comparison.
|
||||||
|
// This is O(n*log(m)) instead of O(n*m) while avoiding memory exhaustion.
|
||||||
|
// See: https://github.com/crosspoint-reader/crosspoint-reader/issues/134
|
||||||
|
|
||||||
|
std::vector<uint32_t> spineSizes;
|
||||||
|
bool useBatchSizes = false;
|
||||||
|
|
||||||
|
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||||
|
Serial.printf("[%lu] [BMC] Using batch size lookup for %d spine items\n", millis(), spineCount);
|
||||||
|
|
||||||
|
std::vector<ZipFile::SizeTarget> targets;
|
||||||
|
targets.reserve(spineCount);
|
||||||
|
|
||||||
|
spineFile.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto entry = readSpineEntry(spineFile);
|
||||||
|
std::string path = FsHelpers::normalisePath(entry.href);
|
||||||
|
|
||||||
|
ZipFile::SizeTarget t;
|
||||||
|
t.hash = ZipFile::fnvHash64(path.c_str(), path.size());
|
||||||
|
t.len = static_cast<uint16_t>(path.size());
|
||||||
|
t.index = static_cast<uint16_t>(i);
|
||||||
|
targets.push_back(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(targets.begin(), targets.end(), [](const ZipFile::SizeTarget& a, const ZipFile::SizeTarget& b) {
|
||||||
|
return a.hash < b.hash || (a.hash == b.hash && a.len < b.len);
|
||||||
|
});
|
||||||
|
|
||||||
|
spineSizes.resize(spineCount, 0);
|
||||||
|
int matched = zip.fillUncompressedSizes(targets, spineSizes);
|
||||||
|
Serial.printf("[%lu] [BMC] Batch lookup matched %d/%d spine items\n", millis(), matched, spineCount);
|
||||||
|
|
||||||
|
targets.clear();
|
||||||
|
targets.shrink_to_fit();
|
||||||
|
|
||||||
|
useBatchSizes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t cumSize = 0;
|
||||||
|
spineFile.seek(0);
|
||||||
|
int lastSpineTocIndex = -1;
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto spineEntry = readSpineEntry(spineFile);
|
||||||
|
|
||||||
|
spineEntry.tocIndex = spineToTocIndex[i];
|
||||||
|
|
||||||
|
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
|
||||||
|
// Logging here is for debugging
|
||||||
|
if (spineEntry.tocIndex == -1) {
|
||||||
|
Serial.printf(
|
||||||
|
"[%lu] [BMC] Warning: Could not find TOC entry for spine item %d: %s, using title from last section\n",
|
||||||
|
millis(), i, spineEntry.href.c_str());
|
||||||
|
spineEntry.tocIndex = lastSpineTocIndex;
|
||||||
|
}
|
||||||
|
lastSpineTocIndex = spineEntry.tocIndex;
|
||||||
|
|
||||||
|
size_t itemSize = 0;
|
||||||
|
if (useBatchSizes) {
|
||||||
|
itemSize = spineSizes[i];
|
||||||
|
if (itemSize == 0) {
|
||||||
|
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||||
|
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||||
|
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||||
|
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||||
|
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cumSize += itemSize;
|
||||||
|
spineEntry.cumulativeSize = cumSize;
|
||||||
|
|
||||||
|
// Write out spine data to book.bin
|
||||||
|
writeSpineEntry(bookFile, spineEntry);
|
||||||
|
}
|
||||||
|
// Close opened zip file
|
||||||
|
zip.close();
|
||||||
|
|
||||||
|
// Loop through toc entries from toc file writing to book.bin
|
||||||
|
tocFile.seek(0);
|
||||||
|
for (int i = 0; i < tocCount; i++) {
|
||||||
|
auto tocEntry = readTocEntry(tocFile);
|
||||||
|
writeTocEntry(bookFile, tocEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
bookFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
tocFile.close();
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [BMC] Successfully built book.bin\n", millis());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::cleanupTmpFiles() const {
|
||||||
|
if (SdMan.exists((cachePath + tmpSpineBinFile).c_str())) {
|
||||||
|
SdMan.remove((cachePath + tmpSpineBinFile).c_str());
|
||||||
|
}
|
||||||
|
if (SdMan.exists((cachePath + tmpTocBinFile).c_str())) {
|
||||||
|
SdMan.remove((cachePath + tmpTocBinFile).c_str());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t BookMetadataCache::writeSpineEntry(FsFile& file, const SpineEntry& entry) const {
|
||||||
|
const uint32_t pos = file.position();
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writePod(file, entry.cumulativeSize);
|
||||||
|
serialization::writePod(file, entry.tocIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t BookMetadataCache::writeTocEntry(FsFile& file, const TocEntry& entry) const {
|
||||||
|
const uint32_t pos = file.position();
|
||||||
|
serialization::writeString(file, entry.title);
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writeString(file, entry.anchor);
|
||||||
|
serialization::writePod(file, entry.level);
|
||||||
|
serialization::writePod(file, entry.spineIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
||||||
|
// this is because in this function we're marking positions of the items
|
||||||
|
void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||||
|
if (!buildMode || !spineFile) {
|
||||||
|
Serial.printf("[%lu] [BMC] createSpineEntry called but not in build mode\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpineEntry entry(href, 0, -1);
|
||||||
|
writeSpineEntry(spineFile, entry);
|
||||||
|
spineCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
||||||
|
const uint8_t level) {
|
||||||
|
if (!buildMode || !tocFile || !spineFile) {
|
||||||
|
Serial.printf("[%lu] [BMC] createTocEntry called but not in build mode\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t spineIndex = -1;
|
||||||
|
|
||||||
|
if (useSpineHrefIndex) {
|
||||||
|
uint64_t targetHash = fnvHash64(href);
|
||||||
|
uint16_t targetLen = static_cast<uint16_t>(href.size());
|
||||||
|
|
||||||
|
auto it =
|
||||||
|
std::lower_bound(spineHrefIndex.begin(), spineHrefIndex.end(), SpineHrefIndexEntry{targetHash, targetLen, 0},
|
||||||
|
[](const SpineHrefIndexEntry& a, const SpineHrefIndexEntry& b) {
|
||||||
|
return a.hrefHash < b.hrefHash || (a.hrefHash == b.hrefHash && a.hrefLen < b.hrefLen);
|
||||||
|
});
|
||||||
|
|
||||||
|
while (it != spineHrefIndex.end() && it->hrefHash == targetHash && it->hrefLen == targetLen) {
|
||||||
|
spineIndex = it->spineIndex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spineIndex == -1) {
|
||||||
|
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spineFile.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto spineEntry = readSpineEntry(spineFile);
|
||||||
|
if (spineEntry.href == href) {
|
||||||
|
spineIndex = static_cast<int16_t>(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (spineIndex == -1) {
|
||||||
|
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TocEntry entry(title, href, anchor, level, spineIndex);
|
||||||
|
writeTocEntry(tocFile, entry);
|
||||||
|
tocCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============= READING / LOADING FUNCTIONS ================ */
|
||||||
|
|
||||||
|
bool BookMetadataCache::load() {
|
||||||
|
if (!SdMan.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(bookFile, version);
|
||||||
|
if (version != BOOK_CACHE_VERSION) {
|
||||||
|
Serial.printf("[%lu] [BMC] Cache version mismatch: expected %d, got %d\n", millis(), BOOK_CACHE_VERSION, version);
|
||||||
|
bookFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialization::readPod(bookFile, lutOffset);
|
||||||
|
serialization::readPod(bookFile, spineCount);
|
||||||
|
serialization::readPod(bookFile, tocCount);
|
||||||
|
|
||||||
|
serialization::readString(bookFile, coreMetadata.title);
|
||||||
|
serialization::readString(bookFile, coreMetadata.author);
|
||||||
|
serialization::readString(bookFile, coreMetadata.language);
|
||||||
|
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
||||||
|
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||||
|
|
||||||
|
loaded = true;
|
||||||
|
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
||||||
|
if (!loaded) {
|
||||||
|
Serial.printf("[%lu] [BMC] getSpineEntry called but cache not loaded\n", millis());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
||||||
|
Serial.printf("[%lu] [BMC] getSpineEntry index %d out of range\n", millis(), index);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to spine LUT item, read from LUT and get out data
|
||||||
|
bookFile.seek(lutOffset + sizeof(uint32_t) * index);
|
||||||
|
uint32_t spineEntryPos;
|
||||||
|
serialization::readPod(bookFile, spineEntryPos);
|
||||||
|
bookFile.seek(spineEntryPos);
|
||||||
|
return readSpineEntry(bookFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||||
|
if (!loaded) {
|
||||||
|
Serial.printf("[%lu] [BMC] getTocEntry called but cache not loaded\n", millis());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
||||||
|
Serial.printf("[%lu] [BMC] getTocEntry index %d out of range\n", millis(), index);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to TOC LUT item, read from LUT and get out data
|
||||||
|
bookFile.seek(lutOffset + sizeof(uint32_t) * spineCount + sizeof(uint32_t) * index);
|
||||||
|
uint32_t tocEntryPos;
|
||||||
|
serialization::readPod(bookFile, tocEntryPos);
|
||||||
|
bookFile.seek(tocEntryPos);
|
||||||
|
return readTocEntry(bookFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(FsFile& file) const {
|
||||||
|
SpineEntry entry;
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readPod(file, entry.cumulativeSize);
|
||||||
|
serialization::readPod(file, entry.tocIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(FsFile& file) const {
|
||||||
|
TocEntry entry;
|
||||||
|
serialization::readString(file, entry.title);
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readString(file, entry.anchor);
|
||||||
|
serialization::readPod(file, entry.level);
|
||||||
|
serialization::readPod(file, entry.spineIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class BookMetadataCache {
|
||||||
|
public:
|
||||||
|
struct BookMetadata {
|
||||||
|
std::string title;
|
||||||
|
std::string author;
|
||||||
|
std::string language;
|
||||||
|
std::string coverItemHref;
|
||||||
|
std::string textReferenceHref;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SpineEntry {
|
||||||
|
std::string href;
|
||||||
|
size_t cumulativeSize;
|
||||||
|
int16_t tocIndex;
|
||||||
|
|
||||||
|
SpineEntry() : cumulativeSize(0), tocIndex(-1) {}
|
||||||
|
SpineEntry(std::string href, const size_t cumulativeSize, const int16_t tocIndex)
|
||||||
|
: href(std::move(href)), cumulativeSize(cumulativeSize), tocIndex(tocIndex) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TocEntry {
|
||||||
|
std::string title;
|
||||||
|
std::string href;
|
||||||
|
std::string anchor;
|
||||||
|
uint8_t level;
|
||||||
|
int16_t spineIndex;
|
||||||
|
|
||||||
|
TocEntry() : level(0), spineIndex(-1) {}
|
||||||
|
TocEntry(std::string title, std::string href, std::string anchor, const uint8_t level, const int16_t spineIndex)
|
||||||
|
: title(std::move(title)),
|
||||||
|
href(std::move(href)),
|
||||||
|
anchor(std::move(anchor)),
|
||||||
|
level(level),
|
||||||
|
spineIndex(spineIndex) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string cachePath;
|
||||||
|
size_t lutOffset;
|
||||||
|
uint16_t spineCount;
|
||||||
|
uint16_t tocCount;
|
||||||
|
bool loaded;
|
||||||
|
bool buildMode;
|
||||||
|
|
||||||
|
FsFile bookFile;
|
||||||
|
// Temp file handles during build
|
||||||
|
FsFile spineFile;
|
||||||
|
FsFile tocFile;
|
||||||
|
|
||||||
|
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
||||||
|
struct SpineHrefIndexEntry {
|
||||||
|
uint64_t hrefHash; // FNV-1a 64-bit hash
|
||||||
|
uint16_t hrefLen; // length for collision reduction
|
||||||
|
int16_t spineIndex;
|
||||||
|
};
|
||||||
|
std::vector<SpineHrefIndexEntry> spineHrefIndex;
|
||||||
|
bool useSpineHrefIndex = false;
|
||||||
|
|
||||||
|
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
||||||
|
|
||||||
|
// FNV-1a 64-bit hash function
|
||||||
|
static uint64_t fnvHash64(const std::string& s) {
|
||||||
|
uint64_t hash = 14695981039346656037ull;
|
||||||
|
for (char c : s) {
|
||||||
|
hash ^= static_cast<uint8_t>(c);
|
||||||
|
hash *= 1099511628211ull;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t writeSpineEntry(FsFile& file, const SpineEntry& entry) const;
|
||||||
|
uint32_t writeTocEntry(FsFile& file, const TocEntry& entry) const;
|
||||||
|
SpineEntry readSpineEntry(FsFile& file) const;
|
||||||
|
TocEntry readTocEntry(FsFile& file) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
BookMetadata coreMetadata;
|
||||||
|
|
||||||
|
explicit BookMetadataCache(std::string cachePath)
|
||||||
|
: cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
|
||||||
|
~BookMetadataCache() = default;
|
||||||
|
|
||||||
|
// Building phase (stream to disk immediately)
|
||||||
|
bool beginWrite();
|
||||||
|
bool beginContentOpfPass();
|
||||||
|
void createSpineEntry(const std::string& href);
|
||||||
|
bool endContentOpfPass();
|
||||||
|
bool beginTocPass();
|
||||||
|
void createTocEntry(const std::string& title, const std::string& href, const std::string& anchor, uint8_t level);
|
||||||
|
bool endTocPass();
|
||||||
|
bool endWrite();
|
||||||
|
bool cleanupTmpFiles() const;
|
||||||
|
|
||||||
|
// Post-processing to update mappings and sizes
|
||||||
|
bool buildBookBin(const std::string& epubPath, const BookMetadata& metadata);
|
||||||
|
|
||||||
|
// Reading phase (read mode)
|
||||||
|
bool load();
|
||||||
|
SpineEntry getSpineEntry(int index);
|
||||||
|
TocEntry getTocEntry(int index);
|
||||||
|
int getSpineCount() const { return spineCount; }
|
||||||
|
int getTocCount() const { return tocCount; }
|
||||||
|
bool isLoaded() const { return loaded; }
|
||||||
|
};
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#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) {}
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#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);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class FsHelpers {
|
|
||||||
public:
|
|
||||||
static bool removeDir(const char* path);
|
|
||||||
};
|
|
||||||
@@ -3,66 +3,61 @@
|
|||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
constexpr uint8_t PAGE_FILE_VERSION = 3;
|
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
|
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
|
||||||
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }
|
|
||||||
|
|
||||||
void PageLine::serialize(std::ostream& os) {
|
|
||||||
serialization::writePod(os, xPos);
|
|
||||||
serialization::writePod(os, yPos);
|
|
||||||
|
|
||||||
// serialize TextBlock pointed to by PageLine
|
|
||||||
block->serialize(os);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<PageLine> PageLine::deserialize(std::istream& is) {
|
bool PageLine::serialize(FsFile& file) {
|
||||||
|
serialization::writePod(file, xPos);
|
||||||
|
serialization::writePod(file, yPos);
|
||||||
|
|
||||||
|
// serialize TextBlock pointed to by PageLine
|
||||||
|
return block->serialize(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
|
||||||
int16_t xPos;
|
int16_t xPos;
|
||||||
int16_t yPos;
|
int16_t yPos;
|
||||||
serialization::readPod(is, xPos);
|
serialization::readPod(file, xPos);
|
||||||
serialization::readPod(is, yPos);
|
serialization::readPod(file, yPos);
|
||||||
|
|
||||||
auto tb = TextBlock::deserialize(is);
|
auto tb = TextBlock::deserialize(file);
|
||||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Page::render(GfxRenderer& renderer, const int fontId) const {
|
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||||
for (auto& element : elements) {
|
for (auto& element : elements) {
|
||||||
element->render(renderer, fontId);
|
element->render(renderer, fontId, xOffset, yOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Page::serialize(std::ostream& os) const {
|
bool Page::serialize(FsFile& file) const {
|
||||||
serialization::writePod(os, PAGE_FILE_VERSION);
|
const uint16_t count = elements.size();
|
||||||
|
serialization::writePod(file, count);
|
||||||
const uint32_t count = elements.size();
|
|
||||||
serialization::writePod(os, count);
|
|
||||||
|
|
||||||
for (const auto& el : elements) {
|
for (const auto& el : elements) {
|
||||||
// Only PageLine exists currently
|
// Only PageLine exists currently
|
||||||
serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine));
|
serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine));
|
||||||
el->serialize(os);
|
if (!el->serialize(file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Page> Page::deserialize(std::istream& is) {
|
std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
||||||
uint8_t version;
|
|
||||||
serialization::readPod(is, version);
|
|
||||||
if (version != PAGE_FILE_VERSION) {
|
|
||||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown version %u\n", millis(), version);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto page = std::unique_ptr<Page>(new Page());
|
auto page = std::unique_ptr<Page>(new Page());
|
||||||
|
|
||||||
uint32_t count;
|
uint16_t count;
|
||||||
serialization::readPod(is, count);
|
serialization::readPod(file, count);
|
||||||
|
|
||||||
for (uint32_t i = 0; i < count; i++) {
|
for (uint16_t i = 0; i < count; i++) {
|
||||||
uint8_t tag;
|
uint8_t tag;
|
||||||
serialization::readPod(is, tag);
|
serialization::readPod(file, tag);
|
||||||
|
|
||||||
if (tag == TAG_PageLine) {
|
if (tag == TAG_PageLine) {
|
||||||
auto pl = PageLine::deserialize(is);
|
auto pl = PageLine::deserialize(file);
|
||||||
page->elements.push_back(std::move(pl));
|
page->elements.push_back(std::move(pl));
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <SdFat.h>
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -15,8 +17,8 @@ class PageElement {
|
|||||||
int16_t yPos;
|
int16_t yPos;
|
||||||
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
|
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
|
||||||
virtual ~PageElement() = default;
|
virtual ~PageElement() = default;
|
||||||
virtual void render(GfxRenderer& renderer, int fontId) = 0;
|
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||||||
virtual void serialize(std::ostream& os) = 0;
|
virtual bool serialize(FsFile& file) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// a line from a block element
|
// a line from a block element
|
||||||
@@ -26,16 +28,16 @@ class PageLine final : public PageElement {
|
|||||||
public:
|
public:
|
||||||
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
||||||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||||
void render(GfxRenderer& renderer, int fontId) override;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
void serialize(std::ostream& os) override;
|
bool serialize(FsFile& file) override;
|
||||||
static std::unique_ptr<PageLine> deserialize(std::istream& is);
|
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
||||||
};
|
};
|
||||||
|
|
||||||
class Page {
|
class Page {
|
||||||
public:
|
public:
|
||||||
// the list of block index and line numbers on this page
|
// the list of block index and line numbers on this page
|
||||||
std::vector<std::shared_ptr<PageElement>> elements;
|
std::vector<std::shared_ptr<PageElement>> elements;
|
||||||
void render(GfxRenderer& renderer, int fontId) const;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
void serialize(std::ostream& os) const;
|
bool serialize(FsFile& file) const;
|
||||||
static std::unique_ptr<Page> deserialize(std::istream& is);
|
static std::unique_ptr<Page> deserialize(FsFile& file);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,12 +5,51 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <iterator>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "hyphenation/Hyphenator.h"
|
||||||
|
|
||||||
constexpr int MAX_COST = std::numeric_limits<int>::max();
|
constexpr int MAX_COST = std::numeric_limits<int>::max();
|
||||||
|
|
||||||
void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) {
|
namespace {
|
||||||
|
|
||||||
|
// Soft hyphen byte pattern used throughout EPUBs (UTF-8 for U+00AD).
|
||||||
|
constexpr char SOFT_HYPHEN_UTF8[] = "\xC2\xAD";
|
||||||
|
constexpr size_t SOFT_HYPHEN_BYTES = 2;
|
||||||
|
|
||||||
|
bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; }
|
||||||
|
|
||||||
|
// Removes every soft hyphen in-place so rendered glyphs match measured widths.
|
||||||
|
void stripSoftHyphensInPlace(std::string& word) {
|
||||||
|
size_t pos = 0;
|
||||||
|
while ((pos = word.find(SOFT_HYPHEN_UTF8, pos)) != std::string::npos) {
|
||||||
|
word.erase(pos, SOFT_HYPHEN_BYTES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the rendered width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
|
||||||
|
uint16_t measureWordWidth(GfxRenderer& renderer, const int fontId, const std::string& word,
|
||||||
|
const CrossPointFont::Style style, const bool appendHyphen = false) {
|
||||||
|
const bool hasSoftHyphen = containsSoftHyphen(word);
|
||||||
|
if (!hasSoftHyphen && !appendHyphen) {
|
||||||
|
return renderer.getTextWidth(fontId, word.c_str(), style);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string sanitized = word;
|
||||||
|
if (hasSoftHyphen) {
|
||||||
|
stripSoftHyphensInPlace(sanitized);
|
||||||
|
}
|
||||||
|
if (appendHyphen) {
|
||||||
|
sanitized.push_back('-');
|
||||||
|
}
|
||||||
|
return renderer.getTextWidth(fontId, sanitized.c_str(), style);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ParsedText::addWord(std::string word, const CrossPointFont::Style fontStyle) {
|
||||||
if (word.empty()) return;
|
if (word.empty()) return;
|
||||||
|
|
||||||
words.push_back(std::move(word));
|
words.push_back(std::move(word));
|
||||||
@@ -18,17 +57,35 @@ void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Consumes data to minimize memory usage
|
// Consumes data to minimize memory usage
|
||||||
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const int horizontalMargin,
|
void ParsedText::layoutAndExtractLines(GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth,
|
||||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||||
|
const bool includeLastLine) {
|
||||||
if (words.empty()) {
|
if (words.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t totalWordCount = words.size();
|
// Apply fixed transforms before any per-line layout work.
|
||||||
const int pageWidth = renderer.getScreenWidth() - horizontalMargin;
|
applyParagraphIndent();
|
||||||
|
|
||||||
|
const int pageWidth = viewportWidth;
|
||||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||||
// width of 1em to indent first line of paragraph if Extra Spacing is enabled
|
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||||
const int indentWidth = (!extraParagraphSpacing) ? 1 * renderer.getTextWidth(fontId, "m", REGULAR) : 0;
|
std::vector<size_t> lineBreakIndices;
|
||||||
|
if (hyphenationEnabled) {
|
||||||
|
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
||||||
|
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths);
|
||||||
|
} else {
|
||||||
|
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths);
|
||||||
|
}
|
||||||
|
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < lineCount; ++i) {
|
||||||
|
extractLine(i, pageWidth, spaceWidth, wordWidths, lineBreakIndices, processLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint16_t> ParsedText::calculateWordWidths(GfxRenderer& renderer, const int fontId) {
|
||||||
|
const size_t totalWordCount = words.size();
|
||||||
|
|
||||||
std::vector<uint16_t> wordWidths;
|
std::vector<uint16_t> wordWidths;
|
||||||
wordWidths.reserve(totalWordCount);
|
wordWidths.reserve(totalWordCount);
|
||||||
@@ -37,12 +94,32 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
|||||||
auto wordStylesIt = wordStyles.begin();
|
auto wordStylesIt = wordStyles.begin();
|
||||||
|
|
||||||
while (wordsIt != words.end()) {
|
while (wordsIt != words.end()) {
|
||||||
wordWidths.push_back(renderer.getTextWidth(fontId, wordsIt->c_str(), *wordStylesIt));
|
wordWidths.push_back(measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt));
|
||||||
|
|
||||||
std::advance(wordsIt, 1);
|
std::advance(wordsIt, 1);
|
||||||
std::advance(wordStylesIt, 1);
|
std::advance(wordStylesIt, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return wordWidths;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<size_t> ParsedText::computeLineBreaks(GfxRenderer& renderer, const int fontId, const int pageWidth,
|
||||||
|
const int spaceWidth, std::vector<uint16_t>& wordWidths) {
|
||||||
|
if (words.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation.
|
||||||
|
for (size_t i = 0; i < wordWidths.size(); ++i) {
|
||||||
|
while (wordWidths[i] > pageWidth) {
|
||||||
|
if (!hyphenateWordAtIndex(i, pageWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t totalWordCount = words.size();
|
||||||
|
|
||||||
// DP table to store the minimum badness (cost) of lines starting at index i
|
// DP table to store the minimum badness (cost) of lines starting at index i
|
||||||
std::vector<int> dp(totalWordCount);
|
std::vector<int> dp(totalWordCount);
|
||||||
// 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i'
|
// 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i'
|
||||||
@@ -53,7 +130,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
|||||||
ans[totalWordCount - 1] = totalWordCount - 1;
|
ans[totalWordCount - 1] = totalWordCount - 1;
|
||||||
|
|
||||||
for (int i = totalWordCount - 2; i >= 0; --i) {
|
for (int i = totalWordCount - 2; i >= 0; --i) {
|
||||||
int currlen = -spaceWidth + indentWidth;
|
int currlen = -spaceWidth;
|
||||||
dp[i] = MAX_COST;
|
dp[i] = MAX_COST;
|
||||||
|
|
||||||
for (size_t j = i; j < totalWordCount; ++j) {
|
for (size_t j = i; j < totalWordCount; ++j) {
|
||||||
@@ -84,88 +161,228 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
|||||||
ans[i] = j; // j is the index of the last word in this optimal line
|
ans[i] = j; // j is the index of the last word in this optimal line
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle oversized word: if no valid configuration found, force single-word line
|
||||||
|
// This prevents cascade failure where one oversized word breaks all preceding words
|
||||||
|
if (dp[i] == MAX_COST) {
|
||||||
|
ans[i] = i; // Just this word on its own line
|
||||||
|
// Inherit cost from next word to allow subsequent words to find valid configurations
|
||||||
|
if (i + 1 < static_cast<int>(totalWordCount)) {
|
||||||
|
dp[i] = dp[i + 1];
|
||||||
|
} else {
|
||||||
|
dp[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stores the index of the word that starts the next line (last_word_index + 1)
|
// Stores the index of the word that starts the next line (last_word_index + 1)
|
||||||
std::vector<size_t> lineBreakIndices;
|
std::vector<size_t> lineBreakIndices;
|
||||||
size_t currentWordIndex = 0;
|
size_t currentWordIndex = 0;
|
||||||
constexpr size_t MAX_LINES = 1000;
|
|
||||||
|
|
||||||
while (currentWordIndex < totalWordCount) {
|
while (currentWordIndex < totalWordCount) {
|
||||||
if (lineBreakIndices.size() >= MAX_LINES) {
|
size_t nextBreakIndex = ans[currentWordIndex] + 1;
|
||||||
break;
|
|
||||||
|
// Safety check: prevent infinite loop if nextBreakIndex doesn't advance
|
||||||
|
if (nextBreakIndex <= currentWordIndex) {
|
||||||
|
// Force advance by at least one word to avoid infinite loop
|
||||||
|
nextBreakIndex = currentWordIndex + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t nextBreakIndex = ans[currentWordIndex] + 1;
|
|
||||||
lineBreakIndices.push_back(nextBreakIndex);
|
lineBreakIndices.push_back(nextBreakIndex);
|
||||||
|
|
||||||
currentWordIndex = nextBreakIndex;
|
currentWordIndex = nextBreakIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize iterators for consumption
|
return lineBreakIndices;
|
||||||
auto wordStartIt = words.begin();
|
}
|
||||||
auto wordStyleStartIt = wordStyles.begin();
|
|
||||||
size_t wordWidthIndex = 0;
|
|
||||||
|
|
||||||
size_t lastBreakAt = 0;
|
void ParsedText::applyParagraphIndent() {
|
||||||
for (const size_t lineBreak : lineBreakIndices) {
|
if (extraParagraphSpacing || words.empty()) {
|
||||||
const size_t lineWordCount = lineBreak - lastBreakAt;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate end iterators for the range to splice
|
if (style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) {
|
||||||
auto wordEndIt = wordStartIt;
|
words.front().insert(0, "\xe2\x80\x83");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
||||||
|
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(GfxRenderer& renderer, const int fontId,
|
||||||
|
const int pageWidth, const int spaceWidth,
|
||||||
|
std::vector<uint16_t>& wordWidths) {
|
||||||
|
std::vector<size_t> lineBreakIndices;
|
||||||
|
size_t currentIndex = 0;
|
||||||
|
|
||||||
|
while (currentIndex < wordWidths.size()) {
|
||||||
|
const size_t lineStart = currentIndex;
|
||||||
|
int lineWidth = 0;
|
||||||
|
|
||||||
|
// Consume as many words as possible for current line, splitting when prefixes fit
|
||||||
|
while (currentIndex < wordWidths.size()) {
|
||||||
|
const bool isFirstWord = currentIndex == lineStart;
|
||||||
|
const int spacing = isFirstWord ? 0 : spaceWidth;
|
||||||
|
const int candidateWidth = spacing + wordWidths[currentIndex];
|
||||||
|
|
||||||
|
// Word fits on current line
|
||||||
|
if (lineWidth + candidateWidth <= pageWidth) {
|
||||||
|
lineWidth += candidateWidth;
|
||||||
|
++currentIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word would overflow — try to split based on hyphenation points
|
||||||
|
const int availableWidth = pageWidth - lineWidth - spacing;
|
||||||
|
const bool allowFallbackBreaks = isFirstWord; // Only for first word on line
|
||||||
|
|
||||||
|
if (availableWidth > 0 &&
|
||||||
|
hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths, allowFallbackBreaks)) {
|
||||||
|
// Prefix now fits; append it to this line and move to next line
|
||||||
|
lineWidth += spacing + wordWidths[currentIndex];
|
||||||
|
++currentIndex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could not split: force at least one word per line to avoid infinite loop
|
||||||
|
if (currentIndex == lineStart) {
|
||||||
|
lineWidth += candidateWidth;
|
||||||
|
++currentIndex;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineBreakIndices.push_back(currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lineBreakIndices;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits words[wordIndex] into prefix (adding a hyphen only when needed) and remainder when a legal breakpoint fits the
|
||||||
|
// available width.
|
||||||
|
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, GfxRenderer& renderer,
|
||||||
|
const int fontId, std::vector<uint16_t>& wordWidths,
|
||||||
|
const bool allowFallbackBreaks) {
|
||||||
|
// Guard against invalid indices or zero available width before attempting to split.
|
||||||
|
if (availableWidth <= 0 || wordIndex >= words.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get iterators to target word and style.
|
||||||
|
auto wordIt = words.begin();
|
||||||
|
auto styleIt = wordStyles.begin();
|
||||||
|
std::advance(wordIt, wordIndex);
|
||||||
|
std::advance(styleIt, wordIndex);
|
||||||
|
|
||||||
|
const std::string& word = *wordIt;
|
||||||
|
const auto style = *styleIt;
|
||||||
|
|
||||||
|
// Collect candidate breakpoints (byte offsets and hyphen requirements).
|
||||||
|
auto breakInfos = Hyphenator::breakOffsets(word, allowFallbackBreaks);
|
||||||
|
if (breakInfos.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t chosenOffset = 0;
|
||||||
|
int chosenWidth = -1;
|
||||||
|
bool chosenNeedsHyphen = true;
|
||||||
|
|
||||||
|
// Iterate over each legal breakpoint and retain the widest prefix that still fits.
|
||||||
|
for (const auto& info : breakInfos) {
|
||||||
|
const size_t offset = info.byteOffset;
|
||||||
|
if (offset == 0 || offset >= word.size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool needsHyphen = info.requiresInsertedHyphen;
|
||||||
|
const int prefixWidth = measureWordWidth(renderer, fontId, word.substr(0, offset), style, needsHyphen);
|
||||||
|
if (prefixWidth > availableWidth || prefixWidth <= chosenWidth) {
|
||||||
|
continue; // Skip if too wide or not an improvement
|
||||||
|
}
|
||||||
|
|
||||||
|
chosenWidth = prefixWidth;
|
||||||
|
chosenOffset = offset;
|
||||||
|
chosenNeedsHyphen = needsHyphen;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosenWidth < 0) {
|
||||||
|
// No hyphenation point produced a prefix that fits in the remaining space.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the word at the selected breakpoint and append a hyphen if required.
|
||||||
|
std::string remainder = word.substr(chosenOffset);
|
||||||
|
wordIt->resize(chosenOffset);
|
||||||
|
if (chosenNeedsHyphen) {
|
||||||
|
wordIt->push_back('-');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the remainder word (with matching style) directly after the prefix.
|
||||||
|
auto insertWordIt = std::next(wordIt);
|
||||||
|
auto insertStyleIt = std::next(styleIt);
|
||||||
|
words.insert(insertWordIt, remainder);
|
||||||
|
wordStyles.insert(insertStyleIt, style);
|
||||||
|
|
||||||
|
// Update cached widths to reflect the new prefix/remainder pairing.
|
||||||
|
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
|
||||||
|
const uint16_t remainderWidth = measureWordWidth(renderer, fontId, remainder, style);
|
||||||
|
wordWidths.insert(wordWidths.begin() + wordIndex + 1, remainderWidth);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
|
||||||
|
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
|
||||||
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
||||||
|
const size_t lineBreak = lineBreakIndices[breakIndex];
|
||||||
|
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
|
||||||
|
const size_t lineWordCount = lineBreak - lastBreakAt;
|
||||||
|
|
||||||
|
// Calculate total word width for this line
|
||||||
|
int lineWordWidthSum = 0;
|
||||||
|
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||||
|
lineWordWidthSum += wordWidths[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate spacing
|
||||||
|
const int spareSpace = pageWidth - lineWordWidthSum;
|
||||||
|
|
||||||
|
int spacing = spaceWidth;
|
||||||
|
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
|
||||||
|
|
||||||
|
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
|
||||||
|
spacing = spareSpace / (lineWordCount - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate initial x position
|
||||||
|
uint16_t xpos = 0;
|
||||||
|
if (style == TextBlock::RIGHT_ALIGN) {
|
||||||
|
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||||
|
} else if (style == TextBlock::CENTER_ALIGN) {
|
||||||
|
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-calculate X positions for words
|
||||||
|
std::list<uint16_t> lineXPos;
|
||||||
|
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||||
|
const uint16_t currentWordWidth = wordWidths[i];
|
||||||
|
lineXPos.push_back(xpos);
|
||||||
|
xpos += currentWordWidth + spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterators always start at the beginning as we are moving content with splice below
|
||||||
|
auto wordEndIt = words.begin();
|
||||||
|
auto wordStyleEndIt = wordStyles.begin();
|
||||||
|
std::advance(wordEndIt, lineWordCount);
|
||||||
|
std::advance(wordStyleEndIt, lineWordCount);
|
||||||
|
|
||||||
|
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||||
|
std::list<std::string> lineWords;
|
||||||
|
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
|
||||||
|
std::list<CrossPointFont::Style> lineWordStyles;
|
||||||
|
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
||||||
|
|
||||||
|
for (auto& word : lineWords) {
|
||||||
|
if (containsSoftHyphen(word)) {
|
||||||
|
stripSoftHyphensInPlace(word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <EpdFontFamily.h>
|
#include <CrossPointFont.h>
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "blocks/TextBlock.h"
|
#include "blocks/TextBlock.h"
|
||||||
|
|
||||||
@@ -14,19 +14,35 @@ class GfxRenderer;
|
|||||||
|
|
||||||
class ParsedText {
|
class ParsedText {
|
||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<EpdFontStyle> wordStyles;
|
std::list<CrossPointFont::Style> wordStyles;
|
||||||
TextBlock::BLOCK_STYLE style;
|
TextBlock::Style style;
|
||||||
bool extraParagraphSpacing;
|
bool extraParagraphSpacing;
|
||||||
|
bool hyphenationEnabled;
|
||||||
|
|
||||||
|
void applyParagraphIndent();
|
||||||
|
std::vector<size_t> computeLineBreaks(GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth,
|
||||||
|
std::vector<uint16_t>& wordWidths);
|
||||||
|
std::vector<size_t> computeHyphenatedLineBreaks(GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth,
|
||||||
|
std::vector<uint16_t>& wordWidths);
|
||||||
|
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, GfxRenderer& renderer, int fontId,
|
||||||
|
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||||
|
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
||||||
|
const std::vector<size_t>& lineBreakIndices,
|
||||||
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
||||||
|
std::vector<uint16_t> calculateWordWidths(GfxRenderer& renderer, int fontId);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ParsedText(const TextBlock::BLOCK_STYLE style, const bool extraParagraphSpacing)
|
explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing,
|
||||||
: style(style), extraParagraphSpacing(extraParagraphSpacing) {}
|
const bool hyphenationEnabled = false)
|
||||||
|
: style(style), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
|
||||||
~ParsedText() = default;
|
~ParsedText() = default;
|
||||||
|
|
||||||
void addWord(std::string word, EpdFontStyle fontStyle);
|
void addWord(std::string word, CrossPointFont::Style fontStyle);
|
||||||
void setStyle(const TextBlock::BLOCK_STYLE style) { this->style = style; }
|
void setStyle(const TextBlock::Style style) { this->style = style; }
|
||||||
TextBlock::BLOCK_STYLE getStyle() const { return style; }
|
TextBlock::Style getStyle() const { return style; }
|
||||||
|
size_t size() const { return words.size(); }
|
||||||
bool isEmpty() const { return words.empty(); }
|
bool isEmpty() const { return words.empty(); }
|
||||||
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, int horizontalMargin,
|
void layoutAndExtractLines(GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
||||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||||
|
bool includeLastLine = true);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,109 +1,117 @@
|
|||||||
#include "Section.h"
|
#include "Section.h"
|
||||||
|
|
||||||
#include <SD.h>
|
#include <SDCardManager.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
#include "FsHelpers.h"
|
|
||||||
#include "Page.h"
|
#include "Page.h"
|
||||||
|
#include "hyphenation/Hyphenator.h"
|
||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 5;
|
namespace {
|
||||||
|
constexpr uint8_t SECTION_FILE_VERSION = 10;
|
||||||
|
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||||
|
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) +
|
||||||
|
sizeof(uint32_t);
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void Section::onPageComplete(std::unique_ptr<Page> page) {
|
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||||
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
|
if (!file) {
|
||||||
|
Serial.printf("[%lu] [SCT] File not open for writing page %d\n", millis(), pageCount);
|
||||||
std::ofstream outputFile("/sd" + filePath);
|
return 0;
|
||||||
page->serialize(outputFile);
|
}
|
||||||
outputFile.close();
|
|
||||||
|
|
||||||
|
const uint32_t position = file.position();
|
||||||
|
if (!page->serialize(file)) {
|
||||||
|
Serial.printf("[%lu] [SCT] Failed to serialize page %d\n", millis(), pageCount);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
|
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
|
||||||
|
|
||||||
pageCount++;
|
pageCount++;
|
||||||
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const int marginRight, const int marginBottom, const int marginLeft,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
const bool extraParagraphSpacing) const {
|
const uint16_t viewportHeight, const bool hyphenationEnabled) {
|
||||||
std::ofstream outputFile(("/sd" + cachePath + "/section.bin").c_str());
|
if (!file) {
|
||||||
serialization::writePod(outputFile, SECTION_FILE_VERSION);
|
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
|
||||||
serialization::writePod(outputFile, fontId);
|
return;
|
||||||
serialization::writePod(outputFile, lineCompression);
|
}
|
||||||
serialization::writePod(outputFile, marginTop);
|
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||||
serialization::writePod(outputFile, marginRight);
|
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
||||||
serialization::writePod(outputFile, marginBottom);
|
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
|
||||||
serialization::writePod(outputFile, marginLeft);
|
sizeof(uint32_t),
|
||||||
serialization::writePod(outputFile, extraParagraphSpacing);
|
"Header size mismatch");
|
||||||
serialization::writePod(outputFile, pageCount);
|
serialization::writePod(file, SECTION_FILE_VERSION);
|
||||||
outputFile.close();
|
serialization::writePod(file, fontId);
|
||||||
|
serialization::writePod(file, lineCompression);
|
||||||
|
serialization::writePod(file, extraParagraphSpacing);
|
||||||
|
serialization::writePod(file, paragraphAlignment);
|
||||||
|
serialization::writePod(file, viewportWidth);
|
||||||
|
serialization::writePod(file, viewportHeight);
|
||||||
|
serialization::writePod(file, hyphenationEnabled);
|
||||||
|
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
|
||||||
|
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const int marginRight, const int marginBottom, const int marginLeft,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
const bool extraParagraphSpacing) {
|
const uint16_t viewportHeight, const bool hyphenationEnabled) {
|
||||||
if (!SD.exists(cachePath.c_str())) {
|
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto sectionFilePath = cachePath + "/section.bin";
|
|
||||||
if (!SD.exists(sectionFilePath.c_str())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ifstream inputFile(("/sd" + sectionFilePath).c_str());
|
|
||||||
|
|
||||||
// Match parameters
|
// Match parameters
|
||||||
{
|
{
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(inputFile, version);
|
serialization::readPod(file, version);
|
||||||
if (version != SECTION_FILE_VERSION) {
|
if (version != SECTION_FILE_VERSION) {
|
||||||
inputFile.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
|
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||||
clearCache();
|
clearCache();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int fileFontId, fileMarginTop, fileMarginRight, fileMarginBottom, fileMarginLeft;
|
int fileFontId;
|
||||||
|
uint16_t fileViewportWidth, fileViewportHeight;
|
||||||
float fileLineCompression;
|
float fileLineCompression;
|
||||||
bool fileExtraParagraphSpacing;
|
bool fileExtraParagraphSpacing;
|
||||||
serialization::readPod(inputFile, fileFontId);
|
uint8_t fileParagraphAlignment;
|
||||||
serialization::readPod(inputFile, fileLineCompression);
|
bool fileHyphenationEnabled;
|
||||||
serialization::readPod(inputFile, fileMarginTop);
|
serialization::readPod(file, fileFontId);
|
||||||
serialization::readPod(inputFile, fileMarginRight);
|
serialization::readPod(file, fileLineCompression);
|
||||||
serialization::readPod(inputFile, fileMarginBottom);
|
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||||
serialization::readPod(inputFile, fileMarginLeft);
|
serialization::readPod(file, fileParagraphAlignment);
|
||||||
serialization::readPod(inputFile, fileExtraParagraphSpacing);
|
serialization::readPod(file, fileViewportWidth);
|
||||||
|
serialization::readPod(file, fileViewportHeight);
|
||||||
|
serialization::readPod(file, fileHyphenationEnabled);
|
||||||
|
|
||||||
if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop ||
|
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||||
marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft ||
|
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||||
extraParagraphSpacing != fileExtraParagraphSpacing) {
|
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||||
inputFile.close();
|
hyphenationEnabled != fileHyphenationEnabled) {
|
||||||
|
file.close();
|
||||||
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
||||||
clearCache();
|
clearCache();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serialization::readPod(inputFile, pageCount);
|
serialization::readPod(file, pageCount);
|
||||||
inputFile.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
|
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Section::setupCacheDir() const {
|
|
||||||
epub->setupCacheDir();
|
|
||||||
SD.mkdir(cachePath.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
||||||
bool Section::clearCache() const {
|
bool Section::clearCache() const {
|
||||||
if (!SD.exists(cachePath.c_str())) {
|
if (!SdMan.exists(filePath.c_str())) {
|
||||||
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
|
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!FsHelpers::removeDir(cachePath.c_str())) {
|
if (!SdMan.remove(filePath.c_str())) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis());
|
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -112,53 +120,125 @@ bool Section::clearCache() const {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
|
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const int marginRight, const int marginBottom, const int marginLeft,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
const bool extraParagraphSpacing) {
|
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||||
const auto localPath = epub->getSpineItem(spineIndex);
|
const std::function<void()>& progressSetupFn,
|
||||||
|
const std::function<void(int)>& progressFn) {
|
||||||
// TODO: Should we get rid of this file all together?
|
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
||||||
// It currently saves us a bit of memory by allowing for all the inflation bits to be released
|
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||||
// before loading the XML parser
|
|
||||||
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||||
File f = SD.open(tmpHtmlPath.c_str(), FILE_WRITE, true);
|
|
||||||
bool success = epub->readItemContentsToStream(localPath, f, 1024);
|
// Create cache directory if it doesn't exist
|
||||||
f.close();
|
{
|
||||||
|
const auto sectionsDir = epub->getCachePath() + "/sections";
|
||||||
|
SdMan.mkdir(sectionsDir.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry logic for SD card timing issues
|
||||||
|
bool success = false;
|
||||||
|
uint32_t fileSize = 0;
|
||||||
|
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
||||||
|
if (attempt > 0) {
|
||||||
|
Serial.printf("[%lu] [SCT] Retrying stream (attempt %d)...\n", millis(), attempt + 1);
|
||||||
|
delay(50); // Brief delay before retry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any incomplete file from previous attempt before retrying
|
||||||
|
if (SdMan.exists(tmpHtmlPath.c_str())) {
|
||||||
|
SdMan.remove(tmpHtmlPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile tmpHtml;
|
||||||
|
if (!SdMan.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
||||||
|
fileSize = tmpHtml.size();
|
||||||
|
tmpHtml.close();
|
||||||
|
|
||||||
|
// If streaming failed, remove the incomplete file immediately
|
||||||
|
if (!success && SdMan.exists(tmpHtmlPath.c_str())) {
|
||||||
|
SdMan.remove(tmpHtmlPath.c_str());
|
||||||
|
Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file\n", millis());
|
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file after retries\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s\n", millis(), tmpHtmlPath.c_str());
|
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
|
||||||
|
|
||||||
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
|
// Only show progress bar for larger chapters where rendering overhead is worth it
|
||||||
|
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
|
||||||
|
progressSetupFn();
|
||||||
|
}
|
||||||
|
|
||||||
ChapterHtmlSlimParser visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
|
if (!SdMan.openFileForWrite("SCT", filePath, file)) {
|
||||||
marginBottom, marginLeft, extraParagraphSpacing,
|
return false;
|
||||||
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); });
|
}
|
||||||
|
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
|
viewportHeight, hyphenationEnabled);
|
||||||
|
std::vector<uint32_t> lut = {};
|
||||||
|
|
||||||
|
ChapterHtmlSlimParser visitor(
|
||||||
|
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
|
viewportHeight, hyphenationEnabled,
|
||||||
|
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
||||||
|
progressFn);
|
||||||
|
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||||
success = visitor.parseAndBuildPages();
|
success = visitor.parseAndBuildPages();
|
||||||
|
|
||||||
SD.remove(tmpHtmlPath.c_str());
|
SdMan.remove(tmpHtmlPath.c_str());
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis());
|
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis());
|
||||||
|
file.close();
|
||||||
|
SdMan.remove(filePath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeCacheMetadata(fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft, extraParagraphSpacing);
|
const uint32_t lutOffset = file.position();
|
||||||
|
bool hasFailedLutRecords = false;
|
||||||
|
// Write LUT
|
||||||
|
for (const uint32_t& pos : lut) {
|
||||||
|
if (pos == 0) {
|
||||||
|
hasFailedLutRecords = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
serialization::writePod(file, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFailedLutRecords) {
|
||||||
|
Serial.printf("[%lu] [SCT] Failed to write LUT due to invalid page positions\n", millis());
|
||||||
|
file.close();
|
||||||
|
SdMan.remove(filePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go back and write LUT offset
|
||||||
|
file.seek(HEADER_SIZE - sizeof(uint32_t) - sizeof(pageCount));
|
||||||
|
serialization::writePod(file, pageCount);
|
||||||
|
serialization::writePod(file, lutOffset);
|
||||||
|
file.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Page> Section::loadPageFromSD() const {
|
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||||
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
|
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
||||||
if (!SD.exists(filePath.c_str() + 3)) {
|
|
||||||
Serial.printf("[%lu] [SCT] Page file does not exist: %s\n", millis(), filePath.c_str());
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ifstream inputFile(filePath);
|
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||||
auto page = Page::deserialize(inputFile);
|
uint32_t lutOffset;
|
||||||
inputFile.close();
|
serialization::readPod(file, lutOffset);
|
||||||
|
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
|
||||||
|
uint32_t pagePos;
|
||||||
|
serialization::readPod(file, pagePos);
|
||||||
|
file.seek(pagePos);
|
||||||
|
|
||||||
|
auto page = Page::deserialize(file);
|
||||||
|
file.close();
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
@@ -10,26 +11,29 @@ class Section {
|
|||||||
std::shared_ptr<Epub> epub;
|
std::shared_ptr<Epub> epub;
|
||||||
const int spineIndex;
|
const int spineIndex;
|
||||||
GfxRenderer& renderer;
|
GfxRenderer& renderer;
|
||||||
std::string cachePath;
|
std::string filePath;
|
||||||
|
FsFile file;
|
||||||
|
|
||||||
void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
int marginLeft, bool extraParagraphSpacing) const;
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled);
|
||||||
void onPageComplete(std::unique_ptr<Page> page);
|
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
int pageCount = 0;
|
uint16_t pageCount = 0;
|
||||||
int currentPage = 0;
|
int currentPage = 0;
|
||||||
|
|
||||||
explicit Section(const std::shared_ptr<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) {
|
: epub(epub),
|
||||||
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
|
spineIndex(spineIndex),
|
||||||
}
|
renderer(renderer),
|
||||||
|
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||||
~Section() = default;
|
~Section() = default;
|
||||||
bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
int marginLeft, bool extraParagraphSpacing);
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled);
|
||||||
void setupCacheDir() const;
|
|
||||||
bool clearCache() const;
|
bool clearCache() const;
|
||||||
bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
int marginLeft, bool extraParagraphSpacing);
|
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
|
||||||
std::unique_ptr<Page> loadPageFromSD() const;
|
const std::function<void()>& progressSetupFn = nullptr,
|
||||||
|
const std::function<void(int)>& progressFn = nullptr);
|
||||||
|
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,12 +3,19 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
void TextBlock::render(GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||||
|
// Validate iterator bounds before rendering
|
||||||
|
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
||||||
|
Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
|
||||||
|
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto wordIt = words.begin();
|
auto wordIt = words.begin();
|
||||||
auto wordStylesIt = wordStyles.begin();
|
auto wordStylesIt = wordStyles.begin();
|
||||||
auto wordXposIt = wordXpos.begin();
|
auto wordXposIt = wordXpos.begin();
|
||||||
|
|
||||||
for (int i = 0; i < words.size(); i++) {
|
for (size_t i = 0; i < words.size(); i++) {
|
||||||
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
|
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
|
||||||
|
|
||||||
std::advance(wordIt, 1);
|
std::advance(wordIt, 1);
|
||||||
@@ -17,50 +24,51 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextBlock::serialize(std::ostream& os) const {
|
bool TextBlock::serialize(FsFile& file) const {
|
||||||
// words
|
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
||||||
const uint32_t wc = words.size();
|
Serial.printf("[%lu] [TXB] Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
|
||||||
serialization::writePod(os, wc);
|
words.size(), wordXpos.size(), wordStyles.size());
|
||||||
for (const auto& w : words) serialization::writeString(os, w);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// wordXpos
|
// Word data
|
||||||
const uint32_t xc = wordXpos.size();
|
serialization::writePod(file, static_cast<uint16_t>(words.size()));
|
||||||
serialization::writePod(os, xc);
|
for (const auto& w : words) serialization::writeString(file, w);
|
||||||
for (auto x : wordXpos) serialization::writePod(os, x);
|
for (auto x : wordXpos) serialization::writePod(file, x);
|
||||||
|
for (auto s : wordStyles) serialization::writePod(file, s);
|
||||||
|
|
||||||
// wordStyles
|
// Block style
|
||||||
const uint32_t sc = wordStyles.size();
|
serialization::writePod(file, style);
|
||||||
serialization::writePod(os, sc);
|
|
||||||
for (auto s : wordStyles) serialization::writePod(os, s);
|
|
||||||
|
|
||||||
// style
|
return true;
|
||||||
serialization::writePod(os, style);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<TextBlock> TextBlock::deserialize(std::istream& is) {
|
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||||
uint32_t wc, xc, sc;
|
uint16_t wc;
|
||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<uint16_t> wordXpos;
|
std::list<uint16_t> wordXpos;
|
||||||
std::list<EpdFontStyle> wordStyles;
|
std::list<CrossPointFont::Style> wordStyles;
|
||||||
BLOCK_STYLE style;
|
Style style;
|
||||||
|
|
||||||
// words
|
// Word count
|
||||||
serialization::readPod(is, wc);
|
serialization::readPod(file, wc);
|
||||||
|
|
||||||
|
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
|
||||||
|
if (wc > 10000) {
|
||||||
|
Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word data
|
||||||
words.resize(wc);
|
words.resize(wc);
|
||||||
for (auto& w : words) serialization::readString(is, w);
|
wordXpos.resize(wc);
|
||||||
|
wordStyles.resize(wc);
|
||||||
|
for (auto& w : words) serialization::readString(file, w);
|
||||||
|
for (auto& x : wordXpos) serialization::readPod(file, x);
|
||||||
|
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||||
|
|
||||||
// wordXpos
|
// Block style
|
||||||
serialization::readPod(is, xc);
|
serialization::readPod(file, style);
|
||||||
wordXpos.resize(xc);
|
|
||||||
for (auto& x : wordXpos) serialization::readPod(is, x);
|
|
||||||
|
|
||||||
// wordStyles
|
|
||||||
serialization::readPod(is, sc);
|
|
||||||
wordStyles.resize(sc);
|
|
||||||
for (auto& s : wordStyles) serialization::readPod(is, s);
|
|
||||||
|
|
||||||
// style
|
|
||||||
serialization::readPod(is, style);
|
|
||||||
|
|
||||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style));
|
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <EpdFontFamily.h>
|
#include <CrossPointFont.h>
|
||||||
|
#include <SdFat.h>
|
||||||
|
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -7,10 +8,10 @@
|
|||||||
|
|
||||||
#include "Block.h"
|
#include "Block.h"
|
||||||
|
|
||||||
// represents a block of words in the html document
|
// Represents a line of text on a page
|
||||||
class TextBlock final : public Block {
|
class TextBlock final : public Block {
|
||||||
public:
|
public:
|
||||||
enum BLOCK_STYLE : uint8_t {
|
enum Style : uint8_t {
|
||||||
JUSTIFIED = 0,
|
JUSTIFIED = 0,
|
||||||
LEFT_ALIGN = 1,
|
LEFT_ALIGN = 1,
|
||||||
CENTER_ALIGN = 2,
|
CENTER_ALIGN = 2,
|
||||||
@@ -20,21 +21,21 @@ class TextBlock final : public Block {
|
|||||||
private:
|
private:
|
||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<uint16_t> wordXpos;
|
std::list<uint16_t> wordXpos;
|
||||||
std::list<EpdFontStyle> wordStyles;
|
std::list<CrossPointFont::Style> wordStyles;
|
||||||
BLOCK_STYLE style;
|
Style style;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos, std::list<EpdFontStyle> word_styles,
|
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
||||||
const BLOCK_STYLE style)
|
std::list<CrossPointFont::Style> word_styles, const Style style)
|
||||||
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
|
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
|
||||||
~TextBlock() override = default;
|
~TextBlock() override = default;
|
||||||
void setStyle(const BLOCK_STYLE style) { this->style = style; }
|
void setStyle(const Style style) { this->style = style; }
|
||||||
BLOCK_STYLE getStyle() const { return style; }
|
Style getStyle() const { return style; }
|
||||||
bool isEmpty() override { return words.empty(); }
|
bool isEmpty() override { return words.empty(); }
|
||||||
void layout(GfxRenderer& renderer) override {};
|
void layout(GfxRenderer& renderer) override {};
|
||||||
// given a renderer works out where to break the words into lines
|
// given a renderer works out where to break the words into lines
|
||||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
void render(GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||||
BlockType getType() override { return TEXT_BLOCK; }
|
BlockType getType() override { return TEXT_BLOCK; }
|
||||||
void serialize(std::ostream& os) const;
|
bool serialize(FsFile& file) const;
|
||||||
static std::unique_ptr<TextBlock> deserialize(std::istream& is);
|
static std::unique_ptr<TextBlock> deserialize(FsFile& file);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
// from
|
|
||||||
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
|
||||||
|
|
||||||
#include "htmlEntities.h"
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
const int MAX_ENTITY_LENGTH = 10;
|
|
||||||
|
|
||||||
// Use book: entities_ww2.epub to test this (Page 7: Entities parser test)
|
|
||||||
// Note the supported keys are only in lowercase
|
|
||||||
// Store the mappings in a unordered hash map
|
|
||||||
static std::unordered_map<std::string, std::string> entity_lookup(
|
|
||||||
{{""", "\""}, {"⁄", "⁄"}, {"&", "&"}, {"<", "<"}, {">", ">"},
|
|
||||||
{"À", "À"}, {"Á", "Á"}, {"Â", "Â"}, {"Ã", "Ã"}, {"Ä", "Ä"},
|
|
||||||
{"Å", "Å"}, {"Æ", "Æ"}, {"Ç", "Ç"}, {"È", "È"}, {"É", "É"},
|
|
||||||
{"Ê", "Ê"}, {"Ë", "Ë"}, {"Ì", "Ì"}, {"Í", "Í"}, {"Î", "Î"},
|
|
||||||
{"Ï", "Ï"}, {"Ð", "Ð"}, {"Ñ", "Ñ"}, {"Ò", "Ò"}, {"Ó", "Ó"},
|
|
||||||
{"Ô", "Ô"}, {"Õ", "Õ"}, {"Ö", "Ö"}, {"Ø", "Ø"}, {"Ù", "Ù"},
|
|
||||||
{"Ú", "Ú"}, {"Û", "Û"}, {"Ü", "Ü"}, {"Ý", "Ý"}, {"Þ", "Þ"},
|
|
||||||
{"ß", "ß"}, {"à", "à"}, {"á", "á"}, {"â", "â"}, {"ã", "ã"},
|
|
||||||
{"ä", "ä"}, {"å", "å"}, {"æ", "æ"}, {"ç", "ç"}, {"è", "è"},
|
|
||||||
{"é", "é"}, {"ê", "ê"}, {"ë", "ë"}, {"ì", "ì"}, {"í", "í"},
|
|
||||||
{"î", "î"}, {"ï", "ï"}, {"ð", "ð"}, {"ñ", "ñ"}, {"ò", "ò"},
|
|
||||||
{"ó", "ó"}, {"ô", "ô"}, {"õ", "õ"}, {"ö", "ö"}, {"ø", "ø"},
|
|
||||||
{"ù", "ù"}, {"ú", "ú"}, {"û", "û"}, {"ü", "ü"}, {"ý", "ý"},
|
|
||||||
{"þ", "þ"}, {"ÿ", "ÿ"}, {" ", " "}, {"¡", "¡"}, {"¢", "¢"},
|
|
||||||
{"£", "£"}, {"¤", "¤"}, {"¥", "¥"}, {"¦", "¦"}, {"§", "§"},
|
|
||||||
{"¨", "¨"}, {"©", "©"}, {"ª", "ª"}, {"«", "«"}, {"¬", "¬"},
|
|
||||||
{"­", ""}, {"®", "®"}, {"¯", "¯"}, {"°", "°"}, {"±", "±"},
|
|
||||||
{"²", "²"}, {"³", "³"}, {"´", "´"}, {"µ", "µ"}, {"¶", "¶"},
|
|
||||||
{"¸", "¸"}, {"¹", "¹"}, {"º", "º"}, {"»", "»"}, {"¼", "¼"},
|
|
||||||
{"½", "½"}, {"¾", "¾"}, {"¿", "¿"}, {"×", "×"}, {"÷", "÷"},
|
|
||||||
{"∀", "∀"}, {"∂", "∂"}, {"∃", "∃"}, {"∅", "∅"}, {"∇", "∇"},
|
|
||||||
{"∈", "∈"}, {"∉", "∉"}, {"∋", "∋"}, {"∏", "∏"}, {"∑", "∑"},
|
|
||||||
{"−", "−"}, {"∗", "∗"}, {"√", "√"}, {"∝", "∝"}, {"∞", "∞"},
|
|
||||||
{"∠", "∠"}, {"∧", "∧"}, {"∨", "∨"}, {"∩", "∩"}, {"∪", "∪"},
|
|
||||||
{"∫", "∫"}, {"∴", "∴"}, {"∼", "∼"}, {"≅", "≅"}, {"≈", "≈"},
|
|
||||||
{"≠", "≠"}, {"≡", "≡"}, {"≤", "≤"}, {"≥", "≥"}, {"⊂", "⊂"},
|
|
||||||
{"⊃", "⊃"}, {"⊄", "⊄"}, {"⊆", "⊆"}, {"⊇", "⊇"}, {"⊕", "⊕"},
|
|
||||||
{"⊗", "⊗"}, {"⊥", "⊥"}, {"⋅", "⋅"}, {"Α", "Α"}, {"Β", "Β"},
|
|
||||||
{"Γ", "Γ"}, {"Δ", "Δ"}, {"Ε", "Ε"}, {"Ζ", "Ζ"}, {"Η", "Η"},
|
|
||||||
{"Θ", "Θ"}, {"Ι", "Ι"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"},
|
|
||||||
{"Ν", "Ν"}, {"Ξ", "Ξ"}, {"Ο", "Ο"}, {"Π", "Π"}, {"Ρ", "Ρ"},
|
|
||||||
{"Σ", "Σ"}, {"Τ", "Τ"}, {"Υ", "Υ"}, {"Φ", "Φ"}, {"Χ", "Χ"},
|
|
||||||
{"Ψ", "Ψ"}, {"Ω", "Ω"}, {"α", "α"}, {"β", "β"}, {"γ", "γ"},
|
|
||||||
{"δ", "δ"}, {"ε", "ε"}, {"ζ", "ζ"}, {"η", "η"}, {"θ", "θ"},
|
|
||||||
{"ι", "ι"}, {"κ", "κ"}, {"λ", "λ"}, {"μ", "μ"}, {"ν", "ν"},
|
|
||||||
{"ξ", "ξ"}, {"ο", "ο"}, {"π", "π"}, {"ρ", "ρ"}, {"ς", "ς"},
|
|
||||||
{"σ", "σ"}, {"τ", "τ"}, {"υ", "υ"}, {"φ", "φ"}, {"χ", "χ"},
|
|
||||||
{"ψ", "ψ"}, {"ω", "ω"}, {"ϑ", "ϑ"}, {"ϒ", "ϒ"}, {"ϖ", "ϖ"},
|
|
||||||
{"Œ", "Œ"}, {"œ", "œ"}, {"Š", "Š"}, {"š", "š"}, {"Ÿ", "Ÿ"},
|
|
||||||
{"ƒ", "ƒ"}, {"ˆ", "ˆ"}, {"˜", "˜"}, {" ", ""}, {" ", ""},
|
|
||||||
{" ", ""}, {"‌", ""}, {"‍", ""}, {"‎", ""}, {"‏", ""},
|
|
||||||
{"–", "–"}, {"—", "—"}, {"‘", "‘"}, {"’", "’"}, {"‚", "‚"},
|
|
||||||
{"“", "“"}, {"”", "”"}, {"„", "„"}, {"†", "†"}, {"‡", "‡"},
|
|
||||||
{"•", "•"}, {"…", "…"}, {"‰", "‰"}, {"′", "′"}, {"″", "″"},
|
|
||||||
{"‹", "‹"}, {"›", "›"}, {"‾", "‾"}, {"€", "€"}, {"™", "™"},
|
|
||||||
{"←", "←"}, {"↑", "↑"}, {"→", "→"}, {"↓", "↓"}, {"↔", "↔"},
|
|
||||||
{"↵", "↵"}, {"⌈", "⌈"}, {"⌉", "⌉"}, {"⌊", "⌊"}, {"⌋", "⌋"},
|
|
||||||
{"◊", "◊"}, {"♠", "♠"}, {"♣", "♣"}, {"♥", "♥"}, {"♦", "♦"}});
|
|
||||||
|
|
||||||
// converts from a unicode code point to the utf8 equivalent
|
|
||||||
void convert_to_utf8(const int code, std::string& res) {
|
|
||||||
// convert to a utf8 sequence
|
|
||||||
if (code < 0x80) {
|
|
||||||
res += static_cast<char>(code);
|
|
||||||
} else if (code < 0x800) {
|
|
||||||
res += static_cast<char>(0xc0 | (code >> 6));
|
|
||||||
res += static_cast<char>(0x80 | (code & 0x3f));
|
|
||||||
} else if (code < 0x10000) {
|
|
||||||
res += static_cast<char>(0xe0 | (code >> 12));
|
|
||||||
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
|
||||||
res += static_cast<char>(0x80 | (code & 0x3f));
|
|
||||||
} else if (code < 0x200000) {
|
|
||||||
res += static_cast<char>(0xf0 | (code >> 18));
|
|
||||||
res += static_cast<char>(0x80 | ((code >> 12) & 0x3f));
|
|
||||||
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
|
||||||
res += static_cast<char>(0x80 | (code & 0x3f));
|
|
||||||
} else if (code < 0x4000000) {
|
|
||||||
res += static_cast<char>(0xf8 | (code >> 24));
|
|
||||||
res += static_cast<char>(0x80 | ((code >> 18) & 0x3f));
|
|
||||||
res += static_cast<char>(0x80 | ((code >> 12) & 0x3f));
|
|
||||||
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
|
||||||
res += static_cast<char>(0x80 | (code & 0x3f));
|
|
||||||
} else if (code < 0x80000000) {
|
|
||||||
res += static_cast<char>(0xfc | (code >> 30));
|
|
||||||
res += static_cast<char>(0x80 | ((code >> 24) & 0x3f));
|
|
||||||
res += static_cast<char>(0x80 | ((code >> 18) & 0x3f));
|
|
||||||
res += static_cast<char>(0x80 | ((code >> 12) & 0x3f));
|
|
||||||
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handles numeric entities - e.g. Ӓ or ሴ
|
|
||||||
bool process_numeric_entity(const std::string& entity, std::string& res) {
|
|
||||||
int code = 0;
|
|
||||||
// is it hex?
|
|
||||||
if (entity[2] == 'x' || entity[2] == 'X') {
|
|
||||||
// parse the hex code
|
|
||||||
code = strtol(entity.substr(3, entity.size() - 3).c_str(), nullptr, 16);
|
|
||||||
} else {
|
|
||||||
code = strtol(entity.substr(2, entity.size() - 3).c_str(), nullptr, 10);
|
|
||||||
}
|
|
||||||
if (code != 0) {
|
|
||||||
// special handling for nbsp
|
|
||||||
if (code == 0xA0) {
|
|
||||||
res += " ";
|
|
||||||
} else {
|
|
||||||
convert_to_utf8(code, res);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handles named entities - e.g. &
|
|
||||||
bool process_string_entity(const std::string& entity, std::string& res) {
|
|
||||||
// it's a named entity - find it in the lookup table
|
|
||||||
// find it in the map
|
|
||||||
const auto it = entity_lookup.find(entity);
|
|
||||||
if (it != entity_lookup.end()) {
|
|
||||||
res += it->second;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace all the entities in the string
|
|
||||||
std::string replaceHtmlEntities(const char* text) {
|
|
||||||
std::string res;
|
|
||||||
res.reserve(strlen(text));
|
|
||||||
for (int i = 0; i < strlen(text); ++i) {
|
|
||||||
bool flag = false;
|
|
||||||
// do we have a potential entity?
|
|
||||||
if (text[i] == '&') {
|
|
||||||
// find the end of the entity
|
|
||||||
int j = i + 1;
|
|
||||||
while (j < strlen(text) && text[j] != ';' && j - i < MAX_ENTITY_LENGTH) {
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
if (j - i > 2) {
|
|
||||||
char entity[j - i + 1];
|
|
||||||
strncpy(entity, text + i, j - i);
|
|
||||||
// is it a numeric code?
|
|
||||||
if (entity[1] == '#') {
|
|
||||||
flag = process_numeric_entity(entity, res);
|
|
||||||
} else {
|
|
||||||
flag = process_string_entity(entity, res);
|
|
||||||
}
|
|
||||||
// skip past the entity if we successfully decoded it
|
|
||||||
if (flag) {
|
|
||||||
i = j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!flag) {
|
|
||||||
res += text[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
// from
|
|
||||||
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
std::string replaceHtmlEntities(const char* text);
|
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#include "HyphenationCommon.h"
|
||||||
|
|
||||||
|
#include <Utf8.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Convert Latin uppercase letters (ASCII plus Latin-1 supplement) to lowercase
|
||||||
|
uint32_t toLowerLatinImpl(const uint32_t cp) {
|
||||||
|
if (cp >= 'A' && cp <= 'Z') {
|
||||||
|
return cp - 'A' + 'a';
|
||||||
|
}
|
||||||
|
if ((cp >= 0x00C0 && cp <= 0x00D6) || (cp >= 0x00D8 && cp <= 0x00DE)) {
|
||||||
|
return cp + 0x20;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cp) {
|
||||||
|
case 0x0152: // Œ
|
||||||
|
return 0x0153; // œ
|
||||||
|
case 0x0178: // Ÿ
|
||||||
|
return 0x00FF; // ÿ
|
||||||
|
case 0x1E9E: // ẞ
|
||||||
|
return 0x00DF; // ß
|
||||||
|
default:
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Cyrillic uppercase letters to lowercase
|
||||||
|
// Cyrillic uppercase range 0x0410-0x042F maps to lowercase by adding 0x20
|
||||||
|
// Special case: Cyrillic capital IO (0x0401) maps to lowercase io (0x0451)
|
||||||
|
uint32_t toLowerCyrillicImpl(const uint32_t cp) {
|
||||||
|
if (cp >= 0x0410 && cp <= 0x042F) {
|
||||||
|
return cp + 0x20;
|
||||||
|
}
|
||||||
|
if (cp == 0x0401) {
|
||||||
|
return 0x0451;
|
||||||
|
}
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
uint32_t toLowerLatin(const uint32_t cp) { return toLowerLatinImpl(cp); }
|
||||||
|
|
||||||
|
uint32_t toLowerCyrillic(const uint32_t cp) { return toLowerCyrillicImpl(cp); }
|
||||||
|
|
||||||
|
bool isLatinLetter(const uint32_t cp) {
|
||||||
|
if ((cp >= 'A' && cp <= 'Z') || (cp >= 'a' && cp <= 'z')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (((cp >= 0x00C0 && cp <= 0x00D6) || (cp >= 0x00D8 && cp <= 0x00F6) || (cp >= 0x00F8 && cp <= 0x00FF)) &&
|
||||||
|
cp != 0x00D7 && cp != 0x00F7) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cp) {
|
||||||
|
case 0x0152: // Œ
|
||||||
|
case 0x0153: // œ
|
||||||
|
case 0x0178: // Ÿ
|
||||||
|
case 0x1E9E: // ẞ
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isCyrillicLetter(const uint32_t cp) { return (cp >= 0x0400 && cp <= 0x052F); }
|
||||||
|
|
||||||
|
bool isAlphabetic(const uint32_t cp) { return isLatinLetter(cp) || isCyrillicLetter(cp); }
|
||||||
|
|
||||||
|
bool isPunctuation(const uint32_t cp) {
|
||||||
|
switch (cp) {
|
||||||
|
case '-':
|
||||||
|
case '.':
|
||||||
|
case ',':
|
||||||
|
case '!':
|
||||||
|
case '?':
|
||||||
|
case ';':
|
||||||
|
case ':':
|
||||||
|
case '"':
|
||||||
|
case '\'':
|
||||||
|
case ')':
|
||||||
|
case '(':
|
||||||
|
case 0x00AB: // «
|
||||||
|
case 0x00BB: // »
|
||||||
|
case 0x2018: // ‘
|
||||||
|
case 0x2019: // ’
|
||||||
|
case 0x201C: // “
|
||||||
|
case 0x201D: // ”
|
||||||
|
case 0x00A0: // no-break space
|
||||||
|
case '{':
|
||||||
|
case '}':
|
||||||
|
case '[':
|
||||||
|
case ']':
|
||||||
|
case '/':
|
||||||
|
case 0x203A: // ›
|
||||||
|
case 0x2026: // …
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAsciiDigit(const uint32_t cp) { return cp >= '0' && cp <= '9'; }
|
||||||
|
|
||||||
|
bool isExplicitHyphen(const uint32_t cp) {
|
||||||
|
switch (cp) {
|
||||||
|
case '-':
|
||||||
|
case 0x00AD: // soft hyphen
|
||||||
|
case 0x058A: // Armenian hyphen
|
||||||
|
case 0x2010: // hyphen
|
||||||
|
case 0x2011: // non-breaking hyphen
|
||||||
|
case 0x2012: // figure dash
|
||||||
|
case 0x2013: // en dash
|
||||||
|
case 0x2014: // em dash
|
||||||
|
case 0x2015: // horizontal bar
|
||||||
|
case 0x2043: // hyphen bullet
|
||||||
|
case 0x207B: // superscript minus
|
||||||
|
case 0x208B: // subscript minus
|
||||||
|
case 0x2212: // minus sign
|
||||||
|
case 0x2E17: // double oblique hyphen
|
||||||
|
case 0x2E3A: // two-em dash
|
||||||
|
case 0x2E3B: // three-em dash
|
||||||
|
case 0xFE58: // small em dash
|
||||||
|
case 0xFE63: // small hyphen-minus
|
||||||
|
case 0xFF0D: // fullwidth hyphen-minus
|
||||||
|
case 0x005F: // Underscore
|
||||||
|
case 0x2026: // Ellipsis
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSoftHyphen(const uint32_t cp) { return cp == 0x00AD; }
|
||||||
|
|
||||||
|
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps) {
|
||||||
|
if (cps.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing footnote references like [12], even if punctuation trails after the closing bracket.
|
||||||
|
if (cps.size() >= 3) {
|
||||||
|
int end = static_cast<int>(cps.size()) - 1;
|
||||||
|
while (end >= 0 && isPunctuation(cps[end].value)) {
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
int pos = end;
|
||||||
|
if (pos >= 0 && isAsciiDigit(cps[pos].value)) {
|
||||||
|
while (pos >= 0 && isAsciiDigit(cps[pos].value)) {
|
||||||
|
--pos;
|
||||||
|
}
|
||||||
|
if (pos >= 0 && cps[pos].value == '[' && end - pos > 1) {
|
||||||
|
cps.erase(cps.begin() + pos, cps.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!cps.empty() && isPunctuation(cps.front().value)) {
|
||||||
|
cps.erase(cps.begin());
|
||||||
|
}
|
||||||
|
while (!cps.empty() && isPunctuation(cps.back().value)) {
|
||||||
|
cps.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<CodepointInfo> collectCodepoints(const std::string& word) {
|
||||||
|
std::vector<CodepointInfo> cps;
|
||||||
|
cps.reserve(word.size());
|
||||||
|
|
||||||
|
const unsigned char* base = reinterpret_cast<const unsigned char*>(word.c_str());
|
||||||
|
const unsigned char* ptr = base;
|
||||||
|
while (*ptr != 0) {
|
||||||
|
const unsigned char* current = ptr;
|
||||||
|
const uint32_t cp = utf8NextCodepoint(&ptr);
|
||||||
|
cps.push_back({cp, static_cast<size_t>(current - base)});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cps;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct CodepointInfo {
|
||||||
|
uint32_t value;
|
||||||
|
size_t byteOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
uint32_t toLowerLatin(uint32_t cp);
|
||||||
|
uint32_t toLowerCyrillic(uint32_t cp);
|
||||||
|
|
||||||
|
bool isLatinLetter(uint32_t cp);
|
||||||
|
bool isCyrillicLetter(uint32_t cp);
|
||||||
|
|
||||||
|
bool isAlphabetic(uint32_t cp);
|
||||||
|
bool isPunctuation(uint32_t cp);
|
||||||
|
bool isAsciiDigit(uint32_t cp);
|
||||||
|
bool isExplicitHyphen(uint32_t cp);
|
||||||
|
bool isSoftHyphen(uint32_t cp);
|
||||||
|
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps);
|
||||||
|
std::vector<CodepointInfo> collectCodepoints(const std::string& word);
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#include "Hyphenator.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "HyphenationCommon.h"
|
||||||
|
#include "LanguageRegistry.h"
|
||||||
|
|
||||||
|
const LanguageHyphenator* Hyphenator::cachedHyphenator_ = nullptr;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Maps a BCP-47 language tag to a language-specific hyphenator.
|
||||||
|
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
||||||
|
if (langTag.empty()) return nullptr;
|
||||||
|
|
||||||
|
// Extract primary subtag and normalize to lowercase (e.g., "en-US" -> "en").
|
||||||
|
std::string primary;
|
||||||
|
primary.reserve(langTag.size());
|
||||||
|
for (char c : langTag) {
|
||||||
|
if (c == '-' || c == '_') break;
|
||||||
|
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
|
||||||
|
primary.push_back(c);
|
||||||
|
}
|
||||||
|
if (primary.empty()) return nullptr;
|
||||||
|
|
||||||
|
return getLanguageHyphenatorForPrimaryTag(primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps a codepoint index back to its byte offset inside the source word.
|
||||||
|
size_t byteOffsetForIndex(const std::vector<CodepointInfo>& cps, const size_t index) {
|
||||||
|
return (index < cps.size()) ? cps[index].byteOffset : (cps.empty() ? 0 : cps.back().byteOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds a vector of break information from explicit hyphen markers in the given codepoints.
|
||||||
|
std::vector<Hyphenator::BreakInfo> buildExplicitBreakInfos(const std::vector<CodepointInfo>& cps) {
|
||||||
|
std::vector<Hyphenator::BreakInfo> breaks;
|
||||||
|
|
||||||
|
// Scan every codepoint looking for explicit/soft hyphen markers that are surrounded by letters.
|
||||||
|
for (size_t i = 1; i + 1 < cps.size(); ++i) {
|
||||||
|
const uint32_t cp = cps[i].value;
|
||||||
|
if (!isExplicitHyphen(cp) || !isAlphabetic(cps[i - 1].value) || !isAlphabetic(cps[i + 1].value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Offset points to the next codepoint so rendering starts after the hyphen marker.
|
||||||
|
breaks.push_back({cps[i + 1].byteOffset, isSoftHyphen(cp)});
|
||||||
|
}
|
||||||
|
|
||||||
|
return breaks;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& word, const bool includeFallback) {
|
||||||
|
if (word.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to codepoints and normalize word boundaries.
|
||||||
|
auto cps = collectCodepoints(word);
|
||||||
|
trimSurroundingPunctuationAndFootnote(cps);
|
||||||
|
const auto* hyphenator = cachedHyphenator_;
|
||||||
|
|
||||||
|
// Explicit hyphen markers (soft or hard) take precedence over language breaks.
|
||||||
|
auto explicitBreakInfos = buildExplicitBreakInfos(cps);
|
||||||
|
if (!explicitBreakInfos.empty()) {
|
||||||
|
return explicitBreakInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask language hyphenator for legal break points.
|
||||||
|
std::vector<size_t> indexes;
|
||||||
|
if (hyphenator) {
|
||||||
|
indexes = hyphenator->breakIndexes(cps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add fallback breaks if needed
|
||||||
|
if (includeFallback && indexes.empty()) {
|
||||||
|
const size_t minPrefix = hyphenator ? hyphenator->minPrefix() : LiangWordConfig::kDefaultMinPrefix;
|
||||||
|
const size_t minSuffix = hyphenator ? hyphenator->minSuffix() : LiangWordConfig::kDefaultMinSuffix;
|
||||||
|
for (size_t idx = minPrefix; idx + minSuffix <= cps.size(); ++idx) {
|
||||||
|
indexes.push_back(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexes.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Hyphenator::BreakInfo> breaks;
|
||||||
|
breaks.reserve(indexes.size());
|
||||||
|
for (const size_t idx : indexes) {
|
||||||
|
breaks.push_back({byteOffsetForIndex(cps, idx), true});
|
||||||
|
}
|
||||||
|
|
||||||
|
return breaks;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hyphenator::setPreferredLanguage(const std::string& lang) { cachedHyphenator_ = hyphenatorForLanguage(lang); }
|
||||||