Compare commits
326
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77a964e83a | ||
|
|
e20f4550ce | ||
|
|
9b3885135f | ||
|
+7 |
e6c6e72a24 | ||
|
|
1d219ae27e | ||
|
|
c4f11015f1 | ||
|
|
abd2266048 | ||
|
|
1df543d48d | ||
|
|
63961625a2 | ||
|
|
34484300e4 | ||
|
|
831144e737 | ||
|
|
42c33528f9 | ||
|
|
6969950cd7 | ||
|
|
bc6f6daeb5 | ||
|
|
8352e1f08f | ||
|
|
dfc38cca4c | ||
|
|
3856348ee6 | ||
|
|
0e228324e6 | ||
|
|
8e091609f7 | ||
|
|
710055f02c | ||
|
|
0245972132 | ||
|
|
0cbfaa007d | ||
|
|
ceb6acc8d7 | ||
|
|
7d56810ee6 | ||
|
|
526c8a5e7a | ||
|
|
0c9e8b3ece | ||
|
|
53beeeed2b | ||
|
|
8dd365b4da | ||
|
|
9665dd7473 | ||
|
|
71719e1d94 | ||
|
|
d6951f81b7 | ||
|
|
99721c081b | ||
|
|
f9286709d1 | ||
|
|
dc39480349 | ||
|
|
b5df6cb2b5 | ||
|
|
16b73744c5 | ||
|
|
79b54b3a75 | ||
|
|
11ca208ec2 | ||
|
|
7a28f90dad | ||
|
|
3dabd30287 | ||
|
|
f1e9dc7f30 | ||
|
|
b467ea7973 | ||
|
|
32a5c1c358 | ||
|
|
a95a63b753 | ||
|
|
4104fa8102 | ||
|
|
e60ba7620d | ||
|
|
cd508d27d5 | ||
|
|
170cc25774 | ||
|
|
c40e92e4d1 | ||
|
|
4d22256745 | ||
|
|
18b36efbae | ||
|
|
a35f372e1b | ||
|
|
4ef433e373 | ||
|
|
a5d7e03f54 | ||
|
|
047b0029c9 | ||
|
|
c3f1dbfa09 | ||
|
|
ea88797c8e | ||
|
|
218201bd1f | ||
|
|
6ee05b08a1 | ||
|
|
a826569a0f | ||
|
|
88594077aa | ||
|
|
ce0b439aa3 | ||
|
|
019587bb77 | ||
|
|
4388bf8cc7 | ||
|
|
6de8b7a666 | ||
|
|
307a6608f0 | ||
|
|
a350492571 | ||
|
|
ef02737c89 | ||
|
|
aff93f1dc0 | ||
|
|
f0a549b680 | ||
|
|
7dc518624c | ||
|
|
620835a6a1 | ||
|
|
3cc8e272ca | ||
|
|
80d1856330 | ||
|
|
76681201bf | ||
|
|
04242fa221 | ||
|
|
2b25f4d168 | ||
|
|
a57c62f0b4 | ||
|
|
3da2cd3cf8 | ||
|
|
88c49b8bed | ||
|
|
f67e6c2831 | ||
|
|
5e95d9a36f | ||
|
|
45a228a645 | ||
|
|
6ff5fcd9a7 | ||
|
|
42b122b8fd | ||
|
|
0e168aa22c | ||
|
|
050a3bd1b6 | ||
|
|
3b4f2a1129 | ||
|
|
09cef70709 | ||
|
|
4fb785af82 | ||
|
|
74c7205967 | ||
|
|
93d4a34c37 | ||
|
|
c4fc4effbd | ||
|
|
5b11e45a36 | ||
|
|
d05cb220bb | ||
|
|
125e091d13 | ||
|
|
6b64a0a2d8 | ||
|
|
1abe307f20 | ||
|
|
f7814cd139 | ||
|
|
3f98a87709 | ||
|
|
30d8a8d011 | ||
|
|
451774ddf8 | ||
|
|
8c27938979 | ||
|
|
4a0ef05899 | ||
|
|
c99a673e5b | ||
|
|
cae8517235 | ||
|
|
7e214ea760 | ||
|
|
b695a48af6 | ||
|
|
a6c5d9aa7c | ||
|
|
2d49c7b7b4 | ||
|
|
cb72916397 | ||
|
|
35988ada55 | ||
|
|
f2fbdccd53 | ||
|
|
128eb614a6 | ||
|
|
31396da064 | ||
|
|
8ab2f22730 | ||
|
|
c0cd7c13a3 | ||
|
|
000289e429 | ||
|
|
ff577540a3 | ||
|
|
43efd80e14 | ||
|
|
5050992bd6 | ||
|
|
7d97687a5b | ||
|
|
d6d0cc869e | ||
|
|
233deacfe1 | ||
|
|
0eb8a9346b | ||
|
|
13592db50f | ||
|
|
f8a9f1f07a | ||
|
|
052f497b9e | ||
|
|
bfdf0a4f78 | ||
|
|
ae94e97fb8 | ||
|
|
04e72a9ede | ||
|
|
52ca658634 | ||
|
|
75bb1d8582 | ||
|
|
7761ced0ed | ||
|
|
2b52bc658c | ||
|
|
57250b97e4 | ||
|
|
13fc8b94b0 | ||
|
|
410c70ab89 | ||
|
|
a6aead660a | ||
|
|
9e4ef008cc | ||
|
|
ebe3123997 | ||
|
|
fa4c8a4e33 | ||
|
|
6dc993852b | ||
|
|
63002d464b | ||
|
|
75ff7b25ab | ||
|
|
4ccafe5cfa | ||
|
|
ecb5b1b4e5 | ||
|
|
f28623dacd | ||
|
|
a610568f8c | ||
|
|
d9f114b652 | ||
|
|
3cb60aa231 | ||
|
|
786b438ea2 | ||
|
|
6e4d0e534d | ||
|
|
f62529ad91 | ||
|
|
88537769f6 | ||
|
|
3696794591 | ||
|
|
c1fad16e10 | ||
|
|
c3093e3f71 | ||
|
|
5f5561b684 | ||
|
|
498e087a68 | ||
|
|
e44c004be6 | ||
|
|
45d19f6e1b | ||
|
|
10a2678584 | ||
|
|
e32d41a37e | ||
|
|
7717ae2683 | ||
|
|
f02c9784ec | ||
|
|
693dba4c94 | ||
|
|
9c55c15a72 | ||
|
|
6ba9658f15 | ||
|
|
8f33bee6ef | ||
|
|
22b96ec22a | ||
|
|
bc1ba7277f | ||
|
|
57267f5372 | ||
|
|
7c4f69680c | ||
|
|
2cc497cdca | ||
|
|
8db3542e90 | ||
|
|
87d9d1dc2a | ||
|
|
588984ec30 | ||
|
|
c9faf2a8c0 | ||
|
|
356fe9a31e | ||
|
|
5da23eed82 | ||
|
|
388fbf206a | ||
|
|
2a38bfd8af | ||
|
|
d7f89e6c0d | ||
|
|
07d715e32d | ||
|
|
63b2643534 | ||
|
|
cabbfcfd7e | ||
|
|
b8e743ef80 | ||
|
|
d461d93e76 | ||
|
|
3e2c518b8e | ||
|
|
ca89e41636 | ||
|
|
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 |
+1
@@ -0,0 +1 @@
|
||||
../../.skills/SKILL.md
|
||||
+102
-18
@@ -1,10 +1,61 @@
|
||||
name: CI
|
||||
'on':
|
||||
name: CI (build)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
|
||||
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 clang-format-21
|
||||
run: |
|
||||
wget https://apt.llvm.org/llvm.sh
|
||||
chmod +x llvm.sh
|
||||
sudo ./llvm.sh 21
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-format-21
|
||||
|
||||
- name: Run 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 uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||
|
||||
- name: Run cppcheck
|
||||
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -16,22 +67,55 @@ jobs:
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: pip install --upgrade platformio
|
||||
|
||||
- name: Install clang-format-21
|
||||
run: |
|
||||
wget https://apt.llvm.org/llvm.sh
|
||||
chmod +x llvm.sh
|
||||
sudo ./llvm.sh 21
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-format-21
|
||||
|
||||
- name: Run cppcheck
|
||||
run: pio check --fail-on-defect 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)
|
||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||
|
||||
- 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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: "PR Formatting"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
@@ -12,19 +12,18 @@ jobs:
|
||||
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 uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: pip install --upgrade platformio
|
||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||
|
||||
- name: Build CrossPoint
|
||||
run: pio run -e gh_release
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||
|
||||
- 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,9 +3,15 @@
|
||||
.DS_Store
|
||||
.vscode
|
||||
lib/EpdFont/fontsrc
|
||||
lib/I18n/I18nKeys.h
|
||||
lib/I18n/I18nStrings.h
|
||||
lib/I18n/I18nStrings.cpp
|
||||
*.generated.h
|
||||
.vs
|
||||
build
|
||||
**/__pycache__/
|
||||
/compile_commands.json
|
||||
/.cache
|
||||
.history/
|
||||
/.venv
|
||||
*.local*
|
||||
|
||||
@@ -0,0 +1,872 @@
|
||||
# CrossPoint Reader Development Guide
|
||||
|
||||
Project: Open-source e-reader firmware for Xteink X4 (ESP32-C3)
|
||||
Mission: Provide a lightweight, high-performance reading experience focused on EPUB rendering on constrained hardware.
|
||||
|
||||
## AI Agent Identity and Cognitive Rules
|
||||
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
|
||||
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
|
||||
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
|
||||
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the open-x4-sdk or official docs first.
|
||||
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
|
||||
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
|
||||
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
|
||||
---
|
||||
|
||||
## Development Environment Awareness
|
||||
|
||||
**CRITICAL**: Detect the host platform at session start to choose appropriate tools and commands.
|
||||
|
||||
### Platform Detection
|
||||
```bash
|
||||
# Detect platform (run once per session)
|
||||
uname -s
|
||||
# Returns: MINGW64_NT-* (Windows Git Bash), Linux, Darwin (macOS)
|
||||
```
|
||||
|
||||
**Detection Required**: Run `uname -s` at session start to determine platform
|
||||
|
||||
### Platform-Specific Behaviors
|
||||
- **Windows (Git Bash)**: Unix commands, `C:\` paths in Windows but `/` in bash, limited glob (use `find`+`xargs`)
|
||||
- **Linux/WSL**: Full bash, Unix paths, native glob support
|
||||
|
||||
**Cross-Platform Code Formatting**:
|
||||
```bash
|
||||
find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Platform and Hardware Constraints
|
||||
|
||||
### Hardware Specs
|
||||
* MCU: ESP32-C3 (Single-core RISC-V @ 160MHz)
|
||||
* RAM: ~380KB usable (VERY LIMITED - primary project constraint)
|
||||
* **NO PSRAM**: ESP32-C3 has no PSRAM capability (unlike ESP32-S3)
|
||||
* **Single Buffer Mode**: Only ONE 48KB framebuffer (not double-buffered)
|
||||
* Flash: 16MB (Instruction storage and static data)
|
||||
* Display: 800x480 E-Ink (Slow refresh, monochrome, 1-2s full update)
|
||||
* Framebuffer: 48,000 bytes (800 × 480 ÷ 8)
|
||||
* Storage: SD Card (Used for books and aggressive caching)
|
||||
|
||||
### The Resource Protocol
|
||||
1. Stack Safety: Limit local function variables to < 256 bytes. The ESP32-C3 default stack is small; use std::unique_ptr or static pools for larger buffers.
|
||||
2. Heap Fragmentation: Avoid repeated new/delete in loops. Allocate buffers once during onEnter() and reuse them.
|
||||
3. Flash Persistence: Large constant data (UI strings, lookup tables) MUST be marked static const to stay in Flash (Instruction Bus), freeing DRAM.
|
||||
4. String Policy: Prohibit std::string and Arduino String in hot paths. Use std::string_view for read-only access and snprintf with fixed char[] buffers for construction.
|
||||
5. UI Strings: All user-facing text must use the `tr()` macro (e.g., `tr(STR_LOADING)`) for i18n support. Never hardcode UI strings directly. For the avoidance of doubt, logging messages (LOG_DBG/LOG_ERR) can be hardcoded, but user-facing text must use `tr()`.
|
||||
6. `constexpr` First: Compile-time constants and lookup tables must be `constexpr`, not just `static const`. This moves computation to compile time, enables dead-branch elimination, and guarantees flash placement. Use `static constexpr` for class-level constants.
|
||||
7. `std::vector` Pre-allocation: Always call `.reserve(N)` before any `push_back()` loop. Each growth event allocates a new block (2×), copies all elements, then frees the old one — three heap operations that fragment DRAM. When the final size is unknown, estimate conservatively.
|
||||
8. SPIFFS Write Throttling: Never write a settings file on every user interaction. Guard all writes with a value-change check (`if (newVal == _current) return;`). Progress saves during reading must be debounced — write on activity exit or every N page turns, not on every turn. SPIFFS sectors have a finite erase cycle limit.
|
||||
|
||||
---
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Build System: PlatformIO
|
||||
|
||||
**PlatformIO is BOTH a VS Code extension AND a CLI tool**:
|
||||
|
||||
1. **VS Code Extension** (Recommended):
|
||||
* Extension ID: `platformio.platformio-ide` (see `.vscode/extensions.json`)
|
||||
* Provides: Toolbar buttons, IntelliSense, integrated build/upload/monitor
|
||||
* Configuration: `.vscode/c_cpp_properties.json`, `.vscode/tasks.json`
|
||||
* Usage: Click Build (✓), Upload (→), or Monitor (🔌) buttons
|
||||
|
||||
2. **CLI Tool** (`pio` command):
|
||||
* **Installation**: Python package (typically `pip install platformio`)
|
||||
* **Windows Location**: `C:\Users\<user>\AppData\Local\Programs\Python\Python3xx\Scripts\pio.exe`
|
||||
* **Verify**: `which pio` (Git Bash) or `where.exe pio` (cmd)
|
||||
* **Usage**: `pio run`, `pio run -t upload`, etc.
|
||||
|
||||
**Configuration Files**:
|
||||
* `platformio.ini`: Main build configuration (committed to git)
|
||||
* `platformio.local.ini`: Local overrides (gitignored, create if needed)
|
||||
* `partitions.csv`: ESP32 flash partition layout
|
||||
|
||||
### Build Environment
|
||||
* **Standard**: C++20 (`-std=c++2a`). No Exceptions, No RTTI.
|
||||
* **Logging**: ALWAYS use `LOG_INF`, `LOG_DBG`, or `LOG_ERR` from `Logging.h`. Raw Serial output is deprecated.
|
||||
* **Environments** (in `platformio.ini`):
|
||||
* `default`: Development (LOG_LEVEL=2, serial enabled)
|
||||
* `gh_release`: Production (LOG_LEVEL=0)
|
||||
* `gh_release_rc`: Release candidate (LOG_LEVEL=1)
|
||||
* `slim`: Minimal build (no serial logging)
|
||||
|
||||
### Critical Build Flags
|
||||
These flags in `platformio.ini` fundamentally affect firmware behavior:
|
||||
|
||||
```cpp
|
||||
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1 // Single framebuffer (saves 48KB RAM!)
|
||||
-DARDUINO_USB_MODE=1 // Enable USB CDC
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1 // Serial available immediately at boot
|
||||
-DXML_CONTEXT_BYTES=1024 // XML parser memory limit (EPUB parsing)
|
||||
-DUSE_UTF8_LONG_NAMES=1 // SD card long filename support
|
||||
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1 // Avoid zlib name conflicts
|
||||
-DXML_GE=0 // Disable XML general entities (security)
|
||||
```
|
||||
|
||||
**SINGLE_BUFFER_MODE implications**:
|
||||
- Only ONE framebuffer exists (not double-buffered)
|
||||
- Grayscale rendering requires temporary buffer allocation (`renderer.storeBwBuffer()`)
|
||||
- Must call `renderer.restoreBwBuffer()` to free temporary buffers
|
||||
- See [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp) for malloc usage
|
||||
|
||||
### Directory Structure
|
||||
* lib/: Internal libraries (Epub engine, GfxRenderer, UITheme, I18n)
|
||||
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
|
||||
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
|
||||
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
|
||||
* open-x4-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
|
||||
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
|
||||
|
||||
### Hardware Abstraction Layer (HAL)
|
||||
|
||||
**CRITICAL**: Always use HAL classes, NOT SDK classes directly.
|
||||
|
||||
| HAL Class | Wraps SDK Class | Purpose | Singleton Macro |
|
||||
|-----------|----------------|---------|-----------------|
|
||||
| `HalDisplay` | `EInkDisplay` | E-ink display control | *(none)* |
|
||||
| `HalGPIO` | `InputManager` | Button input handling | *(none)* |
|
||||
| `HalStorage` | `SDCardManager` | SD card file I/O | `Storage` |
|
||||
|
||||
**Location**: [lib/hal/](../lib/hal/)
|
||||
|
||||
**Why HAL?**
|
||||
- Provides consistent error logging per module
|
||||
- Abstracts SDK implementation details
|
||||
- Centralizes resource management
|
||||
|
||||
**Example - HalStorage**:
|
||||
```cpp
|
||||
#include <HalStorage.h>
|
||||
|
||||
// Use Storage singleton (defined via macro)
|
||||
FsFile file;
|
||||
if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
|
||||
// Read from file
|
||||
file.close(); // Explicit close required
|
||||
}
|
||||
```
|
||||
|
||||
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`.
|
||||
|
||||
---
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Naming Conventions
|
||||
* Classes: PascalCase (e.g., EpubReaderActivity)
|
||||
* Methods/Variables: camelCase (e.g., renderPage())
|
||||
* Constants: UPPER_SNAKE_CASE (e.g., MAX_BUFFER_SIZE)
|
||||
* Private Members: memberVariable (no prefix)
|
||||
* File Names: Match Class names (e.g., EpubReaderActivity.cpp)
|
||||
|
||||
### Header Guards
|
||||
* Use #pragma once for all header files.
|
||||
|
||||
### Memory Safety and RAII
|
||||
* Smart Pointers: Prefer std::unique_ptr. Avoid std::shared_ptr (unnecessary atomic overhead for a single-core RISC-V).
|
||||
* RAII: Use destructors for cleanup, but call file.close() or vTaskDelete() explicitly for deterministic resource release.
|
||||
|
||||
### ESP32-C3 Platform Pitfalls
|
||||
|
||||
#### `std::string_view` and Null Termination
|
||||
`string_view` is *not* null-terminated. Passing `.data()` to any C-style API (`drawText`, `snprintf`, `strcmp`, SdFat file paths) is undefined behaviour when the view is a substring or a view of a non-null-terminated buffer.
|
||||
|
||||
**Rule**: `string_view` is safe only when passing to C++ APIs that accept `string_view`. For any C API boundary, convert explicitly:
|
||||
```cpp
|
||||
// WRONG - undefined behaviour if view is a substring:
|
||||
renderer.drawText(font, x, y, myView.data(), true);
|
||||
|
||||
// CORRECT - guaranteed null-terminated:
|
||||
renderer.drawText(font, x, y, std::string(myView).c_str(), true);
|
||||
|
||||
// CORRECT - for short strings, use a stack buffer:
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%.*s", (int)myView.size(), myView.data());
|
||||
```
|
||||
|
||||
#### `IRAM_ATTR` and Flash Cache Safety
|
||||
All code runs from flash via the instruction cache. During SPI flash operations (OTA write, SPIFFS commit, NVS update) the cache is briefly suspended. Any code that can execute during this window — ISRs in particular — must reside in IRAM or it will crash silently.
|
||||
|
||||
```cpp
|
||||
// ISR handler: must be in IRAM
|
||||
void IRAM_ATTR gpioISR() { ... }
|
||||
|
||||
// Data accessed from IRAM_ATTR code: must be in DRAM, never a flash const
|
||||
static DRAM_ATTR uint32_t isrEventFlags = 0;
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- All ISR handlers: `IRAM_ATTR`
|
||||
- Data read by `IRAM_ATTR` code: `DRAM_ATTR` (a flash-resident `static const` will fault)
|
||||
- Normal task code does **not** need `IRAM_ATTR`
|
||||
|
||||
#### ISR vs Task Shared State
|
||||
`xSemaphoreTake()` (mutex) **cannot** be called from ISR context — it will crash. Use the correct primitive for each communication direction:
|
||||
|
||||
| Direction | Correct primitive |
|
||||
|---|---|
|
||||
| ISR → task (data) | `xQueueSendFromISR()` + `portYIELD_FROM_ISR()` |
|
||||
| ISR → task (signal) | `xSemaphoreGiveFromISR()` + `portYIELD_FROM_ISR()` |
|
||||
| Task → task | `xSemaphoreTake()` / mutex |
|
||||
| Simple flag (single writer ISR) | `volatile bool` + `portENTER_CRITICAL_ISR()` |
|
||||
|
||||
#### RISC-V Alignment
|
||||
ESP32-C3 faults on unaligned multi-byte loads. Never cast a `uint8_t*` buffer to a wider pointer type and dereference it directly. Use `memcpy` for any unaligned read:
|
||||
|
||||
```cpp
|
||||
// WRONG — faults if buf is not 4-byte aligned:
|
||||
uint32_t val = *reinterpret_cast<const uint32_t*>(buf);
|
||||
|
||||
// CORRECT:
|
||||
uint32_t val;
|
||||
memcpy(&val, buf, sizeof(val));
|
||||
```
|
||||
|
||||
This applies to all cache deserialization code and any raw buffer-to-struct casting. `__attribute__((packed))` structs have the same hazard when accessed via member reference.
|
||||
|
||||
#### Template and `std::function` Bloat
|
||||
Each template instantiation generates a separate binary copy. `std::function<void()>` adds ~2–4 KB per unique signature and heap-allocates its closure. Avoid both in library code and any path called from the render loop:
|
||||
|
||||
```cpp
|
||||
// Avoid — heap-allocating, large binary footprint:
|
||||
std::function<void()> callback;
|
||||
|
||||
// Prefer — zero overhead:
|
||||
void (*callback)() = nullptr;
|
||||
|
||||
// For member function + context (common activity callback pattern):
|
||||
struct Callback { void* ctx; void (*fn)(void*); };
|
||||
```
|
||||
|
||||
When a template is necessary, limit instantiations: use explicit template instantiation in a `.cpp` file to prevent the compiler from generating duplicates across translation units.
|
||||
|
||||
---
|
||||
|
||||
### Error Handling Philosophy
|
||||
|
||||
**Source**: [src/main.cpp:132-143](../src/main.cpp), [lib/GfxRenderer/GfxRenderer.cpp:10](../lib/GfxRenderer/GfxRenderer.cpp)
|
||||
|
||||
**Pattern Hierarchy**:
|
||||
1. **LOG_ERR + return false** (90%): `LOG_ERR("MOD", "Failed: %s", reason); return false;`
|
||||
2. **LOG_ERR + fallback**: `LOG_ERR("MOD", "Unavailable"); useDefault();`
|
||||
3. **assert(false)**: Only for fatal "impossible" states (framebuffer missing)
|
||||
4. **ESP.restart()**: Only for recovery (OTA complete)
|
||||
|
||||
**Rules**: NO exceptions, NO abort(), ALWAYS log before error return
|
||||
|
||||
### Acceptable malloc/free Patterns
|
||||
|
||||
**Source**: [src/activities/home/HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
|
||||
|
||||
Despite "prefer stack allocation," malloc is acceptable for:
|
||||
1. **Large temporary buffers** (> 256 bytes, won't fit on stack)
|
||||
2. **One-time allocations** during activity initialization
|
||||
3. **Bitmap rendering buffers** (variable size, used briefly)
|
||||
|
||||
**Pattern**:
|
||||
```cpp
|
||||
// Allocate
|
||||
auto* buffer = static_cast<uint8_t*>(malloc(bufferSize));
|
||||
if (!buffer) {
|
||||
LOG_ERR("MODULE", "malloc failed: %d bytes", bufferSize);
|
||||
return false; // Handle allocation failure
|
||||
}
|
||||
|
||||
// Use buffer
|
||||
processData(buffer, bufferSize);
|
||||
|
||||
// Free immediately after use
|
||||
free(buffer);
|
||||
buffer = nullptr;
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- **ALWAYS check for nullptr** after malloc
|
||||
- **Free immediately** after use (don't hold across multiple operations)
|
||||
- **Set to nullptr** after free (avoid use-after-free)
|
||||
- **Document size**: Comment why stack allocation was rejected
|
||||
|
||||
**Examples in codebase**:
|
||||
- Cover image buffers: [HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp)
|
||||
- Text chunk buffers: [TxtReaderActivity.cpp:259](../src/activities/reader/TxtReaderActivity.cpp)
|
||||
- Bitmap rendering: [GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
|
||||
- OTA update buffer: [OtaUpdater.cpp:40](../src/network/OtaUpdater.cpp)
|
||||
|
||||
---
|
||||
|
||||
## UI and Orientation Guidelines
|
||||
|
||||
### Orientation-Aware Logic
|
||||
* No Hardcoding: Never assume 800 or 480. Use renderer.getScreenWidth() and renderer.getScreenHeight().
|
||||
* Viewable Area: Use renderer.getOrientedViewableTRBL() to stay within physical bezel margins.
|
||||
|
||||
### Logical Button Mapping
|
||||
|
||||
**Source**: [src/MappedInputManager.cpp:20-55](../src/MappedInputManager.cpp)
|
||||
|
||||
Constraint: Physical button positions are fixed on hardware, but their logical functions change based on user settings and screen orientation.
|
||||
|
||||
**Button Categories**:
|
||||
1. **Physical Fixed** (Up/Down side buttons):
|
||||
- `Button::Up` → Always `HalGPIO::BTN_UP`
|
||||
- `Button::Down` → Always `HalGPIO::BTN_DOWN`
|
||||
|
||||
2. **User Remappable** (Front buttons):
|
||||
- `Button::Back` → Maps to `SETTINGS.frontButtonBack` (hardware index)
|
||||
- `Button::Confirm` → Maps to `SETTINGS.frontButtonConfirm`
|
||||
- `Button::Left` → Maps to `SETTINGS.frontButtonLeft`
|
||||
- `Button::Right` → Maps to `SETTINGS.frontButtonRight`
|
||||
|
||||
3. **Reader-Specific** (Page navigation with optional swap):
|
||||
- `Button::PageBack` → Uses side button (swappable via `SETTINGS.sideButtonLayout`)
|
||||
- `Button::PageForward` → Uses side button (swappable)
|
||||
|
||||
**Implementation**:
|
||||
- Activities use **logical buttons** (e.g., `Button::Confirm`)
|
||||
- `MappedInputManager` translates to **physical hardware buttons**
|
||||
- User can remap front buttons in settings
|
||||
- Orientation changes handled separately by renderer coordinate transforms
|
||||
|
||||
**Rule**: Always use `MappedInputManager::Button::*` enums, never raw `HalGPIO::BTN_*` indices (except in ButtonRemapActivity).
|
||||
|
||||
### UITheme (The GUI Macro)
|
||||
* Rule: All UI rendering must go through the GUI macro (UITheme).
|
||||
* Do not hardcode fonts, colors, or positioning. This ensures orientation-aware layout consistency.
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Singleton Access
|
||||
**Available Singletons**:
|
||||
```cpp
|
||||
#define SETTINGS CrossPointSettings::getInstance() // User settings
|
||||
#define APP_STATE CrossPointState::getInstance() // Runtime state
|
||||
#define GUI UITheme::getInstance() // Current theme
|
||||
#define Storage HalStorage::getInstance() // SD card I/O
|
||||
#define I18N I18n::getInstance() // Internationalization
|
||||
```
|
||||
|
||||
### Activity Lifecycle and Memory Management
|
||||
|
||||
**Source**: [src/main.cpp:132-143](../src/main.cpp)
|
||||
|
||||
**CRITICAL**: Activities are **heap-allocated** and **deleted on exit**.
|
||||
|
||||
```cpp
|
||||
// main.cpp navigation pattern
|
||||
void exitActivity() {
|
||||
if (currentActivity) {
|
||||
currentActivity->onExit();
|
||||
delete currentActivity; // Activity deleted here!
|
||||
currentActivity = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void enterNewActivity(Activity* activity) {
|
||||
currentActivity = activity; // Heap-allocated activity
|
||||
currentActivity->onEnter();
|
||||
}
|
||||
```
|
||||
|
||||
**Memory Implications**:
|
||||
- Activity navigation = `delete` old activity + `new` create next activity
|
||||
- Any memory allocated in `onEnter()` MUST be freed in `onExit()`
|
||||
- FreeRTOS tasks MUST be deleted in `onExit()` before activity destruction
|
||||
- File handles MUST be closed in `onExit()`
|
||||
|
||||
**Activity Pattern**:
|
||||
```cpp
|
||||
void onEnter() { Activity::onEnter(); /* alloc: buffer, tasks */ render(); }
|
||||
void loop() { mappedInput.update(); /* handle input */ }
|
||||
void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::onExit(); }
|
||||
```
|
||||
|
||||
**Critical**: Free resources in reverse order. Delete tasks BEFORE activity destruction.
|
||||
|
||||
### FreeRTOS Task Guidelines
|
||||
|
||||
**Source**: [src/activities/util/KeyboardEntryActivity.cpp:45-50](../src/activities/util/KeyboardEntryActivity.cpp)
|
||||
|
||||
**Pattern**: See Activity Lifecycle above. `xTaskCreate(&taskTrampoline, "Name", stackSize, this, 1, &handle)`
|
||||
|
||||
**Stack Sizing** (in BYTES, not words):
|
||||
- **2048**: Simple rendering (most activities)
|
||||
- **4096**: Network, EPUB parsing
|
||||
- Monitor: `uxTaskGetStackHighWaterMark()` if crashes
|
||||
|
||||
**Rules**: Always `vTaskDelete()` in `onExit()` before destruction. Use mutex if shared state.
|
||||
|
||||
### Global Font Loading
|
||||
|
||||
**Source**: [src/main.cpp:40-115](../src/main.cpp)
|
||||
|
||||
**All fonts are loaded as global static objects** at firmware startup:
|
||||
- Bookerly: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic)
|
||||
- Noto Sans: 12, 14, 16, 18pt (4 styles each)
|
||||
- OpenDyslexic: 8, 10, 12, 14pt (4 styles each)
|
||||
- Ubuntu UI fonts: 10, 12pt (2 styles)
|
||||
|
||||
**Total**: ~80+ global `EpdFont` and `EpdFontFamily` objects
|
||||
|
||||
**Compilation Flag**:
|
||||
```cpp
|
||||
#ifndef OMIT_FONTS
|
||||
// Most fonts loaded here
|
||||
#endif
|
||||
```
|
||||
|
||||
**Implications**:
|
||||
- Fonts stored in **Flash** (marked as `static const` in `lib/EpdFont/builtinFonts/`)
|
||||
- Font rendering data cached in **DRAM** when first used
|
||||
- `OMIT_FONTS` can reduce binary size for minimal builds
|
||||
- Font IDs defined in [src/fontIds.h](../src/fontIds.h)
|
||||
|
||||
**Usage**:
|
||||
```cpp
|
||||
#include "fontIds.h"
|
||||
|
||||
renderer.insertFont(FONT_UI_MEDIUM, ui12FontFamily);
|
||||
renderer.drawText(FONT_UI_MEDIUM, x, y, "Hello", true);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing and Debugging
|
||||
|
||||
### Build Commands
|
||||
|
||||
**Via CLI**:
|
||||
```bash
|
||||
# Build firmware (default environment)
|
||||
pio run
|
||||
|
||||
# Build and upload to device
|
||||
pio run -t upload
|
||||
|
||||
# Build specific environment
|
||||
pio run -e gh_release
|
||||
|
||||
# Clean build artifacts
|
||||
pio run -t clean
|
||||
|
||||
# Upload filesystem data (if using SPIFFS/LittleFS)
|
||||
pio run -t uploadfs
|
||||
```
|
||||
|
||||
**Via VS Code**:
|
||||
* Use PlatformIO toolbar: Build (✓), Upload (→), Clean (🗑️)
|
||||
* Or Command Palette: `PlatformIO: Build`, `PlatformIO: Upload`, etc.
|
||||
|
||||
### Monitoring and Debugging
|
||||
|
||||
```bash
|
||||
# Enhanced monitor with color/logging (recommended)
|
||||
python3 scripts/debugging_monitor.py
|
||||
|
||||
# Standard PlatformIO monitor
|
||||
pio device monitor
|
||||
|
||||
# Combined upload + monitor
|
||||
pio run -t upload && pio device monitor
|
||||
```
|
||||
|
||||
**Via VS Code**: Click Monitor (🔌) button in PlatformIO toolbar
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Static analysis (cppcheck)
|
||||
pio check
|
||||
|
||||
# Format code (clang-format) - Windows Git Bash
|
||||
find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i
|
||||
|
||||
# Format code (clang-format) - Linux
|
||||
clang-format -i src/**/*.cpp src/**/*.h
|
||||
```
|
||||
|
||||
### Debugging Crashes
|
||||
|
||||
**Common Crash Causes**:
|
||||
|
||||
1. **Out of Memory** (Most common):
|
||||
```cpp
|
||||
LOG_DBG("MEM", "Free heap: %d bytes", ESP.getFreeHeap());
|
||||
```
|
||||
- Monitor heap usage throughout activity lifecycle
|
||||
- Check if large allocations (>10KB) occur before crash
|
||||
- Verify buffers are freed in `onExit()`
|
||||
|
||||
2. **Stack Overflow**:
|
||||
```cpp
|
||||
LOG_DBG("TASK", "Stack high water: %d", uxTaskGetStackHighWaterMark(taskHandle));
|
||||
```
|
||||
- Occurs during deep recursion or large local variables
|
||||
- Increase task stack size in `xTaskCreate()` (2048 → 4096)
|
||||
- Move large buffers to heap with malloc
|
||||
|
||||
3. **Use-After-Free**:
|
||||
- Activity deleted but task still running
|
||||
- Always `vTaskDelete()` in `onExit()` BEFORE activity destruction
|
||||
- Set pointers to `nullptr` after `free()`
|
||||
|
||||
4. **Corrupt Cache Files**:
|
||||
- Delete `.crosspoint/` directory on SD card
|
||||
- Forces clean re-parse of all EPUBs
|
||||
- Check file format versions in [docs/file-formats.md](../docs/file-formats.md)
|
||||
|
||||
5. **Watchdog Timeout**:
|
||||
- Loop/task blocked for >5 seconds
|
||||
- Add `vTaskDelay(1)` in tight loops
|
||||
- Check for blocking I/O operations
|
||||
|
||||
**Verification Steps**:
|
||||
1. Check serial output for stack traces
|
||||
2. Monitor heap with `ESP.getFreeHeap()` before/after operations
|
||||
3. Verify task deletion with task list (`vTaskList()`)
|
||||
4. Test with `LOG_LEVEL=2` (debug logging enabled)
|
||||
|
||||
---
|
||||
|
||||
## Git Workflow and Repository Awareness
|
||||
|
||||
### Repository Detection Protocol
|
||||
|
||||
**CRITICAL**: ALWAYS verify repository context before git operations. This could be:
|
||||
- A **fork** with `origin` pointing to personal repo, `upstream` to main repo
|
||||
- A **direct clone** with `origin` pointing to main repo
|
||||
- Multiple collaborator remotes
|
||||
|
||||
**Verification Commands** (run at session start):
|
||||
```bash
|
||||
# Check current branch
|
||||
git branch --show-current
|
||||
|
||||
# Check all remotes
|
||||
git remote -v
|
||||
|
||||
# Identify main branch name (could be 'main' or 'master')
|
||||
git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@'
|
||||
|
||||
# Check working tree status
|
||||
git status --short
|
||||
```
|
||||
|
||||
**Example Output** (forked repository):
|
||||
```text
|
||||
origin https://github.com/<your-username>/crosspoint-reader.git (fetch/push)
|
||||
upstream https://github.com/crosspoint-reader/crosspoint-reader.git (fetch/push)
|
||||
```
|
||||
|
||||
### Git Operation Rules
|
||||
|
||||
1. **Never assume branch names**:
|
||||
```bash
|
||||
# Bad: git push origin main
|
||||
# Good: git push origin $(git branch --show-current)
|
||||
```
|
||||
|
||||
2. **Never assume remote names or write permissions**:
|
||||
- **Forked repos**: Push to `origin` (your fork), submit PR to `upstream`
|
||||
- **Direct contributors**: May push feature branches to `upstream`
|
||||
- **Always ask**: "Should I push to origin or create a PR?"
|
||||
|
||||
3. **Check for upstream changes before starting work**:
|
||||
```bash
|
||||
# Sync fork with upstream (if applicable)
|
||||
git fetch upstream
|
||||
git merge upstream/main # or upstream/master
|
||||
```
|
||||
|
||||
4. **Use explicit remote and branch names**:
|
||||
```bash
|
||||
# Check remotes first
|
||||
git remote -v
|
||||
|
||||
# Use explicit syntax
|
||||
git push <remote> <branch>
|
||||
```
|
||||
|
||||
### Branch Naming Convention
|
||||
|
||||
**For feature/fix branches**:
|
||||
```text
|
||||
feature/<short-description> # New features
|
||||
fix/<issue-number>-<description> # Bug fixes
|
||||
refactor/<component-name> # Code refactoring
|
||||
docs/<topic> # Documentation updates
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
- `feature/sd-download-progress`
|
||||
- `fix/123-orientation-crash`
|
||||
- `refactor/hal-storage`
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
**Pattern**:
|
||||
```text
|
||||
<type>: <short summary (50 chars max)>
|
||||
|
||||
<optional detailed description>
|
||||
|
||||
```
|
||||
|
||||
**Types**: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`
|
||||
|
||||
**Example**:
|
||||
```text
|
||||
feat: add real-time SD download progress bar
|
||||
|
||||
Implements progress tracking for book downloads using
|
||||
UITheme progress bar component with heap-safe updates.
|
||||
|
||||
Tested in all 4 orientations with 5MB+ files.
|
||||
```
|
||||
|
||||
### When to Commit
|
||||
|
||||
**DO commit when**:
|
||||
- User explicitly requests: "commit these changes"
|
||||
- Feature is complete and tested on device
|
||||
- Bug fix is verified working
|
||||
- Refactoring preserves all functionality
|
||||
- All tests pass (`pio run` succeeds)
|
||||
|
||||
**DO NOT commit when**:
|
||||
- Changes are untested on actual hardware
|
||||
- Build fails or has warnings
|
||||
- Experimenting or debugging in progress
|
||||
- User hasn't explicitly requested commit
|
||||
- Files excluded by `.gitignore` would be included — always run `git status` and cross-check against `.gitignore` before staging (e.g., `*.generated.h`, `.pio/`, `compile_commands.json`, `platformio.local.ini`)
|
||||
|
||||
**Rule**: **If uncertain, ASK before committing.**
|
||||
|
||||
---
|
||||
|
||||
## Generated Files and Build Artifacts
|
||||
|
||||
### Files Generated by Build Scripts
|
||||
|
||||
**NEVER manually edit these files** - they are regenerated automatically:
|
||||
|
||||
1. **HTML Headers** (generated by `scripts/build_html.py`):
|
||||
- `src/network/html/*.generated.h`
|
||||
- **Source**: HTML templates in `data/html/` directory
|
||||
- **Triggered**: During PlatformIO `pre:` build step
|
||||
- **To modify**: Edit source HTML in `data/html/`, not generated headers
|
||||
|
||||
2. **I18n Headers** (generated by `scripts/gen_i18n.py`):
|
||||
- `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.h`, `lib/I18n/I18nStrings.cpp`
|
||||
- **Source**: YAML translation files in `lib/I18n/translations/` (one per language)
|
||||
- **To modify**: Edit source YAML files, then run `python scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
|
||||
- **Commit**: Source YAML files only. All three generated files (`I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`) are in `.gitignore` and regenerated at build time.
|
||||
|
||||
3. **Build Artifacts** (in `.gitignore`):
|
||||
- `.pio/` - PlatformIO build output
|
||||
- `build/` - Compiled binaries
|
||||
- `*.generated.h` - Any auto-generated headers
|
||||
- `compile_commands.json` - LSP/IDE metadata
|
||||
|
||||
### Modifying Generated Content Workflow
|
||||
|
||||
**To change HTML pages**:
|
||||
1. Edit source: `data/html/<pagename>.html`
|
||||
2. Build: `pio run` (auto-triggers `scripts/build_html.py`)
|
||||
3. Generated headers update: `src/network/html/<pagename>Html.generated.h`
|
||||
4. **Commit ONLY** source HTML, NOT generated `.generated.h` files
|
||||
|
||||
**To add/modify translations (i18n)**:
|
||||
1. Edit or add YAML file: `lib/I18n/translations/<language>.yaml`
|
||||
- Each file must contain: `_language_name`, `_language_code`, `_order`, and `STR_*` keys
|
||||
- English (`english.yaml`) is the reference; missing keys in other languages fall back to English
|
||||
2. Run generator: `python scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
|
||||
3. Generated files update: `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
|
||||
4. **Commit** source YAML files only. All three generated files are in `.gitignore` and regenerated at build time.
|
||||
|
||||
**To use translated strings in code**:
|
||||
```cpp
|
||||
#include <I18n.h>
|
||||
// Use tr() macro with StrId enum (defined in generated I18nKeys.h)
|
||||
renderer.drawText(FONT_UI, x, y, tr(STR_LOADING), true);
|
||||
```
|
||||
|
||||
**To add custom fonts**:
|
||||
1. Place source fonts in `lib/EpdFont/fontsrc/` (gitignored)
|
||||
2. Run conversion script (see `lib/EpdFont/README`)
|
||||
3. Update global font objects in `src/main.cpp:40-115`
|
||||
4. Add font ID constant to `src/fontIds.h`
|
||||
|
||||
---
|
||||
|
||||
## Local Development Configuration
|
||||
|
||||
### platformio.local.ini (Personal Overrides)
|
||||
|
||||
**Purpose**: Personal development settings that should NEVER be committed.
|
||||
|
||||
**Use Cases**:
|
||||
- Serial port configuration (varies by machine)
|
||||
- Debug flags for specific testing
|
||||
- Local build optimizations
|
||||
- Developer-specific paths
|
||||
|
||||
**Example** `platformio.local.ini`:
|
||||
```ini
|
||||
# platformio.local.ini (gitignored)
|
||||
[env:default]
|
||||
upload_port = COM7 # Windows: COMx, Linux: /dev/ttyUSBx
|
||||
monitor_port = COM7
|
||||
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DMY_DEBUG_FLAG=1 # Personal debug flags
|
||||
-DTEST_FEATURE_ENABLED=1
|
||||
```
|
||||
|
||||
**Configuration Hierarchy**:
|
||||
1. `platformio.ini` - **Committed**, shared project settings
|
||||
2. `platformio.local.ini` - **Gitignored**, personal overrides
|
||||
3. Local file extends/overrides base config
|
||||
|
||||
**Rules**:
|
||||
- **NEVER commit** `platformio.local.ini`
|
||||
- **NEVER put** personal info (serial ports, credentials) in main `platformio.ini`
|
||||
- Use `${base.build_flags}` to extend (not replace) base flags
|
||||
|
||||
---
|
||||
|
||||
## Testing and Verification Workflow
|
||||
|
||||
### Testing Checklist
|
||||
|
||||
**AI agent scope** (what you CAN verify):
|
||||
1. ✅ **Build**: `pio run -t clean && pio run` (0 errors/warnings)
|
||||
2. ✅ **Quality**: `pio check` + `find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i`
|
||||
3. ✅ **Format**: Commit messages (`feat:`/`fix:`), no `.gitignore`-excluded files staged (e.g., `*.generated.h`, `.pio/`, `platformio.local.ini`)
|
||||
4. ✅ **CI**: Fix GitHub Actions failures before review
|
||||
5. ✅ **Code review**: Ensure orientation-aware logic is correct in all 4 modes by inspecting switch/case coverage
|
||||
|
||||
**Human tester scope** (flag these for the user):
|
||||
6. 🔲 **Device**: Test on hardware
|
||||
7. 🔲 **Orientations**: Verify all 4 modes (Portrait/Inverted/Landscape CW/CCW)
|
||||
8. 🔲 **Heap**: `ESP.getFreeHeap()` > 50KB, no leaks
|
||||
9. 🔲 **Cache**: If EPUB modified, delete `.crosspoint/` and verify re-parse
|
||||
|
||||
### CI/CD Pipeline Awareness
|
||||
|
||||
**GitHub Actions** run automatically on pull requests:
|
||||
|
||||
| Workflow | File | Purpose |
|
||||
|----------|------|---------|
|
||||
| Build Check | `.github/workflows/ci.yml` | Verifies code compiles |
|
||||
| Format Check | `.github/workflows/pr-formatting-check.yml` | Validates clang-format |
|
||||
| Release Build | `.github/workflows/release.yml` | Production releases |
|
||||
| RC Build | `.github/workflows/release_candidate.yml` | Release candidates |
|
||||
|
||||
**Rules**:
|
||||
- **Fix CI failures BEFORE** requesting review
|
||||
- CI runs on: Push to PR, PR updates
|
||||
- Format check fails → Run clang-format locally
|
||||
- Build check fails → Fix compile errors
|
||||
|
||||
---
|
||||
|
||||
## Serial Monitoring and Live Debugging
|
||||
|
||||
### Serial Monitor Options
|
||||
|
||||
1. **Enhanced**: `python3 scripts/debugging_monitor.py` (color-coded, recommended)
|
||||
2. **Standard**: `pio device monitor` (basic, no colors)
|
||||
3. **VS Code**: Monitor (🔌) button (IDE-integrated)
|
||||
|
||||
### Live Debugging Patterns
|
||||
|
||||
**Heap**: `LOG_DBG("MEM", "Free: %d", ESP.getFreeHeap());` (every 5s in loop)
|
||||
**Stack**: `uxTaskGetStackHighWaterMark(nullptr)` (< 512 bytes → increase stack)
|
||||
**Flush**: `logSerial.flush();` (force output before crash)
|
||||
|
||||
**Port Detection**: Windows: `mode` | Linux: `ls /dev/ttyUSB* /dev/ttyACM*` or `dmesg | grep tty`
|
||||
|
||||
---
|
||||
|
||||
## Cache Management and Invalidation
|
||||
|
||||
### Cache Structure on SD Card
|
||||
|
||||
**Location**: `.crosspoint/` directory on SD card root
|
||||
|
||||
**Structure**: `.crosspoint/epub_<hash>/{book.bin, progress.bin, cover.bmp, sections/*.bin}`
|
||||
|
||||
**Hash**: `std::hash<std::string>{}(filepath)` → Moving/renaming file = new hash = lost progress
|
||||
|
||||
### Cache Invalidation Rules
|
||||
|
||||
**Cache is automatically invalidated when**:
|
||||
1. **File format version changes** (see `docs/file-formats.md`)
|
||||
- `book.bin` version number incremented
|
||||
- `section.bin` version number incremented
|
||||
2. **Render settings change**:
|
||||
- Font family or size (`SETTINGS.fontFamily`, `SETTINGS.fontSize`)
|
||||
- Line spacing (`SETTINGS.lineSpacing`)
|
||||
- Paragraph spacing (`SETTINGS.extraParagraphSpacing`)
|
||||
- Screen margins (`SETTINGS.screenMargin`)
|
||||
3. **Viewport dimensions change**:
|
||||
- Screen orientation change
|
||||
- Display resolution change
|
||||
4. **Book file modified**:
|
||||
- Moved, renamed, or content changed (new hash)
|
||||
|
||||
**Manual Cache Clear** (safe operations):
|
||||
```bash
|
||||
# Delete ALL caches (forces full regeneration)
|
||||
rm -rf /path/to/sd/.crosspoint/
|
||||
|
||||
# Delete specific book cache
|
||||
rm -rf /path/to/sd/.crosspoint/epub_<hash>/
|
||||
|
||||
# Keep progress, delete only rendered sections
|
||||
rm -rf /path/to/sd/.crosspoint/epub_<hash>/sections/
|
||||
```
|
||||
|
||||
**When to Clear Cache**:
|
||||
- EPUB parsing errors after code changes to `lib/Epub/`
|
||||
- Corrupt rendering (missing text, wrong layout)
|
||||
- Testing cache generation logic
|
||||
- After modifying:
|
||||
- `lib/Epub/Epub/Section.cpp`
|
||||
- `lib/Epub/Epub/BookMetadataCache.cpp`
|
||||
- Render settings in `CrossPointSettings`
|
||||
|
||||
### Cache File Format Versioning
|
||||
|
||||
**Source**: `lib/Epub/Epub/Section.cpp`, `lib/Epub/Epub/BookMetadataCache.cpp`
|
||||
|
||||
**Current Versions** (as of docs/file-formats.md):
|
||||
- `book.bin`: **Version 5** (metadata structure)
|
||||
- `section.bin`: **Version 12** (layout structure)
|
||||
|
||||
**Version Increment Rules**:
|
||||
1. **ALWAYS increment version** BEFORE changing binary structure
|
||||
2. Version mismatch → Cache auto-invalidated and regenerated
|
||||
3. Document format changes in `docs/file-formats.md`
|
||||
|
||||
**Example** (incrementing section format version):
|
||||
```cpp
|
||||
// lib/Epub/Epub/Section.cpp
|
||||
static constexpr uint8_t SECTION_FILE_VERSION = 13; // Was 12, now 13
|
||||
|
||||
// Add new field to structure
|
||||
struct PageLine {
|
||||
// ... existing fields ...
|
||||
uint16_t newField; // New field added
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Philosophy: We are building a dedicated e-reader, not a Swiss Army knife. If a feature adds RAM pressure without significantly improving the reading experience, it is Out of Scope.
|
||||
@@ -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.
|
||||
@@ -26,7 +26,7 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
||||
## Features & Usage
|
||||
|
||||
- [x] EPUB parsing and rendering (EPUB 2 and EPUB 3)
|
||||
- [ ] Image support within EPUB
|
||||
- [x] Image support within EPUB
|
||||
- [x] Saved reading position
|
||||
- [x] File explorer with file picker
|
||||
- [x] Basic EPUB picker from root directory
|
||||
@@ -36,6 +36,7 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
||||
- [x] Cover sleep screen
|
||||
- [x] Wifi book upload
|
||||
- [x] Wifi OTA updates
|
||||
- [x] KOReader Sync integration for cross-device reading progress
|
||||
- [x] Configurable font, layout, and display options
|
||||
- [ ] User provided fonts
|
||||
- [ ] Full UTF support
|
||||
@@ -43,13 +44,16 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
||||
|
||||
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.
|
||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint, including the
|
||||
[KOReader Sync quick setup](./USER_GUIDE.md#365-koreader-sync-quick-setup).
|
||||
|
||||
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
|
||||
@@ -58,7 +62,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
|
||||
@@ -82,7 +86,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
|
||||
@@ -95,6 +99,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
|
||||
|
||||
@@ -133,9 +156,14 @@ 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 are new to the codebase, start with the [contributing docs](./docs/contributing/README.md).
|
||||
|
||||
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,58 @@
|
||||
# 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.
|
||||
|
||||
### In-scope — Technically Unsupported
|
||||
|
||||
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
|
||||
|
||||
* **Clock Display:** The ESP32-C3's RTC drifts significantly during deep sleep; making the clock untrustworthy after any sleep cycle. NTP sync could help, but CrossPoint doesn't connect to the internet on every boot.
|
||||
|
||||
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
|
||||
|
||||
## 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!
|
||||
+252
-65
@@ -10,22 +10,31 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
||||
- [First Launch](#first-launch)
|
||||
- [3. Screens](#3-screens)
|
||||
- [3.1 Home Screen](#31-home-screen)
|
||||
- [3.2 Book Selection](#32-book-selection)
|
||||
- [3.3 Reading Mode](#33-reading-mode)
|
||||
- [3.4 File Upload Screen](#34-file-upload-screen)
|
||||
- [3.5 Settings](#35-settings)
|
||||
- [3.6 Sleep Screen](#36-sleep-screen)
|
||||
- [3.2 Reading Mode](#32-reading-mode)
|
||||
- [3.3 Browse Files Screen](#33-browse-files-screen)
|
||||
- [3.4 Recent Books Screen](#34-recent-books-screen)
|
||||
- [3.5 File Transfer Screen](#35-file-transfer-screen)
|
||||
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
|
||||
- [3.6 Settings](#36-settings)
|
||||
- [3.6.1 Display](#361-display)
|
||||
- [3.6.2 Reader](#362-reader)
|
||||
- [3.6.3 Controls](#363-controls)
|
||||
- [3.6.4 System](#364-system)
|
||||
- [3.6.5 KOReader Sync Quick Setup](#365-koreader-sync-quick-setup)
|
||||
- [3.7 Sleep Screen](#37-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):
|
||||
The device utilises the standard buttons on the Xteink X4 (in the same layout as the manufacturer firmware, by default):
|
||||
|
||||
### Button Layout
|
||||
| Location | Buttons |
|
||||
@@ -33,7 +42,12 @@ The device utilises the standard buttons on the Xtink X4 (in the same layout as
|
||||
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
|
||||
| **Right Side** | **Power**, **Volume Up**, **Volume Down**, **Reset** |
|
||||
|
||||
Button layout can be customized in **[Settings](#35-settings)**.
|
||||
Button layout can be customized in the **[Controls Settings](#363-controls)**.
|
||||
|
||||
### Taking a Screenshot
|
||||
When the Power Button and Volume Down button are pressed at the same time, it will take a screenshot and save it in the folder `screenshots/`.
|
||||
|
||||
Alternatively, while reading a book, press the **Confirm** button to open the reader menu and select **Take screenshot**.
|
||||
|
||||
---
|
||||
|
||||
@@ -42,9 +56,9 @@ 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 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.
|
||||
In the **[Controls Settings](#363-controls)** you can configure the power button to turn the device off with a short press instead of a long one.
|
||||
|
||||
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.
|
||||
To reboot the device (for example after a firmware update or if it's frozen), press and release the Reset button, and then quickly press and hold the Power button for a few seconds.
|
||||
|
||||
### First Launch
|
||||
|
||||
@@ -59,29 +73,34 @@ 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, the **[Browse Files](#33-browse-files-screen)** screen, the **[Recent Books](#34-recent-books-screen)** screen, the **[File Transfer](#35-file-transfer-screen)** screen, or **[Settings](#36-settings)**.
|
||||
|
||||
### 3.2 Book Selection
|
||||
|
||||
The Book Selection acts as a folder and file browser.
|
||||
|
||||
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down.
|
||||
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
|
||||
|
||||
### 3.3 Reading Mode
|
||||
### 3.2 Reading Mode
|
||||
|
||||
See [Reading Mode](#4-reading-mode) below for more information.
|
||||
|
||||
### 3.4 File Upload Screen
|
||||
### 3.3 Browse Files 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 Browse Files screen acts as a file and folder browser.
|
||||
|
||||
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down.
|
||||
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
|
||||
* **Delete Files:** Hold and release **Confirm** to delete the selected file. You will be given an option to either confirm or cancel deletion. Folder deletion is not supported.
|
||||
|
||||
### 3.4 Recent Books Screen
|
||||
|
||||
The Recent Books screen lists the most recently opened books in a chronological view, displaying title and author.
|
||||
|
||||
### 3.5 File Transfer Screen
|
||||
|
||||
The File Transfer 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
|
||||
### 3.5.1 Calibre Wireless Transfers
|
||||
|
||||
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
|
||||
|
||||
@@ -93,73 +112,226 @@ CrossPoint supports sending books from Calibre using the CrossPoint Reader devic
|
||||
3. Make sure your computer is on the same WiFi network.
|
||||
4. In Calibre, click "Send to device" to transfer books.
|
||||
|
||||
### 3.5 Settings
|
||||
### 3.6 Settings
|
||||
|
||||
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
|
||||
|
||||
#### 3.6.1 Display
|
||||
|
||||
- **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](#37-sleep-screen) below for more information
|
||||
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
||||
- "None" - A blank screen
|
||||
- "Cover + Custom" - The book cover image, falls back 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
|
||||
- "Crop" - Scale the image down and crop as necessary to try 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
|
||||
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
|
||||
- **Status Bar**: Configure the status bar displayed while reading:
|
||||
- "None" - No status bar
|
||||
- "No Progress" - Show status bar without reading progress
|
||||
- "Full" - Show status bar with reading progress
|
||||
- **Hide Battery %**: Configure where to suppress the battery pecentage display in the status bar; the battery icon will still be shown:
|
||||
- "Never" - Always show battery percentage (default)
|
||||
- "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 percentage display in the status bar; the battery icon will still be shown:
|
||||
- "Never" (default) - Always show battery percentage
|
||||
- "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
|
||||
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting; options are every 1, 5, 10, 15, or 30 pages.
|
||||
|
||||
- **UI Theme**: Set which UI theme to use:
|
||||
- "Classic" - The original Crosspoint theme
|
||||
- "Lyra" - The new theme for Crosspoint featuring rounded elements and menu icons
|
||||
- "Lyra Extended" - Lyra, but displays 3 books instead of 1 on the **[Home Screen](#31-home-screen)**
|
||||
- **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
|
||||
|
||||
#### 3.6.2 Reader
|
||||
- **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" (default), "Large", or "X Large".
|
||||
|
||||
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal" (default), or "Wide".
|
||||
- **Reader Screen Margin**: Controls the screen margins in Reading Mode between 5 and 40 pixels in 5-pixel increments.
|
||||
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
|
||||
- **Embedded Style**: Whether to use the EPUB file's embedded HTML and CSS stylisation and formatting; options are "ON" or "OFF".
|
||||
- **Hyphenation**: Whether to hyphenate text in Reading Mode; options are "ON" or "OFF".
|
||||
- **Reading Orientation**: Set the screen orientation for reading EPUB files:
|
||||
- "Portrait" (default) - Standard portrait orientation
|
||||
- "Landscape CW" - Landscape, rotated clockwise
|
||||
- "Inverted" - Portrait, upside down
|
||||
- "Landscape CCW" - Landscape, rotated counter-clockwise
|
||||
- **Front Button Layout**: Configure the order of the bottom edge buttons:
|
||||
- Back, Confirm, Left, Right (default)
|
||||
- Left, Right, Back, Confirm
|
||||
- Left, Back, Confirm, Right
|
||||
- Back, Confirm, Right, Left
|
||||
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
|
||||
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
|
||||
- **Extra Paragraph Spacing**: Set how to handle paragraph breaks:
|
||||
- "ON" - Vertical space will be added between paragraphs in Reading Mode
|
||||
- "OFF" - Paragraphs will not have vertical space added, but will have first-line indentation
|
||||
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
|
||||
|
||||
#### 3.6.3 Controls
|
||||
|
||||
- **Remap Front Buttons**: A menu for customising the function of each bottom edge button.
|
||||
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from "Prev/Next" (default) to "Next/Prev". This change is only in effect when reading.
|
||||
|
||||
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skips to the next/previous chapter:
|
||||
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
|
||||
- "Page Scroll" - Long-pressing scrolls a page up/down
|
||||
- Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
|
||||
- **Reader Font Family**: Choose the font used for reading:
|
||||
- "Bookerly" (default) - Amazon's reading font
|
||||
- "Noto Sans" - Google's sans-serif font
|
||||
- "Open Dyslexic" - Font designed for readers with dyslexia
|
||||
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium", "Large", or "X Large".
|
||||
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal", or "Wide".
|
||||
- **Reader Screen Margin**: Controls the screen margins in reader mode between 5 and 40 pixels in 5 pixel increments.
|
||||
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
|
||||
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
|
||||
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
|
||||
- **Short Power Button Click**: Controls the effect of a short click of the power button:
|
||||
- "Ignore" (default) - Require a long press to turn off the device
|
||||
- "Sleep" - A short press puts the device into sleep mode
|
||||
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
|
||||
|
||||
#### 3.6.4 System
|
||||
|
||||
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep; options are 1, 5, 10 (default), 15 or 30 minutes.
|
||||
|
||||
- **WiFi Networks**: Connect to WiFi networks for file transfers and firmware updates.
|
||||
- **KOReader Sync**: Options for setting up KOReader for syncing book progress.
|
||||
- **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.
|
||||
- **Clear Reading Cache**: Clear the internal SD card cache.
|
||||
- **Check for updates**: Check for Crosspoint firmware updates over WiFi.
|
||||
- **Language**: Set the system language (see **[Supported Languages](#supported-languages)** for more information).
|
||||
|
||||
### 3.6 Sleep Screen
|
||||
#### 3.6.5 KOReader Sync Quick Setup
|
||||
|
||||
You can customize the sleep screen by placing custom images in specific locations on the SD card:
|
||||
CrossPoint can sync reading progress with KOReader-compatible sync servers.
|
||||
It also interoperates with KOReader apps/devices when they use the same server and credentials.
|
||||
|
||||
- **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.
|
||||
##### Option A: Free Public Server (`sync.koreader.rocks`)
|
||||
|
||||
1. Register a user once (only if needed):
|
||||
|
||||
```bash
|
||||
USERNAME="user"
|
||||
PASSWORD="pass"
|
||||
PASSWORD_MD5="$(printf '%s' "$PASSWORD" | openssl md5 | awk '{print $2}')"
|
||||
|
||||
curl -i "https://sync.koreader.rocks/users/create" \
|
||||
-H "Accept: application/vnd.koreader.v1+json" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
|
||||
```
|
||||
|
||||
Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices.
|
||||
|
||||
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
|
||||
|
||||
2. On each CrossPoint device:
|
||||
- Go to **Settings -> System -> KOReader Sync**.
|
||||
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
|
||||
- Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server).
|
||||
- Run **Authenticate**.
|
||||
|
||||
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
|
||||
- Choose **Apply Remote** to jump to remote progress.
|
||||
- Choose **Upload Local** to push current progress.
|
||||
|
||||
##### Option B: Self-Hosted Server (Docker Compose)
|
||||
|
||||
1. Start a sync server:
|
||||
|
||||
```bash
|
||||
mkdir -p kosync-quickstart
|
||||
cd kosync-quickstart
|
||||
|
||||
cat > compose.yaml <<'YAML'
|
||||
services:
|
||||
kosync:
|
||||
image: koreader/kosync:latest
|
||||
ports:
|
||||
- "7200:7200"
|
||||
- "17200:17200"
|
||||
volumes:
|
||||
- ./data/redis:/var/lib/redis
|
||||
environment:
|
||||
- ENABLE_USER_REGISTRATION=true
|
||||
restart: unless-stopped
|
||||
YAML
|
||||
|
||||
# Docker
|
||||
docker compose up -d
|
||||
|
||||
# Podman (alternative)
|
||||
podman compose up -d
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
|
||||
> `ENABLE_USER_REGISTRATION=true` is convenient for first setup. After creating your users, set it to `false` (or remove it) to avoid unexpected registrations.
|
||||
|
||||
2. Verify the server:
|
||||
|
||||
```bash
|
||||
curl -H "Accept: application/vnd.koreader.v1+json" "http://<server-ip>:17200/healthcheck"
|
||||
# Expected: {"state":"OK"}
|
||||
```
|
||||
|
||||
3. Register a user once.
|
||||
CrossPoint authenticates against KOReader Sync (`koreader/kosync`) using an MD5 key, so register using the MD5 of your password:
|
||||
|
||||
> [!WARNING]
|
||||
> Sending a reusable MD5-derived password over plain HTTP is insecure.
|
||||
> Create unique sync-only credentials and do not reuse main account passwords.
|
||||
> Prefer `https://<server-ip>:7200` whenever traffic leaves a fully trusted LAN or when using untrusted networks.
|
||||
> Use `curl -k` only for self-signed certificate testing.
|
||||
|
||||
```bash
|
||||
USERNAME="user"
|
||||
PASSWORD="pass"
|
||||
PASSWORD_MD5="$(printf '%s' "$PASSWORD" | openssl md5 | awk '{print $2}')"
|
||||
|
||||
curl -i "http://<server-ip>:17200/users/create" \
|
||||
-H "Accept: application/vnd.koreader.v1+json" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
|
||||
```
|
||||
|
||||
If this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, the account already exists.
|
||||
|
||||
4. On each CrossPoint device:
|
||||
- Go to **Settings -> System -> KOReader Sync**.
|
||||
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
|
||||
- Set **Sync Server URL** to `http://<server-ip>:17200`.
|
||||
- Run **Authenticate**.
|
||||
|
||||
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
|
||||
|
||||
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
|
||||
- Choose **Apply Remote** to jump to remote progress.
|
||||
- Choose **Upload Local** to push current progress.
|
||||
|
||||
### 3.7 Sleep Screen
|
||||
|
||||
The **Sleep Screen** setting controls what is displayed when the device goes to sleep:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| **Dark** (default) | The CrossPoint logo on a dark background. |
|
||||
| **Light** | The CrossPoint logo on a white background. |
|
||||
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
|
||||
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
|
||||
| **Cover + Custom** | The cover of the currently open book. Falls back to **Custom** behavior if no book is open. |
|
||||
| **None** | A blank screen. |
|
||||
|
||||
#### Cover settings
|
||||
|
||||
When using **Cover** or **Cover + Custom**, two additional settings apply:
|
||||
|
||||
- **Sleep Screen Cover Mode**: **Fit** (scale to fit, white borders) or **Crop** (scale and crop to fill the screen).
|
||||
- **Sleep Screen Cover Filter**: **None** (grayscale), **Contrast** (black & white), or **Inverted** (inverted black & white).
|
||||
|
||||
#### Custom images
|
||||
|
||||
To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + Custom**, then place images on the SD card:
|
||||
|
||||
- **Multiple Images (recommended):** Create a `.sleep` directory in the root of the SD card and place any number of `.bmp` images inside. One will be randomly selected each time the device sleeps. (A directory named `sleep` is also accepted as a fallback.)
|
||||
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This is used as a fallback if no valid images are found in the `.sleep`/`sleep` directory.
|
||||
|
||||
> [!TIP]
|
||||
> For best results:
|
||||
@@ -178,7 +350,7 @@ Once you have opened a book, the button layout changes to facilitate reading.
|
||||
| **Previous Page** | Press **Left** _or_ **Volume Up** |
|
||||
| **Next Page** | Press **Right** _or_ **Volume Down** |
|
||||
|
||||
The role of the volume (side) buttons can be swapped in **[Settings](#35-settings)**.
|
||||
The role of the volume (side) buttons can be swapped in the **[Controls Settings](#363-controls)**.
|
||||
|
||||
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.
|
||||
|
||||
@@ -186,13 +358,13 @@ If the **Short Power Button Click** setting is set to "Page Turn", you can also
|
||||
* **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.
|
||||
This feature can be disabled in the **[Controls Settings](#363-controls)** 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** 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)**.
|
||||
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
||||
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
|
||||
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)** screen.
|
||||
|
||||
### Supported Languages
|
||||
|
||||
@@ -201,7 +373,7 @@ CrossPoint renders text using the following Unicode character blocks, enabling s
|
||||
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
|
||||
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
|
||||
|
||||
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi.
|
||||
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi.
|
||||
|
||||
---
|
||||
|
||||
@@ -220,3 +392,18 @@ Accessible by pressing **Confirm** while inside a book.
|
||||
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).
|
||||
|
||||
+30
-5
@@ -1,10 +1,33 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Check if clang-format is available and pick the preferred binary.
|
||||
if command -v clang-format-21 >/dev/null 2>&1; then
|
||||
CLANG_FORMAT_BIN="clang-format-21"
|
||||
elif command -v clang-format >/dev/null 2>&1; then
|
||||
CLANG_FORMAT_BIN="clang-format"
|
||||
else
|
||||
printf "'clang-format' not found in current environment\n"
|
||||
printf "Install clang-format-21 (recommended), clang, clang-tools, or clang-format depending on your distro/os and tooling requirements\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GIT_LS_FILES_FLAGS=""
|
||||
if [[ "$1" == "-g" ]]; then
|
||||
if [[ "${1:-}" == "-g" ]]; then
|
||||
GIT_LS_FILES_FLAGS="--modified"
|
||||
fi
|
||||
|
||||
CLANG_FORMAT_VERSION_RAW="$(${CLANG_FORMAT_BIN} --version)"
|
||||
CLANG_FORMAT_MAJOR="$(printf '%s\n' "${CLANG_FORMAT_VERSION_RAW}" | grep -oE '[0-9]+' | head -n1)"
|
||||
|
||||
if [[ -z "${CLANG_FORMAT_MAJOR}" || "${CLANG_FORMAT_MAJOR}" -lt 21 ]]; then
|
||||
echo "Error: ${CLANG_FORMAT_BIN} is too old: ${CLANG_FORMAT_VERSION_RAW}"
|
||||
echo "This repository's .clang-format requires clang-format 21 or newer."
|
||||
echo "Install clang-format-21 and rerun ./bin/clang-format-fix"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Main Logic ---
|
||||
|
||||
# Format all files (or only modified files if -g is passed)
|
||||
@@ -13,8 +36,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/CrossPointFont/builtinFonts/' \
|
||||
| grep -v -E '^lib/CrossPointFont/Group5' \
|
||||
| xargs -r clang-format -style=file -i
|
||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||
| grep -v -E '^lib/uzlib/' \
|
||||
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Runs clang-format -i on project *.cpp and *.h files.
|
||||
|
||||
.DESCRIPTION
|
||||
Formats all C/C++ source and header files in the repository, excluding
|
||||
generated, vendored, and build directories (open-x4-sdk, builtinFonts,
|
||||
hyphenation tries, uzlib, .pio, *.generated.h).
|
||||
|
||||
The clang-format binary path is resolved once and cached in
|
||||
bin/clang-format-fix.local. On first run it checks a default path,
|
||||
then PATH, then common install locations. Edit the .local file to
|
||||
override manually.
|
||||
|
||||
.PARAMETER g
|
||||
Format only git-modified files (git diff --name-only HEAD) instead of
|
||||
the full tree.
|
||||
|
||||
.PARAMETER h
|
||||
Show this help text.
|
||||
|
||||
.EXAMPLE
|
||||
.\clang-format-fix.ps1
|
||||
Format all files.
|
||||
|
||||
.EXAMPLE
|
||||
.\clang-format-fix.ps1 -g
|
||||
Format only git-modified files.
|
||||
#>
|
||||
|
||||
param(
|
||||
[switch]$g,
|
||||
[switch]$h
|
||||
)
|
||||
|
||||
if ($h) {
|
||||
Get-Help $PSCommandPath -Detailed
|
||||
return
|
||||
}
|
||||
|
||||
$repoRoot = (Resolve-Path "$PSScriptRoot\..").Path
|
||||
$configFile = Join-Path $PSScriptRoot 'clang-format-fix.local'
|
||||
$defaultPath = 'C:\Program Files\LLVM\bin\clang-format.exe'
|
||||
|
||||
$candidatePaths = @(
|
||||
'C:\Program Files\LLVM\bin\clang-format.exe'
|
||||
'C:\Program Files (x86)\LLVM\bin\clang-format.exe'
|
||||
'C:\msys64\ucrt64\bin\clang-format.exe'
|
||||
'C:\msys64\mingw64\bin\clang-format.exe'
|
||||
"$env:LOCALAPPDATA\LLVM\bin\clang-format.exe"
|
||||
)
|
||||
|
||||
function Find-ClangFormat {
|
||||
# Try PATH first
|
||||
$inPath = Get-Command clang-format -ErrorAction SilentlyContinue
|
||||
if ($inPath) { return $inPath.Source }
|
||||
|
||||
# Try candidate paths
|
||||
foreach ($p in $candidatePaths) {
|
||||
if (Test-Path $p) { return $p }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Resolve-ClangFormat {
|
||||
# 1. Read from config if present
|
||||
if (Test-Path $configFile) {
|
||||
$saved = (Get-Content $configFile -Raw).Trim()
|
||||
if ($saved -and (Test-Path $saved)) { return $saved }
|
||||
Write-Host "Configured path no longer valid: $saved"
|
||||
}
|
||||
|
||||
# 2. Check default
|
||||
if (Test-Path $defaultPath) {
|
||||
$defaultPath | Set-Content $configFile
|
||||
Write-Host "Saved clang-format path to $configFile"
|
||||
return $defaultPath
|
||||
}
|
||||
|
||||
# 3. Search PATH and candidate locations
|
||||
$found = Find-ClangFormat
|
||||
if ($found) {
|
||||
$found | Set-Content $configFile
|
||||
Write-Host "Found clang-format at $found - saved to $configFile"
|
||||
return $found
|
||||
}
|
||||
|
||||
Write-Error "clang-format not found. Install LLVM or add clang-format to PATH."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$clangFormat = Resolve-ClangFormat
|
||||
|
||||
$exclude = @(
|
||||
'open-x4-sdk'
|
||||
'lib\EpdFont\builtinFonts'
|
||||
'lib\Epub\Epub\hyphenation\generated'
|
||||
'lib\uzlib'
|
||||
'.pio'
|
||||
)
|
||||
|
||||
function Test-Excluded($fullPath) {
|
||||
foreach ($ex in $exclude) {
|
||||
if ($fullPath -like "*\$ex\*") { return $true }
|
||||
}
|
||||
if ($fullPath -like '*.generated.h') { return $true }
|
||||
return $false
|
||||
}
|
||||
|
||||
if ($g) {
|
||||
# Only git-modified *.cpp / *.h files
|
||||
# Covers both staged and unstaged changes
|
||||
$files = @(git -C $repoRoot diff --name-only HEAD) +
|
||||
@(git -C $repoRoot diff --name-only --cached) |
|
||||
Sort-Object -Unique |
|
||||
Where-Object { $_ -match '\.(cpp|h)$' } |
|
||||
ForEach-Object { Get-Item (Join-Path $repoRoot $_) -ErrorAction SilentlyContinue } |
|
||||
Where-Object { $_ -and -not (Test-Excluded $_.FullName) }
|
||||
} else {
|
||||
$files = Get-ChildItem -Path $repoRoot -Recurse -Include *.cpp, *.h -File |
|
||||
Where-Object { -not (Test-Excluded $_.FullName) }
|
||||
}
|
||||
|
||||
$files = @($files)
|
||||
|
||||
if ($files.Count -eq 0) {
|
||||
Write-Host 'No files to format.'
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Formatting $($files.Count) files..."
|
||||
$i = 0
|
||||
$changed = 0
|
||||
$failures = 0
|
||||
foreach ($f in $files) {
|
||||
$i++
|
||||
$rel = $f.FullName.Substring($repoRoot.Length + 1)
|
||||
$hashBefore = (Get-FileHash $f.FullName -Algorithm MD5).Hash
|
||||
& $clangFormat -i $f.FullName
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$failures++
|
||||
Write-Host " [$i/$($files.Count)] $rel (FAILED, exit code $LASTEXITCODE)"
|
||||
continue
|
||||
}
|
||||
$hashAfter = (Get-FileHash $f.FullName -Algorithm MD5).Hash
|
||||
if ($hashBefore -ne $hashAfter) {
|
||||
$changed++
|
||||
Write-Host " [$i/$($files.Count)] $rel (changed)"
|
||||
} else {
|
||||
Write-Host " [$i/$($files.Count)] $rel"
|
||||
}
|
||||
}
|
||||
Write-Host "Done. $changed/$($files.Count) files changed, $failures failed."
|
||||
if ($failures -gt 0) { exit 1 }
|
||||
@@ -0,0 +1,472 @@
|
||||
# Activity & ActivityManager Migration Guide
|
||||
|
||||
This document explains the refactoring from the original per-activity render task model to the centralized `ActivityManager` introduced in [PR #1016](https://github.com/crosspoint-reader/crosspoint-reader/pull/1016). It covers the architectural differences, what changed for activity authors, and the FreeRTOS task and locking model that underpins the system.
|
||||
|
||||
## Overview of Changes
|
||||
|
||||
| Aspect | Old Model | New Model |
|
||||
|--------|-----------|-----------|
|
||||
| Render task | One per activity (8KB stack each) | Single shared task in `ActivityManager` |
|
||||
| Render mutex | Per-activity `renderingMutex` | Single global mutex in `ActivityManager` |
|
||||
| `RenderLock` | Inner class of `Activity` | Standalone class, acquires global mutex |
|
||||
| Subactivities | `ActivityWithSubactivity` base class | Activity stack managed by `ActivityManager` |
|
||||
| Navigation | Free functions in `main.cpp` | `activityManager.goHome()`, `goToReader()`, etc. |
|
||||
| Subactivity results | Callback lambdas stored in parent | `startActivityForResult()` / `setResult()` / `finish()` |
|
||||
| `requestUpdate()` | Notifies activity's own render task | Delegates to `ActivityManager` (immediate or deferred) |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Old Model: Per-Activity Render Tasks
|
||||
|
||||
Each activity created its own FreeRTOS render task on entry and destroyed it on exit:
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Main Task (Arduino loop) │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ currentActivity->loop() │ │
|
||||
│ │ ├── handle input │ │
|
||||
│ │ ├── update state (under RenderLock) │ │
|
||||
│ │ └── requestUpdate() ──notify──► Render Task │ │
|
||||
│ │ (per-activity)│ │
|
||||
│ │ 8KB stack │ │
|
||||
│ │ owns mutex │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ActivityWithSubactivity: │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Parent │────►│ SubActivity │ │
|
||||
│ │ (has render │ │ (has own │ │
|
||||
│ │ task) │ │ render task) │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Problems with this approach:
|
||||
|
||||
- **8KB per render task**: Each activity allocated an 8KB FreeRTOS stack for its render task, even though only one renders at a time
|
||||
- **Dangerous deletion patterns**: `exitActivity()` + `enterNewActivity()` in callbacks led to `delete this` situations where the caller was destroyed while its code was still on the stack
|
||||
- **Subactivity coupling**: Parents stored callbacks to child results, creating tight coupling and lifetime hazards
|
||||
|
||||
### New Model: Centralized ActivityManager
|
||||
|
||||
A single `ActivityManager` owns the render task and manages an activity stack:
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Main Task (Arduino loop) │
|
||||
│ │
|
||||
│ activityManager.loop() │
|
||||
│ │ │
|
||||
│ ├── currentActivity->loop() │
|
||||
│ │ ├── handle input │
|
||||
│ │ ├── update state (under RenderLock) │
|
||||
│ │ └── requestUpdate() │
|
||||
│ │ │
|
||||
│ ├── process pending actions (Push / Pop / Replace) │
|
||||
│ │ │
|
||||
│ └── if requestedUpdate: ──notify──► Render Task │
|
||||
│ (single, shared) │
|
||||
│ 8KB stack │
|
||||
│ global mutex │
|
||||
│ │
|
||||
│ Activity Stack: │
|
||||
│ ┌──────────┬──────────┬──────────┐ ┌──────────┐ │
|
||||
│ │ Home │ Settings │ Wifi │ │ Keyboard │ │
|
||||
│ │ (stack) │ (stack) │ (stack) │ │ (current)│ │
|
||||
│ └──────────┴──────────┴──────────┘ └──────────┘ │
|
||||
│ stackActivities[] currentActivity │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
### 1. Change Base Class
|
||||
|
||||
If your activity extended `ActivityWithSubactivity`, change it to extend `Activity`:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
class MyActivity final : public ActivityWithSubactivity {
|
||||
MyActivity(GfxRenderer& r, MappedInputManager& m, std::function<void()> goBack)
|
||||
: ActivityWithSubactivity("MyActivity", r, m), goBack(goBack) {}
|
||||
};
|
||||
|
||||
// AFTER
|
||||
class MyActivity final : public Activity {
|
||||
MyActivity(GfxRenderer& r, MappedInputManager& m)
|
||||
: Activity("MyActivity", r, m) {}
|
||||
};
|
||||
```
|
||||
|
||||
Note that navigation callbacks like `goBack` are no longer stored — use `finish()` or `activityManager.goHome()` instead.
|
||||
|
||||
### 2. Replace Navigation Functions
|
||||
|
||||
The free functions `exitActivity()` / `enterNewActivity()` in `main.cpp` are gone. Use `ActivityManager` methods:
|
||||
|
||||
```cpp
|
||||
// BEFORE (in main.cpp or via stored callbacks)
|
||||
exitActivity();
|
||||
enterNewActivity(new SettingsActivity(renderer, mappedInput, onGoHome));
|
||||
|
||||
// AFTER (from any Activity method)
|
||||
activityManager.goToSettings();
|
||||
// or for arbitrary navigation:
|
||||
activityManager.replaceActivity(std::make_unique<MyActivity>(renderer, mappedInput));
|
||||
```
|
||||
|
||||
`replaceActivity()` destroys the current activity and clears the stack. Use it for top-level navigation (home, reader, settings, etc.).
|
||||
|
||||
### 3. Replace Subactivity Pattern
|
||||
|
||||
The `enterNewActivity()` / `exitActivity()` subactivity pattern is replaced by a stack with typed results:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
void MyActivity::launchWifi() {
|
||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||
[this](bool connected) { onWifiDone(connected); }));
|
||||
}
|
||||
// Child calls: onComplete(true); // triggers callback, which may call exitActivity()
|
||||
|
||||
// AFTER
|
||||
void MyActivity::launchWifi() {
|
||||
startActivityForResult(
|
||||
std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult& result) {
|
||||
if (result.isCancelled) return;
|
||||
auto& wifi = std::get<WifiResult>(result.data);
|
||||
onWifiDone(wifi.connected);
|
||||
});
|
||||
}
|
||||
// Child calls:
|
||||
// setResult(WifiResult{.connected = true, .ssid = ssid});
|
||||
// finish();
|
||||
```
|
||||
|
||||
Key differences:
|
||||
|
||||
- **`startActivityForResult()`** pushes the current activity onto the stack and launches the child
|
||||
- **`setResult()`** stores a typed result on the child activity
|
||||
- **`finish()`** signals the manager to pop the child, call the result handler, and resume the parent
|
||||
- The parent is never deleted during this process — it's safely stored on the stack
|
||||
|
||||
### 4. Update `render()` Signature
|
||||
|
||||
The `RenderLock` type changed from `Activity::RenderLock` (inner class) to standalone `RenderLock`:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
void render(Activity::RenderLock&&) override;
|
||||
|
||||
// AFTER
|
||||
void render(RenderLock&&) override;
|
||||
```
|
||||
|
||||
Include `RenderLock.h` if not transitively included via `Activity.h`.
|
||||
|
||||
### 5. Update `onEnter()` / `onExit()`
|
||||
|
||||
Activities no longer create or destroy render tasks:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
void MyActivity::onEnter() {
|
||||
Activity::onEnter(); // created render task + logged
|
||||
// ... allocate resources
|
||||
requestUpdate();
|
||||
}
|
||||
void MyActivity::onExit() {
|
||||
// ... free resources
|
||||
Activity::onExit(); // acquired RenderLock, deleted render task
|
||||
}
|
||||
|
||||
// AFTER
|
||||
void MyActivity::onEnter() {
|
||||
Activity::onEnter(); // just logs
|
||||
// ... allocate resources
|
||||
requestUpdate();
|
||||
}
|
||||
void MyActivity::onExit() {
|
||||
// ... free resources
|
||||
Activity::onExit(); // just logs
|
||||
}
|
||||
```
|
||||
|
||||
The render task lifecycle is handled entirely by `ActivityManager::begin()`.
|
||||
|
||||
### 6. Update `requestUpdate()` Calls
|
||||
|
||||
The signature changed to accept an `immediate` flag:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
void requestUpdate(); // always immediate notification to per-activity render task
|
||||
|
||||
// AFTER
|
||||
void requestUpdate(bool immediate = false);
|
||||
// immediate=false (default): deferred until end of current loop iteration
|
||||
// immediate=true: sends notification to render task right away
|
||||
```
|
||||
|
||||
**When to use `immediate`**: Almost never. Deferred updates are batched — if `loop()` triggers multiple state changes that each call `requestUpdate()`, only one render happens. Use `immediate` only when you need the render to start before the current function returns (e.g., before a blocking network call).
|
||||
|
||||
**`requestUpdateAndWait()`**: Blocks the calling task until the render completes. Use sparingly — it's designed for cases where you need the screen to reflect new state before proceeding (e.g., showing "Checking for update..." before calling a network API).
|
||||
|
||||
### 7. Remove Stored Navigation Callbacks
|
||||
|
||||
Old activities often stored `std::function` callbacks for navigation:
|
||||
|
||||
```cpp
|
||||
// BEFORE
|
||||
class SettingsActivity : public ActivityWithSubactivity {
|
||||
const std::function<void()> goBack; // stored callback
|
||||
const std::function<void()> goHome; // stored callback
|
||||
public:
|
||||
SettingsActivity(GfxRenderer& r, MappedInputManager& m,
|
||||
std::function<void()> goBack, std::function<void()> goHome)
|
||||
: ActivityWithSubactivity("Settings", r, m), goBack(goBack), goHome(goHome) {}
|
||||
};
|
||||
|
||||
// AFTER
|
||||
class SettingsActivity : public Activity {
|
||||
public:
|
||||
SettingsActivity(GfxRenderer& r, MappedInputManager& m)
|
||||
: Activity("Settings", r, m) {}
|
||||
// Use finish() to go back, activityManager.goHome() to go home
|
||||
};
|
||||
```
|
||||
|
||||
This removes `std::function` overhead (~2-4KB per unique signature) and eliminates lifetime risks from captured `this` pointers.
|
||||
|
||||
## Technical Details
|
||||
|
||||
### FreeRTOS Task Model
|
||||
|
||||
The firmware runs on an ESP32-C3, a single-core RISC-V microcontroller. FreeRTOS provides cooperative and preemptive multitasking on this single core — only one task executes at any moment, and the scheduler switches between tasks at yield points (blocking calls, `vTaskDelay`, `taskYIELD`) or when a tick interrupt promotes a higher-priority task.
|
||||
|
||||
There are two tasks relevant to the activity system:
|
||||
|
||||
```text
|
||||
┌──────────────────────┐ ┌──────────────────────────┐
|
||||
│ Main Task │ │ Render Task │
|
||||
│ (Arduino loop) │ │ (ActivityManager-owned) │
|
||||
│ Priority: 1 │ │ Priority: 1 │
|
||||
│ │ │ │
|
||||
│ Runs: │ │ Runs: │
|
||||
│ - gpio.update() │ │ - ulTaskNotifyTake() │
|
||||
│ - activity->loop() │ │ (blocks until notified)│
|
||||
│ - pending actions │ │ - RenderLock (mutex) │
|
||||
│ - sleep/power mgmt │ │ - activity->render() │
|
||||
│ - requestUpdate →────┼─────┼─► xTaskNotify() │
|
||||
│ (end of loop) │ │ │
|
||||
└──────────────────────┘ └──────────────────────────┘
|
||||
```
|
||||
|
||||
Both tasks run at priority 1. Since the ESP32-C3 is single-core, they alternate execution: the main task runs `loop()`, then at the end of the loop iteration, notifies the render task if an update was requested. The render task wakes, acquires the mutex, calls `render()`, releases the mutex, and blocks again.
|
||||
|
||||
Do not use `xTaskCreate` inside activities. If you have a use case that seems to require a background task, open a discussion to propose a lifecycle-aware `Worker` abstraction first.
|
||||
|
||||
### The Render Mutex and RenderLock
|
||||
|
||||
A single FreeRTOS mutex (`renderingMutex`) protects shared state between `loop()` and `render()`. Since these run on different tasks, any state read by `render()` and written by `loop()` must be guarded.
|
||||
|
||||
`RenderLock` is an RAII wrapper:
|
||||
|
||||
```cpp
|
||||
// Standalone class (not tied to any specific activity)
|
||||
class RenderLock {
|
||||
bool isLocked = false;
|
||||
public:
|
||||
explicit RenderLock(); // acquires activityManager.renderingMutex
|
||||
explicit RenderLock(Activity&); // same — Activity& param kept for compatibility
|
||||
~RenderLock(); // releases mutex if still held
|
||||
void unlock(); // early release
|
||||
};
|
||||
```
|
||||
|
||||
**Usage patterns:**
|
||||
|
||||
```cpp
|
||||
// In loop(): protect state mutations that render() reads
|
||||
void MyActivity::loop() {
|
||||
if (somethingChanged) {
|
||||
RenderLock lock;
|
||||
state = newState; // safe — render() can't run while lock is held
|
||||
}
|
||||
requestUpdate(); // trigger render after lock is released
|
||||
}
|
||||
|
||||
// In render(): lock is passed in, held for duration of render
|
||||
void MyActivity::render(RenderLock&&) {
|
||||
// Lock is held — safe to read shared state
|
||||
renderer.clearScreen();
|
||||
renderer.drawText(..., stateString, ...);
|
||||
renderer.displayBuffer();
|
||||
// Lock released when RenderLock destructor runs
|
||||
}
|
||||
```
|
||||
|
||||
**Critical rule**: Never call `requestUpdateAndWait()` while holding a `RenderLock`. The render task needs the mutex to call `render()`, so holding it while waiting for the render to complete is a deadlock:
|
||||
|
||||
```text
|
||||
Main Task Render Task
|
||||
────────── ───────────
|
||||
RenderLock lock; (blocked on mutex)
|
||||
requestUpdateAndWait();
|
||||
→ notify render task
|
||||
→ block waiting for
|
||||
render to complete → wakes up
|
||||
→ tries to acquire mutex
|
||||
→ DEADLOCK: main holds mutex,
|
||||
waits for render; render
|
||||
waits for mutex
|
||||
```
|
||||
|
||||
### requestUpdate() vs requestUpdateAndWait()
|
||||
|
||||
```text
|
||||
requestUpdate(false) requestUpdate(true)
|
||||
───────────────── ─────────────────
|
||||
Sets flag only. Notifies render task
|
||||
Render happens after immediately.
|
||||
loop() returns and Render may start
|
||||
ActivityManager checks before the calling
|
||||
the flag. function returns.
|
||||
(Does NOT wait for
|
||||
render to complete.)
|
||||
|
||||
requestUpdateAndWait()
|
||||
──────────────────────
|
||||
Notifies render task AND
|
||||
blocks calling task until
|
||||
render is done. Uses
|
||||
FreeRTOS direct-to-task
|
||||
notification on the
|
||||
caller's task handle.
|
||||
```
|
||||
|
||||
`requestUpdateAndWait()` flow in detail:
|
||||
|
||||
```text
|
||||
Calling Task Render Task
|
||||
──────────── ───────────
|
||||
requestUpdateAndWait()
|
||||
├─ assert: not render task
|
||||
├─ assert: not holding RenderLock
|
||||
├─ store waitingTaskHandle
|
||||
├─ xTaskNotify(renderTask) → wakes render task
|
||||
└─ ulTaskNotifyTake() ─┐
|
||||
(blocked) │ RenderLock lock;
|
||||
│ activity->render();
|
||||
│ // render complete
|
||||
│ taskENTER_CRITICAL
|
||||
│ waiter = waitingTaskHandle
|
||||
│ waitingTaskHandle = nullptr
|
||||
│ taskEXIT_CRITICAL
|
||||
│ xTaskNotify(waiter) ───┐
|
||||
│ │
|
||||
┌──────────────────────┘ │
|
||||
│ (woken by notification) ◄────────────────────────┘
|
||||
└─ return
|
||||
```
|
||||
|
||||
### Activity Lifecycle Under ActivityManager
|
||||
|
||||
```text
|
||||
activityManager.replaceActivity(make_unique<MyActivity>(...))
|
||||
│
|
||||
▼
|
||||
╔═══════════════════════════════════════════════════╗
|
||||
║ pendingAction = Replace ║
|
||||
║ pendingActivity = MyActivity ║
|
||||
╚═══════════════════════════════════════════════════╝
|
||||
│
|
||||
▼ (next loop iteration)
|
||||
ActivityManager::loop()
|
||||
│
|
||||
├── currentActivity->loop() // old activity's last loop
|
||||
│
|
||||
├── process pending action:
|
||||
│ ├── RenderLock lock;
|
||||
│ ├── oldActivity->onExit() // cleanup under lock
|
||||
│ ├── delete oldActivity
|
||||
│ ├── clear stack
|
||||
│ ├── currentActivity = MyActivity
|
||||
│ ├── lock.unlock()
|
||||
│ └── MyActivity->onEnter() // init new activity
|
||||
│
|
||||
└── if requestedUpdate:
|
||||
└── notify render task
|
||||
```
|
||||
|
||||
For push/pop (subactivity) navigation:
|
||||
|
||||
```text
|
||||
Parent calls: startActivityForResult(make_unique<Child>(...), handler)
|
||||
│
|
||||
▼
|
||||
╔══════════════════════════════════════╗
|
||||
║ pendingAction = Push ║
|
||||
║ pendingActivity = Child ║
|
||||
║ parent->resultHandler = handler ║
|
||||
╚══════════════════════════════════════╝
|
||||
│
|
||||
▼ (next loop iteration)
|
||||
├── Parent moved to stackActivities[]
|
||||
├── currentActivity = Child
|
||||
└── Child->onEnter()
|
||||
|
||||
... child runs ...
|
||||
|
||||
Child calls: setResult(MyResult{...}); finish();
|
||||
│
|
||||
▼
|
||||
╔══════════════════════════════════════╗
|
||||
║ pendingAction = Pop ║
|
||||
║ child->result = MyResult{...} ║
|
||||
╚══════════════════════════════════════╝
|
||||
│
|
||||
▼ (next loop iteration)
|
||||
├── result = child->result
|
||||
├── Child->onExit(); delete Child
|
||||
├── currentActivity = Parent (popped from stack)
|
||||
├── Parent->resultHandler(result)
|
||||
└── requestUpdate() // automatic re-render for parent
|
||||
```
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
**Calling `finish()` and continuing to access `this`**: `finish()` sets `pendingAction = Pop` but does not immediately destroy the activity. The activity is destroyed on the next `ActivityManager::loop()` iteration. It's safe to access member variables after `finish()` within the same function, but don't rely on the activity surviving past the current `loop()` call.
|
||||
|
||||
**Modifying shared state without `RenderLock`**: If `render()` reads a variable and `loop()` writes it, the write must be under a `RenderLock`. Without it, `render()` could see a half-written value (e.g., a partially updated string or struct).
|
||||
|
||||
**Creating background tasks that outlive the activity**: Any FreeRTOS task created in `onEnter()` must be deleted in `onExit()` before the activity is destroyed. The `ActivityManager` does not track or clean up background tasks.
|
||||
|
||||
**Holding `RenderLock` across blocking calls**: The render task is blocked on the mutex while you hold the lock. Keep critical sections short — acquire, mutate state, release, then do blocking work.
|
||||
|
||||
```cpp
|
||||
// WRONG — blocks render for the entire network call
|
||||
void MyActivity::doNetworkStuff() {
|
||||
RenderLock lock;
|
||||
state = LOADING;
|
||||
auto result = http.get(url); // blocks for seconds with lock held
|
||||
state = DONE;
|
||||
}
|
||||
|
||||
// CORRECT — release lock before blocking
|
||||
void MyActivity::doNetworkStuff() {
|
||||
{
|
||||
RenderLock lock;
|
||||
state = LOADING;
|
||||
}
|
||||
requestUpdate(true); // render "Loading..." immediately, before we block
|
||||
auto result = http.get(url); // lock is not held
|
||||
{
|
||||
RenderLock lock;
|
||||
state = DONE;
|
||||
}
|
||||
requestUpdate();
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
# Contributing Docs
|
||||
|
||||
This section is a lightweight contributor guide for CrossPoint Reader.
|
||||
It is written for software developers who may be new to embedded development.
|
||||
|
||||
- [Getting Started](./getting-started.md)
|
||||
- [Architecture Overview](./architecture.md)
|
||||
- [Development Workflow](./development-workflow.md)
|
||||
- [Testing and Debugging](./testing-debugging.md)
|
||||
|
||||
If you are new, start with [Getting Started](./getting-started.md).
|
||||
@@ -0,0 +1,199 @@
|
||||
# Architecture Overview
|
||||
|
||||
CrossPoint is firmware for the Xteink X4 (unaffiliated with Xteink), built with PlatformIO targeting the ESP32-C3 microcontroller.
|
||||
|
||||
At a high level, it is firmware that uses an activity-driven application architecture loop with persistent settings/state, SD-card-first caching, and a rendering pipeline optimized for e-ink constraints.
|
||||
|
||||
## System at a glance
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[open-x4-sdk HAL]
|
||||
B --> C[src/main.cpp runtime loop]
|
||||
C --> D[Activities layer]
|
||||
C --> E[State and settings]
|
||||
D --> F[Reader flows]
|
||||
D --> G[Home/Library/Settings flows]
|
||||
D --> H[Network/Web server flows]
|
||||
F --> I[lib/Epub parsing + layout + hyphenation]
|
||||
I --> J[SD cache in .crosspoint]
|
||||
D --> K[GfxRenderer]
|
||||
K --> L[E-ink display buffer]
|
||||
```
|
||||
|
||||
## Runtime lifecycle
|
||||
|
||||
Primary entry point is `src/main.cpp`.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Boot] --> B[Init GPIO and optional serial]
|
||||
B --> C[Init SD storage]
|
||||
C --> D[Load settings and app state]
|
||||
D --> E[Init display and fonts]
|
||||
E --> F{Resume reader?}
|
||||
F -->|No| G[Enter Home activity]
|
||||
F -->|Yes| H[Enter Reader activity]
|
||||
G --> I[Main loop]
|
||||
H --> I
|
||||
I --> J[Poll input and run current activity]
|
||||
J --> K{Sleep condition met?}
|
||||
K -->|No| I
|
||||
K -->|Yes| L[Persist state and enter deep sleep]
|
||||
```
|
||||
|
||||
In each loop iteration, the firmware updates input, runs the active activity, handles auto-sleep/power behavior, and applies a short delay policy to balance responsiveness and power.
|
||||
|
||||
## Activity model
|
||||
|
||||
Activities are screen-level controllers deriving from `src/activities/Activity.h`.
|
||||
Some flows use `src/activities/ActivityWithSubactivity.h` to host nested activities.
|
||||
|
||||
- `onEnter()` and `onExit()` manage setup/teardown
|
||||
- `loop()` handles per-frame behavior
|
||||
- `skipLoopDelay()` and `preventAutoSleep()` are used by long-running flows (for example web server mode)
|
||||
|
||||
Top-level activity groups:
|
||||
|
||||
- `src/activities/home/`: home and library navigation
|
||||
- `src/activities/reader/`: EPUB/XTC/TXT reading flows
|
||||
- `src/activities/settings/`: settings menus and configuration
|
||||
- `src/activities/network/`: WiFi selection, AP/STA mode, file transfer server
|
||||
- `src/activities/boot_sleep/`: boot and sleep transitions
|
||||
|
||||
## Reader and content pipeline
|
||||
|
||||
Reader orchestration starts in `src/activities/reader/ReaderActivity.h` and dispatches to format-specific readers.
|
||||
EPUB processing is implemented in `lib/Epub/`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Select book] --> B[ReaderActivity]
|
||||
B --> C{Format}
|
||||
C -->|EPUB| D[lib/Epub/Epub]
|
||||
C -->|XTC| E[lib/Xtc reader]
|
||||
C -->|TXT| F[lib/Txt reader]
|
||||
D --> G[Parse OPF/TOC/CSS]
|
||||
G --> H[Layout pages/sections]
|
||||
H --> I[Write section and metadata caches]
|
||||
I --> J[Render current page via GfxRenderer]
|
||||
```
|
||||
|
||||
Why caching matters:
|
||||
|
||||
- RAM is limited on ESP32-C3, so expensive parsed/layout data is persisted to SD
|
||||
- repeat opens/page navigation can reuse cached data instead of full reparsing
|
||||
|
||||
## Reader internals call graph
|
||||
|
||||
This diagram zooms into the EPUB path to show the main control and data flow from activity entry to on-screen draw.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[ReaderActivity onEnter] --> B{File type}
|
||||
B -->|EPUB| C[Create Epub object]
|
||||
B -->|XTC/TXT| Z[Use format-specific reader]
|
||||
|
||||
C --> D[Epub load]
|
||||
D --> E[Locate container and OPF]
|
||||
E --> F[Build or load BookMetadataCache]
|
||||
F --> G[Load TOC and spine]
|
||||
G --> H[Load or parse CSS rules]
|
||||
|
||||
H --> I[EpubReaderActivity]
|
||||
I --> J{Section cache exists for current settings?}
|
||||
J -->|Yes| K[Read section bin from SD cache]
|
||||
J -->|No| L[Parse chapter HTML and layout text]
|
||||
L --> M[Apply typography settings and hyphenation]
|
||||
M --> N[Write section cache bin]
|
||||
|
||||
K --> O[Build page model]
|
||||
N --> O
|
||||
O --> P[GfxRenderer draw calls]
|
||||
P --> Q[HAL display framebuffer update]
|
||||
Q --> R[E-ink refresh policy]
|
||||
|
||||
S[SETTINGS singleton] -. influences .-> J
|
||||
S -. influences .-> M
|
||||
T[APP_STATE singleton] -. persists .-> U[Reading progress and resume context]
|
||||
U -. used by .-> I
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- "section cache exists" depends on cache-busting parameters such as font and layout-related settings
|
||||
- rendering favors reusing precomputed layout data to keep page turns responsive on constrained hardware
|
||||
- progress/session state is persisted so the reader can reopen at the last position after reboot/sleep
|
||||
|
||||
## State and persistence
|
||||
|
||||
Two singletons are central:
|
||||
|
||||
- `src/CrossPointSettings.h` (`SETTINGS`): user preferences and behavior flags
|
||||
- `src/CrossPointState.h` (`APP_STATE`): runtime/session state such as current book and sleep context
|
||||
|
||||
Typical persisted areas on SD:
|
||||
|
||||
```text
|
||||
/.crosspoint/
|
||||
epub_<hash>/
|
||||
book.bin
|
||||
progress.bin
|
||||
cover.bmp
|
||||
sections/*.bin
|
||||
settings.bin
|
||||
state.bin
|
||||
```
|
||||
|
||||
For binary cache formats, see `docs/file-formats.md`.
|
||||
|
||||
## Networking architecture
|
||||
|
||||
Network file transfer is controlled by `src/activities/network/CrossPointWebServerActivity.h` and served by `src/network/CrossPointWebServer.h`.
|
||||
|
||||
Modes:
|
||||
|
||||
- STA: join existing WiFi network
|
||||
- AP: create hotspot
|
||||
|
||||
Server behavior:
|
||||
|
||||
- HTTP server on port 80
|
||||
- WebSocket upload server on port 81
|
||||
- file operations backed by SD storage
|
||||
- activity requests faster loop responsiveness while server is running
|
||||
|
||||
Endpoint reference: `docs/webserver-endpoints.md`.
|
||||
|
||||
## Build-time generated assets
|
||||
|
||||
Some sources are generated and should not be edited manually.
|
||||
|
||||
- `scripts/build_html.py` generates `src/network/html/*.generated.h` from HTML files
|
||||
- `scripts/generate_hyphenation_trie.py` generates hyphenation headers under `lib/Epub/Epub/hyphenation/generated/`
|
||||
|
||||
When editing related source assets, regenerate via normal build steps/scripts.
|
||||
|
||||
## Key directories
|
||||
|
||||
- `src/`: app orchestration, settings/state, and activity implementations
|
||||
- `src/network/`: web server and OTA/update networking
|
||||
- `src/components/`: theming and shared UI components
|
||||
- `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
|
||||
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
|
||||
- `open-x4-sdk/`: hardware SDK submodule (display, input, storage, battery)
|
||||
- `docs/`: user and technical documentation
|
||||
|
||||
## Embedded constraints that shape design
|
||||
|
||||
- constrained RAM drives SD-first caching and careful allocations
|
||||
- e-ink refresh cost drives render/update batching choices
|
||||
- main loop responsiveness matters for input, power handling, and watchdog safety
|
||||
- background/network flows must cooperate with sleep and loop timing logic
|
||||
|
||||
## Scope guardrails
|
||||
|
||||
Before implementing larger ideas, check:
|
||||
|
||||
- [SCOPE.md](../../SCOPE.md)
|
||||
- [GOVERNANCE.md](../../GOVERNANCE.md)
|
||||
@@ -0,0 +1,43 @@
|
||||
# Development Workflow
|
||||
|
||||
This page defines the expected local workflow before opening a pull request.
|
||||
|
||||
## 1) Fork and create a focused branch
|
||||
|
||||
- Fork the repository to your own GitHub account
|
||||
- Clone your fork locally and add the upstream repository if needed
|
||||
|
||||
- Branch from `master`
|
||||
- Keep each PR focused on one fix or feature area
|
||||
|
||||
## 2) Implement with scope in mind
|
||||
|
||||
- Confirm your idea is in project scope: [SCOPE.md](../../SCOPE.md)
|
||||
- Prefer incremental changes over broad refactors
|
||||
|
||||
## 3) Run local checks
|
||||
|
||||
```sh
|
||||
./bin/clang-format-fix
|
||||
pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
pio run
|
||||
```
|
||||
|
||||
CI enforces formatting, static analysis, and build checks.
|
||||
Use clang-format 21+ locally to match CI.
|
||||
If `clang-format` is missing or too old locally, see [Getting Started](./getting-started.md).
|
||||
|
||||
## 4) Open the PR
|
||||
|
||||
- Use a semantic title (example: `fix: avoid crash when opening malformed epub`)
|
||||
- Fill out `.github/PULL_REQUEST_TEMPLATE.md`
|
||||
- Describe the problem, approach, and any tradeoffs
|
||||
- Include reproduction and verification steps for bug fixes
|
||||
|
||||
## 5) Review etiquette
|
||||
|
||||
- Be explicit and concise in responses
|
||||
- Keep discussions technical and respectful
|
||||
- Assume good intent and focus on code-level feedback
|
||||
|
||||
For community expectations, see [GOVERNANCE.md](../../GOVERNANCE.md).
|
||||
@@ -0,0 +1,80 @@
|
||||
# Getting Started
|
||||
|
||||
This guide helps you build and run CrossPoint locally.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- PlatformIO Core (`pio`) or VS Code + PlatformIO IDE
|
||||
- Python 3.8+
|
||||
- `clang-format` 21+ in your `PATH` (CI uses clang-format 21)
|
||||
- USB-C cable
|
||||
- Xteink X4 device for hardware testing
|
||||
|
||||
If `./bin/clang-format-fix` fails with either of these errors, install clang-format 21:
|
||||
|
||||
- `clang-format: No such file or directory`
|
||||
- `.clang-format: error: unknown key 'AlignFunctionDeclarations'`
|
||||
|
||||
Examples:
|
||||
|
||||
```sh
|
||||
# Debian/Ubuntu (try this first)
|
||||
sudo apt-get update && sudo apt-get install -y clang-format-21
|
||||
|
||||
# If the package is unavailable, add LLVM apt repo and retry
|
||||
wget https://apt.llvm.org/llvm.sh
|
||||
chmod +x llvm.sh
|
||||
sudo ./llvm.sh 21
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-format-21
|
||||
|
||||
# macOS (Homebrew)
|
||||
brew install clang-format
|
||||
```
|
||||
|
||||
Then verify:
|
||||
|
||||
```sh
|
||||
clang-format-21 --version
|
||||
```
|
||||
|
||||
The reported major version must be 21 or newer.
|
||||
|
||||
## Clone and initialize
|
||||
|
||||
```sh
|
||||
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
|
||||
cd crosspoint-reader
|
||||
```
|
||||
|
||||
If you already cloned without submodules:
|
||||
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
pio run
|
||||
```
|
||||
|
||||
## Flash
|
||||
|
||||
```sh
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
## First checks before opening a PR
|
||||
|
||||
```sh
|
||||
./bin/clang-format-fix
|
||||
pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
pio run
|
||||
```
|
||||
|
||||
## What to read next
|
||||
|
||||
- [Architecture Overview](./architecture.md)
|
||||
- [Development Workflow](./development-workflow.md)
|
||||
- [Testing and Debugging](./testing-debugging.md)
|
||||
@@ -0,0 +1,48 @@
|
||||
# Testing and Debugging
|
||||
|
||||
CrossPoint runs on real hardware, so debugging usually combines local build checks and on-device logs.
|
||||
|
||||
## Local checks
|
||||
|
||||
Make sure `clang-format` 21+ is installed and available in `PATH` before running the formatting step.
|
||||
If needed, see [Getting Started](./getting-started.md).
|
||||
|
||||
```sh
|
||||
./bin/clang-format-fix
|
||||
pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
pio run
|
||||
```
|
||||
|
||||
## Flash and monitor
|
||||
|
||||
Flash firmware:
|
||||
|
||||
```sh
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
Open serial monitor:
|
||||
|
||||
```sh
|
||||
pio device monitor
|
||||
```
|
||||
|
||||
Optional enhanced monitor:
|
||||
|
||||
```sh
|
||||
python3 -m pip install pyserial colorama matplotlib
|
||||
python3 scripts/debugging_monitor.py
|
||||
```
|
||||
|
||||
## Useful bug report contents
|
||||
|
||||
- Firmware version and build environment
|
||||
- Exact steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- Serial logs from boot through failure
|
||||
- Whether issue reproduces after clearing `.crosspoint/` cache on SD card
|
||||
|
||||
## Common troubleshooting references
|
||||
|
||||
- [User Guide troubleshooting section](../../USER_GUIDE.md#7-troubleshooting-issues--escaping-bootloop)
|
||||
- [Webserver troubleshooting](../troubleshooting.md)
|
||||
@@ -45,22 +45,9 @@ byte arrays, and emits headers under
|
||||
`SerializedHyphenationPatterns` descriptor so the reader can keep the automaton
|
||||
in flash.
|
||||
|
||||
To refresh the firmware assets after updating the `.bin` files, run:
|
||||
A convenient script `update_hyphenation.sh` is used to update all languages.
|
||||
To use it, run:
|
||||
|
||||
```
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/en.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-en.trie.h
|
||||
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/fr.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-fr.trie.h
|
||||
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/de.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-de.trie.h
|
||||
|
||||
./scripts/generate_hyphenation_trie.py \
|
||||
--input lib/Epub/Epub/hyphenation/tries/ru.bin \
|
||||
--output lib/Epub/Epub/hyphenation/generated/hyph-ru.trie.h
|
||||
```sh
|
||||
./scripts/update_hypenation.sh
|
||||
```
|
||||
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
# Internationalization (I18N)
|
||||
|
||||
This guide explains the multi-language support system in CrossPoint Reader.
|
||||
|
||||
## Supported Languages
|
||||
|
||||
- English
|
||||
- French
|
||||
- German
|
||||
- Portuguese
|
||||
- Spanish
|
||||
- Swedish
|
||||
- Czech
|
||||
- Russian
|
||||
- Ukrainian
|
||||
- Polish
|
||||
- Danish
|
||||
- Turkish
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 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: "ES"
|
||||
_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. "FR"). Please use the [ISO Code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) of the language. 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, starts 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: "IT"
|
||||
_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::ES);
|
||||
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::ES);
|
||||
|
||||
// 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::FR);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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,60 @@
|
||||
# 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)
|
||||
|
||||
## Portuguese (Portugal)
|
||||
- [victordomingos](https://github.com/victordomingos)
|
||||
|
||||
## Italian
|
||||
- [andreaturchet](https://github.com/andreaturchet)
|
||||
- [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)
|
||||
- [DaniPhii](https://github.com/DaniPhii)
|
||||
|
||||
## Swedish
|
||||
- [dawiik](https://github.com/dawiik)
|
||||
- [steka](https://github.com/steka)
|
||||
|
||||
## Romanian
|
||||
- [ariel-lindemann](https://github.com/ariel-lindemann)
|
||||
|
||||
## Catalan
|
||||
- [angeldenom](https://github.com/angeldenom)
|
||||
|
||||
## Finnish
|
||||
- [plahteenlahti](https://github.com/plahteenlahti)
|
||||
|
||||
## Ukrainian
|
||||
- [mirus-ua](https://github.com/mirus-ua)
|
||||
|
||||
## Belarusian
|
||||
- [Dexif](https://github.com/dexif)
|
||||
|
||||
## Danish
|
||||
- [hajisan](https://github.com/hajisan)
|
||||
+32
-22
@@ -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,11 +165,25 @@ This is useful for organizing your ebooks by genre, author, or series.
|
||||
|
||||
**Note:** Folders must be empty before they can be deleted.
|
||||
|
||||
#### Moving Files
|
||||
|
||||
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
|
||||
|
||||
**Note:** Typing in a nonexistent folder name will result in the following error: "Failed to move: Destination not found"
|
||||
|
||||
#### Renaming Files
|
||||
|
||||
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 a detailed documentation can be found [here](./webserver-endpoints.md).
|
||||
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
|
||||
|
||||
@@ -189,7 +200,6 @@ For power users, you can manage files directly from your terminal using `curl` w
|
||||
- **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)
|
||||
|
||||
---
|
||||
@@ -198,7 +208,7 @@ For power users, you can manage files directly from your terminal using `curl` w
|
||||
|
||||
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
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
|
||||
This directory is intended for project header files.
|
||||
|
||||
A header file is a file containing C declarations and macro definitions
|
||||
to be shared between several project source files. You request the use of a
|
||||
header file in your project source file (C, C++, etc) located in `src` folder
|
||||
by including it, with the C preprocessing directive `#include'.
|
||||
|
||||
```src/main.c
|
||||
|
||||
#include "header.h"
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Including a header file produces the same results as copying the header file
|
||||
into each source file that needs it. Such copying would be time-consuming
|
||||
and error-prone. With a header file, the related declarations appear
|
||||
in only one place. If they need to be changed, they can be changed in one
|
||||
place, and programs that include the header file will automatically use the
|
||||
new version when next recompiled. The header file eliminates the labor of
|
||||
finding and changing all the copies as well as the risk that a failure to
|
||||
find one copy will result in inconsistencies within a program.
|
||||
|
||||
In C, the convention is to give header files names that end with `.h'.
|
||||
|
||||
Read more about using header files in official GCC documentation:
|
||||
|
||||
* Include Syntax
|
||||
* Include Operation
|
||||
* Once-Only Headers
|
||||
* Computed Includes
|
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||
@@ -1,147 +0,0 @@
|
||||
#include "CrossPointFont.h"
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#define FONT_SCALE 2
|
||||
|
||||
namespace {
|
||||
// Number of set bits from 0->15
|
||||
uint8_t bitCount[] = {
|
||||
0, // 0b0000,
|
||||
1, // 0b0001,
|
||||
1, // 0b0010,
|
||||
2, // 0b0011,
|
||||
1, // 0b0100,
|
||||
2, // 0b0101,
|
||||
2, // 0b0110,
|
||||
3, // 0b0111,
|
||||
1, // 0b1000,
|
||||
2, // 0b1001,
|
||||
2, // 0b1010,
|
||||
3, // 0b1011,
|
||||
2, // 0b1100,
|
||||
3, // 0b1101,
|
||||
3, // 0b1110,
|
||||
4, // 0b1111,
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void CrossPointFont::getTextBounds(const char* string, const Style style, const int startX, const int startY, int* minX,
|
||||
int* minY, int* maxX, int* maxY) const {
|
||||
*minX = startX;
|
||||
*minY = startY;
|
||||
*maxX = startX;
|
||||
*maxY = startY;
|
||||
|
||||
if (*string == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
int cursorX = startX;
|
||||
const int cursorY = startY;
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||
const CrossPointFontGlyph* glyph = getGlyph(cp, style);
|
||||
|
||||
if (!glyph) {
|
||||
glyph = getGlyph(REPLACEMENT_GLYPH, style);
|
||||
}
|
||||
|
||||
if (!glyph) {
|
||||
// TODO: Better handle this?
|
||||
continue;
|
||||
}
|
||||
|
||||
*minX = std::min(*minX, cursorX + glyph->xOffset / FONT_SCALE);
|
||||
*maxX = std::max(*maxX, cursorX + (glyph->xOffset + glyph->width) / FONT_SCALE);
|
||||
*minY = std::min(*minY, cursorY + (glyph->yOffset - glyph->height) / FONT_SCALE);
|
||||
*maxY = std::max(*maxY, cursorY + glyph->yOffset / FONT_SCALE);
|
||||
cursorX += glyph->xAdvance / FONT_SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
void CrossPointFont::getTextDimensions(const char* string, const Style style, int* w, int* h) const {
|
||||
int minX = 0, minY = 0, maxX = 0, maxY = 0;
|
||||
|
||||
getTextBounds(string, style, 0, 0, &minX, &minY, &maxX, &maxY);
|
||||
|
||||
*w = maxX - minX;
|
||||
*h = maxY - minY;
|
||||
}
|
||||
|
||||
uint8_t CrossPointFont::styleGroup(const Style style) const {
|
||||
if (style == REGULAR) return 0;
|
||||
|
||||
if (data.header.styles == 0b0001) {
|
||||
// Only have regular font, show regular
|
||||
return 0;
|
||||
}
|
||||
if (data.header.styles == 0b0011) {
|
||||
// Only have bold and regular font
|
||||
// Show bold if style is bold or bold_italic
|
||||
return style == BOLD || style == BOLD_ITALIC ? 1 : 0;
|
||||
}
|
||||
if (data.header.styles == 0b0101) {
|
||||
// Only have italic and regular font
|
||||
// Show italic if style is italic or bold_italic
|
||||
return style == ITALIC || style == BOLD_ITALIC ? 1 : 0;
|
||||
}
|
||||
if (data.header.styles == 0b1001) {
|
||||
// Only have bold_italic and regular font
|
||||
// Show bold_italic if style is any non-regular
|
||||
return style == BOLD_ITALIC || style == BOLD || style == ITALIC ? 1 : 0;
|
||||
}
|
||||
if (data.header.styles == 0b0111) {
|
||||
// Have all but bold_italic
|
||||
// Show bold if style is bold_italic, otherwise show the requested style
|
||||
return style == BOLD_ITALIC ? 1 : style;
|
||||
}
|
||||
if (data.header.styles == 0b1011) {
|
||||
// Have all but italic
|
||||
// Show bold_italic if style is italic, otherwise show the requested style
|
||||
return style == ITALIC ? 2 : style;
|
||||
}
|
||||
if (data.header.styles == 0b1101) {
|
||||
// Have all but bold
|
||||
// Show bold_italic if style is bold, otherwise show the requested style
|
||||
return style == BOLD ? 2 : style;
|
||||
}
|
||||
if (data.header.styles == 0b1111) {
|
||||
return style;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const CrossPointFontGlyph* CrossPointFont::getGlyph(const uint32_t cp, const Style style) const {
|
||||
const CrossPointFontUnicodeInterval* intervals = data.intervals;
|
||||
const int count = data.header.intervalCount;
|
||||
|
||||
if (count == 0) return nullptr;
|
||||
|
||||
// Binary search for O(log n) lookup instead of O(n)
|
||||
// Critical for Korean fonts with many unicode intervals
|
||||
int left = 0;
|
||||
int right = count - 1;
|
||||
|
||||
while (left <= right) {
|
||||
const int mid = left + (right - left) / 2;
|
||||
const CrossPointFontUnicodeInterval* interval = &intervals[mid];
|
||||
|
||||
if (cp < interval->first) {
|
||||
right = mid - 1;
|
||||
} else if (cp > interval->last) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
// Found: cp >= interval->first && cp <= interval->last
|
||||
const uint32_t index =
|
||||
interval->offset + (cp - interval->first) * bitCount[data.header.styles] + styleGroup(style);
|
||||
return &data.glyphs[index];
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "CrossPointFontFormat.h"
|
||||
#include "Group5/Group5.h"
|
||||
|
||||
class CrossPointFont {
|
||||
public:
|
||||
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
|
||||
|
||||
CrossPointFontData data;
|
||||
explicit CrossPointFont(void* rawData) {
|
||||
data.header = *static_cast<CrossPointFontHeader*>(rawData);
|
||||
data.intervals = (CrossPointFontUnicodeInterval*)(static_cast<uint8_t*>(rawData) + sizeof(CrossPointFontHeader));
|
||||
data.glyphs = (CrossPointFontGlyph*)((uint8_t*)data.intervals +
|
||||
sizeof(CrossPointFontUnicodeInterval) * data.header.intervalCount);
|
||||
data.bitmap = (uint8_t*)data.glyphs + sizeof(CrossPointFontGlyph) * data.header.glyphCount;
|
||||
}
|
||||
|
||||
~CrossPointFont() = default;
|
||||
void getTextDimensions(const char* string, Style style, int* w, int* h) const;
|
||||
const CrossPointFontGlyph* getGlyph(uint32_t cp, Style style) const;
|
||||
|
||||
private:
|
||||
void getTextBounds(const char* string, Style style, int startX, int startY, int* minX, int* minY, int* maxX,
|
||||
int* maxY) const;
|
||||
uint8_t styleGroup(Style style) const;
|
||||
};
|
||||
|
||||
// TODO: CrossPointFontSmall
|
||||
@@ -1,57 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// 16-bit marker at the start of a CrossPoint font file
|
||||
// (CrossPoint Font Format)
|
||||
#define CPF_FONT_MARKER 0xCFF1
|
||||
#define CPF_FONT_MARKER_SMALL 0xCFF2
|
||||
|
||||
// Font info per large character (glyph)
|
||||
typedef struct {
|
||||
uint32_t bitmapOffset; /// Offset to compressed bitmap data for this glyph
|
||||
uint16_t width; /// bitmap width in pixels
|
||||
uint16_t height; /// bitmap height in pixels
|
||||
uint16_t xAdvance; /// total width in pixels (bitmap + padding)
|
||||
int16_t xOffset; /// left padding to upper left corner
|
||||
int16_t yOffset; /// top padding to upper left corner
|
||||
} CrossPointFontGlyph;
|
||||
|
||||
// Font info per small character (glyph)
|
||||
typedef struct {
|
||||
uint32_t bitmapOffset; /// Offset to compressed bitmap data for this glyph
|
||||
uint8_t width; /// bitmap width in pixels
|
||||
uint8_t height; /// bitmap height in pixels
|
||||
uint8_t xAdvance; /// total width in pixels (bitmap + padding)
|
||||
int8_t xOffset; /// left padding to upper left corner
|
||||
int16_t yOffset; /// top padding to upper left corner
|
||||
} CrossPointFontSmallGlyph;
|
||||
|
||||
/// Glyph interval structure
|
||||
typedef struct {
|
||||
uint32_t first; /// The first unicode code point of the interval
|
||||
uint32_t last; /// The last unicode code point of the interval
|
||||
uint32_t offset; /// Index of the first code point into the glyph array
|
||||
} CrossPointFontUnicodeInterval;
|
||||
|
||||
typedef struct {
|
||||
uint16_t u16Marker; /// CPF_FONT_MARKER / CPF_FONT_MARKER_SMALL
|
||||
uint16_t height; /// Newline distance (y axis)
|
||||
uint16_t ascender; /// Maximal height of a glyph above the base line
|
||||
uint8_t styles; /// Regular = 0x01, Bold = 0x02, Italic = 0x04, BoldItalic = 0x08, can be OR'd together
|
||||
uint16_t intervalCount; /// Number of unicode intervals.
|
||||
uint32_t glyphCount; /// Number of total glyphs across all styles
|
||||
} CrossPointFontHeader;
|
||||
|
||||
/// Data stored for FONT AS A WHOLE
|
||||
typedef struct {
|
||||
CrossPointFontHeader header;
|
||||
CrossPointFontUnicodeInterval* intervals; /// Valid unicode intervals for this font
|
||||
CrossPointFontGlyph* glyphs; /// Glyph array
|
||||
uint8_t* bitmap; /// Glyph bitmaps, concatenated
|
||||
} CrossPointFontData;
|
||||
|
||||
typedef struct {
|
||||
CrossPointFontHeader header;
|
||||
CrossPointFontUnicodeInterval* intervals; /// Valid unicode intervals for this font
|
||||
CrossPointFontSmallGlyph* glyphs; /// Glyph array
|
||||
uint8_t* bitmap; /// Glyph bitmaps, concatenated
|
||||
} CrossPointFontDataSmall;
|
||||
@@ -1,51 +0,0 @@
|
||||
#include "g5enc.inl"
|
||||
#include "g5dec.inl"
|
||||
//
|
||||
// Group5 1-bit image compression library
|
||||
// Written by Larry Bank (bitbank@pobox.com)
|
||||
// Decoder C++ wrapper functions
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
int G5DECODER::init(int iWidth, int iHeight, uint8_t *pData, int iDataSize)
|
||||
{
|
||||
return g5_decode_init(&_g5dec, iWidth, iHeight, pData, iDataSize);
|
||||
} /* init() */
|
||||
|
||||
int G5DECODER::decodeLine(uint8_t *pOut)
|
||||
{
|
||||
return g5_decode_line(&_g5dec, pOut);
|
||||
} /* decodeLine() */
|
||||
|
||||
//
|
||||
// Encoder C++ wrapper functions
|
||||
//
|
||||
int G5ENCODER::init(int iWidth, int iHeight, uint8_t *pOut, int iOutSize)
|
||||
{
|
||||
return g5_encode_init(&_g5enc, iWidth, iHeight, pOut, iOutSize);
|
||||
} /* init() */
|
||||
|
||||
int G5ENCODER::encodeLine(uint8_t *pPixels)
|
||||
{
|
||||
return g5_encode_encodeLine(&_g5enc, pPixels);
|
||||
} /* encodeLine() */
|
||||
|
||||
int G5ENCODER::size()
|
||||
{
|
||||
return g5_encode_getOutSize(&_g5enc);
|
||||
} /* size() */
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
#ifndef __GROUP5__
|
||||
#define __GROUP5__
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#ifdef __AVR__
|
||||
#include <avr/pgmspace.h>
|
||||
#endif
|
||||
//
|
||||
// Group5 1-bit image compression library
|
||||
// Written by Larry Bank (bitbank@pobox.com)
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// The name "Group5" is derived from the CCITT Group4 standard
|
||||
// This code is based on a lot of the good ideas from CCITT T.6
|
||||
// for FAX image compression, but modified to work in a very
|
||||
// constrained environment. The Huffman tables for horizontal
|
||||
// mode have been replaced with a simple 2-bit flag followed by
|
||||
// short or long counts of a fixed length. The short codes are
|
||||
// always 3 bits (run lengths 0-7) and the long codes are the
|
||||
// number of bits needed to encode the width of the image.
|
||||
// For example, if a 320 pixel wide image is being compressed,
|
||||
// the longest horizontal run needed is 320, which requires 9
|
||||
// bits to encode. The 2 prefix bits have the following meaning:
|
||||
// 00 = short, short (3+3 bits)
|
||||
// 01 = short, long (3+N bits)
|
||||
// 10 = long, short (N+3 bits)
|
||||
// 11 = long, long (N+N bits)
|
||||
// The rest of the code works identically to Group4 2D FAX
|
||||
//
|
||||
// Caution - this is the maximum number of color changes per line
|
||||
// The default value is set low to work embedded systems with little RAM
|
||||
// for font compression, this is plenty since each line of a character should have
|
||||
// a maximum of 7 color changes
|
||||
// You can define this in your compiler macros to override the default vlaue
|
||||
//
|
||||
#ifndef MAX_IMAGE_FLIPS
|
||||
#ifdef __AVR__
|
||||
#define MAX_IMAGE_FLIPS 32
|
||||
#else
|
||||
#define MAX_IMAGE_FLIPS 512
|
||||
#endif // __AVR__
|
||||
#endif
|
||||
// Horizontal prefix bits
|
||||
enum {
|
||||
HORIZ_SHORT_SHORT=0,
|
||||
HORIZ_SHORT_LONG,
|
||||
HORIZ_LONG_SHORT,
|
||||
HORIZ_LONG_LONG
|
||||
};
|
||||
|
||||
// Return code for encoder and decoder
|
||||
enum {
|
||||
G5_SUCCESS = 0,
|
||||
G5_INVALID_PARAMETER,
|
||||
G5_DECODE_ERROR,
|
||||
G5_UNSUPPORTED_FEATURE,
|
||||
G5_ENCODE_COMPLETE,
|
||||
G5_DECODE_COMPLETE,
|
||||
G5_NOT_INITIALIZED,
|
||||
G5_DATA_OVERFLOW,
|
||||
G5_MAX_FLIPS_EXCEEDED
|
||||
};
|
||||
//
|
||||
// Decoder state
|
||||
//
|
||||
typedef struct g5_dec_image_tag
|
||||
{
|
||||
int iWidth, iHeight; // image size
|
||||
int iError;
|
||||
int y; // last y value drawn
|
||||
int iVLCSize;
|
||||
int iHLen; // length of 'long' horizontal codes for this image
|
||||
int iPitch; // width in bytes of output buffer
|
||||
uint32_t u32Accum; // fractional scaling accumulator
|
||||
uint32_t ulBitOff, ulBits; // vlc decode variables
|
||||
uint8_t *pSrc, *pBuf; // starting & current buffer pointer
|
||||
int16_t *pCur, *pRef; // current state of current vs reference flips
|
||||
int16_t CurFlips[MAX_IMAGE_FLIPS];
|
||||
int16_t RefFlips[MAX_IMAGE_FLIPS];
|
||||
} G5DECIMAGE;
|
||||
|
||||
// Due to unaligned memory causing an exception, we have to do these macros the slow way
|
||||
#ifdef __AVR__
|
||||
// assume PROGMEM as the source of data
|
||||
inline uint32_t TIFFMOTOLONG(uint8_t *p)
|
||||
{
|
||||
uint32_t u32 = pgm_read_dword(p);
|
||||
return __builtin_bswap32(u32);
|
||||
}
|
||||
#else
|
||||
#define TIFFMOTOLONG(p) (((uint32_t)(*p)<<24UL) + ((uint32_t)(*(p+1))<<16UL) + ((uint32_t)(*(p+2))<<8UL) + (uint32_t)(*(p+3)))
|
||||
#endif // __AVR__
|
||||
|
||||
#define TOP_BIT 0x80000000
|
||||
#define MAX_VALUE 0xffffffff
|
||||
// Must be a 32-bit target processor
|
||||
#define REGISTER_WIDTH 32
|
||||
#define BIGUINT uint32_t
|
||||
|
||||
//
|
||||
// G5 Encoder
|
||||
//
|
||||
|
||||
typedef struct g5_buffered_bits
|
||||
{
|
||||
unsigned char *pBuf; // buffer pointer
|
||||
uint32_t ulBits; // buffered bits
|
||||
uint32_t ulBitOff; // current bit offset
|
||||
uint32_t ulDataSize; // available data
|
||||
} G5_BUFFERED_BITS;
|
||||
|
||||
//
|
||||
// Encoder state
|
||||
//
|
||||
typedef struct g5_enc_image_tag
|
||||
{
|
||||
int iWidth, iHeight; // image size
|
||||
int iError;
|
||||
int y; // last y encoded
|
||||
int iOutSize;
|
||||
int iDataSize; // generated output size
|
||||
uint8_t *pOutBuf;
|
||||
int16_t *pCur, *pRef; // pointers to swap current and reference lines
|
||||
G5_BUFFERED_BITS bb;
|
||||
int16_t CurFlips[MAX_IMAGE_FLIPS];
|
||||
int16_t RefFlips[MAX_IMAGE_FLIPS];
|
||||
} G5ENCIMAGE;
|
||||
|
||||
#ifdef __cplusplus
|
||||
//
|
||||
// The G5 classes wrap portable C code which does the actual work
|
||||
//
|
||||
class G5ENCODER
|
||||
{
|
||||
public:
|
||||
int init(int iWidth, int iHeight, uint8_t *pOut, int iOutSize);
|
||||
int encodeLine(uint8_t *pPixels);
|
||||
int size();
|
||||
|
||||
private:
|
||||
G5ENCIMAGE _g5enc;
|
||||
};
|
||||
class G5DECODER
|
||||
{
|
||||
public:
|
||||
int init(int iWidth, int iHeight, uint8_t *pData, int iDataSize);
|
||||
int decodeLine(uint8_t *pOut);
|
||||
|
||||
private:
|
||||
G5DECIMAGE _g5dec;
|
||||
};
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif // __GROUP5__
|
||||
@@ -1,346 +0,0 @@
|
||||
//
|
||||
// Group5
|
||||
// A 1-bpp image decoder
|
||||
//
|
||||
// Written by Larry Bank (bitbank@pobox.com)
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
#include "Group5.h"
|
||||
|
||||
/*
|
||||
The code tree that follows has: bit_length, decode routine
|
||||
These codes are for Group 4 (MMR) decoding
|
||||
|
||||
01 = vertneg1, 11h = vert1, 20h = horiz, 30h = pass, 12h = vert2
|
||||
02 = vertneg2, 13h = vert3, 03 = vertneg3, 90h = trash
|
||||
*/
|
||||
|
||||
static const uint8_t code_table[128] =
|
||||
{0x90, 0, 0x40, 0, /* trash, uncompr mode - codes 0 and 1 */
|
||||
3, 7, /* V(-3) pos = 2 */
|
||||
0x13, 7, /* V(3) pos = 3 */
|
||||
2, 6, 2, 6, /* V(-2) pos = 4,5 */
|
||||
0x12, 6, 0x12, 6, /* V(2) pos = 6,7 */
|
||||
0x30, 4, 0x30, 4, 0x30, 4, 0x30, 4, /* pass pos = 8->F */
|
||||
0x30, 4, 0x30, 4, 0x30, 4, 0x30, 4,
|
||||
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3, /* horiz pos = 10->1F */
|
||||
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3,
|
||||
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3,
|
||||
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3,
|
||||
/* V(-1) pos = 20->2F */
|
||||
1, 3, 1, 3, 1, 3, 1, 3,
|
||||
1, 3, 1, 3, 1, 3, 1, 3,
|
||||
1, 3, 1, 3, 1, 3, 1, 3,
|
||||
1, 3, 1, 3, 1, 3, 1, 3,
|
||||
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3, /* V(1) pos = 30->3F */
|
||||
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3,
|
||||
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3,
|
||||
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3};
|
||||
|
||||
static int g5_decode_init(G5DECIMAGE *pImage, int iWidth, int iHeight, uint8_t *pData, int iDataSize)
|
||||
{
|
||||
if (pImage == NULL || iWidth < 1 || iHeight < 1 || pData == NULL || iDataSize < 1)
|
||||
return G5_INVALID_PARAMETER;
|
||||
|
||||
pImage->iVLCSize = iDataSize;
|
||||
pImage->pSrc = pData;
|
||||
pImage->ulBitOff = 0;
|
||||
pImage->y = 0;
|
||||
pImage->ulBits = TIFFMOTOLONG(pData); // preload the first 32 bits of data
|
||||
pImage->iWidth = iWidth;
|
||||
pImage->iHeight = iHeight;
|
||||
return G5_SUCCESS;
|
||||
|
||||
} /* g5_decode_init() */
|
||||
|
||||
static void G5DrawLine(G5DECIMAGE *pPage, int16_t *pCurFlips, uint8_t *pOut)
|
||||
{
|
||||
int x, len, run;
|
||||
uint8_t lBit, rBit, *p;
|
||||
int iStart = 0, xright = pPage->iWidth;
|
||||
uint8_t *pDest;
|
||||
|
||||
iStart = 0;
|
||||
|
||||
pDest = pOut;
|
||||
len = (xright+7)>>3; // number of bytes to generate
|
||||
for (x=0; x<len; x++) {
|
||||
pOut[x] = 0xff; // start with white and only draw the black runs
|
||||
}
|
||||
x = 0;
|
||||
while (x < xright) { // while the scaled x is within the window bounds
|
||||
x = *pCurFlips++; // black starting point
|
||||
run = *pCurFlips++ - x; // get the black run
|
||||
x -= iStart;
|
||||
if (x >= xright || run == 0)
|
||||
break;
|
||||
if ((x + run) > 0) { /* If the run is visible, draw it */
|
||||
if (x < 0) {
|
||||
run += x; /* draw only visible part of run */
|
||||
x = 0;
|
||||
}
|
||||
if ((x + run) > xright) { /* Don't let it go off right edge */
|
||||
run = xright - x;
|
||||
}
|
||||
/* Draw this run */
|
||||
lBit = 0xff << (8 - (x & 7));
|
||||
rBit = 0xff >> ((x + run) & 7);
|
||||
len = ((x+run)>>3) - (x >> 3);
|
||||
p = &pDest[x >> 3];
|
||||
if (len == 0) {
|
||||
lBit |= rBit;
|
||||
*p &= lBit;
|
||||
} else {
|
||||
*p++ &= lBit;
|
||||
while (len > 1) {
|
||||
*p++ = 0;
|
||||
len--;
|
||||
}
|
||||
*p = rBit;
|
||||
}
|
||||
} // visible run
|
||||
} /* while drawing line */
|
||||
} /* G5DrawLine() */
|
||||
//
|
||||
// Initialize internal structures to decode the image
|
||||
//
|
||||
static void Decode_Begin(G5DECIMAGE *pPage)
|
||||
{
|
||||
int i, xsize;
|
||||
int16_t *CurFlips, *RefFlips;
|
||||
|
||||
xsize = pPage->iWidth;
|
||||
|
||||
RefFlips = pPage->RefFlips;
|
||||
CurFlips = pPage->CurFlips;
|
||||
|
||||
/* Seed the current and reference line with XSIZE for V(0) codes */
|
||||
for (i=0; i<MAX_IMAGE_FLIPS-2; i++) {
|
||||
RefFlips[i] = xsize;
|
||||
CurFlips[i] = xsize;
|
||||
}
|
||||
/* Prefill both current and reference lines with 7fff to prevent it from
|
||||
walking off the end if the data gets bunged and the current X is > XSIZE
|
||||
3-16-94 */
|
||||
CurFlips[i] = RefFlips[i] = 0x7fff;
|
||||
CurFlips[i+1] = RefFlips[i+1] = 0x7fff;
|
||||
|
||||
pPage->pCur = CurFlips;
|
||||
pPage->pRef = RefFlips;
|
||||
pPage->pBuf = pPage->pSrc;
|
||||
pPage->ulBits = TIFFMOTOLONG(pPage->pSrc); // load 32 bits to start
|
||||
pPage->ulBitOff = 0;
|
||||
// Calculate the number of bits needed for a long horizontal code
|
||||
#ifdef __AVR__
|
||||
pPage->iHLen = 16 - __builtin_clz(pPage->iWidth);
|
||||
#else
|
||||
pPage->iHLen = 32 - __builtin_clz(pPage->iWidth);
|
||||
#endif
|
||||
} /* Decode_Begin() */
|
||||
//
|
||||
// Decode a single line of G5 data (private function)
|
||||
//
|
||||
static int DecodeLine(G5DECIMAGE *pPage)
|
||||
{
|
||||
signed int a0, a0_p, b1;
|
||||
int16_t *pCur, *pRef, *RefFlips, *CurFlips;
|
||||
int xsize, tot_run=0, tot_run1 = 0;
|
||||
int32_t sCode;
|
||||
uint32_t lBits;
|
||||
uint32_t ulBits, ulBitOff;
|
||||
uint8_t *pBuf/*, *pBufEnd*/;
|
||||
uint32_t u32HMask, u32HLen; // horizontal code mask and length
|
||||
|
||||
pCur = CurFlips = pPage->pCur;
|
||||
pRef = RefFlips = pPage->pRef;
|
||||
ulBits = pPage->ulBits;
|
||||
ulBitOff = pPage->ulBitOff;
|
||||
pBuf = pPage->pBuf;
|
||||
// pBufEnd = &pPage->pSrc[pPage->iVLCSize];
|
||||
u32HLen = pPage->iHLen;
|
||||
u32HMask = (1 << u32HLen) - 1;
|
||||
a0 = -1;
|
||||
xsize = pPage->iWidth;
|
||||
|
||||
while (a0 < xsize) { /* Decode this line */
|
||||
if (ulBitOff > (REGISTER_WIDTH-8)) { // need at least 7 unused bits
|
||||
pBuf += (ulBitOff >> 3);
|
||||
ulBitOff &= 7;
|
||||
ulBits = TIFFMOTOLONG(pBuf);
|
||||
}
|
||||
if ((int32_t)(ulBits << ulBitOff) < 0) { /* V(0) code is the most frequent case (1 bit) */
|
||||
a0 = *pRef++;
|
||||
ulBitOff++; // length = 1 bit
|
||||
*pCur++ = a0;
|
||||
} else { /* Slower method for the less frequence codes */
|
||||
lBits = (ulBits >> ((REGISTER_WIDTH - 8) - ulBitOff)) & 0xfe; /* Only the first 7 bits are useful */
|
||||
sCode = code_table[lBits]; /* Get the code type as an 8-bit value */
|
||||
ulBitOff += code_table[lBits+1]; /* Get the code length */
|
||||
switch (sCode) {
|
||||
case 1: /* V(-1) */
|
||||
case 2: /* V(-2) */
|
||||
case 3: /* V(-3) */
|
||||
a0 = *pRef - sCode; /* A0 = B1 - x */
|
||||
*pCur++ = a0;
|
||||
if (pRef == RefFlips) {
|
||||
pRef += 2;
|
||||
}
|
||||
pRef--;
|
||||
while (a0 >= *pRef) {
|
||||
pRef += 2;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x11: /* V(1) */
|
||||
case 0x12: /* V(2) */
|
||||
case 0x13: /* V(3) */
|
||||
a0 = *pRef++; /* A0 = B1 */
|
||||
b1 = a0;
|
||||
a0 += sCode & 7; /* A0 = B1 + x */
|
||||
if (b1 != xsize && a0 < xsize) {
|
||||
while (a0 >= *pRef) {
|
||||
pRef += 2;
|
||||
}
|
||||
}
|
||||
if (a0 > xsize) {
|
||||
a0 = xsize;
|
||||
}
|
||||
*pCur++ = a0;
|
||||
break;
|
||||
|
||||
case 0x20: /* Horizontal codes */
|
||||
if (ulBitOff > (REGISTER_WIDTH-16)) { // need at least 16 unused bits
|
||||
pBuf += (ulBitOff >> 3);
|
||||
ulBitOff &= 7;
|
||||
ulBits = TIFFMOTOLONG(pBuf);
|
||||
}
|
||||
a0_p = a0;
|
||||
if (a0 < 0) {
|
||||
a0_p = 0;
|
||||
}
|
||||
lBits = (ulBits >> ((REGISTER_WIDTH - 2) - ulBitOff)) & 0x3; // get 2-bit prefix for code type
|
||||
// There are 4 possible horizontal cases: short/short, short/long, long/short, long/long
|
||||
// These are encoded in a 2-bit prefix code, followed by 3 bits for short or N bits for long code
|
||||
// N is the log base 2 of the image width (e.g. 320 pixels requires 9 bits)
|
||||
ulBitOff += 2;
|
||||
switch (lBits) {
|
||||
case HORIZ_SHORT_SHORT:
|
||||
tot_run = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
|
||||
ulBitOff += 3;
|
||||
tot_run1 = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
|
||||
ulBitOff += 3;
|
||||
break;
|
||||
case HORIZ_SHORT_LONG:
|
||||
tot_run = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
|
||||
ulBitOff += 3;
|
||||
tot_run1 = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
|
||||
ulBitOff += u32HLen;
|
||||
break;
|
||||
case HORIZ_LONG_SHORT:
|
||||
tot_run = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
|
||||
ulBitOff += u32HLen;
|
||||
tot_run1 = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
|
||||
ulBitOff += 3;
|
||||
break;
|
||||
case HORIZ_LONG_LONG:
|
||||
tot_run = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
|
||||
ulBitOff += u32HLen;
|
||||
if (ulBitOff > (REGISTER_WIDTH-16)) { // need at least 16 unused bits
|
||||
pBuf += (ulBitOff >> 3);
|
||||
ulBitOff &= 7;
|
||||
ulBits = TIFFMOTOLONG(pBuf);
|
||||
}
|
||||
tot_run1 = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
|
||||
ulBitOff += u32HLen;
|
||||
break;
|
||||
} // switch on lBits
|
||||
a0 = a0_p + tot_run;
|
||||
*pCur++ = a0;
|
||||
a0 += tot_run1;
|
||||
if (a0 < xsize) {
|
||||
while (a0 >= *pRef) {
|
||||
pRef += 2;
|
||||
}
|
||||
}
|
||||
*pCur++ = a0;
|
||||
break;
|
||||
|
||||
case 0x30: /* Pass code */
|
||||
pRef++; /* A0 = B2, iRef+=2 */
|
||||
a0 = *pRef++;
|
||||
break;
|
||||
|
||||
default: /* ERROR */
|
||||
pPage->iError = G5_DECODE_ERROR;
|
||||
goto pilreadg5z;
|
||||
} /* switch */
|
||||
} /* Slow climb */
|
||||
}
|
||||
/*--- Convert flips data into run lengths ---*/
|
||||
*pCur++ = xsize; /* Terminate the line properly */
|
||||
*pCur++ = xsize;
|
||||
pilreadg5z:
|
||||
// Save the current VLC decoder state
|
||||
pPage->ulBits = ulBits;
|
||||
pPage->ulBitOff = ulBitOff;
|
||||
pPage->pBuf = pBuf;
|
||||
return pPage->iError;
|
||||
} /* DecodeLine() */
|
||||
//
|
||||
// Decompress the VLC data
|
||||
//
|
||||
static int g5_decode_line(G5DECIMAGE *pPage, uint8_t *pOut)
|
||||
{
|
||||
int rc;
|
||||
uint8_t *pBufEnd;
|
||||
int16_t *t1;
|
||||
|
||||
if (pPage == NULL || pOut == NULL)
|
||||
return G5_INVALID_PARAMETER;
|
||||
if (pPage->y >= pPage->iHeight)
|
||||
return G5_DECODE_COMPLETE;
|
||||
|
||||
pPage->iError = G5_SUCCESS;
|
||||
|
||||
if (pPage->y == 0) { // first time through
|
||||
Decode_Begin(pPage);
|
||||
}
|
||||
pBufEnd = &pPage->pSrc[pPage->iVLCSize];
|
||||
|
||||
if (pPage->pBuf >= pBufEnd) { // read past the end, error
|
||||
pPage->iError = G5_DECODE_ERROR;
|
||||
return G5_DECODE_ERROR;
|
||||
}
|
||||
rc = DecodeLine(pPage);
|
||||
if (rc == G5_SUCCESS) {
|
||||
// Draw the current line
|
||||
G5DrawLine(pPage, pPage->pCur, pOut);
|
||||
/*--- Swap current and reference lines ---*/
|
||||
t1 = pPage->pRef;
|
||||
pPage->pRef = pPage->pCur;
|
||||
pPage->pCur = t1;
|
||||
pPage->y++;
|
||||
if (pPage->y >= pPage->iHeight) {
|
||||
pPage->iError = G5_DECODE_COMPLETE;
|
||||
}
|
||||
} else {
|
||||
pPage->iError = rc;
|
||||
}
|
||||
return pPage->iError;
|
||||
} /* Decode() */
|
||||
@@ -1,315 +0,0 @@
|
||||
//
|
||||
// G5 Encoder
|
||||
// A 1-bpp image encoding library
|
||||
//
|
||||
// Written by Larry Bank (bitbank@pobox.com)
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
#include "Group5.h"
|
||||
|
||||
/* Number of consecutive 1 bits in a byte from MSB to LSB */
|
||||
static uint8_t bitcount[256] =
|
||||
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 0-15 */
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 16-31 */
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 32-47 */
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 48-63 */
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 64-79 */
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 80-95 */
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 96-111 */
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 112-127 */
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 128-143 */
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 144-159 */
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 160-175 */
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 176-191 */
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, /* 192-207 */
|
||||
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, /* 208-223 */
|
||||
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, /* 224-239 */
|
||||
4,4,4,4,4,4,4,4,5,5,5,5,6,6,7,8}; /* 240-255 */
|
||||
|
||||
/* Table of vertical codes for G5 encoding */
|
||||
/* code followed by length, starting with v(-3) */
|
||||
static const uint8_t vtable[14] =
|
||||
{3,7, /* V(-3) = 0000011 */
|
||||
3,6, /* V(-2) = 000011 */
|
||||
3,3, /* V(-1) = 011 */
|
||||
1,1, /* V(0) = 1 */
|
||||
2,3, /* V(1) = 010 */
|
||||
2,6, /* V(2) = 000010 */
|
||||
2,7}; /* V(3) = 0000010 */
|
||||
|
||||
|
||||
static void G5ENCInsertCode(G5_BUFFERED_BITS *bb, BIGUINT ulCode, int iLen)
|
||||
{
|
||||
if ((bb->ulBitOff + iLen) > REGISTER_WIDTH) { // need to write data
|
||||
bb->ulBits |= (ulCode >> (bb->ulBitOff + iLen - REGISTER_WIDTH)); // partial bits on first word
|
||||
*(BIGUINT *)bb->pBuf = __builtin_bswap32(bb->ulBits);
|
||||
bb->pBuf += sizeof(BIGUINT);
|
||||
bb->ulBits = ulCode << ((REGISTER_WIDTH*2) - (bb->ulBitOff + iLen));
|
||||
bb->ulBitOff += iLen - REGISTER_WIDTH;
|
||||
} else {
|
||||
bb->ulBits |= (ulCode << (REGISTER_WIDTH - bb->ulBitOff - iLen));
|
||||
bb->ulBitOff += iLen;
|
||||
}
|
||||
} /* G5ENCInsertCode() */
|
||||
//
|
||||
// Flush any buffered bits to the output
|
||||
//
|
||||
static void G5ENCFlushBits(G5_BUFFERED_BITS *bb)
|
||||
{
|
||||
while (bb->ulBitOff >= 8)
|
||||
{
|
||||
*bb->pBuf++ = (unsigned char) (bb->ulBits >> (REGISTER_WIDTH - 8));
|
||||
bb->ulBits <<= 8;
|
||||
bb->ulBitOff -= 8;
|
||||
}
|
||||
if (bb->ulBitOff) { // partial byte?
|
||||
*bb->pBuf++ = (unsigned char) (bb->ulBits >> (REGISTER_WIDTH - 8));
|
||||
}
|
||||
bb->ulBitOff = 0;
|
||||
bb->ulBits = 0;
|
||||
} /* G5ENCFlushBits() */
|
||||
//
|
||||
// Initialize the compressor
|
||||
// This must be called before adding data to the output
|
||||
//
|
||||
static int g5_encode_init(G5ENCIMAGE *pImage, int iWidth, int iHeight, uint8_t *pOut, int iOutSize)
|
||||
{
|
||||
int iError = G5_SUCCESS;
|
||||
|
||||
if (pImage == NULL || iHeight <= 0)
|
||||
return G5_INVALID_PARAMETER;
|
||||
pImage->iWidth = iWidth; // image size
|
||||
pImage->iHeight = iHeight;
|
||||
pImage->pCur = pImage->CurFlips;
|
||||
pImage->pRef = pImage->RefFlips;
|
||||
pImage->pOutBuf = pOut; // optional output buffer
|
||||
pImage->iOutSize = iOutSize; // output buffer pre-allocated size
|
||||
pImage->iDataSize = 0; // no data yet
|
||||
pImage->y = 0;
|
||||
for (int i=0; i<MAX_IMAGE_FLIPS; i++) {
|
||||
pImage->RefFlips[i] = iWidth;
|
||||
pImage->CurFlips[i] = iWidth;
|
||||
}
|
||||
pImage->bb.pBuf = pImage->pOutBuf;
|
||||
pImage->bb.ulBits = 0;
|
||||
pImage->bb.ulBitOff = 0;
|
||||
pImage->iError = iError;
|
||||
return iError;
|
||||
} /* g5_encode_init() */
|
||||
//
|
||||
// Internal function to convert uncompressed 1-bit per pixel data
|
||||
// into the run-end data needed to feed the G5 encoder
|
||||
//
|
||||
static int G5ENCEncodeLine(unsigned char *buf, int xsize, int16_t *pDest)
|
||||
{
|
||||
int iCount, xborder;
|
||||
uint8_t i, c;
|
||||
int8_t cBits;
|
||||
int iLen;
|
||||
int16_t x;
|
||||
int16_t *pLimit = pDest + (MAX_IMAGE_FLIPS-4);
|
||||
|
||||
xborder = xsize;
|
||||
iCount = (xsize + 7) >> 3; /* Number of bytes per line */
|
||||
cBits = 8;
|
||||
iLen = 0; /* Current run length */
|
||||
x = 0;
|
||||
|
||||
c = *buf++; /* Get the first byte to start */
|
||||
iCount--;
|
||||
while (iCount >=0) {
|
||||
if (pDest >= pLimit) return G5_MAX_FLIPS_EXCEEDED;
|
||||
i = bitcount[c]; /* Get the number of consecutive bits */
|
||||
iLen += i; /* Add this length to total run length */
|
||||
c <<= i;
|
||||
cBits -= i; /* Minus the number in a byte */
|
||||
if (cBits <= 0)
|
||||
{
|
||||
iLen += cBits; /* Adjust length */
|
||||
cBits = 8;
|
||||
c = *buf++; /* Get another data byte */
|
||||
iCount--;
|
||||
continue; /* Keep doing white until color change */
|
||||
}
|
||||
c = ~c; /* flip color to count black pixels */
|
||||
/* Store the white run length */
|
||||
xborder -= iLen;
|
||||
if (xborder < 0)
|
||||
{
|
||||
iLen += xborder; /* Make sure run length is not past end */
|
||||
break;
|
||||
}
|
||||
x += iLen;
|
||||
*pDest++ = x;
|
||||
iLen = 0;
|
||||
doblack:
|
||||
i = bitcount[c]; /* Get consecutive bits */
|
||||
iLen += i; /* Add to total run length */
|
||||
c <<= i;
|
||||
cBits -= i;
|
||||
if (cBits <= 0)
|
||||
{
|
||||
iLen += cBits; /* Adjust length */
|
||||
cBits = 8;
|
||||
c = *buf++; /* Get another data byte */
|
||||
c = ~c; /* Flip color to find black */
|
||||
iCount--;
|
||||
if (iCount < 0)
|
||||
break;
|
||||
goto doblack;
|
||||
}
|
||||
/* Store the black run length */
|
||||
c = ~c; /* Flip color again to find white pixels */
|
||||
xborder -= iLen;
|
||||
if (xborder < 0)
|
||||
{
|
||||
iLen += xborder; /* Make sure run length is not past end */
|
||||
break;
|
||||
}
|
||||
x += iLen;
|
||||
*pDest++ = x;
|
||||
iLen = 0;
|
||||
} /* while */
|
||||
|
||||
if (pDest >= pLimit) return G5_MAX_FLIPS_EXCEEDED;
|
||||
*pDest++ = xsize;
|
||||
*pDest++ = xsize; // Store a few more XSIZE to end the line
|
||||
*pDest++ = xsize; // so that the compressor doesn't go past
|
||||
*pDest++ = xsize; // the end of the line
|
||||
return G5_SUCCESS;
|
||||
} /* G5ENCEncodeLine() */
|
||||
//
|
||||
// Compress a line of pixels and add it to the output
|
||||
// the input format is expected to be MSB (most significant bit) first
|
||||
// for example, pixel 0 is in byte 0 at bit 7 (0x80)
|
||||
// Returns G5ENC_SUCCESS for each line if all is well and G5ENC_IMAGE_COMPLETE
|
||||
// for the last line
|
||||
//
|
||||
static int g5_encode_encodeLine(G5ENCIMAGE *pImage, uint8_t *pPixels)
|
||||
{
|
||||
int16_t a0, a0_c, b2, a1;
|
||||
int dx, run1, run2;
|
||||
int xsize, iErr, iHighWater;
|
||||
int iCur, iRef, iLen;
|
||||
int iHLen; // number of bits for long horizontal codes
|
||||
int16_t *CurFlips, *RefFlips;
|
||||
G5_BUFFERED_BITS bb;
|
||||
|
||||
if (pImage == NULL || pPixels == NULL)
|
||||
return G5_INVALID_PARAMETER;
|
||||
iHighWater = pImage->iOutSize - 32;
|
||||
iHLen = 32 - __builtin_clz(pImage->iWidth);
|
||||
memcpy(&bb, &pImage->bb, sizeof(G5_BUFFERED_BITS)); // keep local copy
|
||||
CurFlips = pImage->pCur;
|
||||
RefFlips = pImage->pRef;
|
||||
xsize = pImage->iWidth; /* For performance reasons */
|
||||
|
||||
// Convert the incoming line of pixels into run-end data
|
||||
iErr = G5ENCEncodeLine(pPixels, pImage->iWidth, CurFlips);
|
||||
if (iErr != G5_SUCCESS) return iErr; // exceeded the maximum number of color changes
|
||||
/* Encode this line as G5 */
|
||||
a0 = a0_c = 0;
|
||||
iCur = iRef = 0;
|
||||
while (a0 < xsize) {
|
||||
b2 = RefFlips[iRef+1];
|
||||
a1 = CurFlips[iCur];
|
||||
if (b2 < a1) { /* Is b2 to the left of a1? */
|
||||
/* yes, do pass mode */
|
||||
a0 = b2;
|
||||
iRef += 2;
|
||||
G5ENCInsertCode(&bb, 1, 4); /* Pass code = 0001 */
|
||||
} else { /* Try vertical and horizontal mode */
|
||||
dx = RefFlips[iRef] - a1; /* b1 - a1 */
|
||||
if (dx > 3 || dx < -3) { /* Horizontal mode */
|
||||
G5ENCInsertCode(&bb, 1, 3); /* Horizontal code = 001 */
|
||||
run1 = CurFlips[iCur] - a0;
|
||||
run2 = CurFlips[iCur+1] - CurFlips[iCur];
|
||||
if (run1 < 8) {
|
||||
if (run2 < 8) { // short, short
|
||||
G5ENCInsertCode(&bb, HORIZ_SHORT_SHORT, 2); /* short, short = 00 */
|
||||
G5ENCInsertCode(&bb, run1, 3);
|
||||
G5ENCInsertCode(&bb, run2, 3);
|
||||
} else { // short, long
|
||||
G5ENCInsertCode(&bb, HORIZ_SHORT_LONG, 2); /* short, long = 01 */
|
||||
G5ENCInsertCode(&bb, run1, 3);
|
||||
G5ENCInsertCode(&bb, run2, iHLen);
|
||||
}
|
||||
} else { // first run is long
|
||||
if (run2 < 8) { // long, short
|
||||
G5ENCInsertCode(&bb, HORIZ_LONG_SHORT, 2); /* long, short = 10 */
|
||||
G5ENCInsertCode(&bb, run1, iHLen);
|
||||
G5ENCInsertCode(&bb, run2, 3);
|
||||
} else { // long, long
|
||||
G5ENCInsertCode(&bb, HORIZ_LONG_LONG, 2); /* long, long = 11 */
|
||||
G5ENCInsertCode(&bb, run1, iHLen);
|
||||
G5ENCInsertCode(&bb, run2, iHLen);
|
||||
}
|
||||
}
|
||||
a0 = CurFlips[iCur+1]; /* a0 = a2 */
|
||||
if (a0 != xsize) {
|
||||
iCur += 2; /* Skip two color flips */
|
||||
while (RefFlips[iRef] != xsize && RefFlips[iRef] <= a0) {
|
||||
iRef += 2;
|
||||
}
|
||||
}
|
||||
} else { /* Vertical mode */
|
||||
dx = (dx + 3) * 2; /* Convert to index table */
|
||||
G5ENCInsertCode(&bb, vtable[dx], vtable[dx+1]);
|
||||
a0 = a1;
|
||||
a0_c = 1-a0_c;
|
||||
if (a0 != xsize) {
|
||||
if (iRef != 0) {
|
||||
iRef -= 2;
|
||||
}
|
||||
iRef++; /* Skip a color change in cur and ref */
|
||||
iCur++;
|
||||
while (RefFlips[iRef] <= a0 && RefFlips[iRef] != xsize) {
|
||||
iRef += 2;
|
||||
}
|
||||
}
|
||||
} /* vertical mode */
|
||||
} /* horiz/vert mode */
|
||||
} /* while x < xsize */
|
||||
iLen = (int)(bb.pBuf-pImage->pOutBuf);
|
||||
if (iLen >= iHighWater) { // not enough space
|
||||
pImage->iError = iErr = G5_DATA_OVERFLOW; // we don't have a better error
|
||||
return iErr;
|
||||
}
|
||||
if (pImage->y == pImage->iHeight-1) { // last line of image
|
||||
G5ENCFlushBits(&bb); // output the final buffered bits
|
||||
// wrap up final output
|
||||
pImage->iDataSize = 1 + (int)(bb.pBuf-pImage->pOutBuf);
|
||||
iErr = G5_ENCODE_COMPLETE;
|
||||
}
|
||||
pImage->pCur = RefFlips; // swap current and reference lines
|
||||
pImage->pRef = CurFlips;
|
||||
pImage->y++;
|
||||
memcpy(&pImage->bb, &bb, sizeof(bb));
|
||||
return iErr;
|
||||
} /* g5_encode_encodeLine() */
|
||||
//
|
||||
// Returns the number of bytes of G5 created by the encoder
|
||||
//
|
||||
static int g5_encode_getOutSize(G5ENCIMAGE *pImage)
|
||||
{
|
||||
int iSize = 0;
|
||||
if (pImage != NULL)
|
||||
iSize = pImage->iDataSize;
|
||||
return iSize;
|
||||
} /* g5_encode_getOutSize() */
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <builtinFonts/bookerly_12.h>
|
||||
#include <builtinFonts/bookerly_14.h>
|
||||
#include <builtinFonts/bookerly_16.h>
|
||||
#include <builtinFonts/bookerly_18.h>
|
||||
#include <builtinFonts/notosans_8.h>
|
||||
#include <builtinFonts/notosans_12.h>
|
||||
#include <builtinFonts/notosans_14.h>
|
||||
#include <builtinFonts/notosans_16.h>
|
||||
#include <builtinFonts/notosans_18.h>
|
||||
#include <builtinFonts/opendyslexic_8.h>
|
||||
#include <builtinFonts/opendyslexic_10.h>
|
||||
#include <builtinFonts/opendyslexic_12.h>
|
||||
#include <builtinFonts/opendyslexic_14.h>
|
||||
#include <builtinFonts/ubuntu_10.h>
|
||||
#include <builtinFonts/ubuntu_12.h>
|
||||
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
@@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "// The contents of this file are generated by ./lib/CrossPointFont/builtinFonts/build-font-ids.sh"
|
||||
echo "#pragma once"
|
||||
echo ""
|
||||
|
||||
echo "#define BOOKERLY_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define BOOKERLY_14_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_14.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define BOOKERLY_16_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_16.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define BOOKERLY_18_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_18.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define NOTOSANS_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define NOTOSANS_14_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_14.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define NOTOSANS_16_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_16.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define NOTOSANS_18_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_18.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define OPENDYSLEXIC_8_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_8.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define OPENDYSLEXIC_10_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_10.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define OPENDYSLEXIC_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define OPENDYSLEXIC_14_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_14.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define UI_10_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./ubuntu_10.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define UI_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./ubuntu_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
echo "#define SMALL_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_8.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
BOOKERLY_FONT_SIZES=(12 14 16 18)
|
||||
NOTOSANS_FONT_SIZES=(12 14 16 18)
|
||||
OPENDYSLEXIC_FONT_SIZES=(8 10 12 14)
|
||||
|
||||
for size in ${BOOKERLY_FONT_SIZES[@]}; do
|
||||
font_name="bookerly_${size}"
|
||||
font_path_prefix="./source/Bookerly/Bookerly-"
|
||||
output_path="./${font_name}.h"
|
||||
../fontconvert/fontconvert "${font_path_prefix}Regular.ttf" -b "${font_path_prefix}Bold.ttf" -i "${font_path_prefix}Italic.ttf" -bi "${font_path_prefix}BoldItalic.ttf" -o $output_path -p $size
|
||||
done
|
||||
|
||||
for size in ${NOTOSANS_FONT_SIZES[@]}; do
|
||||
font_name="notosans_${size}"
|
||||
font_path_prefix="./source/NotoSans/NotoSans-"
|
||||
output_path="./${font_name}.h"
|
||||
../fontconvert/fontconvert "${font_path_prefix}Regular.ttf" -b "${font_path_prefix}Bold.ttf" -i "${font_path_prefix}Italic.ttf" -bi "${font_path_prefix}BoldItalic.ttf" -o $output_path -p $size
|
||||
done
|
||||
|
||||
for size in ${OPENDYSLEXIC_FONT_SIZES[@]}; do
|
||||
font_name="opendyslexic_${size}"
|
||||
font_path_prefix="./source/OpenDyslexic/OpenDyslexic-"
|
||||
output_path="./${font_name}.h"
|
||||
../fontconvert/fontconvert "${font_path_prefix}Regular.otf" -b "${font_path_prefix}Bold.otf" -i "${font_path_prefix}Italic.otf" -bi "${font_path_prefix}BoldItalic.otf" -o $output_path -p $size
|
||||
done
|
||||
|
||||
UI_FONT_SIZES=(10 12)
|
||||
UI_FONT_STYLES=("Regular" "Bold")
|
||||
|
||||
for size in ${UI_FONT_SIZES[@]}; do
|
||||
font_name="ubuntu_${size}"
|
||||
font_path_prefix="./source/Ubuntu/Ubuntu-"
|
||||
output_path="./${font_name}.h"
|
||||
../fontconvert/fontconvert "${font_path_prefix}Regular.ttf" -b "${font_path_prefix}Bold.ttf" -o $output_path -p $size
|
||||
done
|
||||
|
||||
../fontconvert/fontconvert ./source/NotoSans/NotoSans-Regular.ttf -o ./notosans_8.h -p 8
|
||||
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
@@ -1 +0,0 @@
|
||||
fontconvert
|
||||
@@ -1,12 +0,0 @@
|
||||
all: fontconvert
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -I/usr/local/include/freetype2 -I/usr/include/freetype2 -I/usr/include -I/opt/homebrew/include/freetype2
|
||||
LIBS = -L/opt/homebrew/lib -lfreetype
|
||||
|
||||
fontconvert: main.c ../Group5/Group5.h
|
||||
$(CC) $(CFLAGS) main.c $(LIBS) -o fontconvert
|
||||
strip fontconvert
|
||||
|
||||
clean:
|
||||
rm -f fontconvert
|
||||
@@ -1,553 +0,0 @@
|
||||
//
|
||||
// TrueType to CrossPoint font converter
|
||||
// Copyright (c) 2024 BitBank Software, inc.
|
||||
// Written by Larry Bank, adapted by Dave Allie for CrossPoint
|
||||
// August 31, 2024
|
||||
// The CrossPoint font format is a losslessly compressed bitmap font of a single point size with multiple variants
|
||||
// This was built entirely on the back of Larry Bank's bb_font format.
|
||||
// The data is compressed with a compression scheme based on CCITT T.6
|
||||
// The font structure includes overall size, per-character glyph info and then the
|
||||
// compressed image data at the end.
|
||||
// The font file format is designed to allow both dynamic loading of font data from
|
||||
// external memory/disk or compiling the data as const into a progarm.
|
||||
//
|
||||
// Example usage:
|
||||
// ./fontconvert <regular.ttf> [-b <bold.ttf>] [-i <italic.ttf>] [-bi <bold-italic.ttf>] -p <pt size> -o <out.cpf>
|
||||
// ./fontconvert <regular.ttf> [-b <bold.ttf>] [-i <italic.ttf>] [-bi <bold-italic.ttf>] -p <pt size> -o <out.h>
|
||||
//
|
||||
// This code requires the freetype library
|
||||
// found here: www.freetype.org
|
||||
//
|
||||
|
||||
#ifndef ARDUINO
|
||||
|
||||
#include <ctype.h>
|
||||
#include <ft2build.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../CrossPointFontFormat.h"
|
||||
|
||||
#include FT_GLYPH_H
|
||||
#include FT_MODULE_H
|
||||
#include FT_TRUETYPE_DRIVER_H
|
||||
#include "../Group5/g5enc.inl" // Group5 image compression library
|
||||
G5ENCIMAGE g5enc; // Group5 encoder state
|
||||
|
||||
#define DPI 150 // Approximate resolution of common displays
|
||||
#define OUTBUF_SIZE 1048576 // 1MB
|
||||
#define MAX_INTERVALS 65536
|
||||
#define FONT_SCALE_FACTOR 2
|
||||
// TODO: Re-enable small font
|
||||
// Disabled small font generation to get this working, but want to re-enable
|
||||
#define SMALL_FONT_ENABLED 0
|
||||
|
||||
uint32_t raw_intervals[][2] = {
|
||||
/* Basic Latin */
|
||||
// ASCII letters, digits, punctuation, control characters
|
||||
{0x0000, 0x007F},
|
||||
/* Latin-1 Supplement */
|
||||
// Accented characters for Western European languages
|
||||
{0x0080, 0x00FF},
|
||||
/* Latin Extended-A */
|
||||
// Eastern European and Baltic languages
|
||||
{0x0100, 0x017F},
|
||||
/* General Punctuation (core subset) */
|
||||
// Smart quotes, en dash, em dash, ellipsis, NO-BREAK SPACE
|
||||
{0x2000, 0x206F},
|
||||
/* Basic Symbols From "Latin-1 + Misc" */
|
||||
// dashes, quotes, prime marks
|
||||
{0x2010, 0x203A},
|
||||
// misc punctuation
|
||||
{0x2040, 0x205F},
|
||||
// common currency symbols
|
||||
{0x20A0, 0x20CF},
|
||||
/* Combining Diacritical Marks (minimal subset) */
|
||||
// Needed for proper rendering of many extended Latin languages
|
||||
{0x0300, 0x036F},
|
||||
/* Greek & Coptic */
|
||||
// Used in science, maths, philosophy, some academic texts
|
||||
// {0x0370, 0x03FF},
|
||||
/* Cyrillic */
|
||||
// Russian, Ukrainian, Bulgarian, etc.
|
||||
{0x0400, 0x04FF},
|
||||
/* Math Symbols (common subset) */
|
||||
// Superscripts and Subscripts
|
||||
{0x2070, 0x209F},
|
||||
// General math operators
|
||||
{0x2200, 0x22FF},
|
||||
// Arrows
|
||||
{0x2190, 0x21FF},
|
||||
/* CJK */
|
||||
// Core Unified Ideographs
|
||||
// {0x4E00, 0x9FFF},
|
||||
// Extension A
|
||||
// {0x3400, 0x4DBF},
|
||||
// Extension B
|
||||
// {0x20000, 0x2A6DF},
|
||||
// Extension C–F
|
||||
// {0x2A700, 0x2EBEF},
|
||||
// Extension G
|
||||
// {0x30000, 0x3134F},
|
||||
// Hiragana
|
||||
// {0x3040, 0x309F},
|
||||
// Katakana
|
||||
// {0x30A0, 0x30FF},
|
||||
// Katakana Phonetic Extensions
|
||||
// {0x31F0, 0x31FF},
|
||||
// Halfwidth Katakana
|
||||
// {0xFF60, 0xFF9F},
|
||||
// Hangul Syllables
|
||||
// {0xAC00, 0xD7AF},
|
||||
// Hangul Jamo
|
||||
// {0x1100, 0x11FF},
|
||||
// Hangul Compatibility Jamo
|
||||
// {0x3130, 0x318F},
|
||||
// Hangul Jamo Extended-A
|
||||
// {0xA960, 0xA97F},
|
||||
// Hangul Jamo Extended-B
|
||||
// {0xD7B0, 0xD7FF},
|
||||
// CJK Radicals Supplement
|
||||
// {0x2E80, 0x2EFF},
|
||||
// Kangxi Radicals
|
||||
// {0x2F00, 0x2FDF},
|
||||
// CJK Symbols and Punctuation
|
||||
// {0x3000, 0x303F},
|
||||
// CJK Compatibility Forms
|
||||
// {0xFE30, 0xFE4F},
|
||||
// CJK Compatibility Ideographs
|
||||
// {0xF900, 0xFAFF},
|
||||
/* Specials */
|
||||
// Replacement Character
|
||||
{0xFFFD, 0xFFFD},
|
||||
};
|
||||
|
||||
//
|
||||
// Comparison function for qsort
|
||||
//
|
||||
int compareIntervals(const void* a, const void* b) {
|
||||
const uint32_t* ia = (uint32_t*)a;
|
||||
const uint32_t* ib = (uint32_t*)b;
|
||||
if (ia[0] < ib[0]) return -1;
|
||||
if (ia[0] > ib[0]) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
//
|
||||
// Sort and merge adjacent intervals
|
||||
// Returns the number of intervals after merging
|
||||
//
|
||||
int sortAndMergeIntervals(uint32_t intervals[][2], const int count) {
|
||||
int merged_count = 0;
|
||||
|
||||
// Sort intervals by start value
|
||||
qsort(intervals, count, sizeof(uint32_t) * 2, compareIntervals);
|
||||
|
||||
// Merge overlapping/adjacent intervals
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (merged_count > 0 && intervals[i][0] <= intervals[merged_count - 1][1] + 1) {
|
||||
// Merge with previous interval
|
||||
if (intervals[i][1] > intervals[merged_count - 1][1]) {
|
||||
intervals[merged_count - 1][1] = intervals[i][1];
|
||||
}
|
||||
} else {
|
||||
// Add as new interval
|
||||
if (merged_count != i) {
|
||||
intervals[merged_count][0] = intervals[i][0];
|
||||
intervals[merged_count][1] = intervals[i][1];
|
||||
}
|
||||
merged_count++;
|
||||
}
|
||||
}
|
||||
|
||||
return merged_count;
|
||||
}
|
||||
|
||||
//
|
||||
// Create the comments and const array boilerplate for the hex data bytes
|
||||
//
|
||||
void StartHexFile(FILE* f, int iLen, const char* fname, int size) {
|
||||
int i, j;
|
||||
char szTemp[256];
|
||||
fprintf(f, "#pragma once\n\n");
|
||||
fprintf(f, "//\n// Created with fontconvert, written by Larry Bank, updated for CrossPoint by Dave Allie\n");
|
||||
fprintf(f, "// Point size: %d (scaled %dx)\n", size, FONT_SCALE_FACTOR);
|
||||
fprintf(f, "// compressed font data size = %d bytes\n//\n", iLen);
|
||||
|
||||
strcpy(szTemp, fname);
|
||||
i = strlen(szTemp);
|
||||
if (szTemp[i - 2] == '.') szTemp[i - 2] = 0; // get the leaf name for the data
|
||||
j = i;
|
||||
// go backwards to get rid trim off just the leaf name
|
||||
while (j > 0 && szTemp[j] != '/') {
|
||||
j--;
|
||||
}
|
||||
if (szTemp[j] == '/') j++;
|
||||
fprintf(f, "static const uint8_t %s[] = {\n", &szTemp[j]);
|
||||
} /* StartHexFile() */
|
||||
|
||||
//
|
||||
// Add N bytes of hex data to the output
|
||||
// The data will be arranged in rows of 16 bytes each
|
||||
//
|
||||
void AddHexBytes(FILE* f, void* pData, int iLen, int bLast) {
|
||||
static int iCount = 0; // number of bytes processed so far
|
||||
int i;
|
||||
uint8_t* s = (uint8_t*)pData;
|
||||
for (i = 0; i < iLen; i++) { // process the given data
|
||||
fprintf(f, "0x%02x", *s++);
|
||||
iCount++;
|
||||
if (i < iLen - 1 || !bLast) fprintf(f, ",");
|
||||
if ((iCount & 15) == 0) fprintf(f, "\n"); // next row of 16
|
||||
}
|
||||
if (bLast) {
|
||||
fprintf(f, "};\n");
|
||||
}
|
||||
} /* AddHexBytes() */
|
||||
|
||||
int loadCodePoint(FT_Face face, uint32_t code_point, CrossPointFontGlyph* pGlyphs, uint8_t* pBitmap,
|
||||
uint32_t* glyph_index, uint32_t* iOffset) {
|
||||
uint8_t* s;
|
||||
int iPitch, err;
|
||||
FT_Glyph glyph;
|
||||
|
||||
// MONO renderer provides clean image with perfect crop
|
||||
// (no wasted pixels) via bitmap struct.
|
||||
if ((err = FT_Load_Char(face, code_point, FT_LOAD_TARGET_MONO))) {
|
||||
printf("Error %d loading char U+%04X\n", err, code_point);
|
||||
(*glyph_index)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((err = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_MONO))) {
|
||||
printf("Error %d rendering char U+%04X\n", err, code_point);
|
||||
(*glyph_index)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((err = FT_Get_Glyph(face->glyph, &glyph))) {
|
||||
printf("Error %d getting glyph U+%04X\n", err, code_point);
|
||||
(*glyph_index)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
FT_Bitmap* bitmap = &face->glyph->bitmap;
|
||||
FT_BitmapGlyphRec* g = (FT_BitmapGlyphRec*)glyph;
|
||||
|
||||
// TODO: Restore small font
|
||||
if (0 /* bSmallFont */) {
|
||||
#if SMALL_FONT_ENABLED == 1
|
||||
printf("Stubbed\n");
|
||||
return 1;
|
||||
#else
|
||||
printf("Small font has been disabled\n");
|
||||
return 1;
|
||||
#endif
|
||||
} else {
|
||||
pGlyphs[*glyph_index].bitmapOffset = *iOffset;
|
||||
pGlyphs[*glyph_index].width = bitmap->width;
|
||||
pGlyphs[*glyph_index].height = bitmap->rows;
|
||||
pGlyphs[*glyph_index].xAdvance = (face->glyph->advance.x >> 6);
|
||||
pGlyphs[*glyph_index].xOffset = g->left;
|
||||
pGlyphs[*glyph_index].yOffset = g->top;
|
||||
}
|
||||
s = bitmap->buffer;
|
||||
iPitch = bitmap->pitch;
|
||||
|
||||
g5_encode_init(&g5enc, bitmap->width, bitmap->rows, &pBitmap[*iOffset], OUTBUF_SIZE - *iOffset);
|
||||
for (int y = 0; y < bitmap->rows; y++) {
|
||||
g5_encode_encodeLine(&g5enc, &s[y * iPitch]);
|
||||
} // for y
|
||||
int iLen = g5_encode_getOutSize(&g5enc);
|
||||
*iOffset += iLen;
|
||||
|
||||
FT_Done_Glyph(glyph);
|
||||
(*glyph_index)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
int i, err, size = 0;
|
||||
uint32_t iLen, iOffset = 0;
|
||||
FILE* fOut;
|
||||
// TrueType library structures
|
||||
FT_Library library;
|
||||
char* regularFaceFile;
|
||||
char* boldFaceFile = NULL;
|
||||
char* italicFaceFile = NULL;
|
||||
char* boldItalicFaceFile = NULL;
|
||||
const char* outputFile = NULL;
|
||||
FT_Face faceRegular;
|
||||
FT_Face faceBold;
|
||||
FT_Face faceItalic;
|
||||
FT_Face faceBoldItalic;
|
||||
|
||||
int bSmallFont = 0; // indicates if we're creating a normal or small font file
|
||||
CrossPointFontUnicodeInterval* pIntervals;
|
||||
CrossPointFontGlyph* pGlyphs;
|
||||
#if SMALL_FONT_ENABLED == 1
|
||||
CrossPointFontSmallGlyph* pSmallGlyphs;
|
||||
#endif
|
||||
uint8_t* pBitmap;
|
||||
CrossPointFontHeader epdFontHeader;
|
||||
|
||||
int bHFile; // flag indicating if the output will be a .H file of hex data
|
||||
|
||||
// Process intervals
|
||||
uint32_t intervals[MAX_INTERVALS][2];
|
||||
int intervalCount = sizeof(raw_intervals) / sizeof(raw_intervals[0]);
|
||||
uint32_t totalGlyphs = 0;
|
||||
|
||||
if (argc < 6 || argc % 2 == 1) {
|
||||
printf(
|
||||
"Usage: %s <regular.ttf> [-b <bold.ttf>] [-i <italic.ttf>] [-bi <bold-italic.ttf>] -p point_size -o <out.cpf "
|
||||
"or out.h>\n",
|
||||
argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
regularFaceFile = argv[1];
|
||||
for (int i = 2; i < argc; i += 2) {
|
||||
if (strcmp(argv[i], "-b") == 0) {
|
||||
// Bold font
|
||||
boldFaceFile = argv[i + 1];
|
||||
} else if (strcmp(argv[i], "-i") == 0) {
|
||||
// Italic font
|
||||
italicFaceFile = argv[i + 1];
|
||||
} else if (strcmp(argv[i], "-bi") == 0) {
|
||||
// Bold-Italic font
|
||||
boldItalicFaceFile = argv[i + 1];
|
||||
} else if (strcmp(argv[i], "-p") == 0) {
|
||||
// Point size
|
||||
size = atoi(argv[i + 1]);
|
||||
} else if (strcmp(argv[i], "-o") == 0) {
|
||||
// Output file
|
||||
outputFile = argv[i + 1];
|
||||
// output an H file?
|
||||
bHFile = outputFile[strlen(outputFile) - 1] == 'H' || outputFile[strlen(outputFile) - 1] == 'h';
|
||||
} else {
|
||||
printf("Unknown argument: %s\n", argv[i]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!outputFile) {
|
||||
printf("No output file specified\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (size <= 0) {
|
||||
printf("Invalid point size: %d\n", size);
|
||||
return 1;
|
||||
}
|
||||
|
||||
size = size * FONT_SCALE_FACTOR;
|
||||
bSmallFont = (size < 60) && SMALL_FONT_ENABLED == 1; // Glyph info can fit in signed 8-bit values
|
||||
int fontVariants = 1; // Always at least one variant we treat as regular
|
||||
if (boldFaceFile) fontVariants += 1;
|
||||
if (italicFaceFile) fontVariants += 1;
|
||||
if (boldItalicFaceFile) fontVariants += 1;
|
||||
|
||||
// Copy and sort/merge intervals
|
||||
if (intervalCount > MAX_INTERVALS) {
|
||||
printf("Error: too many intervals (max %d)\n", MAX_INTERVALS);
|
||||
return 1;
|
||||
}
|
||||
for (i = 0; i < intervalCount; i++) {
|
||||
intervals[i][0] = raw_intervals[i][0];
|
||||
intervals[i][1] = raw_intervals[i][1];
|
||||
}
|
||||
intervalCount = sortAndMergeIntervals(intervals, intervalCount);
|
||||
|
||||
// Calculate total number of glyphs
|
||||
for (i = 0; i < intervalCount; i++) {
|
||||
totalGlyphs += intervals[i][1] - intervals[i][0] + 1;
|
||||
}
|
||||
totalGlyphs *= fontVariants;
|
||||
|
||||
printf("Processed intervals: %d, total glyphs: %u\n", intervalCount, totalGlyphs);
|
||||
|
||||
// Allocate memory for intervals
|
||||
pIntervals = (CrossPointFontUnicodeInterval*)malloc(intervalCount * sizeof(CrossPointFontUnicodeInterval));
|
||||
if (!pIntervals) {
|
||||
printf("Error allocating memory for interval data\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Allocate memory for glyphs
|
||||
if (bSmallFont) {
|
||||
#if SMALL_FONT_ENABLED == 1
|
||||
pSmallGlyphs = (CrossPointFontSmallGlyph*)malloc(totalGlyphs * sizeof(CrossPointFontSmallGlyph));
|
||||
if (!pSmallGlyphs) {
|
||||
printf("Error allocating memory for glyph data\n");
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
printf("Small font has been disabled\n");
|
||||
return 1;
|
||||
#endif
|
||||
} else {
|
||||
pGlyphs = (CrossPointFontGlyph*)malloc(totalGlyphs * sizeof(CrossPointFontGlyph));
|
||||
if (!pGlyphs) {
|
||||
printf("Error allocating memory for glyph data\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
pBitmap = (uint8_t*)malloc(OUTBUF_SIZE); // Enough to hold the output
|
||||
if (!pBitmap) {
|
||||
printf("Error allocating memory for bitmap data\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Init FreeType lib, load font
|
||||
if ((err = FT_Init_FreeType(&library))) {
|
||||
printf("FreeType init error: %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
// Use TrueType engine version 35, without subpixel rendering.
|
||||
// This improves clarity of fonts since this library does not
|
||||
// support rendering multiple levels of gray in a glyph.
|
||||
// See https://github.com/adafruit/Adafruit-GFX-Library/issues/103
|
||||
FT_UInt interpreter_version = TT_INTERPRETER_VERSION_35;
|
||||
FT_Property_Set(library, "truetype", "interpreter-version", &interpreter_version);
|
||||
|
||||
if ((err = FT_New_Face(library, regularFaceFile, 0, &faceRegular))) {
|
||||
printf("Font load error: %d\n", err);
|
||||
FT_Done_FreeType(library);
|
||||
return err;
|
||||
}
|
||||
|
||||
if (italicFaceFile && (err = FT_New_Face(library, italicFaceFile, 0, &faceItalic))) {
|
||||
printf("Font load error: %d\n", err);
|
||||
FT_Done_FreeType(library);
|
||||
return err;
|
||||
}
|
||||
|
||||
if (boldFaceFile && (err = FT_New_Face(library, boldFaceFile, 0, &faceBold))) {
|
||||
printf("Font load error: %d\n", err);
|
||||
FT_Done_FreeType(library);
|
||||
return err;
|
||||
}
|
||||
|
||||
if (boldItalicFaceFile && (err = FT_New_Face(library, boldItalicFaceFile, 0, &faceBoldItalic))) {
|
||||
printf("Font load error: %d\n", err);
|
||||
FT_Done_FreeType(library);
|
||||
return err;
|
||||
}
|
||||
|
||||
// Shift the size left by 6 because the library uses '26dot6' fixed-point format
|
||||
FT_Set_Char_Size(faceRegular, size << 6, 0, DPI, 0);
|
||||
if (boldFaceFile) FT_Set_Char_Size(faceBold, size << 6, 0, DPI, 0);
|
||||
if (italicFaceFile) FT_Set_Char_Size(faceItalic, size << 6, 0, DPI, 0);
|
||||
if (boldItalicFaceFile) FT_Set_Char_Size(faceBoldItalic, size << 6, 0, DPI, 0);
|
||||
|
||||
// Build intervals with offsets and process glyphs
|
||||
uint32_t glyph_index = 0;
|
||||
for (int iInterval = 0; iInterval < intervalCount; iInterval++) {
|
||||
const uint32_t intervalStart = intervals[iInterval][0];
|
||||
const uint32_t intervalEnd = intervals[iInterval][1];
|
||||
|
||||
// Store interval with offset
|
||||
pIntervals[iInterval].first = intervalStart;
|
||||
pIntervals[iInterval].last = intervalEnd;
|
||||
pIntervals[iInterval].offset = glyph_index;
|
||||
|
||||
// Process each glyph in this interval
|
||||
// Load the codepoint for each style variant
|
||||
for (uint32_t codePoint = intervalStart; codePoint <= intervalEnd; codePoint++) {
|
||||
loadCodePoint(faceRegular, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
|
||||
if (boldFaceFile) loadCodePoint(faceBold, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
|
||||
if (italicFaceFile) loadCodePoint(faceItalic, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
|
||||
if (boldItalicFaceFile) loadCodePoint(faceBoldItalic, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
|
||||
} // for each code point in interval
|
||||
} // for each interval
|
||||
|
||||
// Try to create the output file
|
||||
fOut = fopen(outputFile, "w+b");
|
||||
if (!fOut) {
|
||||
printf("Error creating output file: %s\n", outputFile);
|
||||
return 1;
|
||||
}
|
||||
|
||||
epdFontHeader.height = faceRegular->size->metrics.height >> 6;
|
||||
epdFontHeader.ascender = faceRegular->size->metrics.ascender >> 6;
|
||||
epdFontHeader.styles = 0b0001;
|
||||
if (boldFaceFile) epdFontHeader.styles |= 0b0010;
|
||||
if (italicFaceFile) epdFontHeader.styles |= 0b0100;
|
||||
if (boldItalicFaceFile) epdFontHeader.styles |= 0b1000;
|
||||
epdFontHeader.intervalCount = intervalCount;
|
||||
epdFontHeader.glyphCount = totalGlyphs;
|
||||
|
||||
// Write the file header
|
||||
if (bSmallFont) {
|
||||
#if SMALL_FONT_ENABLED == 1
|
||||
epdFontHeader.u16Marker = CPF_FONT_MARKER_SMALL;
|
||||
if (faceRegular->size->metrics.height == 0) {
|
||||
// No face height info, assume fixed width and get from a glyph.
|
||||
epdFontHeader.height = pSmallGlyphs[0].height;
|
||||
}
|
||||
|
||||
iLen = sizeof(CrossPointFontHeader) + intervalCount * sizeof(CrossPointFontUnicodeInterval) +
|
||||
totalGlyphs * sizeof(CrossPointFontSmallGlyph) + iOffset;
|
||||
if (bHFile) { // create an H file of hex values
|
||||
StartHexFile(fOut, iLen, outputFile, size);
|
||||
AddHexBytes(fOut, &epdFontHeader, sizeof(CrossPointFontHeader), 0);
|
||||
// Write the intervals
|
||||
AddHexBytes(fOut, pIntervals, sizeof(CrossPointFontUnicodeInterval) * intervalCount, 0);
|
||||
// Write the glyph table
|
||||
AddHexBytes(fOut, pSmallGlyphs, sizeof(CrossPointFontSmallGlyph) * totalGlyphs, 0);
|
||||
// Write the compressed bitmap data
|
||||
AddHexBytes(fOut, pBitmap, iOffset, 1);
|
||||
} else {
|
||||
fwrite(&epdFontHeader, 1, sizeof(CrossPointFontHeader), fOut);
|
||||
// Write the intervals
|
||||
fwrite(pIntervals, 1, intervalCount * sizeof(CrossPointFontUnicodeInterval), fOut);
|
||||
// Write the glyph table
|
||||
fwrite(pSmallGlyphs, 1, totalGlyphs * sizeof(CrossPointFontSmallGlyph), fOut);
|
||||
// Write the compressed bitmap data
|
||||
fwrite(pBitmap, 1, iOffset, fOut);
|
||||
}
|
||||
#else
|
||||
printf("Small font has been disabled\n");
|
||||
return 1;
|
||||
#endif
|
||||
} else {
|
||||
epdFontHeader.u16Marker = CPF_FONT_MARKER;
|
||||
if (faceRegular->size->metrics.height == 0) {
|
||||
// No face height info, assume fixed width and get from a glyph.
|
||||
epdFontHeader.height = pGlyphs[0].height;
|
||||
}
|
||||
|
||||
iLen = sizeof(CrossPointFontHeader) + intervalCount * sizeof(CrossPointFontUnicodeInterval) +
|
||||
totalGlyphs * sizeof(CrossPointFontGlyph) + iOffset;
|
||||
if (bHFile) { // create an H file of hex values
|
||||
StartHexFile(fOut, iLen, outputFile, size);
|
||||
AddHexBytes(fOut, &epdFontHeader, sizeof(CrossPointFontHeader), 0);
|
||||
// Write the intervals
|
||||
AddHexBytes(fOut, pIntervals, sizeof(CrossPointFontUnicodeInterval) * intervalCount, 0);
|
||||
// Write the glyph table
|
||||
AddHexBytes(fOut, pGlyphs, sizeof(CrossPointFontGlyph) * totalGlyphs, 0);
|
||||
// Write the compressed bitmap data
|
||||
AddHexBytes(fOut, pBitmap, iOffset, 1);
|
||||
} else {
|
||||
fwrite(&epdFontHeader, 1, sizeof(CrossPointFontHeader), fOut);
|
||||
// Write the intervals
|
||||
fwrite(pIntervals, 1, intervalCount * sizeof(CrossPointFontUnicodeInterval), fOut);
|
||||
// Write the glyph table
|
||||
fwrite(pGlyphs, 1, totalGlyphs * sizeof(CrossPointFontGlyph), fOut);
|
||||
// Write the compressed bitmap data
|
||||
fwrite(pBitmap, 1, iOffset, fOut);
|
||||
}
|
||||
} // large fonts
|
||||
fflush(fOut);
|
||||
fclose(fOut); // done!
|
||||
FT_Done_FreeType(library);
|
||||
printf("Success!\nFont file size: %d bytes (%d glyphs)\n", iLen, totalGlyphs);
|
||||
|
||||
return 0;
|
||||
} /* main() */
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,173 @@
|
||||
#include "EpdFont.h"
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX,
|
||||
int* maxY) const {
|
||||
*minX = startX;
|
||||
*minY = startY;
|
||||
*maxX = startX;
|
||||
*maxY = startY;
|
||||
|
||||
if (*string == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t cursorXFP = fp4::fromPixel(startX); // 12.4 fixed-point accumulator
|
||||
int lastBaseX = startX;
|
||||
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
|
||||
int lastBaseTop = 0;
|
||||
constexpr int MIN_COMBINING_GAP_PX = 1;
|
||||
uint32_t cp;
|
||||
uint32_t prevCp = 0;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||
const bool isCombining = utf8IsCombiningMark(cp);
|
||||
|
||||
if (!isCombining) {
|
||||
cp = applyLigatures(cp, string);
|
||||
}
|
||||
|
||||
const EpdGlyph* glyph = getGlyph(cp);
|
||||
if (!glyph) {
|
||||
prevCp = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
int raiseBy = 0;
|
||||
if (isCombining) {
|
||||
const int currentGap = glyph->top - glyph->height - lastBaseTop;
|
||||
if (currentGap < MIN_COMBINING_GAP_PX) {
|
||||
raiseBy = MIN_COMBINING_GAP_PX - currentGap;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCombining && prevCp != 0) {
|
||||
cursorXFP += getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||
}
|
||||
|
||||
const int cursorXPixels = fp4::toPixel(cursorXFP); // snap 12.4 fixed-point to nearest pixel
|
||||
const int glyphBaseX = isCombining ? (lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2)) : cursorXPixels;
|
||||
const int glyphBaseY = startY - 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 = cursorXPixels;
|
||||
lastBaseAdvanceFP = glyph->advanceX; // 12.4 fixed-point
|
||||
lastBaseTop = glyph->top;
|
||||
cursorXFP += glyph->advanceX; // 12.4 fixed-point advance
|
||||
prevCp = cp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EpdFont::getTextDimensions(const char* string, int* w, int* h) const {
|
||||
int minX = 0, minY = 0, maxX = 0, maxY = 0;
|
||||
|
||||
getTextBounds(string, 0, 0, &minX, &minY, &maxX, &maxY);
|
||||
|
||||
*w = maxX - minX;
|
||||
*h = maxY - minY;
|
||||
}
|
||||
|
||||
static uint8_t lookupKernClass(const EpdKernClassEntry* entries, const uint16_t count, const uint32_t cp) {
|
||||
if (!entries || count == 0 || cp > 0xFFFF) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto target = static_cast<uint16_t>(cp);
|
||||
const auto* end = entries + count;
|
||||
|
||||
// lower_bound: exact-key lookup. Finds the first entry with codepoint >= target,
|
||||
// then the equality check confirms an exact match exists.
|
||||
const auto it = std::lower_bound(
|
||||
entries, end, target, [](const EpdKernClassEntry& entry, uint16_t value) { return entry.codepoint < value; });
|
||||
|
||||
if (it != end && it->codepoint == target) {
|
||||
return it->classId;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t EpdFont::getKerning(const uint32_t leftCp, const uint32_t rightCp) const {
|
||||
if (!data->kernMatrix) {
|
||||
return 0;
|
||||
}
|
||||
const uint8_t lc = lookupKernClass(data->kernLeftClasses, data->kernLeftEntryCount, leftCp);
|
||||
if (lc == 0) return 0;
|
||||
const uint8_t rc = lookupKernClass(data->kernRightClasses, data->kernRightEntryCount, rightCp);
|
||||
if (rc == 0) return 0;
|
||||
return data->kernMatrix[(lc - 1) * data->kernRightClassCount + (rc - 1)];
|
||||
}
|
||||
|
||||
uint32_t EpdFont::getLigature(const uint32_t leftCp, const uint32_t rightCp) const {
|
||||
const auto* pairs = data->ligaturePairs;
|
||||
const auto count = data->ligaturePairCount;
|
||||
if (!pairs || count == 0 || leftCp > 0xFFFF || rightCp > 0xFFFF) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t key = (leftCp << 16) | rightCp;
|
||||
const auto* end = pairs + count;
|
||||
|
||||
// lower_bound: exact-key lookup. Finds the first entry with pair >= key,
|
||||
// then the equality check confirms an exact match exists.
|
||||
const auto it =
|
||||
std::lower_bound(pairs, end, key, [](const EpdLigaturePair& pair, uint32_t value) { return pair.pair < value; });
|
||||
|
||||
if (it != end && it->pair == key) {
|
||||
return it->ligatureCp;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t EpdFont::applyLigatures(uint32_t cp, const char*& text) const {
|
||||
if (!data->ligaturePairs || data->ligaturePairCount == 0) {
|
||||
return cp;
|
||||
}
|
||||
while (true) {
|
||||
const auto saved = reinterpret_cast<const uint8_t*>(text);
|
||||
const uint32_t nextCp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text));
|
||||
if (nextCp == 0) break;
|
||||
const uint32_t lig = getLigature(cp, nextCp);
|
||||
if (lig == 0) {
|
||||
text = reinterpret_cast<const char*>(saved);
|
||||
break;
|
||||
}
|
||||
cp = lig;
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
||||
const int count = data->intervalCount;
|
||||
if (count == 0) return nullptr;
|
||||
|
||||
const EpdUnicodeInterval* intervals = data->intervals;
|
||||
const auto* end = intervals + count;
|
||||
|
||||
// upper_bound: range lookup. Finds the first interval with first > cp, so the
|
||||
// interval just before it is the last one with first <= cp. That's the only
|
||||
// candidate that could contain cp. Then we verify cp <= candidate.last.
|
||||
const auto it = std::upper_bound(
|
||||
intervals, end, cp, [](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; });
|
||||
|
||||
if (it != intervals) {
|
||||
const auto& interval = *(it - 1);
|
||||
if (cp <= interval.last) {
|
||||
return &data->glyph[interval.offset + (cp - interval.first)];
|
||||
}
|
||||
}
|
||||
|
||||
if (cp != REPLACEMENT_GLYPH) {
|
||||
return getGlyph(REPLACEMENT_GLYPH);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
class EpdFont {
|
||||
void getTextBounds(const char* string, int startX, int startY, int* minX, int* minY, int* maxX, int* maxY) const;
|
||||
|
||||
public:
|
||||
const EpdFontData* data;
|
||||
explicit EpdFont(const EpdFontData* data) : data(data) {}
|
||||
~EpdFont() = default;
|
||||
void getTextDimensions(const char* string, int* w, int* h) const;
|
||||
|
||||
const EpdGlyph* getGlyph(uint32_t cp) const;
|
||||
|
||||
/// Returns the kerning adjustment (4.4 fixed-point in pixels) between two codepoints.
|
||||
/// Returns 0 if no kerning data exists for the pair.
|
||||
int8_t getKerning(uint32_t leftCp, uint32_t rightCp) const;
|
||||
|
||||
/// Returns the ligature codepoint for a pair, or 0 if no ligature exists.
|
||||
uint32_t getLigature(uint32_t leftCp, uint32_t rightCp) const;
|
||||
|
||||
/// Greedily applies ligature substitutions starting from cp, consuming
|
||||
/// as many following codepoints from text as possible. Returns the
|
||||
/// (possibly substituted) codepoint; advances text past consumed chars.
|
||||
uint32_t applyLigatures(uint32_t cp, const char*& text) const;
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
// From
|
||||
// https://github.com/vroland/epdiy/blob/c61e9e923ce2418150d54f88cea5d196cdc40c54/src/epd_internals.h
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
/// Font metrics use "fixed-point 4" (4 fractional bits, i.e. 1/16-pixel
|
||||
/// resolution). Both the 12.4 glyph advances (uint16_t) and the 4.4 kern
|
||||
/// values (int8_t) share the same 4 fractional bits, so they can be freely
|
||||
/// added into a single int32_t accumulator during text layout. The
|
||||
/// accumulator is snapped to the nearest whole pixel only at render time,
|
||||
/// which avoids the per-character rounding errors that plagued integer-only
|
||||
/// layout.
|
||||
///
|
||||
/// The helpers below eliminate the raw bit-shifts that would otherwise be
|
||||
/// scattered across every layout / measurement call site.
|
||||
namespace fp4 {
|
||||
constexpr int FRAC_BITS = 4;
|
||||
constexpr int32_t HALF = 1 << (FRAC_BITS - 1); // 8, added before shift for round-to-nearest
|
||||
|
||||
/// Convert an integer pixel value to 12.4 fixed-point.
|
||||
constexpr int32_t fromPixel(int px) { return static_cast<int32_t>(px) << FRAC_BITS; }
|
||||
|
||||
/// Snap a fixed-point value to the nearest integer pixel.
|
||||
constexpr int toPixel(int32_t fp) { return static_cast<int>((fp + HALF) >> FRAC_BITS); }
|
||||
|
||||
/// Convert a fixed-point value to float (mainly useful for debug logging).
|
||||
constexpr float toFloat(int32_t fp) { return fp / static_cast<float>(1 << FRAC_BITS); }
|
||||
} // namespace fp4
|
||||
|
||||
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
|
||||
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
|
||||
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
|
||||
/// Both share 4 fractional bits so they combine directly in an accumulator.
|
||||
|
||||
/// Font data stored PER GLYPH
|
||||
typedef struct {
|
||||
uint8_t width; ///< Bitmap dimensions in pixels
|
||||
uint8_t height; ///< Bitmap dimensions in pixels
|
||||
uint16_t advanceX; ///< Distance to advance cursor (x axis), 12.4 fixed-point in pixels
|
||||
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 (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
|
||||
uint32_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
|
||||
uint32_t last; ///< The last unicode code point of the interval
|
||||
uint32_t offset; ///< Index of the first code point into the glyph array
|
||||
} EpdUnicodeInterval;
|
||||
|
||||
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
|
||||
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
|
||||
typedef struct {
|
||||
uint16_t codepoint; ///< Unicode codepoint
|
||||
uint8_t classId; ///< 1-based kerning class ID
|
||||
} __attribute__((packed)) EpdKernClassEntry;
|
||||
|
||||
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
|
||||
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
|
||||
typedef struct {
|
||||
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
|
||||
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
|
||||
} __attribute__((packed)) EpdLigaturePair;
|
||||
|
||||
/// Data stored for FONT AS A WHOLE
|
||||
typedef struct {
|
||||
const uint8_t* bitmap; ///< Glyph bitmaps, concatenated
|
||||
const EpdGlyph* glyph; ///< Glyph array
|
||||
const EpdUnicodeInterval* intervals; ///< Valid unicode intervals for this font
|
||||
uint32_t intervalCount; ///< Number of unicode intervals.
|
||||
uint8_t advanceY; ///< Newline distance (y axis)
|
||||
int ascender; ///< Maximal height of a glyph above the base line
|
||||
int descender; ///< Maximal height of a glyph below the base line
|
||||
bool is2Bit;
|
||||
const EpdFontGroup* groups; ///< NULL for uncompressed fonts
|
||||
uint16_t groupCount; ///< 0 for uncompressed fonts
|
||||
const uint16_t* glyphToGroup; ///< Per-glyph group ID (nullptr for contiguous-group fonts)
|
||||
const EpdKernClassEntry* kernLeftClasses; ///< Sorted left-side class map (nullptr if none)
|
||||
const EpdKernClassEntry* kernRightClasses; ///< Sorted right-side class map (nullptr if none)
|
||||
const int8_t* kernMatrix; ///< Flat leftClassCount x rightClassCount matrix, 4.4 fixed-point in pixels
|
||||
uint16_t kernLeftEntryCount; ///< Entries in kernLeftClasses
|
||||
uint16_t kernRightEntryCount; ///< Entries in kernRightClasses
|
||||
uint8_t kernLeftClassCount; ///< Number of distinct left classes (matrix rows)
|
||||
uint8_t kernRightClassCount; ///< Number of distinct right classes (matrix cols)
|
||||
const EpdLigaturePair* ligaturePairs; ///< Sorted ligature pair table (nullptr if none)
|
||||
uint32_t ligaturePairCount; ///< Number of entries in ligaturePairs
|
||||
} EpdFontData;
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "EpdFontFamily.h"
|
||||
|
||||
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
||||
// 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;
|
||||
} else if (hasItalic && italic) {
|
||||
return italic;
|
||||
}
|
||||
|
||||
return regular;
|
||||
}
|
||||
|
||||
void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const Style style) const {
|
||||
getFont(style)->getTextDimensions(string, w, h);
|
||||
}
|
||||
|
||||
const EpdFontData* EpdFontFamily::getData(const Style style) const { return getFont(style)->data; }
|
||||
|
||||
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) const {
|
||||
return getFont(style)->getGlyph(cp);
|
||||
}
|
||||
|
||||
int8_t EpdFontFamily::getKerning(const uint32_t leftCp, const uint32_t rightCp, const Style style) const {
|
||||
return getFont(style)->getKerning(leftCp, rightCp);
|
||||
}
|
||||
|
||||
uint32_t EpdFontFamily::applyLigatures(const uint32_t cp, const char*& text, const Style style) const {
|
||||
return getFont(style)->applyLigatures(cp, text);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include "EpdFont.h"
|
||||
|
||||
class EpdFontFamily {
|
||||
public:
|
||||
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;
|
||||
const EpdFontData* getData(Style style = REGULAR) const;
|
||||
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
|
||||
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
|
||||
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
|
||||
|
||||
private:
|
||||
const EpdFont* regular;
|
||||
const EpdFont* bold;
|
||||
const EpdFont* italic;
|
||||
const EpdFont* boldItalic;
|
||||
|
||||
const EpdFont* getFont(Style style) const;
|
||||
};
|
||||
@@ -0,0 +1,481 @@
|
||||
#include "FontDecompressor.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
FontDecompressor::~FontDecompressor() { deinit(); }
|
||||
|
||||
bool FontDecompressor::init() {
|
||||
clearCache();
|
||||
return true;
|
||||
}
|
||||
|
||||
void FontDecompressor::deinit() {
|
||||
freePageBuffer();
|
||||
freeHotGroup();
|
||||
}
|
||||
|
||||
void FontDecompressor::clearCache() {
|
||||
freePageBuffer();
|
||||
freeHotGroup();
|
||||
}
|
||||
|
||||
void FontDecompressor::freePageBuffer() {
|
||||
for (uint8_t s = 0; s < pageSlotCount; s++) {
|
||||
free(pageSlots[s].buffer);
|
||||
free(pageSlots[s].glyphs);
|
||||
pageSlots[s] = {};
|
||||
}
|
||||
pageSlotCount = 0;
|
||||
}
|
||||
|
||||
void FontDecompressor::freeHotGroup() {
|
||||
hotGroup.clear();
|
||||
hotGroup.shrink_to_fit();
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
hotGlyphBuf.clear();
|
||||
hotGlyphBuf.shrink_to_fit();
|
||||
}
|
||||
|
||||
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
|
||||
// O(1) path for frequency-grouped fonts with glyphToGroup mapping
|
||||
if (fontData->glyphToGroup != nullptr) {
|
||||
return fontData->glyphToGroup[glyphIndex];
|
||||
}
|
||||
|
||||
// Contiguous-group fonts: linear scan
|
||||
for (uint16_t i = 0; i < fontData->groupCount; i++) {
|
||||
uint32_t first = fontData->groups[i].firstGlyphIndex;
|
||||
if (glyphIndex >= first && glyphIndex < first + fontData->groups[i].glyphCount) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return fontData->groupCount; // sentinel = not found
|
||||
}
|
||||
|
||||
bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf,
|
||||
uint32_t outSize) {
|
||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||
|
||||
const uint32_t tDecomp = millis();
|
||||
inflateReader.init(false);
|
||||
inflateReader.setSource(&fontData->bitmap[group.compressedOffset], group.compressedSize);
|
||||
if (!inflateReader.read(outBuf, outSize)) {
|
||||
stats.decompressTimeMs += millis() - tDecomp;
|
||||
LOG_ERR("FDC", "Decompression failed for group %u", groupIndex);
|
||||
return false;
|
||||
}
|
||||
stats.decompressTimeMs += millis() - tDecomp;
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Byte-aligned helpers ---
|
||||
|
||||
uint32_t FontDecompressor::getAlignedOffset(const EpdFontData* fontData, uint16_t groupIndex, uint32_t glyphIndex) {
|
||||
uint32_t offset = 0;
|
||||
|
||||
auto accumGlyph = [&](const EpdGlyph& g) {
|
||||
if (g.width > 0 && g.height > 0) {
|
||||
offset += ((g.width + 3) / 4) * g.height;
|
||||
}
|
||||
};
|
||||
|
||||
if (fontData->glyphToGroup) {
|
||||
// Frequency-grouped: scan glyphs before glyphIndex that belong to this group
|
||||
for (uint32_t i = 0; i < glyphIndex; i++) {
|
||||
if (fontData->glyphToGroup[i] == groupIndex) {
|
||||
accumGlyph(fontData->glyph[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Contiguous-group: sum aligned sizes of preceding glyphs in the group
|
||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||
for (uint32_t i = group.firstGlyphIndex; i < glyphIndex; i++) {
|
||||
accumGlyph(fontData->glyph[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
void FontDecompressor::compactSingleGlyph(const uint8_t* alignedSrc, uint8_t* packedDst, uint8_t width,
|
||||
uint8_t height) {
|
||||
if (width == 0 || height == 0) return;
|
||||
const uint32_t rowStride = (width + 3) / 4;
|
||||
if (width % 4 == 0) {
|
||||
memcpy(packedDst, alignedSrc, rowStride * height);
|
||||
return;
|
||||
}
|
||||
uint8_t outByte = 0, outBits = 0;
|
||||
uint32_t writeIdx = 0;
|
||||
for (uint8_t y = 0; y < height; y++) {
|
||||
for (uint8_t x = 0; x < width; x++) {
|
||||
outByte = (outByte << 2) | ((alignedSrc[y * rowStride + x / 4] >> ((3 - (x % 4)) * 2)) & 0x3);
|
||||
outBits += 2;
|
||||
if (outBits == 8) {
|
||||
packedDst[writeIdx++] = outByte;
|
||||
outByte = 0;
|
||||
outBits = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (outBits > 0) packedDst[writeIdx] = outByte << (8 - outBits);
|
||||
}
|
||||
|
||||
// --- getBitmap: page buffer → hot group → decompress ---
|
||||
|
||||
const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex) {
|
||||
const uint32_t tStart = micros();
|
||||
stats.getBitmapCalls++;
|
||||
|
||||
if (!fontData->groups || fontData->groupCount == 0) {
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return &fontData->bitmap[glyph->dataOffset];
|
||||
}
|
||||
|
||||
// Check page buffer slots (populated by prewarmCache — one slot per font style)
|
||||
for (uint8_t s = 0; s < pageSlotCount; s++) {
|
||||
const auto& slot = pageSlots[s];
|
||||
if (slot.fontData != fontData || slot.glyphCount == 0) continue;
|
||||
|
||||
int left = 0, right = slot.glyphCount - 1;
|
||||
while (left <= right) {
|
||||
int mid = left + (right - left) / 2;
|
||||
if (slot.glyphs[mid].glyphIndex == glyphIndex) {
|
||||
if (slot.glyphs[mid].bufferOffset != UINT32_MAX) {
|
||||
stats.cacheHits++;
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return &slot.buffer[slot.glyphs[mid].bufferOffset];
|
||||
}
|
||||
break; // Not extracted during prewarm; fall through to hot-group path
|
||||
}
|
||||
if (slot.glyphs[mid].glyphIndex < glyphIndex)
|
||||
left = mid + 1;
|
||||
else
|
||||
right = mid - 1;
|
||||
}
|
||||
break; // Found the right slot but glyph wasn't in it; don't check other slots
|
||||
}
|
||||
|
||||
// Fallback: hot group slot
|
||||
uint16_t groupIndex = getGroupIndex(fontData, glyphIndex);
|
||||
if (groupIndex >= fontData->groupCount) {
|
||||
LOG_ERR("FDC", "Glyph %u not found in any group", glyphIndex);
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if hot group already has this group decompressed — if not, decompress it
|
||||
if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
|
||||
stats.cacheMisses++;
|
||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||
|
||||
hotGroup.resize(group.uncompressedSize);
|
||||
if (hotGroup.empty()) {
|
||||
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) {
|
||||
hotGroup.clear();
|
||||
hotGroup.shrink_to_fit();
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
hotGroupFont = fontData;
|
||||
hotGroupIndex = groupIndex;
|
||||
stats.hotGroupBytes = group.uncompressedSize;
|
||||
} else {
|
||||
stats.cacheHits++;
|
||||
}
|
||||
|
||||
// Compact just the requested glyph from byte-aligned data into scratch buffer
|
||||
if (glyph->dataLength > hotGlyphBuf.size()) {
|
||||
hotGlyphBuf.resize(glyph->dataLength);
|
||||
}
|
||||
if (hotGlyphBuf.empty()) {
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
|
||||
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height);
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return hotGlyphBuf.data();
|
||||
}
|
||||
|
||||
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
|
||||
|
||||
int32_t FontDecompressor::findGlyphIndex(const EpdFontData* fontData, uint32_t codepoint) {
|
||||
const EpdUnicodeInterval* intervals = fontData->intervals;
|
||||
const int count = fontData->intervalCount;
|
||||
|
||||
if (count == 0) return -1;
|
||||
|
||||
// Binary search
|
||||
int left = 0;
|
||||
int right = count - 1;
|
||||
|
||||
while (left <= right) {
|
||||
const int mid = left + (right - left) / 2;
|
||||
const EpdUnicodeInterval* interval = &intervals[mid];
|
||||
|
||||
if (codepoint < interval->first) {
|
||||
right = mid - 1;
|
||||
} else if (codepoint > interval->last) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
return static_cast<int32_t>(interval->offset + (codepoint - interval->first));
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8Text) {
|
||||
if (!fontData || !fontData->groups || !utf8Text) return 0;
|
||||
|
||||
// Allocate the next available slot (caller must call freePageBuffer/clearCache to reset)
|
||||
if (pageSlotCount >= MAX_PAGE_SLOTS) {
|
||||
LOG_ERR("FDC", "All %u page buffer slots full, cannot prewarm fontData=%p", MAX_PAGE_SLOTS, (void*)fontData);
|
||||
return -1;
|
||||
}
|
||||
PageSlot& slot = pageSlots[pageSlotCount];
|
||||
|
||||
// Step 1: Collect unique glyph indices needed for this page
|
||||
uint32_t neededGlyphs[MAX_PAGE_GLYPHS];
|
||||
uint16_t glyphCount = 0;
|
||||
bool glyphCapWarned = false;
|
||||
|
||||
const unsigned char* p = reinterpret_cast<const unsigned char*>(utf8Text);
|
||||
while (*p) {
|
||||
uint32_t cp = utf8NextCodepoint(&p);
|
||||
if (cp == 0) break;
|
||||
|
||||
int32_t glyphIdx = findGlyphIndex(fontData, cp);
|
||||
if (glyphIdx < 0) continue;
|
||||
|
||||
// Deduplicate
|
||||
bool found = false;
|
||||
for (uint16_t i = 0; i < glyphCount; i++) {
|
||||
if (neededGlyphs[i] == static_cast<uint32_t>(glyphIdx)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
if (glyphCount < MAX_PAGE_GLYPHS) {
|
||||
neededGlyphs[glyphCount++] = static_cast<uint32_t>(glyphIdx);
|
||||
} else if (!glyphCapWarned) {
|
||||
LOG_DBG("FDC", "Glyph cap (%u) reached during prewarm; excess glyphs will use hot-group fallback",
|
||||
MAX_PAGE_GLYPHS);
|
||||
glyphCapWarned = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (glyphCount == 0) return 0;
|
||||
|
||||
// Step 2: Compute total buffer size and collect unique groups
|
||||
uint32_t totalBytes = 0;
|
||||
uint16_t neededGroups[128];
|
||||
uint8_t groupCount = 0;
|
||||
bool groupCapWarned = false;
|
||||
|
||||
for (uint16_t i = 0; i < glyphCount; i++) {
|
||||
totalBytes += fontData->glyph[neededGlyphs[i]].dataLength;
|
||||
uint16_t gi = getGroupIndex(fontData, neededGlyphs[i]);
|
||||
bool found = false;
|
||||
for (uint8_t j = 0; j < groupCount; j++) {
|
||||
if (neededGroups[j] == gi) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
if (groupCount < 128) {
|
||||
neededGroups[groupCount++] = gi;
|
||||
} else if (!groupCapWarned) {
|
||||
LOG_DBG("FDC", "Group cap (128) reached during prewarm; some groups will use hot-group fallback");
|
||||
groupCapWarned = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stats.uniqueGroupsAccessed = groupCount;
|
||||
|
||||
// Step 3: Allocate page buffer and lookup table for this slot
|
||||
slot.buffer = static_cast<uint8_t*>(malloc(totalBytes));
|
||||
slot.glyphs = static_cast<PageGlyphEntry*>(malloc(glyphCount * sizeof(PageGlyphEntry)));
|
||||
if (!slot.buffer || !slot.glyphs) {
|
||||
LOG_ERR("FDC", "Failed to allocate page buffer (%u bytes, %u glyphs)", totalBytes, glyphCount);
|
||||
free(slot.buffer);
|
||||
free(slot.glyphs);
|
||||
slot = {};
|
||||
return glyphCount;
|
||||
}
|
||||
stats.pageBufferBytes += totalBytes;
|
||||
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
|
||||
|
||||
slot.fontData = fontData;
|
||||
slot.glyphCount = glyphCount;
|
||||
pageSlotCount++;
|
||||
|
||||
// Initialize lookup entries (bufferOffset = UINT32_MAX means not yet extracted)
|
||||
for (uint16_t i = 0; i < glyphCount; i++) {
|
||||
slot.glyphs[i] = {neededGlyphs[i], UINT32_MAX, 0};
|
||||
}
|
||||
|
||||
// Sort by glyphIndex for binary search in getBitmap()
|
||||
for (uint16_t i = 1; i < glyphCount; i++) {
|
||||
PageGlyphEntry key = slot.glyphs[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && slot.glyphs[j].glyphIndex > key.glyphIndex) {
|
||||
slot.glyphs[j + 1] = slot.glyphs[j];
|
||||
j--;
|
||||
}
|
||||
slot.glyphs[j + 1] = key;
|
||||
}
|
||||
|
||||
// Step 3b: Pre-scan to compute each needed glyph's byte-aligned offset within its group.
|
||||
// This avoids recomputing aligned offsets per group during extraction in step 4.
|
||||
uint32_t groupAlignedTracker[128] = {}; // running byte-aligned offset for each needed group
|
||||
|
||||
if (fontData->glyphToGroup) {
|
||||
// Frequency-grouped: single O(totalGlyphs) pass through glyphToGroup
|
||||
const auto& lastInterval = fontData->intervals[fontData->intervalCount - 1];
|
||||
const uint32_t totalGlyphs = lastInterval.offset + (lastInterval.last - lastInterval.first + 1);
|
||||
|
||||
for (uint32_t i = 0; i < totalGlyphs; i++) {
|
||||
const uint16_t gi = fontData->glyphToGroup[i];
|
||||
// Find this glyph's group position in neededGroups
|
||||
uint8_t gpPos = groupCount;
|
||||
for (uint8_t j = 0; j < groupCount; j++) {
|
||||
if (neededGroups[j] == gi) {
|
||||
gpPos = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (gpPos == groupCount) continue; // not a needed group
|
||||
|
||||
const EpdGlyph& glyph = fontData->glyph[i];
|
||||
|
||||
// Binary search in sorted slot.glyphs to find if glyph i is needed
|
||||
int left = 0, right = (int)slot.glyphCount - 1;
|
||||
while (left <= right) {
|
||||
const int mid = left + (right - left) / 2;
|
||||
if (slot.glyphs[mid].glyphIndex == i) {
|
||||
slot.glyphs[mid].alignedOffset = groupAlignedTracker[gpPos];
|
||||
break;
|
||||
}
|
||||
if (slot.glyphs[mid].glyphIndex < i)
|
||||
left = mid + 1;
|
||||
else
|
||||
right = mid - 1;
|
||||
}
|
||||
|
||||
if (glyph.width > 0 && glyph.height > 0) {
|
||||
groupAlignedTracker[gpPos] += ((glyph.width + 3) / 4) * glyph.height;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Contiguous-group: iterate each needed group's glyphs directly
|
||||
for (uint8_t g = 0; g < groupCount; g++) {
|
||||
const EpdFontGroup& group = fontData->groups[neededGroups[g]];
|
||||
uint32_t alignedOff = 0;
|
||||
for (uint16_t j = 0; j < group.glyphCount; j++) {
|
||||
const uint32_t glyphI = group.firstGlyphIndex + j;
|
||||
const EpdGlyph& glyph = fontData->glyph[glyphI];
|
||||
|
||||
int left = 0, right = (int)slot.glyphCount - 1;
|
||||
while (left <= right) {
|
||||
const int mid = left + (right - left) / 2;
|
||||
if (slot.glyphs[mid].glyphIndex == glyphI) {
|
||||
slot.glyphs[mid].alignedOffset = alignedOff;
|
||||
break;
|
||||
}
|
||||
if (slot.glyphs[mid].glyphIndex < glyphI)
|
||||
left = mid + 1;
|
||||
else
|
||||
right = mid - 1;
|
||||
}
|
||||
|
||||
if (glyph.width > 0 && glyph.height > 0) {
|
||||
alignedOff += ((glyph.width + 3) / 4) * glyph.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: For each unique group, decompress to temp buffer and extract needed glyphs
|
||||
uint32_t writeOffset = 0;
|
||||
int missed = 0;
|
||||
|
||||
for (uint8_t g = 0; g < groupCount; g++) {
|
||||
uint16_t groupIdx = neededGroups[g];
|
||||
const EpdFontGroup& group = fontData->groups[groupIdx];
|
||||
|
||||
auto* tempBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
|
||||
if (!tempBuf) {
|
||||
LOG_ERR("FDC", "Failed to allocate temp buffer (%u bytes) for group %u", group.uncompressedSize, groupIdx);
|
||||
missed++;
|
||||
continue;
|
||||
}
|
||||
if (group.uncompressedSize > stats.peakTempBytes) {
|
||||
stats.peakTempBytes = group.uncompressedSize;
|
||||
}
|
||||
|
||||
if (!decompressGroup(fontData, groupIdx, tempBuf, group.uncompressedSize)) {
|
||||
free(tempBuf);
|
||||
missed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract needed glyphs directly from the byte-aligned temp buffer, compacting on the fly.
|
||||
// alignedOffset was pre-computed in step 3b — no full-group compact scan needed.
|
||||
for (uint16_t i = 0; i < slot.glyphCount; i++) {
|
||||
if (slot.glyphs[i].bufferOffset != UINT32_MAX) continue; // already extracted
|
||||
if (getGroupIndex(fontData, slot.glyphs[i].glyphIndex) != groupIdx) continue;
|
||||
|
||||
const EpdGlyph& glyph = fontData->glyph[slot.glyphs[i].glyphIndex];
|
||||
compactSingleGlyph(&tempBuf[slot.glyphs[i].alignedOffset], &slot.buffer[writeOffset], glyph.width, glyph.height);
|
||||
slot.glyphs[i].bufferOffset = writeOffset;
|
||||
writeOffset += glyph.dataLength;
|
||||
}
|
||||
|
||||
free(tempBuf);
|
||||
}
|
||||
|
||||
LOG_DBG("FDC", "Prewarm: %u glyphs in %u bytes from %u groups (%d missed)", glyphCount, writeOffset, groupCount,
|
||||
missed);
|
||||
|
||||
return missed;
|
||||
}
|
||||
|
||||
// --- Stats ---
|
||||
|
||||
void FontDecompressor::resetStats() { stats = Stats{}; }
|
||||
|
||||
void FontDecompressor::logStats(const char* label) {
|
||||
const uint32_t total = stats.cacheHits + stats.cacheMisses;
|
||||
LOG_DBG("FDC", "[%s] hits=%lu misses=%lu (%.1f%% hit rate)", label, stats.cacheHits, stats.cacheMisses,
|
||||
total > 0 ? 100.0f * stats.cacheHits / total : 0.0f);
|
||||
LOG_DBG("FDC", "[%s] decompress=%lums groups_accessed=%u", label, stats.decompressTimeMs, stats.uniqueGroupsAccessed);
|
||||
LOG_DBG("FDC", "[%s] mem: pageBuf=%lu pageGlyphs=%lu hotGroup=%lu peakTemp=%lu", label, stats.pageBufferBytes,
|
||||
stats.pageGlyphsBytes, stats.hotGroupBytes, stats.peakTempBytes);
|
||||
if (stats.getBitmapCalls > 0) {
|
||||
LOG_DBG("FDC", "[%s] getBitmap: %lu calls, %luus total, %luus/call avg", label, stats.getBitmapCalls,
|
||||
stats.getBitmapTimeUs, stats.getBitmapTimeUs / stats.getBitmapCalls);
|
||||
}
|
||||
resetStats();
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include <InflateReader.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "EpdFontData.h"
|
||||
|
||||
class FontDecompressor {
|
||||
public:
|
||||
static constexpr uint16_t MAX_PAGE_GLYPHS = 512;
|
||||
static constexpr uint8_t MAX_PAGE_SLOTS = 4; // One per font style (R/B/I/BI)
|
||||
|
||||
FontDecompressor() = default;
|
||||
~FontDecompressor();
|
||||
|
||||
bool init();
|
||||
void deinit();
|
||||
|
||||
// Returns pointer to decompressed bitmap data for the given glyph.
|
||||
// Checks the page buffer (from prewarm) first, then falls back to the hot group slot.
|
||||
const uint8_t* getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex);
|
||||
|
||||
// Free all cached data (page buffer + hot group).
|
||||
void clearCache();
|
||||
|
||||
// Pre-scan UTF-8 text and extract needed glyph bitmaps into a flat page buffer.
|
||||
// Each group is decompressed once into a temp buffer; only needed glyphs are kept.
|
||||
// Returns the number of glyphs that couldn't be loaded (0 on full success).
|
||||
int prewarmCache(const EpdFontData* fontData, const char* utf8Text);
|
||||
|
||||
struct Stats {
|
||||
uint32_t cacheHits = 0;
|
||||
uint32_t cacheMisses = 0;
|
||||
uint32_t decompressTimeMs = 0;
|
||||
uint16_t uniqueGroupsAccessed = 0;
|
||||
uint32_t pageBufferBytes = 0; // pageBuffer allocation
|
||||
uint32_t pageGlyphsBytes = 0; // pageGlyphs lookup table allocation
|
||||
uint32_t hotGroupBytes = 0; // current hot group allocation
|
||||
uint32_t peakTempBytes = 0; // largest temp buffer in prewarm
|
||||
uint32_t getBitmapTimeUs = 0; // cumulative getBitmap time (micros)
|
||||
uint32_t getBitmapCalls = 0; // number of getBitmap calls
|
||||
};
|
||||
void logStats(const char* label = "FDC");
|
||||
void resetStats();
|
||||
const Stats& getStats() const { return stats; }
|
||||
|
||||
private:
|
||||
Stats stats;
|
||||
InflateReader inflateReader;
|
||||
|
||||
// Page buffer slots: each style gets its own flat glyph buffer with sorted lookup.
|
||||
// Up to MAX_PAGE_SLOTS (4) styles can be prewarmed simultaneously.
|
||||
struct PageGlyphEntry {
|
||||
uint32_t glyphIndex;
|
||||
uint32_t bufferOffset;
|
||||
uint32_t alignedOffset; // byte-aligned offset within its decompressed group (set during prewarm pre-scan)
|
||||
};
|
||||
struct PageSlot {
|
||||
uint8_t* buffer = nullptr;
|
||||
const EpdFontData* fontData = nullptr;
|
||||
PageGlyphEntry* glyphs = nullptr;
|
||||
uint16_t glyphCount = 0;
|
||||
};
|
||||
PageSlot pageSlots[MAX_PAGE_SLOTS] = {};
|
||||
uint8_t pageSlotCount = 0;
|
||||
|
||||
// Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path.
|
||||
// Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf.
|
||||
const EpdFontData* hotGroupFont = nullptr;
|
||||
uint16_t hotGroupIndex = UINT16_MAX;
|
||||
std::vector<uint8_t> hotGroup;
|
||||
|
||||
// Scratch buffer for compacting a single glyph from the hot group.
|
||||
// Valid until the next getBitmap() call.
|
||||
std::vector<uint8_t> hotGlyphBuf;
|
||||
|
||||
void freePageBuffer();
|
||||
void freeHotGroup();
|
||||
uint16_t getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex);
|
||||
uint32_t getAlignedOffset(const EpdFontData* fontData, uint16_t groupIndex, uint32_t glyphIndex);
|
||||
bool decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf, uint32_t outSize);
|
||||
static void compactSingleGlyph(const uint8_t* alignedSrc, uint8_t* packedDst, uint8_t width, uint8_t height);
|
||||
static int32_t findGlyphIndex(const EpdFontData* fontData, uint32_t codepoint);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <builtinFonts/bookerly_12_bold.h>
|
||||
#include <builtinFonts/bookerly_12_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_12_italic.h>
|
||||
#include <builtinFonts/bookerly_12_regular.h>
|
||||
#include <builtinFonts/bookerly_14_bold.h>
|
||||
#include <builtinFonts/bookerly_14_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_14_italic.h>
|
||||
#include <builtinFonts/bookerly_14_regular.h>
|
||||
#include <builtinFonts/bookerly_16_bold.h>
|
||||
#include <builtinFonts/bookerly_16_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_16_italic.h>
|
||||
#include <builtinFonts/bookerly_16_regular.h>
|
||||
#include <builtinFonts/bookerly_18_bold.h>
|
||||
#include <builtinFonts/bookerly_18_bolditalic.h>
|
||||
#include <builtinFonts/bookerly_18_italic.h>
|
||||
#include <builtinFonts/bookerly_18_regular.h>
|
||||
#include <builtinFonts/notosans_8_regular.h>
|
||||
#include <builtinFonts/notosans_12_bold.h>
|
||||
#include <builtinFonts/notosans_12_bolditalic.h>
|
||||
#include <builtinFonts/notosans_12_italic.h>
|
||||
#include <builtinFonts/notosans_12_regular.h>
|
||||
#include <builtinFonts/notosans_14_bold.h>
|
||||
#include <builtinFonts/notosans_14_bolditalic.h>
|
||||
#include <builtinFonts/notosans_14_italic.h>
|
||||
#include <builtinFonts/notosans_14_regular.h>
|
||||
#include <builtinFonts/notosans_16_bold.h>
|
||||
#include <builtinFonts/notosans_16_bolditalic.h>
|
||||
#include <builtinFonts/notosans_16_italic.h>
|
||||
#include <builtinFonts/notosans_16_regular.h>
|
||||
#include <builtinFonts/notosans_18_bold.h>
|
||||
#include <builtinFonts/notosans_18_bolditalic.h>
|
||||
#include <builtinFonts/notosans_18_italic.h>
|
||||
#include <builtinFonts/notosans_18_regular.h>
|
||||
#include <builtinFonts/opendyslexic_10_bold.h>
|
||||
#include <builtinFonts/opendyslexic_10_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_10_italic.h>
|
||||
#include <builtinFonts/opendyslexic_10_regular.h>
|
||||
#include <builtinFonts/opendyslexic_12_bold.h>
|
||||
#include <builtinFonts/opendyslexic_12_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_12_italic.h>
|
||||
#include <builtinFonts/opendyslexic_12_regular.h>
|
||||
#include <builtinFonts/opendyslexic_14_bold.h>
|
||||
#include <builtinFonts/opendyslexic_14_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_14_italic.h>
|
||||
#include <builtinFonts/opendyslexic_14_regular.h>
|
||||
#include <builtinFonts/opendyslexic_8_bold.h>
|
||||
#include <builtinFonts/opendyslexic_8_bolditalic.h>
|
||||
#include <builtinFonts/opendyslexic_8_italic.h>
|
||||
#include <builtinFonts/opendyslexic_8_regular.h>
|
||||
#include <builtinFonts/ubuntu_10_bold.h>
|
||||
#include <builtinFonts/ubuntu_10_regular.h>
|
||||
#include <builtinFonts/ubuntu_12_bold.h>
|
||||
#include <builtinFonts/ubuntu_12_regular.h>
|
||||
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
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
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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user