Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3093e3f71 | ||
|
|
498e087a68 | ||
|
|
e44c004be6 | ||
|
|
45d19f6e1b | ||
|
|
8f33bee6ef | ||
|
|
bc1ba7277f | ||
|
|
57267f5372 | ||
|
|
7c4f69680c | ||
|
|
2cc497cdca | ||
|
|
8db3542e90 | ||
|
|
87d9d1dc2a | ||
|
|
588984ec30 | ||
|
|
b8e743ef80 | ||
|
|
3e2c518b8e | ||
|
|
402e887f73 | ||
|
|
103fac2ee1 | ||
|
|
6527f43cb1 | ||
|
|
eb241ab3fc | ||
|
|
840e8c38f1 | ||
|
|
c4e3c244ea | ||
|
|
3c1bd77813 | ||
|
|
10b7769865 | ||
|
|
448a77f02b | ||
|
|
e1074a84c0 | ||
|
|
e70066e7c2 | ||
|
|
00e25b1a90 | ||
|
|
fdcd71e94d | ||
|
|
e7ee6ff05e | ||
|
|
c6ddc5d6a0 | ||
|
|
feff739963 | ||
|
|
f1740dbe1e | ||
|
|
6be4413c97 | ||
|
|
47aa0dda76 | ||
|
|
f16c0e52fd | ||
|
|
d02e21a48f | ||
|
|
6ec5fc5603 | ||
|
|
9125a7ce68 | ||
|
|
dc6562a51c | ||
|
|
530d43997b | ||
|
|
97c33141bd | ||
|
|
2a32d8a182 | ||
|
|
d6f38d4441 | ||
|
|
513d111634 | ||
|
|
ad9137cfdf | ||
|
|
5c80cface7 | ||
|
|
86d3774a8f | ||
|
|
7ba5978848 | ||
|
|
3d47c081f2 | ||
|
|
6702060960 | ||
|
|
0bc6747483 | ||
|
|
00666377de | ||
|
|
22b77edddf | ||
|
|
2e673c753d | ||
|
|
1a30826981 | ||
|
|
50e6ef9bd8 | ||
|
|
a616f42cb4 | ||
|
|
0508bfc1f7 | ||
|
|
6c3a615fac | ||
|
|
46c2109f1f | ||
|
|
5816ab2a47 | ||
|
|
2c0a105550 | ||
|
|
6e51afb977 | ||
|
|
cb24947477 | ||
|
|
7a385d78a4 | ||
|
|
0991782fb4 | ||
|
|
3ae1007cbe | ||
|
|
efb9b72e64 | ||
|
|
4a210823a8 | ||
|
|
f5b85f5ca1 | ||
|
|
7e93411f46 | ||
|
|
44452a42e9 | ||
|
|
0c2df24f5c | ||
|
|
3a12ca2725 | ||
|
|
98e6789626 | ||
|
|
b5d28a3a9c | ||
|
|
14ef625679 | ||
|
|
64d161e88b | ||
|
|
e73bb3213f | ||
|
|
6202bfd651 | ||
|
|
9e04eec072 | ||
|
|
9b04c2ec76 | ||
|
|
def1094411 | ||
|
|
ffddc2472b | ||
|
|
5765bbe821 | ||
|
|
7538e55795 | ||
|
|
21e7d29286 | ||
|
|
b4b028be3a | ||
|
|
f34d7d2aac | ||
|
|
71769490fb | ||
|
|
cda0a3f898 | ||
|
|
7f40c3f477 | ||
|
|
5e52a46837 | ||
|
|
6909f127b4 | ||
|
|
4f0a3aa4dd | ||
|
|
bb983d0ef4 | ||
|
|
b45eaf7ded | ||
|
|
a87eacc6ab | ||
|
|
1caad578fc | ||
|
|
75b0ed7781 | ||
|
|
5b90b68e99 | ||
|
|
67ddd60fce | ||
|
|
76908d38e1 | ||
|
|
e6f5fa43e6 | ||
|
|
e7e31ac487 | ||
|
|
47f3137dee | ||
|
|
d8632eae08 | ||
|
|
9f78fd33e8 | ||
|
|
bd8132a260 | ||
|
|
3223e85ea5 | ||
|
|
f89ce514c8 | ||
|
|
211153fcd5 | ||
|
|
91777a9023 | ||
|
|
d8e813a78d | ||
|
|
c3b9bc38b9 | ||
|
|
fb0af32ec0 | ||
|
|
cb4d86fec6 | ||
|
|
e94f056e8a | ||
|
|
20c5d8ccf8 | ||
|
|
d35bda8023 | ||
|
|
d762325035 | ||
|
|
7f2b1a818e | ||
|
|
ddbe49f536 | ||
|
|
17fedd2a69 | ||
|
|
768c2f8eed | ||
|
|
216dbc8ee3 | ||
|
|
ee987f07ff | ||
|
|
23ecc52261 | ||
|
|
edaf8fff9d | ||
|
|
c8683340ab | ||
|
|
5a9ee19eb8 | ||
|
|
c49a819939 | ||
|
|
bf87a7dc60 | ||
|
|
2cf799f45b | ||
|
|
db659f3ea2 | ||
|
|
78d6e5931c | ||
|
|
dac11c3fdd | ||
|
|
d403044f76 | ||
|
|
f67c544e16 | ||
|
|
e5c0ddc9fa | ||
|
|
b1dcb7733b | ||
|
|
0d82b03981 | ||
|
|
5a97334ace | ||
|
|
4dd73a211a | ||
|
|
634f6279cb | ||
|
|
11b2a59233 | ||
|
|
12c20bb09e | ||
|
|
6b7065b986 | ||
|
|
f4df513bf3 | ||
|
|
f935b59a41 | ||
|
|
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 |
@@ -1,9 +1,18 @@
|
||||
## Summary
|
||||
|
||||
* **What is the goal of this PR?** (e.g., Fixes a bug in the user authentication module, Implements the new feature for
|
||||
file uploading.)
|
||||
* **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.)
|
||||
* **What changes are included?**
|
||||
|
||||
## Additional Context
|
||||
|
||||
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on).
|
||||
* 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 >**_
|
||||
|
||||
+83
-11
@@ -1,24 +1,25 @@
|
||||
name: CI
|
||||
'on':
|
||||
name: CI (build)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
clang-format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: pip install --upgrade platformio
|
||||
|
||||
- name: Install clang-format-21
|
||||
run: |
|
||||
wget https://apt.llvm.org/llvm.sh
|
||||
@@ -27,11 +28,82 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-format-21
|
||||
|
||||
- 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)
|
||||
|
||||
cppcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: pip install --upgrade platformio
|
||||
|
||||
- name: Run cppcheck
|
||||
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
|
||||
- name: Run clang-format
|
||||
run: 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)
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: pip install --upgrade platformio
|
||||
|
||||
- name: Build CrossPoint
|
||||
run: pio run
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pio run | tee pio.log
|
||||
|
||||
- name: Extract firmware stats
|
||||
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ram_line="$(grep -E "RAM:\\s" -m1 pio.log || true)"
|
||||
flash_line="$(grep -E "Flash:\\s" -m1 pio.log || true)"
|
||||
echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT"
|
||||
echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "## Firmware build stats"
|
||||
if [ -n "$ram_line" ]; then echo "- ${ram_line}"; else echo "- RAM: not found"; fi
|
||||
if [ -n "$flash_line" ]; then echo "- ${flash_line}"; else echo "- Flash: not found"; fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload firmware.bin artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: firmware.bin
|
||||
path: .pio/build/default/firmware.bin
|
||||
if-no-files-found: error
|
||||
|
||||
# This job is used as the PR required actions check, allows for changes to other steps in the future without breaking
|
||||
# PR requirements.
|
||||
test-status:
|
||||
name: Test Status
|
||||
needs:
|
||||
- build
|
||||
- clang-format
|
||||
- cppcheck
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fail because needed jobs failed
|
||||
# Fail if any job failed or was cancelled (skipped jobs are ok)
|
||||
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
|
||||
run: exit 1
|
||||
- name: Success
|
||||
run: exit 0
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: "PR Formatting"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
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:
|
||||
build-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
~/.platformio/.cache
|
||||
key: ${{ runner.os }}-pio
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Compile Release Candidate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-release-candidate:
|
||||
if: startsWith(github.ref_name, 'release/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
~/.platformio/.cache
|
||||
key: ${{ runner.os }}-pio
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: pip install --upgrade platformio
|
||||
|
||||
- name: Extract env
|
||||
run: |
|
||||
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
echo "BRANCH_SUFFIX=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build CrossPoint Release Candidate
|
||||
env:
|
||||
CROSSPOINT_RC_HASH: ${{ env.SHORT_SHA }}
|
||||
run: pio run -e gh_release_rc
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CrossPoint-RC-${{ env.BRANCH_SUFFIX }}
|
||||
path: |
|
||||
.pio/build/gh_release_rc/bootloader.bin
|
||||
.pio/build/gh_release_rc/firmware.bin
|
||||
.pio/build/gh_release_rc/firmware.elf
|
||||
.pio/build/gh_release_rc/firmware.map
|
||||
.pio/build/gh_release_rc/partitions.bin
|
||||
@@ -3,4 +3,10 @@
|
||||
.DS_Store
|
||||
.vscode
|
||||
lib/EpdFont/fontsrc
|
||||
lib/I18n/I18nStrings.cpp
|
||||
*.generated.h
|
||||
.vs
|
||||
build
|
||||
**/__pycache__/
|
||||
/compile_commands.json
|
||||
/.cache
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Project Governance & Community Principles
|
||||
|
||||
CrossPoint Reader is a community-driven, open-source project. Our goal is to provide a high-quality, open-source
|
||||
firmware alternative for the Xteink X4 hardware. To keep this project productive and welcoming as we grow, we ask all
|
||||
contributors to follow these principles.
|
||||
|
||||
### 1. The "Human First" Rule
|
||||
Technical discussions can get heated, but they should never be personal.
|
||||
- **Assume good intent:** We are all volunteers working on this in our free time. If a comment seems abrasive, assume
|
||||
it’s a language barrier or a misunderstanding before taking offense.
|
||||
- **Focus on the code, not the person:** Critique the implementation, the performance, or the UX. Never the intelligence
|
||||
or character of the contributor.
|
||||
- **Inflammatory language:** Personal attacks, trolling, or exclusionary language (based on race, gender, background,
|
||||
etc.) are not welcome here and will be moderated.
|
||||
|
||||
### 2. A "Do-ocracy" with Guidance
|
||||
CrossPoint thrives because people step up to build what they want to see.
|
||||
- If you want a feature, the best way to get it is to start an
|
||||
[Idea Discussion](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas) or open a PR.
|
||||
- If you want to report a bug, check for duplicates and create an
|
||||
[Issue](https://github.com/crosspoint-reader/crosspoint-reader/issues).
|
||||
- While we encourage experimentation, the maintainers reserve the right to guide the project’s technical direction to
|
||||
ensure stability on the ESP32-C3’s constrained hardware.
|
||||
- For more guidance on the scope of the project, see the [SCOPE.md](SCOPE.md) document.
|
||||
|
||||
### 3. Transparent Communication
|
||||
To keep the project healthy, we keep our "work" in the open.
|
||||
- **Public by Default:** All technical decisions and project management discussions happen in GitHub Issues, Pull
|
||||
Requests, or the public Discussions tab.
|
||||
- **Clarity in Writing:** Because we have a global community with different levels of English proficiency, please be as
|
||||
explicit and clear as possible in your PR descriptions and bug reports.
|
||||
|
||||
### 4. Moderation & Safety
|
||||
The maintainers are responsible for keeping the community a safe place to contribute.
|
||||
- We reserve the right to hide comments, lock threads, or block users who repeatedly violate these principles or engage
|
||||
in harassment.
|
||||
- **Reporting:** If you feel you are being harassed or see behavior that is damaging the community, please reach out
|
||||
privately to @daveallie.
|
||||
@@ -41,13 +41,17 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
||||
- [ ] Full UTF support
|
||||
- [x] Screen rotation
|
||||
|
||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
||||
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.
|
||||
|
||||
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
|
||||
|
||||
## Installing
|
||||
|
||||
### Web (latest firmware)
|
||||
|
||||
1. Connect your Xteink X4 to your computer via USB-C
|
||||
1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device
|
||||
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
|
||||
|
||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||
@@ -56,7 +60,7 @@ back to the other partition using the "Swap boot partition" button here https://
|
||||
### Web (specific firmware version)
|
||||
|
||||
1. Connect your Xteink X4 to your computer via USB-C
|
||||
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
|
||||
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
|
||||
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
|
||||
|
||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||
@@ -80,7 +84,7 @@ See [Development](#development) below.
|
||||
CrossPoint uses PlatformIO for building and flashing the firmware. To get started, clone the repository:
|
||||
|
||||
```
|
||||
git clone --recursive https://github.com/daveallie/crosspoint-reader
|
||||
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
|
||||
|
||||
# Or, if you've already cloned without --recursive:
|
||||
git submodule update --init --recursive
|
||||
@@ -93,6 +97,25 @@ Connect your Xteink X4 to your computer via USB-C and run the following command.
|
||||
```sh
|
||||
pio run --target upload
|
||||
```
|
||||
### Debugging
|
||||
|
||||
After flashing the new features, it’s recommended to capture detailed logs from the serial port.
|
||||
|
||||
First, make sure all required Python packages are installed:
|
||||
|
||||
```python
|
||||
python3 -m pip install pyserial colorama matplotlib
|
||||
```
|
||||
after that run the script:
|
||||
```sh
|
||||
# For Linux
|
||||
# This was tested on Debian and should work on most Linux systems.
|
||||
python3 scripts/debugging_monitor.py
|
||||
|
||||
# For macOS
|
||||
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
|
||||
```
|
||||
Minor adjustments may be required for Windows.
|
||||
|
||||
## Internals
|
||||
|
||||
@@ -131,9 +154,12 @@ For more details on the internal file structures, see the [file formats document
|
||||
|
||||
Contributions are very welcome!
|
||||
|
||||
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/daveallie/crosspoint-reader/discussions/categories/ideas).
|
||||
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas).
|
||||
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
|
||||
|
||||
Everyone here is a volunteer, so please be respectful and patient. For more details on our goverance and community
|
||||
principles, please see [GOVERNANCE.md](GOVERNANCE.md).
|
||||
|
||||
### To submit a contribution:
|
||||
|
||||
1. Fork the repo
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Project Vision & Scope: CrossPoint Reader
|
||||
|
||||
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for the Xteink X4. We believe a
|
||||
dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
|
||||
|
||||
## 1. Core Mission
|
||||
|
||||
To provide a lightweight, high-performance firmware that maximizes the potential of the X4, prioritizing legibility and
|
||||
usability over "swiss-army-knife" functionality.
|
||||
|
||||
## 2. Scope
|
||||
|
||||
### In-Scope
|
||||
|
||||
*These are features that directly improve the primary purpose of the device.*
|
||||
|
||||
* **User Experience:** E.g. User-friendly interfaces, and interactions, both inside the reader and navigating the
|
||||
firmware. This includes things like button mapping, book loading, and book navigation like bookmarks.
|
||||
* **Document Rendering:** E.g. Support for rendering documents (primarily EPUB) and improvements to the rendering
|
||||
engine.
|
||||
* **Format Optimization:** E.g. Efficiently parsing EPUB (CSS/Images) and other documents within the device's
|
||||
capabilities.
|
||||
* **Typography & Legibility:** E.g. Custom font support, hyphenation engines, and adjustable line spacing.
|
||||
* **E-Ink Driver Refinement:** E.g. Reducing full-screen flashes (ghosting management) and improving general rendering.
|
||||
* **Library Management:** E.g. Simple, intuitive ways to organize and navigate a collection of books.
|
||||
* **Local Transfer:** E.g. Simple, "pull" based book loading via a basic web-server or public and widely-used standards.
|
||||
* **Language Support:** E.g. Support for multiple languages both in the reader and in the interfaces.
|
||||
* **Reference Tools:** E.g. Local dictionary lookup. Providing quick, offline definitions to enhance comprehension
|
||||
without breaking focus.
|
||||
|
||||
### Out-of-Scope
|
||||
|
||||
*These items are rejected because they compromise the device's stability or mission.*
|
||||
|
||||
* **Interactive Apps:** No Notepads, Calculators, or Games. This is a reader, not a PDA.
|
||||
* **Active Connectivity:** No RSS readers, News aggregators, or Web browsers. Background Wi-Fi tasks drain the battery
|
||||
and complicate the single-core CPU's execution.
|
||||
* **Media Playback:** No Audio players or Audio-books.
|
||||
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
|
||||
capabilities and more powerful chips.
|
||||
|
||||
## 3. Idea Evaluation
|
||||
|
||||
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
|
||||
a lightweight, reliable, and performant e-reader. Things which distract or compromise the device's core mission will not
|
||||
be accepted. As a guiding question, consider if your idea improve the "core reading experience" for the average user,
|
||||
and, critically, not distract from that reading experience.
|
||||
|
||||
> **Note to Contributors:** If you are unsure if your idea fits the scope, please open a **Discussion** before you start
|
||||
> coding!
|
||||
+135
-38
@@ -1,17 +1,40 @@
|
||||
# CrossPoint User Guide
|
||||
|
||||
Welcome to the **CrossPoint** firmware. This guide outlines the hardware controls, navigation, and reading features of
|
||||
the device.
|
||||
Welcome to the **CrossPoint** firmware. This guide outlines the hardware controls, navigation, and reading features of 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.4.1 Calibre Wireless Transfers](#341-calibre-wireless-transfers)
|
||||
- [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)
|
||||
- [Supported Languages](#supported-languages)
|
||||
- [5. Chapter Selection Screen](#5-chapter-selection-screen)
|
||||
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
|
||||
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
|
||||
|
||||
|
||||
## 1. Hardware Overview
|
||||
|
||||
The device utilises the standard buttons on the Xtink X4 (in the same layout as the manufacturer firmware, by default):
|
||||
|
||||
### Button Layout
|
||||
| Location | Buttons |
|
||||
|-----------------|--------------------------------------------|
|
||||
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
|
||||
| **Right Side** | **Power**, **Volume Up**, **Volume Down** |
|
||||
| Location | Buttons |
|
||||
| --------------- | ---------------------------------------------------- |
|
||||
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
|
||||
| **Right Side** | **Power**, **Volume Up**, **Volume Down**, **Reset** |
|
||||
|
||||
Button layout can be customized in **[Settings](#35-settings)**.
|
||||
|
||||
@@ -21,8 +44,10 @@ Button layout can be customized in **[Settings](#35-settings)**.
|
||||
|
||||
### Power On / Off
|
||||
|
||||
To turn the device on or off, **press and hold the Power button for half a second**. In **[Settings](#35-settings)** you can configure
|
||||
the power button to trigger on a short press instead of a long one.
|
||||
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
|
||||
|
||||
@@ -37,15 +62,13 @@ Upon turning the device on for the first time, you will be placed on the **[Home
|
||||
|
||||
### 3.1 Home Screen
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### 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.
|
||||
* **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.
|
||||
|
||||
### 3.3 Reading Mode
|
||||
@@ -54,42 +77,88 @@ 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.
|
||||
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, options are:
|
||||
- "Dark" (default) - The default dark sleep screen
|
||||
- **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
|
||||
- "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)
|
||||
- **Status Bar**: Configure the status bar displayed while reading, options are:
|
||||
- "None" - A blank screen
|
||||
- "Cover + Custom" - The book cover image, fallbacks to "Custom" behavior
|
||||
- **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
|
||||
- **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 word indentation.
|
||||
- **Short Power Button Click**: Whether to trigger the power button on a short press or a long press.
|
||||
- **Reading Orientation**: Set the screen orientation for reading, options are:
|
||||
- "Full w/ Percentage" - Show status bar with book progress (as percentage)
|
||||
- "Full w/ Book Bar" - Show status bar with book progress (as bar)
|
||||
- "Book Bar Only" - Show book progress (as bar)
|
||||
- "Full w/ Chapter Bar" - Show status bar with chapter progress (as bar)
|
||||
- **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, options are:
|
||||
- "Bck, Cnfrm, Lft, Rght" (default) - Back, Confirm, Left, Right
|
||||
- "Lft, Rght, Bck, Cnfrm" - Left, Right, Back, Confirm
|
||||
- "Lft, Bck, Cnfrm, Rght" - Left, Back, Confirm, Right
|
||||
- **Side Button Layout**: Swap the order of the 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, options are:
|
||||
- **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 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.
|
||||
- **Sunlight Fading Fix**: Configure whether to enable a software-fix for the issue where white X4 models may fade when used in direct sunlight
|
||||
- "OFF" (default) - Disable the fix
|
||||
- "ON" - Enable the fix
|
||||
- **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
|
||||
@@ -97,9 +166,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
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.
|
||||
- **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.
|
||||
@@ -117,19 +184,35 @@ Once you have opened a book, the button layout changes to facilitate reading.
|
||||
|
||||
### Page Turning
|
||||
| Action | Buttons |
|
||||
|-------------------|--------------------------------------|
|
||||
| ----------------- | ------------------------------------ |
|
||||
| **Previous Page** | Press **Left** _or_ **Volume Up** |
|
||||
| **Next Page** | Press **Right** _or_ **Volume Down** |
|
||||
|
||||
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
|
||||
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
|
||||
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
|
||||
|
||||
This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake.
|
||||
|
||||
|
||||
### System Navigation
|
||||
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
|
||||
* **Return to Home:** Press and hold **Back** to close the book and return to the **[Home](#31-home-screen)** 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, Greek and Farsi.
|
||||
|
||||
---
|
||||
|
||||
## 5. Chapter Selection Screen
|
||||
@@ -144,7 +227,21 @@ Accessible by pressing **Confirm** while inside a book.
|
||||
|
||||
## 6. Current Limitations & Roadmap
|
||||
|
||||
Please note that this firmware is currently in active development. The following features are **not yet supported** but
|
||||
are planned for future updates:
|
||||
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
|
||||
|
||||
* **Images:** Embedded images in e-books will not render.
|
||||
* **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up.
|
||||
|
||||
---
|
||||
|
||||
## 7. Troubleshooting Issues & Escaping Bootloop
|
||||
|
||||
If an issue or crash is encountered while using Crosspoint, feel free to raise an issue ticket and attach the serial monitor logs. The logs can be obtained by connecting the device to a computer and starting a serial monitor. Either [Serial Monitor](https://www.serialmonitor.org/) or the following command can be used:
|
||||
|
||||
```
|
||||
pio device monitor
|
||||
```
|
||||
|
||||
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
|
||||
|
||||
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder).
|
||||
|
||||
+12
-1
@@ -1,10 +1,18 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Check if clang-format is availible
|
||||
command -v clang-format >/dev/null 2>&1 || {
|
||||
printf "'clang-format' not found in current environment\n"
|
||||
printf "install 'clang', 'clang-tools', or 'clang-format' depending on your distro/os and tooling requirements\n"
|
||||
exit 1
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -13,7 +21,10 @@ fi
|
||||
# --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.
|
||||
# Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated.
|
||||
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
||||
| grep -E '\.(c|cpp|h|hpp)$' \
|
||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||
| grep -v -E '^lib/uzlib/' \
|
||||
| xargs -r clang-format -style=file -i
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# 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.
|
||||
|
||||
A convenient script `update_hyphenation.sh` is used to update all languages.
|
||||
To use it, run:
|
||||
|
||||
```sh
|
||||
./scripts/update_hypenation.sh
|
||||
```
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
# Internationalization (I18N)
|
||||
|
||||
This guide explains the multi-language support system in CrossPoint Reader.
|
||||
|
||||
## Supported Languages
|
||||
|
||||
- English
|
||||
- French
|
||||
- German
|
||||
- Portuguese
|
||||
- Spanish
|
||||
- Swedish
|
||||
- Czech
|
||||
- Russian
|
||||
|
||||
---
|
||||
|
||||
|
||||
## For Developers
|
||||
|
||||
### Translation System Architecture
|
||||
|
||||
The I18N system uses **per-language YAML files** to maintain translations and a Python script to generate C++ code:
|
||||
|
||||
```
|
||||
lib/I18n/
|
||||
├── translations/ # One YAML file per language
|
||||
│ ├── english.yaml
|
||||
│ ├── spanish.yaml
|
||||
│ ├── french.yaml
|
||||
│ └── ...
|
||||
├── I18n.h
|
||||
├── I18n.cpp
|
||||
├── I18nKeys.h # Enums (auto-generated)
|
||||
├── I18nStrings.h # String array declarations (auto-generated)
|
||||
└── I18nStrings.cpp # String array definitions (auto-generated)
|
||||
|
||||
scripts/
|
||||
└── gen_i18n.py # Code generator script
|
||||
```
|
||||
|
||||
**Key principle:** All translations are managed in the YAML files under `lib/I18n/translations/`. The Python script generates the necessary C++ code automatically.
|
||||
|
||||
---
|
||||
|
||||
### YAML File Format
|
||||
|
||||
Each language has its own file in `lib/I18n/translations/` (e.g. `spanish.yaml`).
|
||||
|
||||
A file looks like this:
|
||||
|
||||
```yaml
|
||||
_language_name: "Español"
|
||||
_language_code: "SPANISH"
|
||||
_order: "1"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "BOOTING"
|
||||
STR_BROWSE_FILES: "Buscar archivos"
|
||||
```
|
||||
|
||||
**Metadata keys** (prefixed with `_`):
|
||||
- `_language_name` — Native display name shown to the user (e.g. "Français")
|
||||
- `_language_code` — C++ enum name (e.g. "FRENCH"). Must be a valid C++ identifier.
|
||||
- `_order` — Controls the position in the Language enum (English is always 0)
|
||||
|
||||
**Rules:**
|
||||
- Use UTF-8 encoding
|
||||
- Every line must follow the format: `KEY: "value"`
|
||||
- Keys must be valid C++ identifiers (uppercase, strats with STR_)
|
||||
- Keys must be unique within a file
|
||||
- String values must be quoted
|
||||
- Use `\n` for newlines, `\\` for literal backslashes, `\"` for literal quotes inside values
|
||||
|
||||
---
|
||||
|
||||
### Adding New Strings
|
||||
|
||||
To add a new translatable string:
|
||||
|
||||
#### 1. Edit the English YAML file
|
||||
|
||||
Add the key to `lib/I18n/translations/english.yaml`:
|
||||
|
||||
```yaml
|
||||
STR_MY_NEW_STRING: "My New String"
|
||||
```
|
||||
|
||||
Then add translations in each language file. If a key is missing from a
|
||||
language file, the generator will automatically use the English text as a
|
||||
fallback (and print a warning).
|
||||
|
||||
#### 2. Run the generator script
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
This automatically:
|
||||
- Fills missing translations from English
|
||||
- Updates the `StrId` enum in `I18nKeys.h`
|
||||
- Regenerates all language arrays in `I18nStrings.cpp`
|
||||
|
||||
#### 3. Use in code
|
||||
|
||||
```cpp
|
||||
#include <I18n.h>
|
||||
|
||||
// Using the tr() macro (recommended)
|
||||
renderer.drawText(font, x, y, tr(STR_MY_NEW_STRING));
|
||||
|
||||
// Using I18N.get() directly
|
||||
const char* text = I18N.get(StrId::STR_MY_NEW_STRING);
|
||||
```
|
||||
|
||||
**That's it!** No manual array synchronization needed.
|
||||
|
||||
---
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
To add support for a new language (e.g., Italian):
|
||||
|
||||
#### 1. Create a new YAML file
|
||||
|
||||
Create `lib/I18n/translations/italian.yaml`:
|
||||
|
||||
```yaml
|
||||
_language_name: "Italiano"
|
||||
_language_code: "ITALIAN"
|
||||
_order: "7"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "AVVIO"
|
||||
```
|
||||
|
||||
You only need to include the strings you have translations for. Missing
|
||||
keys will fall back to English automatically.
|
||||
|
||||
#### 2. Run the generator
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
This automatically updates all necessary code.
|
||||
|
||||
---
|
||||
|
||||
### Modifying Existing Translations
|
||||
|
||||
Simply edit the relevant YAML file and regenerate:
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### UTF-8 Encoding
|
||||
|
||||
The YAML files use UTF-8 encoding. Special characters are automatically converted to C++ UTF-8 hex sequences by the generator.
|
||||
|
||||
---
|
||||
|
||||
### I18N API Reference
|
||||
|
||||
```cpp
|
||||
// === Convenience Macros (Recommended) ===
|
||||
|
||||
// tr(id) - Get translated string without StrId:: prefix
|
||||
const char* text = tr(STR_SETTINGS_TITLE);
|
||||
renderer.drawText(font, x, y, tr(STR_BROWSE_FILES));
|
||||
Serial.printf("Status: %s\n", tr(STR_CONNECTED));
|
||||
|
||||
// I18N - Shorthand for I18n::getInstance()
|
||||
I18N.setLanguage(Language::SPANISH);
|
||||
Language lang = I18N.getLanguage();
|
||||
|
||||
// === Full API ===
|
||||
|
||||
// Get the singleton instance
|
||||
I18n& instance = I18n::getInstance();
|
||||
|
||||
// Get translated string (three equivalent ways)
|
||||
const char* text = tr(STR_SETTINGS_TITLE); // Macro (recommended)
|
||||
const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call
|
||||
const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload
|
||||
|
||||
// Set language
|
||||
I18N.setLanguage(Language::SPANISH);
|
||||
|
||||
// Get current language
|
||||
Language lang = I18N.getLanguage();
|
||||
|
||||
// Save language setting to file
|
||||
I18N.saveSettings();
|
||||
|
||||
// Load language setting from file
|
||||
I18N.loadSettings();
|
||||
|
||||
// Get character set for font subsetting (static method)
|
||||
const char* chars = I18n::getCharacterSet(Language::FRENCH);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Storage
|
||||
|
||||
Language settings are stored in:
|
||||
```
|
||||
/.crosspoint/language.bin
|
||||
```
|
||||
|
||||
This file contains:
|
||||
- Version byte
|
||||
- Current language selection (1 byte)
|
||||
|
||||
---
|
||||
|
||||
## Translation Workflow
|
||||
|
||||
### For Developers (Adding Features)
|
||||
|
||||
1. Add new strings to `lib/I18n/translations/english.yaml`
|
||||
2. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
|
||||
3. Use the new `StrId` in your code
|
||||
4. Request translations from translators
|
||||
|
||||
### For Translators
|
||||
|
||||
1. Open the YAML file for your language in `lib/I18n/translations/`
|
||||
2. Add or update translations using the format `STR_KEY: "translated text"`
|
||||
3. Keep translations concise (E-ink space constraints)
|
||||
4. Make sure the file is in UTF-8 encoding
|
||||
5. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/` to verify
|
||||
6. Test on device or submit for review
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 184 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,36 @@
|
||||
# Translators
|
||||
|
||||
Below is a list of users and languages CrossPoint may support in the future.
|
||||
Note because a language is below does not mean there is official support for the language at this time.
|
||||
|
||||
## Contributing
|
||||
|
||||
If you'd like to add your name to this list, please open a PR adding yourself and your Github link. Thank you!
|
||||
|
||||
## French
|
||||
- [Spigaw](https://github.com/Spigaw)
|
||||
- [CaptainFrito](https://github.com/CaptainFrito)
|
||||
|
||||
## German
|
||||
- [DavidOrtmann](https://github.com/DavidOrtmann)
|
||||
|
||||
## Czech
|
||||
- [brbla](https://github.com/brbla)
|
||||
|
||||
## Portuguese (Brazil)
|
||||
- [yagofarias](https://github.com/yagofarias)
|
||||
|
||||
## Italian
|
||||
- [fragolinux](https://github.com/fragolinux)
|
||||
|
||||
## Russian
|
||||
- [madebyKir](https://github.com/madebyKir)
|
||||
- [mrtnvgr](https://github.com/mrtnvgr)
|
||||
|
||||
## Spanish
|
||||
- [yeyeto2788](https://github.com/yeyeto2788)
|
||||
- [Skrzakk](https://github.com/Skrzakk)
|
||||
- [pablohc](https://github.com/pablohc)
|
||||
|
||||
## Swedish
|
||||
- [dawiik](https://github.com/dawiik)
|
||||
@@ -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
|
||||
+31
-68
@@ -1,14 +1,14 @@
|
||||
# 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.
|
||||
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload 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
|
||||
- Upload 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
|
||||
- Create folders to organize your library
|
||||
- Delete files and folders
|
||||
|
||||
## Prerequisites
|
||||
@@ -129,34 +129,31 @@ 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
|
||||
- **Folders** are highlighted in yellow and indicated with a 📁 icon
|
||||
- **EPUB Files** are highlighted in green and indicated with a 📗 icon
|
||||
- **All Other Files** are not highlighted and indicated 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
|
||||
#### Uploading 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.
|
||||
1. Click the **📤 Upload** button in the top-right corner
|
||||
2. Click **Choose File** and select a file from your device
|
||||
3. Click **Upload**
|
||||
4. A progress bar will show the upload status
|
||||
5. The page will automatically refresh when the upload is complete
|
||||
|
||||
<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**
|
||||
1. Click the **📁 New Folder** button in the top-right corner
|
||||
2. Enter a folder name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
|
||||
3. Click **Create Folder**
|
||||
|
||||
This is useful for organizing your ebooks by genre, author, or series.
|
||||
This is useful for organizing your library by genre, author, series or file type.
|
||||
|
||||
#### Deleting Files and Folders
|
||||
|
||||
@@ -168,60 +165,26 @@ This is useful for organizing your ebooks by genre, author, or series.
|
||||
|
||||
**Note:** Folders must be empty before they can be deleted.
|
||||
|
||||
---
|
||||
#### Moving Files
|
||||
|
||||
## Troubleshooting
|
||||
1. Click the **📂** (folder) icon next to any file
|
||||
2. Enter a folder name or select one from the dropdown
|
||||
3. Click **Move** to relocate the file
|
||||
|
||||
### Cannot See the Device on the Network
|
||||
**Note:** Typing in a nonexistent folder name will result in the following error: "Failed to move: Destination not found"
|
||||
|
||||
**Problem:** Browser shows "Cannot connect" or "Site can't be reached"
|
||||
#### Renaming Files
|
||||
|
||||
**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
|
||||
1. Click the **✏️** (pencil) icon next to any file
|
||||
2. Enter a file name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
|
||||
3. Click **Rename** to permanently rename the file
|
||||
|
||||
---
|
||||
|
||||
## 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. Detailed documentation can be found [here](./webserver-endpoints.md).
|
||||
|
||||
## Security Notes
|
||||
|
||||
- The web server runs on port 80 (standard HTTP)
|
||||
@@ -237,7 +200,6 @@ This is useful for organizing your ebooks by genre, author, or series.
|
||||
- **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)
|
||||
|
||||
---
|
||||
@@ -246,7 +208,7 @@ This is useful for organizing your ebooks by genre, author, or series.
|
||||
|
||||
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
|
||||
3. **Upload multiple files** - You can select and upload multiple files at once; the manager will queue them and refresh when the batch is finished
|
||||
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
|
||||
@@ -269,4 +231,5 @@ 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
|
||||
|
||||
+30
-15
@@ -17,13 +17,17 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
|
||||
int cursorX = startX;
|
||||
const int cursorY = startY;
|
||||
int lastBaseX = startX;
|
||||
int lastBaseAdvance = 0;
|
||||
int lastBaseTop = 0;
|
||||
bool hasBaseGlyph = false;
|
||||
constexpr int MIN_COMBINING_GAP_PX = 1;
|
||||
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('?');
|
||||
glyph = getGlyph(REPLACEMENT_GLYPH);
|
||||
}
|
||||
|
||||
if (!glyph) {
|
||||
@@ -31,11 +35,30 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
continue;
|
||||
}
|
||||
|
||||
*minX = std::min(*minX, cursorX + glyph->left);
|
||||
*maxX = std::max(*maxX, cursorX + glyph->left + glyph->width);
|
||||
*minY = std::min(*minY, cursorY + glyph->top - glyph->height);
|
||||
*maxY = std::max(*maxY, cursorY + glyph->top);
|
||||
cursorX += glyph->advanceX;
|
||||
const bool isCombining = utf8IsCombiningMark(cp);
|
||||
int raiseBy = 0;
|
||||
if (isCombining && hasBaseGlyph) {
|
||||
const int currentGap = glyph->top - glyph->height - lastBaseTop;
|
||||
if (currentGap < MIN_COMBINING_GAP_PX) {
|
||||
raiseBy = MIN_COMBINING_GAP_PX - currentGap;
|
||||
}
|
||||
}
|
||||
|
||||
const int glyphBaseX = (isCombining && hasBaseGlyph) ? (lastBaseX + lastBaseAdvance / 2) : cursorX;
|
||||
const int glyphBaseY = cursorY - raiseBy;
|
||||
|
||||
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
||||
*maxX = std::max(*maxX, glyphBaseX + glyph->left + glyph->width);
|
||||
*minY = std::min(*minY, glyphBaseY + glyph->top - glyph->height);
|
||||
*maxY = std::max(*maxY, glyphBaseY + glyph->top);
|
||||
|
||||
if (!isCombining) {
|
||||
lastBaseX = cursorX;
|
||||
lastBaseAdvance = glyph->advanceX;
|
||||
lastBaseTop = glyph->top;
|
||||
hasBaseGlyph = true;
|
||||
cursorX += glyph->advanceX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +71,6 @@ void EpdFont::getTextDimensions(const char* string, int* w, int* h) const {
|
||||
*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;
|
||||
const int count = data->intervalCount;
|
||||
|
||||
@@ -9,7 +9,6 @@ class EpdFont {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -12,9 +12,18 @@ typedef struct {
|
||||
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
|
||||
uint32_t dataOffset; ///< Pointer into EpdFont->bitmap (or within-group offset for compressed fonts)
|
||||
} EpdGlyph;
|
||||
|
||||
/// Compressed font group: a DEFLATE-compressed block of glyph bitmaps
|
||||
typedef struct {
|
||||
uint32_t compressedOffset; ///< Byte offset into compressed data array
|
||||
uint32_t compressedSize; ///< Compressed DEFLATE stream size
|
||||
uint32_t uncompressedSize; ///< Decompressed size
|
||||
uint16_t glyphCount; ///< Number of glyphs in this group
|
||||
uint16_t firstGlyphIndex; ///< First glyph index in the global glyph array
|
||||
} EpdFontGroup;
|
||||
|
||||
/// Glyph interval structure
|
||||
typedef struct {
|
||||
uint32_t first; ///< The first unicode code point of the interval
|
||||
@@ -32,4 +41,6 @@ typedef struct {
|
||||
int ascender; ///< Maximal height of a glyph above the base line
|
||||
int descender; ///< Maximal height of a glyph below the base line
|
||||
bool is2Bit;
|
||||
const EpdFontGroup* groups; ///< NULL for uncompressed fonts
|
||||
uint16_t groupCount; ///< 0 for uncompressed fonts
|
||||
} EpdFontData;
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
#include "EpdFontFamily.h"
|
||||
|
||||
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
||||
if (style == BOLD && bold) {
|
||||
// Extract font style bits (ignore UNDERLINE bit for font selection)
|
||||
const bool hasBold = (style & BOLD) != 0;
|
||||
const bool hasItalic = (style & ITALIC) != 0;
|
||||
|
||||
if (hasBold && hasItalic) {
|
||||
if (boldItalic) return boldItalic;
|
||||
if (bold) return bold;
|
||||
if (italic) return italic;
|
||||
} else if (hasBold && bold) {
|
||||
return bold;
|
||||
}
|
||||
if (style == ITALIC && italic) {
|
||||
} else if (hasItalic && italic) {
|
||||
return italic;
|
||||
}
|
||||
if (style == BOLD_ITALIC) {
|
||||
if (boldItalic) {
|
||||
return boldItalic;
|
||||
}
|
||||
if (bold) {
|
||||
return bold;
|
||||
}
|
||||
if (italic) {
|
||||
return italic;
|
||||
}
|
||||
}
|
||||
|
||||
return regular;
|
||||
}
|
||||
@@ -26,10 +22,6 @@ void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const
|
||||
getFont(style)->getTextDimensions(string, w, h);
|
||||
}
|
||||
|
||||
bool EpdFontFamily::hasPrintableChars(const char* string, const Style style) const {
|
||||
return getFont(style)->hasPrintableChars(string);
|
||||
}
|
||||
|
||||
const EpdFontData* EpdFontFamily::getData(const Style style) const { return getFont(style)->data; }
|
||||
|
||||
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) const {
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
|
||||
class EpdFontFamily {
|
||||
public:
|
||||
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
|
||||
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3, UNDERLINE = 4 };
|
||||
|
||||
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, Style style = REGULAR) const;
|
||||
bool hasPrintableChars(const char* string, Style style = REGULAR) const;
|
||||
const EpdFontData* getData(Style style = REGULAR) const;
|
||||
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
#include "FontDecompressor.h"
|
||||
|
||||
#include <Logging.h>
|
||||
#include <uzlib.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
bool FontDecompressor::init() {
|
||||
clearCache();
|
||||
memset(&decomp, 0, sizeof(decomp));
|
||||
return true;
|
||||
}
|
||||
|
||||
void FontDecompressor::freeAllEntries() {
|
||||
for (auto& entry : cache) {
|
||||
if (entry.data) {
|
||||
free(entry.data);
|
||||
entry.data = nullptr;
|
||||
}
|
||||
entry.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
void FontDecompressor::deinit() { freeAllEntries(); }
|
||||
|
||||
void FontDecompressor::clearCache() {
|
||||
freeAllEntries();
|
||||
accessCounter = 0;
|
||||
}
|
||||
|
||||
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint16_t glyphIndex) {
|
||||
for (uint16_t i = 0; i < fontData->groupCount; i++) {
|
||||
uint16_t first = fontData->groups[i].firstGlyphIndex;
|
||||
if (glyphIndex >= first && glyphIndex < first + fontData->groups[i].glyphCount) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return fontData->groupCount; // sentinel = not found
|
||||
}
|
||||
|
||||
FontDecompressor::CacheEntry* FontDecompressor::findInCache(const EpdFontData* fontData, uint16_t groupIndex) {
|
||||
for (auto& entry : cache) {
|
||||
if (entry.valid && entry.font == fontData && entry.groupIndex == groupIndex) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FontDecompressor::CacheEntry* FontDecompressor::findEvictionCandidate() {
|
||||
// Find an invalid slot first
|
||||
for (auto& entry : cache) {
|
||||
if (!entry.valid) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
// Otherwise evict LRU
|
||||
CacheEntry* lru = &cache[0];
|
||||
for (auto& entry : cache) {
|
||||
if (entry.lastUsed < lru->lastUsed) {
|
||||
lru = &entry;
|
||||
}
|
||||
}
|
||||
return lru;
|
||||
}
|
||||
|
||||
bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, CacheEntry* entry) {
|
||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||
|
||||
// Free old buffer if reusing a slot
|
||||
if (entry->data) {
|
||||
free(entry->data);
|
||||
entry->data = nullptr;
|
||||
}
|
||||
entry->valid = false;
|
||||
|
||||
// Allocate output buffer
|
||||
auto* outBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
|
||||
if (!outBuf) {
|
||||
LOG_ERR("FDC", "Failed to allocate %u bytes for group %u", group.uncompressedSize, groupIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decompress using uzlib
|
||||
const uint8_t* inputBuf = &fontData->bitmap[group.compressedOffset];
|
||||
|
||||
uzlib_uncompress_init(&decomp, NULL, 0);
|
||||
decomp.source = inputBuf;
|
||||
decomp.source_limit = inputBuf + group.compressedSize;
|
||||
decomp.dest_start = outBuf;
|
||||
decomp.dest = outBuf;
|
||||
decomp.dest_limit = outBuf + group.uncompressedSize;
|
||||
|
||||
int res = uzlib_uncompress(&decomp);
|
||||
|
||||
if (res < 0 || decomp.dest != decomp.dest_limit) {
|
||||
LOG_ERR("FDC", "Decompression failed for group %u (status %d)", groupIndex, res);
|
||||
free(outBuf);
|
||||
return false;
|
||||
}
|
||||
|
||||
entry->font = fontData;
|
||||
entry->groupIndex = groupIndex;
|
||||
entry->data = outBuf;
|
||||
entry->dataSize = group.uncompressedSize;
|
||||
entry->valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint16_t glyphIndex) {
|
||||
if (!fontData->groups || fontData->groupCount == 0) {
|
||||
return &fontData->bitmap[glyph->dataOffset];
|
||||
}
|
||||
|
||||
uint16_t groupIndex = getGroupIndex(fontData, glyphIndex);
|
||||
if (groupIndex >= fontData->groupCount) {
|
||||
LOG_ERR("FDC", "Glyph %u not found in any group", glyphIndex);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check cache
|
||||
CacheEntry* entry = findInCache(fontData, groupIndex);
|
||||
if (entry) {
|
||||
entry->lastUsed = ++accessCounter;
|
||||
if (glyph->dataOffset + glyph->dataLength > entry->dataSize) {
|
||||
LOG_ERR("FDC", "dataOffset %u + dataLength %u out of bounds for group %u (size %u)", glyph->dataOffset,
|
||||
glyph->dataLength, groupIndex, entry->dataSize);
|
||||
return nullptr;
|
||||
}
|
||||
return &entry->data[glyph->dataOffset];
|
||||
}
|
||||
|
||||
// Cache miss - decompress
|
||||
entry = findEvictionCandidate();
|
||||
if (!decompressGroup(fontData, groupIndex, entry)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
entry->lastUsed = ++accessCounter;
|
||||
if (glyph->dataOffset + glyph->dataLength > entry->dataSize) {
|
||||
LOG_ERR("FDC", "dataOffset %u + dataLength %u out of bounds for group %u (size %u)", glyph->dataOffset,
|
||||
glyph->dataLength, groupIndex, entry->dataSize);
|
||||
return nullptr;
|
||||
}
|
||||
return &entry->data[glyph->dataOffset];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <uzlib.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "EpdFontData.h"
|
||||
|
||||
class FontDecompressor {
|
||||
public:
|
||||
bool init();
|
||||
void deinit();
|
||||
|
||||
// Returns pointer to decompressed bitmap data for the given glyph.
|
||||
// Valid until LRU eviction (safe for the duration of one glyph render).
|
||||
const uint8_t* getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint16_t glyphIndex);
|
||||
|
||||
// Evict all cached decompressed groups (call between pages for within-page-only caching).
|
||||
void clearCache();
|
||||
|
||||
private:
|
||||
static constexpr uint8_t CACHE_SLOTS = 4;
|
||||
|
||||
struct CacheEntry {
|
||||
const EpdFontData* font = nullptr;
|
||||
uint16_t groupIndex = 0;
|
||||
uint8_t* data = nullptr;
|
||||
uint32_t dataSize = 0;
|
||||
uint32_t lastUsed = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct uzlib_uncomp decomp = {};
|
||||
CacheEntry cache[CACHE_SLOTS] = {};
|
||||
uint32_t accessCounter = 0;
|
||||
|
||||
void freeAllEntries();
|
||||
uint16_t getGroupIndex(const EpdFontData* fontData, uint16_t glyphIndex);
|
||||
CacheEntry* findInCache(const EpdFontData* fontData, uint16_t groupIndex);
|
||||
CacheEntry* findEvictionCandidate();
|
||||
bool decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, CacheEntry* entry);
|
||||
};
|
||||
+2192
-3864
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2544
-4851
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2715
-5985
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3231
-7688
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2310
-3845
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2695
-4944
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2944
-6176
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3288
-7500
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,12 @@
|
||||
* name: notosans_8_regular
|
||||
* size: 8
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py notosans_8_regular 8 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
static const uint8_t notosans_8_regularBitmaps[10949] = {
|
||||
static const uint8_t notosans_8_regularBitmaps[11210] = {
|
||||
0xDB, 0x6D, 0xB6, 0xC3, 0xF4, 0xDE, 0xF7, 0xBD, 0x80, 0x0D, 0x83, 0x30, 0x66, 0x3F, 0xF7, 0xFE,
|
||||
0x36, 0x04, 0xC7, 0xFE, 0xFF, 0xC6, 0x40, 0xD8, 0x1B, 0x00, 0x18, 0x18, 0xFE, 0xFE, 0xD8, 0xF8,
|
||||
0xFC, 0x3E, 0x1F, 0x1B, 0xFF, 0xFE, 0x38, 0x18, 0x00, 0x01, 0xE1, 0x86, 0xCC, 0x13, 0x30, 0xCD,
|
||||
@@ -655,44 +656,60 @@ static const uint8_t notosans_8_regularBitmaps[10949] = {
|
||||
0xF6, 0xF8, 0x7E, 0x80, 0xFF, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0xFF, 0x9F, 0x66, 0x66,
|
||||
0x66, 0x66, 0x66, 0x00, 0xFF, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x06, 0xFF, 0x66, 0x66,
|
||||
0x66, 0x66, 0x66, 0x00, 0x6F, 0xF6, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0xEE, 0x6F, 0x66, 0x66,
|
||||
0x66, 0x66, 0x66, 0x00, 0x00, 0x1F, 0x1D, 0x1C, 0x0C, 0xFE, 0x7F, 0xB0, 0xD8, 0x7F, 0x8F, 0xC3,
|
||||
0x01, 0x80, 0xF8, 0x05, 0x83, 0xE3, 0xF8, 0xFE, 0x6F, 0x1A, 0xCF, 0xB3, 0xE8, 0x7E, 0x1F, 0x87,
|
||||
0x60, 0xFE, 0x3F, 0x0F, 0x00, 0x00, 0x0F, 0xCF, 0xEE, 0x06, 0x03, 0x2D, 0xBE, 0xDC, 0x6C, 0x36,
|
||||
0x1F, 0x07, 0xB1, 0xF8, 0x30, 0x3F, 0x9F, 0xCC, 0x06, 0x03, 0x01, 0xF8, 0xFE, 0x60, 0x7C, 0x7E,
|
||||
0x0C, 0x06, 0x00, 0x00, 0x0F, 0xCF, 0xE6, 0x03, 0x03, 0xF3, 0xF8, 0x60, 0xFE, 0x3F, 0x0C, 0x1F,
|
||||
0xFF, 0xF8, 0x00, 0x00, 0x03, 0x00, 0x0C, 0x3F, 0xFE, 0xFF, 0xFB, 0x8C, 0x7C, 0x31, 0xF1, 0xC7,
|
||||
0xC7, 0x1F, 0x3C, 0x7C, 0xF1, 0xF6, 0xC7, 0x18, 0x00, 0x40, 0x00, 0x73, 0x1C, 0xC7, 0xB1, 0xEC,
|
||||
0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0x67, 0x19, 0xC6, 0x71, 0x8C, 0xFC, 0x0F, 0xE0, 0xC6, 0x0C, 0x6C,
|
||||
0xC6, 0xCC, 0xFF, 0xFD, 0xEF, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0F, 0x00, 0x00, 0xFC, 0x07,
|
||||
0xE0, 0x33, 0x81, 0x8D, 0xEC, 0xFF, 0x7E, 0xC3, 0xF7, 0x1B, 0x1E, 0xCC, 0x76, 0x61, 0xF3, 0xED,
|
||||
0x8F, 0xE0, 0x04, 0x00, 0xCE, 0x79, 0xCF, 0x39, 0xB7, 0x36, 0xFF, 0xFF, 0xFF, 0xFB, 0xDE, 0x7B,
|
||||
0xC7, 0x38, 0xE7, 0x18, 0xC0, 0xFE, 0x7F, 0xEF, 0x0D, 0xED, 0xBD, 0xB7, 0xB6, 0xF6, 0xDE, 0xDB,
|
||||
0xDB, 0x7B, 0x0F, 0x7F, 0xEF, 0xE0, 0x01, 0x81, 0xF8, 0x7F, 0x00, 0xC3, 0xF8, 0xFF, 0x18, 0x63,
|
||||
0x0C, 0x61, 0x8C, 0x31, 0x86, 0x39, 0xC3, 0xF8, 0x10, 0x0F, 0xF1, 0xFE, 0x00, 0x07, 0xE3, 0xF8,
|
||||
0xC0, 0x30, 0x3F, 0xCF, 0xF1, 0xF8, 0xFE, 0x1C, 0x03, 0x00, 0xFE, 0x1F, 0x80, 0x80, 0x61, 0x9C,
|
||||
0xE7, 0x71, 0xD8, 0x7C, 0x3F, 0xEF, 0xF9, 0xF8, 0x76, 0x1C, 0xC7, 0x39, 0xC7, 0xFF, 0xFF, 0xE0,
|
||||
0xC0, 0x3C, 0x0F, 0x07, 0x07, 0xF0, 0xB8, 0x1C, 0x1F, 0x02, 0xC0, 0x30, 0x00, 0x00, 0xFE, 0x00,
|
||||
0xFF, 0x00, 0x33, 0x80, 0x31, 0x80, 0x31, 0xC0, 0x31, 0xC0, 0x31, 0xDE, 0x71, 0xBF, 0x63, 0xB3,
|
||||
0x67, 0x73, 0x7F, 0x77, 0xFC, 0x7E, 0x00, 0x68, 0x00, 0x60, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x07,
|
||||
0x83, 0xE1, 0xB0, 0xD8, 0x6C, 0x3C, 0x0E, 0x06, 0x1F, 0x9F, 0xCF, 0x6D, 0xB8, 0x0C, 0x06, 0x0F,
|
||||
0x0F, 0x00, 0x7E, 0x1F, 0xCF, 0xFF, 0xFF, 0xFF, 0xBF, 0xF7, 0xF1, 0xF8, 0x60, 0x18, 0x06, 0x01,
|
||||
0x80, 0x06, 0x07, 0xF7, 0xFD, 0x98, 0xE6, 0x31, 0x8C, 0x7F, 0x1F, 0xC6, 0xF1, 0xBE, 0x6D, 0xFF,
|
||||
0x3F, 0xC1, 0x80, 0x40, 0x0C, 0x03, 0xC0, 0x78, 0x0F, 0x0F, 0xFD, 0xFF, 0x8C, 0xE7, 0xFE, 0x61,
|
||||
0x8C, 0x3B, 0x03, 0x60, 0x60, 0x00, 0x3F, 0x9F, 0xC0, 0x70, 0x3F, 0xFC, 0x7D, 0xFF, 0xFF, 0xF0,
|
||||
0x38, 0x0F, 0xF7, 0xF8, 0x60, 0x04, 0x0F, 0xE7, 0xFB, 0x90, 0xE4, 0x31, 0x0C, 0x43, 0x10, 0xC4,
|
||||
0x31, 0x0E, 0x41, 0xFE, 0x3F, 0x81, 0x80, 0x40, 0x22, 0x19, 0x8F, 0xFF, 0xFF, 0x66, 0x19, 0x86,
|
||||
0x61, 0x98, 0x66, 0x19, 0xD6, 0x3C, 0x02, 0x00, 0x00, 0x7E, 0x0F, 0xE0, 0xC0, 0x0C, 0x00, 0xE0,
|
||||
0x07, 0x80, 0x3E, 0x00, 0xEF, 0xFF, 0xED, 0xFE, 0xDE, 0x7F, 0xC2, 0xF8, 0x24, 0x80, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xF1, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x80, 0xFF, 0x7E, 0x18, 0xFF,
|
||||
0x18, 0xF8, 0xF0, 0xE0, 0x60, 0x30, 0x38, 0x18, 0x30, 0x0C, 0x83, 0xE3, 0xE0, 0xFE, 0x1F, 0x0F,
|
||||
0x18, 0xC6, 0x31, 0x8C, 0xE3, 0xF0, 0xF8, 0x00, 0x00, 0x00, 0x07, 0x80, 0x36, 0x01, 0xB0, 0x0F,
|
||||
0xFF, 0x1F, 0xFC, 0x60, 0x63, 0xDB, 0x1F, 0xF8, 0xE9, 0x8F, 0x00, 0x5F, 0x80, 0x7E, 0x00, 0x40,
|
||||
0x03, 0x00, 0x18, 0x00, 0xC0, 0x1F, 0x03, 0xFE, 0x3D, 0xB9, 0x8C, 0xCC, 0x67, 0x63, 0x1B, 0x10,
|
||||
0xD8, 0x06, 0xC0, 0x30, 0x7F, 0x1F, 0xE7, 0x19, 0xC6, 0x71, 0x9D, 0xEF, 0xF3, 0xE0, 0xFC, 0x3F,
|
||||
0x07, 0x01, 0xC0, 0x1B, 0x03, 0xE1, 0xFF, 0x3D, 0xED, 0xBF, 0xB6, 0xF6, 0xDE, 0x53, 0xC0, 0x1C,
|
||||
0x01, 0x80, 0x3F, 0xE7, 0xFC, 0x3C, 0x3C, 0xFE, 0xFF, 0xE3, 0xE3, 0xE7, 0xFE, 0xFF, 0xE3, 0xE3,
|
||||
0xE3, 0xFF, 0xFE, 0x3C, 0x3C, 0x00, 0x3F, 0x7E, 0x60, 0x60, 0xE0, 0xE0, 0x60, 0x7E, 0x3F, 0x0C,
|
||||
0x7F, 0x7E, 0x7F, 0xFF, 0xC0,
|
||||
0x66, 0x66, 0x66, 0x00, 0x21, 0xEC, 0xF3, 0xCF, 0x3C, 0xDE, 0x20, 0x15, 0x55, 0x40, 0x00, 0x63,
|
||||
0x9E, 0xDB, 0xFF, 0xC6, 0x00, 0x01, 0xEC, 0x3C, 0xF8, 0x30, 0xFE, 0x20, 0x19, 0xEE, 0x3E, 0xFF,
|
||||
0x3C, 0xDE, 0x30, 0x03, 0xF1, 0x86, 0x18, 0xC3, 0x18, 0x00, 0x31, 0xEC, 0xFE, 0x7B, 0x7C, 0xFE,
|
||||
0x30, 0x33, 0xEC, 0xF3, 0xFD, 0xF1, 0x9E, 0x60, 0x20, 0xCF, 0xBE, 0x30, 0x80, 0xFB, 0xE0, 0x7B,
|
||||
0xEF, 0xBE, 0x0F, 0x69, 0x36, 0xCC, 0x19, 0xB6, 0xDB, 0x58, 0x19, 0xF4, 0xD1, 0x45, 0x14, 0x40,
|
||||
0x21, 0xEC, 0xF3, 0xCF, 0x3C, 0xDE, 0x20, 0x07, 0xF3, 0x33, 0x33, 0x00, 0x33, 0xE5, 0x86, 0x18,
|
||||
0xC6, 0x3E, 0x00, 0x33, 0xE1, 0x86, 0x78, 0x30, 0xFE, 0x20, 0x00, 0x63, 0x9E, 0xDB, 0xFF, 0xC6,
|
||||
0x00, 0x01, 0xEC, 0x3C, 0xF8, 0x30, 0xFE, 0x20, 0x19, 0xEE, 0x3E, 0xFF, 0x3C, 0xDE, 0x30, 0x03,
|
||||
0xF1, 0x86, 0x18, 0xC3, 0x18, 0x00, 0x31, 0xEC, 0xFE, 0x7B, 0x7C, 0xFE, 0x30, 0x33, 0xEC, 0xF3,
|
||||
0xFD, 0xF1, 0x9E, 0x60, 0x20, 0xCF, 0xBE, 0x30, 0x80, 0xFB, 0xE0, 0x7B, 0xEF, 0xBE, 0x0F, 0x69,
|
||||
0x36, 0xCC, 0x19, 0xB6, 0xDB, 0x58, 0x78, 0x37, 0xFB, 0xDD, 0xE0, 0x79, 0x3F, 0xF0, 0x69, 0xE0,
|
||||
0x7B, 0x3C, 0x71, 0x6D, 0xE0, 0xCD, 0xE3, 0x1C, 0x7B, 0x30, 0x78, 0x37, 0xFF, 0xDD, 0xE0, 0x41,
|
||||
0x07, 0xDB, 0x45, 0x14, 0x51, 0x41, 0x04, 0xD6, 0x71, 0xC5, 0x93, 0x55, 0x55, 0x7F, 0x9B, 0x34,
|
||||
0xCD, 0x33, 0x4C, 0xD3, 0x30, 0x7D, 0xB4, 0x51, 0x45, 0x10, 0x7C, 0xD9, 0x12, 0x26, 0xCF, 0x10,
|
||||
0x20, 0x7E, 0x1C, 0x7D, 0xF8, 0x4F, 0x44, 0x46, 0x70, 0x00, 0x1F, 0x1D, 0x1C, 0x0C, 0xFE, 0x7F,
|
||||
0xB0, 0xD8, 0x7F, 0x8F, 0xC3, 0x01, 0x80, 0xF8, 0x05, 0x83, 0xE3, 0xF8, 0xFE, 0x6F, 0x1A, 0xCF,
|
||||
0xB3, 0xE8, 0x7E, 0x1F, 0x87, 0x60, 0xFE, 0x3F, 0x0F, 0x00, 0x00, 0x0F, 0xCF, 0xEE, 0x06, 0x03,
|
||||
0x2D, 0xBE, 0xDC, 0x6C, 0x36, 0x1F, 0x07, 0xB1, 0xF8, 0x30, 0x3F, 0x9F, 0xCC, 0x06, 0x03, 0x01,
|
||||
0xF8, 0xFE, 0x60, 0x7C, 0x7E, 0x0C, 0x06, 0x00, 0x00, 0x0F, 0xCF, 0xE6, 0x03, 0x03, 0xF3, 0xF8,
|
||||
0x60, 0xFE, 0x3F, 0x0C, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x03, 0x00, 0x0C, 0x3F, 0xFE, 0xFF, 0xFB,
|
||||
0x8C, 0x7C, 0x31, 0xF1, 0xC7, 0xC7, 0x1F, 0x3C, 0x7C, 0xF1, 0xF6, 0xC7, 0x18, 0x00, 0x40, 0x00,
|
||||
0x73, 0x1C, 0xC7, 0xB1, 0xEC, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0x67, 0x19, 0xC6, 0x71, 0x8C, 0xFC,
|
||||
0x0F, 0xE0, 0xC6, 0x0C, 0x6C, 0xC6, 0xCC, 0xFF, 0xFD, 0xEF, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC,
|
||||
0x0F, 0x00, 0x00, 0xFC, 0x07, 0xE0, 0x33, 0x81, 0x8D, 0xEC, 0xFF, 0x7E, 0xC3, 0xF7, 0x1B, 0x1E,
|
||||
0xCC, 0x76, 0x61, 0xF3, 0xED, 0x8F, 0xE0, 0x04, 0x00, 0xCE, 0x79, 0xCF, 0x39, 0xB7, 0x36, 0xFF,
|
||||
0xFF, 0xFF, 0xFB, 0xDE, 0x7B, 0xC7, 0x38, 0xE7, 0x18, 0xC0, 0xFE, 0x7F, 0xEF, 0x0D, 0xED, 0xBD,
|
||||
0xB7, 0xB6, 0xF6, 0xDE, 0xDB, 0xDB, 0x7B, 0x0F, 0x7F, 0xEF, 0xE0, 0x01, 0x81, 0xF8, 0x7F, 0x00,
|
||||
0xC3, 0xF8, 0xFF, 0x18, 0x63, 0x0C, 0x61, 0x8C, 0x31, 0x86, 0x39, 0xC3, 0xF8, 0x10, 0x0F, 0xF1,
|
||||
0xFE, 0x00, 0x07, 0xE3, 0xF8, 0xC0, 0x30, 0x3F, 0xCF, 0xF1, 0xF8, 0xFE, 0x1C, 0x03, 0x00, 0xFE,
|
||||
0x1F, 0x80, 0x80, 0x61, 0x9C, 0xE7, 0x71, 0xD8, 0x7C, 0x3F, 0xEF, 0xF9, 0xF8, 0x76, 0x1C, 0xC7,
|
||||
0x39, 0xC7, 0xFF, 0xFF, 0xE0, 0xC0, 0x3C, 0x0F, 0x07, 0x07, 0xF0, 0xB8, 0x1C, 0x1F, 0x02, 0xC0,
|
||||
0x30, 0x00, 0x00, 0xFE, 0x00, 0xFF, 0x00, 0x33, 0x80, 0x31, 0x80, 0x31, 0xC0, 0x31, 0xC0, 0x31,
|
||||
0xDE, 0x71, 0xBF, 0x63, 0xB3, 0x67, 0x73, 0x7F, 0x77, 0xFC, 0x7E, 0x00, 0x68, 0x00, 0x60, 0x00,
|
||||
0xE0, 0x00, 0xC0, 0x00, 0x07, 0x83, 0xE1, 0xB0, 0xD8, 0x6C, 0x3C, 0x0E, 0x06, 0x1F, 0x9F, 0xCF,
|
||||
0x6D, 0xB8, 0x0C, 0x06, 0x0F, 0x0F, 0x00, 0x7E, 0x1F, 0xCF, 0xFF, 0xFF, 0xFF, 0xBF, 0xF7, 0xF1,
|
||||
0xF8, 0x60, 0x18, 0x06, 0x01, 0x80, 0x06, 0x07, 0xF7, 0xFD, 0x98, 0xE6, 0x31, 0x8C, 0x7F, 0x1F,
|
||||
0xC6, 0xF1, 0xBE, 0x6D, 0xFF, 0x3F, 0xC1, 0x80, 0x40, 0x0C, 0x03, 0xC0, 0x78, 0x0F, 0x0F, 0xFD,
|
||||
0xFF, 0x8C, 0xE7, 0xFE, 0x61, 0x8C, 0x3B, 0x03, 0x60, 0x60, 0x00, 0x3F, 0x9F, 0xC0, 0x70, 0x3F,
|
||||
0xFC, 0x7D, 0xFF, 0xFF, 0xF0, 0x38, 0x0F, 0xF7, 0xF8, 0x60, 0x04, 0x0F, 0xE7, 0xFB, 0x90, 0xE4,
|
||||
0x31, 0x0C, 0x43, 0x10, 0xC4, 0x31, 0x0E, 0x41, 0xFE, 0x3F, 0x81, 0x80, 0x40, 0x22, 0x19, 0x8F,
|
||||
0xFF, 0xFF, 0x66, 0x19, 0x86, 0x61, 0x98, 0x66, 0x19, 0xD6, 0x3C, 0x02, 0x00, 0x00, 0x7E, 0x0F,
|
||||
0xE0, 0xC0, 0x0C, 0x00, 0xE0, 0x07, 0x80, 0x3E, 0x00, 0xEF, 0xFF, 0xED, 0xFE, 0xDE, 0x7F, 0xC2,
|
||||
0xF8, 0x24, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03,
|
||||
0x80, 0xFF, 0x7E, 0x18, 0xFF, 0x18, 0xF8, 0xF0, 0xE0, 0x60, 0x30, 0x38, 0x18, 0x30, 0x0C, 0x83,
|
||||
0xE3, 0xE0, 0xFE, 0x1F, 0x0F, 0x18, 0xC6, 0x31, 0x8C, 0xE3, 0xF0, 0xF8, 0x00, 0x00, 0x00, 0x07,
|
||||
0x80, 0x36, 0x01, 0xB0, 0x0F, 0xFF, 0x1F, 0xFC, 0x60, 0x63, 0xDB, 0x1F, 0xF8, 0xE9, 0x8F, 0x00,
|
||||
0x5F, 0x80, 0x7E, 0x00, 0x40, 0x03, 0x00, 0x18, 0x00, 0xC0, 0x1F, 0x03, 0xFE, 0x3D, 0xB9, 0x8C,
|
||||
0xCC, 0x67, 0x63, 0x1B, 0x10, 0xD8, 0x06, 0xC0, 0x30, 0x7F, 0x1F, 0xE7, 0x19, 0xC6, 0x71, 0x9D,
|
||||
0xEF, 0xF3, 0xE0, 0xFC, 0x3F, 0x07, 0x01, 0xC0, 0x1B, 0x03, 0xE1, 0xFF, 0x3D, 0xED, 0xBF, 0xB6,
|
||||
0xF6, 0xDE, 0x53, 0xC0, 0x1C, 0x01, 0x80, 0x3F, 0xE7, 0xFC, 0x3C, 0x3C, 0xFE, 0xFF, 0xE3, 0xE3,
|
||||
0xE7, 0xFE, 0xFF, 0xE3, 0xE3, 0xE3, 0xFF, 0xFE, 0x3C, 0x3C, 0x00, 0x3F, 0x7E, 0x60, 0x60, 0xE0,
|
||||
0xE0, 0x60, 0x7E, 0x3F, 0x0C, 0x7F, 0x7E, 0x7F, 0xFF, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xE0,
|
||||
0x07, 0xF0, 0x0C, 0x38, 0x1F, 0xDC, 0x3F, 0xDE, 0x7F, 0xBF, 0x7F, 0x7F, 0x3F, 0x7E, 0x1F, 0xFC,
|
||||
0x0F, 0xF8, 0x06, 0x70, 0x03, 0xE0, 0x01, 0xC0, 0x00, 0x80,
|
||||
};
|
||||
|
||||
static const EpdGlyph notosans_8_regularGlyphs[] = {
|
||||
@@ -1496,40 +1513,83 @@ static const EpdGlyph notosans_8_regularGlyphs[] = {
|
||||
{ 4, 15, 0, -2, 12, 8, 10332 }, //
|
||||
{ 4, 15, 0, -2, 12, 8, 10340 }, //
|
||||
{ 4, 15, 0, -2, 12, 8, 10348 }, //
|
||||
{ 9, 13, 10, 0, 13, 15, 10356 }, // ₠
|
||||
{ 10, 14, 10, 0, 13, 18, 10371 }, // ₡
|
||||
{ 9, 14, 10, 0, 13, 16, 10389 }, // ₢
|
||||
{ 9, 12, 10, 0, 12, 14, 10405 }, // ₣
|
||||
{ 9, 13, 10, 0, 13, 15, 10419 }, // ₤
|
||||
{ 14, 14, 16, 1, 12, 25, 10434 }, // ₥
|
||||
{ 10, 12, 10, 0, 12, 15, 10459 }, // ₦
|
||||
{ 12, 13, 13, 1, 12, 20, 10474 }, // ₧
|
||||
{ 13, 13, 14, 1, 12, 22, 10494 }, // ₨
|
||||
{ 11, 12, 11, 0, 12, 17, 10516 }, // ₩
|
||||
{ 11, 12, 13, 1, 12, 17, 10533 }, // ₪
|
||||
{ 11, 16, 10, 0, 13, 22, 10550 }, // ₫
|
||||
{ 10, 14, 10, 0, 13, 18, 10572 }, // €
|
||||
{ 10, 12, 10, 0, 12, 15, 10590 }, // ₭
|
||||
{ 10, 12, 10, 0, 12, 15, 10605 }, // ₮
|
||||
{ 16, 17, 16, 0, 13, 34, 10620 }, // ₯
|
||||
{ 9, 17, 10, 0, 13, 20, 10654 }, // ₰
|
||||
{ 10, 12, 10, 0, 12, 15, 10674 }, // ₱
|
||||
{ 10, 15, 12, 1, 13, 19, 10689 }, // ₲
|
||||
{ 11, 12, 10, 0, 12, 17, 10708 }, // ₳
|
||||
{ 9, 14, 9, 0, 13, 16, 10725 }, // ₴
|
||||
{ 10, 15, 11, 1, 13, 19, 10741 }, // ₵
|
||||
{ 10, 12, 10, 0, 11, 15, 10760 }, // ₶
|
||||
{ 12, 15, 12, 0, 13, 23, 10775 }, // ₷
|
||||
{ 9, 12, 9, 0, 12, 14, 10798 }, // ₸
|
||||
{ 8, 12, 10, 1, 12, 12, 10812 }, // ₹
|
||||
{ 10, 13, 10, 0, 12, 17, 10824 }, // ₺
|
||||
{ 13, 14, 13, 0, 13, 23, 10841 }, // ₻
|
||||
{ 13, 12, 14, 0, 12, 20, 10864 }, // ₼
|
||||
{ 10, 12, 10, 0, 12, 15, 10884 }, // ₽
|
||||
{ 11, 13, 13, 1, 13, 18, 10899 }, // ₾
|
||||
{ 8, 16, 10, 1, 14, 16, 10917 }, // ₿
|
||||
{ 8, 13, 8, 0, 10, 13, 10933 }, // ⃀
|
||||
{ 9, 2, 10, 0, 7, 3, 10946 }, // −
|
||||
{ 6, 9, 6, 0, 15, 7, 10356 }, // ⁰
|
||||
{ 2, 9, 3, 0, 13, 3, 10363 }, // ⁱ
|
||||
{ 6, 9, 6, 0, 15, 7, 10366 }, // ⁴
|
||||
{ 6, 9, 6, 0, 15, 7, 10373 }, // ⁵
|
||||
{ 6, 9, 6, 0, 15, 7, 10380 }, // ⁶
|
||||
{ 6, 9, 6, 0, 15, 7, 10387 }, // ⁷
|
||||
{ 6, 9, 6, 0, 15, 7, 10394 }, // ⁸
|
||||
{ 6, 9, 6, 0, 15, 7, 10401 }, // ⁹
|
||||
{ 6, 6, 6, 0, 13, 5, 10408 }, // ⁺
|
||||
{ 6, 2, 6, 0, 11, 2, 10413 }, // ⁻
|
||||
{ 6, 4, 6, 0, 12, 3, 10415 }, // ⁼
|
||||
{ 3, 10, 4, 1, 16, 4, 10418 }, // ⁽
|
||||
{ 3, 10, 4, 0, 16, 4, 10422 }, // ⁾
|
||||
{ 6, 7, 7, 0, 11, 6, 10426 }, // ⁿ
|
||||
{ 6, 9, 6, 0, 6, 7, 10432 }, // ₀
|
||||
{ 4, 9, 6, 0, 6, 5, 10439 }, // ₁
|
||||
{ 6, 9, 6, 0, 6, 7, 10444 }, // ₂
|
||||
{ 6, 9, 6, 0, 6, 7, 10451 }, // ₃
|
||||
{ 6, 9, 6, 0, 6, 7, 10458 }, // ₄
|
||||
{ 6, 9, 6, 0, 6, 7, 10465 }, // ₅
|
||||
{ 6, 9, 6, 0, 6, 7, 10472 }, // ₆
|
||||
{ 6, 9, 6, 0, 6, 7, 10479 }, // ₇
|
||||
{ 6, 9, 6, 0, 6, 7, 10486 }, // ₈
|
||||
{ 6, 9, 6, 0, 6, 7, 10493 }, // ₉
|
||||
{ 6, 6, 6, 0, 4, 5, 10500 }, // ₊
|
||||
{ 6, 2, 6, 0, 2, 2, 10505 }, // ₋
|
||||
{ 6, 4, 6, 0, 3, 3, 10507 }, // ₌
|
||||
{ 3, 10, 4, 1, 7, 4, 10510 }, // ₍
|
||||
{ 3, 10, 4, 0, 7, 4, 10514 }, // ₎
|
||||
{ 6, 6, 6, 0, 4, 5, 10518 }, // ₐ
|
||||
{ 6, 6, 6, 0, 4, 5, 10523 }, // ₑ
|
||||
{ 6, 6, 7, 0, 4, 5, 10528 }, // ₒ
|
||||
{ 6, 6, 6, 0, 4, 5, 10533 }, // ₓ
|
||||
{ 6, 6, 6, 0, 4, 5, 10538 }, // ₔ
|
||||
{ 6, 8, 7, 0, 6, 6, 10543 }, // ₕ
|
||||
{ 6, 8, 6, 0, 6, 6, 10549 }, // ₖ
|
||||
{ 2, 8, 3, 0, 6, 2, 10555 }, // ₗ
|
||||
{ 10, 6, 10, 0, 4, 8, 10557 }, // ₘ
|
||||
{ 6, 6, 7, 0, 4, 5, 10565 }, // ₙ
|
||||
{ 7, 8, 7, 0, 4, 7, 10570 }, // ₚ
|
||||
{ 5, 6, 5, 0, 4, 4, 10577 }, // ₛ
|
||||
{ 4, 7, 4, 0, 5, 4, 10581 }, // ₜ
|
||||
{ 9, 13, 10, 0, 13, 15, 10585 }, // ₠
|
||||
{ 10, 14, 10, 0, 13, 18, 10600 }, // ₡
|
||||
{ 9, 14, 10, 0, 13, 16, 10618 }, // ₢
|
||||
{ 9, 12, 10, 0, 12, 14, 10634 }, // ₣
|
||||
{ 9, 13, 10, 0, 13, 15, 10648 }, // ₤
|
||||
{ 14, 14, 16, 1, 12, 25, 10663 }, // ₥
|
||||
{ 10, 12, 10, 0, 12, 15, 10688 }, // ₦
|
||||
{ 12, 13, 13, 1, 12, 20, 10703 }, // ₧
|
||||
{ 13, 13, 14, 1, 12, 22, 10723 }, // ₨
|
||||
{ 11, 12, 11, 0, 12, 17, 10745 }, // ₩
|
||||
{ 11, 12, 13, 1, 12, 17, 10762 }, // ₪
|
||||
{ 11, 16, 10, 0, 13, 22, 10779 }, // ₫
|
||||
{ 10, 14, 10, 0, 13, 18, 10801 }, // €
|
||||
{ 10, 12, 10, 0, 12, 15, 10819 }, // ₭
|
||||
{ 10, 12, 10, 0, 12, 15, 10834 }, // ₮
|
||||
{ 16, 17, 16, 0, 13, 34, 10849 }, // ₯
|
||||
{ 9, 17, 10, 0, 13, 20, 10883 }, // ₰
|
||||
{ 10, 12, 10, 0, 12, 15, 10903 }, // ₱
|
||||
{ 10, 15, 12, 1, 13, 19, 10918 }, // ₲
|
||||
{ 11, 12, 10, 0, 12, 17, 10937 }, // ₳
|
||||
{ 9, 14, 9, 0, 13, 16, 10954 }, // ₴
|
||||
{ 10, 15, 11, 1, 13, 19, 10970 }, // ₵
|
||||
{ 10, 12, 10, 0, 11, 15, 10989 }, // ₶
|
||||
{ 12, 15, 12, 0, 13, 23, 11004 }, // ₷
|
||||
{ 9, 12, 9, 0, 12, 14, 11027 }, // ₸
|
||||
{ 8, 12, 10, 1, 12, 12, 11041 }, // ₹
|
||||
{ 10, 13, 10, 0, 12, 17, 11053 }, // ₺
|
||||
{ 13, 14, 13, 0, 13, 23, 11070 }, // ₻
|
||||
{ 13, 12, 14, 0, 12, 20, 11093 }, // ₼
|
||||
{ 10, 12, 10, 0, 12, 15, 11113 }, // ₽
|
||||
{ 11, 13, 13, 1, 13, 18, 11128 }, // ₾
|
||||
{ 8, 16, 10, 1, 14, 16, 11146 }, // ₿
|
||||
{ 8, 13, 8, 0, 10, 13, 11162 }, // ⃀
|
||||
{ 9, 2, 10, 0, 7, 3, 11175 }, // −
|
||||
{ 16, 16, 17, 0, 13, 32, 11178 }, // �
|
||||
};
|
||||
|
||||
static const EpdUnicodeInterval notosans_8_regularIntervals[] = {
|
||||
@@ -1542,17 +1602,23 @@ static const EpdUnicodeInterval notosans_8_regularIntervals[] = {
|
||||
{ 0x400, 0x4FF, 0x1B1 },
|
||||
{ 0x2000, 0x2064, 0x2B1 },
|
||||
{ 0x2066, 0x206F, 0x316 },
|
||||
{ 0x20A0, 0x20C0, 0x320 },
|
||||
{ 0x2212, 0x2212, 0x341 },
|
||||
{ 0x2070, 0x2071, 0x320 },
|
||||
{ 0x2074, 0x208E, 0x322 },
|
||||
{ 0x2090, 0x209C, 0x33D },
|
||||
{ 0x20A0, 0x20C0, 0x34A },
|
||||
{ 0x2212, 0x2212, 0x36B },
|
||||
{ 0xFFFD, 0xFFFD, 0x36C },
|
||||
};
|
||||
|
||||
static const EpdFontData notosans_8_regular = {
|
||||
notosans_8_regularBitmaps,
|
||||
notosans_8_regularGlyphs,
|
||||
notosans_8_regularIntervals,
|
||||
11,
|
||||
15,
|
||||
23,
|
||||
18,
|
||||
-5,
|
||||
false,
|
||||
nullptr,
|
||||
0,
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,12 @@
|
||||
* name: ubuntu_10_bold
|
||||
* size: 10
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_10_bold 10 ../builtinFonts/source/Ubuntu/Ubuntu-Bold.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
static const uint8_t ubuntu_10_boldBitmaps[13720] = {
|
||||
static const uint8_t ubuntu_10_boldBitmaps[13863] = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xFF, 0xF0, 0xFF, 0xFF, 0xEF, 0xEF, 0xE7, 0xE7, 0x1E, 0xF0,
|
||||
0xF7, 0x87, 0x3C, 0x39, 0xCF, 0xFF, 0xFF, 0xFC, 0xF7, 0x87, 0x3C, 0x39, 0xC7, 0xFF, 0xFF, 0xFE,
|
||||
0xF7, 0x87, 0xBC, 0x39, 0xE1, 0xCE, 0x00, 0x0F, 0x01, 0xE0, 0x7F, 0x9F, 0xF7, 0xFC, 0xF3, 0x9E,
|
||||
@@ -843,29 +844,38 @@ static const uint8_t ubuntu_10_boldBitmaps[13720] = {
|
||||
0xDF, 0xEE, 0x1E, 0x3F, 0x9F, 0xC3, 0xC3, 0xE1, 0xF0, 0x00, 0x38, 0xF3, 0xE7, 0x9F, 0x1E, 0x3E,
|
||||
0x3C, 0x38, 0x00, 0x00, 0xE1, 0xE3, 0xC3, 0xC7, 0xCF, 0x3C, 0x78, 0xE0, 0x00, 0x00, 0xE0, 0x3C,
|
||||
0x0F, 0x01, 0xE0, 0x78, 0x0F, 0x03, 0xC0, 0x78, 0x1E, 0x03, 0xC0, 0xF0, 0x1E, 0x07, 0x80, 0xE0,
|
||||
0x3C, 0x00, 0x07, 0xF0, 0xFF, 0x1F, 0xE1, 0xF2, 0x3C, 0x07, 0xFE, 0x7F, 0xC3, 0xC0, 0x7F, 0xC7,
|
||||
0xFC, 0x3C, 0x01, 0xF3, 0x1F, 0xF0, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0x0F,
|
||||
0xC0, 0xFC, 0x0F, 0xC3, 0xF0, 0x3F, 0xC3, 0xFC, 0x1F, 0xC3, 0xF0, 0x3F, 0x03, 0xF0, 0x0F, 0x00,
|
||||
0x3F, 0x87, 0xFC, 0x3F, 0xC3, 0x3E, 0xFF, 0xFF, 0xFF, 0x1F, 0x83, 0xF0, 0xFF, 0xFF, 0xFF, 0x78,
|
||||
0x07, 0xCC, 0x7F, 0xE3, 0xFE, 0x1F, 0xC0, 0x7F, 0xFB, 0xFF, 0xC1, 0xF0, 0x03, 0x87, 0xFF, 0x3F,
|
||||
0xF8, 0x0F, 0x01, 0xF8, 0x7F, 0x83, 0xF8, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1E, 0x00,
|
||||
0x3F, 0x83, 0xFC, 0x3F, 0xE3, 0x3F, 0x00, 0xF1, 0xFF, 0x3F, 0xF7, 0xFF, 0x7D, 0xFF, 0x0F, 0xF0,
|
||||
0xF7, 0xBE, 0x7F, 0xE7, 0xFC, 0x1F, 0x80, 0x07, 0x80, 0x0F, 0x80, 0x3F, 0x00, 0x7F, 0x00, 0xFE,
|
||||
0x03, 0xDC, 0x07, 0xBC, 0x1E, 0x78, 0x3C, 0x78, 0x78, 0xF1, 0xE1, 0xE3, 0xC1, 0xE7, 0xFF, 0xDF,
|
||||
0xFF, 0xBF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE,
|
||||
0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x78,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xBC, 0x03, 0xC0, 0x7C, 0x07, 0x80, 0x78, 0x0F, 0x01, 0xE0, 0x78, 0x1E,
|
||||
0x07, 0xC0, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0xE0, 0x3C, 0x0F,
|
||||
0x01, 0xE0, 0x78, 0x0F, 0x03, 0xC0, 0x78, 0x1E, 0x03, 0xC0, 0xF0, 0x1E, 0x07, 0x80, 0xE0, 0x3C,
|
||||
0x00, 0x77, 0xFE, 0xE0, 0x00, 0x3C, 0x00, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x03, 0xC0, 0x0F,
|
||||
0x0E, 0x38, 0xFD, 0xE1, 0xF7, 0x87, 0xFC, 0x03, 0xF0, 0x0F, 0xC0, 0x1E, 0x00, 0x78, 0x00, 0x3D,
|
||||
0xF3, 0xFF, 0xFF, 0xEF, 0xCE, 0x7F, 0xFB, 0xBF, 0xFC, 0xF7, 0xC0, 0x00, 0x07, 0xC7, 0xE3, 0xF3,
|
||||
0xE1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x7E, 0x3E,
|
||||
0x1F, 0x00, 0x00, 0x3C, 0x77, 0xEF, 0x7F, 0xEF, 0xFE, 0xE3, 0xC0, 0x00, 0x3C, 0x77, 0xFF, 0x7F,
|
||||
0xEF, 0xFE, 0xE3, 0xC0, 0x01, 0x80, 0xE0, 0x3B, 0xFF, 0xFF, 0xFF, 0xF0, 0xE0, 0x70, 0xFF, 0xFF,
|
||||
0xFF, 0xFD, 0xC0, 0x70, 0x18, 0x00, 0x03, 0xCF, 0xFF, 0xFF, 0xFE, 0xF8, 0x3F, 0xEF, 0xFC, 0xFF,
|
||||
0x03, 0xC0, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0, 0xF0, 0x3F, 0xCF, 0xFD, 0xFF, 0x07, 0xDF, 0xFF, 0xFF,
|
||||
0xFC, 0xF0, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0,
|
||||
0x3C, 0x00, 0x7C, 0x7E, 0xFE, 0xE7, 0xE7, 0xE7, 0xFE, 0x7E, 0x7C, 0x1E, 0x3E, 0x7E, 0x6E, 0xFF,
|
||||
0xFF, 0x0E, 0x0E, 0x7E, 0xFD, 0xC3, 0xE7, 0xE1, 0xD3, 0xFF, 0xFC, 0x1E, 0x3E, 0x78, 0x7E, 0xFE,
|
||||
0xEE, 0xFE, 0x7E, 0x3C, 0xFE, 0xFE, 0x0E, 0x1C, 0x1C, 0x38, 0x38, 0x38, 0x7C, 0x7E, 0xEE, 0x7E,
|
||||
0x7E, 0xFE, 0xEF, 0xFE, 0x7E, 0x7C, 0x7E, 0xEE, 0xEE, 0x7E, 0x7E, 0x1E, 0x7C, 0x78, 0x7C, 0x7E,
|
||||
0xFE, 0xE7, 0xE7, 0xE7, 0xFE, 0x7E, 0x7C, 0x1D, 0xF7, 0xDF, 0x1C, 0x71, 0xC7, 0x1C, 0x7D, 0xFD,
|
||||
0xB8, 0xF3, 0xCF, 0x1C, 0x7F, 0xFE, 0xFD, 0xFD, 0x79, 0xE3, 0xE1, 0xD3, 0xFF, 0xFC, 0x1E, 0x3E,
|
||||
0x7E, 0x6E, 0xFF, 0xFF, 0x0E, 0x0E, 0x7E, 0xFD, 0xC3, 0xE7, 0xE1, 0xD3, 0xFF, 0xFC, 0x1E, 0x3E,
|
||||
0x78, 0x7E, 0xFE, 0xEE, 0xFE, 0x7E, 0x3C, 0xFE, 0xFE, 0x0E, 0x1C, 0x1C, 0x38, 0x38, 0x38, 0x7C,
|
||||
0x7E, 0xEE, 0x7E, 0x7E, 0xFE, 0xEF, 0xFE, 0x7E, 0x7C, 0x7E, 0xEE, 0xEE, 0x7E, 0x7E, 0x1E, 0x7C,
|
||||
0x78, 0x07, 0xF0, 0xFF, 0x1F, 0xE1, 0xF2, 0x3C, 0x07, 0xFE, 0x7F, 0xC3, 0xC0, 0x7F, 0xC7, 0xFC,
|
||||
0x3C, 0x01, 0xF3, 0x1F, 0xF0, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0x0F, 0xC0,
|
||||
0xFC, 0x0F, 0xC3, 0xF0, 0x3F, 0xC3, 0xFC, 0x1F, 0xC3, 0xF0, 0x3F, 0x03, 0xF0, 0x0F, 0x00, 0x3F,
|
||||
0x87, 0xFC, 0x3F, 0xC3, 0x3E, 0xFF, 0xFF, 0xFF, 0x1F, 0x83, 0xF0, 0xFF, 0xFF, 0xFF, 0x78, 0x07,
|
||||
0xCC, 0x7F, 0xE3, 0xFE, 0x1F, 0xC0, 0x7F, 0xFB, 0xFF, 0xC1, 0xF0, 0x03, 0x87, 0xFF, 0x3F, 0xF8,
|
||||
0x0F, 0x01, 0xF8, 0x7F, 0x83, 0xF8, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1E, 0x00, 0x3F,
|
||||
0x83, 0xFC, 0x3F, 0xE3, 0x3F, 0x00, 0xF1, 0xFF, 0x3F, 0xF7, 0xFF, 0x7D, 0xFF, 0x0F, 0xF0, 0xF7,
|
||||
0xBE, 0x7F, 0xE7, 0xFC, 0x1F, 0x80, 0x07, 0x80, 0x0F, 0x80, 0x3F, 0x00, 0x7F, 0x00, 0xFE, 0x03,
|
||||
0xDC, 0x07, 0xBC, 0x1E, 0x78, 0x3C, 0x78, 0x78, 0xF1, 0xE1, 0xE3, 0xC1, 0xE7, 0xFF, 0xDF, 0xFF,
|
||||
0xBF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F,
|
||||
0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x78, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xBC, 0x03, 0xC0, 0x7C, 0x07, 0x80, 0x78, 0x0F, 0x01, 0xE0, 0x78, 0x1E, 0x07,
|
||||
0xC0, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0xE0, 0x3C, 0x0F, 0x01,
|
||||
0xE0, 0x78, 0x0F, 0x03, 0xC0, 0x78, 0x1E, 0x03, 0xC0, 0xF0, 0x1E, 0x07, 0x80, 0xE0, 0x3C, 0x00,
|
||||
0x77, 0xFE, 0xE0, 0x00, 0x3C, 0x00, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x03, 0xC0, 0x0F, 0x0E,
|
||||
0x38, 0xFD, 0xE1, 0xF7, 0x87, 0xFC, 0x03, 0xF0, 0x0F, 0xC0, 0x1E, 0x00, 0x78, 0x00, 0x3D, 0xF3,
|
||||
0xFF, 0xFF, 0xEF, 0xCE, 0x7F, 0xFB, 0xBF, 0xFC, 0xF7, 0xC0, 0x00, 0x07, 0xC7, 0xE3, 0xF3, 0xE1,
|
||||
0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x7E, 0x3E, 0x1F,
|
||||
0x00, 0x00, 0x3C, 0x77, 0xEF, 0x7F, 0xEF, 0xFE, 0xE3, 0xC0, 0x00, 0x3C, 0x77, 0xFF, 0x7F, 0xEF,
|
||||
0xFE, 0xE3, 0xC0, 0x01, 0x80, 0xE0, 0x3B, 0xFF, 0xFF, 0xFF, 0xF0, 0xE0, 0x70, 0xFF, 0xFF, 0xFF,
|
||||
0xFD, 0xC0, 0x70, 0x18, 0x00, 0x03, 0xCF, 0xFF, 0xFF, 0xFE, 0xF8, 0x3F, 0xEF, 0xFC, 0xFF, 0x03,
|
||||
0xC0, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0, 0xF0, 0x3F, 0xCF, 0xFD, 0xFF, 0x07, 0xDF, 0xFF, 0xFF, 0xFC,
|
||||
0xF0, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0,
|
||||
};
|
||||
|
||||
static const EpdGlyph ubuntu_10_boldGlyphs[] = {
|
||||
@@ -1425,24 +1435,41 @@ static const EpdGlyph ubuntu_10_boldGlyphs[] = {
|
||||
{ 7, 11, 7, 0, 11, 10, 13321 }, // ‹
|
||||
{ 7, 11, 7, 0, 11, 10, 13331 }, // ›
|
||||
{ 11, 15, 3, -4, 15, 21, 13341 }, // ⁄
|
||||
{ 12, 15, 12, 0, 15, 23, 13362 }, // €
|
||||
{ 12, 15, 12, 0, 15, 23, 13385 }, // ₮
|
||||
{ 12, 15, 12, 0, 15, 23, 13408 }, // ₴
|
||||
{ 13, 15, 12, 0, 15, 25, 13431 }, // ₹
|
||||
{ 12, 15, 13, 0, 15, 23, 13456 }, // ∂
|
||||
{ 15, 15, 15, 0, 15, 29, 13479 }, // ∆
|
||||
{ 13, 17, 15, 1, 15, 28, 13508 }, // ∏
|
||||
{ 11, 17, 11, 0, 15, 24, 13536 }, // ∑
|
||||
{ 10, 3, 12, 1, 8, 4, 13560 }, // −
|
||||
{ 11, 15, 3, -4, 15, 21, 13564 }, // ∕
|
||||
{ 5, 4, 5, 0, 9, 3, 13585 }, // ∙
|
||||
{ 14, 15, 13, 0, 15, 27, 13588 }, // √
|
||||
{ 13, 7, 13, 0, 10, 12, 13615 }, // ∞
|
||||
{ 9, 21, 8, 0, 16, 24, 13627 }, // ∫
|
||||
{ 12, 11, 12, 0, 12, 17, 13651 }, // ≈
|
||||
{ 10, 14, 12, 1, 14, 18, 13668 }, // ≠
|
||||
{ 10, 13, 12, 1, 13, 17, 13686 }, // ≤
|
||||
{ 10, 13, 12, 1, 13, 17, 13703 }, // ≥
|
||||
{ 8, 9, 8, 0, 15, 9, 13362 }, // ⁰
|
||||
{ 8, 8, 8, 0, 15, 8, 13371 }, // ⁴
|
||||
{ 7, 9, 8, 0, 15, 8, 13379 }, // ⁵
|
||||
{ 8, 9, 8, 0, 15, 9, 13387 }, // ⁶
|
||||
{ 8, 8, 8, 0, 15, 8, 13396 }, // ⁷
|
||||
{ 8, 9, 8, 0, 15, 9, 13404 }, // ⁸
|
||||
{ 8, 9, 8, 0, 15, 9, 13413 }, // ⁹
|
||||
{ 8, 9, 8, 0, 9, 9, 13422 }, // ₀
|
||||
{ 6, 9, 8, 0, 9, 7, 13431 }, // ₁
|
||||
{ 7, 9, 8, 0, 9, 8, 13438 }, // ₂
|
||||
{ 7, 9, 8, 0, 9, 8, 13446 }, // ₃
|
||||
{ 8, 8, 8, 0, 9, 8, 13454 }, // ₄
|
||||
{ 7, 9, 8, 0, 9, 8, 13462 }, // ₅
|
||||
{ 8, 9, 8, 0, 9, 9, 13470 }, // ₆
|
||||
{ 8, 8, 8, 0, 9, 8, 13479 }, // ₇
|
||||
{ 8, 9, 8, 0, 9, 9, 13487 }, // ₈
|
||||
{ 8, 9, 8, 0, 9, 9, 13496 }, // ₉
|
||||
{ 12, 15, 12, 0, 15, 23, 13505 }, // €
|
||||
{ 12, 15, 12, 0, 15, 23, 13528 }, // ₮
|
||||
{ 12, 15, 12, 0, 15, 23, 13551 }, // ₴
|
||||
{ 13, 15, 12, 0, 15, 25, 13574 }, // ₹
|
||||
{ 12, 15, 13, 0, 15, 23, 13599 }, // ∂
|
||||
{ 15, 15, 15, 0, 15, 29, 13622 }, // ∆
|
||||
{ 13, 17, 15, 1, 15, 28, 13651 }, // ∏
|
||||
{ 11, 17, 11, 0, 15, 24, 13679 }, // ∑
|
||||
{ 10, 3, 12, 1, 8, 4, 13703 }, // −
|
||||
{ 11, 15, 3, -4, 15, 21, 13707 }, // ∕
|
||||
{ 5, 4, 5, 0, 9, 3, 13728 }, // ∙
|
||||
{ 14, 15, 13, 0, 15, 27, 13731 }, // √
|
||||
{ 13, 7, 13, 0, 10, 12, 13758 }, // ∞
|
||||
{ 9, 21, 8, 0, 16, 24, 13770 }, // ∫
|
||||
{ 12, 11, 12, 0, 12, 17, 13794 }, // ≈
|
||||
{ 10, 14, 12, 1, 14, 18, 13811 }, // ≠
|
||||
{ 10, 13, 12, 1, 13, 17, 13829 }, // ≤
|
||||
{ 10, 13, 12, 1, 13, 17, 13846 }, // ≥
|
||||
};
|
||||
|
||||
static const EpdUnicodeInterval ubuntu_10_boldIntervals[] = {
|
||||
@@ -1466,30 +1493,35 @@ static const EpdUnicodeInterval ubuntu_10_boldIntervals[] = {
|
||||
{ 0x2030, 0x2030, 0x228 },
|
||||
{ 0x2039, 0x203A, 0x229 },
|
||||
{ 0x2044, 0x2044, 0x22B },
|
||||
{ 0x20AC, 0x20AC, 0x22C },
|
||||
{ 0x20AE, 0x20AE, 0x22D },
|
||||
{ 0x20B4, 0x20B4, 0x22E },
|
||||
{ 0x20B9, 0x20B9, 0x22F },
|
||||
{ 0x2202, 0x2202, 0x230 },
|
||||
{ 0x2206, 0x2206, 0x231 },
|
||||
{ 0x220F, 0x220F, 0x232 },
|
||||
{ 0x2211, 0x2212, 0x233 },
|
||||
{ 0x2215, 0x2215, 0x235 },
|
||||
{ 0x2219, 0x221A, 0x236 },
|
||||
{ 0x221E, 0x221E, 0x238 },
|
||||
{ 0x222B, 0x222B, 0x239 },
|
||||
{ 0x2248, 0x2248, 0x23A },
|
||||
{ 0x2260, 0x2260, 0x23B },
|
||||
{ 0x2264, 0x2265, 0x23C },
|
||||
{ 0x2070, 0x2070, 0x22C },
|
||||
{ 0x2074, 0x2079, 0x22D },
|
||||
{ 0x2080, 0x2089, 0x233 },
|
||||
{ 0x20AC, 0x20AC, 0x23D },
|
||||
{ 0x20AE, 0x20AE, 0x23E },
|
||||
{ 0x20B4, 0x20B4, 0x23F },
|
||||
{ 0x20B9, 0x20B9, 0x240 },
|
||||
{ 0x2202, 0x2202, 0x241 },
|
||||
{ 0x2206, 0x2206, 0x242 },
|
||||
{ 0x220F, 0x220F, 0x243 },
|
||||
{ 0x2211, 0x2212, 0x244 },
|
||||
{ 0x2215, 0x2215, 0x246 },
|
||||
{ 0x2219, 0x221A, 0x247 },
|
||||
{ 0x221E, 0x221E, 0x249 },
|
||||
{ 0x222B, 0x222B, 0x24A },
|
||||
{ 0x2248, 0x2248, 0x24B },
|
||||
{ 0x2260, 0x2260, 0x24C },
|
||||
{ 0x2264, 0x2265, 0x24D },
|
||||
};
|
||||
|
||||
static const EpdFontData ubuntu_10_bold = {
|
||||
ubuntu_10_boldBitmaps,
|
||||
ubuntu_10_boldGlyphs,
|
||||
ubuntu_10_boldIntervals,
|
||||
35,
|
||||
38,
|
||||
24,
|
||||
20,
|
||||
-4,
|
||||
false,
|
||||
nullptr,
|
||||
0,
|
||||
};
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
* name: ubuntu_10_regular
|
||||
* size: 10
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_10_regular 10 ../builtinFonts/source/Ubuntu/Ubuntu-Regular.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
static const uint8_t ubuntu_10_regularBitmaps[12366] = {
|
||||
static const uint8_t ubuntu_10_regularBitmaps[12484] = {
|
||||
0xEE, 0xEE, 0xE6, 0x66, 0x66, 0x00, 0xEF, 0xE0, 0xEF, 0xDF, 0xBF, 0x6E, 0xC0, 0x1C, 0x61, 0xCE,
|
||||
0x1C, 0xE1, 0x8E, 0xFF, 0xFF, 0xFF, 0x38, 0xC3, 0x9C, 0x31, 0xCF, 0xFF, 0xFF, 0xF7, 0x18, 0x73,
|
||||
0x87, 0x38, 0x63, 0x80, 0x0C, 0x03, 0x03, 0xF9, 0xFE, 0xF1, 0xB8, 0x0E, 0x01, 0xE0, 0x7E, 0x07,
|
||||
@@ -760,27 +761,35 @@ static const uint8_t ubuntu_10_regularBitmaps[12366] = {
|
||||
0x77, 0x77, 0x07, 0x63, 0x63, 0x0E, 0x63, 0x63, 0x0E, 0x63, 0x63, 0x1C, 0x77, 0x77, 0x1C, 0x3E,
|
||||
0x3E, 0x18, 0xE3, 0x9C, 0xE1, 0xC3, 0x8E, 0x18, 0x61, 0xC3, 0x0E, 0x1C, 0xE3, 0x1C, 0x60, 0x00,
|
||||
0xE0, 0x38, 0x07, 0x01, 0xC0, 0x30, 0x0E, 0x01, 0x80, 0x70, 0x0C, 0x03, 0x80, 0x60, 0x1C, 0x07,
|
||||
0x00, 0xE0, 0x38, 0x00, 0x07, 0xE0, 0xFE, 0x1E, 0x21, 0xC0, 0x7F, 0xC7, 0xFC, 0x38, 0x07, 0xF8,
|
||||
0x7F, 0x81, 0x80, 0x1C, 0x01, 0xF2, 0x0F, 0xF0, 0x7F, 0xFF, 0xEF, 0xFE, 0x06, 0x00, 0x60, 0x07,
|
||||
0xC0, 0x7C, 0x1F, 0x83, 0xE0, 0x37, 0xC0, 0x7C, 0x1F, 0x83, 0xE0, 0x36, 0x00, 0x60, 0x06, 0x00,
|
||||
0x3F, 0x83, 0xFC, 0x33, 0xC0, 0x0C, 0x00, 0xCF, 0xFE, 0x07, 0x81, 0xE0, 0xFF, 0xE7, 0x00, 0x70,
|
||||
0x07, 0x00, 0x3C, 0xC3, 0xFE, 0x1F, 0xC0, 0xFF, 0xFF, 0xFC, 0x1C, 0x01, 0xCF, 0xFD, 0xFF, 0x80,
|
||||
0xE0, 0x78, 0xFF, 0x1F, 0x83, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x3F, 0x03, 0xFC, 0x33,
|
||||
0xC0, 0x0E, 0x00, 0xE1, 0xFE, 0x3F, 0xE7, 0x9F, 0x70, 0x76, 0x06, 0x60, 0x66, 0x0E, 0x60, 0xE7,
|
||||
0xBC, 0x3F, 0x81, 0xF0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0xF0, 0x0D, 0xC0, 0x73, 0x81, 0x8E,
|
||||
0x0E, 0x18, 0x38, 0x70, 0xC1, 0xC7, 0x03, 0x1C, 0x0E, 0x60, 0x3B, 0xFF, 0xEF, 0xFF, 0xC0, 0xFF,
|
||||
0xFF, 0xFF, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E,
|
||||
0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x70, 0xFF, 0xFF, 0xFD, 0xC0, 0x38, 0x03, 0x80,
|
||||
0x38, 0x07, 0x00, 0x70, 0x07, 0x01, 0xC0, 0x70, 0x1E, 0x03, 0x80, 0xE0, 0x38, 0x07, 0xFF, 0xFF,
|
||||
0xE0, 0xFF, 0xFF, 0xF0, 0x00, 0xE0, 0x38, 0x07, 0x01, 0xC0, 0x30, 0x0E, 0x01, 0x80, 0x70, 0x0C,
|
||||
0x03, 0x80, 0x60, 0x1C, 0x07, 0x00, 0xE0, 0x38, 0x00, 0xFF, 0x80, 0x00, 0x30, 0x01, 0x80, 0x1C,
|
||||
0x00, 0xC0, 0x06, 0x00, 0x70, 0x03, 0x80, 0x18, 0x70, 0xC7, 0xCE, 0x3E, 0x60, 0x3B, 0x01, 0xF8,
|
||||
0x07, 0x80, 0x3C, 0x00, 0xE0, 0x06, 0x00, 0x7D, 0xE7, 0xFF, 0xE7, 0x3E, 0x73, 0x7F, 0xF7, 0xDE,
|
||||
0x0F, 0x1F, 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x38,
|
||||
0x38, 0x78, 0xF8, 0xF0, 0x3C, 0x6F, 0xFD, 0xBF, 0xB1, 0xE3, 0xC6, 0xFF, 0xDB, 0xFB, 0x1E, 0x03,
|
||||
0x00, 0xC0, 0x73, 0xFF, 0xFF, 0xC3, 0x80, 0xC0, 0x70, 0xFF, 0xFF, 0xF3, 0x01, 0xC0, 0x60, 0x00,
|
||||
0x01, 0xC3, 0xF7, 0xFB, 0xF0, 0xF0, 0x3F, 0x07, 0xF8, 0x3F, 0x01, 0xC0, 0x0F, 0xFF, 0xFF, 0xE0,
|
||||
0x3F, 0x07, 0xF8, 0x3F, 0x03, 0xC3, 0xF7, 0xFB, 0xF0, 0xE0, 0x00, 0x0F, 0xFF, 0xFF,
|
||||
0x00, 0xE0, 0x38, 0x00, 0x7C, 0xDF, 0x9F, 0x3E, 0x7C, 0xDB, 0xBE, 0x0C, 0x1C, 0x3C, 0x7C, 0x6C,
|
||||
0xFE, 0x0C, 0x0C, 0x7E, 0xC1, 0x83, 0xE1, 0xE0, 0xD3, 0xBE, 0x1E, 0x71, 0x83, 0xEE, 0xEC, 0xDB,
|
||||
0x9E, 0xFE, 0x1C, 0x70, 0xC1, 0x87, 0x0C, 0x18, 0x7C, 0xDD, 0x9B, 0xE7, 0xFC, 0xFB, 0xBE, 0x7C,
|
||||
0xDF, 0x9F, 0x37, 0xE1, 0xCF, 0x3C, 0x7C, 0xDF, 0x9F, 0x3E, 0x7C, 0xDB, 0xBE, 0x3F, 0xF3, 0x33,
|
||||
0x33, 0x7C, 0xD8, 0x38, 0xE3, 0xCF, 0x18, 0x3F, 0x7C, 0xD8, 0x31, 0xE0, 0xE0, 0xD3, 0xFE, 0x0C,
|
||||
0x1C, 0x3C, 0x7C, 0x6C, 0xFE, 0x0C, 0x0C, 0x7E, 0xC1, 0x83, 0xE1, 0xE0, 0xD3, 0xBE, 0x1E, 0x71,
|
||||
0x83, 0xEE, 0xEC, 0xDB, 0x9E, 0xFE, 0x1C, 0x70, 0xC1, 0x87, 0x0C, 0x18, 0x7C, 0xDD, 0x9B, 0xE7,
|
||||
0xFC, 0xFB, 0xBE, 0x7C, 0xDF, 0x9F, 0x37, 0xE1, 0xCF, 0x3C, 0x07, 0xE0, 0xFE, 0x1E, 0x21, 0xC0,
|
||||
0x7F, 0xC7, 0xFC, 0x38, 0x07, 0xF8, 0x7F, 0x81, 0x80, 0x1C, 0x01, 0xF2, 0x0F, 0xF0, 0x7F, 0xFF,
|
||||
0xEF, 0xFE, 0x06, 0x00, 0x60, 0x07, 0xC0, 0x7C, 0x1F, 0x83, 0xE0, 0x37, 0xC0, 0x7C, 0x1F, 0x83,
|
||||
0xE0, 0x36, 0x00, 0x60, 0x06, 0x00, 0x3F, 0x83, 0xFC, 0x33, 0xC0, 0x0C, 0x00, 0xCF, 0xFE, 0x07,
|
||||
0x81, 0xE0, 0xFF, 0xE7, 0x00, 0x70, 0x07, 0x00, 0x3C, 0xC3, 0xFE, 0x1F, 0xC0, 0xFF, 0xFF, 0xFC,
|
||||
0x1C, 0x01, 0xCF, 0xFD, 0xFF, 0x80, 0xE0, 0x78, 0xFF, 0x1F, 0x83, 0xC0, 0x1C, 0x01, 0xC0, 0x1C,
|
||||
0x01, 0xC0, 0x3F, 0x03, 0xFC, 0x33, 0xC0, 0x0E, 0x00, 0xE1, 0xFE, 0x3F, 0xE7, 0x9F, 0x70, 0x76,
|
||||
0x06, 0x60, 0x66, 0x0E, 0x60, 0xE7, 0xBC, 0x3F, 0x81, 0xF0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03,
|
||||
0xF0, 0x0D, 0xC0, 0x73, 0x81, 0x8E, 0x0E, 0x18, 0x38, 0x70, 0xC1, 0xC7, 0x03, 0x1C, 0x0E, 0x60,
|
||||
0x3B, 0xFF, 0xEF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E,
|
||||
0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x70, 0xFF,
|
||||
0xFF, 0xFD, 0xC0, 0x38, 0x03, 0x80, 0x38, 0x07, 0x00, 0x70, 0x07, 0x01, 0xC0, 0x70, 0x1E, 0x03,
|
||||
0x80, 0xE0, 0x38, 0x07, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xF0, 0x00, 0xE0, 0x38, 0x07, 0x01, 0xC0,
|
||||
0x30, 0x0E, 0x01, 0x80, 0x70, 0x0C, 0x03, 0x80, 0x60, 0x1C, 0x07, 0x00, 0xE0, 0x38, 0x00, 0xFF,
|
||||
0x80, 0x00, 0x30, 0x01, 0x80, 0x1C, 0x00, 0xC0, 0x06, 0x00, 0x70, 0x03, 0x80, 0x18, 0x70, 0xC7,
|
||||
0xCE, 0x3E, 0x60, 0x3B, 0x01, 0xF8, 0x07, 0x80, 0x3C, 0x00, 0xE0, 0x06, 0x00, 0x7D, 0xE7, 0xFF,
|
||||
0xE7, 0x3E, 0x73, 0x7F, 0xF7, 0xDE, 0x0F, 0x1F, 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x38, 0x38, 0x78, 0xF8, 0xF0, 0x3C, 0x6F, 0xFD, 0xBF, 0xB1, 0xE3,
|
||||
0xC6, 0xFF, 0xDB, 0xFB, 0x1E, 0x03, 0x00, 0xC0, 0x73, 0xFF, 0xFF, 0xC3, 0x80, 0xC0, 0x70, 0xFF,
|
||||
0xFF, 0xF3, 0x01, 0xC0, 0x60, 0x00, 0x01, 0xC3, 0xF7, 0xFB, 0xF0, 0xF0, 0x3F, 0x07, 0xF8, 0x3F,
|
||||
0x01, 0xC0, 0x0F, 0xFF, 0xFF, 0xE0, 0x3F, 0x07, 0xF8, 0x3F, 0x03, 0xC3, 0xF7, 0xFB, 0xF0, 0xE0,
|
||||
0x00, 0x0F, 0xFF, 0xFF,
|
||||
};
|
||||
|
||||
static const EpdGlyph ubuntu_10_regularGlyphs[] = {
|
||||
@@ -1340,24 +1349,41 @@ static const EpdGlyph ubuntu_10_regularGlyphs[] = {
|
||||
{ 6, 9, 6, 0, 10, 7, 12001 }, // ‹
|
||||
{ 6, 9, 6, 0, 10, 7, 12008 }, // ›
|
||||
{ 11, 15, 3, -4, 15, 21, 12015 }, // ⁄
|
||||
{ 12, 14, 12, 0, 14, 21, 12036 }, // €
|
||||
{ 12, 15, 12, 0, 15, 23, 12057 }, // ₮
|
||||
{ 12, 15, 12, 0, 15, 23, 12080 }, // ₴
|
||||
{ 11, 15, 12, 1, 15, 21, 12103 }, // ₹
|
||||
{ 12, 16, 12, 0, 16, 24, 12124 }, // ∂
|
||||
{ 14, 15, 14, 0, 15, 27, 12148 }, // ∆
|
||||
{ 12, 17, 15, 1, 15, 26, 12175 }, // ∏
|
||||
{ 11, 17, 11, 0, 15, 24, 12201 }, // ∑
|
||||
{ 10, 2, 12, 1, 7, 3, 12225 }, // −
|
||||
{ 11, 15, 3, -4, 15, 21, 12228 }, // ∕
|
||||
{ 3, 3, 5, 1, 8, 2, 12249 }, // ∙
|
||||
{ 13, 17, 12, 0, 16, 28, 12251 }, // √
|
||||
{ 12, 6, 12, 0, 9, 9, 12279 }, // ∞
|
||||
{ 8, 20, 8, 0, 16, 20, 12288 }, // ∫
|
||||
{ 11, 8, 12, 0, 10, 11, 12308 }, // ≈
|
||||
{ 10, 13, 12, 1, 13, 17, 12319 }, // ≠
|
||||
{ 10, 12, 12, 1, 12, 15, 12336 }, // ≤
|
||||
{ 10, 12, 12, 1, 12, 15, 12351 }, // ≥
|
||||
{ 7, 8, 8, 0, 15, 7, 12036 }, // ⁰
|
||||
{ 8, 8, 8, 0, 15, 8, 12043 }, // ⁴
|
||||
{ 7, 8, 8, 0, 15, 7, 12051 }, // ⁵
|
||||
{ 7, 8, 8, 0, 15, 7, 12058 }, // ⁶
|
||||
{ 7, 8, 8, 0, 15, 7, 12065 }, // ⁷
|
||||
{ 7, 8, 8, 0, 15, 7, 12072 }, // ⁸
|
||||
{ 7, 8, 8, 0, 15, 7, 12079 }, // ⁹
|
||||
{ 7, 8, 8, 0, 8, 7, 12086 }, // ₀
|
||||
{ 4, 8, 8, 1, 8, 4, 12093 }, // ₁
|
||||
{ 7, 8, 8, 0, 8, 7, 12097 }, // ₂
|
||||
{ 7, 8, 8, 0, 8, 7, 12104 }, // ₃
|
||||
{ 8, 8, 8, 0, 8, 8, 12111 }, // ₄
|
||||
{ 7, 8, 8, 0, 8, 7, 12119 }, // ₅
|
||||
{ 7, 8, 8, 0, 8, 7, 12126 }, // ₆
|
||||
{ 7, 8, 8, 0, 8, 7, 12133 }, // ₇
|
||||
{ 7, 8, 8, 0, 8, 7, 12140 }, // ₈
|
||||
{ 7, 8, 8, 0, 8, 7, 12147 }, // ₉
|
||||
{ 12, 14, 12, 0, 14, 21, 12154 }, // €
|
||||
{ 12, 15, 12, 0, 15, 23, 12175 }, // ₮
|
||||
{ 12, 15, 12, 0, 15, 23, 12198 }, // ₴
|
||||
{ 11, 15, 12, 1, 15, 21, 12221 }, // ₹
|
||||
{ 12, 16, 12, 0, 16, 24, 12242 }, // ∂
|
||||
{ 14, 15, 14, 0, 15, 27, 12266 }, // ∆
|
||||
{ 12, 17, 15, 1, 15, 26, 12293 }, // ∏
|
||||
{ 11, 17, 11, 0, 15, 24, 12319 }, // ∑
|
||||
{ 10, 2, 12, 1, 7, 3, 12343 }, // −
|
||||
{ 11, 15, 3, -4, 15, 21, 12346 }, // ∕
|
||||
{ 3, 3, 5, 1, 8, 2, 12367 }, // ∙
|
||||
{ 13, 17, 12, 0, 16, 28, 12369 }, // √
|
||||
{ 12, 6, 12, 0, 9, 9, 12397 }, // ∞
|
||||
{ 8, 20, 8, 0, 16, 20, 12406 }, // ∫
|
||||
{ 11, 8, 12, 0, 10, 11, 12426 }, // ≈
|
||||
{ 10, 13, 12, 1, 13, 17, 12437 }, // ≠
|
||||
{ 10, 12, 12, 1, 12, 15, 12454 }, // ≤
|
||||
{ 10, 12, 12, 1, 12, 15, 12469 }, // ≥
|
||||
};
|
||||
|
||||
static const EpdUnicodeInterval ubuntu_10_regularIntervals[] = {
|
||||
@@ -1381,30 +1407,35 @@ static const EpdUnicodeInterval ubuntu_10_regularIntervals[] = {
|
||||
{ 0x2030, 0x2030, 0x228 },
|
||||
{ 0x2039, 0x203A, 0x229 },
|
||||
{ 0x2044, 0x2044, 0x22B },
|
||||
{ 0x20AC, 0x20AC, 0x22C },
|
||||
{ 0x20AE, 0x20AE, 0x22D },
|
||||
{ 0x20B4, 0x20B4, 0x22E },
|
||||
{ 0x20B9, 0x20B9, 0x22F },
|
||||
{ 0x2202, 0x2202, 0x230 },
|
||||
{ 0x2206, 0x2206, 0x231 },
|
||||
{ 0x220F, 0x220F, 0x232 },
|
||||
{ 0x2211, 0x2212, 0x233 },
|
||||
{ 0x2215, 0x2215, 0x235 },
|
||||
{ 0x2219, 0x221A, 0x236 },
|
||||
{ 0x221E, 0x221E, 0x238 },
|
||||
{ 0x222B, 0x222B, 0x239 },
|
||||
{ 0x2248, 0x2248, 0x23A },
|
||||
{ 0x2260, 0x2260, 0x23B },
|
||||
{ 0x2264, 0x2265, 0x23C },
|
||||
{ 0x2070, 0x2070, 0x22C },
|
||||
{ 0x2074, 0x2079, 0x22D },
|
||||
{ 0x2080, 0x2089, 0x233 },
|
||||
{ 0x20AC, 0x20AC, 0x23D },
|
||||
{ 0x20AE, 0x20AE, 0x23E },
|
||||
{ 0x20B4, 0x20B4, 0x23F },
|
||||
{ 0x20B9, 0x20B9, 0x240 },
|
||||
{ 0x2202, 0x2202, 0x241 },
|
||||
{ 0x2206, 0x2206, 0x242 },
|
||||
{ 0x220F, 0x220F, 0x243 },
|
||||
{ 0x2211, 0x2212, 0x244 },
|
||||
{ 0x2215, 0x2215, 0x246 },
|
||||
{ 0x2219, 0x221A, 0x247 },
|
||||
{ 0x221E, 0x221E, 0x249 },
|
||||
{ 0x222B, 0x222B, 0x24A },
|
||||
{ 0x2248, 0x2248, 0x24B },
|
||||
{ 0x2260, 0x2260, 0x24C },
|
||||
{ 0x2264, 0x2265, 0x24D },
|
||||
};
|
||||
|
||||
static const EpdFontData ubuntu_10_regular = {
|
||||
ubuntu_10_regularBitmaps,
|
||||
ubuntu_10_regularGlyphs,
|
||||
ubuntu_10_regularIntervals,
|
||||
35,
|
||||
38,
|
||||
24,
|
||||
20,
|
||||
-4,
|
||||
false,
|
||||
nullptr,
|
||||
0,
|
||||
};
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
* name: ubuntu_12_bold
|
||||
* size: 12
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_12_bold 12 ../builtinFonts/source/Ubuntu/Ubuntu-Bold.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
static const uint8_t ubuntu_12_boldBitmaps[18633] = {
|
||||
static const uint8_t ubuntu_12_boldBitmaps[18832] = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xDC, 0x07, 0x7F, 0xFF, 0x70, 0xF7, 0xFD, 0xFF, 0x7F, 0xDF,
|
||||
0xF3, 0xFC, 0xEF, 0x38, 0x0F, 0x3C, 0x0F, 0x3C, 0x0F, 0x3C, 0x0F, 0x3C, 0xFF, 0xFE, 0xFF, 0xFE,
|
||||
0xFF, 0xFE, 0x1E, 0x78, 0x1E, 0x78, 0x3C, 0x78, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0x3C, 0xF0,
|
||||
@@ -1142,37 +1143,49 @@ static const uint8_t ubuntu_12_boldBitmaps[18633] = {
|
||||
0x0E, 0x1F, 0x3E, 0x7C, 0x7C, 0xF8, 0x7C, 0x7C, 0x3E, 0x1F, 0x0E, 0x08, 0x10, 0x78, 0x78, 0x7C,
|
||||
0x3E, 0x3E, 0x1F, 0x3E, 0x3E, 0x7C, 0x78, 0x78, 0x10, 0x00, 0x78, 0x01, 0xE0, 0x0F, 0x80, 0x3C,
|
||||
0x01, 0xF0, 0x07, 0x80, 0x3C, 0x00, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0xF0, 0x03, 0xC0, 0x1E, 0x00,
|
||||
0x78, 0x03, 0xC0, 0x0F, 0x00, 0x78, 0x01, 0xE0, 0x00, 0x07, 0xF8, 0x3F, 0xE1, 0xFF, 0x87, 0xC4,
|
||||
0x3E, 0x00, 0xF0, 0x0F, 0xFE, 0x3F, 0xF8, 0x7C, 0x01, 0xF0, 0x0F, 0xFE, 0x3F, 0xF0, 0x3C, 0x00,
|
||||
0xFC, 0xE1, 0xFF, 0x87, 0xFE, 0x07, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x07, 0xB0,
|
||||
0x1F, 0xC0, 0x7F, 0x07, 0xF8, 0x3F, 0x80, 0xFF, 0xC1, 0x7F, 0x01, 0xFC, 0x3F, 0xC0, 0xFE, 0x01,
|
||||
0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1F, 0xE0, 0xFF, 0xC3, 0xFF, 0x06, 0x3E, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xC3, 0xF8, 0x1F, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7C, 0x01, 0xF9, 0xC3, 0xFF, 0x8F,
|
||||
0xFE, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xF8, 0x01, 0xE3, 0xFF, 0xEF, 0xFF, 0x80,
|
||||
0xF8, 0x0F, 0xE3, 0xFF, 0x0F, 0xF8, 0x3F, 0xC0, 0xFC, 0x01, 0xF8, 0x01, 0xF0, 0x03, 0xE0, 0x07,
|
||||
0xC0, 0x1F, 0xC0, 0x7F, 0xE0, 0x7F, 0xE0, 0xCF, 0xC0, 0x07, 0xC0, 0x07, 0x83, 0xFF, 0x0F, 0xFE,
|
||||
0x3F, 0xFC, 0xFC, 0xF9, 0xE0, 0xF3, 0xC1, 0xE7, 0x83, 0xCF, 0x0F, 0x9F, 0x3E, 0x3F, 0xFC, 0x3F,
|
||||
0xF0, 0x3F, 0x80, 0x03, 0xE0, 0x00, 0xF8, 0x00, 0x7F, 0x00, 0x1F, 0xC0, 0x07, 0xF8, 0x03, 0xFE,
|
||||
0x00, 0xF7, 0xC0, 0x7D, 0xF0, 0x1E, 0x3C, 0x07, 0x8F, 0x83, 0xE3, 0xE0, 0xF0, 0x7C, 0x7C, 0x1F,
|
||||
0x1F, 0x03, 0xC7, 0xFF, 0xFB, 0xFF, 0xFE, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0x81, 0xFF, 0x03, 0xFE, 0x07, 0xFC, 0x0F, 0xF8, 0x1F, 0xF0, 0x3F, 0xE0, 0x7F, 0xC0, 0xFF, 0x81,
|
||||
0xFF, 0x03, 0xFE, 0x07, 0xFC, 0x0F, 0xF8, 0x1F, 0xF0, 0x3F, 0xE0, 0x7F, 0xC0, 0xFF, 0x81, 0xFF,
|
||||
0x03, 0xE0, 0xFF, 0xFB, 0xFF, 0xEF, 0xFF, 0x9F, 0x00, 0x3E, 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xE0,
|
||||
0x0F, 0x80, 0x1F, 0x00, 0xFC, 0x03, 0xE0, 0x1F, 0x00, 0xFC, 0x03, 0xE0, 0x1F, 0x00, 0x7C, 0x03,
|
||||
0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x78, 0x01, 0xE0, 0x0F, 0x80,
|
||||
0x3C, 0x01, 0xF0, 0x07, 0x80, 0x3C, 0x00, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0xF0, 0x03, 0xC0, 0x1E,
|
||||
0x00, 0x78, 0x03, 0xC0, 0x0F, 0x00, 0x78, 0x01, 0xE0, 0x00, 0x79, 0xFF, 0xDF, 0x78, 0x00, 0x0F,
|
||||
0x00, 0x0F, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x1C, 0x78,
|
||||
0x7E, 0x78, 0x7E, 0x78, 0x7F, 0xF0, 0x7F, 0xF0, 0x0F, 0xF0, 0x07, 0xE0, 0x07, 0xE0, 0x03, 0xE0,
|
||||
0x03, 0xC0, 0x3E, 0x7C, 0x7F, 0xFE, 0x7F, 0xFE, 0xF7, 0xEE, 0xF7, 0xEE, 0x7F, 0xFE, 0x7F, 0xFE,
|
||||
0x3E, 0x7C, 0x00, 0x01, 0xF0, 0xFC, 0x7F, 0x1F, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0,
|
||||
0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x3E, 0x3F, 0x8F, 0xC3, 0xE0,
|
||||
0x00, 0x00, 0x3E, 0x1D, 0xFE, 0xF7, 0xFF, 0xBD, 0xFE, 0x71, 0xF0, 0x00, 0x03, 0xE1, 0xDF, 0xEF,
|
||||
0x7F, 0xFB, 0xDF, 0xE7, 0x1F, 0x00, 0x00, 0xC0, 0x1E, 0x01, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07,
|
||||
0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0x80, 0x78, 0x03, 0x00, 0x01, 0xF3, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xEF, 0xC0, 0xFF, 0xEF, 0xFF, 0xFF, 0xF3, 0xFF, 0x01, 0xF0, 0x00, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xF0, 0x78, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0x3F, 0xF0, 0x3F, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFC, 0x78, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0,
|
||||
0x78, 0x03, 0xC0, 0x0F, 0x00, 0x78, 0x01, 0xE0, 0x00, 0x3E, 0x3F, 0xBD, 0xFE, 0x7E, 0x3F, 0x1F,
|
||||
0xCF, 0xEF, 0x7F, 0x1F, 0x00, 0x0F, 0x0F, 0x87, 0xC7, 0xE7, 0x73, 0xBB, 0xFF, 0xFF, 0x07, 0x03,
|
||||
0x80, 0x7F, 0x3F, 0x9C, 0x0E, 0x07, 0xE3, 0xF8, 0x3C, 0x8E, 0x7F, 0x7F, 0x00, 0x0F, 0x1F, 0x9F,
|
||||
0x8F, 0x07, 0xF7, 0xFB, 0xDE, 0xEF, 0x7F, 0x1F, 0x00, 0xFF, 0xFF, 0xC3, 0xC1, 0xE1, 0xE0, 0xF0,
|
||||
0x70, 0x78, 0x3C, 0x1E, 0x00, 0x3F, 0x3F, 0xBD, 0xEE, 0xE7, 0xF3, 0xFB, 0xDF, 0xEF, 0xFF, 0xBF,
|
||||
0x80, 0x3E, 0x3F, 0xBD, 0xDE, 0xF7, 0xFB, 0xFC, 0x1C, 0x3E, 0x7E, 0x3E, 0x00, 0x3E, 0x3F, 0xBD,
|
||||
0xFE, 0x7E, 0x3F, 0x1F, 0xCF, 0xEF, 0x7F, 0x1F, 0x00, 0x1E, 0xFD, 0xFB, 0xF0, 0xE1, 0xC3, 0x87,
|
||||
0x0E, 0x1C, 0x7E, 0x7F, 0x9B, 0xC1, 0xE1, 0xF1, 0xF1, 0xF0, 0xF0, 0x7F, 0x7F, 0x80, 0x7E, 0xFF,
|
||||
0x6F, 0x0F, 0x3E, 0x3F, 0x0F, 0x47, 0xFF, 0xFE, 0x0F, 0x0F, 0x87, 0xC7, 0xE7, 0x73, 0xBB, 0xFF,
|
||||
0xFF, 0x07, 0x03, 0x80, 0x7F, 0x3F, 0x9C, 0x0E, 0x07, 0xE3, 0xF8, 0x3C, 0x8E, 0x7F, 0x7F, 0x00,
|
||||
0x0F, 0x1F, 0x9F, 0x8F, 0x07, 0xF7, 0xFB, 0xDE, 0xEF, 0x7F, 0x1F, 0x00, 0xFF, 0xFF, 0xC3, 0xC1,
|
||||
0xE1, 0xE0, 0xF0, 0x70, 0x78, 0x3C, 0x1E, 0x00, 0x3F, 0x3F, 0xBD, 0xEE, 0xE7, 0xF3, 0xFB, 0xDF,
|
||||
0xEF, 0xFF, 0xBF, 0x80, 0x3E, 0x3F, 0xBD, 0xDE, 0xF7, 0xFB, 0xFC, 0x1C, 0x3E, 0x7E, 0x3E, 0x00,
|
||||
0x07, 0xF8, 0x3F, 0xE1, 0xFF, 0x87, 0xC4, 0x3E, 0x00, 0xF0, 0x0F, 0xFE, 0x3F, 0xF8, 0x7C, 0x01,
|
||||
0xF0, 0x0F, 0xFE, 0x3F, 0xF0, 0x3C, 0x00, 0xFC, 0xE1, 0xFF, 0x87, 0xFE, 0x07, 0xF8, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xC1, 0xE0, 0x07, 0xB0, 0x1F, 0xC0, 0x7F, 0x07, 0xF8, 0x3F, 0x80, 0xFF, 0xC1,
|
||||
0x7F, 0x01, 0xFC, 0x3F, 0xC0, 0xFE, 0x01, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1F, 0xE0, 0xFF, 0xC3,
|
||||
0xFF, 0x06, 0x3E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xF8, 0x1F, 0x83, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0x7C, 0x01, 0xF9, 0xC3, 0xFF, 0x8F, 0xFE, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1,
|
||||
0xF8, 0x01, 0xE3, 0xFF, 0xEF, 0xFF, 0x80, 0xF8, 0x0F, 0xE3, 0xFF, 0x0F, 0xF8, 0x3F, 0xC0, 0xFC,
|
||||
0x01, 0xF8, 0x01, 0xF0, 0x03, 0xE0, 0x07, 0xC0, 0x1F, 0xC0, 0x7F, 0xE0, 0x7F, 0xE0, 0xCF, 0xC0,
|
||||
0x07, 0xC0, 0x07, 0x83, 0xFF, 0x0F, 0xFE, 0x3F, 0xFC, 0xFC, 0xF9, 0xE0, 0xF3, 0xC1, 0xE7, 0x83,
|
||||
0xCF, 0x0F, 0x9F, 0x3E, 0x3F, 0xFC, 0x3F, 0xF0, 0x3F, 0x80, 0x03, 0xE0, 0x00, 0xF8, 0x00, 0x7F,
|
||||
0x00, 0x1F, 0xC0, 0x07, 0xF8, 0x03, 0xFE, 0x00, 0xF7, 0xC0, 0x7D, 0xF0, 0x1E, 0x3C, 0x07, 0x8F,
|
||||
0x83, 0xE3, 0xE0, 0xF0, 0x7C, 0x7C, 0x1F, 0x1F, 0x03, 0xC7, 0xFF, 0xFB, 0xFF, 0xFE, 0xFF, 0xFF,
|
||||
0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0xFF, 0x03, 0xFE, 0x07, 0xFC, 0x0F, 0xF8, 0x1F,
|
||||
0xF0, 0x3F, 0xE0, 0x7F, 0xC0, 0xFF, 0x81, 0xFF, 0x03, 0xFE, 0x07, 0xFC, 0x0F, 0xF8, 0x1F, 0xF0,
|
||||
0x3F, 0xE0, 0x7F, 0xC0, 0xFF, 0x81, 0xFF, 0x03, 0xE0, 0xFF, 0xFB, 0xFF, 0xEF, 0xFF, 0x9F, 0x00,
|
||||
0x3E, 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xE0, 0x0F, 0x80, 0x1F, 0x00, 0xFC, 0x03, 0xE0, 0x1F, 0x00,
|
||||
0xFC, 0x03, 0xE0, 0x1F, 0x00, 0x7C, 0x03, 0xFF, 0xEF, 0xFF, 0xBF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xF0, 0x00, 0x78, 0x01, 0xE0, 0x0F, 0x80, 0x3C, 0x01, 0xF0, 0x07, 0x80, 0x3C, 0x00, 0xF0, 0x07,
|
||||
0x80, 0x1E, 0x00, 0xF0, 0x03, 0xC0, 0x1E, 0x00, 0x78, 0x03, 0xC0, 0x0F, 0x00, 0x78, 0x01, 0xE0,
|
||||
0x00, 0x79, 0xFF, 0xDF, 0x78, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1E, 0x00,
|
||||
0x3C, 0x00, 0x3C, 0x00, 0x3C, 0x1C, 0x78, 0x7E, 0x78, 0x7E, 0x78, 0x7F, 0xF0, 0x7F, 0xF0, 0x0F,
|
||||
0xF0, 0x07, 0xE0, 0x07, 0xE0, 0x03, 0xE0, 0x03, 0xC0, 0x3E, 0x7C, 0x7F, 0xFE, 0x7F, 0xFE, 0xF7,
|
||||
0xEE, 0xF7, 0xEE, 0x7F, 0xFE, 0x7F, 0xFE, 0x3E, 0x7C, 0x00, 0x01, 0xF0, 0xFC, 0x7F, 0x1F, 0x07,
|
||||
0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81,
|
||||
0xE0, 0x78, 0x3E, 0x3F, 0x8F, 0xC3, 0xE0, 0x00, 0x00, 0x3E, 0x1D, 0xFE, 0xF7, 0xFF, 0xBD, 0xFE,
|
||||
0x71, 0xF0, 0x00, 0x03, 0xE1, 0xDF, 0xEF, 0x7F, 0xFB, 0xDF, 0xE7, 0x1F, 0x00, 0x00, 0xC0, 0x1E,
|
||||
0x01, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0x80, 0x78,
|
||||
0x03, 0x00, 0x01, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xC0, 0xFF, 0xEF, 0xFF, 0xFF, 0xF3,
|
||||
0xFF, 0x01, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x78, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0x3F,
|
||||
0xF0, 0x3F, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0,
|
||||
};
|
||||
|
||||
static const EpdGlyph ubuntu_12_boldGlyphs[] = {
|
||||
@@ -1732,24 +1745,41 @@ static const EpdGlyph ubuntu_12_boldGlyphs[] = {
|
||||
{ 8, 13, 8, 0, 14, 13, 18095 }, // ‹
|
||||
{ 8, 13, 8, 0, 14, 13, 18108 }, // ›
|
||||
{ 14, 18, 4, -5, 18, 32, 18121 }, // ⁄
|
||||
{ 14, 17, 14, 1, 17, 30, 18153 }, // €
|
||||
{ 14, 17, 14, 0, 17, 30, 18183 }, // ₮
|
||||
{ 14, 17, 14, 0, 17, 30, 18213 }, // ₴
|
||||
{ 14, 17, 14, 1, 17, 30, 18243 }, // ₹
|
||||
{ 15, 18, 15, 0, 18, 34, 18273 }, // ∂
|
||||
{ 18, 17, 17, 0, 17, 39, 18307 }, // ∆
|
||||
{ 15, 21, 18, 2, 18, 40, 18346 }, // ∏
|
||||
{ 14, 20, 14, 0, 17, 35, 18386 }, // ∑
|
||||
{ 12, 3, 14, 1, 9, 5, 18421 }, // −
|
||||
{ 14, 18, 4, -5, 18, 32, 18426 }, // ∕
|
||||
{ 6, 5, 6, 0, 10, 4, 18458 }, // ∙
|
||||
{ 16, 18, 15, 0, 18, 36, 18462 }, // √
|
||||
{ 16, 8, 16, 0, 11, 16, 18498 }, // ∞
|
||||
{ 10, 25, 10, 0, 19, 32, 18514 }, // ∫
|
||||
{ 14, 11, 14, 0, 13, 20, 18546 }, // ≈
|
||||
{ 12, 14, 14, 1, 14, 21, 18566 }, // ≠
|
||||
{ 12, 15, 14, 1, 15, 23, 18587 }, // ≤
|
||||
{ 12, 15, 14, 1, 15, 23, 18610 }, // ≥
|
||||
{ 9, 10, 9, 0, 18, 12, 18153 }, // ⁰
|
||||
{ 9, 10, 9, 0, 18, 12, 18165 }, // ⁴
|
||||
{ 9, 10, 9, 0, 18, 12, 18177 }, // ⁵
|
||||
{ 9, 10, 9, 0, 18, 12, 18189 }, // ⁶
|
||||
{ 9, 10, 9, 0, 18, 12, 18201 }, // ⁷
|
||||
{ 9, 10, 9, 0, 18, 12, 18213 }, // ⁸
|
||||
{ 9, 10, 9, 0, 18, 12, 18225 }, // ⁹
|
||||
{ 9, 10, 9, 0, 11, 12, 18237 }, // ₀
|
||||
{ 7, 10, 9, 0, 11, 9, 18249 }, // ₁
|
||||
{ 9, 10, 9, 0, 11, 12, 18258 }, // ₂
|
||||
{ 8, 10, 9, 0, 11, 10, 18270 }, // ₃
|
||||
{ 9, 10, 9, 0, 11, 12, 18280 }, // ₄
|
||||
{ 9, 10, 9, 0, 11, 12, 18292 }, // ₅
|
||||
{ 9, 10, 9, 0, 11, 12, 18304 }, // ₆
|
||||
{ 9, 10, 9, 0, 11, 12, 18316 }, // ₇
|
||||
{ 9, 10, 9, 0, 11, 12, 18328 }, // ₈
|
||||
{ 9, 10, 9, 0, 11, 12, 18340 }, // ₉
|
||||
{ 14, 17, 14, 1, 17, 30, 18352 }, // €
|
||||
{ 14, 17, 14, 0, 17, 30, 18382 }, // ₮
|
||||
{ 14, 17, 14, 0, 17, 30, 18412 }, // ₴
|
||||
{ 14, 17, 14, 1, 17, 30, 18442 }, // ₹
|
||||
{ 15, 18, 15, 0, 18, 34, 18472 }, // ∂
|
||||
{ 18, 17, 17, 0, 17, 39, 18506 }, // ∆
|
||||
{ 15, 21, 18, 2, 18, 40, 18545 }, // ∏
|
||||
{ 14, 20, 14, 0, 17, 35, 18585 }, // ∑
|
||||
{ 12, 3, 14, 1, 9, 5, 18620 }, // −
|
||||
{ 14, 18, 4, -5, 18, 32, 18625 }, // ∕
|
||||
{ 6, 5, 6, 0, 10, 4, 18657 }, // ∙
|
||||
{ 16, 18, 15, 0, 18, 36, 18661 }, // √
|
||||
{ 16, 8, 16, 0, 11, 16, 18697 }, // ∞
|
||||
{ 10, 25, 10, 0, 19, 32, 18713 }, // ∫
|
||||
{ 14, 11, 14, 0, 13, 20, 18745 }, // ≈
|
||||
{ 12, 14, 14, 1, 14, 21, 18765 }, // ≠
|
||||
{ 12, 15, 14, 1, 15, 23, 18786 }, // ≤
|
||||
{ 12, 15, 14, 1, 15, 23, 18809 }, // ≥
|
||||
};
|
||||
|
||||
static const EpdUnicodeInterval ubuntu_12_boldIntervals[] = {
|
||||
@@ -1773,30 +1803,35 @@ static const EpdUnicodeInterval ubuntu_12_boldIntervals[] = {
|
||||
{ 0x2030, 0x2030, 0x228 },
|
||||
{ 0x2039, 0x203A, 0x229 },
|
||||
{ 0x2044, 0x2044, 0x22B },
|
||||
{ 0x20AC, 0x20AC, 0x22C },
|
||||
{ 0x20AE, 0x20AE, 0x22D },
|
||||
{ 0x20B4, 0x20B4, 0x22E },
|
||||
{ 0x20B9, 0x20B9, 0x22F },
|
||||
{ 0x2202, 0x2202, 0x230 },
|
||||
{ 0x2206, 0x2206, 0x231 },
|
||||
{ 0x220F, 0x220F, 0x232 },
|
||||
{ 0x2211, 0x2212, 0x233 },
|
||||
{ 0x2215, 0x2215, 0x235 },
|
||||
{ 0x2219, 0x221A, 0x236 },
|
||||
{ 0x221E, 0x221E, 0x238 },
|
||||
{ 0x222B, 0x222B, 0x239 },
|
||||
{ 0x2248, 0x2248, 0x23A },
|
||||
{ 0x2260, 0x2260, 0x23B },
|
||||
{ 0x2264, 0x2265, 0x23C },
|
||||
{ 0x2070, 0x2070, 0x22C },
|
||||
{ 0x2074, 0x2079, 0x22D },
|
||||
{ 0x2080, 0x2089, 0x233 },
|
||||
{ 0x20AC, 0x20AC, 0x23D },
|
||||
{ 0x20AE, 0x20AE, 0x23E },
|
||||
{ 0x20B4, 0x20B4, 0x23F },
|
||||
{ 0x20B9, 0x20B9, 0x240 },
|
||||
{ 0x2202, 0x2202, 0x241 },
|
||||
{ 0x2206, 0x2206, 0x242 },
|
||||
{ 0x220F, 0x220F, 0x243 },
|
||||
{ 0x2211, 0x2212, 0x244 },
|
||||
{ 0x2215, 0x2215, 0x246 },
|
||||
{ 0x2219, 0x221A, 0x247 },
|
||||
{ 0x221E, 0x221E, 0x249 },
|
||||
{ 0x222B, 0x222B, 0x24A },
|
||||
{ 0x2248, 0x2248, 0x24B },
|
||||
{ 0x2260, 0x2260, 0x24C },
|
||||
{ 0x2264, 0x2265, 0x24D },
|
||||
};
|
||||
|
||||
static const EpdFontData ubuntu_12_bold = {
|
||||
ubuntu_12_boldBitmaps,
|
||||
ubuntu_12_boldGlyphs,
|
||||
ubuntu_12_boldIntervals,
|
||||
35,
|
||||
38,
|
||||
29,
|
||||
24,
|
||||
-5,
|
||||
false,
|
||||
nullptr,
|
||||
0,
|
||||
};
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
* name: ubuntu_12_regular
|
||||
* size: 12
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_12_regular 12 ../builtinFonts/source/Ubuntu/Ubuntu-Regular.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
static const uint8_t ubuntu_12_regularBitmaps[16913] = {
|
||||
static const uint8_t ubuntu_12_regularBitmaps[17102] = {
|
||||
0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x9C, 0xE0, 0x01, 0xDE, 0x70, 0xF7, 0xF7, 0xF7, 0x67, 0x67,
|
||||
0x67, 0x0E, 0x18, 0x1C, 0x70, 0x38, 0xE0, 0x71, 0xC0, 0xC3, 0x9F, 0xFF, 0xFF, 0xFF, 0x8E, 0x38,
|
||||
0x1C, 0x70, 0x30, 0xE3, 0xFF, 0xFF, 0xFF, 0xF3, 0x8E, 0x07, 0x1C, 0x0E, 0x38, 0x38, 0x60, 0x71,
|
||||
@@ -1037,35 +1038,46 @@ static const uint8_t ubuntu_12_regularBitmaps[16913] = {
|
||||
0xFE, 0x0E, 0x07, 0xC3, 0xE0, 0x0C, 0x38, 0xF1, 0xC7, 0x8E, 0x1E, 0x1C, 0x3C, 0x38, 0x30, 0xC3,
|
||||
0x87, 0x1E, 0x38, 0xF3, 0x9E, 0x73, 0x8C, 0x00, 0x00, 0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x0E,
|
||||
0x00, 0xE0, 0x0E, 0x00, 0x70, 0x07, 0x00, 0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xE0,
|
||||
0x0E, 0x00, 0x70, 0x00, 0x07, 0xF8, 0x7F, 0x87, 0xCC, 0x38, 0x03, 0x80, 0x7F, 0xF3, 0xFF, 0x87,
|
||||
0x00, 0x38, 0x07, 0xFE, 0x3F, 0xF0, 0x70, 0x03, 0xC0, 0x0E, 0x00, 0x7C, 0x61, 0xFF, 0x07, 0xF8,
|
||||
0xFF, 0xFF, 0xFF, 0xC1, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0xF0, 0x1F, 0x83, 0xF0, 0x7F, 0x03, 0xF8,
|
||||
0x01, 0xF8, 0x0F, 0xC1, 0xF8, 0x3F, 0x81, 0xFC, 0x00, 0xE0, 0x07, 0x00, 0x1F, 0xC0, 0x7F, 0xC1,
|
||||
0x8F, 0x00, 0x1C, 0x00, 0x71, 0xFF, 0xF7, 0xFF, 0xC0, 0xF8, 0x07, 0xC0, 0x7C, 0x07, 0xFF, 0xDF,
|
||||
0xFF, 0x38, 0x00, 0xE0, 0x03, 0xC7, 0x0F, 0xFE, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, 0xC0, 0xF0, 0x03,
|
||||
0x8F, 0xFF, 0x7F, 0xF8, 0x07, 0x00, 0xF8, 0xFF, 0x87, 0xF0, 0x1E, 0x00, 0x78, 0x01, 0xC0, 0x07,
|
||||
0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x7F, 0x83, 0xFE, 0x18, 0xF8, 0x01, 0xE0, 0x07, 0x00, 0x38,
|
||||
0x01, 0xC3, 0xFF, 0x3F, 0xFB, 0xE7, 0xFC, 0x1F, 0xC0, 0xFE, 0x07, 0x70, 0x3B, 0x81, 0xDC, 0x1C,
|
||||
0xFB, 0xE3, 0xFE, 0x0F, 0xE0, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x67, 0x00,
|
||||
0x73, 0x80, 0x38, 0xE0, 0x38, 0x70, 0x1C, 0x38, 0x1C, 0x0E, 0x0E, 0x07, 0x07, 0x03, 0xC7, 0x00,
|
||||
0xE3, 0x80, 0x71, 0xC0, 0x3D, 0xFF, 0xFE, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xF8,
|
||||
0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0,
|
||||
0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF,
|
||||
0xDC, 0x00, 0x70, 0x03, 0xC0, 0x0E, 0x00, 0x38, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x01, 0xE0, 0x1E,
|
||||
0x00, 0xE0, 0x0F, 0x00, 0xF0, 0x07, 0x00, 0x78, 0x03, 0x80, 0x3F, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF,
|
||||
0xFF, 0x00, 0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x70, 0x07, 0x00,
|
||||
0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xE0, 0x0E, 0x00, 0x70, 0x00, 0xFF, 0xF0, 0x00,
|
||||
0x0E, 0x00, 0x18, 0x00, 0x70, 0x00, 0xE0, 0x01, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x30,
|
||||
0x00, 0xE1, 0xE1, 0xC7, 0xC3, 0x0F, 0xCE, 0x03, 0x9C, 0x03, 0xB8, 0x07, 0x60, 0x07, 0xC0, 0x0F,
|
||||
0x80, 0x1E, 0x00, 0x1C, 0x00, 0x3C, 0xF9, 0xFF, 0xF7, 0x7D, 0xF8, 0xE3, 0x77, 0xDD, 0xFF, 0xF3,
|
||||
0xEF, 0x80, 0x07, 0x83, 0xE0, 0xF0, 0x78, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0,
|
||||
0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x1F, 0x0F, 0x83, 0xC0,
|
||||
0x7C, 0x3F, 0xF7, 0xEF, 0xFC, 0x3E, 0x00, 0x00, 0x00, 0x7C, 0x3F, 0xF7, 0xEF, 0xFC, 0x3E, 0x00,
|
||||
0xC0, 0x1C, 0x01, 0xCF, 0xFF, 0xFF, 0xF0, 0x70, 0x07, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x38, 0x03,
|
||||
0x80, 0x30, 0x00, 0x00, 0x70, 0x1F, 0x0F, 0xE3, 0xF8, 0xFC, 0x0F, 0x00, 0xFC, 0x03, 0xF8, 0x0F,
|
||||
0xE0, 0x1F, 0x00, 0x70, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0xE0, 0x0F, 0x80, 0x7F, 0x00, 0xFC,
|
||||
0x03, 0xF0, 0x0F, 0x03, 0xF0, 0xFC, 0x7F, 0x0F, 0x80, 0xE0, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF,
|
||||
0xF0,
|
||||
0x0E, 0x00, 0x70, 0x00, 0x3E, 0x3F, 0x9D, 0xDC, 0x6E, 0x3F, 0x1F, 0x8C, 0xEE, 0x7F, 0x1F, 0x00,
|
||||
0x07, 0x07, 0x87, 0xC7, 0xE7, 0x73, 0x3B, 0xFF, 0xFF, 0x07, 0x03, 0x80, 0x7E, 0xFD, 0x87, 0xEF,
|
||||
0xE3, 0xC1, 0xE7, 0xFF, 0xF8, 0x0F, 0x1F, 0x9F, 0x0F, 0xC7, 0xF3, 0xB9, 0x8E, 0xEE, 0x7F, 0x1F,
|
||||
0x00, 0x7F, 0x3F, 0x81, 0xC1, 0xC0, 0xE0, 0xE0, 0x70, 0x30, 0x38, 0x1C, 0x00, 0x3E, 0x3F, 0x9D,
|
||||
0xCE, 0xE7, 0xE3, 0xF9, 0x8D, 0xEE, 0x7F, 0x1F, 0x00, 0x3E, 0x3F, 0x9D, 0xDC, 0x67, 0xF1, 0xF8,
|
||||
0x1C, 0x3E, 0x7E, 0x3C, 0x00, 0x3E, 0x3F, 0x9D, 0xDC, 0x6E, 0x3F, 0x1F, 0x8C, 0xEE, 0x7F, 0x1F,
|
||||
0x00, 0x3B, 0xFF, 0xF1, 0x8C, 0x63, 0x18, 0xC0, 0x7E, 0x7F, 0x67, 0x07, 0x0E, 0x1C, 0x38, 0x70,
|
||||
0x7F, 0x7F, 0x7E, 0x7E, 0x6F, 0x3E, 0x3E, 0x0F, 0x07, 0x67, 0x7F, 0x7E, 0x07, 0x07, 0x87, 0xC7,
|
||||
0xE7, 0x73, 0x3B, 0xFF, 0xFF, 0x07, 0x03, 0x80, 0x7E, 0xFD, 0x87, 0xEF, 0xE3, 0xC1, 0xE7, 0xFF,
|
||||
0xF8, 0x0F, 0x1F, 0x9F, 0x0F, 0xC7, 0xF3, 0xB9, 0x8E, 0xEE, 0x7F, 0x1F, 0x00, 0x7F, 0x3F, 0x81,
|
||||
0xC1, 0xC0, 0xE0, 0xE0, 0x70, 0x30, 0x38, 0x1C, 0x00, 0x3E, 0x3F, 0x9D, 0xCE, 0xE7, 0xE3, 0xF9,
|
||||
0x8D, 0xEE, 0x7F, 0x1F, 0x00, 0x3E, 0x3F, 0x9D, 0xDC, 0x67, 0xF1, 0xF8, 0x1C, 0x3E, 0x7E, 0x3C,
|
||||
0x00, 0x07, 0xF8, 0x7F, 0x87, 0xCC, 0x38, 0x03, 0x80, 0x7F, 0xF3, 0xFF, 0x87, 0x00, 0x38, 0x07,
|
||||
0xFE, 0x3F, 0xF0, 0x70, 0x03, 0xC0, 0x0E, 0x00, 0x7C, 0x61, 0xFF, 0x07, 0xF8, 0xFF, 0xFF, 0xFF,
|
||||
0xC1, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0xF0, 0x1F, 0x83, 0xF0, 0x7F, 0x03, 0xF8, 0x01, 0xF8, 0x0F,
|
||||
0xC1, 0xF8, 0x3F, 0x81, 0xFC, 0x00, 0xE0, 0x07, 0x00, 0x1F, 0xC0, 0x7F, 0xC1, 0x8F, 0x00, 0x1C,
|
||||
0x00, 0x71, 0xFF, 0xF7, 0xFF, 0xC0, 0xF8, 0x07, 0xC0, 0x7C, 0x07, 0xFF, 0xDF, 0xFF, 0x38, 0x00,
|
||||
0xE0, 0x03, 0xC7, 0x0F, 0xFE, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, 0xC0, 0xF0, 0x03, 0x8F, 0xFF, 0x7F,
|
||||
0xF8, 0x07, 0x00, 0xF8, 0xFF, 0x87, 0xF0, 0x1E, 0x00, 0x78, 0x01, 0xC0, 0x07, 0x00, 0x3C, 0x00,
|
||||
0xF0, 0x03, 0xC0, 0x7F, 0x83, 0xFE, 0x18, 0xF8, 0x01, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC3, 0xFF,
|
||||
0x3F, 0xFB, 0xE7, 0xFC, 0x1F, 0xC0, 0xFE, 0x07, 0x70, 0x3B, 0x81, 0xDC, 0x1C, 0xFB, 0xE3, 0xFE,
|
||||
0x0F, 0xE0, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x67, 0x00, 0x73, 0x80, 0x38,
|
||||
0xE0, 0x38, 0x70, 0x1C, 0x38, 0x1C, 0x0E, 0x0E, 0x07, 0x07, 0x03, 0xC7, 0x00, 0xE3, 0x80, 0x71,
|
||||
0xC0, 0x3D, 0xFF, 0xFE, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F,
|
||||
0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE,
|
||||
0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xDC, 0x00, 0x70,
|
||||
0x03, 0xC0, 0x0E, 0x00, 0x38, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x01, 0xE0, 0x1E, 0x00, 0xE0, 0x0F,
|
||||
0x00, 0xF0, 0x07, 0x00, 0x78, 0x03, 0x80, 0x3F, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0x00, 0x38,
|
||||
0x03, 0x80, 0x1C, 0x01, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x70, 0x07, 0x00, 0x38, 0x03, 0x80,
|
||||
0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xE0, 0x0E, 0x00, 0x70, 0x00, 0xFF, 0xF0, 0x00, 0x0E, 0x00, 0x18,
|
||||
0x00, 0x70, 0x00, 0xE0, 0x01, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x30, 0x00, 0xE1, 0xE1,
|
||||
0xC7, 0xC3, 0x0F, 0xCE, 0x03, 0x9C, 0x03, 0xB8, 0x07, 0x60, 0x07, 0xC0, 0x0F, 0x80, 0x1E, 0x00,
|
||||
0x1C, 0x00, 0x3C, 0xF9, 0xFF, 0xF7, 0x7D, 0xF8, 0xE3, 0x77, 0xDD, 0xFF, 0xF3, 0xEF, 0x80, 0x07,
|
||||
0x83, 0xE0, 0xF0, 0x78, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07,
|
||||
0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x1F, 0x0F, 0x83, 0xC0, 0x7C, 0x3F, 0xF7,
|
||||
0xEF, 0xFC, 0x3E, 0x00, 0x00, 0x00, 0x7C, 0x3F, 0xF7, 0xEF, 0xFC, 0x3E, 0x00, 0xC0, 0x1C, 0x01,
|
||||
0xCF, 0xFF, 0xFF, 0xF0, 0x70, 0x07, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x38, 0x03, 0x80, 0x30, 0x00,
|
||||
0x00, 0x70, 0x1F, 0x0F, 0xE3, 0xF8, 0xFC, 0x0F, 0x00, 0xFC, 0x03, 0xF8, 0x0F, 0xE0, 0x1F, 0x00,
|
||||
0x70, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0xE0, 0x0F, 0x80, 0x7F, 0x00, 0xFC, 0x03, 0xF0, 0x0F,
|
||||
0x03, 0xF0, 0xFC, 0x7F, 0x0F, 0x80, 0xE0, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0,
|
||||
};
|
||||
|
||||
static const EpdGlyph ubuntu_12_regularGlyphs[] = {
|
||||
@@ -1625,24 +1637,41 @@ static const EpdGlyph ubuntu_12_regularGlyphs[] = {
|
||||
{ 7, 11, 7, 0, 13, 10, 16421 }, // ‹
|
||||
{ 6, 11, 7, 1, 13, 9, 16431 }, // ›
|
||||
{ 13, 17, 4, -5, 17, 28, 16440 }, // ⁄
|
||||
{ 13, 17, 14, 1, 17, 28, 16468 }, // €
|
||||
{ 13, 17, 14, 1, 17, 28, 16496 }, // ₮
|
||||
{ 14, 17, 14, 0, 17, 30, 16524 }, // ₴
|
||||
{ 13, 17, 14, 1, 17, 28, 16554 }, // ₹
|
||||
{ 13, 19, 15, 1, 19, 31, 16582 }, // ∂
|
||||
{ 17, 17, 16, 0, 17, 37, 16613 }, // ∆
|
||||
{ 14, 20, 17, 2, 17, 35, 16650 }, // ∏
|
||||
{ 13, 20, 13, 0, 17, 33, 16685 }, // ∑
|
||||
{ 12, 2, 14, 1, 8, 3, 16718 }, // −
|
||||
{ 13, 17, 4, -5, 17, 28, 16721 }, // ∕
|
||||
{ 4, 3, 6, 1, 9, 2, 16749 }, // ∙
|
||||
{ 15, 20, 14, 0, 20, 38, 16751 }, // √
|
||||
{ 14, 7, 15, 0, 11, 13, 16789 }, // ∞
|
||||
{ 10, 24, 9, 0, 19, 30, 16802 }, // ∫
|
||||
{ 12, 10, 14, 1, 13, 15, 16832 }, // ≈
|
||||
{ 12, 13, 14, 1, 14, 20, 16847 }, // ≠
|
||||
{ 12, 15, 14, 1, 15, 23, 16867 }, // ≤
|
||||
{ 12, 15, 14, 1, 15, 23, 16890 }, // ≥
|
||||
{ 9, 10, 9, 0, 18, 12, 16468 }, // ⁰
|
||||
{ 9, 10, 9, 0, 18, 12, 16480 }, // ⁴
|
||||
{ 7, 10, 9, 1, 18, 9, 16492 }, // ⁵
|
||||
{ 9, 10, 9, 0, 18, 12, 16501 }, // ⁶
|
||||
{ 9, 10, 9, 0, 18, 12, 16513 }, // ⁷
|
||||
{ 9, 10, 9, 0, 18, 12, 16525 }, // ⁸
|
||||
{ 9, 10, 9, 0, 18, 12, 16537 }, // ⁹
|
||||
{ 9, 10, 9, 0, 10, 12, 16549 }, // ₀
|
||||
{ 5, 10, 9, 1, 10, 7, 16561 }, // ₁
|
||||
{ 8, 10, 9, 0, 10, 10, 16568 }, // ₂
|
||||
{ 8, 10, 9, 0, 10, 10, 16578 }, // ₃
|
||||
{ 9, 10, 9, 0, 10, 12, 16588 }, // ₄
|
||||
{ 7, 10, 9, 1, 10, 9, 16600 }, // ₅
|
||||
{ 9, 10, 9, 0, 10, 12, 16609 }, // ₆
|
||||
{ 9, 10, 9, 0, 10, 12, 16621 }, // ₇
|
||||
{ 9, 10, 9, 0, 10, 12, 16633 }, // ₈
|
||||
{ 9, 10, 9, 0, 10, 12, 16645 }, // ₉
|
||||
{ 13, 17, 14, 1, 17, 28, 16657 }, // €
|
||||
{ 13, 17, 14, 1, 17, 28, 16685 }, // ₮
|
||||
{ 14, 17, 14, 0, 17, 30, 16713 }, // ₴
|
||||
{ 13, 17, 14, 1, 17, 28, 16743 }, // ₹
|
||||
{ 13, 19, 15, 1, 19, 31, 16771 }, // ∂
|
||||
{ 17, 17, 16, 0, 17, 37, 16802 }, // ∆
|
||||
{ 14, 20, 17, 2, 17, 35, 16839 }, // ∏
|
||||
{ 13, 20, 13, 0, 17, 33, 16874 }, // ∑
|
||||
{ 12, 2, 14, 1, 8, 3, 16907 }, // −
|
||||
{ 13, 17, 4, -5, 17, 28, 16910 }, // ∕
|
||||
{ 4, 3, 6, 1, 9, 2, 16938 }, // ∙
|
||||
{ 15, 20, 14, 0, 20, 38, 16940 }, // √
|
||||
{ 14, 7, 15, 0, 11, 13, 16978 }, // ∞
|
||||
{ 10, 24, 9, 0, 19, 30, 16991 }, // ∫
|
||||
{ 12, 10, 14, 1, 13, 15, 17021 }, // ≈
|
||||
{ 12, 13, 14, 1, 14, 20, 17036 }, // ≠
|
||||
{ 12, 15, 14, 1, 15, 23, 17056 }, // ≤
|
||||
{ 12, 15, 14, 1, 15, 23, 17079 }, // ≥
|
||||
};
|
||||
|
||||
static const EpdUnicodeInterval ubuntu_12_regularIntervals[] = {
|
||||
@@ -1666,30 +1695,35 @@ static const EpdUnicodeInterval ubuntu_12_regularIntervals[] = {
|
||||
{ 0x2030, 0x2030, 0x228 },
|
||||
{ 0x2039, 0x203A, 0x229 },
|
||||
{ 0x2044, 0x2044, 0x22B },
|
||||
{ 0x20AC, 0x20AC, 0x22C },
|
||||
{ 0x20AE, 0x20AE, 0x22D },
|
||||
{ 0x20B4, 0x20B4, 0x22E },
|
||||
{ 0x20B9, 0x20B9, 0x22F },
|
||||
{ 0x2202, 0x2202, 0x230 },
|
||||
{ 0x2206, 0x2206, 0x231 },
|
||||
{ 0x220F, 0x220F, 0x232 },
|
||||
{ 0x2211, 0x2212, 0x233 },
|
||||
{ 0x2215, 0x2215, 0x235 },
|
||||
{ 0x2219, 0x221A, 0x236 },
|
||||
{ 0x221E, 0x221E, 0x238 },
|
||||
{ 0x222B, 0x222B, 0x239 },
|
||||
{ 0x2248, 0x2248, 0x23A },
|
||||
{ 0x2260, 0x2260, 0x23B },
|
||||
{ 0x2264, 0x2265, 0x23C },
|
||||
{ 0x2070, 0x2070, 0x22C },
|
||||
{ 0x2074, 0x2079, 0x22D },
|
||||
{ 0x2080, 0x2089, 0x233 },
|
||||
{ 0x20AC, 0x20AC, 0x23D },
|
||||
{ 0x20AE, 0x20AE, 0x23E },
|
||||
{ 0x20B4, 0x20B4, 0x23F },
|
||||
{ 0x20B9, 0x20B9, 0x240 },
|
||||
{ 0x2202, 0x2202, 0x241 },
|
||||
{ 0x2206, 0x2206, 0x242 },
|
||||
{ 0x220F, 0x220F, 0x243 },
|
||||
{ 0x2211, 0x2212, 0x244 },
|
||||
{ 0x2215, 0x2215, 0x246 },
|
||||
{ 0x2219, 0x221A, 0x247 },
|
||||
{ 0x221E, 0x221E, 0x249 },
|
||||
{ 0x222B, 0x222B, 0x24A },
|
||||
{ 0x2248, 0x2248, 0x24B },
|
||||
{ 0x2260, 0x2260, 0x24C },
|
||||
{ 0x2264, 0x2265, 0x24D },
|
||||
};
|
||||
|
||||
static const EpdFontData ubuntu_12_regular = {
|
||||
ubuntu_12_regularBitmaps,
|
||||
ubuntu_12_regularGlyphs,
|
||||
ubuntu_12_regularIntervals,
|
||||
35,
|
||||
38,
|
||||
29,
|
||||
24,
|
||||
-5,
|
||||
false,
|
||||
nullptr,
|
||||
0,
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ for size in ${BOOKERLY_FONT_SIZES[@]}; do
|
||||
font_name="bookerly_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||
font_path="../builtinFonts/source/Bookerly/Bookerly-${style}.ttf"
|
||||
output_path="../builtinFonts/${font_name}.h"
|
||||
python fontconvert.py $font_name $size $font_path --2bit > $output_path
|
||||
python fontconvert.py $font_name $size $font_path --2bit --compress > $output_path
|
||||
echo "Generated $output_path"
|
||||
done
|
||||
done
|
||||
@@ -24,7 +24,7 @@ for size in ${NOTOSANS_FONT_SIZES[@]}; do
|
||||
font_name="notosans_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||
font_path="../builtinFonts/source/NotoSans/NotoSans-${style}.ttf"
|
||||
output_path="../builtinFonts/${font_name}.h"
|
||||
python fontconvert.py $font_name $size $font_path --2bit > $output_path
|
||||
python fontconvert.py $font_name $size $font_path --2bit --compress > $output_path
|
||||
echo "Generated $output_path"
|
||||
done
|
||||
done
|
||||
@@ -34,7 +34,7 @@ for size in ${OPENDYSLEXIC_FONT_SIZES[@]}; do
|
||||
font_name="opendyslexic_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||
font_path="../builtinFonts/source/OpenDyslexic/OpenDyslexic-${style}.otf"
|
||||
output_path="../builtinFonts/${font_name}.h"
|
||||
python fontconvert.py $font_name $size $font_path --2bit > $output_path
|
||||
python fontconvert.py $font_name $size $font_path --2bit --compress > $output_path
|
||||
echo "Generated $output_path"
|
||||
done
|
||||
done
|
||||
@@ -53,3 +53,7 @@ for size in ${UI_FONT_SIZES[@]}; do
|
||||
done
|
||||
|
||||
python fontconvert.py notosans_8_regular 8 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf > ../builtinFonts/notosans_8_regular.h
|
||||
|
||||
echo ""
|
||||
echo "Running compression verification..."
|
||||
python verify_compression.py ../builtinFonts/
|
||||
|
||||
@@ -15,6 +15,7 @@ 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.")
|
||||
parser.add_argument("--compress", dest="compress", action="store_true", help="Compress glyph bitmaps using DEFLATE with group-based compression.")
|
||||
args = parser.parse_args()
|
||||
|
||||
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
|
||||
@@ -56,6 +57,8 @@ intervals = [
|
||||
# Russian, Ukrainian, Bulgarian, etc.
|
||||
(0x0400, 0x04FF),
|
||||
### Math Symbols (common subset) ###
|
||||
# Superscripts and Subscripts
|
||||
(0x2070, 0x209F),
|
||||
# General math operators
|
||||
(0x2200, 0x22FF),
|
||||
# Arrows
|
||||
@@ -99,6 +102,9 @@ intervals = [
|
||||
# (0xFE30, 0xFE4F),
|
||||
# # CJK Compatibility Ideographs
|
||||
# (0xF900, 0xFAFF),
|
||||
### Specials
|
||||
# Replacement Character
|
||||
(0xFFFD, 0xFFFD),
|
||||
]
|
||||
|
||||
add_ints = []
|
||||
@@ -265,13 +271,115 @@ for index, glyph in enumerate(all_glyphs):
|
||||
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");
|
||||
compress = args.compress
|
||||
|
||||
# Build groups for compression
|
||||
if compress:
|
||||
# Script-based grouping: glyphs that co-occur in typical text rendering
|
||||
# are grouped together for efficient LRU caching on the embedded target.
|
||||
# Since glyphs are in codepoint order, glyphs in the same Unicode block
|
||||
# are contiguous in the array and form natural groups.
|
||||
SCRIPT_GROUP_RANGES = [
|
||||
(0x0000, 0x007F), # ASCII
|
||||
(0x0080, 0x00FF), # Latin-1 Supplement
|
||||
(0x0100, 0x017F), # Latin Extended-A
|
||||
(0x0300, 0x036F), # Combining Diacritical Marks
|
||||
(0x0400, 0x04FF), # Cyrillic
|
||||
(0x2000, 0x206F), # General Punctuation
|
||||
(0x2070, 0x209F), # Superscripts & Subscripts
|
||||
(0x20A0, 0x20CF), # Currency Symbols
|
||||
(0x2190, 0x21FF), # Arrows
|
||||
(0x2200, 0x22FF), # Math Operators
|
||||
(0xFFFD, 0xFFFD), # Replacement Character
|
||||
]
|
||||
|
||||
def get_script_group(code_point):
|
||||
for i, (start, end) in enumerate(SCRIPT_GROUP_RANGES):
|
||||
if start <= code_point <= end:
|
||||
return i
|
||||
return -1
|
||||
|
||||
groups = [] # list of (first_glyph_index, glyph_count)
|
||||
current_group_id = None
|
||||
group_start = 0
|
||||
group_count = 0
|
||||
|
||||
for i, (props, packed) in enumerate(all_glyphs):
|
||||
sg = get_script_group(props.code_point)
|
||||
if sg != current_group_id:
|
||||
if group_count > 0:
|
||||
groups.append((group_start, group_count))
|
||||
current_group_id = sg
|
||||
group_start = i
|
||||
group_count = 1
|
||||
else:
|
||||
group_count += 1
|
||||
|
||||
if group_count > 0:
|
||||
groups.append((group_start, group_count))
|
||||
|
||||
# Compress each group
|
||||
compressed_groups = [] # list of (compressed_bytes, uncompressed_size, glyph_count, first_glyph_index)
|
||||
compressed_bitmap_data = []
|
||||
compressed_offset = 0
|
||||
|
||||
# Also build modified glyph props with within-group offsets
|
||||
modified_glyph_props = list(glyph_props)
|
||||
|
||||
for first_idx, count in groups:
|
||||
# Concatenate bitmap data for this group
|
||||
group_data = b''
|
||||
for gi in range(first_idx, first_idx + count):
|
||||
props, packed = all_glyphs[gi]
|
||||
# Update glyph's dataOffset to be within-group offset
|
||||
within_group_offset = len(group_data)
|
||||
old_props = modified_glyph_props[gi]
|
||||
modified_glyph_props[gi] = GlyphProps(
|
||||
width=old_props.width,
|
||||
height=old_props.height,
|
||||
advance_x=old_props.advance_x,
|
||||
left=old_props.left,
|
||||
top=old_props.top,
|
||||
data_length=old_props.data_length,
|
||||
data_offset=within_group_offset,
|
||||
code_point=old_props.code_point,
|
||||
)
|
||||
group_data += packed
|
||||
|
||||
# Compress with raw DEFLATE (no zlib/gzip header)
|
||||
compressor = zlib.compressobj(level=9, wbits=-15)
|
||||
compressed = compressor.compress(group_data) + compressor.flush()
|
||||
|
||||
compressed_groups.append((compressed, len(group_data), count, first_idx))
|
||||
compressed_bitmap_data.extend(compressed)
|
||||
compressed_offset += len(compressed)
|
||||
|
||||
glyph_props = modified_glyph_props
|
||||
total_compressed = len(compressed_bitmap_data)
|
||||
total_uncompressed = len(glyph_data)
|
||||
print(f"// Compression: {total_uncompressed} -> {total_compressed} bytes ({100*total_compressed/total_uncompressed:.1f}%), {len(groups)} groups", file=sys.stderr)
|
||||
|
||||
print(f"""/**
|
||||
* generated by fontconvert.py
|
||||
* name: {font_name}
|
||||
* size: {size}
|
||||
* mode: {'2-bit' if is2Bit else '1-bit'}{' compressed: true' if compress else ''}
|
||||
* Command used: {' '.join(sys.argv)}
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
""")
|
||||
|
||||
if compress:
|
||||
print(f"static const uint8_t {font_name}Bitmaps[{len(compressed_bitmap_data)}] = {{")
|
||||
for c in chunks(compressed_bitmap_data, 16):
|
||||
print (" " + " ".join(f"0x{b:02X}," for b in c))
|
||||
print ("};\n");
|
||||
else:
|
||||
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):
|
||||
@@ -285,6 +393,14 @@ for i_start, i_end in intervals:
|
||||
offset += i_end - i_start + 1
|
||||
print ("};\n");
|
||||
|
||||
if compress:
|
||||
print(f"static const EpdFontGroup {font_name}Groups[] = {{")
|
||||
compressed_offset = 0
|
||||
for compressed, uncompressed_size, count, first_idx in compressed_groups:
|
||||
print(f" {{ {compressed_offset}, {len(compressed)}, {uncompressed_size}, {count}, {first_idx} }},")
|
||||
compressed_offset += len(compressed)
|
||||
print("};\n")
|
||||
|
||||
print(f"static const EpdFontData {font_name} = {{")
|
||||
print(f" {font_name}Bitmaps,")
|
||||
print(f" {font_name}Glyphs,")
|
||||
@@ -294,4 +410,10 @@ 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'},")
|
||||
if compress:
|
||||
print(f" {font_name}Groups,")
|
||||
print(f" {len(compressed_groups)},")
|
||||
else:
|
||||
print(f" nullptr,")
|
||||
print(f" 0,")
|
||||
print("};")
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Round-trip verification for compressed font headers.
|
||||
|
||||
Parses each generated .h file in the given directory, identifies compressed fonts
|
||||
(those with a Groups array), decompresses each group, and verifies that
|
||||
decompression succeeds and all glyph offsets/lengths fall within bounds.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
|
||||
def parse_hex_array(text):
|
||||
"""Extract bytes from a C hex array string like '{ 0xAB, 0xCD, ... }'"""
|
||||
hex_vals = re.findall(r'0x([0-9A-Fa-f]{2})', text)
|
||||
return bytes(int(h, 16) for h in hex_vals)
|
||||
|
||||
|
||||
def parse_groups(text):
|
||||
"""Parse EpdFontGroup array entries: { compressedOffset, compressedSize, uncompressedSize, glyphCount, firstGlyphIndex }"""
|
||||
groups = []
|
||||
for match in re.finditer(r'\{\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\}', text):
|
||||
groups.append({
|
||||
'compressedOffset': int(match.group(1)),
|
||||
'compressedSize': int(match.group(2)),
|
||||
'uncompressedSize': int(match.group(3)),
|
||||
'glyphCount': int(match.group(4)),
|
||||
'firstGlyphIndex': int(match.group(5)),
|
||||
})
|
||||
return groups
|
||||
|
||||
|
||||
def parse_glyphs(text):
|
||||
"""Parse EpdGlyph array entries: { width, height, advanceX, left, top, dataLength, dataOffset }"""
|
||||
glyphs = []
|
||||
for match in re.finditer(r'\{\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*\}', text):
|
||||
glyphs.append({
|
||||
'width': int(match.group(1)),
|
||||
'height': int(match.group(2)),
|
||||
'advanceX': int(match.group(3)),
|
||||
'left': int(match.group(4)),
|
||||
'top': int(match.group(5)),
|
||||
'dataLength': int(match.group(6)),
|
||||
'dataOffset': int(match.group(7)),
|
||||
})
|
||||
return glyphs
|
||||
|
||||
|
||||
def verify_font_file(filepath):
|
||||
"""Verify a single font header file. Returns (font_name, success, message)."""
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if this is a compressed font (has Groups array)
|
||||
groups_match = re.search(r'static const EpdFontGroup (\w+)Groups\[\]', content)
|
||||
if not groups_match:
|
||||
return (os.path.basename(filepath), None, "uncompressed, skipping")
|
||||
|
||||
font_name = groups_match.group(1)
|
||||
|
||||
# Extract bitmap data
|
||||
bitmap_match = re.search(
|
||||
r'static const uint8_t ' + re.escape(font_name) + r'Bitmaps\[\d+\]\s*=\s*\{([^}]+)\}',
|
||||
content, re.DOTALL
|
||||
)
|
||||
if not bitmap_match:
|
||||
return (font_name, False, "could not find Bitmaps array")
|
||||
|
||||
compressed_data = parse_hex_array(bitmap_match.group(1))
|
||||
|
||||
# Extract groups
|
||||
groups_array_match = re.search(
|
||||
r'static const EpdFontGroup ' + re.escape(font_name) + r'Groups\[\]\s*=\s*\{(.+?)\};',
|
||||
content, re.DOTALL
|
||||
)
|
||||
if not groups_array_match:
|
||||
return (font_name, False, "could not find Groups array")
|
||||
|
||||
groups = parse_groups(groups_array_match.group(1))
|
||||
if not groups:
|
||||
return (font_name, False, "Groups array parsed to 0 entries; check format")
|
||||
|
||||
# Extract glyphs
|
||||
glyphs_match = re.search(
|
||||
r'static const EpdGlyph ' + re.escape(font_name) + r'Glyphs\[\]\s*=\s*\{(.+?)\};',
|
||||
content, re.DOTALL
|
||||
)
|
||||
if not glyphs_match:
|
||||
return (font_name, False, "could not find Glyphs array")
|
||||
|
||||
glyphs = parse_glyphs(glyphs_match.group(1))
|
||||
|
||||
# Verify each group
|
||||
for gi, group in enumerate(groups):
|
||||
# Extract compressed chunk
|
||||
chunk = compressed_data[group['compressedOffset']:group['compressedOffset'] + group['compressedSize']]
|
||||
if len(chunk) != group['compressedSize']:
|
||||
return (font_name, False, f"group {gi}: compressed data truncated (expected {group['compressedSize']}, got {len(chunk)})")
|
||||
|
||||
# Decompress with raw DEFLATE
|
||||
try:
|
||||
decompressed = zlib.decompress(chunk, -15)
|
||||
except zlib.error as e:
|
||||
return (font_name, False, f"group {gi}: decompression failed: {e}")
|
||||
|
||||
if len(decompressed) != group['uncompressedSize']:
|
||||
return (font_name, False, f"group {gi}: size mismatch (expected {group['uncompressedSize']}, got {len(decompressed)})")
|
||||
|
||||
# Verify each glyph's data within the group
|
||||
first = group['firstGlyphIndex']
|
||||
for j in range(group['glyphCount']):
|
||||
glyph_idx = first + j
|
||||
if glyph_idx >= len(glyphs):
|
||||
return (font_name, False, f"group {gi}: glyph index {glyph_idx} out of range")
|
||||
|
||||
glyph = glyphs[glyph_idx]
|
||||
offset = glyph['dataOffset']
|
||||
length = glyph['dataLength']
|
||||
|
||||
if offset + length > len(decompressed):
|
||||
return (font_name, False, f"group {gi}, glyph {glyph_idx}: data extends beyond decompressed buffer "
|
||||
f"(offset={offset}, length={length}, decompressed_size={len(decompressed)})")
|
||||
|
||||
return (font_name, True, f"{len(groups)} groups, {len(glyphs)} glyphs OK")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <font_headers_directory>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
font_dir = sys.argv[1]
|
||||
if not os.path.isdir(font_dir):
|
||||
print(f"Error: {font_dir} is not a directory", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
files = sorted(f for f in os.listdir(font_dir) if f.endswith('.h') and f != 'all.h')
|
||||
passed = 0
|
||||
failed = 0
|
||||
skipped = 0
|
||||
|
||||
for filename in files:
|
||||
filepath = os.path.join(font_dir, filename)
|
||||
_font_name, success, message = verify_font_file(filepath)
|
||||
|
||||
if success is None:
|
||||
skipped += 1
|
||||
elif success:
|
||||
passed += 1
|
||||
print(f" PASS: {filename} ({message})")
|
||||
else:
|
||||
failed += 1
|
||||
print(f" FAIL: {filename} - {message}")
|
||||
|
||||
print(f"\nResults: {passed} passed, {failed} failed, {skipped} skipped (uncompressed)")
|
||||
|
||||
if failed > 0:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+403
-95
@@ -1,9 +1,10 @@
|
||||
#include "Epub.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <HalStorage.h>
|
||||
#include <JpegToBmpConverter.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <Logging.h>
|
||||
#include <PngToBmpConverter.h>
|
||||
#include <ZipFile.h>
|
||||
|
||||
#include "Epub/parsers/ContainerParser.h"
|
||||
@@ -17,7 +18,7 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
||||
|
||||
// Get file size without loading it all into heap
|
||||
if (!getItemSize(containerPath, &containerSize)) {
|
||||
Serial.printf("[%lu] [EBP] Could not find or size META-INF/container.xml\n", millis());
|
||||
LOG_ERR("EBP", "Could not find or size META-INF/container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -29,13 +30,13 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
||||
|
||||
// Stream read (reusing your existing stream logic)
|
||||
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
|
||||
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
|
||||
LOG_ERR("EBP", "Could not read META-INF/container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract the result
|
||||
if (containerParser.fullPath.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
|
||||
LOG_ERR("EBP", "Could not find valid rootfile in container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -46,35 +47,84 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
||||
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());
|
||||
LOG_ERR("EBP", "Could not find content.opf in zip");
|
||||
return false;
|
||||
}
|
||||
|
||||
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str());
|
||||
LOG_DBG("EBP", "Parsing content.opf: %s", contentOpfFilePath.c_str());
|
||||
|
||||
size_t contentOpfSize;
|
||||
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
|
||||
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
|
||||
LOG_ERR("EBP", "Could not get size of content.opf");
|
||||
return false;
|
||||
}
|
||||
|
||||
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get());
|
||||
if (!opfParser.setup()) {
|
||||
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
||||
LOG_ERR("EBP", "Could not setup content.opf parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
||||
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
|
||||
LOG_ERR("EBP", "Could not read content.opf");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Grab data from opfParser into epub
|
||||
bookMetadata.title = opfParser.title;
|
||||
bookMetadata.author = opfParser.author;
|
||||
bookMetadata.language = opfParser.language;
|
||||
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
||||
|
||||
// Guide-based cover fallback: if no cover found via metadata/properties,
|
||||
// try extracting the image reference from the guide's cover page XHTML
|
||||
if (bookMetadata.coverItemHref.empty() && !opfParser.guideCoverPageHref.empty()) {
|
||||
LOG_DBG("EBP", "No cover from metadata, trying guide cover page: %s", opfParser.guideCoverPageHref.c_str());
|
||||
size_t coverPageSize;
|
||||
uint8_t* coverPageData = readItemContentsToBytes(opfParser.guideCoverPageHref, &coverPageSize, true);
|
||||
if (coverPageData) {
|
||||
const std::string coverPageHtml(reinterpret_cast<char*>(coverPageData), coverPageSize);
|
||||
free(coverPageData);
|
||||
|
||||
// Determine base path of the cover page for resolving relative image references
|
||||
std::string coverPageBase;
|
||||
const auto lastSlash = opfParser.guideCoverPageHref.rfind('/');
|
||||
if (lastSlash != std::string::npos) {
|
||||
coverPageBase = opfParser.guideCoverPageHref.substr(0, lastSlash + 1);
|
||||
}
|
||||
|
||||
// Search for image references: xlink:href="..." (SVG) and src="..." (img)
|
||||
std::string imageRef;
|
||||
for (const char* pattern : {"xlink:href=\"", "src=\""}) {
|
||||
auto pos = coverPageHtml.find(pattern);
|
||||
while (pos != std::string::npos) {
|
||||
pos += strlen(pattern);
|
||||
const auto endPos = coverPageHtml.find('"', pos);
|
||||
if (endPos != std::string::npos) {
|
||||
const auto ref = coverPageHtml.substr(pos, endPos - pos);
|
||||
// Check if it's an image file
|
||||
if (ref.length() >= 4) {
|
||||
const auto ext = ref.substr(ref.length() - 4);
|
||||
if (ext == ".png" || ext == ".jpg" || ext == "jpeg" || ext == ".gif") {
|
||||
imageRef = ref;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos = coverPageHtml.find(pattern, pos);
|
||||
}
|
||||
if (!imageRef.empty()) break;
|
||||
}
|
||||
|
||||
if (!imageRef.empty()) {
|
||||
bookMetadata.coverItemHref = FsHelpers::normalisePath(coverPageBase + imageRef);
|
||||
LOG_DBG("EBP", "Found cover image from guide: %s", bookMetadata.coverItemHref.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bookMetadata.textReferenceHref = opfParser.textReferenceHref;
|
||||
|
||||
if (!opfParser.tocNcxPath.empty()) {
|
||||
@@ -85,27 +135,31 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||
tocNavItem = opfParser.tocNavPath;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
|
||||
if (!opfParser.cssFiles.empty()) {
|
||||
cssFiles = opfParser.cssFiles;
|
||||
}
|
||||
|
||||
LOG_DBG("EBP", "Successfully parsed content.opf");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseTocNcxFile() const {
|
||||
// the ncx file should have been specified in the content.opf file
|
||||
if (tocNcxItem.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
|
||||
LOG_DBG("EBP", "No ncx file specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
|
||||
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
||||
|
||||
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
||||
FsFile tempNcxFile;
|
||||
if (!SdMan.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
||||
tempNcxFile.close();
|
||||
if (!SdMan.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
return false;
|
||||
}
|
||||
const auto ncxSize = tempNcxFile.size();
|
||||
@@ -113,14 +167,14 @@ bool Epub::parseTocNcxFile() const {
|
||||
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||
|
||||
if (!ncxParser.setup()) {
|
||||
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
||||
LOG_ERR("EBP", "Could not setup toc ncx parser");
|
||||
tempNcxFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!ncxBuffer) {
|
||||
Serial.printf("[%lu] [EBP] Could not allocate memory for toc ncx parser\n", millis());
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc ncx parser");
|
||||
tempNcxFile.close();
|
||||
return false;
|
||||
}
|
||||
@@ -131,7 +185,7 @@ bool Epub::parseTocNcxFile() const {
|
||||
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
||||
|
||||
if (processedSize != readSize) {
|
||||
Serial.printf("[%lu] [EBP] Could not process all toc ncx data\n", millis());
|
||||
LOG_ERR("EBP", "Could not process all toc ncx data");
|
||||
free(ncxBuffer);
|
||||
tempNcxFile.close();
|
||||
return false;
|
||||
@@ -140,43 +194,46 @@ bool Epub::parseTocNcxFile() const {
|
||||
|
||||
free(ncxBuffer);
|
||||
tempNcxFile.close();
|
||||
SdMan.remove(tmpNcxPath.c_str());
|
||||
Storage.remove(tmpNcxPath.c_str());
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsed TOC items\n", millis());
|
||||
LOG_DBG("EBP", "Parsed TOC items");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseTocNavFile() const {
|
||||
// the nav file should have been specified in the content.opf file (EPUB 3)
|
||||
if (tocNavItem.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No nav file specified\n", millis());
|
||||
LOG_DBG("EBP", "No nav file specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsing toc nav file: %s\n", millis(), tocNavItem.c_str());
|
||||
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
||||
|
||||
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
||||
FsFile tempNavFile;
|
||||
if (!SdMan.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
||||
if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
||||
tempNavFile.close();
|
||||
if (!SdMan.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
||||
if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
||||
return false;
|
||||
}
|
||||
const auto navSize = tempNavFile.size();
|
||||
|
||||
TocNavParser navParser(contentBasePath, navSize, bookMetadataCache.get());
|
||||
// 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());
|
||||
LOG_ERR("EBP", "Could not setup toc nav parser");
|
||||
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());
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc nav parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -185,7 +242,7 @@ bool Epub::parseTocNavFile() const {
|
||||
const auto processedSize = navParser.write(navBuffer, readSize);
|
||||
|
||||
if (processedSize != readSize) {
|
||||
Serial.printf("[%lu] [EBP] Could not process all toc nav data\n", millis());
|
||||
LOG_ERR("EBP", "Could not process all toc nav data");
|
||||
free(navBuffer);
|
||||
tempNavFile.close();
|
||||
return false;
|
||||
@@ -194,22 +251,115 @@ bool Epub::parseTocNavFile() const {
|
||||
|
||||
free(navBuffer);
|
||||
tempNavFile.close();
|
||||
SdMan.remove(tmpNavPath.c_str());
|
||||
Storage.remove(tmpNavPath.c_str());
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsed TOC nav items\n", millis());
|
||||
LOG_DBG("EBP", "Parsed TOC nav items");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Epub::parseCssFiles() const {
|
||||
// Maximum CSS file size we'll attempt to parse (uncompressed)
|
||||
// Larger files risk memory exhaustion on ESP32
|
||||
constexpr size_t MAX_CSS_FILE_SIZE = 128 * 1024; // 128KB
|
||||
// Minimum heap required before attempting CSS parsing
|
||||
constexpr size_t MIN_HEAP_FOR_CSS_PARSING = 64 * 1024; // 64KB
|
||||
|
||||
if (cssFiles.empty()) {
|
||||
LOG_DBG("EBP", "No CSS files to parse, but CssParser created for inline styles");
|
||||
}
|
||||
|
||||
LOG_DBG("EBP", "CSS files to parse: %zu", cssFiles.size());
|
||||
|
||||
// See if we have a cached version of the CSS rules
|
||||
if (cssParser->hasCache()) {
|
||||
LOG_DBG("EBP", "CSS cache exists, skipping parseCssFiles");
|
||||
return;
|
||||
}
|
||||
|
||||
// No cache yet - parse CSS files
|
||||
for (const auto& cssPath : cssFiles) {
|
||||
LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str());
|
||||
|
||||
// Check heap before parsing - CSS parsing allocates heavily
|
||||
const uint32_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_HEAP_FOR_CSS_PARSING) {
|
||||
LOG_ERR("EBP", "Insufficient heap for CSS parsing (%u bytes free, need %zu), skipping: %s", freeHeap,
|
||||
MIN_HEAP_FOR_CSS_PARSING, cssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check CSS file size before decompressing - skip files that are too large
|
||||
size_t cssFileSize = 0;
|
||||
if (getItemSize(cssPath, &cssFileSize)) {
|
||||
if (cssFileSize > MAX_CSS_FILE_SIZE) {
|
||||
LOG_ERR("EBP", "CSS file too large (%zu bytes > %zu max), skipping: %s", cssFileSize, MAX_CSS_FILE_SIZE,
|
||||
cssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract CSS file to temp location
|
||||
const auto tmpCssPath = getCachePath() + "/.tmp.css";
|
||||
FsFile tempCssFile;
|
||||
if (!Storage.openFileForWrite("EBP", tmpCssPath, tempCssFile)) {
|
||||
LOG_ERR("EBP", "Could not create temp CSS file");
|
||||
continue;
|
||||
}
|
||||
if (!readItemContentsToStream(cssPath, tempCssFile, 1024)) {
|
||||
LOG_ERR("EBP", "Could not read CSS file: %s", cssPath.c_str());
|
||||
tempCssFile.close();
|
||||
Storage.remove(tmpCssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
tempCssFile.close();
|
||||
|
||||
// Parse the CSS file
|
||||
if (!Storage.openFileForRead("EBP", tmpCssPath, tempCssFile)) {
|
||||
LOG_ERR("EBP", "Could not open temp CSS file for reading");
|
||||
Storage.remove(tmpCssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
cssParser->loadFromStream(tempCssFile);
|
||||
tempCssFile.close();
|
||||
Storage.remove(tmpCssPath.c_str());
|
||||
}
|
||||
|
||||
// Save to cache for next time
|
||||
if (!cssParser->saveToCache()) {
|
||||
LOG_ERR("EBP", "Failed to save CSS rules to cache");
|
||||
}
|
||||
cssParser->clear();
|
||||
|
||||
LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size());
|
||||
}
|
||||
|
||||
// load in the meta data for the epub file
|
||||
bool Epub::load(const bool buildIfMissing) {
|
||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
||||
bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||
LOG_DBG("EBP", "Loading ePub: %s", filepath.c_str());
|
||||
|
||||
// Initialize spine/TOC cache
|
||||
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||
// Always create CssParser - needed for inline style parsing even without CSS files
|
||||
cssParser.reset(new CssParser(cachePath));
|
||||
|
||||
// Try to load existing cache first
|
||||
if (bookMetadataCache->load()) {
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
if (!skipLoadingCss) {
|
||||
// Rebuild CSS cache when missing or when cache version changed (loadFromCache removes stale file)
|
||||
if (!cssParser->hasCache() || !cssParser->loadFromCache()) {
|
||||
LOG_DBG("EBP", "CSS rules cache missing or stale, attempting to parse CSS files");
|
||||
cssParser->deleteCache();
|
||||
|
||||
if (!parseContentOpf(bookMetadataCache->coreMetadata)) {
|
||||
LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files");
|
||||
// continue anyway - book will work without CSS and we'll still load any inline style CSS
|
||||
}
|
||||
parseCssFiles();
|
||||
// Invalidate section caches so they are rebuilt with the new CSS
|
||||
Storage.removeDir((cachePath + "/sections").c_str());
|
||||
}
|
||||
}
|
||||
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -219,33 +369,38 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
}
|
||||
|
||||
// Cache doesn't exist or is invalid, build it
|
||||
Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis());
|
||||
LOG_DBG("EBP", "Cache not found, building spine/TOC cache");
|
||||
setupCacheDir();
|
||||
|
||||
const uint32_t indexingStart = millis();
|
||||
|
||||
// Begin building cache - stream entries to disk immediately
|
||||
if (!bookMetadataCache->beginWrite()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
|
||||
LOG_ERR("EBP", "Could not begin writing cache");
|
||||
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());
|
||||
LOG_ERR("EBP", "Could not begin writing content.opf pass");
|
||||
return false;
|
||||
}
|
||||
if (!parseContentOpf(bookMetadata)) {
|
||||
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
||||
LOG_ERR("EBP", "Could not parse content.opf");
|
||||
return false;
|
||||
}
|
||||
if (!bookMetadataCache->endContentOpfPass()) {
|
||||
Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis());
|
||||
LOG_ERR("EBP", "Could not end writing content.opf pass");
|
||||
return false;
|
||||
}
|
||||
LOG_DBG("EBP", "OPF pass completed in %lu ms", millis() - opfStart);
|
||||
|
||||
// TOC Pass - try EPUB 3 nav first, fall back to NCX
|
||||
const uint32_t tocStart = millis();
|
||||
if (!bookMetadataCache->beginTocPass()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis());
|
||||
LOG_ERR("EBP", "Could not begin writing toc pass");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -253,74 +408,84 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
|
||||
// Try EPUB 3 nav document first (preferred)
|
||||
if (!tocNavItem.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Attempting to parse EPUB 3 nav document\n", millis());
|
||||
LOG_DBG("EBP", "Attempting to parse EPUB 3 nav document");
|
||||
tocParsed = parseTocNavFile();
|
||||
}
|
||||
|
||||
// Fall back to NCX if nav parsing failed or wasn't available
|
||||
if (!tocParsed && !tocNcxItem.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Falling back to NCX TOC\n", millis());
|
||||
LOG_DBG("EBP", "Falling back to NCX TOC");
|
||||
tocParsed = parseTocNcxFile();
|
||||
}
|
||||
|
||||
if (!tocParsed) {
|
||||
Serial.printf("[%lu] [EBP] Warning: Could not parse any TOC format\n", millis());
|
||||
LOG_ERR("EBP", "Warning: Could not parse any TOC format");
|
||||
// Continue anyway - book will work without TOC
|
||||
}
|
||||
|
||||
if (!bookMetadataCache->endTocPass()) {
|
||||
Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis());
|
||||
LOG_ERR("EBP", "Could not end writing toc pass");
|
||||
return false;
|
||||
}
|
||||
LOG_DBG("EBP", "TOC pass completed in %lu ms", millis() - tocStart);
|
||||
|
||||
// Close the cache files
|
||||
if (!bookMetadataCache->endWrite()) {
|
||||
Serial.printf("[%lu] [EBP] Could not end writing cache\n", millis());
|
||||
LOG_ERR("EBP", "Could not end writing cache");
|
||||
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());
|
||||
LOG_ERR("EBP", "Could not update mappings and sizes");
|
||||
return false;
|
||||
}
|
||||
LOG_DBG("EBP", "buildBookBin completed in %lu ms", millis() - buildStart);
|
||||
LOG_DBG("EBP", "Total indexing completed in %lu ms", millis() - indexingStart);
|
||||
|
||||
if (!bookMetadataCache->cleanupTmpFiles()) {
|
||||
Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis());
|
||||
LOG_DBG("EBP", "Could not cleanup tmp files - ignoring");
|
||||
}
|
||||
|
||||
// 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());
|
||||
LOG_ERR("EBP", "Failed to reload cache after writing");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
if (!skipLoadingCss) {
|
||||
// Parse CSS files after cache reload
|
||||
parseCssFiles();
|
||||
Storage.removeDir((cachePath + "/sections").c_str());
|
||||
}
|
||||
|
||||
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::clearCache() const {
|
||||
if (!SdMan.exists(cachePath.c_str())) {
|
||||
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
|
||||
if (!Storage.exists(cachePath.c_str())) {
|
||||
LOG_DBG("EPB", "Cache does not exist, no action needed");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SdMan.removeDir(cachePath.c_str())) {
|
||||
Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis());
|
||||
if (!Storage.removeDir(cachePath.c_str())) {
|
||||
LOG_ERR("EPB", "Failed to clear cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EPB] Cache cleared successfully\n", millis());
|
||||
LOG_DBG("EPB", "Cache cleared successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Epub::setupCacheDir() const {
|
||||
if (SdMan.exists(cachePath.c_str())) {
|
||||
if (Storage.exists(cachePath.c_str())) {
|
||||
return;
|
||||
}
|
||||
|
||||
SdMan.mkdir(cachePath.c_str());
|
||||
Storage.mkdir(cachePath.c_str());
|
||||
}
|
||||
|
||||
const std::string& Epub::getCachePath() const { return cachePath; }
|
||||
@@ -345,67 +510,210 @@ const std::string& Epub::getAuthor() const {
|
||||
return bookMetadataCache->coreMetadata.author;
|
||||
}
|
||||
|
||||
std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
||||
const std::string& Epub::getLanguage() const {
|
||||
static std::string blank;
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
return blank;
|
||||
}
|
||||
|
||||
bool Epub::generateCoverBmp() const {
|
||||
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().c_str())) {
|
||||
if (Storage.exists(getCoverBmpPath(cropped).c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] Cannot generate cover BMP, cache not loaded\n", millis());
|
||||
LOG_ERR("EBP", "Cannot generate cover BMP, cache not loaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
||||
LOG_ERR("EBP", "No known cover image");
|
||||
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\n", millis());
|
||||
LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
FsFile coverJpg;
|
||||
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
coverJpg.close();
|
||||
|
||||
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile coverBmp;
|
||||
if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(), coverBmp)) {
|
||||
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp);
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
|
||||
coverJpg.close();
|
||||
coverBmp.close();
|
||||
SdMan.remove(coverJpgTempPath.c_str());
|
||||
Storage.remove(coverJpgTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis());
|
||||
SdMan.remove(getCoverBmpPath().c_str());
|
||||
LOG_ERR("EBP", "Failed to generate BMP from cover image");
|
||||
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no");
|
||||
LOG_DBG("EBP", "Generated BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else {
|
||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping\n", millis());
|
||||
}
|
||||
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
|
||||
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||
|
||||
FsFile coverPng;
|
||||
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||
coverPng.close();
|
||||
|
||||
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile coverBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||
coverPng.close();
|
||||
return false;
|
||||
}
|
||||
const bool success = PngToBmpConverter::pngFileToBmpStream(coverPng, coverBmp, cropped);
|
||||
coverPng.close();
|
||||
coverBmp.close();
|
||||
Storage.remove(coverPngTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
LOG_ERR("EBP", "Failed to generate BMP from PNG cover image");
|
||||
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||
}
|
||||
LOG_DBG("EBP", "Generated BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
}
|
||||
|
||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }
|
||||
std::string Epub::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
||||
|
||||
bool Epub::generateThumbBmp(int height) const {
|
||||
// Already generated, return true
|
||||
if (Storage.exists(getThumbBmpPath(height).c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
LOG_ERR("EBP", "Cannot generate thumb BMP, cache not loaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
LOG_DBG("EBP", "No known cover image for thumbnail");
|
||||
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
LOG_DBG("EBP", "Generating thumb BMP from JPG cover image");
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
FsFile coverJpg;
|
||||
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
coverJpg.close();
|
||||
|
||||
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile thumbBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), 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)
|
||||
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||
int THUMB_TARGET_HEIGHT = height;
|
||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
|
||||
THUMB_TARGET_HEIGHT);
|
||||
coverJpg.close();
|
||||
thumbBmp.close();
|
||||
Storage.remove(coverJpgTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
LOG_ERR("EBP", "Failed to generate thumb BMP from JPG cover image");
|
||||
Storage.remove(getThumbBmpPath(height).c_str());
|
||||
}
|
||||
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
|
||||
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
|
||||
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||
|
||||
FsFile coverPng;
|
||||
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||
coverPng.close();
|
||||
|
||||
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile thumbBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||
coverPng.close();
|
||||
return false;
|
||||
}
|
||||
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||
int THUMB_TARGET_HEIGHT = height;
|
||||
const bool success =
|
||||
PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT);
|
||||
coverPng.close();
|
||||
thumbBmp.close();
|
||||
Storage.remove(coverPngTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
LOG_ERR("EBP", "Failed to generate thumb BMP from PNG cover image");
|
||||
Storage.remove(getThumbBmpPath(height).c_str());
|
||||
}
|
||||
LOG_DBG("EBP", "Generated thumb BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else {
|
||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
||||
}
|
||||
|
||||
// Write an empty bmp file to avoid generation attempts in the future
|
||||
FsFile thumbBmp;
|
||||
Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
|
||||
thumbBmp.close();
|
||||
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());
|
||||
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -413,7 +721,7 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
|
||||
|
||||
const auto content = ZipFile(filepath).readFileToMemory(path.c_str(), size, trailingNullByte);
|
||||
if (!content) {
|
||||
Serial.printf("[%lu] [EBP] Failed to read item %s\n", millis(), path.c_str());
|
||||
LOG_DBG("EBP", "Failed to read item %s", path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -422,7 +730,7 @@ 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 {
|
||||
if (itemHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
|
||||
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -446,12 +754,12 @@ size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return get
|
||||
|
||||
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineItem called but cache not loaded\n", millis());
|
||||
LOG_ERR("EBP", "getSpineItem called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
||||
LOG_ERR("EBP", "getSpineItem index:%d is out of range", spineIndex);
|
||||
return bookMetadataCache->getSpineEntry(0);
|
||||
}
|
||||
|
||||
@@ -460,12 +768,12 @@ BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||
|
||||
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getTocItem called but cache not loaded\n", millis());
|
||||
LOG_DBG("EBP", "getTocItem called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex);
|
||||
LOG_DBG("EBP", "getTocItem index:%d is out of range", tocIndex);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -483,18 +791,18 @@ int Epub::getTocItemsCount() const {
|
||||
// work out the section index for a toc index
|
||||
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex called but cache not loaded\n", millis());
|
||||
LOG_ERR("EBP", "getSpineIndexForTocIndex called but cache not loaded");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
|
||||
LOG_ERR("EBP", "getSpineIndexForTocIndex: tocIndex %d out of range", 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);
|
||||
LOG_DBG("EBP", "Section not found for TOC index %d", tocIndex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -512,14 +820,13 @@ size_t Epub::getBookSize() const {
|
||||
|
||||
int Epub::getSpineIndexForTextReference() const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineIndexForTextReference called but cache not loaded\n", millis());
|
||||
LOG_ERR("EBP", "getSpineIndexForTextReference called but cache not loaded");
|
||||
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());
|
||||
LOG_DBG("EBP", "Core Metadata: cover(%d)=%s, textReference(%d)=%s",
|
||||
bookMetadataCache->coreMetadata.coverItemHref.size(), bookMetadataCache->coreMetadata.coverItemHref.c_str(),
|
||||
bookMetadataCache->coreMetadata.textReferenceHref.size(),
|
||||
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
|
||||
|
||||
if (bookMetadataCache->coreMetadata.textReferenceHref.empty()) {
|
||||
// there was no textReference in epub, so we return 0 (the first chapter)
|
||||
@@ -529,24 +836,25 @@ int Epub::getSpineIndexForTextReference() const {
|
||||
// loop through spine items to get the correct index matching the text href
|
||||
for (size_t i = 0; i < getSpineItemsCount(); i++) {
|
||||
if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) {
|
||||
Serial.printf("[%lu] [ERS] Text reference %s found at index %d\n", millis(),
|
||||
bookMetadataCache->coreMetadata.textReferenceHref.c_str(), i);
|
||||
LOG_DBG("EBP", "Text reference %s found at index %d", bookMetadataCache->coreMetadata.textReferenceHref.c_str(),
|
||||
i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// This should not happen, as we checked for empty textReferenceHref earlier
|
||||
Serial.printf("[%lu] [EBP] Section not found for text reference\n", millis());
|
||||
LOG_DBG("EBP", "Section not found for text reference");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate progress in book
|
||||
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
|
||||
// Calculate progress in book (returns 0.0-1.0)
|
||||
float Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
|
||||
const size_t bookSize = getBookSize();
|
||||
if (bookSize == 0) {
|
||||
return 0;
|
||||
return 0.0f;
|
||||
}
|
||||
const size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
|
||||
const size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
|
||||
const size_t sectionProgSize = currentSpineRead * curChapterSize;
|
||||
return round(static_cast<float>(prevChapterSize + sectionProgSize) / bookSize * 100.0);
|
||||
const float sectionProgSize = currentSpineRead * static_cast<float>(curChapterSize);
|
||||
const float totalProgress = static_cast<float>(prevChapterSize) + sectionProgSize;
|
||||
return totalProgress / static_cast<float>(bookSize);
|
||||
}
|
||||
|
||||
+15
-4
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "Epub/BookMetadataCache.h"
|
||||
#include "Epub/css/CssParser.h"
|
||||
|
||||
class ZipFile;
|
||||
|
||||
@@ -24,11 +25,16 @@ class Epub {
|
||||
std::string cachePath;
|
||||
// Spine and TOC cache
|
||||
std::unique_ptr<BookMetadataCache> bookMetadataCache;
|
||||
// CSS parser for styling
|
||||
std::unique_ptr<CssParser> cssParser;
|
||||
// CSS files
|
||||
std::vector<std::string> cssFiles;
|
||||
|
||||
bool findContentOpfFile(std::string* contentOpfFile) const;
|
||||
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
|
||||
bool parseTocNcxFile() const;
|
||||
bool parseTocNavFile() const;
|
||||
void parseCssFiles() const;
|
||||
|
||||
public:
|
||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||
@@ -37,15 +43,19 @@ class Epub {
|
||||
}
|
||||
~Epub() = default;
|
||||
std::string& getBasePath() { return contentBasePath; }
|
||||
bool load(bool buildIfMissing = true);
|
||||
bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
|
||||
bool clearCache() const;
|
||||
void setupCacheDir() const;
|
||||
const std::string& getCachePath() const;
|
||||
const std::string& getPath() const;
|
||||
const std::string& getTitle() const;
|
||||
const std::string& getAuthor() const;
|
||||
std::string getCoverBmpPath() const;
|
||||
bool generateCoverBmp() const;
|
||||
const std::string& getLanguage() const;
|
||||
std::string getCoverBmpPath(bool cropped = false) const;
|
||||
bool generateCoverBmp(bool cropped = false) const;
|
||||
std::string getThumbBmpPath() const;
|
||||
std::string getThumbBmpPath(int height) const;
|
||||
bool generateThumbBmp(int height) const;
|
||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||
bool trailingNullByte = false) const;
|
||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||
@@ -60,5 +70,6 @@ class Epub {
|
||||
int getSpineIndexForTextReference() const;
|
||||
|
||||
size_t getBookSize() const;
|
||||
uint8_t calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
||||
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
||||
CssParser* getCssParser() const { return cssParser.get(); }
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "BookMetadataCache.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
#include <ZipFile.h>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "FsHelpers.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t BOOK_CACHE_VERSION = 3;
|
||||
constexpr uint8_t BOOK_CACHE_VERSION = 5;
|
||||
constexpr char bookBinFile[] = "/book.bin";
|
||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||
@@ -21,15 +21,15 @@ bool BookMetadataCache::beginWrite() {
|
||||
buildMode = true;
|
||||
spineCount = 0;
|
||||
tocCount = 0;
|
||||
Serial.printf("[%lu] [BMC] Entering write mode\n", millis());
|
||||
LOG_DBG("BMC", "Entering write mode");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::beginContentOpfPass() {
|
||||
Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis());
|
||||
LOG_DBG("BMC", "Beginning content opf pass");
|
||||
|
||||
// Open spine file for writing
|
||||
return SdMan.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||
return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endContentOpfPass() {
|
||||
@@ -38,48 +38,76 @@ bool BookMetadataCache::endContentOpfPass() {
|
||||
}
|
||||
|
||||
bool BookMetadataCache::beginTocPass() {
|
||||
Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis());
|
||||
LOG_DBG("BMC", "Beginning toc pass");
|
||||
|
||||
// Open spine file for reading
|
||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
return false;
|
||||
}
|
||||
if (!SdMan.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||
if (!Storage.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;
|
||||
LOG_DBG("BMC", "Using fast index for %d spine items", 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());
|
||||
LOG_DBG("BMC", "endWrite called but not in build mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
buildMode = false;
|
||||
Serial.printf("[%lu] [BMC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||
LOG_DBG("BMC", "Wrote %d spine, %d TOC entries", spineCount, tocCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
|
||||
// Open all three files, writing to meta, reading from spine and toc
|
||||
if (!SdMan.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
||||
if (!Storage.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
bookFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||
if (!Storage.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
return false;
|
||||
@@ -87,8 +115,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
|
||||
constexpr uint32_t headerASize =
|
||||
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.coverItemHref.size() +
|
||||
metadata.textReferenceHref.size() + sizeof(uint32_t) * 4;
|
||||
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;
|
||||
|
||||
@@ -100,6 +129,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
// 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);
|
||||
|
||||
@@ -122,61 +152,106 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
// 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());
|
||||
LOG_ERR("BMC", "Could not open EPUB zip for size calculations");
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
return false;
|
||||
}
|
||||
// TODO: For large ZIPs loading the all localHeaderOffsets will crash.
|
||||
// However not having them loaded is extremely slow. Need a better solution here.
|
||||
// Perhaps only a cache of spine items or a better way to speedup lookups?
|
||||
if (!zip.loadAllFileStatSlims()) {
|
||||
Serial.printf("[%lu] [BMC] Could not load zip local header offsets for size calculations\n", millis());
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
zip.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) {
|
||||
LOG_DBG("BMC", "Using batch size lookup for %d spine items", 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);
|
||||
LOG_DBG("BMC", "Batch lookup matched %d/%d spine items", 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);
|
||||
|
||||
tocFile.seek(0);
|
||||
for (int j = 0; j < tocCount; j++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
if (tocEntry.spineIndex == i) {
|
||||
spineEntry.tocIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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());
|
||||
LOG_DBG("BMC", "Warning: Could not find TOC entry for spine item %d: %s, using title from last section", i,
|
||||
spineEntry.href.c_str());
|
||||
spineEntry.tocIndex = lastSpineTocIndex;
|
||||
}
|
||||
lastSpineTocIndex = spineEntry.tocIndex;
|
||||
|
||||
// Calculate size for cumulative size
|
||||
size_t itemSize = 0;
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
cumSize += itemSize;
|
||||
spineEntry.cumulativeSize = cumSize;
|
||||
if (useBatchSizes) {
|
||||
itemSize = spineSizes[i];
|
||||
if (itemSize == 0) {
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
cumSize += itemSize;
|
||||
spineEntry.cumulativeSize = cumSize;
|
||||
|
||||
// Write out spine data to book.bin
|
||||
writeSpineEntry(bookFile, spineEntry);
|
||||
}
|
||||
@@ -194,16 +269,16 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
|
||||
Serial.printf("[%lu] [BMC] Successfully built book.bin\n", millis());
|
||||
LOG_DBG("BMC", "Successfully built book.bin");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::cleanupTmpFiles() const {
|
||||
if (SdMan.exists((cachePath + tmpSpineBinFile).c_str())) {
|
||||
SdMan.remove((cachePath + tmpSpineBinFile).c_str());
|
||||
if (Storage.exists((cachePath + tmpSpineBinFile).c_str())) {
|
||||
Storage.remove((cachePath + tmpSpineBinFile).c_str());
|
||||
}
|
||||
if (SdMan.exists((cachePath + tmpTocBinFile).c_str())) {
|
||||
SdMan.remove((cachePath + tmpTocBinFile).c_str());
|
||||
if (Storage.exists((cachePath + tmpTocBinFile).c_str())) {
|
||||
Storage.remove((cachePath + tmpTocBinFile).c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -230,7 +305,7 @@ uint32_t BookMetadataCache::writeTocEntry(FsFile& file, const TocEntry& entry) c
|
||||
// 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());
|
||||
LOG_DBG("BMC", "createSpineEntry called but not in build mode");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,25 +317,42 @@ void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||
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());
|
||||
LOG_DBG("BMC", "createTocEntry called but not in build mode");
|
||||
return;
|
||||
}
|
||||
|
||||
int spineIndex = -1;
|
||||
// find spine index
|
||||
// TODO: This lookup is slow as need to scan through all items each time. We can't hold it all in memory due to size.
|
||||
// But perhaps we can load just the hrefs in a vector/list to do an index lookup?
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
if (spineEntry.href == href) {
|
||||
spineIndex = i;
|
||||
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] addTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||
if (spineIndex == -1) {
|
||||
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
|
||||
}
|
||||
} else {
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
if (spineEntry.href == href) {
|
||||
spineIndex = static_cast<int16_t>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (spineIndex == -1) {
|
||||
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
const TocEntry entry(title, href, anchor, level, spineIndex);
|
||||
@@ -271,14 +363,14 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
||||
/* ============= READING / LOADING FUNCTIONS ================ */
|
||||
|
||||
bool BookMetadataCache::load() {
|
||||
if (!SdMan.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||
if (!Storage.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);
|
||||
LOG_DBG("BMC", "Cache version mismatch: expected %d, got %d", BOOK_CACHE_VERSION, version);
|
||||
bookFile.close();
|
||||
return false;
|
||||
}
|
||||
@@ -289,22 +381,23 @@ bool BookMetadataCache::load() {
|
||||
|
||||
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);
|
||||
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", 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());
|
||||
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
||||
Serial.printf("[%lu] [BMC] getSpineEntry index %d out of range\n", millis(), index);
|
||||
LOG_ERR("BMC", "getSpineEntry index %d out of range", index);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -318,12 +411,12 @@ BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index)
|
||||
|
||||
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||
if (!loaded) {
|
||||
Serial.printf("[%lu] [BMC] getTocEntry called but cache not loaded\n", millis());
|
||||
LOG_ERR("BMC", "getTocEntry called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
||||
Serial.printf("[%lu] [BMC] getTocEntry index %d out of range\n", millis(), index);
|
||||
LOG_ERR("BMC", "getTocEntry index %d out of range", index);
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDCardManager.h>
|
||||
#include <HalStorage.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;
|
||||
};
|
||||
@@ -52,6 +55,27 @@ class BookMetadataCache {
|
||||
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;
|
||||
|
||||
+31
-4
@@ -1,6 +1,6 @@
|
||||
#include "Page.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
@@ -25,6 +25,29 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
|
||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||
}
|
||||
|
||||
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
// Images don't use fontId or text rendering
|
||||
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||
}
|
||||
|
||||
bool PageImage::serialize(FsFile& file) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
|
||||
// serialize ImageBlock
|
||||
return imageBlock->serialize(file);
|
||||
}
|
||||
|
||||
std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
|
||||
int16_t xPos;
|
||||
int16_t yPos;
|
||||
serialization::readPod(file, xPos);
|
||||
serialization::readPod(file, yPos);
|
||||
|
||||
auto ib = ImageBlock::deserialize(file);
|
||||
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
|
||||
}
|
||||
|
||||
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||
for (auto& element : elements) {
|
||||
element->render(renderer, fontId, xOffset, yOffset);
|
||||
@@ -36,8 +59,9 @@ bool Page::serialize(FsFile& file) const {
|
||||
serialization::writePod(file, count);
|
||||
|
||||
for (const auto& el : elements) {
|
||||
// Only PageLine exists currently
|
||||
serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine));
|
||||
// Use getTag() method to determine type
|
||||
serialization::writePod(file, static_cast<uint8_t>(el->getTag()));
|
||||
|
||||
if (!el->serialize(file)) {
|
||||
return false;
|
||||
}
|
||||
@@ -59,8 +83,11 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
||||
if (tag == TAG_PageLine) {
|
||||
auto pl = PageLine::deserialize(file);
|
||||
page->elements.push_back(std::move(pl));
|
||||
} else if (tag == TAG_PageImage) {
|
||||
auto pi = PageImage::deserialize(file);
|
||||
page->elements.push_back(std::move(pi));
|
||||
} else {
|
||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
||||
LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
+54
-1
@@ -1,13 +1,16 @@
|
||||
#pragma once
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "blocks/ImageBlock.h"
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
enum PageElementTag : uint8_t {
|
||||
TAG_PageLine = 1,
|
||||
TAG_PageImage = 2, // New tag
|
||||
};
|
||||
|
||||
// represents something that has been added to a page
|
||||
@@ -19,6 +22,7 @@ class PageElement {
|
||||
virtual ~PageElement() = default;
|
||||
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||||
virtual bool serialize(FsFile& file) = 0;
|
||||
virtual PageElementTag getTag() const = 0; // Add type identification
|
||||
};
|
||||
|
||||
// a line from a block element
|
||||
@@ -30,9 +34,24 @@ class PageLine final : public PageElement {
|
||||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(FsFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageLine; }
|
||||
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
||||
};
|
||||
|
||||
// New PageImage class
|
||||
class PageImage final : public PageElement {
|
||||
std::shared_ptr<ImageBlock> imageBlock;
|
||||
|
||||
public:
|
||||
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(FsFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||
static std::unique_ptr<PageImage> deserialize(FsFile& file);
|
||||
const ImageBlock& getImageBlock() const { return *imageBlock; }
|
||||
};
|
||||
|
||||
class Page {
|
||||
public:
|
||||
// the list of block index and line numbers on this page
|
||||
@@ -40,4 +59,38 @@ class Page {
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
bool serialize(FsFile& file) const;
|
||||
static std::unique_ptr<Page> deserialize(FsFile& file);
|
||||
|
||||
// Check if page contains any images (used to force full refresh)
|
||||
bool hasImages() const {
|
||||
return std::any_of(elements.begin(), elements.end(),
|
||||
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||||
}
|
||||
|
||||
// Get bounding box of all images on the page (union of image rects)
|
||||
// Returns false if no images. Coordinates are relative to page origin.
|
||||
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
|
||||
bool found = false;
|
||||
int16_t minX = INT16_MAX, minY = INT16_MAX, maxX = INT16_MIN, maxY = INT16_MIN;
|
||||
for (const auto& el : elements) {
|
||||
if (el->getTag() == TAG_PageImage) {
|
||||
const auto& img = static_cast<const PageImage&>(*el);
|
||||
int16_t x = img.xPos;
|
||||
int16_t y = img.yPos;
|
||||
int16_t right = x + img.getImageBlock().getWidth();
|
||||
int16_t bottom = y + img.getImageBlock().getHeight();
|
||||
minX = std::min(minX, x);
|
||||
minY = std::min(minY, y);
|
||||
maxX = std::max(maxX, right);
|
||||
maxY = std::max(maxY, bottom);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
outX = minX;
|
||||
outY = minY;
|
||||
outW = maxX - minX;
|
||||
outH = maxY - minY;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
};
|
||||
|
||||
+352
-38
@@ -5,16 +5,66 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
#include "hyphenation/Hyphenator.h"
|
||||
|
||||
constexpr int MAX_COST = std::numeric_limits<int>::max();
|
||||
|
||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style 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 advance width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
|
||||
// Uses advance width (sum of glyph advances) rather than bounding box width so that italic glyph overhangs
|
||||
// don't inflate inter-word spacing.
|
||||
uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
|
||||
const EpdFontFamily::Style style, const bool appendHyphen = false) {
|
||||
if (word.size() == 1 && word[0] == ' ' && !appendHyphen) {
|
||||
return renderer.getSpaceWidth(fontId, style);
|
||||
}
|
||||
const bool hasSoftHyphen = containsSoftHyphen(word);
|
||||
if (!hasSoftHyphen && !appendHyphen) {
|
||||
return renderer.getTextAdvanceX(fontId, word.c_str(), style);
|
||||
}
|
||||
|
||||
std::string sanitized = word;
|
||||
if (hasSoftHyphen) {
|
||||
stripSoftHyphensInPlace(sanitized);
|
||||
}
|
||||
if (appendHyphen) {
|
||||
sanitized.push_back('-');
|
||||
}
|
||||
return renderer.getTextAdvanceX(fontId, sanitized.c_str(), style);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
|
||||
const bool attachToPrevious) {
|
||||
if (word.empty()) return;
|
||||
|
||||
words.push_back(std::move(word));
|
||||
wordStyles.push_back(fontStyle);
|
||||
EpdFontFamily::Style combinedStyle = fontStyle;
|
||||
if (underline) {
|
||||
combinedStyle = static_cast<EpdFontFamily::Style>(combinedStyle | EpdFontFamily::UNDERLINE);
|
||||
}
|
||||
wordStyles.push_back(combinedStyle);
|
||||
wordContinues.push_back(attachToPrevious);
|
||||
}
|
||||
|
||||
// Consumes data to minimize memory usage
|
||||
@@ -25,14 +75,27 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply fixed transforms before any per-line layout work.
|
||||
applyParagraphIndent();
|
||||
|
||||
const int pageWidth = viewportWidth;
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||
const auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
const auto lineBreakIndices = computeLineBreaks(pageWidth, spaceWidth, wordWidths);
|
||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
|
||||
// Build indexed continues vector from the parallel list for O(1) access during layout
|
||||
std::vector<bool> continuesVec(wordContinues.begin(), wordContinues.end());
|
||||
|
||||
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, continuesVec);
|
||||
} else {
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec);
|
||||
}
|
||||
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||
|
||||
for (size_t i = 0; i < lineCount; ++i) {
|
||||
extractLine(i, pageWidth, spaceWidth, wordWidths, lineBreakIndices, processLine);
|
||||
extractLine(i, pageWidth, spaceWidth, wordWidths, continuesVec, lineBreakIndices, processLine);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,17 +105,11 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
||||
std::vector<uint16_t> wordWidths;
|
||||
wordWidths.reserve(totalWordCount);
|
||||
|
||||
// add em-space at the beginning of first word in paragraph to indent
|
||||
if (!extraParagraphSpacing) {
|
||||
std::string& first_word = words.front();
|
||||
first_word.insert(0, "\xe2\x80\x83");
|
||||
}
|
||||
|
||||
auto wordsIt = words.begin();
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
|
||||
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(wordStylesIt, 1);
|
||||
@@ -61,8 +118,32 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
||||
return wordWidths;
|
||||
}
|
||||
|
||||
std::vector<size_t> ParsedText::computeLineBreaks(const int pageWidth, const int spaceWidth,
|
||||
const std::vector<uint16_t>& wordWidths) const {
|
||||
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
||||
const int spaceWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec) {
|
||||
if (words.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||
const int firstLineIndent =
|
||||
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? blockStyle.textIndent
|
||||
: 0;
|
||||
|
||||
// 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) {
|
||||
// First word needs to fit in reduced width if there's an indent
|
||||
const int effectiveWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
||||
while (wordWidths[i] > effectiveWidth) {
|
||||
if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true,
|
||||
&continuesVec)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const size_t totalWordCount = words.size();
|
||||
|
||||
// DP table to store the minimum badness (cost) of lines starting at index i
|
||||
@@ -75,22 +156,31 @@ std::vector<size_t> ParsedText::computeLineBreaks(const int pageWidth, const int
|
||||
ans[totalWordCount - 1] = totalWordCount - 1;
|
||||
|
||||
for (int i = totalWordCount - 2; i >= 0; --i) {
|
||||
int currlen = -spaceWidth;
|
||||
int currlen = 0;
|
||||
dp[i] = MAX_COST;
|
||||
|
||||
for (size_t j = i; j < totalWordCount; ++j) {
|
||||
// Current line length: previous width + space + current word width
|
||||
currlen += wordWidths[j] + spaceWidth;
|
||||
// First line has reduced width due to text-indent
|
||||
const int effectivePageWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth;
|
||||
|
||||
if (currlen > pageWidth) {
|
||||
for (size_t j = i; j < totalWordCount; ++j) {
|
||||
// Add space before word j, unless it's the first word on the line or a continuation
|
||||
const int gap = j > static_cast<size_t>(i) && !continuesVec[j] ? spaceWidth : 0;
|
||||
currlen += wordWidths[j] + gap;
|
||||
|
||||
if (currlen > effectivePageWidth) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Cannot break after word j if the next word attaches to it (continuation group)
|
||||
if (j + 1 < totalWordCount && continuesVec[j + 1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int cost;
|
||||
if (j == totalWordCount - 1) {
|
||||
cost = 0; // Last line
|
||||
} else {
|
||||
const int remainingSpace = pageWidth - currlen;
|
||||
const int remainingSpace = effectivePageWidth - currlen;
|
||||
// Use long long for the square to prevent overflow
|
||||
const long long cost_ll = static_cast<long long>(remainingSpace) * remainingSpace + dp[j + 1];
|
||||
|
||||
@@ -140,50 +230,263 @@ std::vector<size_t> ParsedText::computeLineBreaks(const int pageWidth, const int
|
||||
return lineBreakIndices;
|
||||
}
|
||||
|
||||
void ParsedText::applyParagraphIndent() {
|
||||
if (extraParagraphSpacing || words.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (blockStyle.textIndentDefined) {
|
||||
// CSS text-indent is explicitly set (even if 0) - don't use fallback EmSpace
|
||||
// The actual indent positioning is handled in extractLine()
|
||||
} else if (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) {
|
||||
// No CSS text-indent defined - use EmSpace fallback for visual indent
|
||||
words.front().insert(0, "\xe2\x80\x83");
|
||||
}
|
||||
}
|
||||
|
||||
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
||||
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
|
||||
const int pageWidth, const int spaceWidth,
|
||||
std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec) {
|
||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||
const int firstLineIndent =
|
||||
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? blockStyle.textIndent
|
||||
: 0;
|
||||
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
size_t currentIndex = 0;
|
||||
bool isFirstLine = true;
|
||||
|
||||
while (currentIndex < wordWidths.size()) {
|
||||
const size_t lineStart = currentIndex;
|
||||
int lineWidth = 0;
|
||||
|
||||
// First line has reduced width due to text-indent
|
||||
const int effectivePageWidth = isFirstLine ? pageWidth - firstLineIndent : pageWidth;
|
||||
|
||||
// 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 || continuesVec[currentIndex] ? 0 : spaceWidth;
|
||||
const int candidateWidth = spacing + wordWidths[currentIndex];
|
||||
|
||||
// Word fits on current line
|
||||
if (lineWidth + candidateWidth <= effectivePageWidth) {
|
||||
lineWidth += candidateWidth;
|
||||
++currentIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Word would overflow — try to split based on hyphenation points
|
||||
const int availableWidth = effectivePageWidth - lineWidth - spacing;
|
||||
const bool allowFallbackBreaks = isFirstWord; // Only for first word on line
|
||||
|
||||
if (availableWidth > 0 && hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths,
|
||||
allowFallbackBreaks, &continuesVec)) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Don't break before a continuation word (e.g., orphaned "?" after "question").
|
||||
// Backtrack to the start of the continuation group so the whole group moves to the next line.
|
||||
while (currentIndex > lineStart + 1 && currentIndex < wordWidths.size() && continuesVec[currentIndex]) {
|
||||
--currentIndex;
|
||||
}
|
||||
|
||||
lineBreakIndices.push_back(currentIndex);
|
||||
isFirstLine = false;
|
||||
}
|
||||
|
||||
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, const GfxRenderer& renderer,
|
||||
const int fontId, std::vector<uint16_t>& wordWidths,
|
||||
const bool allowFallbackBreaks, std::vector<bool>* continuesVec) {
|
||||
// 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 and continuation flag) directly after the prefix.
|
||||
auto insertWordIt = std::next(wordIt);
|
||||
auto insertStyleIt = std::next(styleIt);
|
||||
words.insert(insertWordIt, remainder);
|
||||
wordStyles.insert(insertStyleIt, style);
|
||||
|
||||
// Continuation flag handling after splitting a word into prefix + remainder.
|
||||
//
|
||||
// The prefix keeps the original word's continuation flag so that no-break-space groups
|
||||
// stay linked. The remainder always gets continues=false because it starts on the next
|
||||
// line and is not attached to the prefix.
|
||||
//
|
||||
// Example: "200 Quadratkilometer" produces tokens:
|
||||
// [0] "200" continues=false
|
||||
// [1] " " continues=true
|
||||
// [2] "Quadratkilometer" continues=true <-- the word being split
|
||||
//
|
||||
// After splitting "Quadratkilometer" at "Quadrat-" / "kilometer":
|
||||
// [0] "200" continues=false
|
||||
// [1] " " continues=true
|
||||
// [2] "Quadrat-" continues=true (KEPT — still attached to the no-break group)
|
||||
// [3] "kilometer" continues=false (NEW — starts fresh on the next line)
|
||||
//
|
||||
// This lets the backtracking loop keep the entire prefix group ("200 Quadrat-") on one
|
||||
// line, while "kilometer" moves to the next line.
|
||||
auto continuesIt = wordContinues.begin();
|
||||
std::advance(continuesIt, wordIndex);
|
||||
// *continuesIt is intentionally left unchanged — the prefix keeps its original attachment.
|
||||
const auto insertContinuesIt = std::next(continuesIt);
|
||||
wordContinues.insert(insertContinuesIt, false);
|
||||
|
||||
// Keep the indexed vector in sync if provided.
|
||||
if (continuesVec) {
|
||||
// (*continuesVec)[wordIndex] stays unchanged — prefix keeps its attachment.
|
||||
continuesVec->insert(continuesVec->begin() + wordIndex + 1, false);
|
||||
}
|
||||
|
||||
// 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::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
|
||||
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
|
||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||
const bool isFirstLine = breakIndex == 0;
|
||||
const int firstLineIndent =
|
||||
isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? blockStyle.textIndent
|
||||
: 0;
|
||||
|
||||
// Calculate total word width for this line and count actual word gaps
|
||||
// (continuation words attach to previous word with no gap)
|
||||
int lineWordWidthSum = 0;
|
||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||
lineWordWidthSum += wordWidths[i];
|
||||
size_t actualGapCount = 0;
|
||||
|
||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
|
||||
// Count gaps: each word after the first creates a gap, unless it's a continuation
|
||||
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
|
||||
actualGapCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate spacing
|
||||
const int spareSpace = pageWidth - lineWordWidthSum;
|
||||
// Calculate spacing (account for indent reducing effective page width on first line)
|
||||
const int effectivePageWidth = pageWidth - firstLineIndent;
|
||||
const int spareSpace = effectivePageWidth - lineWordWidthSum;
|
||||
|
||||
int spacing = spaceWidth;
|
||||
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
|
||||
|
||||
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
|
||||
spacing = spareSpace / (lineWordCount - 1);
|
||||
// For justified text, calculate spacing based on actual gap count
|
||||
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine && actualGapCount >= 1) {
|
||||
spacing = spareSpace / static_cast<int>(actualGapCount);
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Calculate initial x position (first line starts at indent for left/justified text)
|
||||
auto xpos = static_cast<uint16_t>(firstLineIndent);
|
||||
if (blockStyle.alignment == CssTextAlign::Right) {
|
||||
xpos = spareSpace - static_cast<int>(actualGapCount) * spaceWidth;
|
||||
} else if (blockStyle.alignment == CssTextAlign::Center) {
|
||||
xpos = (spareSpace - static_cast<int>(actualGapCount) * spaceWidth) / 2;
|
||||
}
|
||||
|
||||
// Pre-calculate X positions for words
|
||||
// Continuation words attach to the previous word with no space before them
|
||||
std::list<uint16_t> lineXPos;
|
||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||
const uint16_t currentWordWidth = wordWidths[i];
|
||||
|
||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||
const uint16_t currentWordWidth = wordWidths[lastBreakAt + wordIdx];
|
||||
|
||||
lineXPos.push_back(xpos);
|
||||
xpos += currentWordWidth + spacing;
|
||||
|
||||
// Add spacing after this word, unless the next word is a continuation
|
||||
const bool nextIsContinuation = wordIdx + 1 < lineWordCount && continuesVec[lastBreakAt + wordIdx + 1];
|
||||
|
||||
xpos += currentWordWidth + (nextIsContinuation ? 0 : spacing);
|
||||
}
|
||||
|
||||
// Iterators always start at the beginning as we are moving content with splice below
|
||||
auto wordEndIt = words.begin();
|
||||
auto wordStyleEndIt = wordStyles.begin();
|
||||
auto wordContinuesEndIt = wordContinues.begin();
|
||||
std::advance(wordEndIt, lineWordCount);
|
||||
std::advance(wordStyleEndIt, lineWordCount);
|
||||
std::advance(wordContinuesEndIt, lineWordCount);
|
||||
|
||||
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||
std::list<std::string> lineWords;
|
||||
@@ -191,5 +494,16 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
std::list<EpdFontFamily::Style> lineWordStyles;
|
||||
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
||||
|
||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
|
||||
// Consume continues flags (not passed to TextBlock, but must be consumed to stay in sync)
|
||||
std::list<bool> lineContinues;
|
||||
lineContinues.splice(lineContinues.begin(), wordContinues, wordContinues.begin(), wordContinuesEndIt);
|
||||
|
||||
for (auto& word : lineWords) {
|
||||
if (containsSoftHyphen(word)) {
|
||||
stripSoftHyphensInPlace(word);
|
||||
}
|
||||
}
|
||||
|
||||
processLine(
|
||||
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle));
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "blocks/BlockStyle.h"
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
class GfxRenderer;
|
||||
@@ -15,26 +16,37 @@ class GfxRenderer;
|
||||
class ParsedText {
|
||||
std::list<std::string> words;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
TextBlock::Style style;
|
||||
std::list<bool> wordContinues; // true = word attaches to previous (no space before it)
|
||||
BlockStyle blockStyle;
|
||||
bool extraParagraphSpacing;
|
||||
bool hyphenationEnabled;
|
||||
|
||||
std::vector<size_t> computeLineBreaks(int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths) const;
|
||||
void applyParagraphIndent();
|
||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth,
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
|
||||
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
int spaceWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec);
|
||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks,
|
||||
std::vector<bool>* continuesVec = nullptr);
|
||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<size_t>& lineBreakIndices,
|
||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||
|
||||
public:
|
||||
explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing)
|
||||
: style(style), extraParagraphSpacing(extraParagraphSpacing) {}
|
||||
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
|
||||
const BlockStyle& blockStyle = BlockStyle())
|
||||
: blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
|
||||
~ParsedText() = default;
|
||||
|
||||
void addWord(std::string word, EpdFontFamily::Style fontStyle);
|
||||
void setStyle(const TextBlock::Style style) { this->style = style; }
|
||||
TextBlock::Style getStyle() const { return style; }
|
||||
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false);
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
BlockStyle& getBlockStyle() { return blockStyle; }
|
||||
size_t size() const { return words.size(); }
|
||||
bool isEmpty() const { return words.empty(); }
|
||||
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||
bool includeLastLine = true);
|
||||
};
|
||||
};
|
||||
+78
-49
@@ -1,29 +1,33 @@
|
||||
#include "Section.h"
|
||||
|
||||
#include <SDCardManager.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "Epub/css/CssParser.h"
|
||||
#include "Page.h"
|
||||
#include "hyphenation/Hyphenator.h"
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 9;
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 13;
|
||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t);
|
||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
||||
sizeof(uint32_t);
|
||||
} // namespace
|
||||
|
||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
if (!file) {
|
||||
Serial.printf("[%lu] [SCT] File not open for writing page %d\n", millis(), pageCount);
|
||||
LOG_ERR("SCT", "File not open for writing page %d", pageCount);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t position = file.position();
|
||||
if (!page->serialize(file)) {
|
||||
Serial.printf("[%lu] [SCT] Failed to serialize page %d\n", millis(), pageCount);
|
||||
LOG_ERR("SCT", "Failed to serialize page %d", pageCount);
|
||||
return 0;
|
||||
}
|
||||
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
|
||||
LOG_DBG("SCT", "Page %d processed", pageCount);
|
||||
|
||||
pageCount++;
|
||||
return position;
|
||||
@@ -31,14 +35,16 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
|
||||
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight) {
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||
const bool embeddedStyle) {
|
||||
if (!file) {
|
||||
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
|
||||
LOG_DBG("SCT", "File not open for writing header");
|
||||
return;
|
||||
}
|
||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
||||
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(uint32_t),
|
||||
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
|
||||
sizeof(embeddedStyle) + sizeof(uint32_t),
|
||||
"Header size mismatch");
|
||||
serialization::writePod(file, SECTION_FILE_VERSION);
|
||||
serialization::writePod(file, fontId);
|
||||
@@ -47,14 +53,16 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
serialization::writePod(file, paragraphAlignment);
|
||||
serialization::writePod(file, viewportWidth);
|
||||
serialization::writePod(file, viewportHeight);
|
||||
serialization::writePod(file, hyphenationEnabled);
|
||||
serialization::writePod(file, embeddedStyle);
|
||||
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::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight) {
|
||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) {
|
||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,7 +72,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
serialization::readPod(file, version);
|
||||
if (version != SECTION_FILE_VERSION) {
|
||||
file.close();
|
||||
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
|
||||
clearCache();
|
||||
return false;
|
||||
}
|
||||
@@ -74,18 +82,23 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
float fileLineCompression;
|
||||
bool fileExtraParagraphSpacing;
|
||||
uint8_t fileParagraphAlignment;
|
||||
bool fileHyphenationEnabled;
|
||||
bool fileEmbeddedStyle;
|
||||
serialization::readPod(file, fileFontId);
|
||||
serialization::readPod(file, fileLineCompression);
|
||||
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||
serialization::readPod(file, fileParagraphAlignment);
|
||||
serialization::readPod(file, fileViewportWidth);
|
||||
serialization::readPod(file, fileViewportHeight);
|
||||
serialization::readPod(file, fileHyphenationEnabled);
|
||||
serialization::readPod(file, fileEmbeddedStyle);
|
||||
|
||||
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight) {
|
||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) {
|
||||
file.close();
|
||||
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
||||
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||
clearCache();
|
||||
return false;
|
||||
}
|
||||
@@ -93,38 +106,37 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
|
||||
serialization::readPod(file, pageCount);
|
||||
file.close();
|
||||
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
|
||||
LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
||||
bool Section::clearCache() const {
|
||||
if (!SdMan.exists(filePath.c_str())) {
|
||||
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
|
||||
if (!Storage.exists(filePath.c_str())) {
|
||||
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SdMan.remove(filePath.c_str())) {
|
||||
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis());
|
||||
if (!Storage.remove(filePath.c_str())) {
|
||||
LOG_ERR("SCT", "Failed to clear cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis());
|
||||
LOG_DBG("SCT", "Cache cleared successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const std::function<void()>& progressSetupFn,
|
||||
const std::function<void(int)>& progressFn) {
|
||||
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const std::function<void()>& popupFn) {
|
||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||
|
||||
// Create cache directory if it doesn't exist
|
||||
{
|
||||
const auto sectionsDir = epub->getCachePath() + "/sections";
|
||||
SdMan.mkdir(sectionsDir.c_str());
|
||||
Storage.mkdir(sectionsDir.c_str());
|
||||
}
|
||||
|
||||
// Retry logic for SD card timing issues
|
||||
@@ -132,17 +144,17 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
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);
|
||||
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
|
||||
delay(50); // Brief delay before retry
|
||||
}
|
||||
|
||||
// Remove any incomplete file from previous attempt before retrying
|
||||
if (SdMan.exists(tmpHtmlPath.c_str())) {
|
||||
SdMan.remove(tmpHtmlPath.c_str());
|
||||
if (Storage.exists(tmpHtmlPath.c_str())) {
|
||||
Storage.remove(tmpHtmlPath.c_str());
|
||||
}
|
||||
|
||||
FsFile tmpHtml;
|
||||
if (!SdMan.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||
continue;
|
||||
}
|
||||
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
||||
@@ -150,43 +162,57 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
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 && Storage.exists(tmpHtmlPath.c_str())) {
|
||||
Storage.remove(tmpHtmlPath.c_str());
|
||||
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file after retries\n", millis());
|
||||
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
|
||||
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
|
||||
|
||||
// Only show progress bar for larger chapters where rendering overhead is worth it
|
||||
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
|
||||
progressSetupFn();
|
||||
}
|
||||
|
||||
if (!SdMan.openFileForWrite("SCT", filePath, file)) {
|
||||
if (!Storage.openFileForWrite("SCT", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight);
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle);
|
||||
std::vector<uint32_t> lut = {};
|
||||
|
||||
// Derive the content base directory and image cache path prefix for the parser
|
||||
size_t lastSlash = localPath.find_last_of('/');
|
||||
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
||||
std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
||||
|
||||
CssParser* cssParser = nullptr;
|
||||
if (embeddedStyle) {
|
||||
cssParser = epub->getCssParser();
|
||||
if (cssParser) {
|
||||
if (!cssParser->loadFromCache()) {
|
||||
LOG_ERR("SCT", "Failed to load CSS from cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChapterHtmlSlimParser visitor(
|
||||
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight,
|
||||
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled,
|
||||
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
||||
progressFn);
|
||||
embeddedStyle, contentBase, imageBasePath, popupFn, cssParser);
|
||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||
success = visitor.parseAndBuildPages();
|
||||
|
||||
SdMan.remove(tmpHtmlPath.c_str());
|
||||
Storage.remove(tmpHtmlPath.c_str());
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis());
|
||||
LOG_ERR("SCT", "Failed to parse XML and build pages");
|
||||
file.close();
|
||||
SdMan.remove(filePath.c_str());
|
||||
Storage.remove(filePath.c_str());
|
||||
if (cssParser) {
|
||||
cssParser->clear();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -202,9 +228,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
}
|
||||
|
||||
if (hasFailedLutRecords) {
|
||||
Serial.printf("[%lu] [SCT] Failed to write LUT due to invalid page positions\n", millis());
|
||||
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
|
||||
file.close();
|
||||
SdMan.remove(filePath.c_str());
|
||||
Storage.remove(filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -213,11 +239,14 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
serialization::writePod(file, pageCount);
|
||||
serialization::writePod(file, lutOffset);
|
||||
file.close();
|
||||
if (cssParser) {
|
||||
cssParser->clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ class Section {
|
||||
FsFile file;
|
||||
|
||||
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight);
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
|
||||
bool embeddedStyle);
|
||||
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
||||
|
||||
public:
|
||||
@@ -29,11 +30,10 @@ class Section {
|
||||
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||
~Section() = default;
|
||||
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight);
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle);
|
||||
bool clearCache() const;
|
||||
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight,
|
||||
const std::function<void()>& progressSetupFn = nullptr,
|
||||
const std::function<void(int)>& progressFn = nullptr);
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||
const std::function<void()>& popupFn = nullptr);
|
||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
||||
class Block {
|
||||
public:
|
||||
virtual ~Block() = default;
|
||||
virtual void layout(GfxRenderer& renderer) = 0;
|
||||
|
||||
virtual BlockType getType() = 0;
|
||||
virtual bool isEmpty() = 0;
|
||||
virtual void finish() {}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "Epub/css/CssStyle.h"
|
||||
|
||||
/**
|
||||
* BlockStyle - Block-level styling properties
|
||||
*/
|
||||
struct BlockStyle {
|
||||
CssTextAlign alignment = CssTextAlign::Justify;
|
||||
|
||||
// Spacing (in pixels)
|
||||
int16_t marginTop = 0;
|
||||
int16_t marginBottom = 0;
|
||||
int16_t marginLeft = 0;
|
||||
int16_t marginRight = 0;
|
||||
int16_t paddingTop = 0; // treated same as margin for rendering
|
||||
int16_t paddingBottom = 0; // treated same as margin for rendering
|
||||
int16_t paddingLeft = 0; // treated same as margin for rendering
|
||||
int16_t paddingRight = 0; // treated same as margin for rendering
|
||||
int16_t textIndent = 0;
|
||||
bool textIndentDefined = false; // true if text-indent was explicitly set in CSS
|
||||
bool textAlignDefined = false; // true if text-align was explicitly set in CSS
|
||||
|
||||
// Combined horizontal insets (margin + padding)
|
||||
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
|
||||
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
|
||||
[[nodiscard]] int16_t totalHorizontalInset() const { return leftInset() + rightInset(); }
|
||||
|
||||
// Combine with another block style. Useful for parent -> child styles, where the child style should be
|
||||
// applied on top of the parent's style to get the combined style.
|
||||
BlockStyle getCombinedBlockStyle(const BlockStyle& child) const {
|
||||
BlockStyle combinedBlockStyle;
|
||||
|
||||
combinedBlockStyle.marginTop = static_cast<int16_t>(child.marginTop + marginTop);
|
||||
combinedBlockStyle.marginBottom = static_cast<int16_t>(child.marginBottom + marginBottom);
|
||||
combinedBlockStyle.marginLeft = static_cast<int16_t>(child.marginLeft + marginLeft);
|
||||
combinedBlockStyle.marginRight = static_cast<int16_t>(child.marginRight + marginRight);
|
||||
|
||||
combinedBlockStyle.paddingTop = static_cast<int16_t>(child.paddingTop + paddingTop);
|
||||
combinedBlockStyle.paddingBottom = static_cast<int16_t>(child.paddingBottom + paddingBottom);
|
||||
combinedBlockStyle.paddingLeft = static_cast<int16_t>(child.paddingLeft + paddingLeft);
|
||||
combinedBlockStyle.paddingRight = static_cast<int16_t>(child.paddingRight + paddingRight);
|
||||
// Text indent: use child's if defined
|
||||
if (child.textIndentDefined) {
|
||||
combinedBlockStyle.textIndent = child.textIndent;
|
||||
combinedBlockStyle.textIndentDefined = true;
|
||||
} else {
|
||||
combinedBlockStyle.textIndent = textIndent;
|
||||
combinedBlockStyle.textIndentDefined = textIndentDefined;
|
||||
}
|
||||
// Text align: use child's if defined
|
||||
if (child.textAlignDefined) {
|
||||
combinedBlockStyle.alignment = child.alignment;
|
||||
combinedBlockStyle.textAlignDefined = true;
|
||||
} else {
|
||||
combinedBlockStyle.alignment = alignment;
|
||||
combinedBlockStyle.textAlignDefined = textAlignDefined;
|
||||
}
|
||||
return combinedBlockStyle;
|
||||
}
|
||||
|
||||
// Create a BlockStyle from CSS style properties, resolving CssLength values to pixels
|
||||
// emSize is the current font line height, used for em/rem unit conversion
|
||||
// paragraphAlignment is the user's paragraphAlignment setting preference
|
||||
static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment,
|
||||
const uint16_t viewportWidth = 0) {
|
||||
BlockStyle blockStyle;
|
||||
const float vw = viewportWidth;
|
||||
// Resolve all CssLength values to pixels using the current font's em size and viewport width
|
||||
blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize, vw);
|
||||
blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize, vw);
|
||||
blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize, vw);
|
||||
blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize, vw);
|
||||
|
||||
blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize, vw);
|
||||
blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw);
|
||||
blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize, vw);
|
||||
blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize, vw);
|
||||
|
||||
// For textIndent: if it's a percentage we can't resolve (no viewport width),
|
||||
// leave textIndentDefined=false so the EmSpace fallback in applyParagraphIndent() is used
|
||||
if (cssStyle.hasTextIndent() && cssStyle.textIndent.isResolvable(vw)) {
|
||||
blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize, vw);
|
||||
blockStyle.textIndentDefined = true;
|
||||
}
|
||||
blockStyle.textAlignDefined = cssStyle.hasTextAlign();
|
||||
// User setting overrides CSS, unless "Book's Style" alignment setting is selected
|
||||
if (paragraphAlignment == CssTextAlign::None) {
|
||||
blockStyle.alignment = blockStyle.textAlignDefined ? cssStyle.textAlign : CssTextAlign::Justify;
|
||||
} else {
|
||||
blockStyle.alignment = paragraphAlignment;
|
||||
}
|
||||
return blockStyle;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
#include "ImageBlock.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "../converters/DitherUtils.h"
|
||||
#include "../converters/ImageDecoderFactory.h"
|
||||
|
||||
// Cache file format:
|
||||
// - uint16_t width
|
||||
// - uint16_t height
|
||||
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
|
||||
|
||||
ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height)
|
||||
: imagePath(imagePath), width(width), height(height) {}
|
||||
|
||||
bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); }
|
||||
|
||||
namespace {
|
||||
|
||||
std::string getCachePath(const std::string& imagePath) {
|
||||
// Replace extension with .pxc (pixel cache)
|
||||
size_t dotPos = imagePath.rfind('.');
|
||||
if (dotPos != std::string::npos) {
|
||||
return imagePath.substr(0, dotPos) + ".pxc";
|
||||
}
|
||||
return imagePath + ".pxc";
|
||||
}
|
||||
|
||||
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||
int expectedHeight) {
|
||||
FsFile cacheFile;
|
||||
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t cachedWidth, cachedHeight;
|
||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
||||
int widthDiff = abs(cachedWidth - expectedWidth);
|
||||
int heightDiff = abs(cachedHeight - expectedHeight);
|
||||
if (widthDiff > 1 || heightDiff > 1) {
|
||||
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
|
||||
expectedHeight);
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use cached dimensions for rendering (they're the actual decoded size)
|
||||
expectedWidth = cachedWidth;
|
||||
expectedHeight = cachedHeight;
|
||||
|
||||
LOG_DBG("IMG", "Loading from cache: %s (%dx%d)", cachePath.c_str(), cachedWidth, cachedHeight);
|
||||
|
||||
// Read and render row by row to minimize memory usage
|
||||
const int bytesPerRow = (cachedWidth + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
uint8_t* rowBuffer = (uint8_t*)malloc(bytesPerRow);
|
||||
if (!rowBuffer) {
|
||||
LOG_ERR("IMG", "Failed to allocate row buffer");
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int row = 0; row < cachedHeight; row++) {
|
||||
if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) {
|
||||
LOG_ERR("IMG", "Cache read error at row %d", row);
|
||||
free(rowBuffer);
|
||||
cacheFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
int destY = y + row;
|
||||
for (int col = 0; col < cachedWidth; col++) {
|
||||
int byteIdx = col / 4;
|
||||
int bitShift = 6 - (col % 4) * 2; // MSB first within byte
|
||||
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
||||
|
||||
drawPixelWithRenderMode(renderer, x + col, destY, pixelValue);
|
||||
}
|
||||
}
|
||||
|
||||
free(rowBuffer);
|
||||
cacheFile.close();
|
||||
LOG_DBG("IMG", "Cache render complete");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
|
||||
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
const int screenHeight = renderer.getScreenHeight();
|
||||
|
||||
// Bounds check render position using logical screen dimensions
|
||||
if (x < 0 || y < 0 || x + width > screenWidth || y + height > screenHeight) {
|
||||
LOG_ERR("IMG", "Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)", x, y, width, height, screenWidth,
|
||||
screenHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to render from cache first
|
||||
std::string cachePath = getCachePath(imagePath);
|
||||
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||
return; // Successfully rendered from cache
|
||||
}
|
||||
|
||||
// No cache - need to decode the image
|
||||
// Check if image file exists
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
size_t fileSize = file.size();
|
||||
file.close();
|
||||
|
||||
if (fileSize == 0) {
|
||||
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("IMG", "Decoding and caching: %s", imagePath.c_str());
|
||||
|
||||
RenderConfig config;
|
||||
config.x = x;
|
||||
config.y = y;
|
||||
config.maxWidth = width;
|
||||
config.maxHeight = height;
|
||||
config.useGrayscale = true;
|
||||
config.useDithering = true;
|
||||
config.performanceMode = false;
|
||||
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
|
||||
config.cachePath = cachePath; // Enable caching during decode
|
||||
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||
if (!decoder) {
|
||||
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("IMG", "Using %s decoder", decoder->getFormatName());
|
||||
|
||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||
if (!success) {
|
||||
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("IMG", "Decode successful");
|
||||
}
|
||||
|
||||
bool ImageBlock::serialize(FsFile& file) {
|
||||
serialization::writeString(file, imagePath);
|
||||
serialization::writePod(file, width);
|
||||
serialization::writePod(file, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<ImageBlock> ImageBlock::deserialize(FsFile& file) {
|
||||
std::string path;
|
||||
serialization::readString(file, path);
|
||||
int16_t w, h;
|
||||
serialization::readPod(file, w);
|
||||
serialization::readPod(file, h);
|
||||
return std::unique_ptr<ImageBlock>(new ImageBlock(path, w, h));
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include <SdFat.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Block.h"
|
||||
|
||||
class ImageBlock final : public Block {
|
||||
public:
|
||||
ImageBlock(const std::string& imagePath, int16_t width, int16_t height);
|
||||
~ImageBlock() override = default;
|
||||
|
||||
const std::string& getImagePath() const { return imagePath; }
|
||||
int16_t getWidth() const { return width; }
|
||||
int16_t getHeight() const { return height; }
|
||||
|
||||
bool imageExists() const;
|
||||
|
||||
BlockType getType() override { return IMAGE_BLOCK; }
|
||||
bool isEmpty() override { return false; }
|
||||
|
||||
void render(GfxRenderer& renderer, const int x, const int y);
|
||||
bool serialize(FsFile& file);
|
||||
static std::unique_ptr<ImageBlock> deserialize(FsFile& file);
|
||||
|
||||
private:
|
||||
std::string imagePath;
|
||||
int16_t width;
|
||||
int16_t height;
|
||||
};
|
||||
@@ -1,22 +1,46 @@
|
||||
#include "TextBlock.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
void TextBlock::render(const 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());
|
||||
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", (uint32_t)words.size(),
|
||||
(uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
|
||||
return;
|
||||
}
|
||||
|
||||
auto wordIt = words.begin();
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
auto wordXposIt = wordXpos.begin();
|
||||
|
||||
for (size_t i = 0; i < words.size(); i++) {
|
||||
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
|
||||
const int wordX = *wordXposIt + x;
|
||||
const EpdFontFamily::Style currentStyle = *wordStylesIt;
|
||||
renderer.drawText(fontId, wordX, y, wordIt->c_str(), true, currentStyle);
|
||||
|
||||
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
||||
const std::string& w = *wordIt;
|
||||
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle);
|
||||
// y is the top of the text line; add ascender to reach baseline, then offset 2px below
|
||||
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
|
||||
|
||||
int startX = wordX;
|
||||
int underlineWidth = fullWordWidth;
|
||||
|
||||
// if word starts with em-space ("\xe2\x80\x83"), account for the additional indent before drawing the line
|
||||
if (w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && static_cast<uint8_t>(w[1]) == 0x80 &&
|
||||
static_cast<uint8_t>(w[2]) == 0x83) {
|
||||
const char* visiblePtr = w.c_str() + 3;
|
||||
const int prefixWidth = renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
|
||||
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
||||
startX = wordX + prefixWidth;
|
||||
underlineWidth = visibleWidth;
|
||||
}
|
||||
|
||||
renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, true);
|
||||
}
|
||||
|
||||
std::advance(wordIt, 1);
|
||||
std::advance(wordStylesIt, 1);
|
||||
@@ -26,8 +50,8 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
|
||||
bool TextBlock::serialize(FsFile& file) const {
|
||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
||||
Serial.printf("[%lu] [TXB] Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
|
||||
words.size(), wordXpos.size(), wordStyles.size());
|
||||
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", words.size(),
|
||||
wordXpos.size(), wordStyles.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -37,8 +61,19 @@ bool TextBlock::serialize(FsFile& file) const {
|
||||
for (auto x : wordXpos) serialization::writePod(file, x);
|
||||
for (auto s : wordStyles) serialization::writePod(file, s);
|
||||
|
||||
// Block style
|
||||
serialization::writePod(file, style);
|
||||
// Style (alignment + margins/padding/indent)
|
||||
serialization::writePod(file, blockStyle.alignment);
|
||||
serialization::writePod(file, blockStyle.textAlignDefined);
|
||||
serialization::writePod(file, blockStyle.marginTop);
|
||||
serialization::writePod(file, blockStyle.marginBottom);
|
||||
serialization::writePod(file, blockStyle.marginLeft);
|
||||
serialization::writePod(file, blockStyle.marginRight);
|
||||
serialization::writePod(file, blockStyle.paddingTop);
|
||||
serialization::writePod(file, blockStyle.paddingBottom);
|
||||
serialization::writePod(file, blockStyle.paddingLeft);
|
||||
serialization::writePod(file, blockStyle.paddingRight);
|
||||
serialization::writePod(file, blockStyle.textIndent);
|
||||
serialization::writePod(file, blockStyle.textIndentDefined);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -48,14 +83,14 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
Style style;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
// Word count
|
||||
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);
|
||||
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -67,8 +102,20 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
for (auto& x : wordXpos) serialization::readPod(file, x);
|
||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||
|
||||
// Block style
|
||||
serialization::readPod(file, style);
|
||||
// Style (alignment + margins/padding/indent)
|
||||
serialization::readPod(file, blockStyle.alignment);
|
||||
serialization::readPod(file, blockStyle.textAlignDefined);
|
||||
serialization::readPod(file, blockStyle.marginTop);
|
||||
serialization::readPod(file, blockStyle.marginBottom);
|
||||
serialization::readPod(file, blockStyle.marginLeft);
|
||||
serialization::readPod(file, blockStyle.marginRight);
|
||||
serialization::readPod(file, blockStyle.paddingTop);
|
||||
serialization::readPod(file, blockStyle.paddingBottom);
|
||||
serialization::readPod(file, blockStyle.paddingLeft);
|
||||
serialization::readPod(file, blockStyle.paddingRight);
|
||||
serialization::readPod(file, blockStyle.textIndent);
|
||||
serialization::readPod(file, blockStyle.textIndentDefined);
|
||||
|
||||
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), blockStyle));
|
||||
}
|
||||
|
||||
@@ -1,38 +1,33 @@
|
||||
#pragma once
|
||||
#include <EpdFontFamily.h>
|
||||
#include <SdFat.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Block.h"
|
||||
#include "BlockStyle.h"
|
||||
|
||||
// Represents a line of text on a page
|
||||
class TextBlock final : public Block {
|
||||
public:
|
||||
enum Style : uint8_t {
|
||||
JUSTIFIED = 0,
|
||||
LEFT_ALIGN = 1,
|
||||
CENTER_ALIGN = 2,
|
||||
RIGHT_ALIGN = 3,
|
||||
};
|
||||
|
||||
private:
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
Style style;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
public:
|
||||
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
||||
std::list<EpdFontFamily::Style> word_styles, const Style style)
|
||||
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
|
||||
std::list<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
|
||||
: words(std::move(words)),
|
||||
wordXpos(std::move(word_xpos)),
|
||||
wordStyles(std::move(word_styles)),
|
||||
blockStyle(blockStyle) {}
|
||||
~TextBlock() override = default;
|
||||
void setStyle(const Style style) { this->style = style; }
|
||||
Style getStyle() const { return style; }
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
bool isEmpty() override { return words.empty(); }
|
||||
void layout(GfxRenderer& renderer) override {};
|
||||
// given a renderer works out where to break the words into lines
|
||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||
BlockType getType() override { return TEXT_BLOCK; }
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 4x4 Bayer matrix for ordered dithering
|
||||
inline const uint8_t bayer4x4[4][4] = {
|
||||
{0, 8, 2, 10},
|
||||
{12, 4, 14, 6},
|
||||
{3, 11, 1, 9},
|
||||
{15, 7, 13, 5},
|
||||
};
|
||||
|
||||
// Apply Bayer dithering and quantize to 4 levels (0-3)
|
||||
// Stateless - works correctly with any pixel processing order
|
||||
inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
|
||||
int bayer = bayer4x4[y & 3][x & 3];
|
||||
int dither = (bayer - 8) * 5; // Scale to +/-40 (half of quantization step 85)
|
||||
|
||||
int adjusted = gray + dither;
|
||||
if (adjusted < 0) adjusted = 0;
|
||||
if (adjusted > 255) adjusted = 255;
|
||||
|
||||
if (adjusted < 64) return 0;
|
||||
if (adjusted < 128) return 1;
|
||||
if (adjusted < 192) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
// Draw a pixel respecting the current render mode for grayscale support
|
||||
inline void drawPixelWithRenderMode(GfxRenderer& renderer, int x, int y, uint8_t pixelValue) {
|
||||
GfxRenderer::RenderMode renderMode = renderer.getRenderMode();
|
||||
if (renderMode == GfxRenderer::BW && pixelValue < 3) {
|
||||
renderer.drawPixel(x, y, true);
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (pixelValue == 1 || pixelValue == 2)) {
|
||||
renderer.drawPixel(x, y, false);
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && pixelValue == 1) {
|
||||
renderer.drawPixel(x, y, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "ImageDecoderFactory.h"
|
||||
|
||||
#include <Logging.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "JpegToFramebufferConverter.h"
|
||||
#include "PngToFramebufferConverter.h"
|
||||
|
||||
std::unique_ptr<JpegToFramebufferConverter> ImageDecoderFactory::jpegDecoder = nullptr;
|
||||
std::unique_ptr<PngToFramebufferConverter> ImageDecoderFactory::pngDecoder = nullptr;
|
||||
|
||||
ImageToFramebufferDecoder* ImageDecoderFactory::getDecoder(const std::string& imagePath) {
|
||||
std::string ext = imagePath;
|
||||
size_t dotPos = ext.rfind('.');
|
||||
if (dotPos != std::string::npos) {
|
||||
ext = ext.substr(dotPos);
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
} else {
|
||||
ext = "";
|
||||
}
|
||||
|
||||
if (JpegToFramebufferConverter::supportsFormat(ext)) {
|
||||
if (!jpegDecoder) {
|
||||
jpegDecoder.reset(new JpegToFramebufferConverter());
|
||||
}
|
||||
return jpegDecoder.get();
|
||||
} else if (PngToFramebufferConverter::supportsFormat(ext)) {
|
||||
if (!pngDecoder) {
|
||||
pngDecoder.reset(new PngToFramebufferConverter());
|
||||
}
|
||||
return pngDecoder.get();
|
||||
}
|
||||
|
||||
LOG_ERR("DEC", "No decoder found for image: %s", imagePath.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ImageDecoderFactory::isFormatSupported(const std::string& imagePath) { return getDecoder(imagePath) != nullptr; }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user