Merge branch 'master' into feature/extend-image

This commit is contained in:
Jonas Diemer 2026-01-19 20:13:10 +01:00 committed by GitHub
commit 077526a126
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
132 changed files with 41851 additions and 385 deletions

View File

@ -0,0 +1,26 @@
name: "PR Formatting"
on:
pull_request_target:
types:
- opened
- reopened
- edited
permissions:
statuses: write
jobs:
title-check:
name: Title Check
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
with:
egress-policy: audit
- name: Check PR Title
uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@ -4,3 +4,5 @@
.vscode
lib/EpdFont/fontsrc
*.generated.h
build
**/__pycache__/

View File

@ -20,9 +20,10 @@ Button layout can be customized in **[Settings](#35-settings)**.
### Power On / Off
To turn the device on or off, **press and hold the Power button for half a second**. In **[Settings](#35-settings)** you can configure the power button to trigger on a short press instead of a long one.
To turn the device on or off, **press and hold the Power button for approximately half a second**.
In **[Settings](#35-settings)** you can configure the power button to turn the device off with a short press instead of a long one.
To reboot the device (for example if it's frozen, or after a firmware update), press and release the Reset button, and then hold the Power button for a few seconds.
To reboot the device (for example if it's frozen, or after a firmware update), press and release the Reset button, and then quickly press and hold the Power button for a few seconds.
### First Launch
@ -63,18 +64,29 @@ See the [webserver docs](./docs/webserver.md) for more information on how to con
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
- **Sleep Screen**: Which sleep screen to display when the device sleeps:
- "Dark" (default) - The default dark sleep screen
- "Dark" (default) - The default dark Crosspoint logo sleep screen
- "Light" - The same default sleep screen, on a white background
- "Custom" - Custom images from the SD card, see [Sleep Screen](#36-sleep-screen) below for more information
- "Custom" - Custom images from the SD card; see [Sleep Screen](#36-sleep-screen) below for more information
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
- "Blank" - A blank screen
- "None" - A blank screen
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
- "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected)
- **Status Bar**: Configure the status bar displayed while reading:
- "None" - No status bar
- "No Progress" - Show status bar without reading progress
- "Full" - Show status bar with reading progress
- **Hide Battery %**: Configure where to suppress the battery pecentage display in the status bar; the battery icon will still be shown:
- "Never" - Always show battery percentage (default)
- "In Reader" - Show battery percentage everywhere except in reading mode
- "Always" - Always hide battery percentage
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book. If disabled, paragraphs will not have vertical space between them, but will have first-line indentation.
- **Short Power Button Click**: Whether to trigger the power button on a short press or a long press.
- **Reading Orientation**: Set the screen orientation for reading:
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
- **Short Power Button Click**: Controls the effect of a short click of the power button:
- "Ignore" - Require a long press to turn off the device
- "Sleep" - A short press powers the device off
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
- **Reading Orientation**: Set the screen orientation for reading EPUB files:
- "Portrait" (default) - Standard portrait orientation
- "Landscape CW" - Landscape, rotated clockwise
- "Inverted" - Portrait, upside down
@ -83,16 +95,22 @@ The Settings screen allows you to configure the device's behavior. There are a f
- Back, Confirm, Left, Right (default)
- Left, Right, Back, Confirm
- Left, Back, Confirm, Right
- **Side Button Layout**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
- "Page Scroll" - Long-pressing scrolls a page up/down
- Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Reader Font Family**: Choose the font used for reading:
- "Bookerly" (default) - Amazon's reading font
- "Noto Sans" - Google's sans-serif font
- "Open Dyslexic" - Font designed for readers with dyslexia
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium", "Large", or "X Large".
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal", or "Wide".
- **Reader Screen Margin**: Controls the screen margins in reader mode between 5 and 40 pixels in 5 pixel increments.
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
- **Calibre Settings**: Set up integration for accessing a Calibre web library or connecting to Calibre as a wireless device.
- **Check for updates**: Check for firmware updates over WiFi.
### 3.6 Sleep Screen
@ -124,10 +142,15 @@ Once you have opened a book, the button layout changes to facilitate reading.
The role of the volume (side) buttons can be swapped in **[Settings](#35-settings)**.
If the **Short Power Button Click** setting is set to "Page Turn", you can also turn to the next page by briefly pressing the Power button.
### Chapter Navigation
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake.
### System Navigation
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.

View File

@ -0,0 +1,66 @@
# Hypher Binary Tries
CrossPoint embeds the exact binary automata produced by
[Typst's `hypher`](https://github.com/typst/hypher).
## File layout
Each `.bin` blob is a single self-contained automaton:
```
uint32_t root_addr_be; // big-endian offset of the root node
uint8_t levels[]; // shared "levels" tape (dist/score pairs)
uint8_t nodes[]; // node records packed back-to-back
```
The size of the `levels` tape is implicit. Individual nodes reference slices
inside that tape via 12-bit offsets, so no additional pointers are required.
### Node encoding
Every node starts with a single control byte:
- Bit 7 set when the node stores scores (`levels`).
- Bits 5-6 stride of the target deltas (1, 2, or 3 bytes, big-endian).
- Bits 0-4 transition count (values ≥ 31 spill into an extra byte).
If the `levels` flag is set, two more bytes follow. Together they encode a
12-bit offset into the global `levels` tape and a 4-bit length. Each byte in the
levels tape packs a distance/score pair as `dist * 10 + score`, where `dist`
counts how many UTF-8 bytes we advanced since the previous digit.
After the optional levels header come the transition labels (one byte per edge)
followed by the signed target deltas. Targets are stored as relative offsets
from the current node address. Deltas up to ±128 fit in a single byte, larger
distances grow to 2 or 3 bytes. The runtime walks the transitions with a simple
linear scan and materializes the absolute address by adding the decoded delta
to the current nodes base.
## Embedding blobs into the firmware
The helper script `scripts/generate_hyphenation_trie.py` acts as a thin
wrapper: it reads the hypher-generated `.bin` files, formats them as `constexpr`
byte arrays, and emits headers under
`lib/Epub/Epub/hyphenation/generated/`. Each header defines the raw data plus a
`SerializedHyphenationPatterns` descriptor so the reader can keep the automaton
in flash.
To refresh the firmware assets after updating the `.bin` files, run:
```
./scripts/generate_hyphenation_trie.py \
--input lib/Epub/Epub/hyphenation/tries/en.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-en.trie.h
./scripts/generate_hyphenation_trie.py \
--input lib/Epub/Epub/hyphenation/tries/fr.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-fr.trie.h
./scripts/generate_hyphenation_trie.py \
--input lib/Epub/Epub/hyphenation/tries/de.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-de.trie.h
./scripts/generate_hyphenation_trie.py \
--input lib/Epub/Epub/hyphenation/tries/ru.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-ru.trie.h
```

View File

@ -22,8 +22,7 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
const EpdGlyph* glyph = getGlyph(cp);
if (!glyph) {
// TODO: Replace with fallback glyph property?
glyph = getGlyph('?');
glyph = getGlyph(REPLACEMENT_GLYPH);
}
if (!glyph) {

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_12_boldBitmaps[51217] = {
static const uint8_t bookerly_12_boldBitmaps[51367] = {
0x0A, 0x83, 0xFC, 0x3F, 0x87, 0xF4, 0x7F, 0x43, 0xF4, 0x3F, 0x03, 0xF0, 0x3F, 0x03, 0xF0, 0x3F,
0x03, 0xF0, 0x2F, 0x01, 0xF0, 0x00, 0x00, 0x10, 0x2F, 0xC3, 0xFC, 0x3F, 0xC0, 0x50, 0x7E, 0x1F,
0x7F, 0x8F, 0xDF, 0xD3, 0xF3, 0xF4, 0xFC, 0xFD, 0x3F, 0x2F, 0x0F, 0xCB, 0xC3, 0xF2, 0xE0, 0xF8,
@ -3209,7 +3209,16 @@ static const uint8_t bookerly_12_boldBitmaps[51217] = {
0x2F, 0xFF, 0xFF, 0xFE, 0xBF, 0xFF, 0xFF, 0xFA, 0xFF, 0xFF, 0xFF, 0xE0, 0x2F, 0x47, 0xFC, 0x7F,
0xC3, 0xF4, 0x00, 0x00, 0x1F, 0x80, 0x0B, 0xC0, 0x0B, 0xD0, 0xFF, 0x40, 0xBF, 0x80, 0x7F, 0xC3,
0xFD, 0x02, 0xFE, 0x01, 0xFF, 0x0B, 0xE0, 0x07, 0xF0, 0x03, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00,
0x7F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x74, 0x1F, 0x80, 0x00,
0x00, 0x00, 0x74, 0x00, 0x78, 0x00, 0x00, 0x00, 0x7D, 0x04, 0x0B, 0x80, 0x00, 0x00, 0x7F, 0xFF,
0xE0, 0xF8, 0x00, 0x00, 0x7F, 0xFF, 0xFD, 0x2F, 0x80, 0x00, 0x7F, 0xFF, 0xFF, 0x87, 0xF8, 0x00,
0x7F, 0xFF, 0xFF, 0xE1, 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0x80, 0xBF, 0xF8, 0x2F, 0xFF, 0xF8, 0x00,
0x3F, 0xFE, 0x02, 0xFF, 0xFC, 0x00, 0xBF, 0xFE, 0x00, 0x2F, 0xFF, 0x42, 0xFF, 0xFE, 0x00, 0x02,
0xFF, 0xE2, 0xFF, 0xFE, 0x00, 0x00, 0x2F, 0xFC, 0xBF, 0xFE, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFE,
0x00, 0x00, 0x00, 0x2F, 0xD2, 0xFE, 0x00, 0x00, 0x00, 0x02, 0xF0, 0x3E, 0x00, 0x00, 0x00, 0x00,
0x2D, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_12_boldGlyphs[] = {
@ -3941,6 +3950,7 @@ static const EpdGlyph bookerly_12_boldGlyphs[] = {
{ 15, 18, 25, 5, 18, 68, 51112 }, // ⊥
{ 6, 5, 16, 5, 9, 8, 51180 }, // ⋅
{ 23, 5, 25, 1, 9, 29, 51188 }, // ⋯
{ 25, 24, 25, 0, 21, 150, 51217 }, // <20>
};
static const EpdUnicodeInterval bookerly_12_boldIntervals[] = {
@ -4004,13 +4014,14 @@ static const EpdUnicodeInterval bookerly_12_boldIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_12_bold = {
bookerly_12_boldBitmaps,
bookerly_12_boldGlyphs,
bookerly_12_boldIntervals,
60,
61,
33,
27,
-7,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_12_bolditalicBitmaps[52522] = {
static const uint8_t bookerly_12_bolditalicBitmaps[52672] = {
0x00, 0x68, 0x01, 0xFC, 0x03, 0xF8, 0x07, 0xF4, 0x07, 0xF0, 0x0B, 0xE0, 0x0F, 0xD0, 0x0F, 0xC0,
0x0F, 0xC0, 0x0F, 0x80, 0x1F, 0x40, 0x1F, 0x00, 0x2F, 0x00, 0x2D, 0x00, 0x00, 0x00, 0x04, 0x00,
0x7F, 0x00, 0xBF, 0x00, 0xBD, 0x00, 0x10, 0x00, 0x0B, 0x87, 0xC2, 0xF4, 0xFC, 0x3F, 0x2F, 0x83,
@ -3290,7 +3290,16 @@ static const uint8_t bookerly_12_bolditalicBitmaps[52522] = {
0xC0, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x2F, 0xFF, 0xFF, 0xFE, 0xBF, 0xFF, 0xFF,
0xFA, 0xFF, 0xFF, 0xFF, 0xE0, 0x2F, 0x47, 0xFC, 0x7F, 0xC3, 0xF4, 0x00, 0x00, 0x1F, 0x80, 0x0B,
0xC0, 0x0B, 0xD0, 0xFF, 0x40, 0xBF, 0x80, 0x7F, 0xC3, 0xFD, 0x02, 0xFE, 0x01, 0xFF, 0x0B, 0xE0,
0x07, 0xF0, 0x03, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x07, 0xF0, 0x03, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00,
0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x7F,
0xF8, 0x00, 0x00, 0x00, 0x00, 0x74, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x74, 0x00, 0x78, 0x00, 0x00,
0x00, 0x7D, 0x04, 0x0B, 0x80, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0xF8, 0x00, 0x00, 0x7F, 0xFF, 0xFD,
0x2F, 0x80, 0x00, 0x7F, 0xFF, 0xFF, 0x87, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xE1, 0xFF, 0x80, 0x7F,
0xFF, 0xFF, 0x80, 0xBF, 0xF8, 0x2F, 0xFF, 0xF8, 0x00, 0x3F, 0xFE, 0x02, 0xFF, 0xFC, 0x00, 0xBF,
0xFE, 0x00, 0x2F, 0xFF, 0x42, 0xFF, 0xFE, 0x00, 0x02, 0xFF, 0xE2, 0xFF, 0xFE, 0x00, 0x00, 0x2F,
0xFC, 0xBF, 0xFE, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x2F, 0xD2, 0xFE, 0x00,
0x00, 0x00, 0x02, 0xF0, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x2D, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x02,
0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_12_bolditalicGlyphs[] = {
@ -4022,6 +4031,7 @@ static const EpdGlyph bookerly_12_bolditalicGlyphs[] = {
{ 15, 18, 25, 5, 18, 68, 52417 }, // ⊥
{ 6, 5, 16, 5, 9, 8, 52485 }, // ⋅
{ 23, 5, 25, 1, 9, 29, 52493 }, // ⋯
{ 25, 24, 25, 0, 21, 150, 52522 }, // <20>
};
static const EpdUnicodeInterval bookerly_12_bolditalicIntervals[] = {
@ -4085,13 +4095,14 @@ static const EpdUnicodeInterval bookerly_12_bolditalicIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_12_bolditalic = {
bookerly_12_bolditalicBitmaps,
bookerly_12_bolditalicGlyphs,
bookerly_12_bolditalicIntervals,
60,
61,
33,
27,
-7,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_12_italicBitmaps[48812] = {
static const uint8_t bookerly_12_italicBitmaps[48962] = {
0x00, 0x14, 0x03, 0xD0, 0x1F, 0x00, 0xF8, 0x03, 0xD0, 0x0F, 0x00, 0x7C, 0x02, 0xD0, 0x0B, 0x00,
0x3C, 0x00, 0xE0, 0x03, 0x80, 0x1D, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x1D, 0x01, 0xF8, 0x07,
0xC0, 0x04, 0x00, 0x0A, 0x07, 0x0B, 0x87, 0xC3, 0xD2, 0xE0, 0xF0, 0xB4, 0x38, 0x3C, 0x0D, 0x0E,
@ -3058,7 +3058,17 @@ static const uint8_t bookerly_12_italicBitmaps[48812] = {
0x00, 0x00, 0x2E, 0x00, 0x00, 0x00, 0xB8, 0x00, 0x00, 0x02, 0xE0, 0x00, 0x00, 0x0B, 0x80, 0x00,
0x00, 0x2E, 0x00, 0x00, 0x00, 0xB8, 0x00, 0x05, 0x56, 0xE5, 0x54, 0xBF, 0xFF, 0xFF, 0xFA, 0xFF,
0xFF, 0xFF, 0xE0, 0x10, 0xBD, 0xFE, 0xBC, 0x04, 0x00, 0x04, 0x00, 0x05, 0x1F, 0xC0, 0x0F, 0xC0,
0x0B, 0xDB, 0xF0, 0x03, 0xF0, 0x03, 0xF8, 0xF4, 0x00, 0xF8, 0x00, 0x7C,
0x0B, 0xDB, 0xF0, 0x03, 0xF0, 0x03, 0xF8, 0xF4, 0x00, 0xF8, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x40,
0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x80, 0x00, 0x00, 0x00,
0x00, 0x7F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x74, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x74, 0x00, 0x78,
0x00, 0x00, 0x00, 0x7D, 0x04, 0x0B, 0x80, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0xF8, 0x00, 0x00, 0x7F,
0xFF, 0xFD, 0x2F, 0x80, 0x00, 0x7F, 0xFF, 0xFF, 0x87, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xE1, 0xFF,
0x80, 0x7F, 0xFF, 0xFF, 0x80, 0xBF, 0xF8, 0x2F, 0xFF, 0xF8, 0x00, 0x3F, 0xFE, 0x02, 0xFF, 0xFC,
0x00, 0xBF, 0xFE, 0x00, 0x2F, 0xFF, 0x42, 0xFF, 0xFE, 0x00, 0x02, 0xFF, 0xE2, 0xFF, 0xFE, 0x00,
0x00, 0x2F, 0xFC, 0xBF, 0xFE, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x2F, 0xD2,
0xFE, 0x00, 0x00, 0x00, 0x02, 0xF0, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x2D, 0x2E, 0x00, 0x00, 0x00,
0x00, 0x02, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00,
};
static const EpdGlyph bookerly_12_italicGlyphs[] = {
@ -3790,6 +3800,7 @@ static const EpdGlyph bookerly_12_italicGlyphs[] = {
{ 15, 18, 25, 5, 18, 68, 48719 }, // ⊥
{ 4, 4, 16, 6, 9, 4, 48787 }, // ⋅
{ 21, 4, 25, 2, 9, 21, 48791 }, // ⋯
{ 25, 24, 25, 0, 21, 150, 48812 }, // <20>
};
static const EpdUnicodeInterval bookerly_12_italicIntervals[] = {
@ -3853,13 +3864,14 @@ static const EpdUnicodeInterval bookerly_12_italicIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_12_italic = {
bookerly_12_italicBitmaps,
bookerly_12_italicGlyphs,
bookerly_12_italicIntervals,
60,
61,
33,
27,
-7,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_12_regularBitmaps[47071] = {
static const uint8_t bookerly_12_regularBitmaps[47221] = {
0x28, 0xBC, 0xFC, 0xF8, 0xF8, 0xF8, 0xF4, 0xF4, 0xF4, 0xF4, 0xB4, 0xB4, 0x74, 0x74, 0x00, 0x00,
0x78, 0xFD, 0xFC, 0x10, 0x28, 0x2E, 0xE1, 0xFB, 0x47, 0xED, 0x1E, 0xB4, 0x7A, 0xD1, 0xEB, 0x03,
0x94, 0x04, 0x00, 0x0E, 0x02, 0xC0, 0x00, 0x0D, 0x03, 0x80, 0x00, 0x1D, 0x03, 0x40, 0x00, 0x2C,
@ -2949,7 +2949,17 @@ static const uint8_t bookerly_12_regularBitmaps[47071] = {
0x00, 0x0B, 0x80, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00, 0xB8, 0x00, 0x00, 0x02, 0xE0, 0x00, 0x00,
0x0B, 0x80, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00, 0xB8, 0x00, 0x05, 0x56, 0xE5, 0x54, 0xBF, 0xFF,
0xFF, 0xFA, 0xFF, 0xFF, 0xFF, 0xE0, 0x10, 0xBD, 0xFE, 0xBC, 0x04, 0x00, 0x04, 0x00, 0x05, 0x1F,
0xC0, 0x0F, 0xC0, 0x0B, 0xDB, 0xF0, 0x03, 0xF0, 0x03, 0xF8, 0xF4, 0x00, 0xF8, 0x00, 0x7C,
0xC0, 0x0F, 0xC0, 0x0B, 0xDB, 0xF0, 0x03, 0xF0, 0x03, 0xF8, 0xF4, 0x00, 0xF8, 0x00, 0x7C, 0x00,
0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x80,
0x00, 0x00, 0x00, 0x00, 0x7F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x74, 0x1F, 0x80, 0x00, 0x00, 0x00,
0x74, 0x00, 0x78, 0x00, 0x00, 0x00, 0x7D, 0x04, 0x0B, 0x80, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0xF8,
0x00, 0x00, 0x7F, 0xFF, 0xFD, 0x2F, 0x80, 0x00, 0x7F, 0xFF, 0xFF, 0x87, 0xF8, 0x00, 0x7F, 0xFF,
0xFF, 0xE1, 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0x80, 0xBF, 0xF8, 0x2F, 0xFF, 0xF8, 0x00, 0x3F, 0xFE,
0x02, 0xFF, 0xFC, 0x00, 0xBF, 0xFE, 0x00, 0x2F, 0xFF, 0x42, 0xFF, 0xFE, 0x00, 0x02, 0xFF, 0xE2,
0xFF, 0xFE, 0x00, 0x00, 0x2F, 0xFC, 0xBF, 0xFE, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFE, 0x00, 0x00,
0x00, 0x2F, 0xD2, 0xFE, 0x00, 0x00, 0x00, 0x02, 0xF0, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x2D, 0x2E,
0x00, 0x00, 0x00, 0x00, 0x02, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_12_regularGlyphs[] = {
@ -3681,6 +3691,7 @@ static const EpdGlyph bookerly_12_regularGlyphs[] = {
{ 15, 18, 25, 5, 18, 68, 46978 }, // ⊥
{ 4, 4, 16, 6, 9, 4, 47046 }, // ⋅
{ 21, 4, 25, 2, 9, 21, 47050 }, // ⋯
{ 25, 24, 25, 0, 21, 150, 47071 }, // <20>
};
static const EpdUnicodeInterval bookerly_12_regularIntervals[] = {
@ -3744,13 +3755,14 @@ static const EpdUnicodeInterval bookerly_12_regularIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_12_regular = {
bookerly_12_regularBitmaps,
bookerly_12_regularGlyphs,
bookerly_12_regularIntervals,
60,
61,
33,
27,
-7,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_14_boldBitmaps[67020] = {
static const uint8_t bookerly_14_boldBitmaps[67209] = {
0x1A, 0x4B, 0xF8, 0xFF, 0x8F, 0xF4, 0xFF, 0x4F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xBF,
0x0B, 0xF0, 0x7F, 0x07, 0xF0, 0x3F, 0x03, 0xF0, 0x10, 0x00, 0x00, 0x2F, 0x4B, 0xFC, 0xBF, 0xC7,
0xFC, 0x15, 0x00, 0x2F, 0x42, 0xE7, 0xF4, 0xFF, 0xBF, 0x0F, 0xEB, 0xF0, 0xFE, 0xBF, 0x0F, 0xEB,
@ -4196,7 +4196,19 @@ static const uint8_t bookerly_14_boldBitmaps[67020] = {
0xFF, 0xFF, 0xFF, 0xFA, 0xFF, 0xFF, 0xFF, 0xFE, 0xBF, 0xFF, 0xFF, 0xFF, 0x80, 0x01, 0x03, 0xFC,
0xBF, 0xEB, 0xFE, 0x3F, 0xC0, 0x00, 0x05, 0x00, 0x00, 0x40, 0x00, 0x04, 0x0F, 0xF0, 0x02, 0xFE,
0x00, 0x3F, 0xDB, 0xFD, 0x00, 0xFF, 0xC0, 0x1F, 0xFA, 0xFF, 0x40, 0x3F, 0xF0, 0x07, 0xFE, 0x7F,
0xC0, 0x0B, 0xF4, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xC0, 0x0B, 0xF4, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xE0, 0x00, 0x00,
0x00, 0x00, 0x02, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x5B, 0xE0, 0x00, 0x00, 0x00, 0x02,
0xC0, 0x02, 0xE0, 0x00, 0x00, 0x00, 0x2E, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x02, 0xFC, 0x69, 0x02,
0xE0, 0x00, 0x00, 0x2F, 0xFF, 0xFF, 0x03, 0xE0, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0x0F, 0xE0, 0x00,
0x2F, 0xFF, 0xFF, 0xFC, 0x2F, 0xE0, 0x02, 0xFF, 0xFF, 0xFF, 0xF0, 0xBF, 0xE0, 0x2F, 0xFF, 0xFF,
0xFF, 0x42, 0xFF, 0xE2, 0xFF, 0xFF, 0xFE, 0x40, 0x0F, 0xFF, 0xE3, 0xFF, 0xFF, 0xC0, 0x00, 0xFF,
0xFF, 0x03, 0xFF, 0xFF, 0x00, 0x2F, 0xFF, 0xF0, 0x03, 0xFF, 0xFC, 0x0B, 0xFF, 0xFF, 0x00, 0x03,
0xFF, 0xF0, 0xBF, 0xFF, 0xF0, 0x00, 0x03, 0xFF, 0xD1, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0xFF, 0xFF,
0xFF, 0xF0, 0x00, 0x00, 0x03, 0xFF, 0xEF, 0xFF, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x0F, 0xF0, 0x00,
0x00, 0x00, 0x03, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x03, 0xD2, 0xF0, 0x00, 0x00, 0x00, 0x00,
0x03, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_14_boldGlyphs[] = {
@ -4928,6 +4940,7 @@ static const EpdGlyph bookerly_14_boldGlyphs[] = {
{ 17, 21, 29, 6, 21, 90, 66883 }, // ⊥
{ 6, 6, 18, 6, 11, 9, 66973 }, // ⋅
{ 25, 6, 29, 2, 11, 38, 66982 }, // ⋯
{ 27, 28, 29, 1, 24, 189, 67020 }, // <20>
};
static const EpdUnicodeInterval bookerly_14_boldIntervals[] = {
@ -4991,13 +5004,14 @@ static const EpdUnicodeInterval bookerly_14_boldIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_14_bold = {
bookerly_14_boldBitmaps,
bookerly_14_boldGlyphs,
bookerly_14_boldIntervals,
60,
61,
38,
31,
-8,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_14_bolditalicBitmaps[70024] = {
static const uint8_t bookerly_14_bolditalicBitmaps[70213] = {
0x00, 0x1A, 0x40, 0x2F, 0xD0, 0x0F, 0xF0, 0x0B, 0xF8, 0x03, 0xFD, 0x00, 0xFF, 0x00, 0x3F, 0x80,
0x1F, 0xD0, 0x07, 0xF0, 0x02, 0xFC, 0x00, 0xBE, 0x00, 0x3F, 0x40, 0x0F, 0xC0, 0x03, 0xF0, 0x00,
0xF8, 0x00, 0x3D, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0xF4, 0x01, 0xFF, 0x00, 0xBF, 0x80, 0x1F,
@ -4384,7 +4384,19 @@ static const uint8_t bookerly_14_bolditalicBitmaps[70024] = {
0xFF, 0xFF, 0xFF, 0xFE, 0xBF, 0xFF, 0xFF, 0xFF, 0x80, 0x01, 0x03, 0xFC, 0xBF, 0xEB, 0xFE, 0x3F,
0xC0, 0x00, 0x05, 0x00, 0x00, 0x40, 0x00, 0x04, 0x0F, 0xF0, 0x02, 0xFE, 0x00, 0x3F, 0xDB, 0xFD,
0x00, 0xFF, 0xC0, 0x1F, 0xFA, 0xFF, 0x40, 0x3F, 0xF0, 0x07, 0xFE, 0x7F, 0xC0, 0x0B, 0xF4, 0x00,
0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF,
0xE0, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x5B, 0xE0, 0x00, 0x00, 0x00, 0x02, 0xC0, 0x02, 0xE0, 0x00,
0x00, 0x00, 0x2E, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x02, 0xFC, 0x69, 0x02, 0xE0, 0x00, 0x00, 0x2F,
0xFF, 0xFF, 0x03, 0xE0, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0x0F, 0xE0, 0x00, 0x2F, 0xFF, 0xFF, 0xFC,
0x2F, 0xE0, 0x02, 0xFF, 0xFF, 0xFF, 0xF0, 0xBF, 0xE0, 0x2F, 0xFF, 0xFF, 0xFF, 0x42, 0xFF, 0xE2,
0xFF, 0xFF, 0xFE, 0x40, 0x0F, 0xFF, 0xE3, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x03, 0xFF, 0xFF,
0x00, 0x2F, 0xFF, 0xF0, 0x03, 0xFF, 0xFC, 0x0B, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0xF0, 0xBF, 0xFF,
0xF0, 0x00, 0x03, 0xFF, 0xD1, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00,
0x03, 0xFF, 0xEF, 0xFF, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x03, 0xF0,
0x3F, 0x00, 0x00, 0x00, 0x00, 0x03, 0xD2, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_14_bolditalicGlyphs[] = {
@ -5116,6 +5128,7 @@ static const EpdGlyph bookerly_14_bolditalicGlyphs[] = {
{ 17, 21, 29, 6, 21, 90, 69887 }, // ⊥
{ 6, 6, 18, 6, 11, 9, 69977 }, // ⋅
{ 25, 6, 29, 2, 11, 38, 69986 }, // ⋯
{ 27, 28, 29, 1, 24, 189, 70024 }, // <20>
};
static const EpdUnicodeInterval bookerly_14_bolditalicIntervals[] = {
@ -5179,13 +5192,14 @@ static const EpdUnicodeInterval bookerly_14_bolditalicIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_14_bolditalic = {
bookerly_14_bolditalicBitmaps,
bookerly_14_bolditalicGlyphs,
bookerly_14_bolditalicIntervals,
60,
61,
38,
31,
-8,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_14_italicBitmaps[64856] = {
static const uint8_t bookerly_14_italicBitmaps[65045] = {
0x00, 0x05, 0x00, 0x0F, 0x80, 0x0B, 0xD0, 0x03, 0xF0, 0x00, 0xF8, 0x00, 0x7D, 0x00, 0x2F, 0x00,
0x0F, 0x80, 0x03, 0xD0, 0x00, 0xF0, 0x00, 0x7C, 0x00, 0x1E, 0x00, 0x0B, 0x40, 0x02, 0xC0, 0x00,
0xF0, 0x00, 0x3C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBC, 0x00, 0x3F, 0x40, 0x0F,
@ -4061,7 +4061,19 @@ static const uint8_t bookerly_14_italicBitmaps[64856] = {
0xFC, 0x00, 0x06, 0xAA, 0xBF, 0xAA, 0xA6, 0xFF, 0xFF, 0xFF, 0xFE, 0xBF, 0xFF, 0xFF, 0xFF, 0x80,
0x0A, 0x43, 0xFC, 0x3F, 0xC2, 0xF8, 0x00, 0x00, 0x0A, 0x00, 0x01, 0xA0, 0x00, 0x29, 0x0F, 0xF0,
0x01, 0xFD, 0x00, 0x2F, 0xC3, 0xFC, 0x00, 0xBF, 0x80, 0x0F, 0xF0, 0xFD, 0x00, 0x0F, 0xC0, 0x01,
0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF,
0xE0, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x5B, 0xE0, 0x00, 0x00, 0x00, 0x02, 0xC0, 0x02, 0xE0, 0x00,
0x00, 0x00, 0x2E, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x02, 0xFC, 0x69, 0x02, 0xE0, 0x00, 0x00, 0x2F,
0xFF, 0xFF, 0x03, 0xE0, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0x0F, 0xE0, 0x00, 0x2F, 0xFF, 0xFF, 0xFC,
0x2F, 0xE0, 0x02, 0xFF, 0xFF, 0xFF, 0xF0, 0xBF, 0xE0, 0x2F, 0xFF, 0xFF, 0xFF, 0x42, 0xFF, 0xE2,
0xFF, 0xFF, 0xFE, 0x40, 0x0F, 0xFF, 0xE3, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x03, 0xFF, 0xFF,
0x00, 0x2F, 0xFF, 0xF0, 0x03, 0xFF, 0xFC, 0x0B, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0xF0, 0xBF, 0xFF,
0xF0, 0x00, 0x03, 0xFF, 0xD1, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00,
0x03, 0xFF, 0xEF, 0xFF, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x03, 0xF0,
0x3F, 0x00, 0x00, 0x00, 0x00, 0x03, 0xD2, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_14_italicGlyphs[] = {
@ -4793,6 +4805,7 @@ static const EpdGlyph bookerly_14_italicGlyphs[] = {
{ 17, 21, 29, 6, 21, 90, 64726 }, // ⊥
{ 6, 5, 18, 6, 10, 8, 64816 }, // ⋅
{ 25, 5, 29, 2, 10, 32, 64824 }, // ⋯
{ 27, 28, 29, 1, 24, 189, 64856 }, // <20>
};
static const EpdUnicodeInterval bookerly_14_italicIntervals[] = {
@ -4856,13 +4869,14 @@ static const EpdUnicodeInterval bookerly_14_italicIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_14_italic = {
bookerly_14_italicBitmaps,
bookerly_14_italicGlyphs,
bookerly_14_italicIntervals,
60,
61,
38,
31,
-8,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_14_regularBitmaps[62675] = {
static const uint8_t bookerly_14_regularBitmaps[62864] = {
0x1A, 0x1F, 0xCB, 0xE2, 0xF4, 0xBD, 0x2F, 0x4B, 0xC2, 0xF0, 0xBC, 0x2F, 0x07, 0xC1, 0xF0, 0x7C,
0x0F, 0x03, 0xC0, 0xF0, 0x00, 0x00, 0x00, 0x40, 0xFC, 0xBF, 0x5F, 0xC1, 0x40, 0x18, 0x06, 0x1F,
0x07, 0xCB, 0xC2, 0xF1, 0xF0, 0xBC, 0x7C, 0x1F, 0x1F, 0x07, 0xC7, 0xC1, 0xF1, 0xF0, 0x7C, 0x34,
@ -3925,7 +3925,18 @@ static const uint8_t bookerly_14_regularBitmaps[62675] = {
0xAA, 0xA6, 0xFF, 0xFF, 0xFF, 0xFE, 0xBF, 0xFF, 0xFF, 0xFF, 0x80, 0x0A, 0x43, 0xFC, 0x3F, 0xC2,
0xF8, 0x00, 0x00, 0x0A, 0x00, 0x01, 0xA0, 0x00, 0x29, 0x0F, 0xF0, 0x01, 0xFD, 0x00, 0x2F, 0xC3,
0xFC, 0x00, 0xBF, 0x80, 0x0F, 0xF0, 0xFD, 0x00, 0x0F, 0xC0, 0x01, 0xF8, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xE0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x2F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00,
0x2E, 0x5B, 0xE0, 0x00, 0x00, 0x00, 0x02, 0xC0, 0x02, 0xE0, 0x00, 0x00, 0x00, 0x2E, 0x00, 0x01,
0xE0, 0x00, 0x00, 0x02, 0xFC, 0x69, 0x02, 0xE0, 0x00, 0x00, 0x2F, 0xFF, 0xFF, 0x03, 0xE0, 0x00,
0x02, 0xFF, 0xFF, 0xFF, 0x0F, 0xE0, 0x00, 0x2F, 0xFF, 0xFF, 0xFC, 0x2F, 0xE0, 0x02, 0xFF, 0xFF,
0xFF, 0xF0, 0xBF, 0xE0, 0x2F, 0xFF, 0xFF, 0xFF, 0x42, 0xFF, 0xE2, 0xFF, 0xFF, 0xFE, 0x40, 0x0F,
0xFF, 0xE3, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0x00, 0x2F, 0xFF, 0xF0, 0x03,
0xFF, 0xFC, 0x0B, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0xF0, 0xBF, 0xFF, 0xF0, 0x00, 0x03, 0xFF, 0xD1,
0xFF, 0xFF, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x03, 0xFF, 0xEF, 0xFF, 0x00,
0x00, 0x00, 0x03, 0xFC, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x03, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00,
0x03, 0xD2, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xF0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_14_regularGlyphs[] = {
@ -4657,6 +4668,7 @@ static const EpdGlyph bookerly_14_regularGlyphs[] = {
{ 17, 21, 29, 6, 21, 90, 62545 }, // ⊥
{ 6, 5, 18, 6, 10, 8, 62635 }, // ⋅
{ 25, 5, 29, 2, 10, 32, 62643 }, // ⋯
{ 27, 28, 29, 1, 24, 189, 62675 }, // <20>
};
static const EpdUnicodeInterval bookerly_14_regularIntervals[] = {
@ -4720,13 +4732,14 @@ static const EpdUnicodeInterval bookerly_14_regularIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_14_regular = {
bookerly_14_regularBitmaps,
bookerly_14_regularGlyphs,
bookerly_14_regularIntervals,
60,
61,
38,
31,
-8,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_16_boldBitmaps[85154] = {
static const uint8_t bookerly_16_boldBitmaps[85402] = {
0x0A, 0x90, 0xFF, 0x87, 0xFE, 0x2F, 0xF4, 0xBF, 0xC2, 0xFF, 0x0B, 0xFC, 0x2F, 0xF0, 0xBF, 0xC2,
0xFF, 0x0B, 0xFC, 0x1F, 0xF0, 0x7F, 0xC0, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0xC0, 0xBF, 0x01,
0x40, 0x00, 0x00, 0x05, 0x40, 0xFF, 0xC7, 0xFF, 0x1F, 0xFC, 0x3F, 0xF0, 0x19, 0x00, 0x0A, 0x80,
@ -5330,7 +5330,22 @@ static const uint8_t bookerly_16_boldBitmaps[85154] = {
0x2F, 0xFC, 0x7F, 0xE0, 0x15, 0x00, 0x0A, 0x90, 0x00, 0x2A, 0x40, 0x00, 0xA9, 0x0F, 0xFC, 0x00,
0x3F, 0xF0, 0x00, 0xFF, 0xC7, 0xFF, 0x40, 0x1F, 0xFD, 0x00, 0x7F, 0xF5, 0xFF, 0xD0, 0x07, 0xFF,
0x40, 0x1F, 0xFD, 0x3F, 0xF0, 0x00, 0xFF, 0xC0, 0x03, 0xFF, 0x01, 0x50, 0x00, 0x05, 0x40, 0x00,
0x15, 0x00,
0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFC, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xEF, 0xFC, 0x00, 0x00,
0x00, 0x00, 0x00, 0xF0, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xBC, 0x00, 0x00,
0x00, 0x00, 0xFC, 0x00, 0x00, 0xBC, 0x00, 0x00, 0x00, 0x0F, 0xF9, 0xBE, 0x40, 0xFC, 0x00, 0x00,
0x00, 0xFF, 0xFF, 0xFF, 0x81, 0xFC, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0x43, 0xFC, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0xFC, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFC, 0x2F, 0xFC, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0xD0, 0xFF, 0xFC, 0x0F, 0xFF, 0xFF, 0xFF, 0xE4, 0x03, 0xFF, 0xFC, 0x7F, 0xFF,
0xFF, 0xD0, 0x00, 0x2F, 0xFF, 0xF4, 0x7F, 0xFF, 0xFE, 0x00, 0x02, 0xFF, 0xFF, 0x40, 0x7F, 0xFF,
0xF8, 0x00, 0x7F, 0xFF, 0xF4, 0x00, 0x7F, 0xFF, 0xE0, 0x2F, 0xFF, 0xFF, 0x40, 0x00, 0x7F, 0xFF,
0xC2, 0xFF, 0xFF, 0xF4, 0x00, 0x00, 0x7F, 0xFF, 0x07, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x7F, 0xFE,
0x7F, 0xFF, 0xF4, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x7F, 0xF4,
0x7F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x80, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x03,
0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7D, 0x2F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF4,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_16_boldGlyphs[] = {
@ -6062,6 +6077,7 @@ static const EpdGlyph bookerly_16_boldGlyphs[] = {
{ 19, 23, 33, 7, 23, 110, 84989 }, // ⊥
{ 7, 6, 21, 7, 12, 11, 85099 }, // ⋅
{ 29, 6, 33, 2, 12, 44, 85110 }, // ⋯
{ 31, 32, 33, 1, 28, 248, 85154 }, // <20>
};
static const EpdUnicodeInterval bookerly_16_boldIntervals[] = {
@ -6125,13 +6141,14 @@ static const EpdUnicodeInterval bookerly_16_boldIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_16_bold = {
bookerly_16_boldBitmaps,
bookerly_16_boldGlyphs,
bookerly_16_boldIntervals,
60,
61,
44,
36,
-9,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_16_bolditalicBitmaps[88512] = {
static const uint8_t bookerly_16_bolditalicBitmaps[88760] = {
0x00, 0x06, 0xA0, 0x00, 0xBF, 0xC0, 0x07, 0xFD, 0x00, 0x3F, 0xF0, 0x00, 0xFF, 0x80, 0x07, 0xFD,
0x00, 0x1F, 0xF0, 0x00, 0xBF, 0x80, 0x03, 0xFD, 0x00, 0x0F, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFE,
0x00, 0x07, 0xF4, 0x00, 0x1F, 0xC0, 0x00, 0xBF, 0x00, 0x02, 0xF8, 0x00, 0x0B, 0xD0, 0x00, 0x3F,
@ -5540,6 +5540,22 @@ static const uint8_t bookerly_16_bolditalicBitmaps[88512] = {
0x7F, 0xE0, 0x15, 0x00, 0x0A, 0x90, 0x00, 0x2A, 0x40, 0x00, 0xA9, 0x0F, 0xFC, 0x00, 0x3F, 0xF0,
0x00, 0xFF, 0xC7, 0xFF, 0x40, 0x1F, 0xFD, 0x00, 0x7F, 0xF5, 0xFF, 0xD0, 0x07, 0xFF, 0x40, 0x1F,
0xFD, 0x3F, 0xF0, 0x00, 0xFF, 0xC0, 0x03, 0xFF, 0x01, 0x50, 0x00, 0x05, 0x40, 0x00, 0x15, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFC, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xEF, 0xFC, 0x00, 0x00, 0x00, 0x00,
0x00, 0xF0, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xBC, 0x00, 0x00, 0x00, 0x00,
0xFC, 0x00, 0x00, 0xBC, 0x00, 0x00, 0x00, 0x0F, 0xF9, 0xBE, 0x40, 0xFC, 0x00, 0x00, 0x00, 0xFF,
0xFF, 0xFF, 0x81, 0xFC, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0x43, 0xFC, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFE, 0x0F, 0xFC, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFC, 0x2F, 0xFC, 0x00, 0xFF, 0xFF, 0xFF,
0xFF, 0xD0, 0xFF, 0xFC, 0x0F, 0xFF, 0xFF, 0xFF, 0xE4, 0x03, 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xD0,
0x00, 0x2F, 0xFF, 0xF4, 0x7F, 0xFF, 0xFE, 0x00, 0x02, 0xFF, 0xFF, 0x40, 0x7F, 0xFF, 0xF8, 0x00,
0x7F, 0xFF, 0xF4, 0x00, 0x7F, 0xFF, 0xE0, 0x2F, 0xFF, 0xFF, 0x40, 0x00, 0x7F, 0xFF, 0xC2, 0xFF,
0xFF, 0xF4, 0x00, 0x00, 0x7F, 0xFF, 0x07, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFF,
0xF4, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x7F, 0xF4, 0x7F, 0xF4,
0x00, 0x00, 0x00, 0x00, 0x7F, 0x80, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x03, 0xF4, 0x00,
0x00, 0x00, 0x00, 0x00, 0x7D, 0x2F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF4, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x7F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_16_bolditalicGlyphs[] = {
@ -6271,6 +6287,7 @@ static const EpdGlyph bookerly_16_bolditalicGlyphs[] = {
{ 19, 23, 33, 7, 23, 110, 88347 }, // ⊥
{ 7, 6, 21, 7, 12, 11, 88457 }, // ⋅
{ 29, 6, 33, 2, 12, 44, 88468 }, // ⋯
{ 31, 32, 33, 1, 28, 248, 88512 }, // <20>
};
static const EpdUnicodeInterval bookerly_16_bolditalicIntervals[] = {
@ -6334,13 +6351,14 @@ static const EpdUnicodeInterval bookerly_16_bolditalicIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_16_bolditalic = {
bookerly_16_bolditalicBitmaps,
bookerly_16_bolditalicGlyphs,
bookerly_16_bolditalicIntervals,
60,
61,
44,
36,
-9,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_16_italicBitmaps[82412] = {
static const uint8_t bookerly_16_italicBitmaps[82660] = {
0x00, 0x01, 0x40, 0x00, 0xFC, 0x00, 0x3F, 0x80, 0x03, 0xF0, 0x00, 0x7F, 0x00, 0x0B, 0xE0, 0x00,
0xFC, 0x00, 0x0F, 0xC0, 0x00, 0xF8, 0x00, 0x1F, 0x40, 0x02, 0xF0, 0x00, 0x2F, 0x00, 0x03, 0xE0,
0x00, 0x3D, 0x00, 0x03, 0xC0, 0x00, 0x7C, 0x00, 0x07, 0x80, 0x00, 0x74, 0x00, 0x0A, 0x00, 0x00,
@ -5158,7 +5158,23 @@ static const uint8_t bookerly_16_italicBitmaps[82412] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x01, 0xFC, 0x3F, 0xE3, 0xFE,
0x2F, 0xC0, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBD, 0x00, 0x03, 0xF0, 0x00, 0x0B,
0xDB, 0xFC, 0x00, 0x2F, 0xF0, 0x00, 0xBF, 0xFF, 0xF0, 0x00, 0xFF, 0xC0, 0x03, 0xFF, 0x7F, 0x40,
0x01, 0xFD, 0x00, 0x07, 0xF4, 0x10, 0x00, 0x00, 0x40, 0x00, 0x01, 0x00,
0x01, 0xFD, 0x00, 0x07, 0xF4, 0x10, 0x00, 0x00, 0x40, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFC,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xEF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x01, 0xFC,
0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xBC, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xBC,
0x00, 0x00, 0x00, 0x0F, 0xF9, 0xBE, 0x40, 0xFC, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x81, 0xFC,
0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0x43, 0xFC, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0xFC,
0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFC, 0x2F, 0xFC, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xD0, 0xFF, 0xFC,
0x0F, 0xFF, 0xFF, 0xFF, 0xE4, 0x03, 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xD0, 0x00, 0x2F, 0xFF, 0xF4,
0x7F, 0xFF, 0xFE, 0x00, 0x02, 0xFF, 0xFF, 0x40, 0x7F, 0xFF, 0xF8, 0x00, 0x7F, 0xFF, 0xF4, 0x00,
0x7F, 0xFF, 0xE0, 0x2F, 0xFF, 0xFF, 0x40, 0x00, 0x7F, 0xFF, 0xC2, 0xFF, 0xFF, 0xF4, 0x00, 0x00,
0x7F, 0xFF, 0x07, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFF, 0xF4, 0x00, 0x00, 0x00,
0x7F, 0xFF, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x7F, 0xF4, 0x7F, 0xF4, 0x00, 0x00, 0x00, 0x00,
0x7F, 0x80, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x03, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00,
0x7D, 0x2F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x7F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_16_italicGlyphs[] = {
@ -5890,6 +5906,7 @@ static const EpdGlyph bookerly_16_italicGlyphs[] = {
{ 19, 23, 33, 7, 23, 110, 82252 }, // ⊥
{ 6, 6, 21, 7, 12, 9, 82362 }, // ⋅
{ 27, 6, 33, 3, 12, 41, 82371 }, // ⋯
{ 31, 32, 33, 1, 28, 248, 82412 }, // <20>
};
static const EpdUnicodeInterval bookerly_16_italicIntervals[] = {
@ -5953,13 +5970,14 @@ static const EpdUnicodeInterval bookerly_16_italicIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_16_italic = {
bookerly_16_italicBitmaps,
bookerly_16_italicGlyphs,
bookerly_16_italicIntervals,
60,
61,
44,
36,
-9,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_16_regularBitmaps[79871] = {
static const uint8_t bookerly_16_regularBitmaps[80119] = {
0x0A, 0x83, 0xF8, 0x3F, 0x47, 0xF4, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x3F, 0x03, 0xF0, 0x3F,
0x03, 0xF0, 0x3F, 0x03, 0xF0, 0x3F, 0x03, 0xF0, 0x2F, 0x02, 0xF0, 0x04, 0x00, 0x00, 0x00, 0x01,
0xA4, 0x3F, 0xC3, 0xFC, 0x3F, 0xC0, 0x50, 0x05, 0x01, 0x93, 0xE0, 0x7D, 0x7E, 0x0B, 0xD7, 0xE0,
@ -4999,7 +4999,23 @@ static const uint8_t bookerly_16_regularBitmaps[79871] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x01, 0xFC,
0x3F, 0xE3, 0xFE, 0x2F, 0xC0, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBD, 0x00, 0x03,
0xF0, 0x00, 0x0B, 0xDB, 0xFC, 0x00, 0x2F, 0xF0, 0x00, 0xBF, 0xFF, 0xF0, 0x00, 0xFF, 0xC0, 0x03,
0xFF, 0x7F, 0x40, 0x01, 0xFD, 0x00, 0x07, 0xF4, 0x10, 0x00, 0x00, 0x40, 0x00, 0x01, 0x00,
0xFF, 0x7F, 0x40, 0x01, 0xFD, 0x00, 0x07, 0xF4, 0x10, 0x00, 0x00, 0x40, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xEF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00,
0xF0, 0x01, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xBC, 0x00, 0x00, 0x00, 0x00, 0xFC,
0x00, 0x00, 0xBC, 0x00, 0x00, 0x00, 0x0F, 0xF9, 0xBE, 0x40, 0xFC, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0x81, 0xFC, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0x43, 0xFC, 0x00, 0x00, 0xFF, 0xFF, 0xFF,
0xFE, 0x0F, 0xFC, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFC, 0x2F, 0xFC, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xD0, 0xFF, 0xFC, 0x0F, 0xFF, 0xFF, 0xFF, 0xE4, 0x03, 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xD0, 0x00,
0x2F, 0xFF, 0xF4, 0x7F, 0xFF, 0xFE, 0x00, 0x02, 0xFF, 0xFF, 0x40, 0x7F, 0xFF, 0xF8, 0x00, 0x7F,
0xFF, 0xF4, 0x00, 0x7F, 0xFF, 0xE0, 0x2F, 0xFF, 0xFF, 0x40, 0x00, 0x7F, 0xFF, 0xC2, 0xFF, 0xFF,
0xF4, 0x00, 0x00, 0x7F, 0xFF, 0x07, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFF, 0xF4,
0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x7F, 0xF4, 0x7F, 0xF4, 0x00,
0x00, 0x00, 0x00, 0x7F, 0x80, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x03, 0xF4, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7D, 0x2F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF4, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_16_regularGlyphs[] = {
@ -5731,6 +5747,7 @@ static const EpdGlyph bookerly_16_regularGlyphs[] = {
{ 19, 23, 33, 7, 23, 110, 79711 }, // ⊥
{ 6, 6, 21, 7, 12, 9, 79821 }, // ⋅
{ 27, 6, 33, 3, 12, 41, 79830 }, // ⋯
{ 31, 32, 33, 1, 28, 248, 79871 }, // <20>
};
static const EpdUnicodeInterval bookerly_16_regularIntervals[] = {
@ -5794,13 +5811,14 @@ static const EpdUnicodeInterval bookerly_16_regularIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_16_regular = {
bookerly_16_regularBitmaps,
bookerly_16_regularGlyphs,
bookerly_16_regularIntervals,
60,
61,
44,
36,
-9,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_18_boldBitmaps[112410] = {
static const uint8_t bookerly_18_boldBitmaps[112734] = {
0x01, 0x54, 0x1F, 0xFC, 0x3F, 0xF8, 0x3F, 0xF8, 0x7F, 0xF4, 0x7F, 0xF4, 0x7F, 0xF0, 0x7F, 0xF0,
0x7F, 0xF0, 0x7F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0,
0x2F, 0xF0, 0x2F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF0, 0x0F, 0xF0, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00,
@ -7033,7 +7033,27 @@ static const uint8_t bookerly_18_boldBitmaps[112410] = {
0x00, 0x00, 0x54, 0x00, 0x00, 0x54, 0x01, 0xFF, 0xC0, 0x00, 0xBF, 0xF0, 0x00, 0x3F, 0xF4, 0x3F,
0xFE, 0x00, 0x0F, 0xFF, 0x40, 0x0B, 0xFF, 0xC3, 0xFF, 0xF0, 0x01, 0xFF, 0xF4, 0x00, 0xBF, 0xFC,
0x3F, 0xFE, 0x00, 0x1F, 0xFF, 0x00, 0x0B, 0xFF, 0xC1, 0xFF, 0xC0, 0x00, 0xFF, 0xE0, 0x00, 0x3F,
0xF4, 0x01, 0x50, 0x00, 0x01, 0x50, 0x00, 0x00, 0x54, 0x00,
0xF4, 0x01, 0x50, 0x00, 0x01, 0x50, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xBE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0B, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xFF, 0xF8, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xBE, 0x56, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xE0, 0x00, 0x2F, 0x80,
0x00, 0x00, 0x00, 0x00, 0x0B, 0xC0, 0x00, 0x07, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xC0, 0x00,
0x01, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xBF, 0xD1, 0xA9, 0x00, 0xBE, 0x00, 0x00, 0x00, 0x02, 0xFF,
0xFF, 0xFF, 0xD0, 0x3F, 0x80, 0x00, 0x00, 0x0B, 0xFF, 0xFF, 0xFF, 0xF0, 0x2F, 0xE0, 0x00, 0x00,
0x2F, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xF8, 0x00, 0x00, 0xBF, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFE,
0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF, 0x80, 0x0B, 0xFF, 0xFF, 0xFF, 0xFF, 0xF4,
0x1F, 0xFF, 0xE0, 0x2F, 0xFF, 0xFF, 0xFF, 0xFE, 0x40, 0x1F, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF,
0x80, 0x00, 0x3F, 0xFF, 0xFD, 0x1F, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0xFF, 0xFF, 0xF4, 0x07, 0xFF,
0xFF, 0xF8, 0x00, 0x07, 0xFF, 0xFF, 0xD0, 0x01, 0xFF, 0xFF, 0xF8, 0x00, 0xBF, 0xFF, 0xFF, 0x40,
0x00, 0x7F, 0xFF, 0xFC, 0x0B, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x0F, 0xFF, 0xFF,
0xF4, 0x00, 0x00, 0x07, 0xFF, 0xFD, 0x0F, 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x01, 0xFF, 0xFE, 0x1F,
0xFF, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x1F,
0xFF, 0xEB, 0xFF, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x00, 0xFF, 0xD0, 0x00, 0x00, 0x00,
0x00, 0x01, 0xFE, 0x00, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFD, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x1F, 0x97, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xD0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x7D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_18_boldGlyphs[] = {
@ -7765,6 +7785,7 @@ static const EpdGlyph bookerly_18_boldGlyphs[] = {
{ 22, 27, 38, 8, 27, 149, 112187 }, // ⊥
{ 8, 7, 24, 8, 14, 14, 112336 }, // ⋅
{ 34, 7, 38, 2, 14, 60, 112350 }, // ⋯
{ 36, 36, 38, 1, 32, 324, 112410 }, // <20>
};
static const EpdUnicodeInterval bookerly_18_boldIntervals[] = {
@ -7828,13 +7849,14 @@ static const EpdUnicodeInterval bookerly_18_boldIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_18_bold = {
bookerly_18_boldBitmaps,
bookerly_18_boldGlyphs,
bookerly_18_boldIntervals,
60,
61,
49,
40,
-10,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_18_bolditalicBitmaps[115736] = {
static const uint8_t bookerly_18_bolditalicBitmaps[116060] = {
0x00, 0x00, 0x54, 0x00, 0x07, 0xFE, 0x00, 0x0F, 0xFC, 0x00, 0x2F, 0xFC, 0x00, 0x3F, 0xF4, 0x00,
0x7F, 0xF0, 0x00, 0xBF, 0xF0, 0x00, 0xFF, 0xD0, 0x00, 0xFF, 0xC0, 0x00, 0xFF, 0xC0, 0x01, 0xFF,
0x80, 0x01, 0xFF, 0x40, 0x02, 0xFF, 0x00, 0x02, 0xFE, 0x00, 0x03, 0xFD, 0x00, 0x03, 0xFC, 0x00,
@ -7241,7 +7241,27 @@ static const uint8_t bookerly_18_bolditalicBitmaps[115736] = {
0x54, 0x00, 0x00, 0x54, 0x01, 0xFF, 0xC0, 0x00, 0xBF, 0xF0, 0x00, 0x3F, 0xF4, 0x3F, 0xFE, 0x00,
0x0F, 0xFF, 0x40, 0x0B, 0xFF, 0xC3, 0xFF, 0xF0, 0x01, 0xFF, 0xF4, 0x00, 0xBF, 0xFC, 0x3F, 0xFE,
0x00, 0x1F, 0xFF, 0x00, 0x0B, 0xFF, 0xC1, 0xFF, 0xC0, 0x00, 0xFF, 0xE0, 0x00, 0x3F, 0xF4, 0x01,
0x50, 0x00, 0x01, 0x50, 0x00, 0x00, 0x54, 0x00,
0x50, 0x00, 0x01, 0x50, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBE, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B,
0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xBE, 0x56, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xE0, 0x00, 0x2F, 0x80, 0x00, 0x00,
0x00, 0x00, 0x0B, 0xC0, 0x00, 0x07, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xC0, 0x00, 0x01, 0xF8,
0x00, 0x00, 0x00, 0x00, 0xBF, 0xD1, 0xA9, 0x00, 0xBE, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF,
0xD0, 0x3F, 0x80, 0x00, 0x00, 0x0B, 0xFF, 0xFF, 0xFF, 0xF0, 0x2F, 0xE0, 0x00, 0x00, 0x2F, 0xFF,
0xFF, 0xFF, 0xF8, 0x1F, 0xF8, 0x00, 0x00, 0xBF, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFE, 0x00, 0x02,
0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF, 0x80, 0x0B, 0xFF, 0xFF, 0xFF, 0xFF, 0xF4, 0x1F, 0xFF,
0xE0, 0x2F, 0xFF, 0xFF, 0xFF, 0xFE, 0x40, 0x1F, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0x00,
0x3F, 0xFF, 0xFD, 0x1F, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0xFF, 0xFF, 0xF4, 0x07, 0xFF, 0xFF, 0xF8,
0x00, 0x07, 0xFF, 0xFF, 0xD0, 0x01, 0xFF, 0xFF, 0xF8, 0x00, 0xBF, 0xFF, 0xFF, 0x40, 0x00, 0x7F,
0xFF, 0xFC, 0x0B, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x0F, 0xFF, 0xFF, 0xF4, 0x00,
0x00, 0x07, 0xFF, 0xFD, 0x0F, 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x01, 0xFF, 0xFE, 0x1F, 0xFF, 0xFF,
0x40, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xEB,
0xFF, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x00, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x01,
0xFE, 0x00, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFD, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x1F, 0x97, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xD0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7D, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_18_bolditalicGlyphs[] = {
@ -7973,6 +7993,7 @@ static const EpdGlyph bookerly_18_bolditalicGlyphs[] = {
{ 22, 27, 38, 8, 27, 149, 115513 }, // ⊥
{ 8, 7, 24, 8, 14, 14, 115662 }, // ⋅
{ 34, 7, 38, 2, 14, 60, 115676 }, // ⋯
{ 36, 36, 38, 1, 32, 324, 115736 }, // <20>
};
static const EpdUnicodeInterval bookerly_18_bolditalicIntervals[] = {
@ -8036,13 +8057,14 @@ static const EpdUnicodeInterval bookerly_18_bolditalicIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_18_bolditalic = {
bookerly_18_bolditalicBitmaps,
bookerly_18_bolditalicGlyphs,
bookerly_18_bolditalicIntervals,
60,
61,
49,
40,
-10,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_18_italicBitmaps[108017] = {
static const uint8_t bookerly_18_italicBitmaps[108341] = {
0x00, 0x00, 0x00, 0x00, 0xBE, 0x00, 0x2F, 0xD0, 0x03, 0xFC, 0x00, 0x7F, 0x80, 0x0B, 0xF0, 0x00,
0xFF, 0x00, 0x0F, 0xE0, 0x00, 0xFC, 0x00, 0x1F, 0xC0, 0x02, 0xF8, 0x00, 0x2F, 0x40, 0x03, 0xF0,
0x00, 0x3F, 0x00, 0x03, 0xE0, 0x00, 0x7D, 0x00, 0x07, 0xC0, 0x00, 0xBC, 0x00, 0x0F, 0x80, 0x00,
@ -6759,7 +6759,27 @@ static const uint8_t bookerly_18_italicBitmaps[108017] = {
0x50, 0x0A, 0x80, 0x00, 0x02, 0x90, 0x00, 0x01, 0xA0, 0x3F, 0xF0, 0x00, 0x1F, 0xF4, 0x00, 0x0B,
0xFC, 0x7F, 0xF0, 0x00, 0x2F, 0xF8, 0x00, 0x0F, 0xFD, 0x7F, 0xF0, 0x00, 0x2F, 0xF8, 0x00, 0x0F,
0xFD, 0x3F, 0xD0, 0x00, 0x0F, 0xF0, 0x00, 0x07, 0xF8, 0x05, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00,
0x40,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x2F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBE, 0x56, 0xFE, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0xE0, 0x00, 0x2F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x0B, 0xC0, 0x00, 0x07, 0xE0,
0x00, 0x00, 0x00, 0x00, 0x2F, 0xC0, 0x00, 0x01, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xBF, 0xD1, 0xA9,
0x00, 0xBE, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xD0, 0x3F, 0x80, 0x00, 0x00, 0x0B, 0xFF,
0xFF, 0xFF, 0xF0, 0x2F, 0xE0, 0x00, 0x00, 0x2F, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xF8, 0x00, 0x00,
0xBF, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFE, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF,
0x80, 0x0B, 0xFF, 0xFF, 0xFF, 0xFF, 0xF4, 0x1F, 0xFF, 0xE0, 0x2F, 0xFF, 0xFF, 0xFF, 0xFE, 0x40,
0x1F, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x3F, 0xFF, 0xFD, 0x1F, 0xFF, 0xFF, 0xF8,
0x00, 0x00, 0xFF, 0xFF, 0xF4, 0x07, 0xFF, 0xFF, 0xF8, 0x00, 0x07, 0xFF, 0xFF, 0xD0, 0x01, 0xFF,
0xFF, 0xF8, 0x00, 0xBF, 0xFF, 0xFF, 0x40, 0x00, 0x7F, 0xFF, 0xFC, 0x0B, 0xFF, 0xFF, 0xFD, 0x00,
0x00, 0x1F, 0xFF, 0xFC, 0x0F, 0xFF, 0xFF, 0xF4, 0x00, 0x00, 0x07, 0xFF, 0xFD, 0x0F, 0xFF, 0xFF,
0xD0, 0x00, 0x00, 0x01, 0xFF, 0xFE, 0x1F, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF,
0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xEB, 0xFF, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x07,
0xFF, 0x00, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFE, 0x00, 0xFF, 0x40, 0x00, 0x00, 0x00,
0x00, 0x00, 0x7F, 0x00, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x97, 0xF4, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x40,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x14, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_18_italicGlyphs[] = {
@ -7491,6 +7511,7 @@ static const EpdGlyph bookerly_18_italicGlyphs[] = {
{ 22, 27, 38, 8, 27, 149, 107811 }, // ⊥
{ 6, 6, 24, 9, 13, 9, 107960 }, // ⋅
{ 32, 6, 38, 3, 13, 48, 107969 }, // ⋯
{ 36, 36, 38, 1, 32, 324, 108017 }, // <20>
};
static const EpdUnicodeInterval bookerly_18_italicIntervals[] = {
@ -7554,13 +7575,14 @@ static const EpdUnicodeInterval bookerly_18_italicIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_18_italic = {
bookerly_18_italicBitmaps,
bookerly_18_italicGlyphs,
bookerly_18_italicIntervals,
60,
61,
49,
40,
-10,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t bookerly_18_regularBitmaps[105210] = {
static const uint8_t bookerly_18_regularBitmaps[105534] = {
0x05, 0x43, 0xF8, 0xBF, 0x4F, 0xF4, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFE,
0x0F, 0xE0, 0xBE, 0x0B, 0xE0, 0xBE, 0x0B, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x03, 0xE0, 0x3E, 0x01,
0x00, 0x00, 0x00, 0x00, 0x04, 0x03, 0xFC, 0xBF, 0xCF, 0xFC, 0xBF, 0xC1, 0x90, 0x00, 0x00, 0x00,
@ -6583,7 +6583,27 @@ static const uint8_t bookerly_18_regularBitmaps[105210] = {
0xF0, 0x1A, 0x47, 0xFC, 0xFF, 0xDF, 0xFD, 0x7F, 0xC0, 0x50, 0x0A, 0x80, 0x00, 0x02, 0x90, 0x00,
0x01, 0xA0, 0x3F, 0xF0, 0x00, 0x1F, 0xF4, 0x00, 0x0B, 0xFC, 0x7F, 0xF0, 0x00, 0x2F, 0xF8, 0x00,
0x0F, 0xFD, 0x7F, 0xF0, 0x00, 0x2F, 0xF8, 0x00, 0x0F, 0xFD, 0x3F, 0xD0, 0x00, 0x0F, 0xF0, 0x00,
0x07, 0xF8, 0x05, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40,
0x07, 0xF8, 0x05, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xBE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0B, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xFF, 0xF8, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xBE, 0x56, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xE0, 0x00, 0x2F, 0x80,
0x00, 0x00, 0x00, 0x00, 0x0B, 0xC0, 0x00, 0x07, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xC0, 0x00,
0x01, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xBF, 0xD1, 0xA9, 0x00, 0xBE, 0x00, 0x00, 0x00, 0x02, 0xFF,
0xFF, 0xFF, 0xD0, 0x3F, 0x80, 0x00, 0x00, 0x0B, 0xFF, 0xFF, 0xFF, 0xF0, 0x2F, 0xE0, 0x00, 0x00,
0x2F, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xF8, 0x00, 0x00, 0xBF, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFE,
0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF, 0x80, 0x0B, 0xFF, 0xFF, 0xFF, 0xFF, 0xF4,
0x1F, 0xFF, 0xE0, 0x2F, 0xFF, 0xFF, 0xFF, 0xFE, 0x40, 0x1F, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF,
0x80, 0x00, 0x3F, 0xFF, 0xFD, 0x1F, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0xFF, 0xFF, 0xF4, 0x07, 0xFF,
0xFF, 0xF8, 0x00, 0x07, 0xFF, 0xFF, 0xD0, 0x01, 0xFF, 0xFF, 0xF8, 0x00, 0xBF, 0xFF, 0xFF, 0x40,
0x00, 0x7F, 0xFF, 0xFC, 0x0B, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x0F, 0xFF, 0xFF,
0xF4, 0x00, 0x00, 0x07, 0xFF, 0xFD, 0x0F, 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x01, 0xFF, 0xFE, 0x1F,
0xFF, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x1F,
0xFF, 0xEB, 0xFF, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x00, 0xFF, 0xD0, 0x00, 0x00, 0x00,
0x00, 0x01, 0xFE, 0x00, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFD, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x1F, 0x97, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xD0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x7D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph bookerly_18_regularGlyphs[] = {
@ -7315,6 +7335,7 @@ static const EpdGlyph bookerly_18_regularGlyphs[] = {
{ 22, 27, 38, 8, 27, 149, 105004 }, // ⊥
{ 6, 6, 24, 9, 13, 9, 105153 }, // ⋅
{ 32, 6, 38, 3, 13, 48, 105162 }, // ⋯
{ 36, 36, 38, 1, 32, 324, 105210 }, // <20>
};
static const EpdUnicodeInterval bookerly_18_regularIntervals[] = {
@ -7378,13 +7399,14 @@ static const EpdUnicodeInterval bookerly_18_regularIntervals[] = {
{ 0x22A5, 0x22A5, 0x2D5 },
{ 0x22C5, 0x22C5, 0x2D6 },
{ 0x22EF, 0x22EF, 0x2D7 },
{ 0xFFFD, 0xFFFD, 0x2D8 },
};
static const EpdFontData bookerly_18_regular = {
bookerly_18_regularBitmaps,
bookerly_18_regularGlyphs,
bookerly_18_regularIntervals,
60,
61,
49,
40,
-10,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_12_boldBitmaps[49639] = {
static const uint8_t notosans_12_boldBitmaps[49772] = {
0x7F, 0xAF, 0xE7, 0xF9, 0xFD, 0x7F, 0x5F, 0xD7, 0xF4, 0xFD, 0x3F, 0x0F, 0xC3, 0xF0, 0xFC, 0x00,
0x00, 0x02, 0xE2, 0xFE, 0xBF, 0x9F, 0xD0, 0x40, 0x7F, 0x0F, 0xD7, 0xF0, 0xFD, 0x7F, 0x0F, 0xC3,
0xF0, 0xFC, 0x3F, 0x0F, 0xC3, 0xE0, 0xBC, 0x29, 0x06, 0x80, 0x00, 0x0F, 0x43, 0xD0, 0x00, 0x1F,
@ -3110,7 +3110,15 @@ static const uint8_t notosans_12_boldBitmaps[49639] = {
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xBF, 0x40, 0x00, 0x7F,
0xC0, 0x14, 0x3F, 0xFF, 0xF8, 0x0F, 0xFF, 0xF8, 0x02, 0xFF, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFC, 0x55, 0x55, 0x54, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF,
0xDF, 0xFF, 0xFF, 0xF5, 0xAA, 0xAA, 0xA8,
0xDF, 0xFF, 0xFF, 0xF5, 0xAA, 0xAA, 0xA8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F,
0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0x00, 0x00, 0x00, 0x03,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x3D, 0x00, 0x2F, 0x00, 0x00, 0x03, 0xE0, 0x00, 0x2F, 0x00, 0x00,
0x3F, 0xC6, 0xD0, 0x7F, 0x00, 0x03, 0xFF, 0xFF, 0xC1, 0xFF, 0x00, 0x3F, 0xFF, 0xFE, 0x07, 0xFF,
0x03, 0xFF, 0xFF, 0xE0, 0x3F, 0xFF, 0x2F, 0xFF, 0xFE, 0x03, 0xFF, 0xFE, 0x3F, 0xFF, 0xF0, 0x3F,
0xFF, 0xF0, 0x3F, 0xFF, 0x82, 0xFF, 0xFF, 0x00, 0x3F, 0xFF, 0x5F, 0xFF, 0xF0, 0x00, 0x3F, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x3F, 0xF5, 0xFF, 0xF0, 0x00, 0x00, 0x3F, 0x41, 0xFF, 0x00, 0x00, 0x00,
0x3D, 0x07, 0xF0, 0x00, 0x00, 0x00, 0x3E, 0xBF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF0, 0x00, 0x00,
0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_12_boldGlyphs[] = {
@ -3948,6 +3956,7 @@ static const EpdGlyph notosans_12_boldGlyphs[] = {
{ 12, 24, 14, 2, 21, 72, 49497 }, // ₿
{ 12, 19, 13, 1, 14, 57, 49569 }, // ⃀
{ 13, 4, 15, 1, 11, 13, 49626 }, //
{ 23, 23, 25, 1, 19, 133, 49639 }, // <20>
};
static const EpdUnicodeInterval notosans_12_boldIntervals[] = {
@ -3962,13 +3971,14 @@ static const EpdUnicodeInterval notosans_12_boldIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20C0, 0x320 },
{ 0x2212, 0x2212, 0x341 },
{ 0xFFFD, 0xFFFD, 0x342 },
};
static const EpdFontData notosans_12_bold = {
notosans_12_boldBitmaps,
notosans_12_boldGlyphs,
notosans_12_boldIntervals,
11,
12,
34,
27,
-8,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_12_bolditalicBitmaps[52863] = {
static const uint8_t notosans_12_bolditalicBitmaps[52996] = {
0x01, 0xFE, 0x02, 0xFE, 0x02, 0xFD, 0x03, 0xFC, 0x03, 0xFC, 0x03, 0xF8, 0x07, 0xF4, 0x0B, 0xF0,
0x0B, 0xF0, 0x0F, 0xE0, 0x0F, 0xD0, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x2F, 0x00, 0x7F, 0x80,
0xBF, 0x80, 0x7F, 0x00, 0x04, 0x00, 0x2F, 0x4B, 0xD3, 0xF4, 0xFD, 0x3F, 0x0F, 0xC3, 0xE0, 0xF8,
@ -3311,7 +3311,16 @@ static const uint8_t notosans_12_bolditalicBitmaps[52863] = {
0xFC, 0x0F, 0xE0, 0x3F, 0x82, 0xFD, 0x03, 0xFF, 0xFF, 0x40, 0x7F, 0xFF, 0x80, 0x0B, 0xFF, 0xFD,
0x00, 0xFF, 0x1F, 0xF0, 0x0F, 0xE0, 0x7F, 0x80, 0xFD, 0x03, 0xF8, 0x1F, 0xC0, 0x7F, 0x42, 0xFC,
0x1F, 0xF0, 0x3F, 0xFF, 0xFE, 0x03, 0xFF, 0xFF, 0x80, 0x3F, 0xFF, 0x90, 0x00, 0x34, 0xF0, 0x00,
0x07, 0x0E, 0x00, 0x00, 0x50, 0x40, 0x00, 0x05, 0x54, 0x3F, 0xFD, 0x7F, 0xFC, 0x7F, 0xFC,
0x07, 0x0E, 0x00, 0x00, 0x50, 0x40, 0x00, 0x05, 0x54, 0x3F, 0xFD, 0x7F, 0xFC, 0x7F, 0xFC, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x3F, 0xFF, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x3D, 0x00, 0x2F,
0x00, 0x00, 0x03, 0xE0, 0x00, 0x2F, 0x00, 0x00, 0x3F, 0xC6, 0xD0, 0x7F, 0x00, 0x03, 0xFF, 0xFF,
0xC1, 0xFF, 0x00, 0x3F, 0xFF, 0xFE, 0x07, 0xFF, 0x03, 0xFF, 0xFF, 0xE0, 0x3F, 0xFF, 0x2F, 0xFF,
0xFE, 0x03, 0xFF, 0xFE, 0x3F, 0xFF, 0xF0, 0x3F, 0xFF, 0xF0, 0x3F, 0xFF, 0x82, 0xFF, 0xFF, 0x00,
0x3F, 0xFF, 0x5F, 0xFF, 0xF0, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x3F, 0xF5, 0xFF, 0xF0,
0x00, 0x00, 0x3F, 0x41, 0xFF, 0x00, 0x00, 0x00, 0x3D, 0x07, 0xF0, 0x00, 0x00, 0x00, 0x3E, 0xBF,
0x00, 0x00, 0x00, 0x00, 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_12_bolditalicGlyphs[] = {
@ -4148,6 +4157,7 @@ static const EpdGlyph notosans_12_bolditalicGlyphs[] = {
{ 18, 21, 15, -1, 21, 95, 52676 }, // ₾
{ 14, 24, 14, 0, 21, 84, 52771 }, // ₿
{ 8, 4, 8, 0, 9, 8, 52855 }, //
{ 23, 23, 25, 1, 19, 133, 52863 }, // <20>
};
static const EpdUnicodeInterval notosans_12_bolditalicIntervals[] = {
@ -4162,13 +4172,14 @@ static const EpdUnicodeInterval notosans_12_bolditalicIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20BF, 0x320 },
{ 0x2212, 0x2212, 0x340 },
{ 0xFFFD, 0xFFFD, 0x341 },
};
static const EpdFontData notosans_12_bolditalic = {
notosans_12_bolditalicBitmaps,
notosans_12_bolditalicGlyphs,
notosans_12_bolditalicIntervals,
11,
12,
34,
27,
-8,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_12_italicBitmaps[48397] = {
static const uint8_t notosans_12_italicBitmaps[48530] = {
0x00, 0xB8, 0x03, 0xE0, 0x0F, 0x40, 0x7C, 0x02, 0xF0, 0x0F, 0x80, 0x3D, 0x00, 0xF0, 0x07, 0xC0,
0x1E, 0x00, 0xB4, 0x03, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x40, 0x7E, 0x01, 0xF4, 0x01,
0x00, 0x00, 0x1E, 0x0F, 0x0B, 0x87, 0xC2, 0xD1, 0xE0, 0xF0, 0xB4, 0x38, 0x2C, 0x0D, 0x0E, 0x02,
@ -3032,7 +3032,16 @@ static const uint8_t notosans_12_italicBitmaps[48397] = {
0x0F, 0x00, 0x2E, 0x07, 0xC0, 0x0B, 0x82, 0xE0, 0x07, 0xC0, 0xF9, 0x5B, 0xD0, 0x3F, 0xFF, 0x80,
0x0F, 0xAB, 0xF4, 0x07, 0xC0, 0x1F, 0x02, 0xE0, 0x03, 0xD0, 0xF4, 0x00, 0xF8, 0x3C, 0x00, 0x3D,
0x0F, 0x00, 0x1F, 0x07, 0xC0, 0x1F, 0x82, 0xFF, 0xFF, 0x80, 0xFF, 0xFE, 0x40, 0x03, 0x8B, 0x00,
0x00, 0xD3, 0x80, 0x00, 0x10, 0x40, 0x00, 0x2A, 0xA8, 0xFF, 0xE1, 0x55, 0x00,
0x00, 0xD3, 0x80, 0x00, 0x10, 0x40, 0x00, 0x2A, 0xA8, 0xFF, 0xE1, 0x55, 0x00, 0x00, 0x00, 0x03,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x3F, 0xFF, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x3D, 0x00, 0x2F, 0x00, 0x00,
0x03, 0xE0, 0x00, 0x2F, 0x00, 0x00, 0x3F, 0xC6, 0xD0, 0x7F, 0x00, 0x03, 0xFF, 0xFF, 0xC1, 0xFF,
0x00, 0x3F, 0xFF, 0xFE, 0x07, 0xFF, 0x03, 0xFF, 0xFF, 0xE0, 0x3F, 0xFF, 0x2F, 0xFF, 0xFE, 0x03,
0xFF, 0xFE, 0x3F, 0xFF, 0xF0, 0x3F, 0xFF, 0xF0, 0x3F, 0xFF, 0x82, 0xFF, 0xFF, 0x00, 0x3F, 0xFF,
0x5F, 0xFF, 0xF0, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x3F, 0xF5, 0xFF, 0xF0, 0x00, 0x00,
0x3F, 0x41, 0xFF, 0x00, 0x00, 0x00, 0x3D, 0x07, 0xF0, 0x00, 0x00, 0x00, 0x3E, 0xBF, 0x00, 0x00,
0x00, 0x00, 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00,
0x00, 0x00,
};
static const EpdGlyph notosans_12_italicGlyphs[] = {
@ -3869,6 +3878,7 @@ static const EpdGlyph notosans_12_italicGlyphs[] = {
{ 17, 21, 15, -1, 21, 90, 48223 }, // ₾
{ 13, 24, 14, 1, 21, 78, 48313 }, // ₿
{ 7, 3, 8, 0, 8, 6, 48391 }, //
{ 23, 23, 25, 1, 19, 133, 48397 }, // <20>
};
static const EpdUnicodeInterval notosans_12_italicIntervals[] = {
@ -3883,13 +3893,14 @@ static const EpdUnicodeInterval notosans_12_italicIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20BF, 0x320 },
{ 0x2212, 0x2212, 0x340 },
{ 0xFFFD, 0xFFFD, 0x341 },
};
static const EpdFontData notosans_12_italic = {
notosans_12_italicBitmaps,
notosans_12_italicGlyphs,
notosans_12_italicIntervals,
11,
12,
34,
27,
-8,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_12_regularBitmaps[45168] = {
static const uint8_t notosans_12_regularBitmaps[45301] = {
0x3E, 0x3E, 0x3E, 0x3E, 0x3D, 0x2D, 0x2D, 0x2D, 0x2D, 0x2C, 0x1C, 0x1C, 0x1C, 0x00, 0x00, 0x3E,
0x3F, 0x3E, 0x04, 0x7C, 0x2D, 0x7C, 0x2D, 0x3C, 0x2D, 0x38, 0x2D, 0x38, 0x1C, 0x38, 0x1C, 0x24,
0x08, 0x00, 0x0B, 0x01, 0xC0, 0x00, 0x0E, 0x02, 0xC0, 0x00, 0x0E, 0x03, 0xC0, 0x00, 0x1D, 0x03,
@ -2831,6 +2831,15 @@ static const uint8_t notosans_12_regularBitmaps[45168] = {
0x95, 0x93, 0xE0, 0x00, 0x7C, 0x00, 0x0B, 0x80, 0x00, 0xF8, 0x00, 0x0F, 0x40, 0x00, 0xB8, 0x00,
0x0B, 0x80, 0x00, 0x7C, 0x00, 0x03, 0xF0, 0x00, 0x0F, 0xEA, 0xF0, 0x2F, 0xFE, 0x00, 0x00, 0x00,
0x00, 0x00, 0xBF, 0xFF, 0xF6, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00,
0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x00,
0x00, 0x00, 0x00, 0x3F, 0xFF, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x3D, 0x00,
0x2F, 0x00, 0x00, 0x03, 0xE0, 0x00, 0x2F, 0x00, 0x00, 0x3F, 0xC6, 0xD0, 0x7F, 0x00, 0x03, 0xFF,
0xFF, 0xC1, 0xFF, 0x00, 0x3F, 0xFF, 0xFE, 0x07, 0xFF, 0x03, 0xFF, 0xFF, 0xE0, 0x3F, 0xFF, 0x2F,
0xFF, 0xFE, 0x03, 0xFF, 0xFE, 0x3F, 0xFF, 0xF0, 0x3F, 0xFF, 0xF0, 0x3F, 0xFF, 0x82, 0xFF, 0xFF,
0x00, 0x3F, 0xFF, 0x5F, 0xFF, 0xF0, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x3F, 0xF5, 0xFF,
0xF0, 0x00, 0x00, 0x3F, 0x41, 0xFF, 0x00, 0x00, 0x00, 0x3D, 0x07, 0xF0, 0x00, 0x00, 0x00, 0x3E,
0xBF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00,
0x00, 0x20, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_12_regularGlyphs[] = {
@ -3668,6 +3677,7 @@ static const EpdGlyph notosans_12_regularGlyphs[] = {
{ 12, 24, 14, 2, 21, 72, 45042 }, // ₿
{ 10, 18, 12, 1, 14, 45, 45114 }, // ⃀
{ 12, 3, 14, 1, 10, 9, 45159 }, //
{ 23, 23, 25, 1, 19, 133, 45168 }, // <20>
};
static const EpdUnicodeInterval notosans_12_regularIntervals[] = {
@ -3682,13 +3692,14 @@ static const EpdUnicodeInterval notosans_12_regularIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20C0, 0x320 },
{ 0x2212, 0x2212, 0x341 },
{ 0xFFFD, 0xFFFD, 0x342 },
};
static const EpdFontData notosans_12_regular = {
notosans_12_regularBitmaps,
notosans_12_regularGlyphs,
notosans_12_regularIntervals,
11,
12,
34,
27,
-8,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_14_boldBitmaps[67224] = {
static const uint8_t notosans_14_boldBitmaps[67413] = {
0x7F, 0xD7, 0xFE, 0x3F, 0xD3, 0xFD, 0x3F, 0xD3, 0xFD, 0x3F, 0xD3, 0xFC, 0x3F, 0xC3, 0xFC, 0x3F,
0xC3, 0xFC, 0x2F, 0xC2, 0xFC, 0x00, 0x00, 0x00, 0x05, 0x03, 0xFD, 0x7F, 0xE7, 0xFE, 0x3F, 0xC0,
0x50, 0x3F, 0xC3, 0xFC, 0x3F, 0xC3, 0xFC, 0x3F, 0xC3, 0xFC, 0x3F, 0x82, 0xFC, 0x3F, 0x82, 0xFC,
@ -4209,7 +4209,19 @@ static const uint8_t notosans_14_boldBitmaps[67224] = {
0x00, 0x00, 0x7F, 0xE0, 0x00, 0x03, 0xFF, 0x95, 0xB8, 0x1F, 0xFF, 0xFF, 0x80, 0x7F, 0xFF, 0xF8,
0x00, 0xBF, 0xFE, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x06, 0xAA, 0xAA, 0xA8, 0xBF, 0xFF,
0xFF, 0xCB, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xBF, 0xFF, 0xFF, 0xF6, 0xFF, 0xFF, 0xFF,
0xEB, 0xFF, 0xFF, 0xFF, 0x85, 0x55, 0x55, 0x54,
0xEB, 0xFF, 0xFF, 0xFF, 0x85, 0x55, 0x55, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0xD0, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xD0, 0x00,
0x00, 0x00, 0x0F, 0xE9, 0x6F, 0xD0, 0x00, 0x00, 0x00, 0xF4, 0x00, 0x0F, 0xD0, 0x00, 0x00, 0x0F,
0xC0, 0x00, 0x0F, 0xD0, 0x00, 0x00, 0xFF, 0x86, 0xE0, 0x2F, 0xD0, 0x00, 0x0F, 0xFF, 0xFF, 0xC0,
0x7F, 0xD0, 0x00, 0xFF, 0xFF, 0xFF, 0x02, 0xFF, 0xD0, 0x0F, 0xFF, 0xFF, 0xF4, 0x0F, 0xFF, 0xD0,
0xFF, 0xFF, 0xFF, 0x40, 0xBF, 0xFF, 0xD7, 0xFF, 0xFF, 0xF4, 0x0B, 0xFF, 0xFF, 0x87, 0xFF, 0xFF,
0x80, 0xFF, 0xFF, 0xF8, 0x07, 0xFF, 0xFE, 0x07, 0xFF, 0xFF, 0x80, 0x07, 0xFF, 0xF8, 0x2F, 0xFF,
0xF8, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00,
0x07, 0xFD, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x07, 0xF0, 0x0F, 0xF8, 0x00, 0x00, 0x00, 0x07, 0xD0,
0x3F, 0x80, 0x00, 0x00, 0x00, 0x07, 0xD7, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x80, 0x00,
0x00, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00,
0x00, 0x04, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_14_boldGlyphs[] = {
@ -5047,6 +5059,7 @@ static const EpdGlyph notosans_14_boldGlyphs[] = {
{ 14, 27, 17, 2, 24, 95, 67033 }, // ₿
{ 14, 23, 15, 1, 17, 81, 67128 }, // ⃀
{ 15, 4, 17, 1, 12, 15, 67209 }, //
{ 27, 28, 29, 1, 23, 189, 67224 }, // <20>
};
static const EpdUnicodeInterval notosans_14_boldIntervals[] = {
@ -5061,13 +5074,14 @@ static const EpdUnicodeInterval notosans_14_boldIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20C0, 0x320 },
{ 0x2212, 0x2212, 0x341 },
{ 0xFFFD, 0xFFFD, 0x342 },
};
static const EpdFontData notosans_14_bold = {
notosans_14_boldBitmaps,
notosans_14_boldGlyphs,
notosans_14_boldIntervals,
11,
12,
40,
32,
-9,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_14_bolditalicBitmaps[71405] = {
static const uint8_t notosans_14_bolditalicBitmaps[71594] = {
0x00, 0xBF, 0xC0, 0x0F, 0xFC, 0x00, 0xFF, 0x80, 0x0F, 0xF4, 0x01, 0xFF, 0x00, 0x2F, 0xF0, 0x02,
0xFE, 0x00, 0x3F, 0xD0, 0x03, 0xFC, 0x00, 0x3F, 0xC0, 0x07, 0xF8, 0x00, 0xBF, 0x40, 0x0F, 0xF0,
0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0xFC, 0x00, 0xBF, 0xD0, 0x0B,
@ -4470,7 +4470,19 @@ static const uint8_t notosans_14_bolditalicBitmaps[71405] = {
0x00, 0x1F, 0xE0, 0x7F, 0xD0, 0x0B, 0xF4, 0x0F, 0xF8, 0x03, 0xFC, 0x03, 0xFE, 0x00, 0xFF, 0x00,
0xFF, 0x80, 0x7F, 0xC0, 0xBF, 0xD0, 0x1F, 0xFA, 0xFF, 0xF0, 0x0B, 0xFF, 0xFF, 0xF4, 0x03, 0xFF,
0xFF, 0xF4, 0x00, 0xFF, 0xFF, 0xD0, 0x00, 0x03, 0xC3, 0xC0, 0x00, 0x00, 0xF0, 0xF0, 0x00, 0x00,
0x38, 0x38, 0x00, 0x00, 0x2A, 0xAA, 0x4F, 0xFF, 0xE3, 0xFF, 0xF5, 0xFF, 0xFC,
0x38, 0x38, 0x00, 0x00, 0x2A, 0xAA, 0x4F, 0xFF, 0xE3, 0xFF, 0xF5, 0xFF, 0xFC, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xD0, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xD0, 0x00, 0x00, 0x00,
0x00, 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x0F, 0xE9, 0x6F, 0xD0, 0x00, 0x00, 0x00, 0xF4, 0x00,
0x0F, 0xD0, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x0F, 0xD0, 0x00, 0x00, 0xFF, 0x86, 0xE0, 0x2F, 0xD0,
0x00, 0x0F, 0xFF, 0xFF, 0xC0, 0x7F, 0xD0, 0x00, 0xFF, 0xFF, 0xFF, 0x02, 0xFF, 0xD0, 0x0F, 0xFF,
0xFF, 0xF4, 0x0F, 0xFF, 0xD0, 0xFF, 0xFF, 0xFF, 0x40, 0xBF, 0xFF, 0xD7, 0xFF, 0xFF, 0xF4, 0x0B,
0xFF, 0xFF, 0x87, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xF8, 0x07, 0xFF, 0xFE, 0x07, 0xFF, 0xFF, 0x80,
0x07, 0xFF, 0xF8, 0x2F, 0xFF, 0xF8, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x07, 0xFF,
0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x07, 0xFD, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x07, 0xF0, 0x0F, 0xF8,
0x00, 0x00, 0x00, 0x07, 0xD0, 0x3F, 0x80, 0x00, 0x00, 0x00, 0x07, 0xD7, 0xF8, 0x00, 0x00, 0x00,
0x00, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_14_bolditalicGlyphs[] = {
@ -5307,6 +5319,7 @@ static const EpdGlyph notosans_14_bolditalicGlyphs[] = {
{ 20, 24, 18, -1, 24, 120, 71161 }, // ₾
{ 17, 27, 16, 0, 24, 115, 71281 }, // ₿
{ 9, 4, 9, 0, 10, 9, 71396 }, //
{ 27, 28, 29, 1, 23, 189, 71405 }, // <20>
};
static const EpdUnicodeInterval notosans_14_bolditalicIntervals[] = {
@ -5321,13 +5334,14 @@ static const EpdUnicodeInterval notosans_14_bolditalicIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20BF, 0x320 },
{ 0x2212, 0x2212, 0x340 },
{ 0xFFFD, 0xFFFD, 0x341 },
};
static const EpdFontData notosans_14_bolditalic = {
notosans_14_bolditalicBitmaps,
notosans_14_bolditalicGlyphs,
notosans_14_bolditalicIntervals,
11,
12,
40,
32,
-9,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_14_italicBitmaps[65135] = {
static const uint8_t notosans_14_italicBitmaps[65324] = {
0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0B, 0xD0, 0x02, 0xF0, 0x00, 0xFC, 0x00, 0x3E, 0x00, 0x0F, 0x40,
0x07, 0xC0, 0x02, 0xF0, 0x00, 0xF8, 0x00, 0x3D, 0x00, 0x0F, 0x00, 0x07, 0xC0, 0x01, 0xE0, 0x00,
0xB4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x03, 0xF0, 0x01, 0xFC, 0x00, 0x7F, 0x00, 0x05,
@ -4078,7 +4078,19 @@ static const uint8_t notosans_14_italicBitmaps[65135] = {
0x0B, 0xFF, 0xFD, 0x00, 0x0F, 0xC0, 0x6F, 0x80, 0x0F, 0x80, 0x0B, 0xD0, 0x0F, 0x40, 0x03, 0xE0,
0x1F, 0x00, 0x03, 0xE0, 0x2F, 0x00, 0x03, 0xE0, 0x3E, 0x00, 0x07, 0xD0, 0x3E, 0x00, 0x0F, 0xC0,
0x7D, 0x01, 0xBF, 0x40, 0x7F, 0xFF, 0xFD, 0x00, 0xBF, 0xFF, 0xD0, 0x00, 0x07, 0x4B, 0x00, 0x00,
0x0B, 0x0F, 0x00, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x3F, 0xFF, 0x0F, 0xFF, 0xC1, 0x55, 0x50,
0x0B, 0x0F, 0x00, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x3F, 0xFF, 0x0F, 0xFF, 0xC1, 0x55, 0x50, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F,
0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xD0, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x0F, 0xE9, 0x6F, 0xD0, 0x00, 0x00, 0x00,
0xF4, 0x00, 0x0F, 0xD0, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x0F, 0xD0, 0x00, 0x00, 0xFF, 0x86, 0xE0,
0x2F, 0xD0, 0x00, 0x0F, 0xFF, 0xFF, 0xC0, 0x7F, 0xD0, 0x00, 0xFF, 0xFF, 0xFF, 0x02, 0xFF, 0xD0,
0x0F, 0xFF, 0xFF, 0xF4, 0x0F, 0xFF, 0xD0, 0xFF, 0xFF, 0xFF, 0x40, 0xBF, 0xFF, 0xD7, 0xFF, 0xFF,
0xF4, 0x0B, 0xFF, 0xFF, 0x87, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xF8, 0x07, 0xFF, 0xFE, 0x07, 0xFF,
0xFF, 0x80, 0x07, 0xFF, 0xF8, 0x2F, 0xFF, 0xF8, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00,
0x07, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x07, 0xFD, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x07, 0xF0,
0x0F, 0xF8, 0x00, 0x00, 0x00, 0x07, 0xD0, 0x3F, 0x80, 0x00, 0x00, 0x00, 0x07, 0xD7, 0xF8, 0x00,
0x00, 0x00, 0x00, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00,
0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_14_italicGlyphs[] = {
@ -4915,6 +4927,7 @@ static const EpdGlyph notosans_14_italicGlyphs[] = {
{ 20, 24, 17, -1, 24, 120, 64900 }, // ₾
{ 16, 27, 16, 1, 24, 108, 65020 }, // ₿
{ 9, 3, 9, 0, 9, 7, 65128 }, //
{ 27, 28, 29, 1, 23, 189, 65135 }, // <20>
};
static const EpdUnicodeInterval notosans_14_italicIntervals[] = {
@ -4929,13 +4942,14 @@ static const EpdUnicodeInterval notosans_14_italicIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20BF, 0x320 },
{ 0x2212, 0x2212, 0x340 },
{ 0xFFFD, 0xFFFD, 0x341 },
};
static const EpdFontData notosans_14_italic = {
notosans_14_italicBitmaps,
notosans_14_italicGlyphs,
notosans_14_italicIntervals,
11,
12,
40,
32,
-9,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_14_regularBitmaps[61202] = {
static const uint8_t notosans_14_regularBitmaps[61391] = {
0xBD, 0xBD, 0xBD, 0x7D, 0x7D, 0x7C, 0x7C, 0x7C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x00,
0x00, 0x14, 0xBE, 0xFE, 0xBD, 0x14, 0x3E, 0x0B, 0xCF, 0x82, 0xF3, 0xD0, 0xBC, 0xF4, 0x2E, 0x3D,
0x07, 0x8B, 0x41, 0xE2, 0xC0, 0x78, 0x60, 0x09, 0x00, 0x02, 0xC0, 0x1D, 0x00, 0x00, 0x0F, 0x00,
@ -3833,7 +3833,18 @@ static const uint8_t notosans_14_regularBitmaps[61202] = {
0x00, 0x00, 0xBC, 0x00, 0x00, 0x7D, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x1F, 0xD0,
0x05, 0x07, 0xFF, 0xFE, 0x00, 0xBF, 0xFD, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFE,
0x7F, 0xFF, 0xFE, 0x15, 0x55, 0x54, 0x15, 0x55, 0x55, 0x52, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF,
0xFF, 0x00,
0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0F, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x0F,
0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x0F, 0xE9, 0x6F, 0xD0,
0x00, 0x00, 0x00, 0xF4, 0x00, 0x0F, 0xD0, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x0F, 0xD0, 0x00, 0x00,
0xFF, 0x86, 0xE0, 0x2F, 0xD0, 0x00, 0x0F, 0xFF, 0xFF, 0xC0, 0x7F, 0xD0, 0x00, 0xFF, 0xFF, 0xFF,
0x02, 0xFF, 0xD0, 0x0F, 0xFF, 0xFF, 0xF4, 0x0F, 0xFF, 0xD0, 0xFF, 0xFF, 0xFF, 0x40, 0xBF, 0xFF,
0xD7, 0xFF, 0xFF, 0xF4, 0x0B, 0xFF, 0xFF, 0x87, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xF8, 0x07, 0xFF,
0xFE, 0x07, 0xFF, 0xFF, 0x80, 0x07, 0xFF, 0xF8, 0x2F, 0xFF, 0xF8, 0x00, 0x07, 0xFF, 0xFF, 0xFF,
0xFF, 0x80, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x07, 0xFD, 0x07, 0xFF, 0x80, 0x00,
0x00, 0x07, 0xF0, 0x0F, 0xF8, 0x00, 0x00, 0x00, 0x07, 0xD0, 0x3F, 0x80, 0x00, 0x00, 0x00, 0x07,
0xD7, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x07, 0xF8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_14_regularGlyphs[] = {
@ -4671,6 +4682,7 @@ static const EpdGlyph notosans_14_regularGlyphs[] = {
{ 14, 27, 17, 2, 24, 95, 61032 }, // ₿
{ 12, 21, 14, 1, 16, 63, 61127 }, // ⃀
{ 15, 3, 17, 1, 12, 12, 61190 }, //
{ 27, 28, 29, 1, 23, 189, 61202 }, // <20>
};
static const EpdUnicodeInterval notosans_14_regularIntervals[] = {
@ -4685,13 +4697,14 @@ static const EpdUnicodeInterval notosans_14_regularIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20C0, 0x320 },
{ 0x2212, 0x2212, 0x341 },
{ 0xFFFD, 0xFFFD, 0x342 },
};
static const EpdFontData notosans_14_regular = {
notosans_14_regularBitmaps,
notosans_14_regularGlyphs,
notosans_14_regularIntervals,
11,
12,
40,
32,
-9,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_16_boldBitmaps[86944] = {
static const uint8_t notosans_16_boldBitmaps[87192] = {
0x3F, 0xF4, 0xFF, 0xD3, 0xFF, 0x4F, 0xFD, 0x3F, 0xF4, 0xFF, 0xD3, 0xFF, 0x0F, 0xFC, 0x2F, 0xF0,
0xBF, 0xC2, 0xFF, 0x0B, 0xFC, 0x1F, 0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0xF8, 0x05, 0x40, 0x00, 0x00,
0x00, 0x07, 0xF8, 0x3F, 0xF4, 0xFF, 0xE3, 0xFF, 0x4B, 0xFC, 0x01, 0x40, 0xFF, 0x82, 0xFE, 0xFF,
@ -5442,6 +5442,22 @@ static const uint8_t notosans_16_boldBitmaps[86944] = {
0xFF, 0xE0, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x55, 0x55, 0x54, 0x7F, 0xFF,
0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xFC, 0x2A, 0xAA, 0xAA, 0xAA, 0x5F, 0xFF,
0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFD, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00,
0x00, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE4, 0x06, 0xFD, 0x00, 0x00, 0x00, 0x00,
0xF8, 0x00, 0x00, 0xFD, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0xFD, 0x00, 0x00, 0x00, 0xFF,
0xC1, 0xA8, 0x03, 0xFD, 0x00, 0x00, 0x0F, 0xFF, 0xBF, 0xF4, 0x0B, 0xFD, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xE0, 0x2F, 0xFD, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFD, 0x00, 0xFF, 0xFF, 0xFF,
0xF0, 0x07, 0xFF, 0xFD, 0x0F, 0xFF, 0xFF, 0xFF, 0x00, 0x7F, 0xFF, 0xFD, 0x3F, 0xFF, 0xFF, 0xF0,
0x07, 0xFF, 0xFF, 0xF4, 0x3F, 0xFF, 0xFF, 0x80, 0xBF, 0xFF, 0xFF, 0x40, 0x3F, 0xFF, 0xFD, 0x03,
0xFF, 0xFF, 0xF4, 0x00, 0x3F, 0xFF, 0xF4, 0x1F, 0xFF, 0xFF, 0x40, 0x00, 0x3F, 0xFF, 0xFF, 0xFF,
0xFF, 0xF4, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x3F, 0xFF, 0x5B, 0xFF,
0xF4, 0x00, 0x00, 0x00, 0x3F, 0xF0, 0x0F, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x3F, 0xC0, 0x2F, 0xF4,
0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x5B, 0xF4, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF4, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_16_boldGlyphs[] = {
@ -6279,6 +6295,7 @@ static const EpdGlyph notosans_16_boldGlyphs[] = {
{ 15, 32, 19, 3, 28, 120, 86702 }, // ₿
{ 16, 25, 17, 1, 19, 100, 86822 }, // ⃀
{ 17, 5, 19, 1, 14, 22, 86922 }, //
{ 31, 32, 33, 1, 26, 248, 86944 }, // <20>
};
static const EpdUnicodeInterval notosans_16_boldIntervals[] = {
@ -6293,13 +6310,14 @@ static const EpdUnicodeInterval notosans_16_boldIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20C0, 0x320 },
{ 0x2212, 0x2212, 0x341 },
{ 0xFFFD, 0xFFFD, 0x342 },
};
static const EpdFontData notosans_16_bold = {
notosans_16_boldBitmaps,
notosans_16_boldGlyphs,
notosans_16_boldIntervals,
11,
12,
45,
36,
-10,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_16_bolditalicBitmaps[92423] = {
static const uint8_t notosans_16_bolditalicBitmaps[92671] = {
0x00, 0x3F, 0xF4, 0x01, 0xFF, 0xD0, 0x07, 0xFF, 0x00, 0x2F, 0xF8, 0x00, 0xFF, 0xD0, 0x03, 0xFF,
0x00, 0x0F, 0xFC, 0x00, 0x7F, 0xE0, 0x01, 0xFF, 0x40, 0x0B, 0xFC, 0x00, 0x3F, 0xF0, 0x00, 0xFF,
0x80, 0x03, 0xFD, 0x00, 0x1F, 0xF0, 0x00, 0x7F, 0xC0, 0x02, 0xFE, 0x00, 0x01, 0x50, 0x00, 0x00,
@ -5784,7 +5784,22 @@ static const uint8_t notosans_16_bolditalicBitmaps[92423] = {
0xFE, 0x07, 0xFF, 0xC0, 0x1F, 0xFF, 0xFF, 0xFE, 0x00, 0xBF, 0xFF, 0xFF, 0xF0, 0x03, 0xFF, 0xFF,
0xFE, 0x00, 0x0F, 0xFF, 0xFF, 0x40, 0x00, 0x01, 0xF0, 0xF4, 0x00, 0x00, 0x07, 0x83, 0xC0, 0x00,
0x00, 0x2E, 0x1F, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x2F, 0xFF, 0xF3, 0xFF, 0xFE, 0x3F,
0xFF, 0xD7, 0xFF, 0xFD, 0x00, 0x00, 0x00,
0xFF, 0xD7, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0F, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0F, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x0F,
0xE4, 0x06, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xFD, 0x00, 0x00, 0x00, 0x0F, 0xE0,
0x00, 0x00, 0xFD, 0x00, 0x00, 0x00, 0xFF, 0xC1, 0xA8, 0x03, 0xFD, 0x00, 0x00, 0x0F, 0xFF, 0xBF,
0xF4, 0x0B, 0xFD, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xE0, 0x2F, 0xFD, 0x00, 0x0F, 0xFF, 0xFF, 0xFF,
0x00, 0xFF, 0xFD, 0x00, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFD, 0x0F, 0xFF, 0xFF, 0xFF, 0x00,
0x7F, 0xFF, 0xFD, 0x3F, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xF4, 0x3F, 0xFF, 0xFF, 0x80, 0xBF,
0xFF, 0xFF, 0x40, 0x3F, 0xFF, 0xFD, 0x03, 0xFF, 0xFF, 0xF4, 0x00, 0x3F, 0xFF, 0xF4, 0x1F, 0xFF,
0xFF, 0x40, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF4, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF,
0x40, 0x00, 0x00, 0x3F, 0xFF, 0x5B, 0xFF, 0xF4, 0x00, 0x00, 0x00, 0x3F, 0xF0, 0x0F, 0xFF, 0x40,
0x00, 0x00, 0x00, 0x3F, 0xC0, 0x2F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xFF, 0x40, 0x00,
0x00, 0x00, 0x00, 0x3F, 0x5B, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0x40, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x40, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_16_bolditalicGlyphs[] = {
@ -6621,6 +6636,7 @@ static const EpdGlyph notosans_16_bolditalicGlyphs[] = {
{ 23, 28, 20, -1, 28, 161, 92097 }, // ₾
{ 19, 32, 18, 0, 28, 152, 92258 }, // ₿
{ 10, 5, 11, 0, 11, 13, 92410 }, //
{ 31, 32, 33, 1, 26, 248, 92423 }, // <20>
};
static const EpdUnicodeInterval notosans_16_bolditalicIntervals[] = {
@ -6635,13 +6651,14 @@ static const EpdUnicodeInterval notosans_16_bolditalicIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20BF, 0x320 },
{ 0x2212, 0x2212, 0x340 },
{ 0xFFFD, 0xFFFD, 0x341 },
};
static const EpdFontData notosans_16_bolditalic = {
notosans_16_bolditalicBitmaps,
notosans_16_bolditalicGlyphs,
notosans_16_bolditalicIntervals,
11,
12,
45,
36,
-10,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_16_italicBitmaps[83982] = {
static const uint8_t notosans_16_italicBitmaps[84230] = {
0x00, 0x1F, 0xC0, 0x02, 0xFC, 0x00, 0x3F, 0x80, 0x03, 0xF4, 0x00, 0x3F, 0x00, 0x07, 0xF0, 0x00,
0xBE, 0x00, 0x0B, 0xD0, 0x00, 0xFC, 0x00, 0x0F, 0xC0, 0x00, 0xF8, 0x00, 0x1F, 0x40, 0x02, 0xF0,
0x00, 0x3F, 0x00, 0x03, 0xE0, 0x00, 0x3D, 0x00, 0x07, 0xC0, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00,
@ -5256,7 +5256,23 @@ static const uint8_t notosans_16_italicBitmaps[83982] = {
0x00, 0x7F, 0x01, 0xF8, 0x00, 0x07, 0xE0, 0x1F, 0x40, 0x00, 0xBE, 0x02, 0xF4, 0x00, 0x0F, 0xC0,
0x3F, 0x00, 0x07, 0xF8, 0x03, 0xFA, 0xAB, 0xFF, 0x00, 0x7F, 0xFF, 0xFF, 0x80, 0x0B, 0xFF, 0xFF,
0x80, 0x00, 0x03, 0xC2, 0xD0, 0x00, 0x00, 0x3C, 0x3C, 0x00, 0x00, 0x07, 0x83, 0xC0, 0x00, 0x00,
0x10, 0x10, 0x00, 0x00, 0x05, 0x55, 0x42, 0xFF, 0xFC, 0x3F, 0xFF, 0xC1, 0x55, 0x54,
0x10, 0x10, 0x00, 0x00, 0x05, 0x55, 0x42, 0xFF, 0xFC, 0x3F, 0xFF, 0xC1, 0x55, 0x54, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE4, 0x06, 0xFD, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00,
0x00, 0xFD, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0xFD, 0x00, 0x00, 0x00, 0xFF, 0xC1, 0xA8,
0x03, 0xFD, 0x00, 0x00, 0x0F, 0xFF, 0xBF, 0xF4, 0x0B, 0xFD, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xE0,
0x2F, 0xFD, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFD, 0x00, 0xFF, 0xFF, 0xFF, 0xF0, 0x07,
0xFF, 0xFD, 0x0F, 0xFF, 0xFF, 0xFF, 0x00, 0x7F, 0xFF, 0xFD, 0x3F, 0xFF, 0xFF, 0xF0, 0x07, 0xFF,
0xFF, 0xF4, 0x3F, 0xFF, 0xFF, 0x80, 0xBF, 0xFF, 0xFF, 0x40, 0x3F, 0xFF, 0xFD, 0x03, 0xFF, 0xFF,
0xF4, 0x00, 0x3F, 0xFF, 0xF4, 0x1F, 0xFF, 0xFF, 0x40, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF4,
0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x3F, 0xFF, 0x5B, 0xFF, 0xF4, 0x00,
0x00, 0x00, 0x3F, 0xF0, 0x0F, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x3F, 0xC0, 0x2F, 0xF4, 0x00, 0x00,
0x00, 0x00, 0x3F, 0x00, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x5B, 0xF4, 0x00, 0x00, 0x00,
0x00, 0x00, 0x3F, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF4, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x3F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_16_italicGlyphs[] = {
@ -6093,6 +6109,7 @@ static const EpdGlyph notosans_16_italicGlyphs[] = {
{ 22, 28, 20, -1, 28, 154, 83674 }, // ₾
{ 18, 32, 18, 1, 28, 144, 83828 }, // ₿
{ 10, 4, 10, 0, 11, 10, 83972 }, //
{ 31, 32, 33, 1, 26, 248, 83982 }, // <20>
};
static const EpdUnicodeInterval notosans_16_italicIntervals[] = {
@ -6107,13 +6124,14 @@ static const EpdUnicodeInterval notosans_16_italicIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20BF, 0x320 },
{ 0x2212, 0x2212, 0x340 },
{ 0xFFFD, 0xFFFD, 0x341 },
};
static const EpdFontData notosans_16_italic = {
notosans_16_italicBitmaps,
notosans_16_italicGlyphs,
notosans_16_italicIntervals,
11,
12,
45,
36,
-10,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_16_regularBitmaps[78480] = {
static const uint8_t notosans_16_regularBitmaps[78728] = {
0x3F, 0x0F, 0xC3, 0xF0, 0xFC, 0x3F, 0x0F, 0xC3, 0xF0, 0xFC, 0x3F, 0x0F, 0xC3, 0xF0, 0xF8, 0x2E,
0x0B, 0x82, 0xE0, 0xB8, 0x2D, 0x01, 0x00, 0x00, 0x00, 0x3F, 0x2F, 0xDB, 0xF9, 0xFC, 0x04, 0x00,
0xBC, 0x0B, 0xDB, 0xC0, 0xBD, 0xBC, 0x0B, 0xDB, 0xC0, 0x7C, 0xBC, 0x07, 0xC7, 0xC0, 0x7C, 0x7C,
@ -4913,6 +4913,22 @@ static const uint8_t notosans_16_regularBitmaps[78480] = {
0x2F, 0x80, 0x00, 0x01, 0xFD, 0x00, 0x00, 0x0B, 0xF9, 0x06, 0xD0, 0x2F, 0xFF, 0xFD, 0x00, 0x7F,
0xFF, 0x80, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x01, 0x55, 0x55, 0x54, 0x3F, 0xFF, 0xFF, 0xE3,
0xFF, 0xFF, 0xFD, 0x7F, 0xFF, 0xFF, 0xFF, 0x5F, 0xFF, 0xFF, 0xFF, 0xD1, 0x55, 0x55, 0x55, 0x50,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFD, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00,
0x00, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE4, 0x06, 0xFD, 0x00, 0x00, 0x00, 0x00,
0xF8, 0x00, 0x00, 0xFD, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0xFD, 0x00, 0x00, 0x00, 0xFF,
0xC1, 0xA8, 0x03, 0xFD, 0x00, 0x00, 0x0F, 0xFF, 0xBF, 0xF4, 0x0B, 0xFD, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xE0, 0x2F, 0xFD, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFD, 0x00, 0xFF, 0xFF, 0xFF,
0xF0, 0x07, 0xFF, 0xFD, 0x0F, 0xFF, 0xFF, 0xFF, 0x00, 0x7F, 0xFF, 0xFD, 0x3F, 0xFF, 0xFF, 0xF0,
0x07, 0xFF, 0xFF, 0xF4, 0x3F, 0xFF, 0xFF, 0x80, 0xBF, 0xFF, 0xFF, 0x40, 0x3F, 0xFF, 0xFD, 0x03,
0xFF, 0xFF, 0xF4, 0x00, 0x3F, 0xFF, 0xF4, 0x1F, 0xFF, 0xFF, 0x40, 0x00, 0x3F, 0xFF, 0xFF, 0xFF,
0xFF, 0xF4, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0x3F, 0xFF, 0x5B, 0xFF,
0xF4, 0x00, 0x00, 0x00, 0x3F, 0xF0, 0x0F, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x3F, 0xC0, 0x2F, 0xF4,
0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x5B, 0xF4, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF4, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_16_regularGlyphs[] = {
@ -5750,6 +5766,7 @@ static const EpdGlyph notosans_16_regularGlyphs[] = {
{ 15, 32, 19, 3, 28, 120, 78263 }, // ₿
{ 14, 24, 16, 1, 19, 84, 78383 }, // ⃀
{ 17, 3, 19, 1, 13, 13, 78467 }, //
{ 31, 32, 33, 1, 26, 248, 78480 }, // <20>
};
static const EpdUnicodeInterval notosans_16_regularIntervals[] = {
@ -5764,13 +5781,14 @@ static const EpdUnicodeInterval notosans_16_regularIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20C0, 0x320 },
{ 0x2212, 0x2212, 0x341 },
{ 0xFFFD, 0xFFFD, 0x342 },
};
static const EpdFontData notosans_16_regular = {
notosans_16_regularBitmaps,
notosans_16_regularGlyphs,
notosans_16_regularIntervals,
11,
12,
45,
36,
-10,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_18_boldBitmaps[108125] = {
static const uint8_t notosans_18_boldBitmaps[108432] = {
0xBF, 0xF7, 0xFF, 0xDF, 0xFF, 0x6F, 0xFD, 0xBF, 0xF2, 0xFF, 0xCB, 0xFF, 0x2F, 0xFC, 0x7F, 0xF1,
0xFF, 0xC7, 0xFF, 0x1F, 0xFC, 0x7F, 0xF0, 0xFF, 0xC3, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x81,
0x54, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x0B, 0xFF, 0x3F, 0xFD, 0xFF, 0xF7, 0xFF, 0xD7, 0xFE, 0x01,
@ -6765,7 +6765,26 @@ static const uint8_t notosans_18_boldBitmaps[108125] = {
0xF8, 0x00, 0x1F, 0xFF, 0xFD, 0x00, 0x00, 0x01, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF,
0xC2, 0xAA, 0xAA, 0xAA, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF,
0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xF8,
0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xE0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFA, 0xFF,
0xE0, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x2F, 0xE0, 0x00, 0x00, 0x00, 0x01, 0xFC, 0x00,
0x00, 0x0F, 0xE0, 0x00, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x01, 0xFF,
0xD0, 0x6A, 0x00, 0x3F, 0xE0, 0x00, 0x00, 0x1F, 0xFF, 0xDF, 0xFE, 0x00, 0xFF, 0xE0, 0x00, 0x01,
0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0xFF, 0xE0, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xE0,
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x7F, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0xFF,
0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x3F, 0xFF, 0xFF, 0xE2, 0xFF, 0xFF, 0xFF, 0xF0, 0x07,
0xFF, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0x40, 0x7F, 0xFF, 0xFF, 0xF0, 0x02, 0xFF, 0xFF, 0xFC,
0x03, 0xFF, 0xFF, 0xFF, 0x00, 0x02, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x02, 0xFF,
0xFF, 0xEA, 0xBF, 0xFF, 0xFF, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00,
0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xF4, 0x0F, 0xFF, 0xF0, 0x00,
0x00, 0x00, 0x02, 0xFF, 0x80, 0x1F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFE, 0x00, 0x7F, 0xF0,
0x00, 0x00, 0x00, 0x00, 0x02, 0xFC, 0x02, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xF9, 0x6F,
0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_18_boldGlyphs[] = {
@ -7603,6 +7622,7 @@ static const EpdGlyph notosans_18_boldGlyphs[] = {
{ 18, 35, 21, 3, 31, 158, 107817 }, // ₿
{ 18, 28, 20, 1, 21, 126, 107975 }, // ⃀
{ 19, 5, 22, 1, 16, 24, 108101 }, //
{ 35, 35, 38, 1, 29, 307, 108125 }, // <20>
};
static const EpdUnicodeInterval notosans_18_boldIntervals[] = {
@ -7617,13 +7637,14 @@ static const EpdUnicodeInterval notosans_18_boldIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20C0, 0x320 },
{ 0x2212, 0x2212, 0x341 },
{ 0xFFFD, 0xFFFD, 0x342 },
};
static const EpdFontData notosans_18_bold = {
notosans_18_boldBitmaps,
notosans_18_boldGlyphs,
notosans_18_boldIntervals,
11,
12,
51,
41,
-11,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_18_bolditalicBitmaps[115270] = {
static const uint8_t notosans_18_bolditalicBitmaps[115577] = {
0x00, 0x1F, 0xFE, 0x00, 0x2F, 0xFE, 0x00, 0x3F, 0xFD, 0x00, 0x3F, 0xFC, 0x00, 0x3F, 0xFC, 0x00,
0x7F, 0xF8, 0x00, 0x7F, 0xF4, 0x00, 0xBF, 0xF0, 0x00, 0xFF, 0xF0, 0x00, 0xFF, 0xE0, 0x00, 0xFF,
0xD0, 0x01, 0xFF, 0xC0, 0x02, 0xFF, 0x80, 0x02, 0xFF, 0x40, 0x03, 0xFF, 0x00, 0x03, 0xFF, 0x00,
@ -7212,7 +7212,26 @@ static const uint8_t notosans_18_bolditalicBitmaps[115270] = {
0xFF, 0xFF, 0xFF, 0x40, 0x0B, 0xFF, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x03,
0xE0, 0xF8, 0x00, 0x00, 0x00, 0xF8, 0x3E, 0x00, 0x00, 0x00, 0x3D, 0x0F, 0x40, 0x00, 0x00, 0x1A,
0x06, 0x80, 0x00, 0x00, 0x05, 0x55, 0x54, 0x2F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x3F, 0xFF, 0xF8,
0x3F, 0xFF, 0xF4, 0x15, 0x55, 0x50,
0x3F, 0xFF, 0xF4, 0x15, 0x55, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xE0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF,
0xE0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFA, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0,
0x00, 0x2F, 0xE0, 0x00, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x00, 0x1F,
0xF0, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x01, 0xFF, 0xD0, 0x6A, 0x00, 0x3F, 0xE0, 0x00, 0x00,
0x1F, 0xFF, 0xDF, 0xFE, 0x00, 0xFF, 0xE0, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0xFF, 0xE0,
0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xE0, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x7F,
0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFE, 0x00,
0x3F, 0xFF, 0xFF, 0xE2, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF,
0x40, 0x7F, 0xFF, 0xFF, 0xF0, 0x02, 0xFF, 0xFF, 0xFC, 0x03, 0xFF, 0xFF, 0xFF, 0x00, 0x02, 0xFF,
0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x02, 0xFF, 0xFF, 0xEA, 0xBF, 0xFF, 0xFF, 0x00, 0x00,
0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
0x00, 0x00, 0x02, 0xFF, 0xF4, 0x0F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x02, 0xFF, 0x80, 0x1F, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x02, 0xFE, 0x00, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFC, 0x02,
0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xF9, 0x6F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xF0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_18_bolditalicGlyphs[] = {
@ -8049,6 +8068,7 @@ static const EpdGlyph notosans_18_bolditalicGlyphs[] = {
{ 26, 31, 23, -1, 31, 202, 114866 }, // ₾
{ 21, 35, 21, 0, 31, 184, 115068 }, // ₿
{ 12, 6, 12, 0, 13, 18, 115252 }, //
{ 35, 35, 38, 1, 29, 307, 115270 }, // <20>
};
static const EpdUnicodeInterval notosans_18_bolditalicIntervals[] = {
@ -8063,13 +8083,14 @@ static const EpdUnicodeInterval notosans_18_bolditalicIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20BF, 0x320 },
{ 0x2212, 0x2212, 0x340 },
{ 0xFFFD, 0xFFFD, 0x341 },
};
static const EpdFontData notosans_18_bolditalic = {
notosans_18_bolditalicBitmaps,
notosans_18_bolditalicGlyphs,
notosans_18_bolditalicIntervals,
11,
12,
51,
41,
-11,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_18_italicBitmaps[105127] = {
static const uint8_t notosans_18_italicBitmaps[105434] = {
0x00, 0x0B, 0xF0, 0x00, 0x3F, 0xC0, 0x00, 0xFF, 0x00, 0x07, 0xF8, 0x00, 0x1F, 0xD0, 0x00, 0xBF,
0x00, 0x03, 0xFC, 0x00, 0x0F, 0xE0, 0x00, 0x3F, 0x40, 0x01, 0xFC, 0x00, 0x0B, 0xF0, 0x00, 0x2F,
0x80, 0x00, 0xFD, 0x00, 0x03, 0xF0, 0x00, 0x0F, 0xC0, 0x00, 0x7E, 0x00, 0x02, 0xF4, 0x00, 0x0F,
@ -6578,7 +6578,26 @@ static const uint8_t notosans_18_italicBitmaps[105127] = {
0xC0, 0x00, 0x3F, 0xC0, 0x2F, 0x80, 0x02, 0xFF, 0x40, 0x3F, 0xFF, 0xFF, 0xFD, 0x00, 0x3F, 0xFF,
0xFF, 0xF4, 0x00, 0x7F, 0xFF, 0xFE, 0x40, 0x00, 0x01, 0xF0, 0xB8, 0x00, 0x00, 0x02, 0xE0, 0xF4,
0x00, 0x00, 0x03, 0xD0, 0xF4, 0x00, 0x00, 0x02, 0x80, 0xA0, 0x00, 0x00, 0x0A, 0xAA, 0xA0, 0xBF,
0xFF, 0xD3, 0xFF, 0xFF, 0x05, 0x55, 0x54,
0xFF, 0xD3, 0xFF, 0xFF, 0x05, 0x55, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xE0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF,
0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFA, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x1F,
0xE0, 0x00, 0x2F, 0xE0, 0x00, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x00,
0x1F, 0xF0, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x01, 0xFF, 0xD0, 0x6A, 0x00, 0x3F, 0xE0, 0x00,
0x00, 0x1F, 0xFF, 0xDF, 0xFE, 0x00, 0xFF, 0xE0, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0xFF,
0xE0, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xE0, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
0x7F, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFE,
0x00, 0x3F, 0xFF, 0xFF, 0xE2, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0x02, 0xFF, 0xFF,
0xFF, 0x40, 0x7F, 0xFF, 0xFF, 0xF0, 0x02, 0xFF, 0xFF, 0xFC, 0x03, 0xFF, 0xFF, 0xFF, 0x00, 0x02,
0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x02, 0xFF, 0xFF, 0xEA, 0xBF, 0xFF, 0xFF, 0x00,
0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x02, 0xFF, 0xF4, 0x0F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x02, 0xFF, 0x80, 0x1F,
0xFF, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFE, 0x00, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFC,
0x02, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xF9, 0x6F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xF0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_18_italicGlyphs[] = {
@ -7415,6 +7434,7 @@ static const EpdGlyph notosans_18_italicGlyphs[] = {
{ 25, 31, 22, -1, 31, 194, 104747 }, // ₾
{ 20, 35, 21, 1, 31, 175, 104941 }, // ₿
{ 11, 4, 12, 0, 12, 11, 105116 }, //
{ 35, 35, 38, 1, 29, 307, 105127 }, // <20>
};
static const EpdUnicodeInterval notosans_18_italicIntervals[] = {
@ -7429,13 +7449,14 @@ static const EpdUnicodeInterval notosans_18_italicIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20BF, 0x320 },
{ 0x2212, 0x2212, 0x340 },
{ 0xFFFD, 0xFFFD, 0x341 },
};
static const EpdFontData notosans_18_italic = {
notosans_18_italicBitmaps,
notosans_18_italicGlyphs,
notosans_18_italicIntervals,
11,
12,
51,
41,
-11,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_18_regularBitmaps[98532] = {
static const uint8_t notosans_18_regularBitmaps[98839] = {
0x2F, 0xC3, 0xFC, 0x3F, 0xC3, 0xFC, 0x3F, 0xC2, 0xFC, 0x2F, 0x82, 0xF8, 0x2F, 0x82, 0xF8, 0x1F,
0x81, 0xF4, 0x1F, 0x41, 0xF4, 0x1F, 0x40, 0xF4, 0x0F, 0x40, 0xF0, 0x0F, 0x00, 0x50, 0x00, 0x00,
0x00, 0x05, 0x03, 0xFC, 0x7F, 0xD3, 0xFD, 0x3F, 0xC0, 0x50, 0x7F, 0x02, 0xF9, 0xFC, 0x0B, 0xF7,
@ -6166,7 +6166,26 @@ static const uint8_t notosans_18_regularBitmaps[98532] = {
0xBC, 0x00, 0xFF, 0xFF, 0xFC, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFD, 0x3F, 0xFF, 0xFF, 0xFD, 0x2A, 0xAA, 0xAA,
0xA8, 0x2A, 0xAA, 0xAA, 0xAA, 0xA4, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0x80,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xE0, 0x00,
0x00, 0x00, 0x00, 0x01, 0xFF, 0xFA, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x2F,
0xE0, 0x00, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x00, 0x1F, 0xF0, 0x00,
0x00, 0x1F, 0xE0, 0x00, 0x00, 0x01, 0xFF, 0xD0, 0x6A, 0x00, 0x3F, 0xE0, 0x00, 0x00, 0x1F, 0xFF,
0xDF, 0xFE, 0x00, 0xFF, 0xE0, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0xFF, 0xE0, 0x00, 0x1F,
0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xE0, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x7F, 0xFF, 0xE0,
0x1F, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x3F, 0xFF,
0xFF, 0xE2, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0x40, 0x7F,
0xFF, 0xFF, 0xF0, 0x02, 0xFF, 0xFF, 0xFC, 0x03, 0xFF, 0xFF, 0xFF, 0x00, 0x02, 0xFF, 0xFF, 0xF0,
0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x02, 0xFF, 0xFF, 0xEA, 0xBF, 0xFF, 0xFF, 0x00, 0x00, 0x02, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00,
0x02, 0xFF, 0xF4, 0x0F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x02, 0xFF, 0x80, 0x1F, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x02, 0xFE, 0x00, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFC, 0x02, 0xFF, 0x00,
0x00, 0x00, 0x00, 0x00, 0x02, 0xF9, 0x6F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const EpdGlyph notosans_18_regularGlyphs[] = {
@ -7004,6 +7023,7 @@ static const EpdGlyph notosans_18_regularGlyphs[] = {
{ 18, 35, 21, 3, 31, 158, 98247 }, // ₿
{ 16, 27, 18, 1, 21, 108, 98405 }, // ⃀
{ 19, 4, 21, 1, 15, 19, 98513 }, //
{ 35, 35, 38, 1, 29, 307, 98532 }, // <20>
};
static const EpdUnicodeInterval notosans_18_regularIntervals[] = {
@ -7018,13 +7038,14 @@ static const EpdUnicodeInterval notosans_18_regularIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20C0, 0x320 },
{ 0x2212, 0x2212, 0x341 },
{ 0xFFFD, 0xFFFD, 0x342 },
};
static const EpdFontData notosans_18_regular = {
notosans_18_regularBitmaps,
notosans_18_regularGlyphs,
notosans_18_regularIntervals,
11,
12,
51,
41,
-11,

View File

@ -7,7 +7,7 @@
#pragma once
#include "EpdFontData.h"
static const uint8_t notosans_8_regularBitmaps[10949] = {
static const uint8_t notosans_8_regularBitmaps[10981] = {
0xDB, 0x6D, 0xB6, 0xC3, 0xF4, 0xDE, 0xF7, 0xBD, 0x80, 0x0D, 0x83, 0x30, 0x66, 0x3F, 0xF7, 0xFE,
0x36, 0x04, 0xC7, 0xFE, 0xFF, 0xC6, 0x40, 0xD8, 0x1B, 0x00, 0x18, 0x18, 0xFE, 0xFE, 0xD8, 0xF8,
0xFC, 0x3E, 0x1F, 0x1B, 0xFF, 0xFE, 0x38, 0x18, 0x00, 0x01, 0xE1, 0x86, 0xCC, 0x13, 0x30, 0xCD,
@ -692,7 +692,9 @@ static const uint8_t notosans_8_regularBitmaps[10949] = {
0x07, 0x01, 0xC0, 0x1B, 0x03, 0xE1, 0xFF, 0x3D, 0xED, 0xBF, 0xB6, 0xF6, 0xDE, 0x53, 0xC0, 0x1C,
0x01, 0x80, 0x3F, 0xE7, 0xFC, 0x3C, 0x3C, 0xFE, 0xFF, 0xE3, 0xE3, 0xE7, 0xFE, 0xFF, 0xE3, 0xE3,
0xE3, 0xFF, 0xFE, 0x3C, 0x3C, 0x00, 0x3F, 0x7E, 0x60, 0x60, 0xE0, 0xE0, 0x60, 0x7E, 0x3F, 0x0C,
0x7F, 0x7E, 0x7F, 0xFF, 0xC0,
0x7F, 0x7E, 0x7F, 0xFF, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0C, 0x38, 0x1F,
0xDC, 0x3F, 0xDE, 0x7F, 0xBF, 0x7F, 0x7F, 0x3F, 0x7E, 0x1F, 0xFC, 0x0F, 0xF8, 0x06, 0x70, 0x03,
0xE0, 0x01, 0xC0, 0x00, 0x80,
};
static const EpdGlyph notosans_8_regularGlyphs[] = {
@ -1530,6 +1532,7 @@ static const EpdGlyph notosans_8_regularGlyphs[] = {
{ 8, 16, 10, 1, 14, 16, 10917 }, // ₿
{ 8, 13, 8, 0, 10, 13, 10933 }, // ⃀
{ 9, 2, 10, 0, 7, 3, 10946 }, //
{ 16, 16, 17, 0, 13, 32, 10949 }, // <20>
};
static const EpdUnicodeInterval notosans_8_regularIntervals[] = {
@ -1544,13 +1547,14 @@ static const EpdUnicodeInterval notosans_8_regularIntervals[] = {
{ 0x2066, 0x206F, 0x316 },
{ 0x20A0, 0x20C0, 0x320 },
{ 0x2212, 0x2212, 0x341 },
{ 0xFFFD, 0xFFFD, 0x342 },
};
static const EpdFontData notosans_8_regular = {
notosans_8_regularBitmaps,
notosans_8_regularGlyphs,
notosans_8_regularIntervals,
11,
12,
23,
18,
-5,

View File

@ -99,6 +99,9 @@ intervals = [
# (0xFE30, 0xFE4F),
# # CJK Compatibility Ideographs
# (0xF900, 0xFAFF),
### Specials
# Replacement Character
(0xFFFD, 0xFFFD),
]
add_ints = []

View File

@ -74,6 +74,7 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
// Grab data from opfParser into epub
bookMetadata.title = opfParser.title;
bookMetadata.author = opfParser.author;
bookMetadata.language = opfParser.language;
bookMetadata.coverItemHref = opfParser.coverItemHref;
bookMetadata.textReferenceHref = opfParser.textReferenceHref;
@ -348,6 +349,15 @@ const std::string& Epub::getAuthor() const {
return bookMetadataCache->coreMetadata.author;
}
const std::string& Epub::getLanguage() const {
static std::string blank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return blank;
}
return bookMetadataCache->coreMetadata.language;
}
std::string Epub::getCoverBmpPath(bool cropped) const {
const auto coverFileName = std::string("cover") + (cropped ? "_crop" : "");
return cachePath + "/" + coverFileName + ".bmp";
@ -409,6 +419,70 @@ bool Epub::generateCoverBmp(bool cropped) const {
return false;
}
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
bool Epub::generateThumbBmp() const {
// Already generated, return true
if (SdMan.exists(getThumbBmpPath().c_str())) {
return true;
}
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
Serial.printf("[%lu] [EBP] Cannot generate thumb BMP, cache not loaded\n", millis());
return false;
}
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
if (coverImageHref.empty()) {
Serial.printf("[%lu] [EBP] No known cover image for thumbnail\n", millis());
return false;
}
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
Serial.printf("[%lu] [EBP] Generating thumb BMP from JPG cover image\n", millis());
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
FsFile coverJpg;
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
return false;
}
readItemContentsToStream(coverImageHref, coverJpg, 1024);
coverJpg.close();
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
return false;
}
FsFile thumbBmp;
if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) {
coverJpg.close();
return false;
}
// Use smaller target size for Continue Reading card (half of screen: 240x400)
// Generate 1-bit BMP for fast home screen rendering (no gray passes needed)
constexpr int THUMB_TARGET_WIDTH = 240;
constexpr int THUMB_TARGET_HEIGHT = 400;
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
THUMB_TARGET_HEIGHT);
coverJpg.close();
thumbBmp.close();
SdMan.remove(coverJpgTempPath.c_str());
if (!success) {
Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis());
SdMan.remove(getThumbBmpPath().c_str());
}
Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(),
success ? "yes" : "no");
return success;
} else {
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping thumbnail\n", millis());
}
return false;
}
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
if (itemHref.empty()) {
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
@ -545,14 +619,15 @@ int Epub::getSpineIndexForTextReference() const {
return 0;
}
// Calculate progress in book
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
// Calculate progress in book (returns 0.0-1.0)
float Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
const size_t bookSize = getBookSize();
if (bookSize == 0) {
return 0;
return 0.0f;
}
const size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
const size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
const size_t sectionProgSize = currentSpineRead * curChapterSize;
return round(static_cast<float>(prevChapterSize + sectionProgSize) / bookSize * 100.0);
const float sectionProgSize = currentSpineRead * static_cast<float>(curChapterSize);
const float totalProgress = static_cast<float>(prevChapterSize) + sectionProgSize;
return totalProgress / static_cast<float>(bookSize);
}

View File

@ -44,8 +44,11 @@ class Epub {
const std::string& getPath() const;
const std::string& getTitle() const;
const std::string& getAuthor() const;
const std::string& getLanguage() const;
std::string getCoverBmpPath(bool cropped = false) const;
bool generateCoverBmp(bool cropped = false) const;
std::string getThumbBmpPath() const;
bool generateThumbBmp() const;
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
bool trailingNullByte = false) const;
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
@ -60,5 +63,5 @@ class Epub {
int getSpineIndexForTextReference() const;
size_t getBookSize() const;
uint8_t calculateProgress(int currentSpineIndex, float currentSpineRead) const;
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
};

View File

@ -9,7 +9,7 @@
#include "FsHelpers.h"
namespace {
constexpr uint8_t BOOK_CACHE_VERSION = 4;
constexpr uint8_t BOOK_CACHE_VERSION = 5;
constexpr char bookBinFile[] = "/book.bin";
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
@ -87,8 +87,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
constexpr uint32_t headerASize =
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.coverItemHref.size() +
metadata.textReferenceHref.size() + sizeof(uint32_t) * 4;
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
sizeof(uint32_t) * 5;
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
const uint32_t lutOffset = headerASize + metadataSize;
@ -100,6 +101,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
// Metadata
serialization::writeString(bookFile, metadata.title);
serialization::writeString(bookFile, metadata.author);
serialization::writeString(bookFile, metadata.language);
serialization::writeString(bookFile, metadata.coverItemHref);
serialization::writeString(bookFile, metadata.textReferenceHref);
@ -289,6 +291,7 @@ bool BookMetadataCache::load() {
serialization::readString(bookFile, coreMetadata.title);
serialization::readString(bookFile, coreMetadata.author);
serialization::readString(bookFile, coreMetadata.language);
serialization::readString(bookFile, coreMetadata.coverItemHref);
serialization::readString(bookFile, coreMetadata.textReferenceHref);

View File

@ -9,6 +9,7 @@ class BookMetadataCache {
struct BookMetadata {
std::string title;
std::string author;
std::string language;
std::string coverItemHref;
std::string textReferenceHref;
};

View File

@ -5,11 +5,50 @@
#include <algorithm>
#include <cmath>
#include <functional>
#include <iterator>
#include <limits>
#include <vector>
#include "hyphenation/Hyphenator.h"
constexpr int MAX_COST = std::numeric_limits<int>::max();
namespace {
// Soft hyphen byte pattern used throughout EPUBs (UTF-8 for U+00AD).
constexpr char SOFT_HYPHEN_UTF8[] = "\xC2\xAD";
constexpr size_t SOFT_HYPHEN_BYTES = 2;
bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; }
// Removes every soft hyphen in-place so rendered glyphs match measured widths.
void stripSoftHyphensInPlace(std::string& word) {
size_t pos = 0;
while ((pos = word.find(SOFT_HYPHEN_UTF8, pos)) != std::string::npos) {
word.erase(pos, SOFT_HYPHEN_BYTES);
}
}
// Returns the rendered width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
const EpdFontFamily::Style style, const bool appendHyphen = false) {
const bool hasSoftHyphen = containsSoftHyphen(word);
if (!hasSoftHyphen && !appendHyphen) {
return renderer.getTextWidth(fontId, word.c_str(), style);
}
std::string sanitized = word;
if (hasSoftHyphen) {
stripSoftHyphensInPlace(sanitized);
}
if (appendHyphen) {
sanitized.push_back('-');
}
return renderer.getTextWidth(fontId, sanitized.c_str(), style);
}
} // namespace
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle) {
if (word.empty()) return;
@ -25,10 +64,19 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
return;
}
// Apply fixed transforms before any per-line layout work.
applyParagraphIndent();
const int pageWidth = viewportWidth;
const int spaceWidth = renderer.getSpaceWidth(fontId);
const auto wordWidths = calculateWordWidths(renderer, fontId);
const auto lineBreakIndices = computeLineBreaks(pageWidth, spaceWidth, wordWidths);
auto wordWidths = calculateWordWidths(renderer, fontId);
std::vector<size_t> lineBreakIndices;
if (hyphenationEnabled) {
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths);
} else {
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths);
}
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
for (size_t i = 0; i < lineCount; ++i) {
@ -42,17 +90,11 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
std::vector<uint16_t> wordWidths;
wordWidths.reserve(totalWordCount);
// add em-space at the beginning of first word in paragraph to indent
if (!extraParagraphSpacing) {
std::string& first_word = words.front();
first_word.insert(0, "\xe2\x80\x83");
}
auto wordsIt = words.begin();
auto wordStylesIt = wordStyles.begin();
while (wordsIt != words.end()) {
wordWidths.push_back(renderer.getTextWidth(fontId, wordsIt->c_str(), *wordStylesIt));
wordWidths.push_back(measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt));
std::advance(wordsIt, 1);
std::advance(wordStylesIt, 1);
@ -61,8 +103,21 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
return wordWidths;
}
std::vector<size_t> ParsedText::computeLineBreaks(const int pageWidth, const int spaceWidth,
const std::vector<uint16_t>& wordWidths) const {
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
const int spaceWidth, std::vector<uint16_t>& wordWidths) {
if (words.empty()) {
return {};
}
// Ensure any word that would overflow even as the first entry on a line is split using fallback hyphenation.
for (size_t i = 0; i < wordWidths.size(); ++i) {
while (wordWidths[i] > pageWidth) {
if (!hyphenateWordAtIndex(i, pageWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true)) {
break;
}
}
}
const size_t totalWordCount = words.size();
// DP table to store the minimum badness (cost) of lines starting at index i
@ -140,6 +195,138 @@ std::vector<size_t> ParsedText::computeLineBreaks(const int pageWidth, const int
return lineBreakIndices;
}
void ParsedText::applyParagraphIndent() {
if (extraParagraphSpacing || words.empty()) {
return;
}
if (style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) {
words.front().insert(0, "\xe2\x80\x83");
}
}
// Builds break indices while opportunistically splitting the word that would overflow the current line.
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
const int pageWidth, const int spaceWidth,
std::vector<uint16_t>& wordWidths) {
std::vector<size_t> lineBreakIndices;
size_t currentIndex = 0;
while (currentIndex < wordWidths.size()) {
const size_t lineStart = currentIndex;
int lineWidth = 0;
// Consume as many words as possible for current line, splitting when prefixes fit
while (currentIndex < wordWidths.size()) {
const bool isFirstWord = currentIndex == lineStart;
const int spacing = isFirstWord ? 0 : spaceWidth;
const int candidateWidth = spacing + wordWidths[currentIndex];
// Word fits on current line
if (lineWidth + candidateWidth <= pageWidth) {
lineWidth += candidateWidth;
++currentIndex;
continue;
}
// Word would overflow — try to split based on hyphenation points
const int availableWidth = pageWidth - lineWidth - spacing;
const bool allowFallbackBreaks = isFirstWord; // Only for first word on line
if (availableWidth > 0 &&
hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths, allowFallbackBreaks)) {
// Prefix now fits; append it to this line and move to next line
lineWidth += spacing + wordWidths[currentIndex];
++currentIndex;
break;
}
// Could not split: force at least one word per line to avoid infinite loop
if (currentIndex == lineStart) {
lineWidth += candidateWidth;
++currentIndex;
}
break;
}
lineBreakIndices.push_back(currentIndex);
}
return lineBreakIndices;
}
// Splits words[wordIndex] into prefix (adding a hyphen only when needed) and remainder when a legal breakpoint fits the
// available width.
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const GfxRenderer& renderer,
const int fontId, std::vector<uint16_t>& wordWidths,
const bool allowFallbackBreaks) {
// Guard against invalid indices or zero available width before attempting to split.
if (availableWidth <= 0 || wordIndex >= words.size()) {
return false;
}
// Get iterators to target word and style.
auto wordIt = words.begin();
auto styleIt = wordStyles.begin();
std::advance(wordIt, wordIndex);
std::advance(styleIt, wordIndex);
const std::string& word = *wordIt;
const auto style = *styleIt;
// Collect candidate breakpoints (byte offsets and hyphen requirements).
auto breakInfos = Hyphenator::breakOffsets(word, allowFallbackBreaks);
if (breakInfos.empty()) {
return false;
}
size_t chosenOffset = 0;
int chosenWidth = -1;
bool chosenNeedsHyphen = true;
// Iterate over each legal breakpoint and retain the widest prefix that still fits.
for (const auto& info : breakInfos) {
const size_t offset = info.byteOffset;
if (offset == 0 || offset >= word.size()) {
continue;
}
const bool needsHyphen = info.requiresInsertedHyphen;
const int prefixWidth = measureWordWidth(renderer, fontId, word.substr(0, offset), style, needsHyphen);
if (prefixWidth > availableWidth || prefixWidth <= chosenWidth) {
continue; // Skip if too wide or not an improvement
}
chosenWidth = prefixWidth;
chosenOffset = offset;
chosenNeedsHyphen = needsHyphen;
}
if (chosenWidth < 0) {
// No hyphenation point produced a prefix that fits in the remaining space.
return false;
}
// Split the word at the selected breakpoint and append a hyphen if required.
std::string remainder = word.substr(chosenOffset);
wordIt->resize(chosenOffset);
if (chosenNeedsHyphen) {
wordIt->push_back('-');
}
// Insert the remainder word (with matching style) directly after the prefix.
auto insertWordIt = std::next(wordIt);
auto insertStyleIt = std::next(styleIt);
words.insert(insertWordIt, remainder);
wordStyles.insert(insertStyleIt, style);
// Update cached widths to reflect the new prefix/remainder pairing.
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
const uint16_t remainderWidth = measureWordWidth(renderer, fontId, remainder, style);
wordWidths.insert(wordWidths.begin() + wordIndex + 1, remainderWidth);
return true;
}
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
@ -191,5 +378,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
std::list<EpdFontFamily::Style> lineWordStyles;
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
for (auto& word : lineWords) {
if (containsSoftHyphen(word)) {
stripSoftHyphensInPlace(word);
}
}
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
}
}

View File

@ -17,16 +17,24 @@ class ParsedText {
std::list<EpdFontFamily::Style> wordStyles;
TextBlock::Style style;
bool extraParagraphSpacing;
bool hyphenationEnabled;
std::vector<size_t> computeLineBreaks(int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths) const;
void applyParagraphIndent();
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth,
std::vector<uint16_t>& wordWidths);
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
int spaceWidth, std::vector<uint16_t>& wordWidths);
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
public:
explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing)
: style(style), extraParagraphSpacing(extraParagraphSpacing) {}
explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing,
const bool hyphenationEnabled = false)
: style(style), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
~ParsedText() = default;
void addWord(std::string word, EpdFontFamily::Style fontStyle);
@ -37,4 +45,4 @@ class ParsedText {
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
bool includeLastLine = true);
};
};

View File

@ -4,12 +4,14 @@
#include <Serialization.h>
#include "Page.h"
#include "hyphenation/Hyphenator.h"
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 9;
constexpr uint8_t SECTION_FILE_VERSION = 10;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t);
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) +
sizeof(uint32_t);
} // namespace
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
@ -31,14 +33,15 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight) {
const uint16_t viewportHeight, const bool hyphenationEnabled) {
if (!file) {
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
return;
}
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(uint32_t),
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
sizeof(uint32_t),
"Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION);
serialization::writePod(file, fontId);
@ -47,13 +50,14 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
serialization::writePod(file, paragraphAlignment);
serialization::writePod(file, viewportWidth);
serialization::writePod(file, viewportHeight);
serialization::writePod(file, hyphenationEnabled);
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset
}
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight) {
const uint16_t viewportHeight, const bool hyphenationEnabled) {
if (!SdMan.openFileForRead("SCT", filePath, file)) {
return false;
}
@ -74,16 +78,19 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
float fileLineCompression;
bool fileExtraParagraphSpacing;
uint8_t fileParagraphAlignment;
bool fileHyphenationEnabled;
serialization::readPod(file, fileFontId);
serialization::readPod(file, fileLineCompression);
serialization::readPod(file, fileExtraParagraphSpacing);
serialization::readPod(file, fileParagraphAlignment);
serialization::readPod(file, fileViewportWidth);
serialization::readPod(file, fileViewportHeight);
serialization::readPod(file, fileHyphenationEnabled);
if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight) {
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled) {
file.close();
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
clearCache();
@ -115,7 +122,8 @@ bool Section::clearCache() const {
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const std::function<void()>& progressSetupFn,
const uint16_t viewportHeight, const bool hyphenationEnabled,
const std::function<void()>& progressSetupFn,
const std::function<void(int)>& progressFn) {
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
const auto localPath = epub->getSpineItem(spineIndex).href;
@ -172,14 +180,15 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
return false;
}
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight);
viewportHeight, hyphenationEnabled);
std::vector<uint32_t> lut = {};
ChapterHtmlSlimParser visitor(
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight,
viewportHeight, hyphenationEnabled,
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
progressFn);
Hyphenator::setPreferredLanguage(epub->getLanguage());
success = visitor.parseAndBuildPages();
SdMan.remove(tmpHtmlPath.c_str());

View File

@ -15,7 +15,7 @@ class Section {
FsFile file;
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight);
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled);
uint32_t onPageComplete(std::unique_ptr<Page> page);
public:
@ -29,10 +29,10 @@ class Section {
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
~Section() = default;
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight);
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled);
bool clearCache() const;
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
const std::function<void()>& progressSetupFn = nullptr,
const std::function<void(int)>& progressFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile();

View File

@ -0,0 +1,179 @@
#include "HyphenationCommon.h"
#include <Utf8.h>
namespace {
// Convert Latin uppercase letters (ASCII plus Latin-1 supplement) to lowercase
uint32_t toLowerLatinImpl(const uint32_t cp) {
if (cp >= 'A' && cp <= 'Z') {
return cp - 'A' + 'a';
}
if ((cp >= 0x00C0 && cp <= 0x00D6) || (cp >= 0x00D8 && cp <= 0x00DE)) {
return cp + 0x20;
}
switch (cp) {
case 0x0152: // Œ
return 0x0153; // œ
case 0x0178: // Ÿ
return 0x00FF; // ÿ
case 0x1E9E: // ẞ
return 0x00DF; // ß
default:
return cp;
}
}
// Convert Cyrillic uppercase letters to lowercase
// Cyrillic uppercase range 0x0410-0x042F maps to lowercase by adding 0x20
// Special case: Cyrillic capital IO (0x0401) maps to lowercase io (0x0451)
uint32_t toLowerCyrillicImpl(const uint32_t cp) {
if (cp >= 0x0410 && cp <= 0x042F) {
return cp + 0x20;
}
if (cp == 0x0401) {
return 0x0451;
}
return cp;
}
} // namespace
uint32_t toLowerLatin(const uint32_t cp) { return toLowerLatinImpl(cp); }
uint32_t toLowerCyrillic(const uint32_t cp) { return toLowerCyrillicImpl(cp); }
bool isLatinLetter(const uint32_t cp) {
if ((cp >= 'A' && cp <= 'Z') || (cp >= 'a' && cp <= 'z')) {
return true;
}
if (((cp >= 0x00C0 && cp <= 0x00D6) || (cp >= 0x00D8 && cp <= 0x00F6) || (cp >= 0x00F8 && cp <= 0x00FF)) &&
cp != 0x00D7 && cp != 0x00F7) {
return true;
}
switch (cp) {
case 0x0152: // Œ
case 0x0153: // œ
case 0x0178: // Ÿ
case 0x1E9E: // ẞ
return true;
default:
return false;
}
}
bool isCyrillicLetter(const uint32_t cp) { return (cp >= 0x0400 && cp <= 0x052F); }
bool isAlphabetic(const uint32_t cp) { return isLatinLetter(cp) || isCyrillicLetter(cp); }
bool isPunctuation(const uint32_t cp) {
switch (cp) {
case '-':
case '.':
case ',':
case '!':
case '?':
case ';':
case ':':
case '"':
case '\'':
case ')':
case '(':
case 0x00AB: // «
case 0x00BB: // »
case 0x2018: //
case 0x2019: //
case 0x201C: // “
case 0x201D: // ”
case 0x00A0: // no-break space
case '{':
case '}':
case '[':
case ']':
case '/':
case 0x203A: //
case 0x2026: // …
return true;
default:
return false;
}
}
bool isAsciiDigit(const uint32_t cp) { return cp >= '0' && cp <= '9'; }
bool isExplicitHyphen(const uint32_t cp) {
switch (cp) {
case '-':
case 0x00AD: // soft hyphen
case 0x058A: // Armenian hyphen
case 0x2010: // hyphen
case 0x2011: // non-breaking hyphen
case 0x2012: // figure dash
case 0x2013: // en dash
case 0x2014: // em dash
case 0x2015: // horizontal bar
case 0x2043: // hyphen bullet
case 0x207B: // superscript minus
case 0x208B: // subscript minus
case 0x2212: // minus sign
case 0x2E17: // double oblique hyphen
case 0x2E3A: // two-em dash
case 0x2E3B: // three-em dash
case 0xFE58: // small em dash
case 0xFE63: // small hyphen-minus
case 0xFF0D: // fullwidth hyphen-minus
return true;
default:
return false;
}
}
bool isSoftHyphen(const uint32_t cp) { return cp == 0x00AD; }
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps) {
if (cps.empty()) {
return;
}
// Remove trailing footnote references like [12], even if punctuation trails after the closing bracket.
if (cps.size() >= 3) {
int end = static_cast<int>(cps.size()) - 1;
while (end >= 0 && isPunctuation(cps[end].value)) {
--end;
}
int pos = end;
if (pos >= 0 && isAsciiDigit(cps[pos].value)) {
while (pos >= 0 && isAsciiDigit(cps[pos].value)) {
--pos;
}
if (pos >= 0 && cps[pos].value == '[' && end - pos > 1) {
cps.erase(cps.begin() + pos, cps.end());
}
}
}
while (!cps.empty() && isPunctuation(cps.front().value)) {
cps.erase(cps.begin());
}
while (!cps.empty() && isPunctuation(cps.back().value)) {
cps.pop_back();
}
}
std::vector<CodepointInfo> collectCodepoints(const std::string& word) {
std::vector<CodepointInfo> cps;
cps.reserve(word.size());
const unsigned char* base = reinterpret_cast<const unsigned char*>(word.c_str());
const unsigned char* ptr = base;
while (*ptr != 0) {
const unsigned char* current = ptr;
const uint32_t cp = utf8NextCodepoint(&ptr);
cps.push_back({cp, static_cast<size_t>(current - base)});
}
return cps;
}

View File

@ -0,0 +1,25 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
struct CodepointInfo {
uint32_t value;
size_t byteOffset;
};
uint32_t toLowerLatin(uint32_t cp);
uint32_t toLowerCyrillic(uint32_t cp);
bool isLatinLetter(uint32_t cp);
bool isCyrillicLetter(uint32_t cp);
bool isAlphabetic(uint32_t cp);
bool isPunctuation(uint32_t cp);
bool isAsciiDigit(uint32_t cp);
bool isExplicitHyphen(uint32_t cp);
bool isSoftHyphen(uint32_t cp);
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps);
std::vector<CodepointInfo> collectCodepoints(const std::string& word);

View File

@ -0,0 +1,97 @@
#include "Hyphenator.h"
#include <vector>
#include "HyphenationCommon.h"
#include "LanguageRegistry.h"
const LanguageHyphenator* Hyphenator::cachedHyphenator_ = nullptr;
namespace {
// Maps a BCP-47 language tag to a language-specific hyphenator.
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
if (langTag.empty()) return nullptr;
// Extract primary subtag and normalize to lowercase (e.g., "en-US" -> "en").
std::string primary;
primary.reserve(langTag.size());
for (char c : langTag) {
if (c == '-' || c == '_') break;
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
primary.push_back(c);
}
if (primary.empty()) return nullptr;
return getLanguageHyphenatorForPrimaryTag(primary);
}
// Maps a codepoint index back to its byte offset inside the source word.
size_t byteOffsetForIndex(const std::vector<CodepointInfo>& cps, const size_t index) {
return (index < cps.size()) ? cps[index].byteOffset : (cps.empty() ? 0 : cps.back().byteOffset);
}
// Builds a vector of break information from explicit hyphen markers in the given codepoints.
std::vector<Hyphenator::BreakInfo> buildExplicitBreakInfos(const std::vector<CodepointInfo>& cps) {
std::vector<Hyphenator::BreakInfo> breaks;
// Scan every codepoint looking for explicit/soft hyphen markers that are surrounded by letters.
for (size_t i = 1; i + 1 < cps.size(); ++i) {
const uint32_t cp = cps[i].value;
if (!isExplicitHyphen(cp) || !isAlphabetic(cps[i - 1].value) || !isAlphabetic(cps[i + 1].value)) {
continue;
}
// Offset points to the next codepoint so rendering starts after the hyphen marker.
breaks.push_back({cps[i + 1].byteOffset, isSoftHyphen(cp)});
}
return breaks;
}
} // namespace
std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& word, const bool includeFallback) {
if (word.empty()) {
return {};
}
// Convert to codepoints and normalize word boundaries.
auto cps = collectCodepoints(word);
trimSurroundingPunctuationAndFootnote(cps);
const auto* hyphenator = cachedHyphenator_;
// Explicit hyphen markers (soft or hard) take precedence over language breaks.
auto explicitBreakInfos = buildExplicitBreakInfos(cps);
if (!explicitBreakInfos.empty()) {
return explicitBreakInfos;
}
// Ask language hyphenator for legal break points.
std::vector<size_t> indexes;
if (hyphenator) {
indexes = hyphenator->breakIndexes(cps);
}
// Only add fallback breaks if needed
if (includeFallback && indexes.empty()) {
const size_t minPrefix = hyphenator ? hyphenator->minPrefix() : LiangWordConfig::kDefaultMinPrefix;
const size_t minSuffix = hyphenator ? hyphenator->minSuffix() : LiangWordConfig::kDefaultMinSuffix;
for (size_t idx = minPrefix; idx + minSuffix <= cps.size(); ++idx) {
indexes.push_back(idx);
}
}
if (indexes.empty()) {
return {};
}
std::vector<Hyphenator::BreakInfo> breaks;
breaks.reserve(indexes.size());
for (const size_t idx : indexes) {
breaks.push_back({byteOffsetForIndex(cps, idx), true});
}
return breaks;
}
void Hyphenator::setPreferredLanguage(const std::string& lang) { cachedHyphenator_ = hyphenatorForLanguage(lang); }

View File

@ -0,0 +1,24 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
class LanguageHyphenator;
class Hyphenator {
public:
struct BreakInfo {
size_t byteOffset;
bool requiresInsertedHyphen;
};
// Returns byte offsets where the word may be hyphenated. When includeFallback is true, all positions obeying the
// minimum prefix/suffix constraints are returned even if no language-specific rule matches.
static std::vector<BreakInfo> breakOffsets(const std::string& word, bool includeFallback);
// Provide a publication-level language hint (e.g. "en", "en-US", "ru") used to select hyphenation rules.
static void setPreferredLanguage(const std::string& lang);
private:
static const LanguageHyphenator* cachedHyphenator_;
};

View File

@ -0,0 +1,23 @@
#pragma once
#include "LiangHyphenation.h"
// Generic Liang-backed hyphenator that stores pattern metadata plus language-specific helpers.
class LanguageHyphenator {
public:
LanguageHyphenator(const SerializedHyphenationPatterns& patterns, bool (*isLetterFn)(uint32_t),
uint32_t (*toLowerFn)(uint32_t), size_t minPrefix = LiangWordConfig::kDefaultMinPrefix,
size_t minSuffix = LiangWordConfig::kDefaultMinSuffix)
: patterns_(patterns), config_(isLetterFn, toLowerFn, minPrefix, minSuffix) {}
std::vector<size_t> breakIndexes(const std::vector<CodepointInfo>& cps) const {
return liangBreakIndexes(cps, patterns_, config_);
}
size_t minPrefix() const { return config_.minPrefix; }
size_t minSuffix() const { return config_.minSuffix; }
protected:
const SerializedHyphenationPatterns& patterns_;
LiangWordConfig config_;
};

View File

@ -0,0 +1,42 @@
#include "LanguageRegistry.h"
#include <algorithm>
#include <array>
#include "HyphenationCommon.h"
#include "generated/hyph-de.trie.h"
#include "generated/hyph-en.trie.h"
#include "generated/hyph-fr.trie.h"
#include "generated/hyph-ru.trie.h"
namespace {
// English hyphenation patterns (3/3 minimum prefix/suffix length)
LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin, 3, 3);
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
using EntryArray = std::array<LanguageEntry, 4>;
const EntryArray& entries() {
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
{"french", "fr", &frenchHyphenator},
{"german", "de", &germanHyphenator},
{"russian", "ru", &russianHyphenator}}};
return kEntries;
}
} // namespace
const LanguageHyphenator* getLanguageHyphenatorForPrimaryTag(const std::string& primaryTag) {
const auto& allEntries = entries();
const auto it = std::find_if(allEntries.begin(), allEntries.end(),
[&primaryTag](const LanguageEntry& entry) { return primaryTag == entry.primaryTag; });
return (it != allEntries.end()) ? it->hyphenator : nullptr;
}
LanguageEntryView getLanguageEntries() {
const auto& allEntries = entries();
return LanguageEntryView{allEntries.data(), allEntries.size()};
}

View File

@ -0,0 +1,26 @@
#pragma once
#include <cstddef>
#include <string>
#include "LanguageHyphenator.h"
struct LanguageEntry {
const char* cliName;
const char* primaryTag;
const LanguageHyphenator* hyphenator;
};
struct LanguageEntryView {
const LanguageEntry* data;
size_t size;
const LanguageEntry* begin() const { return data; }
const LanguageEntry* end() const { return data + size; }
};
// Returns the Liang-backed hyphenator for a given primary language tag (e.g., "en", "fr").
const LanguageHyphenator* getLanguageHyphenatorForPrimaryTag(const std::string& primaryTag);
// Exposes the list of supported languages primarily for tooling/tests.
LanguageEntryView getLanguageEntries();

View File

@ -0,0 +1,405 @@
#include "LiangHyphenation.h"
#include <algorithm>
#include <vector>
/*
* Liang hyphenation pipeline overview (Typst-style binary trie variant)
* --------------------------------------------------------------------
* 1. Input normalization (buildAugmentedWord)
* - Accepts a vector of CodepointInfo structs emitted by the EPUB text
* parser. Each codepoint is validated with LiangWordConfig::isLetter so
* we abort early on digits, punctuation, etc. If the word is valid we
* build an "augmented" byte sequence: leading '.', lowercase UTF-8 bytes
* for every letter, then a trailing '.'. While doing this we capture the
* UTF-8 byte offset for each character and a reverse lookup table that
* maps UTF-8 byte indexes back to codepoint indexes. This lets the rest
* of the algorithm stay byte-oriented (matching the serialized automaton)
* while still emitting hyphen positions in codepoint space.
*
* 2. Automaton decoding
* - SerializedHyphenationPatterns stores a contiguous blob generated from
* Typst's binary tries. The first 4 bytes contain the root offset. Each
* node packs transitions, variable-stride relative offsets to child
* nodes, and an optional pointer into a shared "levels" list. We parse
* that layout lazily via decodeState/transition, keeping everything in
* flash memory; no heap allocations besides the stack-local AutomatonState
* structs. getAutomaton caches parseAutomaton results per blob pointer so
* multiple words hitting the same language only pay the cost once.
*
* 3. Pattern application
* - We walk the augmented bytes left-to-right. For each starting byte we
* stream transitions through the trie, terminating when a transition
* fails. Whenever a node exposes level data we expand the packed
* "dist+level" bytes: `dist` is the delta (in UTF-8 bytes) from the
* starting cursor and `level` is the Liang priority digit. Using the
* bytecodepoint lookup we mark the corresponding index in `scores`.
* Scores are only updated if the new level is higher, mirroring Liang's
* "max digit wins" rule.
*
* 4. Output filtering
* - collectBreakIndexes converts odd-valued score entries back to codepoint
* break positions while enforcing `minPrefix`/`minSuffix` constraints from
* LiangWordConfig. The caller (language-specific hyphenators) can then
* translate these indexes into renderer glyph offsets, page layout data,
* etc.
*
* Keeping the entire algorithm small and deterministic is critical on the
* ESP32-C3: we avoid recursion, dynamic allocations per node, or copying the
* trie. All lookups stay within the generated blob, which lives in flash, and
* the working buffers (augmented bytes/scores) scale with the word length rather
* than the pattern corpus.
*/
namespace {
struct AugmentedWord {
std::vector<uint8_t> bytes;
std::vector<size_t> charByteOffsets;
std::vector<int32_t> byteToCharIndex;
bool empty() const { return bytes.empty(); }
size_t charCount() const { return charByteOffsets.size(); }
};
// Encode a single Unicode codepoint into UTF-8 and append to the provided buffer.
size_t encodeUtf8(uint32_t cp, std::vector<uint8_t>& out) {
if (cp <= 0x7Fu) {
out.push_back(static_cast<uint8_t>(cp));
return 1;
}
if (cp <= 0x7FFu) {
out.push_back(static_cast<uint8_t>(0xC0u | ((cp >> 6) & 0x1Fu)));
out.push_back(static_cast<uint8_t>(0x80u | (cp & 0x3Fu)));
return 2;
}
if (cp <= 0xFFFFu) {
out.push_back(static_cast<uint8_t>(0xE0u | ((cp >> 12) & 0x0Fu)));
out.push_back(static_cast<uint8_t>(0x80u | ((cp >> 6) & 0x3Fu)));
out.push_back(static_cast<uint8_t>(0x80u | (cp & 0x3Fu)));
return 3;
}
out.push_back(static_cast<uint8_t>(0xF0u | ((cp >> 18) & 0x07u)));
out.push_back(static_cast<uint8_t>(0x80u | ((cp >> 12) & 0x3Fu)));
out.push_back(static_cast<uint8_t>(0x80u | ((cp >> 6) & 0x3Fu)));
out.push_back(static_cast<uint8_t>(0x80u | (cp & 0x3Fu)));
return 4;
}
// Build the dotted, lowercase UTF-8 representation plus lookup tables.
AugmentedWord buildAugmentedWord(const std::vector<CodepointInfo>& cps, const LiangWordConfig& config) {
AugmentedWord word;
if (cps.empty()) {
return word;
}
word.bytes.reserve(cps.size() * 2 + 2);
word.charByteOffsets.reserve(cps.size() + 2);
word.charByteOffsets.push_back(0);
word.bytes.push_back('.');
for (const auto& info : cps) {
if (!config.isLetter(info.value)) {
word.bytes.clear();
word.charByteOffsets.clear();
word.byteToCharIndex.clear();
return word;
}
word.charByteOffsets.push_back(word.bytes.size());
encodeUtf8(config.toLower(info.value), word.bytes);
}
word.charByteOffsets.push_back(word.bytes.size());
word.bytes.push_back('.');
word.byteToCharIndex.assign(word.bytes.size(), -1);
for (size_t i = 0; i < word.charByteOffsets.size(); ++i) {
const size_t offset = word.charByteOffsets[i];
if (offset < word.byteToCharIndex.size()) {
word.byteToCharIndex[offset] = static_cast<int32_t>(i);
}
}
return word;
}
// Decoded view of a single trie node pulled straight out of the serialized blob.
// - transitions: contiguous list of next-byte values
// - targets: packed relative offsets (1/2/3 bytes) for each transition
// - levels: optional pointer into the global levels list with packed dist/level pairs
struct AutomatonState {
const uint8_t* data = nullptr;
size_t size = 0;
size_t addr = 0;
uint8_t stride = 1;
size_t childCount = 0;
const uint8_t* transitions = nullptr;
const uint8_t* targets = nullptr;
const uint8_t* levels = nullptr;
size_t levelsLen = 0;
bool valid() const { return data != nullptr; }
};
// Lightweight descriptor for the entire embedded automaton.
// The blob format is:
// [0..3] - big-endian root offset
// [4....] - node heap containing variable-sized headers + transition data
struct EmbeddedAutomaton {
const uint8_t* data = nullptr;
size_t size = 0;
uint32_t rootOffset = 0;
bool valid() const { return data != nullptr && size >= 4 && rootOffset < size; }
};
// Decode the serialized automaton header and root offset.
EmbeddedAutomaton parseAutomaton(const SerializedHyphenationPatterns& patterns) {
EmbeddedAutomaton automaton;
if (!patterns.data || patterns.size < 4) {
return automaton;
}
automaton.data = patterns.data;
automaton.size = patterns.size;
automaton.rootOffset = (static_cast<uint32_t>(patterns.data[0]) << 24) |
(static_cast<uint32_t>(patterns.data[1]) << 16) |
(static_cast<uint32_t>(patterns.data[2]) << 8) | static_cast<uint32_t>(patterns.data[3]);
if (automaton.rootOffset >= automaton.size) {
automaton.data = nullptr;
automaton.size = 0;
}
return automaton;
}
// Cache parsed automata per blob pointer to avoid reparsing.
const EmbeddedAutomaton& getAutomaton(const SerializedHyphenationPatterns& patterns) {
struct CacheEntry {
const SerializedHyphenationPatterns* key;
EmbeddedAutomaton automaton;
};
static std::vector<CacheEntry> cache;
for (const auto& entry : cache) {
if (entry.key == &patterns) {
return entry.automaton;
}
}
cache.push_back({&patterns, parseAutomaton(patterns)});
return cache.back().automaton;
}
// Interpret the node located at `addr`, returning transition metadata.
AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
AutomatonState state;
if (!automaton.valid() || addr >= automaton.size) {
return state;
}
const uint8_t* base = automaton.data + addr;
size_t remaining = automaton.size - addr;
size_t pos = 0;
const uint8_t header = base[pos++];
// Header layout (bits):
// 7 - hasLevels flag
// 6..5 - stride selector (0 -> 1 byte, otherwise 1|2|3)
// 4..0 - child count (5 bits), 31 == overflow -> extra byte
const bool hasLevels = (header >> 7) != 0;
uint8_t stride = static_cast<uint8_t>((header >> 5) & 0x03u);
if (stride == 0) {
stride = 1;
}
size_t childCount = static_cast<size_t>(header & 0x1Fu);
if (childCount == 31u) {
if (pos >= remaining) {
return AutomatonState{};
}
childCount = base[pos++];
}
const uint8_t* levelsPtr = nullptr;
size_t levelsLen = 0;
if (hasLevels) {
if (pos + 1 >= remaining) {
return AutomatonState{};
}
const uint8_t offsetHi = base[pos++];
const uint8_t offsetLoLen = base[pos++];
// The 12-bit offset (hi<<4 | top nibble) points into the blob-level levels list.
// The bottom nibble stores how many packed entries belong to this node.
const size_t offset = (static_cast<size_t>(offsetHi) << 4) | (offsetLoLen >> 4);
levelsLen = offsetLoLen & 0x0Fu;
if (offset + levelsLen > automaton.size) {
return AutomatonState{};
}
levelsPtr = automaton.data + offset;
}
if (pos + childCount > remaining) {
return AutomatonState{};
}
const uint8_t* transitions = base + pos;
pos += childCount;
const size_t targetsBytes = childCount * stride;
if (pos + targetsBytes > remaining) {
return AutomatonState{};
}
const uint8_t* targets = base + pos;
state.data = automaton.data;
state.size = automaton.size;
state.addr = addr;
state.stride = stride;
state.childCount = childCount;
state.transitions = transitions;
state.targets = targets;
state.levels = levelsPtr;
state.levelsLen = levelsLen;
return state;
}
// Convert the packed stride-sized delta back into a signed offset.
int32_t decodeDelta(const uint8_t* buf, uint8_t stride) {
if (stride == 1) {
return static_cast<int8_t>(buf[0]);
}
if (stride == 2) {
return static_cast<int16_t>((static_cast<uint16_t>(buf[0]) << 8) | static_cast<uint16_t>(buf[1]));
}
const int32_t unsignedVal =
(static_cast<int32_t>(buf[0]) << 16) | (static_cast<int32_t>(buf[1]) << 8) | static_cast<int32_t>(buf[2]);
return unsignedVal - (1 << 23);
}
// Follow a single byte transition from `state`, decoding the child node on success.
bool transition(const EmbeddedAutomaton& automaton, const AutomatonState& state, uint8_t letter, AutomatonState& out) {
if (!state.valid()) {
return false;
}
// Children remain sorted by letter in the serialized blob, but the lists are
// short enough that a linear scan keeps code size down compared to binary search.
for (size_t idx = 0; idx < state.childCount; ++idx) {
if (state.transitions[idx] != letter) {
continue;
}
const uint8_t* deltaPtr = state.targets + idx * state.stride;
const int32_t delta = decodeDelta(deltaPtr, state.stride);
// Deltas are relative to the current node's address, allowing us to keep all
// targets within 24 bits while still referencing further nodes in the blob.
const int64_t nextAddr = static_cast<int64_t>(state.addr) + delta;
if (nextAddr < 0 || static_cast<size_t>(nextAddr) >= automaton.size) {
return false;
}
out = decodeState(automaton, static_cast<size_t>(nextAddr));
return out.valid();
}
return false;
}
// Converts odd score positions back into codepoint indexes, honoring min prefix/suffix constraints.
// Each break corresponds to scores[breakIndex + 1] because of the leading '.' sentinel.
// Convert odd score entries into hyphen positions while honoring prefix/suffix limits.
std::vector<size_t> collectBreakIndexes(const std::vector<CodepointInfo>& cps, const std::vector<uint8_t>& scores,
const size_t minPrefix, const size_t minSuffix) {
std::vector<size_t> indexes;
const size_t cpCount = cps.size();
if (cpCount < 2) {
return indexes;
}
for (size_t breakIndex = 1; breakIndex < cpCount; ++breakIndex) {
if (breakIndex < minPrefix) {
continue;
}
const size_t suffixCount = cpCount - breakIndex;
if (suffixCount < minSuffix) {
continue;
}
const size_t scoreIdx = breakIndex + 1;
if (scoreIdx >= scores.size()) {
break;
}
if ((scores[scoreIdx] & 1u) == 0) {
continue;
}
indexes.push_back(breakIndex);
}
return indexes;
}
} // namespace
// Entry point that runs the full Liang pipeline for a single word.
std::vector<size_t> liangBreakIndexes(const std::vector<CodepointInfo>& cps,
const SerializedHyphenationPatterns& patterns, const LiangWordConfig& config) {
const auto augmented = buildAugmentedWord(cps, config);
if (augmented.empty()) {
return {};
}
const EmbeddedAutomaton& automaton = getAutomaton(patterns);
if (!automaton.valid()) {
return {};
}
const AutomatonState root = decodeState(automaton, automaton.rootOffset);
if (!root.valid()) {
return {};
}
// Liang scores: one entry per augmented char (leading/trailing dots included).
std::vector<uint8_t> scores(augmented.charCount(), 0);
// Walk every starting character position and stream bytes through the trie.
for (size_t charStart = 0; charStart < augmented.charByteOffsets.size(); ++charStart) {
const size_t byteStart = augmented.charByteOffsets[charStart];
AutomatonState state = root;
for (size_t cursor = byteStart; cursor < augmented.bytes.size(); ++cursor) {
AutomatonState next;
if (!transition(automaton, state, augmented.bytes[cursor], next)) {
break; // No more matches for this prefix.
}
state = next;
if (state.levels && state.levelsLen > 0) {
size_t offset = 0;
// Each packed byte stores the byte-distance delta and the Liang level digit.
for (size_t i = 0; i < state.levelsLen; ++i) {
const uint8_t packed = state.levels[i];
const size_t dist = static_cast<size_t>(packed / 10);
const uint8_t level = static_cast<uint8_t>(packed % 10);
offset += dist;
const size_t splitByte = byteStart + offset;
if (splitByte >= augmented.byteToCharIndex.size()) {
continue;
}
const int32_t boundary = augmented.byteToCharIndex[splitByte];
if (boundary < 0) {
continue; // Mid-codepoint byte, wait for the next one.
}
if (boundary < 2 || boundary + 2 > static_cast<int32_t>(augmented.charCount())) {
continue; // Skip splits that land in the leading/trailing sentinels.
}
const size_t idx = static_cast<size_t>(boundary);
if (idx >= scores.size()) {
continue;
}
scores[idx] = std::max(scores[idx], level);
}
}
}
}
return collectBreakIndexes(cps, scores, config.minPrefix, config.minSuffix);
}

View File

@ -0,0 +1,38 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
#include "HyphenationCommon.h"
#include "SerializedHyphenationTrie.h"
// Encapsulates every language-specific dial the Liang algorithm needs at runtime. The helpers are
// intentionally represented as bare function pointers because we invoke them inside tight loops and
// want to avoid the overhead of std::function or functors. The minima default to the TeX-recommended
// "2/2" split but individual languages (English, for example) can override them.
struct LiangWordConfig {
static constexpr size_t kDefaultMinPrefix = 2;
static constexpr size_t kDefaultMinSuffix = 2;
// Predicate used to reject non-alphabetic characters before pattern lookup. Returning false causes
// the entire word to be skipped, matching the behavior of classic TeX hyphenation tables.
bool (*isLetter)(uint32_t);
// Language-specific case folding that matches how the TeX patterns were authored (usually lower-case
// ASCII for Latin and lowercase Cyrillic for Russian). Patterns are stored in UTF-8, so this must
// operate on Unicode scalars rather than bytes.
uint32_t (*toLower)(uint32_t);
// Minimum codepoints required on the left/right of any break. These correspond to TeX's
// lefthyphenmin and righthyphenmin knobs.
size_t minPrefix;
size_t minSuffix;
// Lightweight aggregate constructor so call sites can declare `const LiangWordConfig config(...)`
// without verbose member assignment boilerplate.
LiangWordConfig(bool (*letterFn)(uint32_t), uint32_t (*lowerFn)(uint32_t), size_t prefix = kDefaultMinPrefix,
size_t suffix = kDefaultMinSuffix)
: isLetter(letterFn), toLower(lowerFn), minPrefix(prefix), minSuffix(suffix) {}
};
// Shared Liang pattern evaluator used by every language-specific hyphenator.
std::vector<size_t> liangBreakIndexes(const std::vector<CodepointInfo>& cps,
const SerializedHyphenationPatterns& patterns, const LiangWordConfig& config);

View File

@ -0,0 +1,10 @@
#pragma once
#include <cstddef>
#include <cstdint>
// Lightweight descriptor that points at a serialized Liang hyphenation trie stored in flash.
struct SerializedHyphenationPatterns {
const std::uint8_t* data;
size_t size;
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,383 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "../SerializedHyphenationTrie.h"
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
alignas(4) constexpr uint8_t fr_trie_data[] = {
0x00, 0x00, 0x1A, 0xF4, 0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C,
0x2B, 0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17, 0x04, 0x1F,
0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36, 0x34, 0x21, 0x2A, 0x0D, 0x0C,
0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C, 0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D,
0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B, 0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E,
0x48, 0x2C, 0x0B, 0x29, 0x16, 0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16,
0x3E, 0x0D, 0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B, 0x16,
0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C,
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD,
0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21,
0x73, 0xFD, 0xA0, 0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68,
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD,
0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61, 0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD,
0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA, 0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74,
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF,
0x5E, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21,
0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x72,
0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xEF, 0xFF, 0xFD,
0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0,
0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0, 0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61,
0x6E, 0x75, 0xF2, 0xFD, 0x21, 0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74,
0xFF, 0x06, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21, 0x74,
0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2, 0x02, 0x52, 0x6E, 0x75,
0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD, 0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF,
0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70, 0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD,
0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21,
0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0,
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96, 0xFF, 0x96, 0x41,
0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52, 0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62,
0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05, 0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06,
0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21,
0x65, 0xFD, 0xC2, 0x02, 0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73,
0x74, 0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF, 0xF4, 0xFF,
0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7, 0x21, 0x67, 0xFC, 0x41, 0x65,
0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02,
0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5,
0xF1, 0xFD, 0xA0, 0x00, 0x61, 0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8,
0xFE, 0xC8, 0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43, 0x64,
0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E, 0x70, 0x73, 0x72, 0x67,
0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D, 0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01,
0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1,
0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE,
0x47, 0xFE, 0x47, 0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD,
0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43, 0x63, 0x74, 0x75,
0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F, 0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC,
0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0, 0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21,
0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72,
0xFD, 0x21, 0x76, 0xFD, 0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21,
0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD, 0xA0, 0x04,
0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x61, 0xFD,
0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD, 0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41,
0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02, 0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74,
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04,
0x31, 0x21, 0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x61,
0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x6F, 0xFE, 0x7B, 0xA0,
0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21, 0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04,
0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63, 0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3,
0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3, 0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73,
0xFD, 0x21, 0x74, 0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69,
0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E, 0x72, 0x73, 0xFF,
0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9, 0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41,
0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C, 0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72,
0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05,
0x23, 0x21, 0x6E, 0xFD, 0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E,
0xFF, 0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF, 0xFD, 0x44,
0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9, 0xF3, 0x41, 0xA9, 0xFC, 0x86,
0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64, 0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D,
0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C, 0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41,
0x68, 0xFC, 0x92, 0x23, 0x61, 0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A,
0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79, 0x6F,
0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74, 0x75,
0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0, 0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61,
0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE, 0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63,
0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63, 0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21,
0x6E, 0xFB, 0x41, 0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB,
0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21, 0xC3, 0xFC, 0x21,
0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3, 0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6,
0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64, 0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F,
0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD, 0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68,
0xFE, 0xCA, 0x21, 0x6F, 0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF,
0x44, 0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5, 0x21, 0x74,
0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD, 0x21, 0xA9, 0xF9, 0xA0, 0x01,
0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA,
0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA, 0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73,
0xFA, 0xA4, 0xFA, 0xA4, 0xFF, 0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E,
0xFD, 0x44, 0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5, 0x21,
0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41, 0xA9, 0xFC, 0x27, 0x21,
0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21, 0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75,
0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2, 0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61,
0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73, 0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA,
0x3F, 0x21, 0xA9, 0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D,
0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4, 0xFC, 0xBD, 0x21,
0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93, 0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF,
0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9, 0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43,
0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD, 0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21,
0xA0, 0x01, 0xC1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB,
0x81, 0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41, 0x73, 0xFE,
0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD, 0x43, 0x6F, 0x73, 0x75, 0xFF,
0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74,
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41,
0x61, 0xFE, 0xA9, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC,
0x21, 0x74, 0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD, 0x25,
0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB, 0xFD, 0x21, 0x61, 0xFC,
0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD, 0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC,
0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68, 0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70,
0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA, 0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC,
0x41, 0x6C, 0xFB, 0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC,
0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7, 0xFF, 0xFD, 0x41,
0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C, 0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08,
0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80, 0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9,
0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9, 0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF,
0xD7, 0xFF, 0xE4, 0xFD, 0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75,
0xB9, 0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21, 0xA9, 0xFD,
0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9, 0x42, 0x66, 0x78, 0xFB, 0x18,
0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1, 0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2,
0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41, 0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65,
0x63, 0x68, 0x75, 0x6F, 0xFF, 0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF,
0xFD, 0xC3, 0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43, 0x63,
0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69,
0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41, 0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21,
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21,
0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7,
0xF8, 0xFF, 0xF9, 0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42,
0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41, 0x69, 0xF7, 0xD2,
0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73, 0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47,
0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7,
0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70, 0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9,
0x41, 0x75, 0xF8, 0x30, 0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63,
0xF8, 0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x73, 0xF8,
0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41, 0x69, 0xF8, 0x73, 0x21, 0x75,
0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA, 0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6,
0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72, 0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6,
0xBF, 0xF6, 0xBF, 0x42, 0x63, 0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD,
0x21, 0x74, 0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2, 0x61,
0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xF9, 0xF6, 0x99, 0xF6,
0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF, 0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8,
0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF, 0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61,
0xC3, 0x62, 0x63, 0x64, 0x65, 0x69, 0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8,
0xB1, 0xF8, 0xE6, 0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E,
0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85, 0xF8, 0x85, 0xA0,
0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42,
0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73, 0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2,
0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21,
0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63,
0x61, 0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1,
0xE1, 0xFD, 0x41, 0x74, 0xFE, 0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70,
0xF6, 0xFD, 0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x2E, 0xFF, 0x33, 0x21,
0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63, 0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21,
0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22, 0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03,
0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD,
0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F,
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E, 0xFD, 0x21, 0x74,
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD,
0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64, 0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76,
0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6, 0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2,
0xFF, 0xC9, 0xFF, 0xD4, 0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4,
0xBB, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02, 0x41, 0x21,
0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1, 0x2E, 0x73, 0x6E, 0xF1, 0xF4,
0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2, 0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD,
0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74, 0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2,
0x00, 0xE2, 0x65, 0x75, 0xFF, 0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE,
0x62, 0xFF, 0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43, 0x65,
0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92, 0x21, 0x65, 0xFD, 0x21,
0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65, 0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF,
0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD,
0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF,
0xE7, 0xFF, 0xF6, 0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65,
0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0x21,
0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92,
0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC, 0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF,
0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F,
0xFF, 0xA8, 0xFF, 0xBF, 0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD,
0x8D, 0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0x41,
0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD,
0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70, 0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21,
0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA,
0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21,
0x74, 0xFD, 0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F, 0xE2,
0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21, 0x6E, 0xFC, 0x21, 0x65,
0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59,
0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE,
0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41, 0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00,
0xE2, 0x2E, 0x62, 0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE,
0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D, 0xFC, 0xEE, 0xA0,
0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5, 0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75,
0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72, 0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7,
0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD, 0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21,
0x72, 0xFC, 0x41, 0x69, 0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD,
0x42, 0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4, 0x21, 0x69,
0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF, 0x21, 0x74, 0xFC, 0x21, 0x69,
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9,
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7, 0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C,
0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2,
0x88, 0xA1, 0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69, 0xFC,
0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE, 0x8A, 0xFD, 0x27, 0xFD,
0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF, 0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3,
0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1, 0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41,
0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74,
0xF2, 0x5A, 0xA1, 0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC,
0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2, 0xF5, 0x21, 0xA9,
0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0, 0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21,
0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41,
0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1, 0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
0xFB, 0x41, 0xFF, 0xFB, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73,
0xFC, 0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73, 0xFB, 0x34,
0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69, 0x6F, 0x61, 0xC3, 0x65, 0x6E,
0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08, 0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A,
0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21,
0x75, 0xFD, 0x41, 0x61, 0xF3, 0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9,
0xFF, 0xFC, 0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA,
0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF, 0xFC, 0xFB, 0x62, 0x42,
0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E, 0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB,
0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C, 0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1,
0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41,
0x68, 0xF8, 0xC0, 0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB,
0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A, 0xC3, 0x69, 0x63,
0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39, 0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD,
0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03, 0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E,
0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A, 0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21,
0x61, 0xFD, 0x21, 0x76, 0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3,
0x73, 0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D, 0xFD, 0x21,
0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2, 0x69, 0x75, 0xC3, 0x6F, 0x65,
0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41, 0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62,
0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9, 0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD,
0xFC, 0x3E, 0xFC, 0x3E, 0x41, 0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01,
0xFF, 0xFC, 0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21, 0xC3,
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C, 0xEB, 0xFD, 0x42, 0xA9,
0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1,
0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21,
0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75,
0x65, 0xF9, 0x7E, 0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72,
0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21, 0x75, 0xFC, 0xA0,
0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47,
0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF, 0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00,
0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB, 0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0,
0x07, 0x62, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69,
0xFA, 0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35, 0xFF, 0xDA,
0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6, 0x41, 0x75, 0xF8, 0xC2, 0x22,
0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22,
0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D, 0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73,
0x74, 0x79, 0xFE, 0xAE, 0xFE, 0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF,
0xC2, 0xFF, 0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64, 0xF1,
0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC, 0x41, 0x6C, 0xF1, 0x8F,
0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C,
0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42, 0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F,
0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66,
0xB5, 0xBC, 0xCE, 0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3,
0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61, 0xFC, 0x22, 0x63,
0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD,
0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1, 0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F,
0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70, 0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21,
0x72, 0xF7, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72,
0xFA, 0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD, 0x41, 0x61,
0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72, 0xFF, 0x9D, 0xFF, 0xC9, 0xFF,
0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC, 0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68,
0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69, 0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21,
0x72, 0xFD, 0x21, 0x76, 0xFD, 0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61,
0xFF, 0xA5, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21, 0x61,
0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA, 0xFF, 0xDF, 0xFF, 0xEB,
0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61, 0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21,
0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63, 0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4,
0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1,
0x41, 0x70, 0xED, 0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69,
0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70, 0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C,
0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD, 0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F,
0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21, 0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F,
0x72, 0x73, 0x75, 0x79, 0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF,
0xFD, 0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72, 0xF6, 0xA6,
0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74, 0xF0, 0x03, 0xFF, 0xFC, 0x45,
0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD, 0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21,
0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61, 0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79,
0xFB, 0x9D, 0x21, 0x68, 0xFC, 0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D,
0xFB, 0xEE, 0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41, 0x6D,
0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E,
0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2, 0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8,
0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21, 0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7,
0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7, 0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F,
0xFF, 0xF3, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21,
0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6D, 0x72,
0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65, 0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB,
0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0,
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5,
0x5F, 0xF5, 0x5F, 0x41, 0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA,
0x2E, 0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20, 0xFF, 0x4D,
0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC, 0xF5, 0x35, 0xC1, 0x00, 0xE1,
0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D,
0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75, 0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5,
0x32, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2,
0x65, 0x69, 0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD, 0xC4,
0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD, 0xC4, 0xF4, 0xD1, 0x45,
0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87,
0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2,
0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C,
0x61, 0xC3, 0x65, 0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF,
0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4, 0x79, 0x41, 0x69,
0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E,
0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5, 0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF,
0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21, 0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02,
0x6F, 0x73, 0xF5, 0x12, 0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72,
0x70, 0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00, 0xE2, 0x75,
0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD, 0x41, 0x6D, 0xF4, 0x02, 0x21,
0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22, 0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65,
0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0, 0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76,
0xFF, 0xFC, 0xF3, 0xDA, 0x41, 0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9,
0xFB, 0x17, 0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6, 0x66,
0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29, 0xC6, 0x00, 0xE2, 0x2E,
0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF, 0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64,
0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21,
0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72,
0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62,
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65, 0xF3, 0x44, 0x21,
0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9, 0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65,
0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5, 0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF,
0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F, 0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11,
0x41, 0x69, 0xF3, 0x97, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21,
0x6E, 0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x6F, 0xF6,
0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62, 0x6E, 0xF3, 0x6F, 0xFF, 0xEF,
0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82,
0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21, 0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F,
0xF7, 0x81, 0x21, 0x72, 0xFC, 0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74,
0xF7, 0x6F, 0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63,
0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC, 0xDC, 0xFB, 0x21, 0x74,
0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D, 0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73,
0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D, 0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3,
0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E, 0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75,
0x72, 0xFF, 0xFB, 0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD,
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1, 0x00, 0xE1, 0x6D,
0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1,
0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06, 0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2,
0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65,
0xC3, 0x69, 0x6F, 0x73, 0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD,
0x22, 0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xF1,
0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41, 0x76, 0xF3, 0xC0, 0x41, 0x76,
0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC, 0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21,
0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73, 0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42,
0x6E, 0x73, 0xE9, 0xBA, 0xE9, 0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2,
0x00, 0xE1, 0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41, 0x74,
0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF, 0xFD, 0xF2, 0x88, 0x42,
0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73, 0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41,
0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2,
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0,
0xFD, 0x41, 0x6D, 0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69,
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB, 0xF2, 0x50, 0xF0,
0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8,
0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0,
0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A,
0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79,
0x6D, 0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80, 0xF0, 0x80,
0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1, 0x00, 0xE2, 0x2E, 0xF1, 0xAF,
0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0, 0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0,
0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0, 0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1,
0x75, 0xF9, 0x41, 0x69, 0xFA, 0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E,
0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69, 0x6F,
0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0, 0x0B, 0xF4, 0x0D, 0xFF,
0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC, 0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38,
0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61, 0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2,
0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF,
0xCB, 0xEF, 0xCB, 0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66,
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0, 0xEF, 0x48, 0xF0,
0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F, 0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98,
0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4, 0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE,
0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65, 0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA,
};
constexpr SerializedHyphenationPatterns fr_patterns = {
fr_trie_data,
sizeof(fr_trie_data),
};

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@ constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
const char* IMAGE_TAGS[] = {"img"};
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
const char* SKIP_TAGS[] = {"head", "table"};
const char* SKIP_TAGS[] = {"head"};
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
@ -51,7 +51,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) {
makePages();
}
currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing));
currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing, hyphenationEnabled));
}
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
@ -63,13 +63,44 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
return;
}
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
// TODO: Start processing image tags
// Special handling for tables - show placeholder text instead of dropping silently
if (strcmp(name, "table") == 0) {
// Add placeholder text
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
if (self->currentTextBlock) {
self->currentTextBlock->addWord("[Table omitted]", EpdFontFamily::ITALIC);
}
// Skip table contents
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
// TODO: Start processing image tags
std::string alt;
if (atts != nullptr) {
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "alt") == 0) {
alt = "[Image: " + std::string(atts[i + 1]) + "]";
}
}
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
self->depth += 1;
self->characterData(userData, alt.c_str(), alt.length());
} else {
// Skip for now
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
}
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
// start skip
self->skipUntilDepth = self->depth;
@ -97,6 +128,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->startNewTextBlock(self->currentTextBlock->getStyle());
} else {
self->startNewTextBlock((TextBlock::Style)self->paragraphAlignment);
if (strcmp(name, "li") == 0) {
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
}
}
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
@ -136,18 +170,17 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
continue;
}
// Skip soft-hyphen with UTF-8 representation (U+00AD) = 0xC2 0xAD
const XML_Char SHY_BYTE_1 = static_cast<XML_Char>(0xC2);
const XML_Char SHY_BYTE_2 = static_cast<XML_Char>(0xAD);
// 1. Check for the start of the 2-byte Soft Hyphen sequence
if (s[i] == SHY_BYTE_1) {
// 2. Check if the next byte exists AND if it completes the sequence
// We must check i + 1 < len to prevent reading past the end of the buffer.
if ((i + 1 < len) && (s[i + 1] == SHY_BYTE_2)) {
// Sequence 0xC2 0xAD found!
// Skip the current byte (0xC2) and the next byte (0xAD)
i++; // Increment 'i' one more time to skip the 0xAD byte
continue; // Skip the rest of the loop and move to the next iteration
// Skip Zero Width No-Break Space / BOM (U+FEFF) = 0xEF 0xBB 0xBF
const XML_Char FEFF_BYTE_1 = static_cast<XML_Char>(0xEF);
const XML_Char FEFF_BYTE_2 = static_cast<XML_Char>(0xBB);
const XML_Char FEFF_BYTE_3 = static_cast<XML_Char>(0xBF);
if (s[i] == FEFF_BYTE_1) {
// Check if the next two bytes complete the 3-byte sequence
if ((i + 2 < len) && (s[i + 1] == FEFF_BYTE_2) && (s[i + 2] == FEFF_BYTE_3)) {
// Sequence 0xEF 0xBB 0xBF found!
i += 2; // Skip the next two bytes
continue; // Move to the next iteration
}
}

View File

@ -36,6 +36,7 @@ class ChapterHtmlSlimParser {
uint8_t paragraphAlignment;
uint16_t viewportWidth;
uint16_t viewportHeight;
bool hyphenationEnabled;
void startNewTextBlock(TextBlock::Style style);
void makePages();
@ -48,7 +49,7 @@ class ChapterHtmlSlimParser {
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight,
const uint16_t viewportHeight, const bool hyphenationEnabled,
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
const std::function<void(int)>& progressFn = nullptr)
: filepath(filepath),
@ -59,6 +60,7 @@ class ChapterHtmlSlimParser {
paragraphAlignment(paragraphAlignment),
viewportWidth(viewportWidth),
viewportHeight(viewportHeight),
hyphenationEnabled(hyphenationEnabled),
completePageFn(completePageFn),
progressFn(progressFn) {}
~ChapterHtmlSlimParser() = default;

View File

@ -107,6 +107,11 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
return;
}
if (self->state == IN_METADATA && strcmp(name, "dc:language") == 0) {
self->state = IN_BOOK_LANGUAGE;
return;
}
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
self->state = IN_MANIFEST;
if (!SdMan.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
@ -266,6 +271,11 @@ void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s,
self->author.append(s, len);
return;
}
if (self->state == IN_BOOK_LANGUAGE) {
self->language.append(s, len);
return;
}
}
void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) {
@ -300,6 +310,11 @@ void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name)
return;
}
if (self->state == IN_BOOK_LANGUAGE && strcmp(name, "dc:language") == 0) {
self->state = IN_METADATA;
return;
}
if (self->state == IN_METADATA && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
self->state = IN_PACKAGE;
return;

View File

@ -13,6 +13,7 @@ class ContentOpfParser final : public Print {
IN_METADATA,
IN_BOOK_TITLE,
IN_BOOK_AUTHOR,
IN_BOOK_LANGUAGE,
IN_MANIFEST,
IN_SPINE,
IN_GUIDE,
@ -34,6 +35,7 @@ class ContentOpfParser final : public Print {
public:
std::string title;
std::string author;
std::string language;
std::string tocNcxPath;
std::string tocNavPath; // EPUB 3 nav document path
std::string coverItemHref;

View File

@ -228,7 +228,10 @@ BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const {
}
case 1: {
for (int x = 0; x < width; x++) {
lum = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 0xFF : 0x00;
// Get palette index (0 or 1) from bit at position x
const uint8_t palIndex = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 1 : 0;
// Use palette lookup for proper black/white mapping
lum = paletteLum[palIndex];
packPixel(lum);
}
break;

View File

@ -42,6 +42,8 @@ class Bitmap {
bool isTopDown() const { return topDown; }
bool hasGreyscale() const { return bpp > 1; }
int getRowBytes() const { return rowBytes; }
bool is1Bit() const { return bpp == 1; }
uint16_t getBpp() const { return bpp; }
private:
static uint16_t readLE16(FsFile& f);

View File

@ -88,3 +88,19 @@ uint8_t quantize(int gray, int x, int y) {
return quantizeSimple(gray);
}
}
// 1-bit noise dithering for fast home screen rendering
// Uses hash-based noise for consistent dithering that works well at small sizes
uint8_t quantize1bit(int gray, int x, int y) {
gray = adjustPixel(gray);
// Generate noise threshold using integer hash (no regular pattern to alias)
uint32_t hash = static_cast<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
hash = (hash ^ (hash >> 13)) * 1274126177u;
const int threshold = static_cast<int>(hash >> 24); // 0-255
// Simple threshold with noise: gray >= (128 + noise offset) -> white
// The noise adds variation around the 128 midpoint
const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192
return (gray >= adjustedThreshold) ? 1 : 0;
}

View File

@ -5,8 +5,89 @@
// Helper functions
uint8_t quantize(int gray, int x, int y);
uint8_t quantizeSimple(int gray);
uint8_t quantize1bit(int gray, int x, int y);
int adjustPixel(int gray);
// 1-bit Atkinson dithering - better quality than noise dithering for thumbnails
// Error distribution pattern (same as 2-bit but quantizes to 2 levels):
// X 1/8 1/8
// 1/8 1/8 1/8
// 1/8
class Atkinson1BitDitherer {
public:
explicit Atkinson1BitDitherer(int width) : width(width) {
errorRow0 = new int16_t[width + 4](); // Current row
errorRow1 = new int16_t[width + 4](); // Next row
errorRow2 = new int16_t[width + 4](); // Row after next
}
~Atkinson1BitDitherer() {
delete[] errorRow0;
delete[] errorRow1;
delete[] errorRow2;
}
// EXPLICITLY DELETE THE COPY CONSTRUCTOR
Atkinson1BitDitherer(const Atkinson1BitDitherer& other) = delete;
// EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR
Atkinson1BitDitherer& operator=(const Atkinson1BitDitherer& other) = delete;
uint8_t processPixel(int gray, int x) {
// Apply brightness/contrast/gamma adjustments
gray = adjustPixel(gray);
// Add accumulated error
int adjusted = gray + errorRow0[x + 2];
if (adjusted < 0) adjusted = 0;
if (adjusted > 255) adjusted = 255;
// Quantize to 2 levels (1-bit): 0 = black, 1 = white
uint8_t quantized;
int quantizedValue;
if (adjusted < 128) {
quantized = 0;
quantizedValue = 0;
} else {
quantized = 1;
quantizedValue = 255;
}
// Calculate error (only distribute 6/8 = 75%)
int error = (adjusted - quantizedValue) >> 3; // error/8
// Distribute 1/8 to each of 6 neighbors
errorRow0[x + 3] += error; // Right
errorRow0[x + 4] += error; // Right+1
errorRow1[x + 1] += error; // Bottom-left
errorRow1[x + 2] += error; // Bottom
errorRow1[x + 3] += error; // Bottom-right
errorRow2[x + 2] += error; // Two rows down
return quantized;
}
void nextRow() {
int16_t* temp = errorRow0;
errorRow0 = errorRow1;
errorRow1 = errorRow2;
errorRow2 = temp;
memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
}
void reset() {
memset(errorRow0, 0, (width + 4) * sizeof(int16_t));
memset(errorRow1, 0, (width + 4) * sizeof(int16_t));
memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
}
private:
int width;
int16_t* errorRow0;
int16_t* errorRow1;
int16_t* errorRow2;
};
// Atkinson dithering - distributes only 6/8 (75%) of error for cleaner results
// Error distribution pattern:
// X 1/8 1/8

View File

@ -163,6 +163,12 @@ void GfxRenderer::drawVal(const int x, const int y, const uint8_t val) const {
}
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
const float cropX, const float cropY, bool extend) const {
// For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit)
if (bitmap.is1Bit() && cropX == 0.0f && cropY == 0.0f) {
drawBitmap1Bit(bitmap, x, y, maxWidth, maxHeight);
return;
}
float scale = 1.0f;
bool isScaled = false;
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
@ -204,6 +210,9 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
if (screenY >= getScreenHeight()) {
break;
}
if (screenY < 0) {
continue;
}
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
Serial.printf("[%lu] [GFX] Failed to read row %d from bitmap\n", millis(), bmpY);
@ -226,6 +235,9 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
if (screenX >= getScreenWidth()) {
break;
}
if (screenX < 0) {
continue;
}
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
@ -316,6 +328,143 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
free(rowBytes);
}
void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y, const int maxWidth,
const int maxHeight) const {
float scale = 1.0f;
bool isScaled = false;
if (maxWidth > 0 && bitmap.getWidth() > maxWidth) {
scale = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
isScaled = true;
}
if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight()));
isScaled = true;
}
// For 1-bit BMP, output is still 2-bit packed (for consistency with readNextRow)
const int outputRowSize = (bitmap.getWidth() + 3) / 4;
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
if (!outputRow || !rowBytes) {
Serial.printf("[%lu] [GFX] !! Failed to allocate 1-bit BMP row buffers\n", millis());
free(outputRow);
free(rowBytes);
return;
}
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
// Read rows sequentially using readNextRow
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
Serial.printf("[%lu] [GFX] Failed to read row %d from 1-bit bitmap\n", millis(), bmpY);
free(outputRow);
free(rowBytes);
return;
}
// Calculate screen Y based on whether BMP is top-down or bottom-up
const int bmpYOffset = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
int screenY = y + (isScaled ? static_cast<int>(std::floor(bmpYOffset * scale)) : bmpYOffset);
if (screenY >= getScreenHeight()) {
continue; // Continue reading to keep row counter in sync
}
if (screenY < 0) {
continue;
}
for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) {
int screenX = x + (isScaled ? static_cast<int>(std::floor(bmpX * scale)) : bmpX);
if (screenX >= getScreenWidth()) {
break;
}
if (screenX < 0) {
continue;
}
// Get 2-bit value (result of readNextRow quantization)
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
// For 1-bit source: 0 or 1 -> map to black (0,1,2) or white (3)
// val < 3 means black pixel (draw it)
if (val < 3) {
drawPixel(screenX, screenY, true);
}
// White pixels (val == 3) are not drawn (leave background)
}
}
free(outputRow);
free(rowBytes);
}
void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state) const {
if (numPoints < 3) return;
// Find bounding box
int minY = yPoints[0], maxY = yPoints[0];
for (int i = 1; i < numPoints; i++) {
if (yPoints[i] < minY) minY = yPoints[i];
if (yPoints[i] > maxY) maxY = yPoints[i];
}
// Clip to screen
if (minY < 0) minY = 0;
if (maxY >= getScreenHeight()) maxY = getScreenHeight() - 1;
// Allocate node buffer for scanline algorithm
auto* nodeX = static_cast<int*>(malloc(numPoints * sizeof(int)));
if (!nodeX) {
Serial.printf("[%lu] [GFX] !! Failed to allocate polygon node buffer\n", millis());
return;
}
// Scanline fill algorithm
for (int scanY = minY; scanY <= maxY; scanY++) {
int nodes = 0;
// Find all intersection points with edges
int j = numPoints - 1;
for (int i = 0; i < numPoints; i++) {
if ((yPoints[i] < scanY && yPoints[j] >= scanY) || (yPoints[j] < scanY && yPoints[i] >= scanY)) {
// Calculate X intersection using fixed-point to avoid float
int dy = yPoints[j] - yPoints[i];
if (dy != 0) {
nodeX[nodes++] = xPoints[i] + (scanY - yPoints[i]) * (xPoints[j] - xPoints[i]) / dy;
}
}
j = i;
}
// Sort nodes by X (simple bubble sort, numPoints is small)
for (int i = 0; i < nodes - 1; i++) {
for (int k = i + 1; k < nodes; k++) {
if (nodeX[i] > nodeX[k]) {
int temp = nodeX[i];
nodeX[i] = nodeX[k];
nodeX[k] = temp;
}
}
}
// Fill between pairs of nodes
for (int i = 0; i < nodes - 1; i += 2) {
int startX = nodeX[i];
int endX = nodeX[i + 1];
// Clip to screen
if (startX < 0) startX = 0;
if (endX >= getScreenWidth()) endX = getScreenWidth() - 1;
// Draw horizontal line
for (int x = startX; x <= endX; x++) {
drawPixel(x, scanY, state);
}
}
}
free(nodeX);
}
void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); }
void GfxRenderer::invertScreen() const {
@ -401,7 +550,10 @@ int GfxRenderer::getLineHeight(const int fontId) const {
}
void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3,
const char* btn4) const {
const char* btn4) {
const Orientation orig_orientation = getOrientation();
setOrientation(Orientation::Portrait);
const int pageHeight = getScreenHeight();
constexpr int buttonWidth = 106;
constexpr int buttonHeight = 40;
@ -414,12 +566,15 @@ void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char
// Only draw if the label is non-empty
if (labels[i] != nullptr && labels[i][0] != '\0') {
const int x = buttonPositions[i];
fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false);
drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight);
const int textWidth = getTextWidth(fontId, labels[i]);
const int textX = x + (buttonWidth - 1 - textWidth) / 2;
drawText(fontId, textX, pageHeight - buttonY + textYOffset, labels[i]);
}
}
setOrientation(orig_orientation);
}
void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, const char* bottomBtn) const {
@ -509,7 +664,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
const EpdGlyph* glyph = font.getGlyph(cp, style);
if (!glyph) {
glyph = font.getGlyph('?', style);
glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
}
if (!glyph) {
continue;
@ -687,8 +842,7 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
const bool pixelState, const EpdFontFamily::Style style) const {
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
if (!glyph) {
// TODO: Replace with fallback glyph property?
glyph = fontFamily.getGlyph('?', style);
glyph = fontFamily.getGlyph(REPLACEMENT_GLYPH, style);
}
// no glyph?

View File

@ -69,6 +69,8 @@ class GfxRenderer {
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0, float cropY = 0,
bool extend = false) const;
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
// Text
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
@ -83,7 +85,7 @@ class GfxRenderer {
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
// UI Components
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const;
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4);
void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn) const;
private:
@ -98,8 +100,8 @@ class GfxRenderer {
void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const;
bool storeBwBuffer(); // Returns true if buffer was stored successfully
void restoreBwBuffer();
bool storeBwBuffer(); // Returns true if buffer was stored successfully
void restoreBwBuffer(); // Restore and free the stored buffer
void cleanupGrayscaleWithFrameBuffer() const;
// Low level functions

View File

@ -87,8 +87,47 @@ void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) {
}
}
// Helper function: Write BMP header with 1-bit color depth (black and white)
static void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) {
// Calculate row padding (each row must be multiple of 4 bytes)
const int bytesPerRow = (width + 31) / 32 * 4; // 1 bit per pixel, round up to 4-byte boundary
const int imageSize = bytesPerRow * height;
const uint32_t fileSize = 62 + imageSize; // 14 (file header) + 40 (DIB header) + 8 (palette) + image
// BMP File Header (14 bytes)
bmpOut.write('B');
bmpOut.write('M');
write32(bmpOut, fileSize); // File size
write32(bmpOut, 0); // Reserved
write32(bmpOut, 62); // Offset to pixel data (14 + 40 + 8)
// DIB Header (BITMAPINFOHEADER - 40 bytes)
write32(bmpOut, 40);
write32Signed(bmpOut, width);
write32Signed(bmpOut, -height); // Negative height = top-down bitmap
write16(bmpOut, 1); // Color planes
write16(bmpOut, 1); // Bits per pixel (1 bit)
write32(bmpOut, 0); // BI_RGB (no compression)
write32(bmpOut, imageSize);
write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI)
write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI)
write32(bmpOut, 2); // colorsUsed
write32(bmpOut, 2); // colorsImportant
// Color Palette (2 colors x 4 bytes = 8 bytes)
// Format: Blue, Green, Red, Reserved (BGRA)
// Note: In 1-bit BMP, palette index 0 = black, 1 = white
uint8_t palette[8] = {
0x00, 0x00, 0x00, 0x00, // Color 0: Black
0xFF, 0xFF, 0xFF, 0x00 // Color 1: White
};
for (const uint8_t i : palette) {
bmpOut.write(i);
}
}
// Helper function: Write BMP header with 2-bit color depth
void JpegToBmpConverter::writeBmpHeader(Print& bmpOut, const int width, const int height) {
static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) {
// Calculate row padding (each row must be multiple of 4 bytes)
const int bytesPerRow = (width * 2 + 31) / 32 * 4; // 2 bits per pixel, round up
const int imageSize = bytesPerRow * height;
@ -159,9 +198,11 @@ unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const un
return 0; // Success
}
// Core function: Convert JPEG file to 2-bit BMP
bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) {
Serial.printf("[%lu] [JPG] Converting JPEG to BMP\n", millis());
// Internal implementation with configurable target size and bit depth
bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
bool oneBit) {
Serial.printf("[%lu] [JPG] Converting JPEG to %s BMP (target: %dx%d)\n", millis(), oneBit ? "1-bit" : "2-bit",
targetWidth, targetHeight);
// Setup context for picojpeg callback
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
@ -196,10 +237,10 @@ bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) {
uint32_t scaleY_fp = 65536;
bool needsScaling = false;
if (USE_PRESCALE && (imageInfo.m_width > TARGET_MAX_WIDTH || imageInfo.m_height > TARGET_MAX_HEIGHT)) {
if (targetWidth > 0 && targetHeight > 0 && (imageInfo.m_width > targetWidth || imageInfo.m_height > targetHeight)) {
// Calculate scale to fit within target dimensions while maintaining aspect ratio
const float scaleToFitWidth = static_cast<float>(TARGET_MAX_WIDTH) / imageInfo.m_width;
const float scaleToFitHeight = static_cast<float>(TARGET_MAX_HEIGHT) / imageInfo.m_height;
const float scaleToFitWidth = static_cast<float>(targetWidth) / imageInfo.m_width;
const float scaleToFitHeight = static_cast<float>(targetHeight) / imageInfo.m_height;
// We scale to the smaller dimension, so we can potentially crop later.
// TODO: ideally, we already crop here.
const float scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
@ -218,16 +259,19 @@ bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) {
needsScaling = true;
Serial.printf("[%lu] [JPG] Pre-scaling %dx%d -> %dx%d (fit to %dx%d)\n", millis(), imageInfo.m_width,
imageInfo.m_height, outWidth, outHeight, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT);
imageInfo.m_height, outWidth, outHeight, targetWidth, targetHeight);
}
// Write BMP header with output dimensions
int bytesPerRow;
if (USE_8BIT_OUTPUT) {
if (USE_8BIT_OUTPUT && !oneBit) {
writeBmpHeader8bit(bmpOut, outWidth, outHeight);
bytesPerRow = (outWidth + 3) / 4 * 4;
} else if (oneBit) {
writeBmpHeader1bit(bmpOut, outWidth, outHeight);
bytesPerRow = (outWidth + 31) / 32 * 4; // 1 bit per pixel
} else {
writeBmpHeader(bmpOut, outWidth, outHeight);
writeBmpHeader2bit(bmpOut, outWidth, outHeight);
bytesPerRow = (outWidth * 2 + 31) / 32 * 4;
}
@ -258,11 +302,16 @@ bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) {
return false;
}
// Create ditherer if enabled (only for 2-bit output)
// Create ditherer if enabled
// Use OUTPUT dimensions for dithering (after prescaling)
AtkinsonDitherer* atkinsonDitherer = nullptr;
FloydSteinbergDitherer* fsDitherer = nullptr;
if (!USE_8BIT_OUTPUT) {
Atkinson1BitDitherer* atkinson1BitDitherer = nullptr;
if (oneBit) {
// For 1-bit output, use Atkinson dithering for better quality
atkinson1BitDitherer = new Atkinson1BitDitherer(outWidth);
} else if (!USE_8BIT_OUTPUT) {
if (USE_ATKINSON) {
atkinsonDitherer = new AtkinsonDitherer(outWidth);
} else if (USE_FLOYD_STEINBERG) {
@ -348,12 +397,25 @@ bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) {
// No scaling - direct output (1:1 mapping)
memset(rowBuffer, 0, bytesPerRow);
if (USE_8BIT_OUTPUT) {
if (USE_8BIT_OUTPUT && !oneBit) {
for (int x = 0; x < outWidth; x++) {
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
rowBuffer[x] = adjustPixel(gray);
}
} else if (oneBit) {
// 1-bit output with Atkinson dithering for better quality
for (int x = 0; x < outWidth; x++) {
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
const uint8_t bit =
atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x) : quantize1bit(gray, x, y);
// Pack 1-bit value: MSB first, 8 pixels per byte
const int byteIndex = x / 8;
const int bitOffset = 7 - (x % 8);
rowBuffer[byteIndex] |= (bit << bitOffset);
}
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
} else {
// 2-bit output
for (int x = 0; x < outWidth; x++) {
const uint8_t gray = adjustPixel(mcuRowBuffer[bufferY * imageInfo.m_width + x]);
uint8_t twoBit;
@ -411,12 +473,25 @@ bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) {
if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) {
memset(rowBuffer, 0, bytesPerRow);
if (USE_8BIT_OUTPUT) {
if (USE_8BIT_OUTPUT && !oneBit) {
for (int x = 0; x < outWidth; x++) {
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
rowBuffer[x] = adjustPixel(gray);
}
} else if (oneBit) {
// 1-bit output with Atkinson dithering for better quality
for (int x = 0; x < outWidth; x++) {
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
const uint8_t bit = atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x)
: quantize1bit(gray, x, currentOutY);
// Pack 1-bit value: MSB first, 8 pixels per byte
const int byteIndex = x / 8;
const int bitOffset = 7 - (x % 8);
rowBuffer[byteIndex] |= (bit << bitOffset);
}
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
} else {
// 2-bit output
for (int x = 0; x < outWidth; x++) {
const uint8_t gray = adjustPixel((rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0);
uint8_t twoBit;
@ -464,9 +539,29 @@ bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) {
if (fsDitherer) {
delete fsDitherer;
}
if (atkinson1BitDitherer) {
delete atkinson1BitDitherer;
}
free(mcuRowBuffer);
free(rowBuffer);
Serial.printf("[%lu] [JPG] Successfully converted JPEG to BMP\n", millis());
return true;
}
// Core function: Convert JPEG file to 2-bit BMP (uses default target size)
bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) {
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false);
}
// Convert with custom target size (for thumbnails, 2-bit)
bool JpegToBmpConverter::jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth,
int targetMaxHeight) {
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, false);
}
// Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering
bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth,
int targetMaxHeight) {
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true);
}

View File

@ -5,11 +5,15 @@ class Print;
class ZipFile;
class JpegToBmpConverter {
static void writeBmpHeader(Print& bmpOut, int width, int height);
// [COMMENTED OUT] static uint8_t grayscaleTo2Bit(uint8_t grayscale, int x, int y);
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
unsigned char* pBytes_actually_read, void* pCallback_data);
static bool jpegFileToBmpStreamInternal(class FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
bool oneBit);
public:
static bool jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut);
// Convert with custom target size (for thumbnails)
static bool jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
// Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering
static bool jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
};

View File

@ -0,0 +1,168 @@
#include "KOReaderCredentialStore.h"
#include <HardwareSerial.h>
#include <MD5Builder.h>
#include <SDCardManager.h>
#include <Serialization.h>
// Initialize the static instance
KOReaderCredentialStore KOReaderCredentialStore::instance;
namespace {
// File format version
constexpr uint8_t KOREADER_FILE_VERSION = 1;
// KOReader credentials file path
constexpr char KOREADER_FILE[] = "/.crosspoint/koreader.bin";
// Default sync server URL
constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443";
// Obfuscation key - "KOReader" in ASCII
// This is NOT cryptographic security, just prevents casual file reading
constexpr uint8_t OBFUSCATION_KEY[] = {0x4B, 0x4F, 0x52, 0x65, 0x61, 0x64, 0x65, 0x72};
constexpr size_t KEY_LENGTH = sizeof(OBFUSCATION_KEY);
} // namespace
void KOReaderCredentialStore::obfuscate(std::string& data) const {
for (size_t i = 0; i < data.size(); i++) {
data[i] ^= OBFUSCATION_KEY[i % KEY_LENGTH];
}
}
bool KOReaderCredentialStore::saveToFile() const {
// Make sure the directory exists
SdMan.mkdir("/.crosspoint");
FsFile file;
if (!SdMan.openFileForWrite("KRS", KOREADER_FILE, file)) {
return false;
}
// Write header
serialization::writePod(file, KOREADER_FILE_VERSION);
// Write username (plaintext - not particularly sensitive)
serialization::writeString(file, username);
Serial.printf("[%lu] [KRS] Saving username: %s\n", millis(), username.c_str());
// Write password (obfuscated)
std::string obfuscatedPwd = password;
obfuscate(obfuscatedPwd);
serialization::writeString(file, obfuscatedPwd);
// Write server URL
serialization::writeString(file, serverUrl);
// Write match method
serialization::writePod(file, static_cast<uint8_t>(matchMethod));
file.close();
Serial.printf("[%lu] [KRS] Saved KOReader credentials to file\n", millis());
return true;
}
bool KOReaderCredentialStore::loadFromFile() {
FsFile file;
if (!SdMan.openFileForRead("KRS", KOREADER_FILE, file)) {
Serial.printf("[%lu] [KRS] No credentials file found\n", millis());
return false;
}
// Read and verify version
uint8_t version;
serialization::readPod(file, version);
if (version != KOREADER_FILE_VERSION) {
Serial.printf("[%lu] [KRS] Unknown file version: %u\n", millis(), version);
file.close();
return false;
}
// Read username
if (file.available()) {
serialization::readString(file, username);
} else {
username.clear();
}
// Read and deobfuscate password
if (file.available()) {
serialization::readString(file, password);
obfuscate(password); // XOR is symmetric, so same function deobfuscates
} else {
password.clear();
}
// Read server URL
if (file.available()) {
serialization::readString(file, serverUrl);
} else {
serverUrl.clear();
}
// Read match method
if (file.available()) {
uint8_t method;
serialization::readPod(file, method);
matchMethod = static_cast<DocumentMatchMethod>(method);
} else {
matchMethod = DocumentMatchMethod::FILENAME;
}
file.close();
Serial.printf("[%lu] [KRS] Loaded KOReader credentials for user: %s\n", millis(), username.c_str());
return true;
}
void KOReaderCredentialStore::setCredentials(const std::string& user, const std::string& pass) {
username = user;
password = pass;
Serial.printf("[%lu] [KRS] Set credentials for user: %s\n", millis(), user.c_str());
}
std::string KOReaderCredentialStore::getMd5Password() const {
if (password.empty()) {
return "";
}
// Calculate MD5 hash of password using ESP32's MD5Builder
MD5Builder md5;
md5.begin();
md5.add(password.c_str());
md5.calculate();
return md5.toString().c_str();
}
bool KOReaderCredentialStore::hasCredentials() const { return !username.empty() && !password.empty(); }
void KOReaderCredentialStore::clearCredentials() {
username.clear();
password.clear();
saveToFile();
Serial.printf("[%lu] [KRS] Cleared KOReader credentials\n", millis());
}
void KOReaderCredentialStore::setServerUrl(const std::string& url) {
serverUrl = url;
Serial.printf("[%lu] [KRS] Set server URL: %s\n", millis(), url.empty() ? "(default)" : url.c_str());
}
std::string KOReaderCredentialStore::getBaseUrl() const {
if (serverUrl.empty()) {
return DEFAULT_SERVER_URL;
}
// Normalize URL: add http:// if no protocol specified (local servers typically don't have SSL)
if (serverUrl.find("://") == std::string::npos) {
return "http://" + serverUrl;
}
return serverUrl;
}
void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
matchMethod = method;
Serial.printf("[%lu] [KRS] Set match method: %s\n", millis(),
method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
}

View File

@ -0,0 +1,69 @@
#pragma once
#include <cstdint>
#include <string>
// Document matching method for KOReader sync
enum class DocumentMatchMethod : uint8_t {
FILENAME = 0, // Match by filename (simpler, works across different file sources)
BINARY = 1, // Match by partial MD5 of file content (more accurate, but files must be identical)
};
/**
* Singleton class for storing KOReader sync credentials on the SD card.
* Credentials are stored in /sd/.crosspoint/koreader.bin with basic
* XOR obfuscation to prevent casual reading (not cryptographically secure).
*/
class KOReaderCredentialStore {
private:
static KOReaderCredentialStore instance;
std::string username;
std::string password;
std::string serverUrl; // Custom sync server URL (empty = default)
DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility
// Private constructor for singleton
KOReaderCredentialStore() = default;
// XOR obfuscation (symmetric - same for encode/decode)
void obfuscate(std::string& data) const;
public:
// Delete copy constructor and assignment
KOReaderCredentialStore(const KOReaderCredentialStore&) = delete;
KOReaderCredentialStore& operator=(const KOReaderCredentialStore&) = delete;
// Get singleton instance
static KOReaderCredentialStore& getInstance() { return instance; }
// Save/load from SD card
bool saveToFile() const;
bool loadFromFile();
// Credential management
void setCredentials(const std::string& user, const std::string& pass);
const std::string& getUsername() const { return username; }
const std::string& getPassword() const { return password; }
// Get MD5 hash of password for API authentication
std::string getMd5Password() const;
// Check if credentials are set
bool hasCredentials() const;
// Clear credentials
void clearCredentials();
// Server URL management
void setServerUrl(const std::string& url);
const std::string& getServerUrl() const { return serverUrl; }
// Get base URL for API calls (with http:// normalization if no protocol, falls back to default)
std::string getBaseUrl() const;
// Document matching method
void setMatchMethod(DocumentMatchMethod method);
DocumentMatchMethod getMatchMethod() const { return matchMethod; }
};
// Helper macro to access credential store
#define KOREADER_STORE KOReaderCredentialStore::getInstance()

View File

@ -0,0 +1,96 @@
#include "KOReaderDocumentId.h"
#include <HardwareSerial.h>
#include <MD5Builder.h>
#include <SDCardManager.h>
namespace {
// Extract filename from path (everything after last '/')
std::string getFilename(const std::string& path) {
const size_t pos = path.rfind('/');
if (pos == std::string::npos) {
return path;
}
return path.substr(pos + 1);
}
} // namespace
std::string KOReaderDocumentId::calculateFromFilename(const std::string& filePath) {
const std::string filename = getFilename(filePath);
if (filename.empty()) {
return "";
}
MD5Builder md5;
md5.begin();
md5.add(filename.c_str());
md5.calculate();
std::string result = md5.toString().c_str();
Serial.printf("[%lu] [KODoc] Filename hash: %s (from '%s')\n", millis(), result.c_str(), filename.c_str());
return result;
}
size_t KOReaderDocumentId::getOffset(int i) {
// Offset = 1024 << (2*i)
// For i = -1: 1024 >> 2 = 256
// For i >= 0: 1024 << (2*i)
if (i < 0) {
return CHUNK_SIZE >> (-2 * i);
}
return CHUNK_SIZE << (2 * i);
}
std::string KOReaderDocumentId::calculate(const std::string& filePath) {
FsFile file;
if (!SdMan.openFileForRead("KODoc", filePath, file)) {
Serial.printf("[%lu] [KODoc] Failed to open file: %s\n", millis(), filePath.c_str());
return "";
}
const size_t fileSize = file.fileSize();
Serial.printf("[%lu] [KODoc] Calculating hash for file: %s (size: %zu)\n", millis(), filePath.c_str(), fileSize);
// Initialize MD5 builder
MD5Builder md5;
md5.begin();
// Buffer for reading chunks
uint8_t buffer[CHUNK_SIZE];
size_t totalBytesRead = 0;
// Read from each offset (i = -1 to 10)
for (int i = -1; i < OFFSET_COUNT - 1; i++) {
const size_t offset = getOffset(i);
// Skip if offset is beyond file size
if (offset >= fileSize) {
continue;
}
// Seek to offset
if (!file.seekSet(offset)) {
Serial.printf("[%lu] [KODoc] Failed to seek to offset %zu\n", millis(), offset);
continue;
}
// Read up to CHUNK_SIZE bytes
const size_t bytesToRead = std::min(CHUNK_SIZE, fileSize - offset);
const size_t bytesRead = file.read(buffer, bytesToRead);
if (bytesRead > 0) {
md5.add(buffer, bytesRead);
totalBytesRead += bytesRead;
}
}
file.close();
// Calculate final hash
md5.calculate();
std::string result = md5.toString().c_str();
Serial.printf("[%lu] [KODoc] Hash calculated: %s (from %zu bytes)\n", millis(), result.c_str(), totalBytesRead);
return result;
}

View File

@ -0,0 +1,45 @@
#pragma once
#include <string>
/**
* Calculate KOReader document ID (partial MD5 hash).
*
* KOReader identifies documents using a partial MD5 hash of the file content.
* The algorithm reads 1024 bytes at specific offsets and computes the MD5 hash
* of the concatenated data.
*
* Offsets are calculated as: 1024 << (2*i) for i = -1 to 10
* Producing: 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304,
* 16777216, 67108864, 268435456, 1073741824 bytes
*
* If an offset is beyond the file size, it is skipped.
*/
class KOReaderDocumentId {
public:
/**
* Calculate the KOReader document hash for a file (binary/content-based).
*
* @param filePath Path to the file (typically an EPUB)
* @return 32-character lowercase hex string, or empty string on failure
*/
static std::string calculate(const std::string& filePath);
/**
* Calculate document hash from filename only (filename-based sync mode).
* This is simpler and works when files have the same name across devices.
*
* @param filePath Path to the file (only the filename portion is used)
* @return 32-character lowercase hex MD5 of the filename
*/
static std::string calculateFromFilename(const std::string& filePath);
private:
// Size of each chunk to read at each offset
static constexpr size_t CHUNK_SIZE = 1024;
// Number of offsets to try (i = -1 to 10, so 12 offsets)
static constexpr int OFFSET_COUNT = 12;
// Calculate offset for index i: 1024 << (2*i)
static size_t getOffset(int i);
};

View File

@ -0,0 +1,198 @@
#include "KOReaderSyncClient.h"
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <HardwareSerial.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ctime>
#include "KOReaderCredentialStore.h"
namespace {
// Device identifier for CrossPoint reader
constexpr char DEVICE_NAME[] = "CrossPoint";
constexpr char DEVICE_ID[] = "crosspoint-reader";
void addAuthHeaders(HTTPClient& http) {
http.addHeader("Accept", "application/vnd.koreader.v1+json");
http.addHeader("x-auth-user", KOREADER_STORE.getUsername().c_str());
http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password().c_str());
}
bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; }
} // namespace
KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
if (!KOREADER_STORE.hasCredentials()) {
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
return NO_CREDENTIALS;
}
std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
Serial.printf("[%lu] [KOSync] Authenticating: %s\n", millis(), url.c_str());
HTTPClient http;
std::unique_ptr<WiFiClientSecure> secureClient;
WiFiClient plainClient;
if (isHttpsUrl(url)) {
secureClient.reset(new WiFiClientSecure);
secureClient->setInsecure();
http.begin(*secureClient, url.c_str());
} else {
http.begin(plainClient, url.c_str());
}
addAuthHeaders(http);
const int httpCode = http.GET();
http.end();
Serial.printf("[%lu] [KOSync] Auth response: %d\n", millis(), httpCode);
if (httpCode == 200) {
return OK;
} else if (httpCode == 401) {
return AUTH_FAILED;
} else if (httpCode < 0) {
return NETWORK_ERROR;
}
return SERVER_ERROR;
}
KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash,
KOReaderProgress& outProgress) {
if (!KOREADER_STORE.hasCredentials()) {
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
return NO_CREDENTIALS;
}
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
Serial.printf("[%lu] [KOSync] Getting progress: %s\n", millis(), url.c_str());
HTTPClient http;
std::unique_ptr<WiFiClientSecure> secureClient;
WiFiClient plainClient;
if (isHttpsUrl(url)) {
secureClient.reset(new WiFiClientSecure);
secureClient->setInsecure();
http.begin(*secureClient, url.c_str());
} else {
http.begin(plainClient, url.c_str());
}
addAuthHeaders(http);
const int httpCode = http.GET();
if (httpCode == 200) {
// Parse JSON response from response string
String responseBody = http.getString();
http.end();
JsonDocument doc;
const DeserializationError error = deserializeJson(doc, responseBody);
if (error) {
Serial.printf("[%lu] [KOSync] JSON parse failed: %s\n", millis(), error.c_str());
return JSON_ERROR;
}
outProgress.document = documentHash;
outProgress.progress = doc["progress"].as<std::string>();
outProgress.percentage = doc["percentage"].as<float>();
outProgress.device = doc["device"].as<std::string>();
outProgress.deviceId = doc["device_id"].as<std::string>();
outProgress.timestamp = doc["timestamp"].as<int64_t>();
Serial.printf("[%lu] [KOSync] Got progress: %.2f%% at %s\n", millis(), outProgress.percentage * 100,
outProgress.progress.c_str());
return OK;
}
http.end();
Serial.printf("[%lu] [KOSync] Get progress response: %d\n", millis(), httpCode);
if (httpCode == 401) {
return AUTH_FAILED;
} else if (httpCode == 404) {
return NOT_FOUND;
} else if (httpCode < 0) {
return NETWORK_ERROR;
}
return SERVER_ERROR;
}
KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgress& progress) {
if (!KOREADER_STORE.hasCredentials()) {
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
return NO_CREDENTIALS;
}
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
Serial.printf("[%lu] [KOSync] Updating progress: %s\n", millis(), url.c_str());
HTTPClient http;
std::unique_ptr<WiFiClientSecure> secureClient;
WiFiClient plainClient;
if (isHttpsUrl(url)) {
secureClient.reset(new WiFiClientSecure);
secureClient->setInsecure();
http.begin(*secureClient, url.c_str());
} else {
http.begin(plainClient, url.c_str());
}
addAuthHeaders(http);
http.addHeader("Content-Type", "application/json");
// Build JSON body (timestamp not required per API spec)
JsonDocument doc;
doc["document"] = progress.document;
doc["progress"] = progress.progress;
doc["percentage"] = progress.percentage;
doc["device"] = DEVICE_NAME;
doc["device_id"] = DEVICE_ID;
std::string body;
serializeJson(doc, body);
Serial.printf("[%lu] [KOSync] Request body: %s\n", millis(), body.c_str());
const int httpCode = http.PUT(body.c_str());
http.end();
Serial.printf("[%lu] [KOSync] Update progress response: %d\n", millis(), httpCode);
if (httpCode == 200 || httpCode == 202) {
return OK;
} else if (httpCode == 401) {
return AUTH_FAILED;
} else if (httpCode < 0) {
return NETWORK_ERROR;
}
return SERVER_ERROR;
}
const char* KOReaderSyncClient::errorString(Error error) {
switch (error) {
case OK:
return "Success";
case NO_CREDENTIALS:
return "No credentials configured";
case NETWORK_ERROR:
return "Network error";
case AUTH_FAILED:
return "Authentication failed";
case SERVER_ERROR:
return "Server error (try again later)";
case JSON_ERROR:
return "JSON parse error";
case NOT_FOUND:
return "No progress found";
default:
return "Unknown error";
}
}

View File

@ -0,0 +1,59 @@
#pragma once
#include <string>
/**
* Progress data from KOReader sync server.
*/
struct KOReaderProgress {
std::string document; // Document hash
std::string progress; // XPath-like progress string
float percentage; // Progress percentage (0.0 to 1.0)
std::string device; // Device name
std::string deviceId; // Device ID
int64_t timestamp; // Unix timestamp of last update
};
/**
* HTTP client for KOReader sync API.
*
* Base URL: https://sync.koreader.rocks:443/
*
* API Endpoints:
* GET /users/auth - Authenticate (validate credentials)
* GET /syncs/progress/:document - Get progress for a document
* PUT /syncs/progress - Update progress for a document
*
* Authentication:
* x-auth-user: username
* x-auth-key: MD5 hash of password
*/
class KOReaderSyncClient {
public:
enum Error { OK = 0, NO_CREDENTIALS, NETWORK_ERROR, AUTH_FAILED, SERVER_ERROR, JSON_ERROR, NOT_FOUND };
/**
* Authenticate with the sync server (validate credentials).
* @return OK on success, error code on failure
*/
static Error authenticate();
/**
* Get reading progress for a document.
* @param documentHash The document hash (from KOReaderDocumentId)
* @param outProgress Output: the progress data
* @return OK on success, NOT_FOUND if no progress exists, error code on failure
*/
static Error getProgress(const std::string& documentHash, KOReaderProgress& outProgress);
/**
* Update reading progress for a document.
* @param progress The progress data to upload
* @return OK on success, error code on failure
*/
static Error updateProgress(const KOReaderProgress& progress);
/**
* Get human-readable error message.
*/
static const char* errorString(Error error);
};

View File

@ -0,0 +1,112 @@
#include "ProgressMapper.h"
#include <HardwareSerial.h>
#include <cmath>
KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, const CrossPointPosition& pos) {
KOReaderPosition result;
// Calculate page progress within current spine item
float intraSpineProgress = 0.0f;
if (pos.totalPages > 0) {
intraSpineProgress = static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages);
}
// Calculate overall book progress (0.0-1.0)
result.percentage = epub->calculateProgress(pos.spineIndex, intraSpineProgress);
// Generate XPath with estimated paragraph position based on page
result.xpath = generateXPath(pos.spineIndex, pos.pageNumber, pos.totalPages);
// Get chapter info for logging
const int tocIndex = epub->getTocIndexForSpineIndex(pos.spineIndex);
const std::string chapterName = (tocIndex >= 0) ? epub->getTocItem(tocIndex).title : "unknown";
Serial.printf("[%lu] [ProgressMapper] CrossPoint -> KOReader: chapter='%s', page=%d/%d -> %.2f%% at %s\n", millis(),
chapterName.c_str(), pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str());
return result;
}
CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epub, const KOReaderPosition& koPos,
int totalPagesInSpine) {
CrossPointPosition result;
result.spineIndex = 0;
result.pageNumber = 0;
result.totalPages = totalPagesInSpine;
const size_t bookSize = epub->getBookSize();
if (bookSize == 0) {
return result;
}
// First, try to get spine index from XPath (DocFragment)
int xpathSpineIndex = parseDocFragmentIndex(koPos.xpath);
if (xpathSpineIndex >= 0 && xpathSpineIndex < epub->getSpineItemsCount()) {
result.spineIndex = xpathSpineIndex;
// When we have XPath, go to page 0 of the spine - byte-based page calculation is unreliable
result.pageNumber = 0;
} else {
// Fall back to percentage-based lookup for both spine and page
const size_t targetBytes = static_cast<size_t>(bookSize * koPos.percentage);
// Find the spine item that contains this byte position
for (int i = 0; i < epub->getSpineItemsCount(); i++) {
const size_t cumulativeSize = epub->getCumulativeSpineItemSize(i);
if (cumulativeSize >= targetBytes) {
result.spineIndex = i;
break;
}
}
// Estimate page number within the spine item using percentage (only when no XPath)
if (totalPagesInSpine > 0 && result.spineIndex < epub->getSpineItemsCount()) {
const size_t prevCumSize = (result.spineIndex > 0) ? epub->getCumulativeSpineItemSize(result.spineIndex - 1) : 0;
const size_t currentCumSize = epub->getCumulativeSpineItemSize(result.spineIndex);
const size_t spineSize = currentCumSize - prevCumSize;
if (spineSize > 0) {
const size_t bytesIntoSpine = (targetBytes > prevCumSize) ? (targetBytes - prevCumSize) : 0;
const float intraSpineProgress = static_cast<float>(bytesIntoSpine) / static_cast<float>(spineSize);
const float clampedProgress = std::max(0.0f, std::min(1.0f, intraSpineProgress));
result.pageNumber = static_cast<int>(clampedProgress * totalPagesInSpine);
result.pageNumber = std::max(0, std::min(result.pageNumber, totalPagesInSpine - 1));
}
}
}
Serial.printf("[%lu] [ProgressMapper] KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d\n", millis(),
koPos.percentage * 100, koPos.xpath.c_str(), result.spineIndex, result.pageNumber);
return result;
}
std::string ProgressMapper::generateXPath(int spineIndex, int pageNumber, int totalPages) {
// KOReader uses 1-based DocFragment indices
// Use a simple xpath pointing to the DocFragment - KOReader will use the percentage for fine positioning
// Avoid specifying paragraph numbers as they may not exist in the target document
return "/body/DocFragment[" + std::to_string(spineIndex + 1) + "]/body";
}
int ProgressMapper::parseDocFragmentIndex(const std::string& xpath) {
// Look for DocFragment[N] pattern
const size_t start = xpath.find("DocFragment[");
if (start == std::string::npos) {
return -1;
}
const size_t numStart = start + 12; // Length of "DocFragment["
const size_t numEnd = xpath.find(']', numStart);
if (numEnd == std::string::npos) {
return -1;
}
try {
const int docFragmentIndex = std::stoi(xpath.substr(numStart, numEnd - numStart));
// KOReader uses 1-based indices, we use 0-based
return docFragmentIndex - 1;
} catch (...) {
return -1;
}
}

View File

@ -0,0 +1,72 @@
#pragma once
#include <Epub.h>
#include <memory>
#include <string>
/**
* CrossPoint position representation.
*/
struct CrossPointPosition {
int spineIndex; // Current spine item (chapter) index
int pageNumber; // Current page within the spine item
int totalPages; // Total pages in the current spine item
};
/**
* KOReader position representation.
*/
struct KOReaderPosition {
std::string xpath; // XPath-like progress string
float percentage; // Progress percentage (0.0 to 1.0)
};
/**
* Maps between CrossPoint and KOReader position formats.
*
* CrossPoint tracks position as (spineIndex, pageNumber).
* KOReader uses XPath-like strings + percentage.
*
* Since CrossPoint discards HTML structure during parsing, we generate
* synthetic XPath strings based on spine index, using percentage as the
* primary sync mechanism.
*/
class ProgressMapper {
public:
/**
* Convert CrossPoint position to KOReader format.
*
* @param epub The EPUB book
* @param pos CrossPoint position
* @return KOReader position
*/
static KOReaderPosition toKOReader(const std::shared_ptr<Epub>& epub, const CrossPointPosition& pos);
/**
* Convert KOReader position to CrossPoint format.
*
* Note: The returned pageNumber may be approximate since different
* rendering settings produce different page counts.
*
* @param epub The EPUB book
* @param koPos KOReader position
* @param totalPagesInSpine Total pages in the target spine item (for page estimation)
* @return CrossPoint position
*/
static CrossPointPosition toCrossPoint(const std::shared_ptr<Epub>& epub, const KOReaderPosition& koPos,
int totalPagesInSpine = 0);
private:
/**
* Generate XPath for KOReader compatibility.
* Format: /body/DocFragment[spineIndex+1]/body/p[estimatedParagraph]
* Paragraph is estimated based on page position within the chapter.
*/
static std::string generateXPath(int spineIndex, int pageNumber, int totalPages);
/**
* Parse DocFragment index from XPath string.
* Returns -1 if not found.
*/
static int parseDocFragmentIndex(const std::string& xpath);
};

191
lib/Txt/Txt.cpp Normal file
View File

@ -0,0 +1,191 @@
#include "Txt.h"
#include <FsHelpers.h>
#include <JpegToBmpConverter.h>
Txt::Txt(std::string path, std::string cacheBasePath)
: filepath(std::move(path)), cacheBasePath(std::move(cacheBasePath)) {
// Generate cache path from file path hash
const size_t hash = std::hash<std::string>{}(filepath);
cachePath = this->cacheBasePath + "/txt_" + std::to_string(hash);
}
bool Txt::load() {
if (loaded) {
return true;
}
if (!SdMan.exists(filepath.c_str())) {
Serial.printf("[%lu] [TXT] File does not exist: %s\n", millis(), filepath.c_str());
return false;
}
FsFile file;
if (!SdMan.openFileForRead("TXT", filepath, file)) {
Serial.printf("[%lu] [TXT] Failed to open file: %s\n", millis(), filepath.c_str());
return false;
}
fileSize = file.size();
file.close();
loaded = true;
Serial.printf("[%lu] [TXT] Loaded TXT file: %s (%zu bytes)\n", millis(), filepath.c_str(), fileSize);
return true;
}
std::string Txt::getTitle() const {
// Extract filename without path and extension
size_t lastSlash = filepath.find_last_of('/');
std::string filename = (lastSlash != std::string::npos) ? filepath.substr(lastSlash + 1) : filepath;
// Remove .txt extension
if (filename.length() >= 4 && filename.substr(filename.length() - 4) == ".txt") {
filename = filename.substr(0, filename.length() - 4);
}
return filename;
}
void Txt::setupCacheDir() const {
if (!SdMan.exists(cacheBasePath.c_str())) {
SdMan.mkdir(cacheBasePath.c_str());
}
if (!SdMan.exists(cachePath.c_str())) {
SdMan.mkdir(cachePath.c_str());
}
}
std::string Txt::findCoverImage() const {
// Get the folder containing the txt file
size_t lastSlash = filepath.find_last_of('/');
std::string folder = (lastSlash != std::string::npos) ? filepath.substr(0, lastSlash) : "";
if (folder.empty()) {
folder = "/";
}
// Get the base filename without extension (e.g., "mybook" from "/books/mybook.txt")
std::string baseName = getTitle();
// Image extensions to try
const char* extensions[] = {".bmp", ".jpg", ".jpeg", ".png", ".BMP", ".JPG", ".JPEG", ".PNG"};
// First priority: look for image with same name as txt file (e.g., mybook.jpg)
for (const auto& ext : extensions) {
std::string coverPath = folder + "/" + baseName + ext;
if (SdMan.exists(coverPath.c_str())) {
Serial.printf("[%lu] [TXT] Found matching cover image: %s\n", millis(), coverPath.c_str());
return coverPath;
}
}
// Fallback: look for cover image files
const char* coverNames[] = {"cover", "Cover", "COVER"};
for (const auto& name : coverNames) {
for (const auto& ext : extensions) {
std::string coverPath = folder + "/" + std::string(name) + ext;
if (SdMan.exists(coverPath.c_str())) {
Serial.printf("[%lu] [TXT] Found fallback cover image: %s\n", millis(), coverPath.c_str());
return coverPath;
}
}
}
return "";
}
std::string Txt::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
bool Txt::generateCoverBmp() const {
// Already generated, return true
if (SdMan.exists(getCoverBmpPath().c_str())) {
return true;
}
std::string coverImagePath = findCoverImage();
if (coverImagePath.empty()) {
Serial.printf("[%lu] [TXT] No cover image found for TXT file\n", millis());
return false;
}
// Setup cache directory
setupCacheDir();
// Get file extension
const size_t len = coverImagePath.length();
const bool isJpg =
(len >= 4 && (coverImagePath.substr(len - 4) == ".jpg" || coverImagePath.substr(len - 4) == ".JPG")) ||
(len >= 5 && (coverImagePath.substr(len - 5) == ".jpeg" || coverImagePath.substr(len - 5) == ".JPEG"));
const bool isBmp = len >= 4 && (coverImagePath.substr(len - 4) == ".bmp" || coverImagePath.substr(len - 4) == ".BMP");
if (isBmp) {
// Copy BMP file to cache
Serial.printf("[%lu] [TXT] Copying BMP cover image to cache\n", millis());
FsFile src, dst;
if (!SdMan.openFileForRead("TXT", coverImagePath, src)) {
return false;
}
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), dst)) {
src.close();
return false;
}
uint8_t buffer[1024];
while (src.available()) {
size_t bytesRead = src.read(buffer, sizeof(buffer));
dst.write(buffer, bytesRead);
}
src.close();
dst.close();
Serial.printf("[%lu] [TXT] Copied BMP cover to cache\n", millis());
return true;
}
if (isJpg) {
// Convert JPG/JPEG to BMP (same approach as Epub)
Serial.printf("[%lu] [TXT] Generating BMP from JPG cover image\n", millis());
FsFile coverJpg, coverBmp;
if (!SdMan.openFileForRead("TXT", coverImagePath, coverJpg)) {
return false;
}
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) {
coverJpg.close();
return false;
}
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp);
coverJpg.close();
coverBmp.close();
if (!success) {
Serial.printf("[%lu] [TXT] Failed to generate BMP from JPG cover image\n", millis());
SdMan.remove(getCoverBmpPath().c_str());
} else {
Serial.printf("[%lu] [TXT] Generated BMP from JPG cover image\n", millis());
}
return success;
}
// PNG files are not supported (would need a PNG decoder)
Serial.printf("[%lu] [TXT] Cover image format not supported (only BMP/JPG/JPEG)\n", millis());
return false;
}
bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const {
if (!loaded) {
return false;
}
FsFile file;
if (!SdMan.openFileForRead("TXT", filepath, file)) {
return false;
}
if (!file.seek(offset)) {
file.close();
return false;
}
size_t bytesRead = file.read(buffer, length);
file.close();
return bytesRead > 0;
}

33
lib/Txt/Txt.h Normal file
View File

@ -0,0 +1,33 @@
#pragma once
#include <SDCardManager.h>
#include <memory>
#include <string>
class Txt {
std::string filepath;
std::string cacheBasePath;
std::string cachePath;
bool loaded = false;
size_t fileSize = 0;
public:
explicit Txt(std::string path, std::string cacheBasePath);
bool load();
[[nodiscard]] const std::string& getPath() const { return filepath; }
[[nodiscard]] const std::string& getCachePath() const { return cachePath; }
[[nodiscard]] std::string getTitle() const;
[[nodiscard]] size_t getFileSize() const { return fileSize; }
void setupCacheDir() const;
// Cover image support - looks for cover.bmp/jpg/jpeg/png in same folder as txt file
[[nodiscard]] std::string getCoverBmpPath() const;
[[nodiscard]] bool generateCoverBmp() const;
[[nodiscard]] std::string findCoverImage() const;
// Read content from file
[[nodiscard]] bool readContent(uint8_t* buffer, size_t offset, size_t length) const;
};

View File

@ -2,4 +2,6 @@
#include <cstdint>
#define REPLACEMENT_GLYPH 0xFFFD
uint32_t utf8NextCodepoint(const unsigned char** string);

View File

@ -203,7 +203,7 @@ bool Xtc::generateCoverBmp() const {
coverBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
// Color palette (2 colors for 1-bit)
// XTC uses inverted polarity: 0 = black, 1 = white
// XTC 1-bit polarity: 0 = black, 1 = white (standard BMP palette order)
// Color 0: Black (text/foreground in XTC)
uint8_t black[4] = {0x00, 0x00, 0x00, 0x00};
coverBmp.write(black, 4);
@ -293,6 +293,267 @@ bool Xtc::generateCoverBmp() const {
return true;
}
std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
bool Xtc::generateThumbBmp() const {
// Already generated
if (SdMan.exists(getThumbBmpPath().c_str())) {
return true;
}
if (!loaded || !parser) {
Serial.printf("[%lu] [XTC] Cannot generate thumb BMP, file not loaded\n", millis());
return false;
}
if (parser->getPageCount() == 0) {
Serial.printf("[%lu] [XTC] No pages in XTC file\n", millis());
return false;
}
// Setup cache directory
setupCacheDir();
// Get first page info for cover
xtc::PageInfo pageInfo;
if (!parser->getPageInfo(0, pageInfo)) {
Serial.printf("[%lu] [XTC] Failed to get first page info\n", millis());
return false;
}
// Get bit depth
const uint8_t bitDepth = parser->getBitDepth();
// Calculate target dimensions for thumbnail (fit within 240x400 Continue Reading card)
constexpr int THUMB_TARGET_WIDTH = 240;
constexpr int THUMB_TARGET_HEIGHT = 400;
// Calculate scale factor
float scaleX = static_cast<float>(THUMB_TARGET_WIDTH) / pageInfo.width;
float scaleY = static_cast<float>(THUMB_TARGET_HEIGHT) / pageInfo.height;
float scale = (scaleX < scaleY) ? scaleX : scaleY;
// Only scale down, never up
if (scale >= 1.0f) {
// Page is already small enough, just use cover.bmp
// Copy cover.bmp to thumb.bmp
if (generateCoverBmp()) {
FsFile src, dst;
if (SdMan.openFileForRead("XTC", getCoverBmpPath(), src)) {
if (SdMan.openFileForWrite("XTC", getThumbBmpPath(), dst)) {
uint8_t buffer[512];
while (src.available()) {
size_t bytesRead = src.read(buffer, sizeof(buffer));
dst.write(buffer, bytesRead);
}
dst.close();
}
src.close();
}
Serial.printf("[%lu] [XTC] Copied cover to thumb (no scaling needed)\n", millis());
return SdMan.exists(getThumbBmpPath().c_str());
}
return false;
}
uint16_t thumbWidth = static_cast<uint16_t>(pageInfo.width * scale);
uint16_t thumbHeight = static_cast<uint16_t>(pageInfo.height * scale);
Serial.printf("[%lu] [XTC] Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)\n", millis(), pageInfo.width,
pageInfo.height, thumbWidth, thumbHeight, scale);
// Allocate buffer for page data
size_t bitmapSize;
if (bitDepth == 2) {
bitmapSize = ((static_cast<size_t>(pageInfo.width) * pageInfo.height + 7) / 8) * 2;
} else {
bitmapSize = ((pageInfo.width + 7) / 8) * pageInfo.height;
}
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
if (!pageBuffer) {
Serial.printf("[%lu] [XTC] Failed to allocate page buffer (%lu bytes)\n", millis(), bitmapSize);
return false;
}
// Load first page (cover)
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
if (bytesRead == 0) {
Serial.printf("[%lu] [XTC] Failed to load cover page for thumb\n", millis());
free(pageBuffer);
return false;
}
// Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes)
FsFile thumbBmp;
if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(), thumbBmp)) {
Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis());
free(pageBuffer);
return false;
}
// Write 1-bit BMP header for fast home screen rendering
const uint32_t rowSize = (thumbWidth + 31) / 32 * 4; // 1 bit per pixel, aligned to 4 bytes
const uint32_t imageSize = rowSize * thumbHeight;
const uint32_t fileSize = 14 + 40 + 8 + imageSize; // 8 bytes for 2-color palette
// File header
thumbBmp.write('B');
thumbBmp.write('M');
thumbBmp.write(reinterpret_cast<const uint8_t*>(&fileSize), 4);
uint32_t reserved = 0;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&reserved), 4);
uint32_t dataOffset = 14 + 40 + 8; // 1-bit palette has 2 colors (8 bytes)
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dataOffset), 4);
// DIB header
uint32_t dibHeaderSize = 40;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
int32_t widthVal = thumbWidth;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&widthVal), 4);
int32_t heightVal = -static_cast<int32_t>(thumbHeight); // Negative for top-down
thumbBmp.write(reinterpret_cast<const uint8_t*>(&heightVal), 4);
uint16_t planes = 1;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
uint16_t bitsPerPixel = 1; // 1-bit for black and white
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bitsPerPixel), 2);
uint32_t compression = 0;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&compression), 4);
thumbBmp.write(reinterpret_cast<const uint8_t*>(&imageSize), 4);
int32_t ppmX = 2835;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmX), 4);
int32_t ppmY = 2835;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmY), 4);
uint32_t colorsUsed = 2;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsUsed), 4);
uint32_t colorsImportant = 2;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
// Color palette (2 colors for 1-bit: black and white)
uint8_t palette[8] = {
0x00, 0x00, 0x00, 0x00, // Color 0: Black
0xFF, 0xFF, 0xFF, 0x00 // Color 1: White
};
thumbBmp.write(palette, 8);
// Allocate row buffer for 1-bit output
uint8_t* rowBuffer = static_cast<uint8_t*>(malloc(rowSize));
if (!rowBuffer) {
free(pageBuffer);
thumbBmp.close();
return false;
}
// Fixed-point scale factor (16.16)
uint32_t scaleInv_fp = static_cast<uint32_t>(65536.0f / scale);
// Pre-calculate plane info for 2-bit mode
const size_t planeSize = (bitDepth == 2) ? ((static_cast<size_t>(pageInfo.width) * pageInfo.height + 7) / 8) : 0;
const uint8_t* plane1 = (bitDepth == 2) ? pageBuffer : nullptr;
const uint8_t* plane2 = (bitDepth == 2) ? pageBuffer + planeSize : nullptr;
const size_t colBytes = (bitDepth == 2) ? ((pageInfo.height + 7) / 8) : 0;
const size_t srcRowBytes = (bitDepth == 1) ? ((pageInfo.width + 7) / 8) : 0;
for (uint16_t dstY = 0; dstY < thumbHeight; dstY++) {
memset(rowBuffer, 0xFF, rowSize); // Start with all white (bit 1)
// Calculate source Y range with bounds checking
uint32_t srcYStart = (static_cast<uint32_t>(dstY) * scaleInv_fp) >> 16;
uint32_t srcYEnd = (static_cast<uint32_t>(dstY + 1) * scaleInv_fp) >> 16;
if (srcYStart >= pageInfo.height) srcYStart = pageInfo.height - 1;
if (srcYEnd > pageInfo.height) srcYEnd = pageInfo.height;
if (srcYEnd <= srcYStart) srcYEnd = srcYStart + 1;
if (srcYEnd > pageInfo.height) srcYEnd = pageInfo.height;
for (uint16_t dstX = 0; dstX < thumbWidth; dstX++) {
// Calculate source X range with bounds checking
uint32_t srcXStart = (static_cast<uint32_t>(dstX) * scaleInv_fp) >> 16;
uint32_t srcXEnd = (static_cast<uint32_t>(dstX + 1) * scaleInv_fp) >> 16;
if (srcXStart >= pageInfo.width) srcXStart = pageInfo.width - 1;
if (srcXEnd > pageInfo.width) srcXEnd = pageInfo.width;
if (srcXEnd <= srcXStart) srcXEnd = srcXStart + 1;
if (srcXEnd > pageInfo.width) srcXEnd = pageInfo.width;
// Area averaging: sum grayscale values (0-255 range)
uint32_t graySum = 0;
uint32_t totalCount = 0;
for (uint32_t srcY = srcYStart; srcY < srcYEnd && srcY < pageInfo.height; srcY++) {
for (uint32_t srcX = srcXStart; srcX < srcXEnd && srcX < pageInfo.width; srcX++) {
uint8_t grayValue = 255; // Default: white
if (bitDepth == 2) {
// XTH 2-bit mode: pixel value 0-3
// Bounds check for column index
if (srcX < pageInfo.width) {
const size_t colIndex = pageInfo.width - 1 - srcX;
const size_t byteInCol = srcY / 8;
const size_t bitInByte = 7 - (srcY % 8);
const size_t byteOffset = colIndex * colBytes + byteInCol;
// Bounds check for buffer access
if (byteOffset < planeSize) {
const uint8_t bit1 = (plane1[byteOffset] >> bitInByte) & 1;
const uint8_t bit2 = (plane2[byteOffset] >> bitInByte) & 1;
const uint8_t pixelValue = (bit1 << 1) | bit2;
// Convert 2-bit (0-3) to grayscale: 0=black, 3=white
// pixelValue: 0=white, 1=light gray, 2=dark gray, 3=black (XTC polarity)
grayValue = (3 - pixelValue) * 85; // 0->255, 1->170, 2->85, 3->0
}
}
} else {
// 1-bit mode
const size_t byteIdx = srcY * srcRowBytes + srcX / 8;
const size_t bitIdx = 7 - (srcX % 8);
// Bounds check for buffer access
if (byteIdx < bitmapSize) {
const uint8_t pixelBit = (pageBuffer[byteIdx] >> bitIdx) & 1;
// XTC 1-bit polarity: 0=black, 1=white (same as BMP palette)
grayValue = pixelBit ? 255 : 0;
}
}
graySum += grayValue;
totalCount++;
}
}
// Calculate average grayscale and quantize to 1-bit with noise dithering
uint8_t avgGray = (totalCount > 0) ? static_cast<uint8_t>(graySum / totalCount) : 255;
// Hash-based noise dithering for 1-bit output
uint32_t hash = static_cast<uint32_t>(dstX) * 374761393u + static_cast<uint32_t>(dstY) * 668265263u;
hash = (hash ^ (hash >> 13)) * 1274126177u;
const int threshold = static_cast<int>(hash >> 24); // 0-255
const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192
// Quantize to 1-bit: 0=black, 1=white
uint8_t oneBit = (avgGray >= adjustedThreshold) ? 1 : 0;
// Pack 1-bit value into row buffer (MSB first, 8 pixels per byte)
const size_t byteIndex = dstX / 8;
const size_t bitOffset = 7 - (dstX % 8);
// Bounds check for row buffer access
if (byteIndex < rowSize) {
if (oneBit) {
rowBuffer[byteIndex] |= (1 << bitOffset); // Set bit for white
} else {
rowBuffer[byteIndex] &= ~(1 << bitOffset); // Clear bit for black
}
}
}
// Write row (already padded to 4-byte boundary by rowSize)
thumbBmp.write(rowBuffer, rowSize);
}
free(rowBuffer);
thumbBmp.close();
free(pageBuffer);
Serial.printf("[%lu] [XTC] Generated thumb BMP (%dx%d): %s\n", millis(), thumbWidth, thumbHeight,
getThumbBmpPath().c_str());
return true;
}
uint32_t Xtc::getPageCount() const {
if (!loaded || !parser) {
return 0;

View File

@ -62,6 +62,9 @@ class Xtc {
// Cover image support (for sleep screen)
std::string getCoverBmpPath() const;
bool generateCoverBmp() const;
// Thumbnail support (for Continue Reading card)
std::string getThumbBmpPath() const;
bool generateThumbBmp() const;
// Page access
uint32_t getPageCount() const;

View File

@ -2,7 +2,7 @@
default_envs = default
[crosspoint]
version = 0.13.1
version = 0.14.0
[base]
platform = espressif32 @ 6.12.0
@ -45,8 +45,9 @@ lib_deps =
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
ArduinoJson @ 7.4.2
QRCode @ 0.0.1
bblanchon/ArduinoJson @ 7.4.2
ricmoo/QRCode @ 0.0.1
links2004/WebSockets @ 2.7.3
[env:default]
extends = base

View File

@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Embed hypher-generated `.bin` tries into constexpr headers."""
from __future__ import annotations
import argparse
import pathlib
def _format_bytes(blob: bytes, per_line: int = 16) -> str:
# Render the blob as a comma separated list of hex literals with consistent wrapping.
lines = []
for i in range(0, len(blob), per_line):
chunk = ', '.join(f"0x{b:02X}" for b in blob[i : i + per_line])
lines.append(f" {chunk},")
if not lines:
lines.append(" 0x00,")
return '\n'.join(lines)
def _symbol_from_output(path: pathlib.Path) -> str:
# Derive a stable C identifier from the destination header name (e.g., hyph-en.trie.h -> en).
name = path.name
if name.endswith('.trie.h'):
name = name[:-7]
if name.startswith('hyph-'):
name = name[5:]
name = name.replace('-', '_')
if name.endswith('.trie'):
name = name[:-5]
return name
def write_header(path: pathlib.Path, blob: bytes, symbol: str) -> None:
# Emit a constexpr header containing the raw bytes plus a SerializedHyphenationPatterns descriptor.
path.parent.mkdir(parents=True, exist_ok=True)
data_symbol = f"{symbol}_trie_data"
patterns_symbol = f"{symbol}_patterns"
bytes_literal = _format_bytes(blob)
content = f"""#pragma once
#include <cstddef>
#include <cstdint>
#include "../SerializedHyphenationTrie.h"
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
alignas(4) constexpr uint8_t {data_symbol}[] = {{
{bytes_literal}
}};
constexpr SerializedHyphenationPatterns {patterns_symbol} = {{
{data_symbol},
sizeof({data_symbol}),
}};
"""
path.write_text(content)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--input', dest='inputs', action='append', required=True,
help='Path to a hypher-generated .bin trie')
parser.add_argument('--output', dest='outputs', action='append', required=True,
help='Destination header path (hyph-*.trie.h)')
args = parser.parse_args()
if len(args.inputs) != len(args.outputs):
raise SystemExit('input/output counts must match')
for src, dst in zip(args.inputs, args.outputs):
# Process each input/output pair independently so mixed-language refreshes work in one invocation.
src_path = pathlib.Path(src)
blob = src_path.read_bytes()
out_path = pathlib.Path(dst)
symbol = _symbol_from_output(out_path)
write_header(out_path, blob, symbol)
print(f'wrote {dst} ({len(blob)} bytes payload)')
if __name__ == '__main__':
main()

View File

@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance;
namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 17;
constexpr uint8_t SETTINGS_COUNT = 20;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace
@ -47,6 +47,8 @@ bool CrossPointSettings::saveToFile() const {
serialization::writeString(outputFile, std::string(opdsServerUrl));
serialization::writePod(outputFile, textAntiAliasing);
serialization::writePod(outputFile, hideBatteryPercentage);
serialization::writePod(outputFile, longPressChapterSkip);
serialization::writePod(outputFile, hyphenationEnabled);
outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -109,10 +111,15 @@ bool CrossPointSettings::loadFromFile() {
strncpy(opdsServerUrl, urlStr.c_str(), sizeof(opdsServerUrl) - 1);
opdsServerUrl[sizeof(opdsServerUrl) - 1] = '\0';
}
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, textAntiAliasing);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, hideBatteryPercentage);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, longPressChapterSkip);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, hyphenationEnabled);
if (++settingsRead >= fileSettingsCount) break;
} while (false);
inputFile.close();

View File

@ -84,12 +84,16 @@ class CrossPointSettings {
uint8_t sleepTimeout = SLEEP_10_MIN;
// E-ink refresh frequency (default 15 pages)
uint8_t refreshFrequency = REFRESH_15;
uint8_t hyphenationEnabled = 0;
// Reader screen margin settings
uint8_t screenMargin = 5;
// OPDS browser settings
char opdsServerUrl[128] = "";
// Hide battery percentage
uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press chapter skip on side buttons
uint8_t longPressChapterSkip = 1;
~CrossPointSettings() = default;

View File

@ -5,7 +5,7 @@
#include <Serialization.h>
namespace {
constexpr uint8_t STATE_FILE_VERSION = 1;
constexpr uint8_t STATE_FILE_VERSION = 2;
constexpr char STATE_FILE[] = "/.crosspoint/state.bin";
} // namespace
@ -19,6 +19,7 @@ bool CrossPointState::saveToFile() const {
serialization::writePod(outputFile, STATE_FILE_VERSION);
serialization::writeString(outputFile, openEpubPath);
serialization::writePod(outputFile, lastSleepImage);
outputFile.close();
return true;
}
@ -31,13 +32,18 @@ bool CrossPointState::loadFromFile() {
uint8_t version;
serialization::readPod(inputFile, version);
if (version != STATE_FILE_VERSION) {
if (version > STATE_FILE_VERSION) {
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
inputFile.close();
return false;
}
serialization::readString(inputFile, openEpubPath);
if (version >= 2) {
serialization::readPod(inputFile, lastSleepImage);
} else {
lastSleepImage = 0;
}
inputFile.close();
return true;

View File

@ -8,6 +8,7 @@ class CrossPointState {
public:
std::string openEpubPath;
uint8_t lastSleepImage;
~CrossPointState() = default;
// Get singleton instance

View File

@ -3,6 +3,7 @@
#include <Epub.h>
#include <GfxRenderer.h>
#include <SDCardManager.h>
#include <Txt.h>
#include <Xtc.h>
#include "CrossPointSettings.h"
@ -80,7 +81,13 @@ void SleepActivity::renderCustomSleepScreen() const {
const auto numFiles = files.size();
if (numFiles > 0) {
// Generate a random number between 1 and numFiles
const auto randomFileIndex = random(numFiles);
auto randomFileIndex = random(numFiles);
// If we picked the same image as last time, reroll
while (numFiles > 1 && randomFileIndex == APP_STATE.lastSleepImage) {
randomFileIndex = random(numFiles);
}
APP_STATE.lastSleepImage = randomFileIndex;
APP_STATE.saveToFile();
const auto filename = "/sleep/" + files[randomFileIndex];
FsFile file;
if (SdMan.openFileForRead("SLP", filename, file)) {
@ -202,6 +209,7 @@ void SleepActivity::renderCoverSleepScreen() const {
std::string coverBmpPath;
bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP;
// Check if the current book is XTC, TXT, or EPUB
if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") ||
StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) {
// Handle XTC file
@ -217,6 +225,20 @@ void SleepActivity::renderCoverSleepScreen() const {
}
coverBmpPath = lastXtc.getCoverBmpPath();
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".txt")) {
// Handle TXT file - looks for cover image in the same folder
Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint");
if (!lastTxt.load()) {
Serial.println("[SLP] Failed to load last TXT");
return renderDefaultSleepScreen();
}
if (!lastTxt.generateCoverBmp()) {
Serial.println("[SLP] No cover image found for TXT file");
return renderDefaultSleepScreen();
}
coverBmpPath = lastTxt.getCoverBmpPath();
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) {
// Handle EPUB file
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");

View File

@ -1,8 +1,10 @@
#include "HomeActivity.h"
#include <Bitmap.h>
#include <Epub.h>
#include <GfxRenderer.h>
#include <SDCardManager.h>
#include <Xtc.h>
#include <cstring>
#include <vector>
@ -46,7 +48,7 @@ void HomeActivity::onEnter() {
lastBookTitle = lastBookTitle.substr(lastSlash + 1);
}
// If epub, try to load the metadata for title/author
// If epub, try to load the metadata for title/author and cover
if (StringUtils::checkFileExtension(lastBookTitle, ".epub")) {
Epub epub(APP_STATE.openEpubPath, "/.crosspoint");
epub.load(false);
@ -56,10 +58,31 @@ void HomeActivity::onEnter() {
if (!epub.getAuthor().empty()) {
lastBookAuthor = std::string(epub.getAuthor());
}
} else if (StringUtils::checkFileExtension(lastBookTitle, ".xtch")) {
lastBookTitle.resize(lastBookTitle.length() - 5);
} else if (StringUtils::checkFileExtension(lastBookTitle, ".xtc")) {
lastBookTitle.resize(lastBookTitle.length() - 4);
// Try to generate thumbnail image for Continue Reading card
if (epub.generateThumbBmp()) {
coverBmpPath = epub.getThumbBmpPath();
hasCoverImage = true;
}
} else if (StringUtils::checkFileExtension(lastBookTitle, ".xtch") ||
StringUtils::checkFileExtension(lastBookTitle, ".xtc")) {
// Handle XTC file
Xtc xtc(APP_STATE.openEpubPath, "/.crosspoint");
if (xtc.load()) {
if (!xtc.getTitle().empty()) {
lastBookTitle = std::string(xtc.getTitle());
}
// Try to generate thumbnail image for Continue Reading card
if (xtc.generateThumbBmp()) {
coverBmpPath = xtc.getThumbBmpPath();
hasCoverImage = true;
}
}
// Remove extension from title if we don't have metadata
if (StringUtils::checkFileExtension(lastBookTitle, ".xtch")) {
lastBookTitle.resize(lastBookTitle.length() - 5);
} else if (StringUtils::checkFileExtension(lastBookTitle, ".xtc")) {
lastBookTitle.resize(lastBookTitle.length() - 4);
}
}
}
@ -69,7 +92,7 @@ void HomeActivity::onEnter() {
updateRequired = true;
xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask",
4096, // Stack size
4096, // Stack size (increased for cover image rendering)
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
@ -87,6 +110,51 @@ void HomeActivity::onExit() {
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
// Free the stored cover buffer if any
freeCoverBuffer();
}
bool HomeActivity::storeCoverBuffer() {
uint8_t* frameBuffer = renderer.getFrameBuffer();
if (!frameBuffer) {
return false;
}
// Free any existing buffer first
freeCoverBuffer();
const size_t bufferSize = GfxRenderer::getBufferSize();
coverBuffer = static_cast<uint8_t*>(malloc(bufferSize));
if (!coverBuffer) {
return false;
}
memcpy(coverBuffer, frameBuffer, bufferSize);
return true;
}
bool HomeActivity::restoreCoverBuffer() {
if (!coverBuffer) {
return false;
}
uint8_t* frameBuffer = renderer.getFrameBuffer();
if (!frameBuffer) {
return false;
}
const size_t bufferSize = GfxRenderer::getBufferSize();
memcpy(frameBuffer, coverBuffer, bufferSize);
return true;
}
void HomeActivity::freeCoverBuffer() {
if (coverBuffer) {
free(coverBuffer);
coverBuffer = nullptr;
}
coverBufferStored = false;
}
void HomeActivity::loop() {
@ -138,8 +206,12 @@ void HomeActivity::displayTaskLoop() {
}
}
void HomeActivity::render() const {
renderer.clearScreen();
void HomeActivity::render() {
// If we have a stored cover buffer, restore it instead of clearing
const bool bufferRestored = coverBufferStored && restoreCoverBuffer();
if (!bufferRestored) {
renderer.clearScreen();
}
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
@ -154,38 +226,109 @@ void HomeActivity::render() const {
constexpr int bookY = 30;
const bool bookSelected = hasContinueReading && selectorIndex == 0;
// Bookmark dimensions (used in multiple places)
const int bookmarkWidth = bookWidth / 8;
const int bookmarkHeight = bookHeight / 5;
const int bookmarkX = bookX + bookWidth - bookmarkWidth - 10;
const int bookmarkY = bookY + 5;
// Draw book card regardless, fill with message based on `hasContinueReading`
{
if (bookSelected) {
renderer.fillRect(bookX, bookY, bookWidth, bookHeight);
} else {
renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
// Draw cover image as background if available (inside the box)
// Only load from SD on first render, then use stored buffer
if (hasContinueReading && hasCoverImage && !coverBmpPath.empty() && !coverRendered) {
// First time: load cover from SD and render
FsFile file;
if (SdMan.openFileForRead("HOME", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
// Calculate position to center image within the book card
int coverX, coverY;
if (bitmap.getWidth() > bookWidth || bitmap.getHeight() > bookHeight) {
const float imgRatio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
const float boxRatio = static_cast<float>(bookWidth) / static_cast<float>(bookHeight);
if (imgRatio > boxRatio) {
coverX = bookX;
coverY = bookY + (bookHeight - static_cast<int>(bookWidth / imgRatio)) / 2;
} else {
coverX = bookX + (bookWidth - static_cast<int>(bookHeight * imgRatio)) / 2;
coverY = bookY;
}
} else {
coverX = bookX + (bookWidth - bitmap.getWidth()) / 2;
coverY = bookY + (bookHeight - bitmap.getHeight()) / 2;
}
// Draw the cover image centered within the book card
renderer.drawBitmap(bitmap, coverX, coverY, bookWidth, bookHeight);
// Draw border around the card
renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
// No bookmark ribbon when cover is shown - it would just cover the art
// Store the buffer with cover image for fast navigation
coverBufferStored = storeCoverBuffer();
coverRendered = true;
// First render: if selected, draw selection indicators now
if (bookSelected) {
renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2);
renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4);
}
}
file.close();
}
} else if (!bufferRestored && !coverRendered) {
// No cover image: draw border or fill, plus bookmark as visual flair
if (bookSelected) {
renderer.fillRect(bookX, bookY, bookWidth, bookHeight);
} else {
renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
}
// Draw bookmark ribbon when no cover image (visual decoration)
if (hasContinueReading) {
const int notchDepth = bookmarkHeight / 3;
const int centerX = bookmarkX + bookmarkWidth / 2;
const int xPoints[5] = {
bookmarkX, // top-left
bookmarkX + bookmarkWidth, // top-right
bookmarkX + bookmarkWidth, // bottom-right
centerX, // center notch point
bookmarkX // bottom-left
};
const int yPoints[5] = {
bookmarkY, // top-left
bookmarkY, // top-right
bookmarkY + bookmarkHeight, // bottom-right
bookmarkY + bookmarkHeight - notchDepth, // center notch point
bookmarkY + bookmarkHeight // bottom-left
};
// Draw bookmark ribbon (inverted if selected)
renderer.fillPolygon(xPoints, yPoints, 5, !bookSelected);
}
}
// Bookmark icon in the top-right corner of the card
const int bookmarkWidth = bookWidth / 8;
const int bookmarkHeight = bookHeight / 5;
const int bookmarkX = bookX + bookWidth - bookmarkWidth - 8;
constexpr int bookmarkY = bookY + 1;
// Main bookmark body (solid)
renderer.fillRect(bookmarkX, bookmarkY, bookmarkWidth, bookmarkHeight, !bookSelected);
// Carve out an inverted triangle notch at the bottom center to create angled points
const int notchHeight = bookmarkHeight / 2; // depth of the notch
for (int i = 0; i < notchHeight; ++i) {
const int y = bookmarkY + bookmarkHeight - 1 - i;
const int xStart = bookmarkX + i;
const int width = bookmarkWidth - 2 * i;
if (width <= 0) {
break;
}
// Draw a horizontal strip in the opposite color to "cut" the notch
renderer.fillRect(xStart, y, width, 1, bookSelected);
// If buffer was restored, draw selection indicators if needed
if (bufferRestored && bookSelected && coverRendered) {
// Draw selection border (no bookmark inversion needed since cover has no bookmark)
renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2);
renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4);
} else if (!coverRendered && !bufferRestored) {
// Selection border already handled above in the no-cover case
}
}
if (hasContinueReading) {
// Invert text colors based on selection state:
// - With cover: selected = white text on black box, unselected = black text on white box
// - Without cover: selected = white text on black card, unselected = black text on white card
// Split into words (avoid stringstream to keep this light on the MCU)
std::vector<std::string> words;
words.reserve(8);
@ -218,18 +361,25 @@ void HomeActivity::render() const {
lines.back().append("...");
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
lines.back().resize(lines.back().size() - 5);
// Remove "..." first, then remove one UTF-8 char, then add "..." back
lines.back().resize(lines.back().size() - 3); // Remove "..."
StringUtils::utf8RemoveLastChar(lines.back());
lines.back().append("...");
}
break;
}
int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
while (wordWidth > maxLineWidth && i.size() > 5) {
// Word itself is too long, trim it
i.resize(i.size() - 5);
i.append("...");
wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
while (wordWidth > maxLineWidth && !i.empty()) {
// Word itself is too long, trim it (UTF-8 safe)
StringUtils::utf8RemoveLastChar(i);
// Check if we have room for ellipsis
std::string withEllipsis = i + "...";
wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str());
if (wordWidth <= maxLineWidth) {
i = withEllipsis;
break;
}
}
int newLineWidth = renderer.getTextWidth(UI_12_FONT_ID, currentLine.c_str());
@ -261,6 +411,43 @@ void HomeActivity::render() const {
// Vertically center the title block within the card
int titleYStart = bookY + (bookHeight - totalTextHeight) / 2;
// If cover image was rendered, draw box behind title and author
if (coverRendered) {
constexpr int boxPadding = 8;
// Calculate the max text width for the box
int maxTextWidth = 0;
for (const auto& line : lines) {
const int lineWidth = renderer.getTextWidth(UI_12_FONT_ID, line.c_str());
if (lineWidth > maxTextWidth) {
maxTextWidth = lineWidth;
}
}
if (!lastBookAuthor.empty()) {
std::string trimmedAuthor = lastBookAuthor;
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
StringUtils::utf8RemoveLastChar(trimmedAuthor);
}
if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) <
renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) {
trimmedAuthor.append("...");
}
const int authorWidth = renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str());
if (authorWidth > maxTextWidth) {
maxTextWidth = authorWidth;
}
}
const int boxWidth = maxTextWidth + boxPadding * 2;
const int boxHeight = totalTextHeight + boxPadding * 2;
const int boxX = (pageWidth - boxWidth) / 2;
const int boxY = titleYStart - boxPadding;
// Draw box (inverted when selected: black box instead of white)
renderer.fillRect(boxX, boxY, boxWidth, boxHeight, bookSelected);
// Draw border around the box (inverted when selected: white border instead of black)
renderer.drawRect(boxX, boxY, boxWidth, boxHeight, !bookSelected);
}
for (const auto& line : lines) {
renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), !bookSelected);
titleYStart += renderer.getLineHeight(UI_12_FONT_ID);
@ -269,16 +456,40 @@ void HomeActivity::render() const {
if (!lastBookAuthor.empty()) {
titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2;
std::string trimmedAuthor = lastBookAuthor;
// Trim author if too long
// Trim author if too long (UTF-8 safe)
bool wasTrimmed = false;
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
trimmedAuthor.resize(trimmedAuthor.size() - 5);
StringUtils::utf8RemoveLastChar(trimmedAuthor);
wasTrimmed = true;
}
if (wasTrimmed && !trimmedAuthor.empty()) {
// Make room for ellipsis
while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth &&
!trimmedAuthor.empty()) {
StringUtils::utf8RemoveLastChar(trimmedAuthor);
}
trimmedAuthor.append("...");
}
renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, trimmedAuthor.c_str(), !bookSelected);
}
renderer.drawCenteredText(UI_10_FONT_ID, bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2,
"Continue Reading", !bookSelected);
// "Continue Reading" label at the bottom
const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2;
if (coverRendered) {
// Draw box behind "Continue Reading" text (inverted when selected: black box instead of white)
const char* continueText = "Continue Reading";
const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText);
constexpr int continuePadding = 6;
const int continueBoxWidth = continueTextWidth + continuePadding * 2;
const int continueBoxHeight = renderer.getLineHeight(UI_10_FONT_ID) + continuePadding;
const int continueBoxX = (pageWidth - continueBoxWidth) / 2;
const int continueBoxY = continueY - continuePadding / 2;
renderer.fillRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, bookSelected);
renderer.drawRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, !bookSelected);
renderer.drawCenteredText(UI_10_FONT_ID, continueY, continueText, !bookSelected);
} else {
renderer.drawCenteredText(UI_10_FONT_ID, continueY, "Continue Reading", !bookSelected);
}
} else {
// No book to continue reading
const int y =

View File

@ -14,8 +14,13 @@ class HomeActivity final : public Activity {
bool updateRequired = false;
bool hasContinueReading = false;
bool hasOpdsUrl = false;
bool hasCoverImage = false;
bool coverRendered = false; // Track if cover has been rendered once
bool coverBufferStored = false; // Track if cover buffer is stored
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
std::string lastBookTitle;
std::string lastBookAuthor;
std::string coverBmpPath;
const std::function<void()> onContinueReading;
const std::function<void()> onReaderOpen;
const std::function<void()> onSettingsOpen;
@ -24,8 +29,11 @@ class HomeActivity final : public Activity {
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void render();
int getMenuItemCount() const;
bool storeCoverBuffer(); // Store frame buffer for cover image
bool restoreCoverBuffer(); // Restore frame buffer from stored cover
void freeCoverBuffer(); // Free the stored cover buffer
public:
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,

View File

@ -4,6 +4,7 @@
#include <ESPmDNS.h>
#include <GfxRenderer.h>
#include <WiFi.h>
#include <esp_task_wdt.h>
#include <qrcode.h>
#include <cstddef>
@ -83,9 +84,8 @@ void CrossPointWebServerActivity::onExit() {
dnsServer = nullptr;
}
// CRITICAL: Wait for LWIP stack to flush any pending packets
Serial.printf("[%lu] [WEBACT] Waiting 500ms for network stack to flush pending packets...\n", millis());
delay(500);
// Brief wait for LWIP stack to flush pending packets
delay(50);
// Disconnect WiFi gracefully
if (isApMode) {
@ -95,11 +95,11 @@ void CrossPointWebServerActivity::onExit() {
Serial.printf("[%lu] [WEBACT] Disconnecting WiFi (graceful)...\n", millis());
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
}
delay(100); // Allow disconnect frame to be sent
delay(30); // Allow disconnect frame to be sent
Serial.printf("[%lu] [WEBACT] Setting WiFi mode OFF...\n", millis());
WiFi.mode(WIFI_OFF);
delay(100); // Allow WiFi hardware to fully power down
delay(30); // Allow WiFi hardware to power down
Serial.printf("[%lu] [WEBACT] [MEM] Free heap after WiFi disconnect: %d bytes\n", millis(), ESP.getFreeHeap());
@ -283,8 +283,28 @@ void CrossPointWebServerActivity::loop() {
dnsServer->processNextRequest();
}
// Handle web server requests - call handleClient multiple times per loop
// to improve responsiveness and upload throughput
// STA mode: Monitor WiFi connection health
if (!isApMode && webServer && webServer->isRunning()) {
static unsigned long lastWifiCheck = 0;
if (millis() - lastWifiCheck > 2000) { // Check every 2 seconds
lastWifiCheck = millis();
const wl_status_t wifiStatus = WiFi.status();
if (wifiStatus != WL_CONNECTED) {
Serial.printf("[%lu] [WEBACT] WiFi disconnected! Status: %d\n", millis(), wifiStatus);
// Show error and exit gracefully
state = WebServerActivityState::SHUTTING_DOWN;
updateRequired = true;
return;
}
// Log weak signal warnings
const int rssi = WiFi.RSSI();
if (rssi < -75) {
Serial.printf("[%lu] [WEBACT] Warning: Weak WiFi signal: %d dBm\n", millis(), rssi);
}
}
}
// Handle web server requests - maximize throughput with watchdog safety
if (webServer && webServer->isRunning()) {
const unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime;
@ -294,17 +314,32 @@ void CrossPointWebServerActivity::loop() {
timeSinceLastHandleClient);
}
// Call handleClient multiple times to process pending requests faster
// This is critical for upload performance - HTTP file uploads send data
// in chunks and each handleClient() call processes incoming data
constexpr int HANDLE_CLIENT_ITERATIONS = 10;
for (int i = 0; i < HANDLE_CLIENT_ITERATIONS && webServer->isRunning(); i++) {
// Reset watchdog BEFORE processing - HTTP header parsing can be slow
esp_task_wdt_reset();
// Process HTTP requests in tight loop for maximum throughput
// More iterations = more data processed per main loop cycle
constexpr int MAX_ITERATIONS = 500;
for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) {
webServer->handleClient();
// Reset watchdog every 32 iterations
if ((i & 0x1F) == 0x1F) {
esp_task_wdt_reset();
}
// Yield and check for exit button every 64 iterations
if ((i & 0x3F) == 0x3F) {
yield();
// Check for exit button inside loop for responsiveness
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onGoBack();
return;
}
}
}
lastHandleClientTime = millis();
}
// Handle exit on Back button
// Handle exit on Back button (also check outside loop)
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onGoBack();
return;

View File

@ -37,6 +37,14 @@ void WifiSelectionActivity::onEnter() {
savePromptSelection = 0;
forgetPromptSelection = 0;
// Cache MAC address for display
uint8_t mac[6];
WiFi.macAddress(mac);
char macStr[32];
snprintf(macStr, sizeof(macStr), "MAC address: %02x-%02x-%02x-%02x-%02x-%02x", mac[0], mac[1], mac[2], mac[3], mac[4],
mac[5]);
cachedMacAddress = std::string(macStr);
// Trigger first update to show scanning message
updateRequired = true;
@ -572,6 +580,9 @@ void WifiSelectionActivity::renderNetworkList() const {
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr);
}
// Show MAC address above the network count and legend
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str());
// Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved");
const auto labels = mappedInput.mapLabels("« Back", "Connect", "", "");

View File

@ -62,6 +62,9 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
// Password to potentially save (from keyboard or saved credentials)
std::string enteredPassword;
// Cached MAC address string for display
std::string cachedMacAddress;
// Whether network was connected using a saved password (skip save prompt)
bool usedSavedPassword = false;

View File

@ -118,9 +118,11 @@ void EpubReaderActivity::loop() {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Don't start activity transition while rendering
xSemaphoreTake(renderingMutex, portMAX_DELAY);
const int currentPage = section ? section->currentPage : 0;
const int totalPages = section ? section->pageCount : 0;
exitActivity();
enterNewActivity(new EpubReaderChapterSelectionActivity(
this->renderer, this->mappedInput, epub, currentSpineIndex,
this->renderer, this->mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages,
[this] {
exitActivity();
updateRequired = true;
@ -133,6 +135,16 @@ void EpubReaderActivity::loop() {
}
exitActivity();
updateRequired = true;
},
[this](const int newSpineIndex, const int newPage) {
// Handle sync position
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
currentSpineIndex = newSpineIndex;
nextPageNumber = newPage;
section.reset();
}
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
}
@ -168,7 +180,7 @@ void EpubReaderActivity::loop() {
return;
}
const bool skipChapter = mappedInput.getHeldTime() > skipChapterMs;
const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs;
if (skipChapter) {
// We don't want to delete the section mid-render, so grab the semaphore
@ -268,7 +280,7 @@ void EpubReaderActivity::renderScreen() {
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight)) {
viewportHeight, SETTINGS.hyphenationEnabled)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
// Progress bar dimensions
@ -313,7 +325,7 @@ void EpubReaderActivity::renderScreen() {
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, progressSetup, progressCallback)) {
viewportHeight, SETTINGS.hyphenationEnabled, progressSetup, progressCallback)) {
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
section.reset();
return;
@ -430,11 +442,13 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
if (showProgress) {
// Calculate progress in book
const float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount;
const uint8_t bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg);
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
// Right aligned text for progress counter
const std::string progress = std::to_string(section->currentPage + 1) + "/" + std::to_string(section->pageCount) +
" " + std::to_string(bookProgress) + "%";
char progressStr[32];
snprintf(progressStr, sizeof(progressStr), "%d/%d %.1f%%", section->currentPage + 1, section->pageCount,
bookProgress);
const std::string progress = progressStr;
progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str());
renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY,
progress.c_str());

View File

@ -2,6 +2,8 @@
#include <GfxRenderer.h>
#include "KOReaderCredentialStore.h"
#include "KOReaderSyncActivity.h"
#include "MappedInputManager.h"
#include "fontIds.h"
@ -10,6 +12,26 @@ namespace {
constexpr int SKIP_PAGE_MS = 700;
} // namespace
bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
int EpubReaderChapterSelectionActivity::getTotalItems() const {
// Add 2 for sync options (top and bottom) if credentials are configured
const int syncCount = hasSyncOption() ? 2 : 0;
return epub->getTocItemsCount() + syncCount;
}
bool EpubReaderChapterSelectionActivity::isSyncItem(int index) const {
if (!hasSyncOption()) return false;
// First item and last item are sync options
return index == 0 || index == getTotalItems() - 1;
}
int EpubReaderChapterSelectionActivity::tocIndexFromItemIndex(int itemIndex) const {
// Account for the sync option at the top
const int offset = hasSyncOption() ? 1 : 0;
return itemIndex - offset;
}
int EpubReaderChapterSelectionActivity::getPageItems() const {
// Layout constants used in renderScreen
constexpr int startY = 60;
@ -34,17 +56,21 @@ void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
}
void EpubReaderChapterSelectionActivity::onEnter() {
Activity::onEnter();
ActivityWithSubactivity::onEnter();
if (!epub) {
return;
}
renderingMutex = xSemaphoreCreateMutex();
// Account for sync option offset when finding current TOC index
const int syncOffset = hasSyncOption() ? 1 : 0;
selectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
if (selectorIndex == -1) {
selectorIndex = 0;
}
selectorIndex += syncOffset; // Offset for top sync option
// Trigger first update
updateRequired = true;
@ -57,7 +83,7 @@ void EpubReaderChapterSelectionActivity::onEnter() {
}
void EpubReaderChapterSelectionActivity::onExit() {
Activity::onExit();
ActivityWithSubactivity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
@ -69,7 +95,30 @@ void EpubReaderChapterSelectionActivity::onExit() {
renderingMutex = nullptr;
}
void EpubReaderChapterSelectionActivity::launchSyncActivity() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new KOReaderSyncActivity(
renderer, mappedInput, epub, epubPath, currentSpineIndex, currentPage, totalPagesInSpine,
[this]() {
// On cancel
exitActivity();
updateRequired = true;
},
[this](int newSpineIndex, int newPage) {
// On sync complete
exitActivity();
onSyncPosition(newSpineIndex, newPage);
}));
xSemaphoreGive(renderingMutex);
}
void EpubReaderChapterSelectionActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
mappedInput.wasReleased(MappedInputManager::Button::Left);
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
@ -77,9 +126,18 @@ void EpubReaderChapterSelectionActivity::loop() {
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
const int pageItems = getPageItems();
const int totalItems = getTotalItems();
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const auto newSpineIndex = epub->getSpineIndexForTocIndex(selectorIndex);
// Check if sync option is selected (first or last item)
if (isSyncItem(selectorIndex)) {
launchSyncActivity();
return;
}
// Get TOC index (account for top sync offset)
const int tocIndex = tocIndexFromItemIndex(selectorIndex);
const auto newSpineIndex = epub->getSpineIndexForTocIndex(tocIndex);
if (newSpineIndex == -1) {
onGoBack();
} else {
@ -89,17 +147,16 @@ void EpubReaderChapterSelectionActivity::loop() {
onGoBack();
} else if (prevReleased) {
if (skipPage) {
selectorIndex =
((selectorIndex / pageItems - 1) * pageItems + epub->getTocItemsCount()) % epub->getTocItemsCount();
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + totalItems) % totalItems;
} else {
selectorIndex = (selectorIndex + epub->getTocItemsCount() - 1) % epub->getTocItemsCount();
selectorIndex = (selectorIndex + totalItems - 1) % totalItems;
}
updateRequired = true;
} else if (nextReleased) {
if (skipPage) {
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % epub->getTocItemsCount();
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % totalItems;
} else {
selectorIndex = (selectorIndex + 1) % epub->getTocItemsCount();
selectorIndex = (selectorIndex + 1) % totalItems;
}
updateRequired = true;
}
@ -107,7 +164,7 @@ void EpubReaderChapterSelectionActivity::loop() {
void EpubReaderChapterSelectionActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
@ -122,6 +179,7 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
const auto pageWidth = renderer.getScreenWidth();
const int pageItems = getPageItems();
const int totalItems = getTotalItems();
const std::string title =
renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40, EpdFontFamily::BOLD);
@ -129,11 +187,24 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30);
for (int tocIndex = pageStartIndex; tocIndex < epub->getTocItemsCount() && tocIndex < pageStartIndex + pageItems;
tocIndex++) {
auto item = epub->getTocItem(tocIndex);
renderer.drawText(UI_10_FONT_ID, 20 + (item.level - 1) * 15, 60 + (tocIndex % pageItems) * 30, item.title.c_str(),
tocIndex != selectorIndex);
for (int itemIndex = pageStartIndex; itemIndex < totalItems && itemIndex < pageStartIndex + pageItems; itemIndex++) {
const int displayY = 60 + (itemIndex % pageItems) * 30;
const bool isSelected = (itemIndex == selectorIndex);
if (isSyncItem(itemIndex)) {
// Draw sync option (at top or bottom)
renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected);
} else {
// Draw TOC item (account for top sync offset)
const int tocIndex = tocIndexFromItemIndex(itemIndex);
auto item = epub->getTocItem(tocIndex);
const int indentSize = 20 + (item.level - 1) * 15;
const std::string chapterName =
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - indentSize);
renderer.drawText(UI_10_FONT_ID, indentSize, 60 + (tocIndex % pageItems) * 30, chapterName.c_str(),
tocIndex != selectorIndex);
}
}
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");

Some files were not shown because too many files have changed in this diff Show More